@hraness/wordcell 0.22.0 → 0.22.1

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 (45) hide show
  1. package/README.md +4 -4
  2. package/dist/authoring.js +5 -3
  3. package/dist/{authority-1pgftmwq.js → authority-06eqhqam.js} +2 -2
  4. package/dist/benchmark.js +3 -3
  5. package/dist/cli.js +15 -15
  6. package/dist/evaluation-builder.js +10 -10
  7. package/dist/evaluation-kb.js +10 -10
  8. package/dist/graph-authority.js +4 -4
  9. package/dist/graph-percolation.js +6 -6
  10. package/dist/graph.js +1 -1
  11. package/dist/{index-e6p9e926.js → index-1k595z6v.js} +2 -2
  12. package/dist/{index-1tm7bgx7.js → index-233z9wmn.js} +1 -1
  13. package/dist/{index-a6kbv718.js → index-76dbn6pe.js} +1 -1
  14. package/dist/{index-4ag64rc1.js → index-810nfzdj.js} +73 -18
  15. package/dist/{index-avbce90h.js → index-bqtqeak3.js} +4 -4
  16. package/dist/{index-t2bs9xdr.js → index-djrcc1yf.js} +4 -4
  17. package/dist/{index-6sw24nvv.js → index-ecqpgr5y.js} +53 -2
  18. package/dist/{index-0dqfnrz9.js → index-hmw17zaa.js} +2 -2
  19. package/dist/{index-nd6nynv2.js → index-jk1sgx36.js} +1 -1
  20. package/dist/{index-qwgsmtsz.js → index-n386evxj.js} +1 -1
  21. package/dist/{index-2hf27mws.js → index-q7tbg07z.js} +7 -7
  22. package/dist/{index-ekpwvbra.js → index-qbssx940.js} +1 -2
  23. package/dist/{index-hcw140eb.js → index-rr9kzq4n.js} +2 -2
  24. package/dist/{index-11621h23.js → index-tf4wzjew.js} +1 -1
  25. package/dist/{index-gm9t95d9.js → index-vnybgywh.js} +1 -1
  26. package/dist/{index-famy7fhs.js → index-xv80vzfp.js} +4 -4
  27. package/dist/index.js +16 -14
  28. package/dist/percolate.js +2 -2
  29. package/dist/portfolio.js +10 -10
  30. package/dist/publish.js +3 -3
  31. package/dist/sdk.js +9 -9
  32. package/dist/search.js +2 -2
  33. package/dist/semantic.js +4 -4
  34. package/dist/{support-mywv1em0.js → support-1rh2d7mt.js} +14 -14
  35. package/dist/workflows/decision-context.js +10 -10
  36. package/dist/workflows/index.js +10 -10
  37. package/package.json +6 -5
  38. package/skills/wordcell/SKILL.md +2 -2
  39. package/skills/wordcell/references/publish.md +34 -0
  40. package/skills/wordcell/references/refresh.md +14 -0
  41. package/src/authoring-model.ts +29 -0
  42. package/src/authoring-program.ts +38 -3
  43. package/src/authoring.ts +17 -3
  44. package/src/cli-program.ts +94 -3
  45. package/src/graph.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/wordcell",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "A local knowledge base for coding agents. Save decisions in Markdown and recover the context behind your code.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -476,14 +476,15 @@
476
476
  "url-metadata:tool:build": "bun ./dist/cli.js url-metadata tool build",
477
477
  "url-metadata:tool:check": "bun ./dist/cli.js url-metadata tool check",
478
478
  "test:package": "bun run ./scripts/package-smoke.ts",
479
- "check": "bun run check:effect && bun run check:workflows && bun run check:portfolio-inventory && bun run check:installed-command-docs && bun run typecheck && bun run build && bun run test:package && bun run test && bun run url-metadata:tool:check",
479
+ "check": "bun run check:worker && bun run check:effect && bun run check:workflows && bun run check:portfolio-inventory && bun run check:installed-command-docs && bun run typecheck && bun run build && bun run test:package && bun run test && bun run url-metadata:tool:check",
480
480
  "prepack": "bun run check",
481
481
  "check:effect": "bun run scripts/check-effect-policy.ts",
482
- "kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.0/hraness-wordcell-0.22.0.tgz wordcell search --root kb --mode exact --rerank typesafe --rerank-limit 25",
483
- "kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.0/hraness-wordcell-0.22.0.tgz wordcell search --root kb --mode exact"
482
+ "kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.1/hraness-wordcell-0.22.1.tgz wordcell search --root kb --mode exact --rerank typesafe --rerank-limit 25",
483
+ "kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.1/hraness-wordcell-0.22.1.tgz wordcell search --root kb --mode exact",
484
+ "check:worker": "tsc -p worker/tsconfig.json --noEmit"
484
485
  },
485
486
  "dependencies": {
486
- "@hraness/oh": "https://github.com/hraness/oh/releases/download/v0.10.8/hraness-oh-0.10.8.tgz",
487
+ "@hraness/oh": "https://github.com/hraness/oh/releases/download/v0.11.0/hraness-oh-0.11.0.tgz",
487
488
  "@steipete/sweet-cookie": "0.4.3",
488
489
  "@tobilu/qmd": "git+https://github.com/hraness/qmd.git#aa993dceb3ef8cfb71d470554ca437570f5a2b3c",
489
490
  "agent-browser": "0.32.3",
@@ -35,7 +35,7 @@ selected.
35
35
  | Create or update an implementation plan, proposal, RFC, migration plan, execution audit, or phased checklist in the vault | [Write a durable plan](references/plan.md) and [use its structure](references/plan-structure.md) |
36
36
  | Review recurring ideas, promote concepts, or add and verify typed relationships | [Percolate concepts and relationships](references/percolate.md) |
37
37
  | Refresh or validate the catalog, graph, attachments, repository scopes, context mappings, or overall vault health | [Refresh and check the knowledge base](references/refresh.md) |
38
- | Publish a vault or a selected subsection as a hosted read-only static site with browser-local search | [Publish a static site](references/publish.md) |
38
+ | Publish a vault or a selected subsection as a hosted read-only static site with browser-local search — locally via `wordcell publish`, or over the wordcell.io REST/MCP surface when no filesystem or Bun runtime is available | [Publish a static site](references/publish.md) |
39
39
 
40
40
  Read more than one primary reference only when the request spans those
41
41
  workflows. For example, saving a source and linking it from a maintained note
@@ -56,7 +56,7 @@ missing:
56
56
  ```sh
57
57
  command -v wordcell >/dev/null 2>&1 || {
58
58
  command -v bun >/dev/null 2>&1 || exit 1
59
- bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.0/hraness-wordcell-0.22.0.tgz
59
+ bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.1/hraness-wordcell-0.22.1.tgz
60
60
  }
61
61
  wordcell --help
62
62
  ```
@@ -91,3 +91,37 @@ local build alone is not a live deployment.
91
91
 
92
92
  The full artifact contract and hosting options are in
93
93
  [the publishing guide](https://github.com/hraness/wordcell/blob/main/docs/publish.md).
94
+
95
+ ## Publish through wordcell.io when the CLI cannot run
96
+
97
+ Agents on platforms without a filesystem or Bun runtime can publish the same
98
+ `hraness.wordcell.site.v1` artifact through the hosted surface. It runs the
99
+ identical projection server-side; published sites are public.
100
+
101
+ ```sh
102
+ # Self-serve capability token (IP-limited; shown once — store it).
103
+ TOKEN=$(curl -sf -X POST https://wordcell.io/api/v1/tokens \
104
+ -H 'content-type: application/json' -d '{"label":"my-agent"}' | jq -r .token)
105
+
106
+ # Publish a vault as a files map: path -> utf8 string, {"base64":"..."}, or
107
+ # {"upload":"<id>"} from POST /api/v1/uploads for larger assets.
108
+ curl -sf -X PUT "https://wordcell.io/api/v1/sites/my-notes" \
109
+ -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
110
+ -d '{"files":{"index.md":"# Notes\n","a.md":"# A\n"},"title":"My notes"}'
111
+ # -> {"site":{"url":"https://wordcell.io/p/<key8>/my-notes/",...},"idempotent":false}
112
+ ```
113
+
114
+ `GET /api/v1/sites` lists the token's sites and `DELETE /api/v1/sites/{slug}`
115
+ unpublishes. Identical bytes republish idempotently without a revision bump;
116
+ changed bytes sweep the superseded artifact. Deletes and republishes can take
117
+ up to 60 seconds to propagate at the edge. MCP clients can instead point at the
118
+ streamable-HTTP endpoint `POST https://wordcell.io/api/v1/mcp`, which exposes
119
+ `create_token`, `publish_site`, `list_sites`, and `delete_site` tools over the
120
+ same routes — `create_token` is unauthenticated so an MCP-only client can
121
+ onboard itself. `GET /api/v1/openapi.json` describes the REST surface.
122
+
123
+ The hosted surface carries the same selection semantics (`selection` mirrors
124
+ the CLI selectors) and the same redaction warning: `publish: false` excludes a
125
+ note, it does not erase text quoted elsewhere. Bounded inputs apply — at most
126
+ 256 files and 4 MiB inline per publish, 32 MiB per uploaded asset, 50 live
127
+ sites per token.
@@ -126,6 +126,20 @@ After any note or link edit, run the refresh command again so derived state and
126
126
  wordcell check --root "$KB_ROOT"
127
127
  ```
128
128
 
129
+ When the vault lives inside a repository and `KB_REPO` is set, add the advisory
130
+ repository-state pass:
131
+
132
+ ```sh
133
+ wordcell check --root "$KB_ROOT" --repo "$KB_REPO"
134
+ ```
135
+
136
+ This compares every authored `repository_scopes` declaration with the current
137
+ working tree and reports each current record whose declared path is now absent
138
+ or unusable. It is advisory: the exit code does not change, and a terminal plan
139
+ that names a removed path stays silent because that is valid authored history.
140
+ Treat an absent-scope advisory as a note to reread against the code, not as a
141
+ proven error; confirm the path in the repository before editing the note.
142
+
129
143
  Finish only when the graph check and any required agent-context check succeed,
130
144
  the configured catalog mode is satisfied, and broken or ambiguous links,
131
145
  relationships, and local attachments are resolved. Summarize deliberate
@@ -89,6 +89,11 @@ export interface AuthoringOptions {
89
89
  readonly dependencies?: Partial<AuthoringDependencies>;
90
90
  }
91
91
 
92
+ export interface UpdateNoteBodyOptions extends AuthoringOptions {
93
+ /** Exact revision read before preparing the replacement body. */
94
+ readonly expectedRevision: NoteRevision;
95
+ }
96
+
92
97
  export class InvalidCanonicalNoteIdError extends TypeError {
93
98
  readonly noteId: string;
94
99
 
@@ -692,6 +697,30 @@ export function normalizedRequestedBody(body: string): string {
692
697
  return body.endsWith("\n") ? body : `${body}\n`;
693
698
  }
694
699
 
700
+ /** Replace prose without serializing or modifying the authored frontmatter. */
701
+ export function renderUpdatedNoteBody(
702
+ snapshot: Pick<NoteSnapshot, "content" | "relativePath">,
703
+ parts: FrontmatterParts,
704
+ body: string,
705
+ ): string {
706
+ if (typeof body !== "string") throw new TypeError("a note body must be a string");
707
+ if (Buffer.byteLength(body, "utf8") > MAX_NOTE_BYTES) {
708
+ throw new RangeError("the note body is too large for bounded authoring");
709
+ }
710
+ if (Buffer.from(body, "utf8").toString("utf8") !== body) {
711
+ throw new TypeError("a note body must contain well-formed Unicode");
712
+ }
713
+ const normalized = normalizedRequestedBody(body);
714
+ if (!parts.hadFrontmatter) {
715
+ if (frontmatter(normalized, snapshot.relativePath).hadFrontmatter) {
716
+ throw new TypeError("a body update cannot introduce frontmatter into a note");
717
+ }
718
+ return normalized;
719
+ }
720
+ const header = snapshot.content.slice(0, snapshot.content.length - parts.bodySuffix.length);
721
+ return `${header}${parts.newline}${parts.newline}${normalized}`;
722
+ }
723
+
695
724
  export function renderCreatedNote(input: CreateNoteInput, documentId: string): string {
696
725
  const title = validateTitle(input.title);
697
726
  const type = validateType(input.type);
@@ -3,13 +3,13 @@ import { Effect, Exit } from "effect";
3
3
  import {
4
4
  MAX_NOTE_BYTES, NoteRevisionConflictError, addRelationToParts, assertCompatibleCreate,
5
5
  assertExpected, canonicalNoteId, canonicalRelationTarget, checkedExpectedRevision,
6
- frontmatter, isErrno, normalizeRelationPredicate, noteResult, pathFor, relationsFromParts,
7
- removeRelationFromParts, renderCreatedNote, renderFrontmatter, revisionFor,
6
+ existingDocumentId, frontmatter, isErrno, normalizeRelationPredicate, noteResult, pathFor, relationsFromParts,
7
+ removeRelationFromParts, renderCreatedNote, renderFrontmatter, renderUpdatedNoteBody, requireRevision, revisionFor,
8
8
  sameQuarantinedSnapshot, sameSnapshot, withRecoveryPath,
9
9
  } from "./authoring-model.js";
10
10
  import type {
11
11
  AuthoringDependencies, AuthoringInstallContext, AuthoringOptions, CreateNoteInput,
12
- DirectoryIdentity, NoteAuthoringResult, NoteRevision, NoteSnapshot, RecoveryLocation, Vault,
12
+ DirectoryIdentity, NoteAuthoringResult, NoteRevision, NoteSnapshot, RecoveryLocation, UpdateNoteBodyOptions, Vault,
13
13
  } from "./authoring-model.js";
14
14
  import type { NoteLock } from "./note-lock.js";
15
15
  import { parseDocumentId } from "./portfolio-identity.js";
@@ -276,3 +276,38 @@ export function editNoteRelationProgram(
276
276
  );
277
277
  });
278
278
  }
279
+
280
+ export function updateNoteBodyProgram(
281
+ platform: AuthoringPlatform, root: string, idInput: string, body: string,
282
+ options: UpdateNoteBodyOptions,
283
+ ): Effect.Effect<NoteAuthoringResult, AuthoringFailure> {
284
+ return Effect.gen(function*() {
285
+ const id = yield* authoringSync(() => canonicalNoteId(idInput));
286
+ const expected = yield* authoringSync(() => {
287
+ if (typeof options?.expectedRevision !== "string") {
288
+ throw new TypeError("expectedRevision is required for a note body update");
289
+ }
290
+ return requireRevision(options.expectedRevision);
291
+ });
292
+ const vault = yield* authoringNative(() => platform.resolveVault(root));
293
+ const dependencies = yield* authoringSync(() => platform.dependenciesFor(options.dependencies));
294
+ return yield* authoringResource(
295
+ authoringNative(() => platform.acquireNoteLock(vault.root, id, options.lock)),
296
+ (lock) => Effect.gen(function*() {
297
+ yield* authoringNative(() => platform.recoverInterruptedAuthoring(vault, id, lock));
298
+ const source = yield* authoringNative(() => platform.readSnapshot(vault, id));
299
+ yield* authoringSync(() => assertExpected(source, expected));
300
+ const parts = yield* authoringSync(() => frontmatter(source.content, source.relativePath));
301
+ const identity = yield* authoringSync(() => existingDocumentId(parts));
302
+ if (identity.kind === "invalid") return yield* fail(new TypeError("the note has an invalid document_id"));
303
+ const documentId = identity.kind === "valid" ? identity.documentId : undefined;
304
+ const relations = yield* authoringSync(() => relationsFromParts(parts, source.relativePath));
305
+ const content = yield* authoringSync(() => renderUpdatedNoteBody(source, parts, body));
306
+ if (content === source.content) return yield* authoringSync(() => noteResult(source, relations, false, documentId));
307
+ const revision = yield* installNote(platform, vault, id, content, source, lock, dependencies);
308
+ return yield* authoringSync(() => noteResult({ relativePath: source.relativePath, revision }, relations, true, documentId));
309
+ }),
310
+ (lock) => authoringNative(() => lock.release()),
311
+ );
312
+ });
313
+ }
package/src/authoring.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type {
2
2
  AuthoringOptions, CreateConceptNoteInput, CreateNoteInput, NoteAuthoringResult,
3
- NoteRelation, NoteRevision,
3
+ NoteRelation, NoteRevision, UpdateNoteBodyOptions,
4
4
  } from "./authoring-model.js";
5
5
  import { canonicalNoteId, frontmatter, relationsFromParts } from "./authoring-model.js";
6
6
  import {
7
7
  assertNoInterruptedRecovery, nativeAuthoringPlatform, readSnapshot, resolveVault,
8
8
  } from "./authoring-platform.js";
9
- import { createNoteProgram, editNoteRelationProgram } from "./authoring-program.js";
9
+ import { createNoteProgram, editNoteRelationProgram, updateNoteBodyProgram } from "./authoring-program.js";
10
10
  import { runAuthoring } from "./authoring-runtime.js";
11
11
 
12
12
  export {
@@ -15,7 +15,7 @@ export {
15
15
  } from "./authoring-model.js";
16
16
  export type {
17
17
  AuthoringDependencies, AuthoringInstallContext, AuthoringOptions,
18
- CreateConceptNoteInput, CreateNoteInput, NoteAuthoringResult, NoteRelation, NoteRevision,
18
+ CreateConceptNoteInput, CreateNoteInput, NoteAuthoringResult, NoteRelation, NoteRevision, UpdateNoteBodyOptions,
19
19
  } from "./authoring-model.js";
20
20
 
21
21
  /**
@@ -60,6 +60,20 @@ export async function createConceptNote(
60
60
  return createNote(root, { ...input, type: "concept" }, options);
61
61
  }
62
62
 
63
+ /**
64
+ * Replace one existing note's body at an exact revision. Frontmatter bytes,
65
+ * stable identity and relations remain unchanged. Like createNote, this adds a
66
+ * final newline and separates frontmatter from the body with one blank line.
67
+ *
68
+ * A stale revision fails even if the requested body already matches. Callers
69
+ * must read back an interrupted operation before deciding whether to retry.
70
+ */
71
+ export async function updateNoteBody(
72
+ root: string, id: string, body: string, options: UpdateNoteBodyOptions,
73
+ ): Promise<NoteAuthoringResult> {
74
+ return runAuthoring(updateNoteBodyProgram(nativeAuthoringPlatform, root, id, body, options));
75
+ }
76
+
63
77
  /** Add one exact outbound typed relation, idempotently. */
64
78
  export async function addNoteRelation(
65
79
  root: string, sourceId: string, predicate: string, targetId: string, options: AuthoringOptions = {},
@@ -122,10 +122,14 @@ import {
122
122
  type QuerySort,
123
123
  } from "./query.js";
124
124
  import {
125
+ auditRepositoryMemoryScopes,
125
126
  buildRepositoryMemoryContext,
127
+ MAX_REPOSITORY_MEMORY_DETAIL_LIMIT,
126
128
  MAX_REPOSITORY_SCOPES,
127
129
  repositoryMemoryGroupKeys,
128
130
  type RepositoryMemoryContext,
131
+ type RepositoryMemoryScopeAudit,
132
+ type RepositoryMemoryScopeAuditError,
129
133
  } from "./repository-memory.js";
130
134
  import { validateSearchQuery } from "./search.js";
131
135
  import {
@@ -248,7 +252,7 @@ Usage:
248
252
  wordcell inspect <url> [capture options]
249
253
  wordcell pdf <file-or-url> [PDF options]
250
254
  wordcell refresh [--root <directory>] [--index <path>] [--json]
251
- wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--json]
255
+ wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--repo <repository>] [--json]
252
256
  wordcell catalog [--root <directory>] [--index <path>] [--json]
253
257
  wordcell graph [--root <directory>] [--index <path>] [--json]
254
258
  wordcell graph rebuild [--fresh] [--root <directory>] [--index <path>] [--json]
@@ -443,6 +447,8 @@ type ParsedCommand =
443
447
  readonly depth?: number;
444
448
  readonly limit?: number;
445
449
  readonly noCatalog?: boolean;
450
+ /** `check --repo` only: repository working tree for the advisory scope lane. */
451
+ readonly repository?: string;
446
452
  }
447
453
  | {
448
454
  readonly kind: "note-create";
@@ -528,6 +534,7 @@ type CliDependencies = GraphCliDependencies & {
528
534
  readonly percolateWithGraph?: typeof percolateWithGraph;
529
535
  readonly inspectAgentContextRepository?: typeof inspectAgentContextRepository;
530
536
  readonly buildRepositoryMemoryContext?: typeof buildRepositoryMemoryContext;
537
+ readonly auditRepositoryMemoryScopes?: typeof auditRepositoryMemoryScopes;
531
538
  readonly auditAgentGuideRepository?: typeof auditAgentGuideRepository;
532
539
  readonly validateMarkdownAttachments?: typeof validateMarkdownAttachments;
533
540
  readonly publishVault?: typeof publishVault;
@@ -623,6 +630,7 @@ function parseVaultCommand(command: VaultCommand, arguments_: readonly string[])
623
630
  let depth = 1;
624
631
  let limit: number | undefined;
625
632
  let noCatalog = false;
633
+ let repository: string | undefined;
626
634
  const positional: string[] = [];
627
635
 
628
636
  for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
@@ -636,6 +644,13 @@ function parseVaultCommand(command: VaultCommand, arguments_: readonly string[])
636
644
  noCatalog = true;
637
645
  continue;
638
646
  }
647
+ if (argument === "--repo" && command === "check") {
648
+ const value = readValue(arguments_, cursor);
649
+ if (value === null) return { ok: false, message: "--repo requires a value" };
650
+ repository = value;
651
+ cursor += 1;
652
+ continue;
653
+ }
639
654
  if (argument === "--root" || argument === "--index") {
640
655
  const value = readValue(arguments_, cursor);
641
656
  if (value === null) return { ok: false, message: `${argument} requires a value` };
@@ -700,6 +715,7 @@ function parseVaultCommand(command: VaultCommand, arguments_: readonly string[])
700
715
  options: index === undefined ? {} : { index },
701
716
  json,
702
717
  ...(command === "check" && noCatalog ? { noCatalog: true } : {}),
718
+ ...(command === "check" && repository !== undefined ? { repository } : {}),
703
719
  },
704
720
  };
705
721
  }
@@ -2637,6 +2653,7 @@ function summary(
2637
2653
  options: {
2638
2654
  readonly noCatalog?: boolean;
2639
2655
  readonly attachments?: AttachmentValidationReport;
2656
+ readonly repositoryScopes?: RepositoryMemoryScopeAudit;
2640
2657
  } = {},
2641
2658
  ): Record<string, unknown> {
2642
2659
  return {
@@ -2662,6 +2679,9 @@ function summary(
2662
2679
  issues: options.attachments.issues,
2663
2680
  },
2664
2681
  }),
2682
+ ...(options.repositoryScopes === undefined
2683
+ ? {}
2684
+ : { repositoryScopes: options.repositoryScopes }),
2665
2685
  };
2666
2686
  }
2667
2687
 
@@ -2697,6 +2717,60 @@ function renderAdvisories(analysis: VaultAnalysis): string[] {
2697
2717
  return lines;
2698
2718
  }
2699
2719
 
2720
+ function renderRepositoryScopeAuditError(error: RepositoryMemoryScopeAuditError): string {
2721
+ if (error.kind === "invalid-record") {
2722
+ return `${safe(error.path)}: ${error.issues.map(safe).join(" ")}`;
2723
+ }
2724
+ const declared = error.recordPaths.map(safe).join(", ")
2725
+ + (error.recordPathsTruncated ? ", …" : "");
2726
+ return `${safe(error.scope)}: ${safe(error.state.reason)} at ${safe(error.state.path)}`
2727
+ + ` (declared by ${declared})`;
2728
+ }
2729
+
2730
+ /**
2731
+ * Advisory repository-state lane for `check --repo`.
2732
+ *
2733
+ * Every line derives from authored `repository_scopes` declarations and the
2734
+ * working tree they name. Nothing here changes the exit code: a current note
2735
+ * that names a path the repository no longer has is reported for a reader to
2736
+ * judge, never treated as a vault correctness failure.
2737
+ */
2738
+ function renderRepositoryScopeAudit(audit: RepositoryMemoryScopeAudit): string[] {
2739
+ const { counts } = audit;
2740
+ const lines = [
2741
+ `Repository scopes: ${counts.authoredRecords} declaring note${counts.authoredRecords === 1 ? "" : "s"}; `
2742
+ + `${counts.distinctScopes} distinct scope${counts.distinctScopes === 1 ? "" : "s"} `
2743
+ + `(${counts.presentScopes} present, ${counts.absentScopes} absent, ${counts.invalidScopes} unusable).`,
2744
+ ];
2745
+ if (audit.advisories.total > 0) {
2746
+ lines.push(audit.advisories.total === 1
2747
+ ? "Advisory: 1 current note declares a repository path that no longer exists."
2748
+ : `Advisory: ${audit.advisories.total} current notes declare repository paths that no longer exist.`);
2749
+ for (const advisory of audit.advisories.details) {
2750
+ lines.push(` ${safe(advisory.path)} declares absent scope ${safe(advisory.scope)}`);
2751
+ }
2752
+ if (audit.advisories.truncated) {
2753
+ lines.push(
2754
+ ` … ${audit.advisories.total - audit.advisories.returned} more; rerun with --json for the complete audit.`,
2755
+ );
2756
+ }
2757
+ }
2758
+ if (audit.errors.total > 0) {
2759
+ lines.push(
2760
+ `Advisory: ${audit.errors.total} repository scope declaration problem${audit.errors.total === 1 ? "" : "s"}.`,
2761
+ );
2762
+ for (const error of audit.errors.details) {
2763
+ lines.push(` ${renderRepositoryScopeAuditError(error)}`);
2764
+ }
2765
+ if (audit.errors.truncated) {
2766
+ lines.push(
2767
+ ` … ${audit.errors.total - audit.errors.returned} more; rerun with --json for the complete audit.`,
2768
+ );
2769
+ }
2770
+ }
2771
+ return lines;
2772
+ }
2773
+
2700
2774
  function checkExitCode(
2701
2775
  snapshot: VaultSnapshot,
2702
2776
  noCatalog = false,
@@ -2723,6 +2797,7 @@ function renderSnapshot(
2723
2797
  snapshot: VaultSnapshot,
2724
2798
  noCatalog = false,
2725
2799
  attachments?: AttachmentValidationReport,
2800
+ repositoryScopes?: RepositoryMemoryScopeAudit,
2726
2801
  ): string {
2727
2802
  const lines = [
2728
2803
  `${command === "refresh" ? "Refreshed" : "Checked"} ${safe(snapshot.root)}`,
@@ -2742,6 +2817,7 @@ function renderSnapshot(
2742
2817
  lines.push("error: attachment validation was truncated by a resource limit");
2743
2818
  }
2744
2819
  lines.push(...renderAdvisories(snapshot.analysis));
2820
+ if (repositoryScopes !== undefined) lines.push(...renderRepositoryScopeAudit(repositoryScopes));
2745
2821
  return `${lines.join("\n")}\n`;
2746
2822
  }
2747
2823
 
@@ -3806,9 +3882,24 @@ async function runVault(
3806
3882
  documents: snapshot.notes.map(({ path, content }) => ({ path, content })),
3807
3883
  })
3808
3884
  : undefined;
3885
+ const repositoryScopes = command.kind === "check" && command.repository !== undefined
3886
+ ? await (dependencies.auditRepositoryMemoryScopes ?? auditRepositoryMemoryScopes)(
3887
+ snapshot.notes,
3888
+ {
3889
+ repositoryRoot: command.repository,
3890
+ detailLimit: MAX_REPOSITORY_MEMORY_DETAIL_LIMIT,
3891
+ },
3892
+ )
3893
+ : undefined;
3809
3894
  output.stdout(command.json
3810
- ? terminalSafeJson(summary(snapshot, { noCatalog, ...(attachments === undefined ? {} : { attachments }) }))
3811
- : sanitizeTerminalText(renderSnapshot(command.kind, snapshot, noCatalog, attachments)));
3895
+ ? terminalSafeJson(summary(snapshot, {
3896
+ noCatalog,
3897
+ ...(attachments === undefined ? {} : { attachments }),
3898
+ ...(repositoryScopes === undefined ? {} : { repositoryScopes }),
3899
+ }))
3900
+ : sanitizeTerminalText(
3901
+ renderSnapshot(command.kind, snapshot, noCatalog, attachments, repositoryScopes),
3902
+ ));
3812
3903
  return checkExitCode(snapshot, noCatalog, attachments);
3813
3904
  }
3814
3905
  if (command.kind === "graph") {
package/src/graph.ts CHANGED
@@ -608,7 +608,7 @@ function aliasesFromMetadata(metadata: MetadataObject): readonly string[] {
608
608
  }
609
609
 
610
610
  function frontmatterOf(content: string, path: string): Frontmatter {
611
- const lines = content.split("\n");
611
+ const lines = content.split(/\r?\n/u);
612
612
  if (lines[0]?.trim() !== "---") {
613
613
  return {
614
614
  values: new Map(),