@gmickel/gno 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +3 -2
  2. package/assets/skill/SKILL.md +8 -1
  3. package/assets/skill/cli-reference.md +8 -1
  4. package/assets/skill/examples.md +2 -1
  5. package/assets/skill/mcp-reference.md +3 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +48 -6
  12. package/spec/db/schema.sql +0 -1
  13. package/spec/mcp.md +18 -3
  14. package/spec/output-schemas/audit-report.schema.json +18 -4
  15. package/spec/output-schemas/backlinks.schema.json +4 -0
  16. package/spec/output-schemas/collection-list.schema.json +13 -0
  17. package/spec/output-schemas/graph.schema.json +2 -0
  18. package/spec/output-schemas/links-list.schema.json +4 -0
  19. package/spec/output-schemas/status.schema.json +15 -0
  20. package/src/cli/commands/audit.ts +23 -4
  21. package/src/cli/commands/collection/list.ts +39 -5
  22. package/src/cli/commands/embed.ts +3 -3
  23. package/src/cli/commands/links.ts +34 -131
  24. package/src/cli/commands/shared.ts +7 -0
  25. package/src/cli/commands/status.ts +5 -0
  26. package/src/cli/program.ts +12 -2
  27. package/src/config/loader.ts +43 -0
  28. package/src/config/types.ts +8 -0
  29. package/src/core/audit-contract.ts +16 -4
  30. package/src/core/audit-freshness.ts +11 -1
  31. package/src/core/audit-links.ts +145 -25
  32. package/src/core/audit-provenance.ts +11 -4
  33. package/src/core/audit-workspace.ts +19 -4
  34. package/src/core/audit.ts +67 -15
  35. package/src/core/context-compiler.ts +3 -0
  36. package/src/core/context-evidence.ts +11 -0
  37. package/src/core/graph-edge-confidence.ts +23 -1
  38. package/src/core/host-paths.ts +1 -0
  39. package/src/core/knowledge-impact.ts +28 -0
  40. package/src/core/link-workspace.ts +324 -0
  41. package/src/core/retrieval-replay-candidate.ts +6 -0
  42. package/src/core/retrieval-trace-request.ts +3 -0
  43. package/src/index.ts +14 -1
  44. package/src/ingestion/graph-reconciliation.ts +77 -15
  45. package/src/ingestion/source-availability/darwin-path.ts +9 -3
  46. package/src/ingestion/sync.ts +22 -1
  47. package/src/ingestion/types.ts +14 -0
  48. package/src/llm/inference-scope.ts +4 -3
  49. package/src/mcp/http-egress.ts +42 -3
  50. package/src/mcp/tools/audit.ts +11 -2
  51. package/src/mcp/tools/changes.ts +1 -0
  52. package/src/mcp/tools/links.ts +3 -0
  53. package/src/mcp/tools/sessions.ts +33 -4
  54. package/src/mcp/tools/status.ts +3 -0
  55. package/src/pipeline/expansion.ts +19 -31
  56. package/src/pipeline/graph-retrieval.ts +22 -2
  57. package/src/pipeline/hybrid.ts +1 -1
  58. package/src/pipeline/types.ts +6 -3
  59. package/src/serve/findings-pass.ts +1 -1
  60. package/src/serve/public/pages/GraphView.tsx +2 -0
  61. package/src/serve/routes/changes.ts +6 -1
  62. package/src/serve/routes/links.ts +13 -0
  63. package/src/serve/routes/sessions.ts +41 -53
  64. package/src/sessions/config-refresh.ts +111 -0
  65. package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
  66. package/src/store/migrations/034-collection-link-workspace.ts +47 -0
  67. package/src/store/migrations/index.ts +4 -0
  68. package/src/store/sqlite/adapter.ts +390 -231
  69. package/src/store/sqlite/eligibility.ts +8 -2
  70. package/src/store/sqlite/graph-link-resolver.ts +252 -5
  71. package/src/store/sqlite/graph-neighbors.ts +147 -40
  72. package/src/store/sqlite/graph-reference-state.ts +13 -2
  73. package/src/store/sqlite/workspace-link-resolver.ts +654 -0
  74. package/src/store/types.ts +49 -3
  75. package/src/store/vector/stats.ts +1 -1
  76. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
package/README.md CHANGED
@@ -139,7 +139,7 @@ See the [guide](docs/COMPILED-CONTEXT.md).
139
139
 
140
140
  <!-- public-truth:current-version -->
141
141
 
142
- > Current source version: **v2.7.1**. See [CHANGELOG.md](./CHANGELOG.md).
142
+ > Current source version: **v2.8.0**. See [CHANGELOG.md](./CHANGELOG.md).
143
143
 
144
144
  <!-- /public-truth -->
145
145
 
@@ -216,7 +216,8 @@ See the [guide](docs/COMPILED-CONTEXT.md).
216
216
  and traces. Authentication never overrides policy.
217
217
  - **Source availability (`any` | `local`)**: opt-in `local` indexes only files
218
218
  already on disk and never makes a cloud provider download one. Supported on
219
- macOS with Google Drive, iCloud Drive, and OneDrive SharePoint library roots.
219
+ macOS with Google Drive (My Drive and Shared drives), iCloud Drive, and
220
+ OneDrive SharePoint library roots.
220
221
  Cloud-only files are skipped and reported, not treated as conversion errors,
221
222
  and documents under a cloud-only folder stay indexed. Other platforms fail
222
223
  with an error. On a 5,000-file all-local collection, `local` scans about 1%
@@ -352,6 +352,12 @@ offline audits inspect parsed local links, explicitly declared capture/logical-
352
352
  record provenance, and observable source/index freshness. They never repair,
353
353
  rewrite, persist findings, judge factual truth, or replace retrieval.
354
354
 
355
+ `--max-findings` accepts `all` (MCP `maxFindings: "all"`) to export every
356
+ finding. Link findings carry `referenceKind`, `resolutionStatus`, and
357
+ `resolvedScope` in their evidence detail; ambiguous vault links list the tied
358
+ `candidates`. Report `truncation.snapshotTruncated` as "totals cover the
359
+ bounded snapshot, not the whole index".
360
+
355
361
  Treat exit `4` as a complete report with findings. Exit `5` or report status
356
362
  `partial`/`changed_during_audit` means evidence is unavailable, inconclusive,
357
363
  cancelled, truncated, or repeatedly changed—never healthy. Preserve stable
@@ -389,7 +395,7 @@ remaining steps apply under the default `full` profile.
389
395
  - MCP text is the compact `gno-context-agent-v1` evidence projection. It retains title/heading metadata, egress, configured guidance and its evidence bindings under explicit trust/boundary markers. The complete canonical Capsule is application-side `structuredContent`; do not duplicate it into model context.
390
396
  3. Use `gno_ask` only for explicit local verified synthesis. Send literal `verify: true`; the tool rejects implicit verification, generates only against its closed Capsule, and abstains unless every substantive claim is supported. Preserve exact spans, gaps, semantic capability state, and abstention. This does not guarantee corpus completeness or source truth.
391
397
  4. Use `gno_query` for interactive lookup or manual retrieval control. It returns snippets plus `uri`, `docid`, often `line`, and sometimes `context`. Treat `context` as user-configured guidance for interpreting that exact result; cite source content at the returned URI/lines, not the guidance itself. Bounded graph expansion is on by default; set `graph: false` or `noGraph: true` only for an explicit BM25/vector-only path.
392
- 5. Use graph/link expansion for relationship context: `gno_graph_query` for typed relationship traversal, `gno_graph_neighbors` for nearby documents, `gno_graph_path` for "how are X and Y connected?", `gno_links`/`gno_backlinks` for one-document link expansion, and `gno_similar` for semantic neighbors. Prefer explicit or typed edges over inferred, ambiguous, or similarity edges when confidence matters.
398
+ 5. Use graph/link expansion for relationship context: `gno_graph_query` for typed relationship traversal, `gno_graph_neighbors` for nearby documents, `gno_graph_path` for "how are X and Y connected?", `gno_links`/`gno_backlinks` for one-document link expansion, and `gno_similar` for semantic neighbors. Prefer explicit or typed edges over inferred, ambiguous, or similarity edges when confidence matters. Plain `[[Note]]` links resolve across collections of the same vault (link workspace), so backlinks and impact can name other collections; pass `collection`/`collections` to keep results inside the user's scope. Never tell users to rewrite links into `[[collection:Note]]` to make them resolve.
393
399
  6. Use `gno_query_diagnose` when a known target document should have appeared but did not; it reports BM25/vector/fusion/graph/rerank stage presence and filter state.
394
400
  7. Use `gno_get` with `fromLine`/`lineCount` for targeted reads, or `gno_multi_get` to batch top refs.
395
401
  8. Use `gno_section` only when you need a durable section locator or must re-resolve one after edits. Prefer search → `gno_get` for ordinary retrieval. `action=create` needs `ref` plus exactly one of `anchor`|`line`; `action=resolve` needs `ref` plus `target`. Cite or open content only for `exact`/`recovered` results, then follow the tool's ready-to-use `gno_get` guidance (`fromLine = lineStart`; `lineCount = lineEnd - lineStart + 1`). Never navigate or cite `ambiguous`/`stale`/`missing`.
@@ -418,6 +424,7 @@ changed source:
418
424
  gno changes --since 2026-07-20T00:00:00Z --json
419
425
  gno diff gno://notes/plan.md --json
420
426
  gno impact gno://notes/plan.md --max-depth 3 --json
427
+ gno impact gno://notes/plan.md --collection notes --json # stay in scope
421
428
  ```
422
429
 
423
430
  Treat cursors and change IDs as opaque. Journal results are bounded,
@@ -594,7 +594,8 @@ gno impact gno://notes/plan.md --max-depth 3 --json
594
594
  `changes` accepts an ISO time or opaque cursor and optional collection/limit.
595
595
  `diff` reports structural headings, links, and typed-relationship changes for
596
596
  one retained change. `impact` follows inbound evidence edges with explicit
597
- depth/node/edge/frontier/visited bounds. Expired journal history is reported,
597
+ depth/node/edge/frontier/visited bounds; repeat `-c, --collection <name>` to
598
+ keep the traversal (and every path step) inside those collections. Expired journal history is reported,
598
599
  not reconstructed.
599
600
 
600
601
  ## Private Retrieval Traces
@@ -679,6 +680,12 @@ gno backlinks <ref> [options]
679
680
  `--relation <type>` to query semantic typed edges instead of positional
680
681
  wiki/markdown links. Do not combine `--type` with `--edge-type`.
681
682
 
683
+ Plain `[[Note]]` and `[[Folder/Note]]` links resolve across all collections
684
+ that share one vault (the nearest `.obsidian` folder, or `workspaceRoot` in
685
+ the config): exact path, then same folder, then shallowest file; ties stay
686
+ unresolved and are listed by `gno audit links`. JSON names the other
687
+ collection (`resolvedCollection` for links, `sourceCollection` for backlinks).
688
+
682
689
  ### gno graph query
683
690
 
684
691
  Bounded typed-edge traversal from a document.
@@ -314,7 +314,8 @@ gno similar gno://notes/auth.md --cross-collection
314
314
  # In your documents:
315
315
 
316
316
  See [[API Design]] for details.
317
- Check [[work:Project Plan]] for cross-collection link.
317
+ Check [[Projects/Plan]] for a path link (resolves across collections of one vault).
318
+ Check [[work:Project Plan]] to name the target collection explicitly.
318
319
  Read [[Security#OAuth]] for specific section.
319
320
  ```
320
321
 
@@ -144,7 +144,9 @@ lives in `structuredContent`; its text projection is deliberately compact and
144
144
  should not be expanded back into duplicate model context.
145
145
 
146
146
  Use `gno_changes`, `gno_diff`, and `gno_impact` for retained metadata history
147
- and bounded dependency questions. Use `gno_trace_list` and `gno_trace_show` for
147
+ and bounded dependency questions. `gno_impact` takes optional `collections`
148
+ to keep the traversal in scope; links in a vault can resolve across
149
+ collections, so unscoped graph tools cover every collection. Use `gno_trace_list` and `gno_trace_show` for
148
150
  private local diagnostics. Invoke `gno_trace_label` only when the user
149
151
  explicitly provides a relevant, irrelevant, or missing-expected judgment.
150
152
  Trace export/replay/delete/purge and saved-Capsule watch lifecycle remain
Binary file
@@ -0,0 +1 @@
1
+ 2066003653182eb93964c5c058c86f691abf536f5093d1d0e8db2f382aedce19 gno-browser-clipper-v2.8.0.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "2.7.1"
24
+ "version": "2.8.0"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -263,6 +263,8 @@ gno status [--json|--md]
263
263
  {
264
264
  "name": "work",
265
265
  "path": "/path",
266
+ "workspaceRoot": "/vault",
267
+ "workspaceSource": "detected",
266
268
  "documentCount": 100,
267
269
  "chunkCount": 500,
268
270
  "embeddedCount": 500
@@ -624,9 +626,11 @@ refuses descent into dataless or availability-unknown directories
624
626
  (`DATALESS_DIRECTORY` skip, or the fail-closed codes above) and preserves
625
627
  previously indexed descendants under those unproven prefixes rather than
626
628
  marking them inactive. Eligible files have no availability `errorCode`.
627
- Evidence-qualified scope: Google Drive, iCloud Drive, and OneDrive only for the
628
- tested OS/provider configuration; OneDrive only for both validated immediate
629
- SharePoint library roots. No Windows/Linux guarantee; metadata/provider
629
+ Evidence-qualified scope: Google Drive (`My Drive` and immediate Shared drives
630
+ under `Shared drives/<drive>`; the `Shared drives` folder itself is
631
+ unsupported), iCloud Drive, and OneDrive only for the tested OS/provider
632
+ configuration; OneDrive only for the two validated immediate SharePoint library
633
+ roots. No Windows/Linux guarantee; metadata/provider
630
634
  bookkeeping may occur; GNO does not pin/evict/download as product behavior.
631
635
  Source availability is distinct from `egressPolicy` (where derived content may
632
636
  travel).
@@ -886,6 +890,15 @@ projection.
886
890
  content-free decision, lineage, partial disclosure, audit metadata, and
887
891
  remediation contract.
888
892
 
893
+ `workspaceSource` is `detected` (nearest `.obsidian/` ancestor-or-self of the
894
+ collection root), `configured` (`workspaceRoot` setting), `disabled`
895
+ (`workspaceRoot: false`), `unavailable` (the root or an ancestor could not be
896
+ inspected; links stay collection-scoped) or `none`. `workspaceRoot` is the
897
+ effective workspace root, present only for `detected`/`configured`, and is a
898
+ same-host-only field like `path`. Terminal output adds a `Link workspace:` line
899
+ per workspace collection. `gno collection list` reports the same per
900
+ collection as `effectiveWorkspaceRoot` and `workspaceSource`.
901
+
889
902
  ### gno audit
890
903
 
891
904
  Read-only, offline knowledge-integrity audits. This command is distinct from
@@ -901,8 +914,17 @@ gno audit [links|provenance|freshness|all] [--collection <name>...] \
901
914
  [--json] [--output <path>]
902
915
  ```
903
916
 
904
- The default category is `all`; `--max-findings` defaults to 100 and is bounded
905
- to 1–1000. Truncation limits returned findings but preserves exact totals.
917
+ The default category is `all`; `--max-findings` defaults to 100 and accepts an
918
+ integer from 1 to 100000 or `all`. `all` returns every finding of the bounded
919
+ audit snapshot, and per-rule caps follow the same value. Any other value
920
+ (zero, negative, non-integer, above 100000) is a validation error (exit 1)
921
+ that states the accepted range. Truncation limits returned findings but
922
+ preserves exact totals. The report's `truncation` block reports three separate
923
+ conditions: `findingsTruncated` (the finding cap cut the list),
924
+ `snapshotTruncated` (the bounded snapshot of 50,000 documents/links was cut, so
925
+ totals cover the snapshot only and are not complete-index totals), and
926
+ `evidenceTruncated` (evidence items, tied-candidate lists, or evidence detail
927
+ text were shortened).
906
928
  `--output` writes only the requested report artifact with local file
907
929
  permissions. Human output renders the same report represented by
908
930
  `audit-report.schema.json`.
@@ -924,6 +946,20 @@ healthy.
924
946
  - `4` — complete report with findings
925
947
  - `5` — partial, inconclusive, unavailable, or changed-during-audit evidence
926
948
 
949
+ Link findings resolve with the workspace-aware resolver (see
950
+ docs/ARCHITECTURE.md "Resolution"). Their evidence `detail` JSON carries
951
+ separate `referenceKind` (`wiki-name`, `wiki-path`, `explicit-collection`,
952
+ `markdown`), `resolutionStatus` (`unresolved`, `ambiguous`) and
953
+ `resolvedScope` (`same-collection`, `cross-collection`, `explicit-collection`,
954
+ or null when unresolved). Ambiguous workspace links add `candidateCount` and
955
+ `candidates` (tied candidate URIs in canonical path order); in a
956
+ collection-scoped audit, candidates outside the requested collections are
957
+ counted in `candidatesWithheld` and never named, and a list longer than the
958
+ detail bound sets `candidatesTruncated` (and `truncation.evidenceTruncated`).
959
+ Orphans stay "no incoming or outgoing resolved links", with connectivity drawn
960
+ from the whole index even when the audited documents are scoped; a tied link
961
+ connects nothing.
962
+
927
963
  The JSON contract is versioned as `gno://schemas/audit-report@1.0`. Finding IDs
928
964
  are stable SHA-256 identities derived from rule, normalized subject/location,
929
965
  and load-bearing evidence. Wall-clock timing and traversal order do not affect
@@ -4179,13 +4215,19 @@ Find active documents that depend on one document through inbound typed,
4179
4215
  wiki-link, or Markdown-link edges.
4180
4216
 
4181
4217
  ```bash
4182
- gno impact <doc> [--max-depth <n>] [--max-nodes <n>] [--max-edges <n>] [--frontier-limit <n>] [--visited-limit <n>] [--json]
4218
+ gno impact <doc> [-c, --collection <name>...] [--max-depth <n>] [--max-nodes <n>] [--max-edges <n>] [--frontier-limit <n>] [--visited-limit <n>] [--json]
4183
4219
  ```
4184
4220
 
4185
4221
  The traversal is cycle-safe and enforces depth, node, edge, frontier, and
4186
4222
  visited-row caps. Every impacted document includes one deterministic
4187
4223
  dependency-to-root evidence path. JSON output uses `impact.schema.json`.
4188
4224
 
4225
+ `--collection` (repeatable) limits the traversal to those collections: only
4226
+ their documents are visited, returned, or used as a path step, so a document
4227
+ outside the scope never bridges two in-scope documents. Omitted means every
4228
+ indexed collection. An unknown collection, or a `<doc>` outside the requested
4229
+ collections, is a validation error (exit 1).
4230
+
4189
4231
  **Exit Codes:**
4190
4232
 
4191
4233
  - 0: Success
@@ -172,7 +172,6 @@ CREATE TABLE IF NOT EXISTS documents (
172
172
  );
173
173
 
174
174
  CREATE INDEX IF NOT EXISTS idx_documents_collection ON documents(collection);
175
- CREATE INDEX IF NOT EXISTS idx_documents_active ON documents(active);
176
175
  CREATE INDEX IF NOT EXISTS idx_documents_mirror_hash ON documents(mirror_hash);
177
176
  CREATE INDEX IF NOT EXISTS idx_documents_docid ON documents(docid);
178
177
  CREATE INDEX IF NOT EXISTS idx_documents_uri ON documents(uri);
package/spec/mcp.md CHANGED
@@ -351,7 +351,8 @@ Collection names are case-insensitive on input and normalized to lowercase in re
351
351
  `gno_audit` is a read-only, offline tool that returns the same
352
352
  `gno://schemas/audit-report@1.0` report as `gno audit`. Its closed input accepts
353
353
  `category` (`links`, `provenance`, `freshness`, or `all`), collection/path/tag
354
- filters, and `maxFindings` (1–1000). `maxAgeDays`, `orphanRoots`, and
354
+ filters, and `maxFindings` (an integer from 1 to 100000, or `"all"` for every
355
+ finding of the bounded snapshot; default 100). `maxAgeDays`, `orphanRoots`, and
355
356
  `orphanIgnorePrefixes` are optional explicit policy inputs. Request
356
357
  cancellation returns a partial report rather than a false clean result. The
357
358
  tool is annotated with
@@ -1841,8 +1842,15 @@ pending), `sourceUnavailable`, `staleParser` and last import time, and the
1841
1842
  switches, pending and running work, last run, last success, next due time,
1842
1843
  recovery action). The result contains no host paths.
1843
1844
 
1845
+ Each call (and each `gno_sessions_import` call) first re-reads the server's
1846
+ config file and adopts it when it changed, so source changes made by another
1847
+ process show without a restart. An unchanged file is a no-op; a source-only
1848
+ change keeps open HTTP sessions.
1849
+
1844
1850
  **Errors:** `SESSIONS_NOT_CONFIGURED` when the server's config has no
1845
- `sessions` block.
1851
+ `sessions` block; `SESSIONS_RUNTIME_FAILURE` when the config file cannot be
1852
+ read (never answered from the stale config); `SESSIONS_BINDING_MISMATCH` when
1853
+ the file is now bound to a different index (not adopted).
1846
1854
 
1847
1855
  ### gno_sessions_import
1848
1856
 
@@ -2371,7 +2379,14 @@ structure is derived from `structureDelta.truncated`.
2371
2379
 
2372
2380
  ### gno_impact
2373
2381
 
2374
- Read-only inbound dependency traversal for `ref`. Inputs `maxDepth`,
2382
+ Read-only inbound dependency traversal for `ref`. Optional `collections`
2383
+ (array of collection names) limits the traversal as `gno impact --collection`
2384
+ does; omitted means every collection. Over Streamable HTTP, egress policy is
2385
+ checked on `collections` (every collection when omitted, because the result
2386
+ can reach any collection a link resolves into) and on the collection of `ref`;
2387
+ a docid `ref` is checked against every collection. The same rule applies to
2388
+ `gno_backlinks` and `gno_graph*` refs, and `gno_similar` with
2389
+ `crossCollection: true` is checked against every collection. Inputs `maxDepth`,
2375
2390
  `maxNodes`, `maxEdges`, `frontierLimit`, and `visitedLimit` use the same bounds
2376
2391
  as CLI/REST/SDK. Structured content is `impact.schema.json`; each impacted
2377
2392
  document includes a deterministic evidence path over typed or backlink
@@ -260,13 +260,27 @@
260
260
  "truncation": {
261
261
  "type": "object",
262
262
  "additionalProperties": false,
263
- "required": ["findingsTruncated", "maxFindings"],
263
+ "required": [
264
+ "findingsTruncated",
265
+ "maxFindings",
266
+ "snapshotTruncated",
267
+ "evidenceTruncated"
268
+ ],
264
269
  "properties": {
265
270
  "findingsTruncated": { "type": "boolean" },
266
271
  "maxFindings": {
267
- "type": "integer",
268
- "minimum": 1,
269
- "maximum": 1000
272
+ "oneOf": [
273
+ { "type": "integer", "minimum": 1, "maximum": 100000 },
274
+ { "const": "all" }
275
+ ]
276
+ },
277
+ "snapshotTruncated": {
278
+ "type": "boolean",
279
+ "description": "The bounded audit snapshot was cut; finding totals cover the snapshot only, not the whole index"
280
+ },
281
+ "evidenceTruncated": {
282
+ "type": "boolean",
283
+ "description": "Evidence items, candidate lists, or evidence detail text were shortened"
270
284
  }
271
285
  }
272
286
  },
@@ -28,6 +28,10 @@
28
28
  "type": "string",
29
29
  "description": "Source document title"
30
30
  },
31
+ "sourceCollection": {
32
+ "type": "string",
33
+ "description": "Collection of the linking document (may differ from the target's in a link workspace)"
34
+ },
31
35
  "linkText": {
32
36
  "type": "string",
33
37
  "description": "Link display text"
@@ -39,6 +39,19 @@
39
39
  "languageHint": {
40
40
  "type": "string",
41
41
  "description": "BCP-47 language hint for the collection"
42
+ },
43
+ "workspaceRoot": {
44
+ "description": "Configured link workspace: an absolute path joins the collection to that workspace root; false keeps links inside the collection. Absent means auto-detect. Same-host callers only: omitted for a remote REST caller.",
45
+ "oneOf": [{ "type": "string" }, { "const": false }]
46
+ },
47
+ "effectiveWorkspaceRoot": {
48
+ "type": "string",
49
+ "description": "CLI list only: effective link workspace root (nearest .obsidian folder, or the configured root). Absent when links stay inside the collection."
50
+ },
51
+ "workspaceSource": {
52
+ "type": "string",
53
+ "enum": ["none", "detected", "configured", "disabled", "unavailable"],
54
+ "description": "CLI list only: how the link workspace was set"
42
55
  }
43
56
  }
44
57
  }
@@ -92,6 +92,8 @@
92
92
  "enum": [
93
93
  "exact-title",
94
94
  "exact-path",
95
+ "exact-name",
96
+ "tie-break",
95
97
  "path-fallback",
96
98
  "ambiguous-fallback",
97
99
  "similarity"
@@ -78,6 +78,10 @@
78
78
  "type": "string",
79
79
  "description": "URI of resolved target document"
80
80
  },
81
+ "resolvedCollection": {
82
+ "type": "string",
83
+ "description": "Collection of the resolved target (may differ from the source's in a link workspace)"
84
+ },
81
85
  "resolvedTitle": {
82
86
  "type": "string",
83
87
  "description": "Title of resolved target document"
@@ -111,6 +111,21 @@
111
111
  "type": "string",
112
112
  "description": "Collection root path. Same-host callers only: omitted for a remote REST or HTTP MCP caller."
113
113
  },
114
+ "workspaceRoot": {
115
+ "type": "string",
116
+ "description": "Effective link workspace root: plain wiki links resolve across every collection under it. Present only when the collection is in a workspace. Same-host callers only: omitted for a remote REST or HTTP MCP caller."
117
+ },
118
+ "workspaceSource": {
119
+ "type": "string",
120
+ "enum": [
121
+ "none",
122
+ "detected",
123
+ "configured",
124
+ "disabled",
125
+ "unavailable"
126
+ ],
127
+ "description": "How the link workspace was set: detected (nearest .obsidian folder), configured (workspaceRoot setting), disabled (workspaceRoot: false), unavailable (root could not be inspected; links stay collection-scoped), none (no workspace)"
128
+ },
114
129
  "documentCount": {
115
130
  "type": "integer",
116
131
  "description": "Number of active documents",
@@ -14,8 +14,11 @@ import type { WorkspaceAuditProgress } from "../../core/audit-workspace";
14
14
  import { getIndexDbPath } from "../../app/constants";
15
15
  import { loadConfig } from "../../config";
16
16
  import {
17
+ AUDIT_MAX_FINDINGS_RANGE_MESSAGE,
17
18
  auditExitCode,
18
19
  AUDIT_CATEGORIES,
20
+ parseAuditMaxFindingsInput,
21
+ resolveAuditMaxFindings,
19
22
  serializeAuditReportCanonical,
20
23
  } from "../../core/audit";
21
24
  import { runWorkspaceAudit } from "../../core/audit-workspace";
@@ -31,7 +34,8 @@ export interface AuditCommandOptions {
31
34
  collections?: string[];
32
35
  paths?: string[];
33
36
  tags?: string[];
34
- maxFindings?: number;
37
+ /** Positive integer, `all`, or the raw CLI string for either. */
38
+ maxFindings?: number | string;
35
39
  maxAgeDays?: number;
36
40
  orphanRoots?: string[];
37
41
  orphanIgnorePrefixes?: string[];
@@ -80,11 +84,18 @@ export const audit = async (
80
84
  error: "category must be links, provenance, freshness, or all",
81
85
  };
82
86
  }
83
- if (invalidPositiveInteger(options.maxFindings)) {
87
+ const maxFindings =
88
+ typeof options.maxFindings === "string"
89
+ ? parseAuditMaxFindingsInput(options.maxFindings)
90
+ : options.maxFindings;
91
+ if (
92
+ (options.maxFindings !== undefined && maxFindings === undefined) ||
93
+ !resolveAuditMaxFindings(maxFindings).ok
94
+ ) {
84
95
  return {
85
96
  success: false,
86
97
  invalid: true,
87
- error: "maxFindings must be a positive integer",
98
+ error: AUDIT_MAX_FINDINGS_RANGE_MESSAGE,
88
99
  };
89
100
  }
90
101
  if (invalidPositiveInteger(options.maxAgeDays)) {
@@ -155,7 +166,7 @@ export const audit = async (
155
166
  collectionFilters: requestedCollections,
156
167
  pathFilters: options.paths,
157
168
  tagFilters: requestedTags,
158
- maxFindings: options.maxFindings,
169
+ maxFindings,
159
170
  agePolicy:
160
171
  options.maxAgeDays === undefined
161
172
  ? undefined
@@ -188,6 +199,14 @@ export const formatAuditReport = (
188
199
  `Categories: ${report.scope.categories.join(", ")}`,
189
200
  `Rules: ${report.counts.rules.total} (${report.counts.rules.fail} failed, ${report.counts.rules.unavailable} unavailable, ${report.counts.rules.inconclusive} inconclusive)`,
190
201
  `Findings: ${report.counts.findings.total}${report.counts.findings.truncated ? ` (${report.counts.findings.returned} shown)` : ""}`,
202
+ ...(report.truncation.snapshotTruncated
203
+ ? [
204
+ "Snapshot: truncated; totals cover the bounded audit snapshot, not the whole index",
205
+ ]
206
+ : []),
207
+ ...(report.truncation.evidenceTruncated
208
+ ? ["Evidence: some finding evidence was shortened"]
209
+ : []),
191
210
  `Examined: ${report.counts.examined.documents} document/rule observations`,
192
211
  `Duration: ${report.durationMs}ms`,
193
212
  ];
@@ -5,6 +5,10 @@
5
5
  import type { Collection } from "../../../config";
6
6
 
7
7
  import { loadConfig } from "../../../config";
8
+ import {
9
+ detectCollectionWorkspace,
10
+ formatLinkWorkspace,
11
+ } from "../../../core/link-workspace";
8
12
  import { bold, cyan, dim } from "../../colors";
9
13
  import { CliError } from "../../errors";
10
14
 
@@ -23,7 +27,30 @@ interface ListOptions {
23
27
  md?: boolean;
24
28
  }
25
29
 
26
- function formatMarkdown(collections: Collection[]): string {
30
+ type ListedCollection = Collection & {
31
+ /** Effective link workspace root (detected or configured). */
32
+ effectiveWorkspaceRoot?: string;
33
+ workspaceSource: ReturnType<typeof detectCollectionWorkspace>["source"];
34
+ };
35
+
36
+ /** Resolve each collection's effective link workspace from the filesystem. */
37
+ const withLinkWorkspaces = (collections: Collection[]): ListedCollection[] =>
38
+ collections.map((collection) => {
39
+ const workspace = detectCollectionWorkspace(collection);
40
+ return {
41
+ ...collection,
42
+ ...(workspace.root ? { effectiveWorkspaceRoot: workspace.root } : {}),
43
+ workspaceSource: workspace.source,
44
+ };
45
+ });
46
+
47
+ const linkWorkspaceLine = (collection: ListedCollection): string | null =>
48
+ formatLinkWorkspace({
49
+ workspaceRoot: collection.effectiveWorkspaceRoot,
50
+ workspaceSource: collection.workspaceSource,
51
+ });
52
+
53
+ function formatMarkdown(collections: ListedCollection[]): string {
27
54
  const lines: string[] = ["# Collections", ""];
28
55
  if (collections.length === 0) {
29
56
  lines.push("No collections configured.");
@@ -43,12 +70,14 @@ function formatMarkdown(collections: Collection[]): string {
43
70
  if (coll.updateCmd) {
44
71
  lines.push(`- **Update Command:** \`${coll.updateCmd}\``);
45
72
  }
73
+ const workspace = linkWorkspaceLine(coll);
74
+ if (workspace) lines.push(`- **Link workspace:** ${workspace}`);
46
75
  lines.push("");
47
76
  }
48
77
  return lines.join("\n");
49
78
  }
50
79
 
51
- function formatTerminal(collections: Collection[]): string {
80
+ function formatTerminal(collections: ListedCollection[]): string {
52
81
  if (collections.length === 0) {
53
82
  return dim("No collections configured.");
54
83
  }
@@ -78,6 +107,10 @@ function formatTerminal(collections: Collection[]): string {
78
107
  if (updateCmd) {
79
108
  lines.push(` ${dim("Update:")} ${updateCmd}`);
80
109
  }
110
+ const workspace = linkWorkspaceLine(coll);
111
+ if (workspace) {
112
+ lines.push(` ${dim("Links:")} workspace ${sanitize(workspace)}`);
113
+ }
81
114
  lines.push("");
82
115
  }
83
116
  return lines.join("\n");
@@ -96,13 +129,14 @@ export async function collectionList(options: ListOptions): Promise<void> {
96
129
  const config = result.value;
97
130
 
98
131
  // Format and output
132
+ const collections = withLinkWorkspaces(config.collections);
99
133
  let output: string;
100
134
  if (options.json) {
101
- output = JSON.stringify(config.collections, null, 2);
135
+ output = JSON.stringify(collections, null, 2);
102
136
  } else if (options.md) {
103
- output = formatMarkdown(config.collections);
137
+ output = formatMarkdown(collections);
104
138
  } else {
105
- output = formatTerminal(config.collections);
139
+ output = formatTerminal(collections);
106
140
  }
107
141
 
108
142
  process.stdout.write(`${output}\n`);
@@ -818,7 +818,7 @@ function getActiveChunkCount(
818
818
  `
819
819
  SELECT COUNT(*) as count FROM content_chunks c
820
820
  WHERE EXISTS (
821
- SELECT 1 FROM documents d
821
+ SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
822
822
  WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
823
823
  )
824
824
  `
@@ -851,7 +851,7 @@ function getActiveChunks(
851
851
  'force' as reason
852
852
  FROM content_chunks c
853
853
  WHERE EXISTS (
854
- SELECT 1 FROM documents d
854
+ SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
855
855
  WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
856
856
  )
857
857
  AND (c.mirror_hash > ? OR (c.mirror_hash = ? AND c.seq > ?))
@@ -864,7 +864,7 @@ function getActiveChunks(
864
864
  'force' as reason
865
865
  FROM content_chunks c
866
866
  WHERE EXISTS (
867
- SELECT 1 FROM documents d
867
+ SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
868
868
  WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
869
869
  )
870
870
  ORDER BY c.mirror_hash, c.seq