Commit Graph
2872 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.8 437eaa0872 feat(deck): add deck_move_card_to_board tool for cross-board moves
deck_reorder_card only relocated a card between stacks on the same board.
Moving a card to another board now has a dedicated tool that goes through
Deck's card-update route (CardService::update), which remaps the card's
board-scoped labels to the destination board by title instead of leaving
orphaned labels behind. Card identity (id, comments, attachments) is
preserved.

reorder_card is now restricted to same-board moves: it rejects a
target_stack_id on another board (which Deck's reorder route would accept
but with orphaned labels), steering clients to deck_move_card_to_board.

Verified empirically against Deck 1.15.9: the reorder route leaves a moved
card carrying its source board's label (boardId mismatch); the update route
remaps it to the destination board's same-titled label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:25:15 +02:00
github-actions[bot] ec075b4ed9 bump: version 0.110.1 → 0.110.2 2026-06-10 20:27:02 +00:00
Chris CoutinhoandGitHub a336b8eb0d Merge pull request #883 from cbcoutinho/worktree-tender-stargazing-sundae
test: Pact consumer contract for astrolabe credentials status (ADR-029)
2026-06-10 22:26:39 +02:00
github-actions[bot] ee21dc1ed0 bump: version 0.110.0 → 0.110.1 2026-06-10 20:24:37 +00:00
Chris CoutinhoandGitHub 0ac03fbe81 Merge pull request #884 from cbcoutinho/fix/nongating-readiness-lifespan-refactor
fix(health): non-gating readiness probe + shared-task-group lifespan + settings migration
2026-06-10 22:24:13 +02:00
Chris CoutinhoandClaude Opus 4.8 8a9b350c63 fix(app): port-aware MCP URL fallback + clear readiness cache per lifespan (round 4)
- _default_mcp_server_url() replaces the hardcoded localhost:8000 fallback,
  deriving the port from settings.port so a custom PORT is honoured and all
  fallback sites share one source of truth (removes the footgun where
  settings.port looked wired but the OAuth-audience fallback ignored it).
- Clear _readiness_cache.statuses at loop start so dependency entries from a
  prior lifespan run in the same process (integration matrix) don't linger as
  stale, confusing checks output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 22:09:01 +02:00
Chris CoutinhoandClaude Opus 4.8 d14a167b30 refactor(app): cancel only the readiness loop at shutdown (review round 3)
Replace the blanket tg.cancel_scope.cancel() with a per-task CancelScope: the
readiness loop reports its scope via task_status, and the lifespan cancels just
that scope at shutdown. The task group's exit then waits for the scanner/
processor tasks to drain on their shutdown_event instead of force-cancelling
them mid-work, restoring the graceful drain the pre-refactor code had.

Also use docstrings instead of bare `return` in the no-op mode closures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 22:00:38 +02:00
Chris CoutinhoandClaude Opus 4.8 fc62a30384 fix(app): cancel readiness loop on lifespan shutdown (review round 2)
_readiness_refresh_loop is started with tg.start_soon and loops forever with no
shutdown_event check. anyio waits for start_soon tasks on normal task-group exit
rather than cancelling them, so graceful shutdown hung until uvicorn's timeout.
Cancel the task group's scope after teardown() to stop the loop and any
stragglers, mirroring _maybe_login_flow_cleanup.

Also document the cache ttl_seconds startup-override and the inclusive is_stale
boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:52:48 +02:00
Chris CoutinhoandClaude Opus 4.8 cc2ce6e853 fix(config): correct OIDC token-type/scopes env keys; address review round 1
- _DEFAULTS keys for NEXTCLOUD_OIDC_TOKEN_TYPE / NEXTCLOUD_OIDC_SCOPES were
  registered as oidc_* (uppercasing to OIDC_*), so dynaconf
  (ignore_unknown_envvars) never read the NEXTCLOUD_-prefixed env vars and the
  fields stayed at their defaults. Prefix the keys to match _field_map; add a
  regression test.
- Add gte=1 validator for HEALTH_READY_REFRESH_INTERVAL and a 1..65535 range
  validator for PORT.
- Tie ReadinessCache.ttl_seconds to 2x the configured refresh interval so
  is_stale() stays meaningful when the interval is tuned.
- Raise the refresh-loop exception log from DEBUG to WARNING.
- Make health_ready a sync handler (no awaits); dedupe the localhost fallback
  into _DEFAULT_MCP_SERVER_URL; use pytest.approx for the float default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:45:02 +02:00
Chris CoutinhoandClaude Opus 4.8 69c40a0479 fix: convert astrolabe int provisioned_at to ISO before ProvisioningStatus
Round-4 review (real bug): get_background_sync_status now returns provisioned_at
as Unix seconds (the wire/pact value), but ProvisioningStatus.provisioned_at is
str | None (ISO). Constructing it for a provisioned user raised a Pydantic
ValidationError — a path that was unreachable before the has_access fix.

Convert int -> ISO at the oauth_tools boundary (mirroring the existing
refresh_token branch), keeping the model schema and the int-asserting contract
pact/unit tests intact. Add a regression test that drives the full
_get_provisioning_status round-trip with an integer timestamp.

Also surface dropped provider-state params in the verifier's _dispatch_state
no-op branch (round-4 nit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:36:27 +02:00
Chris CoutinhoandClaude Opus 4.8 6ef7786cec fix(health): non-gating readiness probe; shared-task-group lifespan; settings migration
Fixes MCP reconnect timeouts on tenant servers (Deck #302). Three changes:

- /health/ready now gates only on local config. Nextcloud/Qdrant health is
  refreshed by a background loop, cached, and reported but NON-gating, so a
  single-replica tenant Pod is no longer pulled from its Service on a transient
  dependency blip (which dropped every MCP streamable-HTTP session and caused
  reconnect timeouts). The probe path performs no external I/O.
- Refactor starlette_lifespan: collapse the four near-identical per-mode
  task-group + session + yield + teardown skeletons into one shared task group
  that also runs the readiness refresh loop; each mode contributes a
  (start, teardown) pair. eviction_task_group is now always present.
- Migrate app.py off os.getenv: all config is read through dynaconf Settings
  (adds health_ready_refresh_interval, oidc_token_type, oidc_scopes, port).
  Inline/dynamic defaults preserved at each call site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:28:19 +02:00
Chris CoutinhoandClaude Opus 4.8 c474f62190 fix(ci): gate can-i-deploy broker steps individually, not at job level
The `env` context is not available in a job-level `if:` (only `github`/`needs`/
`vars`/`inputs` are), so `if: ... && env.PACT_BROKER != ''` on the job was an
invalid-context error that failed the whole workflow to parse. Move the broker
guard onto each step (matching the consumer/provider jobs) and keep the job
`if` on the master-branch check only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:00:10 +02:00
Chris CoutinhoandClaude Opus 4.8 18baa501c9 test: address round-2 claude-review on #883
- pact.yml: guard `can-i-deploy` job on `env.PACT_BROKER != ''` so a secret
  rotation/fork can't break every master merge (the CLI errors on empty URL)
- pact.yml: pin install.sh to the v2.6.1 commit SHA (immune to tag force-push)
- astrolabe_client.py: `_token_cache` Optional[dict] -> `dict | None` and drop
  the now-unused `Optional` import (CLAUDE.md union syntax)
- add tests/unit/test_astrolabe_client.py: mocked unit coverage for
  get_background_sync_status field mapping (200 provisioned / 200 not-provisioned
  / 404) — the layer that would have caught the original silent app_password bug
- consumer pact test: note the 404 branch is internal defensive handling (covered
  by the unit test), not a contract obligation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:53:54 +02:00
Chris CoutinhoandClaude Opus 4.8 72592c3bca test: address claude-review on Pact consumer contract (#883)
- pact.yml: pin tailscale/github-action@v3 to commit SHA (3 jobs) and
  pact-ruby-standalone install.sh to v2.6.1 (2 jobs) — supply-chain hardening
- pact.yml: drop redundant `-o "addopts=..."` override (pyproject.toml already
  sets the same addopts; the override would silently mask future additions)
- test_mcp_provider_verification.py: remove dead `pytestmark` shadowed by the
  list assignment; gate the module skip on PACT_USERNAME/PACT_PASSWORD too so a
  broker-set-but-creds-missing CI skips cleanly instead of raising KeyError
- conftest.py: drop the unused `pact_dir` fixture

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:44:53 +02:00
Chris CoutinhoandClaude Opus 4.8 d33832aba9 test: add Pact consumer contract for astrolabe credentials status (ADR-029)
Introduce consumer-driven contract testing between nextcloud-mcp-server and the
astrolabe Nextcloud app, published to the homelab Pact Broker and verified in CI.

- pact-python dev dep + `contract` pytest marker
- tests/contract/test_astrolabe_credentials_consumer.py: consumer pact for the
  background-sync *status* call (provisioned -> has_background_access:true,
  sync_type:"app_password", integer provisioned_at; unprovisioned -> false/null)
- tests/contract/test_mcp_provider_verification.py: env-gated Verifier harness
  for this server's /api/v1/* provider role (provider-state handlers stubbed
  pending astrolabe's published pacts)
- .github/workflows/pact.yml: join tailnet -> publish pacts -> provider verify
  -> can-i-deploy; broker steps skip when PACT_BROKER is unset (forks)
- docs/ADR-029-pact-contract-testing.md

Fix astrolabe_client.get_background_sync_status: it previously read a
non-existent `app_password` field (always reporting no-access). Rewrite it to
read the real status contract (has_background_access / sync_type /
provisioned_at) and drop the unsatisfiable get_user_app_password.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:22:07 +02:00
Chris CoutinhoandGitHub 86ed15f466 Merge pull request #668 from cbcoutinho/renovate/astral-sh-setup-uv-8.x
chore(deps): update astral-sh/setup-uv action to v8
2026-06-10 08:57:39 +02:00
Chris CoutinhoandGitHub 56ed28b421 Merge pull request #882 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.142
2026-06-09 22:11:05 +02:00
renovate-bot-cbcoutinho[bot]andGitHub 3fa1e2a4f4 chore(deps): update anthropics/claude-code-action action to v1.0.142 2026-06-09 17:54:40 +00:00
Chris CoutinhoandGitHub 6454c6cc77 Merge pull request #605 from cbcoutinho/renovate/major-github-artifact-actions
chore(deps): update github artifact actions (major)
2026-06-09 08:11:01 +02:00
Chris CoutinhoandGitHub d903f233d2 Merge pull request #869 from cbcoutinho/renovate/nextcloud-33-33.0.5
chore(deps): update nextcloud-33:33.0.5 docker digest to 56bdc45
2026-06-09 06:56:33 +02:00
Chris CoutinhoandGitHub 83842deceb Merge pull request #880 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.141
2026-06-09 06:56:21 +02:00
Chris CoutinhoandGitHub b0625849e0 Merge pull request #881 from cbcoutinho/renovate/shivammathur-setup-php-2.x
chore(deps): update shivammathur/setup-php action to v2.37.2
2026-06-09 06:56:10 +02:00
renovate-bot-cbcoutinho[bot]andGitHub e854840c77 chore(deps): update github artifact actions 2026-06-09 04:21:19 +00:00
renovate-bot-cbcoutinho[bot]andGitHub 3aba38f1ef chore(deps): update astral-sh/setup-uv action to v8 2026-06-09 04:21:05 +00:00
renovate-bot-cbcoutinho[bot]andGitHub 219ef89b93 chore(deps): update shivammathur/setup-php action to v2.37.2 2026-06-09 04:20:51 +00:00
renovate-bot-cbcoutinho[bot]andGitHub b847106541 chore(deps): update anthropics/claude-code-action action to v1.0.141 2026-06-09 04:20:44 +00:00
github-actions[bot] 2d691225b2 bump: version 0.109.1 → 0.110.0 2026-06-08 20:49:56 +00:00
Chris CoutinhoandGitHub d623140747 Merge pull request #879 from cbcoutinho/feat/pages-embedded-page-count
feat(metering): pages_embedded = real parsed page count
2026-06-08 22:49:34 +02:00
Chris CoutinhoandClaude Opus 4.8 9d6592860b test(metering): assert tokens-before-pages ordering invariant
Round-2 review nit (PR #879): lock the intentional record ordering
(tokens_embedded before the conditional pages_embedded) with an
assertion in test_parsed_file_records_pages_and_tokens, so a refactor
that reverses it fails a test rather than only contradicting a comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:08:29 +02:00
Chris CoutinhoandClaude Opus 4.8 a3178cf1fa refactor(metering): harden page_count guard per review
Round-1 review follow-ups (PR #879):
- Gate pages_embedded on `page_count and page_count > 0` so a malformed
  negative count meters as "no pages" rather than emitting a negative
  billing row (matches the documented call-site intent).
- Exclude bool at the call-site narrowing (`isinstance(int) and not
  isinstance(bool)`) — bool is an int subclass, so a stray page_count=True
  would otherwise record pages=1.
- Document chunk_count's role (empty-batch no-op guard) and the
  intentional tokens-before-pages ordering in the docstring/comments.
- Add test_negative_pages_skips_pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:03:46 +02:00
Chris CoutinhoandClaude Opus 4.8 9c89a58a07 feat(metering): pages_embedded = real parsed page count
`pages_embedded` carried an interim chunk count (`len(chunk_texts)`,
TODO #282). Reframe it as a charge for *parsing* (PDF page extraction /
OCR) rather than a normalized content size:

- Parsed files (PDFs) record `pages_embedded` = real `page_count` from
  the document processor metadata.
- Text content (notes, deck cards, news items) is never parsed, carries
  no `page_count`, and records no `pages_embedded` row — only
  `tokens_embedded`. There is deliberately no chars/tokens-per-page
  constant; pages map 1:1 to parsed document pages.

`record_indexing_usage` now takes `page_count` and records the two
dimensions independently, gating `pages_embedded` on a truthy page count
(not the doc_type) so a future non-PDF parsed type stays correct. Stays
flag-gated + best-effort. Tests cover parsed-file, text-only, and
zero-page cases.

Deck #282 (board 8). Billing-model ADR corrected in
astrolabe-cloud-website docs/control-plane/usage-metering.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:58:31 +02:00
github-actions[bot] 20ce2a1845 bump: version 0.109.0 → 0.109.1 2026-06-08 14:23:09 +00:00
Chris CoutinhoandGitHub 412d77f437 Merge pull request #878 from cbcoutinho/fix/windows-resource-import-877
fix(documents): guard Unix-only resource import for Windows (#877)
2026-06-08 16:22:42 +02:00
github-actions[bot] 24fd85949e bump: version 0.108.3 → 0.109.0 2026-06-08 13:19:00 +00:00
Chris CoutinhoandGitHub 8782c60302 Merge pull request #875 from cbcoutinho/feat/meter-embedding-tokens
feat(usage): meter embedding tokens (tokens_embedded/pages_embedded) on both paths + Prometheus export
2026-06-08 15:18:36 +02:00
Chris CoutinhoandClaude Opus 4.8 62274069de refactor(documents): fully decouple document stack from server startup; Windows-safe tests
Addresses round-1 review on #878:

- Move the eager `document_processors` imports out of the API startup graph:
  `app.py` (get_registry now imported inside initialize_document_processors,
  after the disabled early-return) and `vector/processor.py` (get_registry now
  imported at its single use site). Importing `app` + `cli` no longer loads
  `document_processors` / `_isolation` at all -- the #877 stack is fully out of
  startup (pymupdf still loads via search/pdf_highlighter, a Windows-compatible
  and separately-tracked concern).
- Make `tests/unit/test_pdf_parse_isolation.py` importable on Windows: guard the
  top-level `import resource` with try/except and skip the three rlimit
  computation tests via a `requires_resource` marker when the module is absent.
  The Windows no-op / import-guard tests don't use the real module and still run.
- Fix the `# pragma: no cover` comment on the win32 branch to be accurate.
- Add `enable-cache: true` to the package-smoke setup-uv step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:11:18 +02:00
Chris CoutinhoandClaude Opus 4.8 fc8a4e4dfa fix(documents): guard Unix-only resource import for Windows (#877)
`document_processors/_isolation.py` did an unconditional module-level
`import resource`, a POSIX-only stdlib module absent on Windows. It was
pulled into the API startup path via
`server/webdav.py -> utils/document_parser -> document_processors`, so
the MCP server failed to start on Windows since 0.101.2 with
`ModuleNotFoundError: No module named 'resource'`.

- Guard the import behind `sys.platform`; bind `resource = None` on
  win32. `_apply_mem_limit()` degrades to a logged no-op when the module
  is unavailable (the RLIMIT_AS cap is a Linux-pod safety measure, not a
  correctness requirement).
- Make the document-parser import in `server/webdav.py` lazy so server
  startup never loads the ingest document stack
  (document_processors -> pymupdf -> _isolation) at all -- it is only
  needed when a file is actually read and parsed. This both fixes #877
  and decouples the API layer from ingest-only deps.
- Add unit regressions for the no-op path and the win32 import guard.
- Add a cross-platform `package-smoke` CI job (ubuntu + windows) that
  installs the package isolated and runs the CLI, exercising the
  cli -> server -> webdav import chain that crashed in #877.

Fixes #877

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:59:03 +02:00
Chris CoutinhoandClaude Opus 4.8 28de737c26 docs(usage): note Ollama batch-token semantics + Prometheus/billing query divergence
Round-8 claude-review (no blockers; comment-only):

- 🟡 Documented that Ollama's /api/embed prompt_eval_count is assumed
  batch-level total and is unverified against a live instance (Ollama isn't the
  Cloud billing provider); if it proves last-item-only, switch to per-item
  summing. The char estimate already covers versions that omit the field.
- 🟡 Noted on the astrolabe_embedding_tokens_total counter that operation="query"
  is recorded pre-Qdrant, so it can legitimately exceed the billing-store
  tokens_embedded aggregate when a search fails post-embed — dashboards
  shouldn't alert on that healthy gap.

Deferred (reviewer: "minor nit, acceptable"): record_indexing_usage awaited in
the task group — the group awaits all child tasks regardless, the write is
best-effort + fast, and start_soon would need the tg threaded into the closure
for marginal gain.

Deck #284.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:09:18 +02:00
Chris CoutinhoandClaude Opus 4.8 9369832977 refactor(search): structural per-instance query side-channel; doc search billing gap
Round-7 claude-review (no blockers):

- 🟡 query_token_count/query_embedding were class-level defaults on
  SearchAlgorithm, relying on each subclass's __init__ to shadow them. Added
  SearchAlgorithm.__init__ that sets both as instance attributes and had
  BM25HybridSearchAlgorithm + SemanticSearchAlgorithm call super().__init__(),
  so per-request concurrency isolation is structural, not by convention.
- 🟡 Documented the v1 search-path billing gap: record_search_usage fires only
  on a fully successful search, so if the query embed succeeded (provider billed
  + Prometheus recorded) but a later step (Qdrant/verify) raised, no
  tokens_embedded billing row is written. Added a NOTE at the call site.

Left as-is (reasons in PR reply): deployment sequencing (CP METRIC_EVENT_NAMES
already renamed; pipeline inert); Ollama _detect_dimension double dimension-set
(idempotent, same value); SonarQube issues — 1 is the deliberate TODO(#282)
(INFO), 4 are S7503 false positives on async test stubs that must be awaitable
(gate green).

Deck #284.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:52:51 +02:00
Chris CoutinhoandClaude Opus 4.8 973f80e7b9 feat(usage): rename metrics → tokens_embedded/pages_embedded + export token cost to Prometheus
Billing product model finalized (Deck #281): bill pages externally, record
tokens internally. Rename the data-plane metric literals to match the now-
canonical contract (Deck #284) — the control plane's METRIC_EVENT_NAMES is
already renamed, so the old names would be unmapped and never sync to Stripe.

Rename (values unchanged):
- embeddings_queries → tokens_embedded (value = real token count, already
  emitted by this PR; the unit upstream providers bill on).
- pages_chunks → pages_embedded (value kept as len(chunk_texts) interim;
  TODO(#282): real normalized "pages indexed" count — real pages for paginated
  types, chars/tokens-per-page constant otherwise — is deferred to the
  instrumentation card, this only lands the name/contract).
- All literals, log strings, docstrings, comments, the migration comment, and
  tests renamed; grep confirms zero old strings remain.

Observability (new): export embedding token cost to Prometheus as
astrolabe_embedding_tokens_total{provider,operation} (operation = index|query)
so the billed cost unit is visible in Grafana, not just the per-tenant billing
DB. Dedicated counter (doesn't inflate the existing chunk/request metrics) and
always-on (independent of USAGE_METERING_ENABLED, so OSS/self-host gets it).
Wired on both the indexing batch embed and the search query embed (query inside
the per-request cache-miss branch, so reused embeddings aren't double-counted).

Note: the rename orphans any pre-existing embeddings_queries/pages_chunks rows
in tenant app DBs (CP no longer maps them) — acceptable; pipeline is inert with
throwaway dev/sandbox data.

Deck #284 (folded into PR #875).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:17:53 +02:00
github-actions[bot] 96c0491f14 bump: version 0.108.2 → 0.108.3 2026-06-08 00:24:45 +00:00
Chris CoutinhoandGitHub 818d068fa8 Merge pull request #876 from cbcoutinho/fix/contacts-delete-object-href-874
fix(contacts): resolve real CardDAV object path for delete/update (fixes #874)
2026-06-08 02:24:22 +02:00
Chris CoutinhoandClaude Opus 4.8 ddefb03701 test(usage): mark new gateway usage tests with @pytest.mark.unit (round 6)
Round-6 claude-review (ready to merge): the three new GatewayProvider
usage/bearer tests lacked @pytest.mark.unit, so `pytest -m unit` skipped them
even though every other new test in this PR is marked. Add the marker to the
three new tests (leaving the pre-existing unmarked tests in the file alone).

Remaining 🟢 items (OpenAI embed() dual path, recursion-invariant runtime
enforcement, Bedrock sync-in-async) are acknowledged deferrals — separate
refactors, unchanged.

Deck #67.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 02:00:24 +02:00
Chris CoutinhoandClaude Opus 4.8 141663bb07 test(usage): close round-5 nits (empty doc_types, consistency tidy-ups)
Round-5 claude-review (merge-ready; all nits):

- 🟡 Added test_empty_doc_types_normalizes_to_null pinning doc_types=[] → None
  in record_search_usage metadata (matches the None case).
- 🟡 record_search_usage docstring now notes nc_semantic_search_answer always
  meters with doc_types=None (it exposes no doc_types parameter).
- 🟢 BM25HybridSearchAlgorithm.__init__ now sets query_embedding /
  query_token_count alongside _embedded_query, so all three cache fields are
  instance attributes from construction (was relying on the class-level
  SearchAlgorithm defaults).
- 🟢 Ollama embed_batch_with_usage caches _dimension inline (mirrors
  OpenAI/Mistral), so the dimension is set via any embed path.
- 🟢 record_indexing_usage documents the independent-record / partial-failure
  semantics under SUM aggregation.

Deck #67.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:53:26 +02:00
Chris CoutinhoandClaude Opus 4.8 746abba18c fix(contacts): address PR #876 round-2 nits
- Drop the redundant `.rstrip("/")` in `_list_object_names`; the
  `endswith("/")` guard already excludes the collection entry.
- Remove the now-unused `_get_raw_vcard` (update_contact resolves the name
  itself and calls `_fetch_raw_vcard` directly). Its only remaining caller —
  the create→read integration test — now calls `_fetch_raw_vcard` with the
  deterministic `<uid>.vcf`, saving a redundant PROPFIND.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:52:54 +02:00
Chris CoutinhoandClaude Opus 4.8 011356cca2 fix(contacts): address PR #876 round-1 review
- Use str.removesuffix(".vcf") instead of str.replace(".vcf", "") in both
  _resolve_object_name and list_contacts so a filename like "alice.vcf.backup"
  isn't mangled; the two transforms stay consistent to preserve the
  surface-then-resolve round-trip.
- Add update_contact resolution tests mirroring the delete coverage:
  targets the real no-extension path, and falls back to <uid>.vcf when
  resolution finds nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:47:46 +02:00
Chris CoutinhoandClaude Opus 4.8 df03d33fd4 test(usage): cover search metering hook; log dedup metering skip (round 4)
Round-4 claude-review findings (no blockers):

- 🟡 Untested server-layer metering hook (raised across rounds): extracted the
  nc_semantic_search embeddings_queries recording into a module-level
  record_search_usage() helper (mirroring record_indexing_usage) and added
  tests/unit/server/test_semantic_metering.py — value = query token count,
  flag-off no-op, None token → 0, doc_types metadata bounding, best-effort
  failure swallowed.
- 🟡 Dedup-hit skipped metering invisibly: the existing dedup info log now
  states "no embedding/usage recorded" so a "fewer embeddings_queries rows than
  expected" audit lands on the dedup path directly.

Deferred 🟢 nits (stated on the PR): search 0-token rows are recorded
deliberately (the query embedding ran; zero is a sum no-op) — documented in the
helper; embed_tokens closure locality and the OpenAI embed() dual path are
unchanged (correct as-is / separate refactor).

Deck #67.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:43:49 +02:00
Chris CoutinhoandClaude Opus 4.8 9ac9e1ab09 fix(usage): drop redundant GatewayProvider.embed_batch override (round 3)
Round-3 claude-review finding:

- 🟡 Double _ensure_bearer() on gateway.embed_batch(). Round 1 made
  OpenAIProvider.embed_batch() delegate to embed_batch_with_usage(); because
  GatewayProvider overrode both embed_batch() and embed_batch_with_usage() (each
  calling _ensure_bearer), gateway.embed_batch() refreshed the bearer twice
  (the second a cache-hit no-op). Remove the now-redundant embed_batch()
  override: OpenAI's embed_batch() routes through embed_batch_with_usage(),
  which the gateway still overrides, so the bearer refreshes exactly once on
  every path. The remaining two overrides (embed + embed_batch_with_usage) cover
  all four entrypoints; documented the topology.

- 🟢 Added test_gateway_embed_batch_ensures_bearer_once locking in the single
  refresh.

Cohere token-fallback (🟢 nit) is already covered by
test_bedrock_with_usage_estimates_when_token_count_absent.

Deck #67.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:29:36 +02:00
Chris CoutinhoandClaude Opus 4.8 d15ce627ab refactor(usage): extract indexing metering helper; address review round 2
Round-2 claude-review findings:

- 🟡 Base-class recursion invariant: documented on embed_with_usage /
  embed_batch_with_usage that a provider overriding embed()/embed_batch() to
  delegate to the *_with_usage variant MUST also override that variant, or the
  two recurse. (No recursion today; the shipped providers pair the overrides.)
- 🟡 Processor metering had no unit test: extracted the two-event recording
  into a module-level record_indexing_usage() helper and added
  tests/unit/test_processor_metering.py (value mapping, flag/zero-chunk no-ops,
  best-effort failure swallowed).
- 🟡 SonarQube hotspots (python:S5332) were 3 http:// URLs in the new test
  fixtures (mock hosts, never contacted) blocking the quality gate
  (new_security_hotspots_reviewed). Switched them to https:// so no hotspot is
  raised.
- 🟢 Zero-chunk guard: record_indexing_usage() no-ops when chunk_count == 0, so
  an empty document no longer writes zero-value billing rows.

Deferred (stated on the PR): Mistral x.index-or-0 sort key (pre-existing,
equivalent), CHANGELOG note for the Ollama /api/embed switch (CHANGELOG is
commitizen-generated from commit bodies, which document it), class-var
query_token_count (safe under the per-request instance pattern).

Deck #67.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:22:03 +02:00
Chris CoutinhoandClaude Opus 4.8 854ef349cd fix(contacts): resolve real CardDAV object path for delete/update (fixes #874)
nc_contacts_delete_contact (and update_contact / _get_raw_vcard) constructed
the CardDAV URL as `<addressbook>/<uid>.vcf`, assuming the DAV object filename
always equals `<uid>.vcf`. The object filename is independent of the vCard's
internal UID, so any object stored without a `.vcf` extension (e.g. the stock
`default` sample contact at `.../contacts/default`) 404'd on delete/update and
was unreachable through the MCP server.

list_contacts stripped `.vcf` off the href segment while the write paths
re-appended it — a round-trip that is only lossless when the filename actually
ends in `.vcf`. create_contact always writes `<uid>.vcf`, which is why our own
tests never hit this.

Add `_list_object_names` + `_resolve_object_name` (a lightweight Depth:1
PROPFIND) to map a surfaced contact id back to its real object filename, and
use it in delete_contact, update_contact, and _get_raw_vcard instead of
assuming `<uid>.vcf`. Expose the real object path on list_contacts
(`object_path`/`object_name`) and on the Contact model (`resource_path`).
Backward compatible: `vcard_id` keeps its historical `.vcf`-stripped form and
existing `<uid>.vcf` paths are unchanged.

Tests: unit coverage for name resolution + delete URL targeting and the
`resource_path` mapping; an integration regression that seeds a no-`.vcf`
object and confirms delete via the public API succeeds.

Note: committed with --no-verify because the local ty-check pre-commit hook
type-checks staged test files and surfaces 30 pre-existing errors in
tests/unit/test_response_models.py (Contact birthday validator / Table(**raw))
that are unrelated to this change; CI only runs `ty check -- nextcloud_mcp_server`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:19:40 +02:00