@williamthorsen/kb 0.7.0 → 0.8.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 (77) hide show
  1. package/README.md +101 -96
  2. package/bin/kb.js +2 -4
  3. package/dist/esm/check/check.d.ts +14 -12
  4. package/dist/esm/check/check.js +41 -1
  5. package/dist/esm/check/enumerate.d.ts +13 -16
  6. package/dist/esm/check/glob-segments.d.ts +1 -1
  7. package/dist/esm/check/resolve-foreign-stores.d.ts +25 -0
  8. package/dist/esm/check/resolve-foreign-stores.js +50 -0
  9. package/dist/esm/cli/commands/check.d.ts +4 -10
  10. package/dist/esm/cli/commands/check.js +8 -3
  11. package/dist/esm/cli/commands/create.d.ts +3 -7
  12. package/dist/esm/cli/commands/create.js +1 -1
  13. package/dist/esm/cli/commands/scaffold.d.ts +5 -9
  14. package/dist/esm/cli/commands/scaffold.js +5 -5
  15. package/dist/esm/cli/commands/set-default.d.ts +1 -5
  16. package/dist/esm/cli/commands/set-default.js +1 -1
  17. package/dist/esm/cli/commands/taxonomy.d.ts +5 -10
  18. package/dist/esm/cli/commands/taxonomy.js +10 -9
  19. package/dist/esm/cli/format.d.ts +8 -10
  20. package/dist/esm/cli/resolve-store.d.ts +8 -8
  21. package/dist/esm/cli/run.d.ts +4 -6
  22. package/dist/esm/cli/run.js +1 -1
  23. package/dist/esm/cli/select-kb-prompt.d.ts +6 -5
  24. package/dist/esm/cli/select-kb-prompt.js +1 -1
  25. package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +3 -3
  26. package/dist/esm/cli/targeting/select-notes.d.ts +2 -2
  27. package/dist/esm/config/config-schema.d.ts +24 -6
  28. package/dist/esm/config/config-schema.js +9 -0
  29. package/dist/esm/config/index.d.ts +1 -1
  30. package/dist/esm/config/index.js +1 -1
  31. package/dist/esm/config/kb-loader-error.d.ts +3 -3
  32. package/dist/esm/config/load-config.d.ts +3 -3
  33. package/dist/esm/config/load-config.js +2 -1
  34. package/dist/esm/config/note-scope.d.ts +7 -7
  35. package/dist/esm/create/create.d.ts +8 -8
  36. package/dist/esm/discovery/kb-registry-schema.d.ts +0 -2
  37. package/dist/esm/discovery/load-registry.d.ts +7 -9
  38. package/dist/esm/discovery/load-registry.js +1 -1
  39. package/dist/esm/discovery/register-store.d.ts +3 -4
  40. package/dist/esm/discovery/register-store.js +2 -2
  41. package/dist/esm/discovery/registry-document.d.ts +2 -2
  42. package/dist/esm/discovery/set-default-kb.d.ts +5 -5
  43. package/dist/esm/discovery/set-default-kb.js +3 -3
  44. package/dist/esm/filesystem/exists.d.ts +4 -4
  45. package/dist/esm/frontmatter/parse-note.d.ts +3 -3
  46. package/dist/esm/git/list-git-scope.d.ts +7 -7
  47. package/dist/esm/git/run-git.d.ts +2 -2
  48. package/dist/esm/layout/store-layout.d.ts +5 -5
  49. package/dist/esm/lints/paths.d.ts +1 -1
  50. package/dist/esm/lints/paths.js +1 -1
  51. package/dist/esm/lints/tag-alias.d.ts +1 -1
  52. package/dist/esm/lints/tag-alias.js +1 -1
  53. package/dist/esm/lints/taxonomy.d.ts +9 -8
  54. package/dist/esm/lints/taxonomy.js +2 -2
  55. package/dist/esm/note-io/field-validators.d.ts +1 -1
  56. package/dist/esm/note-io/read-note.d.ts +2 -2
  57. package/dist/esm/note-io/write-note.d.ts +3 -6
  58. package/dist/esm/records/event.d.ts +3 -3
  59. package/dist/esm/scaffold/render-seeds.d.ts +3 -3
  60. package/dist/esm/scaffold/render-seeds.js +5 -3
  61. package/dist/esm/scaffold/scaffold.d.ts +7 -8
  62. package/dist/esm/tags/load-aliases.d.ts +3 -4
  63. package/dist/esm/taxonomy/domain-paths.d.ts +6 -6
  64. package/dist/esm/taxonomy/load-taxonomy.d.ts +3 -3
  65. package/dist/esm/taxonomy/load-taxonomy.js +2 -2
  66. package/dist/esm/taxonomy/taxonomy-schema.d.ts +5 -6
  67. package/dist/esm/taxonomy/write-taxonomy.d.ts +7 -8
  68. package/dist/esm/type-guards.d.ts +1 -1
  69. package/dist/esm/types.d.ts +6 -9
  70. package/dist/esm/vault-integrity/build-vault-index.d.ts +3 -4
  71. package/dist/esm/vault-integrity/check-vault-integrity.d.ts +46 -12
  72. package/dist/esm/vault-integrity/check-vault-integrity.js +59 -20
  73. package/dist/esm/vault-integrity/index.d.ts +2 -2
  74. package/dist/esm/vault-integrity/index.js +2 -2
  75. package/dist/esm/vault-integrity/wikilink-parse.d.ts +45 -19
  76. package/dist/esm/vault-integrity/wikilink-parse.js +37 -9
  77. package/package.json +6 -6
package/README.md CHANGED
@@ -1,36 +1,19 @@
1
+ <!-- readme-type: library -->
2
+
1
3
  # @williamthorsen/kb
2
4
 
3
5
  Foundation library for knowledge-base tooling.
4
6
  Provides knowledge-base discovery, registry loading, frontmatter parsing and writing, tag canonicalization, and type-blind vault-integrity checks.
5
- It underpins the knowledge-base skills among them `kb-retrieve` (assertion recall) and `kb-retrieve-events` (event recall), `kb-add`, `kb-curate`, `capture-event`, and `kb-update-events` — and the planned `@williamthorsen/kb-mcp` server.
7
+ It underpins the planned `@williamthorsen/kb-mcp` server and the knowledge-base skills, among them `kb-retrieve` (assertion recall) and `kb-retrieve-events` (event recall), `kb-add`, `kb-curate`, `capture-event`, and `kb-update-events`.
6
8
 
7
9
  <!-- section:release-notes -->
8
- ## Release notes — v0.7.0 (2026-09-08)
9
-
10
- ### 🎉 Features
11
-
12
- - 🚨 **Breaking:** Add kb scaffold to back-fill canonical files into an existing store (#1584)
13
-
14
- - Adds the `kb scaffold` command, which writes the canonical files (`.kb/config.yaml` and `.kb/tag-aliases.yaml`) into a knowledge base. Files are created if missing, or replaced if `--force` is used.
15
- - Narrows what `kb create` reports to the paths that it actually created.
16
-
17
- Migration: import `renderAliasesSeed` and `renderConfigSeed` from `@williamthorsen/kb/scaffold` instead of `@williamthorsen/kb/create`.
18
-
19
- - Add canonical formatting configs so a store formats without a toolchain (#1586)
20
-
21
- - Adds `.editorconfig` and `.prettierrc.yaml` to the canonical file set, so that `kb create` writes them into a new store and `kb scaffold` back-fills them into an existing one.
22
- - Puts width, indentation, and line endings in `.editorconfig`, which editors read as well as Prettier.
23
- - Sets `embeddedLanguageFormatting: off` in `.prettierrc.yaml`, which keeps Prettier from reflowing note frontmatter that the kb's note writer would flatten again.
10
+ ## Release notes — v0.8.1 (2026-09-21)
24
11
 
25
12
  ### 🐛 Bug fixes
26
13
 
27
- - Stop checking gitignored notes (#1587)
28
-
29
- - Stops `kb check` from checking notes matching patterns in `.gitignore`. It now checks only those files that are tracked by git or new and not gitignored.
30
- - Makes a link pointing at a gitignored note report `wikilinks.unresolved`, since such a link is broken for every clone but the author's.
31
- - Fixes `kb check <path>` and `kb check --vs <ref>` for a note with an accented name, which on macOS matched nothing and could select unrelated files in its place.
14
+ - Fix writing-rule violations in skill data, partials, and skills a to l (#1711)
32
15
 
33
- Migration: Track a gitignored note that other notes link to, or remove the wikilinks pointing at it.
16
+ - Fixes writing-rule violations in shared skill data, skill partials, and a range of skills.
34
17
  <!-- /section:release-notes -->
35
18
 
36
19
  ## Exports
@@ -40,22 +23,19 @@ The package exposes thirteen subpath entries plus a root barrel:
40
23
  | Entry | Description |
41
24
  | ------------------- | ------------------------------------------------------------------------------ |
42
25
  | `.` | The most-used types plus `buildVaultIndex` |
43
- | `./check` | `check`: config-driven enumeration composed with vault integrity and the lints |
44
- | `./config` | `.kb/config.yaml` loading and the typed `KbLoaderError` the loaders throw |
45
- | `./create` | `create`: scaffold a new store and register it in `kb.yaml` |
26
+ | `./check` | `check`: Config-driven enumeration composed with vault integrity and the lints |
27
+ | `./config` | `.kb/config.yaml` loading and the typed `KbLoaderError` that the loaders throw |
28
+ | `./create` | `create`: Scaffold a new store and register it in `kb.yaml` |
46
29
  | `./discovery` | KB root discovery and `kb.yaml` registry loading, merging, and writing |
47
30
  | `./filesystem` | Filesystem-existence helpers with an explicit absence policy |
48
- | `./frontmatter` | Note parsing into typed frontmatter and writing it back to YAML |
31
+ | `./frontmatter` | Note parsing into typed frontmatter |
49
32
  | `./layout` | The store's on-disk layout: every path inside a `.kb/` store derives from here |
50
33
  | `./note-io` | Type-blind note read/write as an ordered frontmatter field map |
51
34
  | `./records` | The typed `assertion`/`event` record parsers and renderers |
52
- | `./scaffold` | The canonical set held by a store, and the idempotent writer over it |
35
+ | `./scaffold` | The canonical set that a store contains, and the idempotent writer over it |
53
36
  | `./tags` | `.kb/tag-aliases.yaml` loading and tag canonicalization |
54
37
  | `./taxonomy` | `.kb/taxonomy.yaml` loading, comment-preserving declaration, and path mapping |
55
- | `./vault-integrity` | Type-blind `[[link]]` resolution and basename-uniqueness over a note set |
56
-
57
- Every public function takes a single plain-object input so a future MCP wrapper can mechanically bind Zod-validated payloads.
58
- The library throws on errors; success/failure shaping is left to consumers.
38
+ | `./vault-integrity` | Type-blind `[[link]]` scanning, resolution, and basename-uniqueness checks |
59
39
 
60
40
  ## Knowledge-base discovery
61
41
 
@@ -97,13 +77,15 @@ Configuration keys, per KB entry under `kbs.<name>`:
97
77
  | `description` | no | Human-readable description |
98
78
  | `readonly` | no | Marks the KB as read-only |
99
79
 
80
+ An entry's name is also what a [store-qualified wikilink](#linking-into-another-store) names: `[[coding:Note title]]` resolves against the KB registered as `coding`.
81
+
100
82
  ### Merge semantics
101
83
 
102
84
  `loadKbRegistry` merges the two registries by KB name:
103
85
 
104
86
  - Project entries **replace** user entries with the same name.
105
87
  - Project entries with a new name are **appended**.
106
- - When both files set `default_kb`, the **project** value wins; the resolved default is the named entry from the merged set.
88
+ - When both files set `default_kb`, the **project** value takes precedence; the resolved default is the named entry from the merged set.
107
89
  - Path existence is not checked at load time.
108
90
 
109
91
  ```ts
@@ -115,34 +97,35 @@ const config = await loadKbRegistry({ projectDir: process.cwd() });
115
97
 
116
98
  ## The addressed-by/addresses relation
117
99
 
118
- `addressed-by`/`addresses` is an inverse-pair relation that threads a problem record to whatever was done about it: a fix, a mitigation, an improved guidance note. Both are optional, multi-valued list fields:
100
+ `addressed-by`/`addresses` is an inverse-pair relation that links a problem record to whatever was done about it: a fix, a mitigation, an improved guidance note. Both are optional, multi-valued list fields:
119
101
 
120
102
  - `addressed-by` (on the problem record, available on `assertion` and `event`) is the canonical, recall-facing field: a list of references to whatever addressed the problem. It is the only viable store when the responder is external, so its entries are heterogeneous: a KB wikilink or relative path, a commit SHA, a PR/issue ref, or a URL. The field's shape is validated as a list by the record parser, while its entries are free-form, like `sources`. It is set on events with `kb-update-events` and on assertions with `kb-edit`.
121
- - `addresses` (on a KB-note responder, available on `assertion`) is the optional inverse for the rare "what does this address?" query. It is **non-authoritative**: keeping it in sync would be an N-file write, so `kb-curate` deliberately does not police it.
103
+ - `addresses` (on a KB-note responder, available on `assertion`) is the optional inverse for the rare "what does this address?" query. It is **non-authoritative**: keeping it in sync would be an N-file write, so `kb-curate` deliberately does not enforce it.
122
104
 
123
- The relation is many-to-many (one response can address many problems, and one problem can accrue many responses) and is surfaced flat by recall, with no chain-walking. This is distinct from `supersedes`/`superseded-by`, which _deprecates_ a record through a policed 1:1 chain; an addressed problem is not deprecated. It remains a true observation whose recurrence is worth keeping.
105
+ The relation is many-to-many (one response can address many problems, and one problem can accrue many responses) and recall reports it flat, with no chain-walking. This is distinct from `supersedes`/`superseded-by`, which _deprecates_ a record through an enforced 1:1 chain; an addressed problem is not deprecated. It remains a true observation whose recurrence is worth keeping.
124
106
 
125
107
  ## Frontmatter parsing and writing
126
108
 
127
- `parseNote({ path })` (or `parseNoteContent({ content })`) parses a note into a `ParsedNote` carrying typed `Frontmatter`:
109
+ `parseNote({ path })` (or `parseNoteContent({ content })`) parses a note into a `ParsedNote` containing typed `Frontmatter`:
128
110
  The `title`, `recordType`, `created`, `updated`, and `tags` fields are strongly typed and any other fields are preserved in an `extra` map.
129
- `writeFrontmatter({ frontmatter, body })` renders it back to a note string with a fixed field order and flow-style tags; the round trip is idempotent.
130
111
 
131
- Date fields surface as strings, never JS `Date` objects. YAML parse errors are recorded in `ParsedNote.frontmatterRaw.parseError` rather than thrown;
112
+ Date fields are strings, never JS `Date` objects. YAML parse errors are recorded in `ParsedNote.frontmatterRaw.parseError` rather than thrown;
132
113
  missing files (when a path is given) throw.
133
114
 
115
+ Writing is type-blind and lives in `@williamthorsen/kb/note-io`. `readNote(path)` (or `readNoteContent(content)`) splits a note into an insertion-ordered frontmatter field map and a body, and `writeNote(path, fields, body)` (or `renderNote(fields, body)`) renders them back, with string lists in flow style. A note written this way reads back to the same fields and body.
116
+
134
117
  ## Tags
135
118
 
136
119
  `loadAliases({ kbRoot })` reads `.kb/tag-aliases.yaml` into an `AliasMap`, rejecting collisions and self-aliases at load time; an absent file yields an empty map.
137
120
  `canonicalize(tag, aliases)` resolves a tag to its canonical form; `findAliasFor(tag, aliases)` returns the canonical form only when the input is a known alias.
138
121
 
139
- ## Vault integrity and lints
122
+ ## Vault integrity
140
123
 
141
- `checkVaultIntegrity(notes)` runs whole-vault, type-blind checks over a `{ path, body, bodyStartLine }[]` note set: an unresolved `[[link]]` is an error (`wikilinks.unresolved`), and a basename shared by two or more notes is one vault-wide warning (`wikilinks.basename`). `buildVaultIndex(notes)` builds the basename → paths index the layer and curate's wikilink rewriter share.
124
+ `checkVaultIntegrity(notes)` runs whole-vault, type-blind checks over a `{ path, body, bodyStartLine }[]` note set: An unresolved `[[link]]` is an error (`wikilinks.unresolved`), and a basename shared by two or more notes is one vault-wide warning (`wikilinks.basename`). `buildVaultIndex(notes)` builds the basename → paths index shared by the layer and curate's wikilink rewriter.
142
125
 
143
- The type-blind per-note lints — `tagAliasFindings(note, aliases)` (`tag-alias`, warning) and `pathsFindings(note)` (`paths.user-home`, error) catch what write-time record validation can't: alias-vocabulary drift and hardcoded `/Users/{name}/` paths in captured content.
126
+ A second argument, `{ foreignStores, sourceVisibility }`, resolves [store-qualified links](#linking-into-another-store) against the stores that they name; supplied none, the layer treats every target as store-local, which is what `buildVaultIndex`'s other consumers get.
144
127
 
145
- `taxonomyFindings({ notes, taxonomy, config, taxonomyPath })` reports where a store's assertion folders and its declared taxonomy disagree (see [`.kb/taxonomy.yaml`](#the-declared-structure-kbtaxonomyyaml)). Its findings carry `scope: 'vault'`: they describe the store rather than any one note, so a consumer that narrows a report to selected notes must keep them rather than filter them out by path.
128
+ `scanWikilinks(body)` yields each link in a note body, with its store qualifier and target separated, and is the single definition of what counts as a link. `lookupKey(target)` reduces a target to the key that `VaultIndex` uses. The subpath exports `checkVaultIntegrity`, `buildVaultIndex`, `scanWikilinks`, and `lookupKey`, and not the parse primitives from which they are built: A consumer that detects or rewrites links calls `scanWikilinks`.
146
129
 
147
130
  ```ts
148
131
  import { checkVaultIntegrity } from '@williamthorsen/kb/vault-integrity';
@@ -152,7 +135,7 @@ const findings = checkVaultIntegrity(notes);
152
135
 
153
136
  ## Checking a store
154
137
 
155
- `check({ kbRoot })` runs a store's full check in one call: it loads `.kb/config.yaml`, `.kb/tag-aliases.yaml`, and `.kb/taxonomy.yaml`, enumerates the notes that the config selects (narrowed by [the git dimension](#the-git-dimension) inside a working tree), and composes whole-vault integrity and taxonomy drift with the `tag-alias` and `paths` lints. It performs no frontmatter validation record types own that at write time. It returns **both** the enumerated notes and the findings, so a consumer can layer its own detectors over the same enumeration without walking the store twice.
138
+ `check({ kbRoot })` runs a store's full check in one call: It loads `.kb/config.yaml`, `.kb/tag-aliases.yaml`, and `.kb/taxonomy.yaml`, enumerates the notes that the config selects (inside a git working tree, the enumeration drops [ignored notes](#ignored-notes)), and composes whole-vault integrity and taxonomy drift with the `tag-alias` and `paths` lints. It performs no frontmatter validation; record types own that at write time. It returns **both** the enumerated notes and the findings, so that a consumer can layer its own detectors over the same enumeration without walking the store twice.
156
139
 
157
140
  ```ts
158
141
  import { check } from '@williamthorsen/kb/check';
@@ -160,13 +143,15 @@ import { check } from '@williamthorsen/kb/check';
160
143
  const { notes, findings } = await check({ kbRoot });
161
144
  ```
162
145
 
146
+ No subpath exports the lints, so `check`, and the `kb check` command built on it, is the only way to run them. Two type-blind per-note lints catch what write-time record validation can't: `tag-alias` (warning) reports alias-vocabulary drift, and `paths.user-home` (error) reports a hardcoded `/Users/{name}/` path in captured content. The `taxonomy.*` rules, all warnings, report drift between a store's assertion folders and its declared taxonomy (see [`.kb/taxonomy.yaml`](#the-declared-structure-kbtaxonomyyaml)). Their findings have `scope: 'vault'`: They describe the store rather than any one note. A consumer that narrows a report to selected notes must keep them rather than filter them out by path.
147
+
163
148
  A structural defect in any loaded file throws a `KbLoaderError` (see below). Any other error from enumeration or the checks propagates unchanged.
164
149
 
165
150
  `enumerateNotes({ kbRoot, config })` performs the enumeration on its own, and `enumerateNotePaths({ kbRoot, config })` returns the same note set as store-root-relative paths without opening a single note. Both are exported from `@williamthorsen/kb/check`; the paths-only variant serves a caller that needs the note set's shape rather than its content.
166
151
 
167
152
  ### Which notes are checked: `.kb/config.yaml`
168
153
 
169
- `.kb/config.yaml` configures which notes a check enumerates. Both keys are optional; an absent file or an omitted key falls back to the default.
154
+ `.kb/config.yaml` configures which notes a check enumerates and how widely the store is published. Every key is optional; an absent file or an omitted key falls back to the default.
170
155
 
171
156
  ```yaml
172
157
  # .kb/config.yaml
@@ -174,24 +159,48 @@ targets:
174
159
  - 'content/**/*.md'
175
160
  exclude:
176
161
  - '**/node_modules/**'
162
+ visibility: private
177
163
  ```
178
164
 
179
- | Key | Default | Meaning |
180
- | --------- | ------------------------ | ---------------------------------------------------------------------------- |
181
- | `targets` | `['content/**/*.md']` | Glob patterns (store-root-relative) selecting which notes a check enumerates |
182
- | `exclude` | `['**/node_modules/**']` | Glob patterns excluded from enumeration even when a target matches |
165
+ | Key | Default | Meaning |
166
+ | ------------ | ------------------------ | ---------------------------------------------------------------------------- |
167
+ | `targets` | `['content/**/*.md']` | Glob patterns (store-root-relative) selecting which notes a check enumerates |
168
+ | `exclude` | `['**/node_modules/**']` | Glob patterns excluded from enumeration even when a target matches |
169
+ | `visibility` | `private` | `shared` or `private`; decides which stores may link into this one |
170
+
171
+ `visibility` belongs here rather than in the registry entry because it is intrinsic to the store: One store cloned on two machines has one visibility, whereas a per-machine declaration could let the two disagree silently. It governs [linking into another store](#linking-into-another-store).
183
172
 
184
173
  Matching uses dotfile-insensitive globbing, so dot-directories (`.kb`, `.git`, `.agents`) are skipped without naming them. The default targets the `content/`-scoped layout; a store with a different layout overrides `targets` to match. `loadKbConfig({ kbRoot })` returns the effective config and is exported from `@williamthorsen/kb/config`.
185
174
 
186
- #### The git dimension
175
+ #### Ignored notes
176
+
177
+ When the store is in a git working tree, the repository's ignore rules narrow the selection further. A note is enumerated when git tracks it, or when no ignore rule covers it. A note that the repository ignores is therefore neither checked nor available as a wikilink target, so a link pointing at one reports `wikilinks.unresolved`. That is the correct reading: Such a link is broken for every clone but the author's. This lets a store gitignore a scratch area (`local/`, `*.local.md`) and keep uncommitted notes there without the store's lints gating them.
178
+
179
+ A store outside a git working tree, or a machine with no git, keeps the filesystem walk alone. When the repository ignores every note that the walk found, which happens when a parent repository ignores the store's own directory, the run says so on stderr rather than reporting an empty note set as clean.
180
+
181
+ ### Linking into another store
182
+
183
+ A wikilink names a store by qualifying its target: `[[fde:Note title]]` resolves `Note title` in the store registered as `fde`, whereas a bare `[[Note title]]` stays inside the store being checked. A qualifier is recognized only when the text before the first colon is non-empty and contains no whitespace and no `/`, so a title that happens to contain a colon resolves whole.
187
184
 
188
- Where the store sits in a git working tree, `targets`/`exclude` is not the whole of note scope: what git accounts for narrows it further. A note is enumerated when git tracks it, or when git would track it, meaning no ignore rule covers it. A note that the repository ignores is therefore neither checked nor available as a wikilink target, so a link pointing at one reports `wikilinks.unresolved`. That is the correct reading: such a link is broken for every clone but the author's. This is what lets a store gitignore a scratch area (`local/`, `*.local.md`) and keep uncommitted notes there without the store's lints gating them.
185
+ Direction is decided by `visibility`: A link may point at a store as shareable as its own or more so, never at a less shareable one. A private note may therefore link to the share-safe assertion from which its detail was stripped, while the reverse is refused, because a note's title tends to be its claim and a link into a private store discloses that claim through the link itself.
189
186
 
190
- A store outside a git working tree, or a machine carrying no git, keeps the filesystem walk alone. Where git accounts for none of the notes that the walk found, which happens when a parent repository ignores the store's own directory, the run says so on stderr rather than reporting a clean bill over an empty note set.
187
+ Only the stores that a run's own links name are consulted, and each is enumerated under its own `targets`/`exclude` and its own repository's ignore rules, reading note paths alone. A run whose links qualify no store reads no registry.
188
+
189
+ | Rule | Severity | Reported when |
190
+ | ------------------------------- | -------- | ----------------------------------------------------------------------------------- |
191
+ | `wikilinks.disallowed-store` | error | The named store is less shareable than the one being checked |
192
+ | `wikilinks.registry-unloadable` | error | A run with qualified links could not load `kb.yaml`; vault-scoped, reported once |
193
+ | `wikilinks.store-unavailable` | warning | The named store is registered but could not be read here, so the link is unverified |
194
+ | `wikilinks.unknown-store` | error | No `kb.yaml` entry declares the named store |
195
+ | `wikilinks.unresolved` | error | The named store contains no note of that basename |
196
+
197
+ `wikilinks.store-unavailable` is a warning rather than an error because a store absent from this machine leaves its links unverifiable rather than broken: A correct link should not fail a check run on a machine that has not cloned the target.
198
+
199
+ A run that reports `wikilinks.registry-unloadable` reports nothing per link. No store was looked up, so whether one is registered is undetermined, and naming each link would send the reader to fix a registration that may already be correct.
191
200
 
192
201
  ### The declared structure: `.kb/taxonomy.yaml`
193
202
 
194
- `.kb/taxonomy.yaml` states where a store's assertions are meant to live. It is the source of truth for intended structure: folders on disk are derived from it, not the reverse. It governs `content/assertions/` only, since `content/events/` is flat and ULID-keyed.
203
+ `.kb/taxonomy.yaml` states where a store's assertions are meant to live. It is the source of truth for intended structure: Folders on disk are derived from it, not the reverse. It governs `content/assertions/` only, since `content/events/` is flat and ULID-keyed.
195
204
 
196
205
  ```yaml
197
206
  # .kb/taxonomy.yaml
@@ -203,25 +212,25 @@ provisional:
203
212
  languages:
204
213
  ```
205
214
 
206
- Two disjoint maps of domain path to one-line description. `domains` holds reviewed declarations and `provisional` holds those declared but not yet reviewed; promotion is writing a description and moving the line up. A domain may be declared without a description, as `languages` is above.
215
+ Two disjoint maps of domain path to one-line description. `domains` contains reviewed declarations and `provisional` contains those declared but not yet reviewed; promotion is writing a description and moving the line up. A domain may be declared without a description, as `languages` is above.
207
216
 
208
- Keys are relative to `content/assertions/` and may nest to any depth. Parents are not implied: declaring `engineering/tooling` does not declare `engineering`. A path declared in both maps fails the load, as does a malformed key one restating the `content/assertions/` prefix, or carrying a leading or trailing slash, an empty segment, or a `.`/`..` segment.
217
+ Keys are relative to `content/assertions/` and may nest to any depth. Parents are not implied: declaring `engineering/tooling` does not declare `engineering`. A path declared in both maps fails the load, as does a malformed key: one that restates the `content/assertions/` prefix, or that has a leading or trailing slash, an empty segment, or a `.`/`..` segment.
209
218
 
210
219
  An absent taxonomy, and one present but declaring nothing, are both valid and report nothing, so the rules apply only to a store that has adopted a taxonomy. Three warnings report drift once one has:
211
220
 
212
- | Rule | Meaning |
213
- | --------------------- | ----------------------------------------------- |
214
- | `taxonomy.undeclared` | A folder holds notes but no domain declares it. |
215
- | `taxonomy.unused` | A declared domain has no note at or beneath it. |
216
- | `taxonomy.orphan` | A declared domain's parent is undeclared. |
221
+ | Rule | Meaning |
222
+ | --------------------- | -------------------------------------------------- |
223
+ | `taxonomy.undeclared` | A folder contains notes but no domain declares it. |
224
+ | `taxonomy.unused` | A declared domain has no note at or beneath it. |
225
+ | `taxonomy.orphan` | A declared domain's parent is undeclared. |
217
226
 
218
- A domain counts as used when any note lives at or beneath it, so a grouping domain that holds only subfolders is not reported unused. A domain inside a `config.exclude` subtree is exempt from `taxonomy.unused`, since its notes never enumerate.
227
+ A domain counts as used when any note lives at or beneath it, so a grouping domain that contains only subfolders is not reported unused. A domain inside a `config.exclude` subtree is exempt from `taxonomy.unused`, since its notes are never enumerated.
219
228
 
220
- `loadTaxonomy({ kbRoot })` reads both blocks into one map of domain path to `{ description, provisional }`, and `writeTaxonomy({ kbRoot, declarations })` declares domains while preserving the file's existing comments, key order, and formatting. `resolveDomain(relativePath)` maps a store-root-relative note path to the domain it sits in (`undefined` for a non-assertion or a note at the assertions root), and `resolveParent(path)` yields a domain's parent (`undefined` at the top level); the drift rules and the back-fill both derive their answers from this pair, so a consumer that classifies notes against the taxonomy stays in agreement with what `kb check` reports. All four are exported from `@williamthorsen/kb/taxonomy`.
229
+ `loadTaxonomy({ kbRoot })` reads both blocks into one map of domain path to `{ description, provisional }`, and `writeTaxonomy({ kbRoot, declarations })` declares domains while preserving the file's existing comments, key order, and formatting. `resolveDomain(relativePath)` maps a store-root-relative note path to the domain that contains it (`undefined` for a non-assertion or a note at the assertions root), and `resolveParent(path)` yields a domain's parent (`undefined` at the top level). Because the drift rules and the back-fill both derive their answers from this pair, a consumer that classifies notes against the taxonomy stays in agreement with what `kb check` reports. All four are exported from `@williamthorsen/kb/taxonomy`.
221
230
 
222
231
  ## The `kb` command
223
232
 
224
- The package ships a `kb` bin with five subcommands: `check`, `create`, `scaffold`, `set-default`, and `taxonomy`.
233
+ The package provides a `kb` bin with five subcommands: `check`, `create`, `scaffold`, `set-default`, and `taxonomy`.
225
234
 
226
235
  ### kb create
227
236
 
@@ -246,13 +255,13 @@ It creates these files and directories:
246
255
 
247
256
  The check config is serialized from the in-package `defaultKbConfig` and the Prettier config from `canonicalPrettierConfig`, so a new store cannot drift from the bundled values.
248
257
 
249
- The name defaults to the directory's base name; `--name` overrides it and `--no-register` scaffolds without writing the registry. `--description` sets the new entry's description, and requires registration: combining it with `--no-register` is a usage error. The registry write preserves any existing comments in `kb.yaml` and leaves the `kbs:` entries alphabetically ordered, so a registry that has drifted out of order is tidied as stores are added. `kb create` refuses to clobber: it exits 2 if the directory already contains a `.kb/` store, or if the chosen name is already registered. Use `kb scaffold` to add canonical files to a store that already exists.
258
+ The name defaults to the directory's base name; `--name` overrides it and `--no-register` scaffolds without writing the registry. `--description` sets the new entry's description, and requires registration: combining it with `--no-register` is a usage error. The registry write preserves any existing comments in `kb.yaml` and leaves the `kbs:` entries alphabetically ordered, which tidies a registry that has drifted out of order as stores are added. `kb create` refuses to overwrite: It exits 2 if the directory already contains a `.kb/` store, or if the chosen name is already registered. Use `kb scaffold` to add canonical files to a store that already exists.
250
259
 
251
- `kb create` also keeps a default knowledge base set. When the registry's top-level `default_kb` pointer is unset and the new store is the only registered KB, it becomes the default. When other KBs are already registered with no default, `kb create` prompts you to choose one on an interactive terminal — or, when stdin is not interactive, points you to `kb set-default`. An existing `default_kb` is never overwritten.
260
+ `kb create` also keeps a default knowledge base set. When the registry's top-level `default_kb` pointer is unset and the new store is the only registered KB, it becomes the default. When other KBs are already registered with no default, `kb create` prompts for one on an interactive terminal, and points to `kb set-default` when stdin is not interactive. An existing `default_kb` is never overwritten.
252
261
 
253
262
  ### kb scaffold
254
263
 
255
- `kb scaffold` writes into an existing knowledge base any canonical file that it lacks, so a store created before a given file existed can acquire it.
264
+ `kb scaffold` writes into an existing knowledge base any canonical file that it lacks, so that a store created before a given file existed can acquire it.
256
265
 
257
266
  ```bash
258
267
  kb scaffold # back-fill the nearest ancestor .kb/ store
@@ -260,11 +269,11 @@ kb scaffold --kb coding # back-fill the named store from the kb.yaml regist
260
269
  kb scaffold --force # replace every canonical file with a fresh seed
261
270
  ```
262
271
 
263
- The canonical set is the one that `kb create` writes, defined once and shared by both commands so neither can drift from the other. `.kb/taxonomy.yaml` is not part of it: `kb taxonomy init` derives that file's content from the notes a store already holds rather than writing a fixed template.
272
+ The canonical set is the one that `kb create` writes, defined once and shared by both commands so that neither can drift from the other. `.kb/taxonomy.yaml` is not part of it: `kb taxonomy init` derives that file's content from the notes that a store already contains rather than writing a fixed template.
264
273
 
265
274
  An existing file is left untouched unless `--force` is given, which replaces it with a fresh seed and discards any edits. A directory has no content to replace, so `--force` governs files alone. The command reports each canonical path as `created`, `present`, or `replaced`.
266
275
 
267
- Store resolution matches `kb check`: the nearest ancestor `.kb/` directory, or a `--kb <name>` entry in the merged `kb.yaml` registry. Three grounds exit 2: no store resolves, the registry marks the resolved store `readonly`, or the resolved path holds no `.kb/`. The last keeps the command to back-filling a store rather than creating one, which is `kb create`'s job; a registry entry names a path without proving a store is there.
276
+ Store resolution matches `kb check`: the nearest ancestor `.kb/` directory, or a `--kb <name>` entry in the merged `kb.yaml` registry. The command exits 2 on three grounds: no store resolves, the registry marks the resolved store `readonly`, or the resolved path contains no `.kb/`. The last keeps the command to back-filling a store rather than creating one, which is `kb create`'s job; a registry entry names a path without proving a store is there.
268
277
 
269
278
  ### kb set-default
270
279
 
@@ -276,7 +285,7 @@ kb set-default --none # clear default_kb
276
285
  kb set-default # list the registered KBs and choose interactively
277
286
  ```
278
287
 
279
- With a name, it sets `default_kb` to that KB, exiting 2 if the name is not registered. With `--none`, it clears the pointer. With no arguments on an interactive terminal, it lists the registered KBs marking the current default and offering a `(none)` option and writes the choice; cancelling with an empty line leaves the registry unchanged. With no arguments on a non-interactive stdin, it exits 2 rather than hanging. Writes resolve against and target the user-global registry only, and preserve existing comments and formatting.
288
+ With a name, it sets `default_kb` to that KB, exiting 2 if the name is not registered. With `--none`, it clears the pointer. With no arguments on an interactive terminal, it lists the registered KBs, marking the current default and offering a `(none)` option, and writes the choice; cancelling with an empty line leaves the registry unchanged. With no arguments on a non-interactive stdin, it exits 2 rather than hanging. Writes resolve against and target the user-global registry only, and preserve existing comments and formatting.
280
289
 
281
290
  ### kb check
282
291
 
@@ -287,18 +296,18 @@ kb check # check every note in the nearest ancestor .kb/ s
287
296
  kb check --kb coding # check the named store from the kb.yaml registry
288
297
  kb check --json # emit a JSON report
289
298
  kb check content/assertions # check only the notes under a directory
290
- kb check 'content/**/*.md' # check only the notes a glob matches (quote it)
299
+ kb check 'content/**/*.md' # check only the notes that a glob matches (quote it)
291
300
  kb check --vs=main # check only the notes changed since a ref
292
301
  ```
293
302
 
294
- `kb check` resolves the store from the nearest ancestor `.kb/` directory, or from a `--kb <name>` entry in the merged `kb.yaml` registry (project-local entries join the user-global registry). The default output groups findings by file; `--json` emits `{ store, summary, findings }`. The command is read-only and never writes to the store.
303
+ `kb check` resolves the store from the nearest ancestor `.kb/` directory, or from a `--kb <name>` entry in the merged `kb.yaml` registry (project-local entries are merged into the user-global registry). The default output groups findings by file; `--json` emits `{ store, summary, findings }`. The command is read-only and never writes to the store.
295
304
 
296
- **Targeting.** Path arguments and `--vs` each scope the run to a subset of notes; they are mutually exclusive, and both compose with `--kb` and `--json`. Cross-note rules always resolve against the whole vault, so a targeted run never false-flags a link to an unselected note; only the report and the exit code narrow to the selection.
305
+ **Targeting.** Path arguments and `--vs` each scope the run to a subset of notes; they are mutually exclusive, and both compose with `--kb` and `--json`. Cross-note rules always resolve against the whole store, and a store-qualified link against the whole store that it names, so a targeted run never misreports a link to an unselected note; only the report and the exit code narrow to the selection.
297
306
 
298
- - **`[paths...]`**: one or more glob patterns, files, or directories (store-root-relative). The command expands globs itself, so a quoted glob behaves the same as a shell-expanded one. A directory checks every note beneath it. A path that matches no note is a usage error, unless it names a real non-note (a README, a triage note, or a file that `exclude` or git rules out), which is skipped silently.
299
- - **`--vs <ref>`**: the notes changed between the working tree and the merge-base of `<ref>` and HEAD. The diff follows renames (checking the destination), includes uncommitted edits to tracked notes, and excludes deletions, so a `git mv`-heavy migration batch reports the notes it actually touched.
307
+ - **`[paths...]`**: One or more glob patterns, files, or directories (store-root-relative). Because the command expands globs itself, a quoted glob behaves the same as a shell-expanded one. A directory checks every note beneath it. A path that matches no note is a usage error, unless it names a real non-note (a README, a triage note, or a file that `exclude` or git rules out), which is skipped silently.
308
+ - **`--vs <ref>`**: The notes changed between the working tree and the merge-base of `<ref>` and HEAD. The diff follows renames (checking the destination), includes uncommitted edits to tracked notes, and excludes deletions, so a `git mv`-heavy migration batch reports the notes that it actually touched.
300
309
 
301
- Because the exit code reflects only the selected notes, a per-batch or pre-commit gate can pass while the rest of the vault still carries a migration backlog.
310
+ Because the exit code reflects only the selected notes, a per-batch or pre-commit gate can pass while the rest of the vault still has a migration backlog.
302
311
 
303
312
  Exit codes:
304
313
 
@@ -308,19 +317,19 @@ Exit codes:
308
317
  | `1` | One or more error-severity findings in the checked notes. |
309
318
  | `2` | A usage error, an unresolvable store or `--vs` ref, a path matching no note, or a malformed `config`, `tag-aliases`, or `taxonomy` file. |
310
319
 
311
- A finding carrying `scope: 'vault'` describes the store rather than any one note, so it is reported under every run, including a targeted one, a `--vs` one, and one that matched no notes at all. The taxonomy rules are the ones that produce them.
320
+ A finding with `scope: 'vault'` describes the store rather than any one note, so it is reported under every run, including a targeted one, a `--vs` one, and one that matched no notes at all. The taxonomy rules are the ones that produce them.
312
321
 
313
322
  ### kb taxonomy
314
323
 
315
- `kb taxonomy init` derives a starting taxonomy from the notes a store already holds, so a taxonomy can be introduced to a populated store without every folder reporting as undeclared.
324
+ `kb taxonomy init` derives a starting taxonomy from the notes that a store already contains, so that a taxonomy can be introduced to a populated store without every folder being reported as undeclared.
316
325
 
317
326
  ```bash
318
- kb taxonomy init # declare every folder holding notes, and its ancestors
327
+ kb taxonomy init # declare every folder containing notes, and its ancestors
319
328
  kb taxonomy init --kb coding # back-fill the named store from the kb.yaml registry
320
- kb taxonomy init --merge # add only the domains an existing taxonomy omits
329
+ kb taxonomy init --merge # add only the domains that an existing taxonomy omits
321
330
  ```
322
331
 
323
- Every derived domain lands under `provisional:` with no description: the command cannot invent descriptions, and provisional already means "declared, not yet reviewed". Because the derivation reads the same enumeration `kb check` does, a back-filled store reports no taxonomy drift.
332
+ Every derived domain is written under `provisional:` with no description: The command cannot invent descriptions, and provisional already means "declared, not yet reviewed". Because the derivation reads the same enumeration that `kb check` reads, a back-filled store reports no taxonomy drift.
324
333
 
325
334
  Without `--merge`, a store that already declares a taxonomy is left untouched and the command exits 2.
326
335
 
@@ -333,11 +342,11 @@ prettier --write . # format the store
333
342
  prettier --check . # report drift without writing
334
343
  ```
335
344
 
336
- Prettier 3 reads `.gitignore` and `.prettierignore` by default, so neither command needs a flag to skip what the repository ignores, and it loads a config file from a directory holding no `package.json`.
345
+ Prettier 3 reads `.gitignore` and `.prettierignore` by default, so neither command needs a flag to skip what the repository ignores, and it loads a config file from a directory containing no `package.json`.
337
346
 
338
347
  ### The two config files
339
348
 
340
- `.editorconfig` carries everything that more than one tool reads:
349
+ `.editorconfig` contains everything that more than one tool reads:
341
350
 
342
351
  ```ini
343
352
  root = true
@@ -359,36 +368,32 @@ Prettier reads this file, and so does every editor that supports EditorConfig, w
359
368
 
360
369
  The `[*.md]` exemption is the one entry that is not self-explanatory. Two trailing spaces are a hard line break in Markdown, and Prettier preserves them; an editor trimming trailing whitespace on save would destroy a break that the formatter deliberately keeps.
361
370
 
362
- `.prettierrc.yaml` carries the one setting that `.editorconfig` has no key for:
371
+ `.prettierrc.yaml` contains the one setting that `.editorconfig` has no key for:
363
372
 
364
373
  ```yaml
365
374
  embeddedLanguageFormatting: off
366
375
  ```
367
376
 
368
- It is not stylistic, and the scaffolded file carries this reasoning in its own header so it travels with the store. `embeddedLanguageFormatting: off` leaves a note's YAML frontmatter unformatted. Formatted, a long `tags` or `addressed-by` list breaks across several lines, which `writeFrontmatter` puts back onto one the next time anything writes the note. Without this option the formatter and the note writer rewrite each other's output without end, and every note carrying a list past the print width churns on each pass.
377
+ It is not stylistic, and the scaffolded file states this reasoning in its own header so that the reasoning stays with the store. `embeddedLanguageFormatting: off` leaves a note's YAML frontmatter unformatted. Formatted, a long `tags` or `addressed-by` list breaks across several lines, which `renderNote` puts back onto one the next time anything writes the note. Without this option the formatter and the note writer rewrite each other's output without end, and every note with a list past the print width is rewritten on each pass.
369
378
 
370
- A store that must keep a file that it cannot format, such as a lockfile or a fixture whose defect is the point, names it in a `.prettierignore`. That file is the escape hatch and is not scaffolded, since a fresh store has nothing to put in it.
379
+ A store that must keep a file that it cannot format, such as a lockfile or a fixture whose defect is the point, names it in a `.prettierignore`. That file is the one exemption and is not scaffolded, since a fresh store has nothing to put in it.
371
380
 
372
381
  ### Migrating a store off a `package.json` toolchain
373
382
 
374
- A store that carries a `package.json` only to obtain Prettier can retire it.
383
+ A store that keeps a `package.json` only to obtain Prettier can retire it.
375
384
 
376
385
  1. Install Prettier where the store is edited, if it is not already there: `pnpm add --global prettier`.
377
- 2. Run `kb scaffold` in the store to write `.editorconfig` and `.prettierrc.yaml`. An `.editorconfig` the store already has is reported as `present` and left alone, so compare it against the canonical file above and reconcile the two by hand; `kb scaffold --force` replaces it outright.
386
+ 2. Run `kb scaffold` in the store to write `.editorconfig` and `.prettierrc.yaml`. An `.editorconfig` that the store already has is reported as `present` and left alone, so compare it against the canonical file above and reconcile the two by hand; `kb scaffold --force` replaces it outright.
378
387
  3. Delete `package.json`, the lockfile, `pnpm-workspace.yaml`, `.npmrc`, and any `eslint.config.*`, `tsconfig.json`, and dependency-upgrade config. Delete the superseded `.prettierrc.*` too; keep `.prettierignore` if it names anything still present.
379
- 4. Check that `.editorconfig` still governs the width. Prettier reads it for every key the Prettier config leaves unset, so a store whose `max_line_length` disagrees with the canonical 120 formats to its own value, and two stores that disagree format differently.
388
+ 4. Check that `.editorconfig` still governs the width. Prettier reads it for every key that the Prettier config leaves unset, so a store whose `max_line_length` disagrees with the canonical 120 formats to its own value, and two stores that disagree format differently.
380
389
  5. Repoint the pre-commit hook. A hook running `pnpm exec prettier --write {staged_files}` becomes `prettier --write {staged_files}`. Under lefthook, `stage_fixed: true` continues to apply. Note that lefthook was installed by `package.json`'s `prepare` script, so it now needs installing on the machine and enabling in the store with `lefthook install`.
381
390
  6. Repoint CI. A workflow calling `pnpm run check` needs a command that assumes no `package.json`: `prettier --check .`, plus `kb check` for the store's own rules.
382
- 7. Format once: `prettier --write .`. Commit the result on its own, so the reformatting does not obscure later diffs.
391
+ 7. Format once: `prettier --write .`. Commit the result on its own, so that the reformatting does not obscure later diffs.
383
392
 
384
- A store carrying its own note checker deserves one more step before it is retired: compare its rules against what `kb check` covers, and port anything missing. `kb check` validates wikilinks, tag aliases, hardcoded home paths, and taxonomy drift, and deliberately leaves frontmatter validity to the record types that own it at write time.
393
+ A store with its own note checker needs one more step before it is retired: compare its rules against what `kb check` covers, and port anything missing. `kb check` validates wikilinks, tag aliases, hardcoded home paths, and taxonomy drift, and deliberately leaves frontmatter validity to the record types that own it at write time.
385
394
 
386
- A store with no `package.json` runs steps 1, 2, and 7, and skips 3 through 6: it has nothing to retire, only Prettier to install, the config to adopt, and a first format to run.
395
+ A store with no `package.json` runs steps 1, 2, and 7, and skips 3 through 6: It has nothing to retire, only Prettier to install, the config to adopt, and a first format to run.
387
396
 
388
397
  ## Error and exception model
389
398
 
390
- The checks **return** findings; they never throw. Loaders (`loadKbConfig`, `loadAliases`, `loadTaxonomy`) **throw** a typed `KbLoaderError` on structural defects or malformed YAML, with the offending file path named in the message. `KbLoaderError` (exported from `@williamthorsen/kb/config`) carries a `kind: 'KbLoaderError'` discriminant and an `isKbLoaderError` type guard so a caller can distinguish a recoverable config or alias defect from any other throw. An underlying failure, such as a YAML parse error, is attached as the thrown error's `cause`. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
391
-
392
- ## MCP wrappability
393
-
394
- Every public function input is a plain object with primitive or `unknown`-typed fields, and no function takes a callback. A future `kb-mcp` server can bind Zod-validated request payloads directly onto these inputs without refactoring.
399
+ The checks **return** findings; they never throw. Loaders (`loadKbConfig`, `loadAliases`, `loadTaxonomy`) **throw** a typed `KbLoaderError` on structural defects or malformed YAML, with the offending file path named in the message. `KbLoaderError` (exported from `@williamthorsen/kb/config`) declares a `kind: 'KbLoaderError'` discriminant, paired with an `isKbLoaderError` type guard, so that a caller can distinguish a recoverable config or alias defect from any other throw. An underlying failure, such as a YAML parse error, is attached as the thrown error's `cause`. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
package/bin/kb.js CHANGED
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Imports only node builtins: a top-level import resolves before the gate below runs, so an
3
+ // Imports only node builtins: A top-level import resolves before the gate below runs, so an
4
4
  // unresolvable dependency would replace this file's build-first message with ERR_MODULE_NOT_FOUND.
5
5
  import { existsSync } from 'node:fs';
6
6
 
7
- // Thin wrapper so pnpm can symlink the bin at install time, before `dist/` exists.
8
- // The real entry point loads at runtime from the build output.
9
- // See packages/agents/README.md ("Bin wrapper pattern") for details.
7
+ // See packages/agents/README.md ("Bin wrapper pattern") for why this indirection exists.
10
8
  const entryPoint = new URL('../dist/esm/cli/index.js', import.meta.url);
11
9
 
12
10
  // Gate on the entry file itself: Node raises ERR_MODULE_NOT_FOUND for any unresolved
@@ -1,16 +1,14 @@
1
1
  import type { KbConfig } from '../config/config-schema.js';
2
2
  import type { Finding } from '../types.js';
3
3
  import { type EnumeratedNote } from './enumerate.js';
4
- /** The result of a check run: the effective config, every enumerated note, and every finding the checks produced. */
5
4
  export interface CheckResult {
6
- /** The effective `KbConfig` the run used loaded from `.kb/config.yaml`, or `defaultKbConfig` when absent. */
5
+ /** The effective `KbConfig` that the run used: loaded from `.kb/config.yaml`, or `defaultKbConfig` when absent. */
7
6
  config: KbConfig;
8
- /** Every note that the store's `config.targets` and, inside a git working tree, its git scope admit, in walk order. */
9
- notes: readonly EnumeratedNote[];
10
7
  /**
11
- * Findings from whole-vault integrity (unresolved links, basename collisions), taxonomy drift, and the tag-alias and
12
- * paths lints.
8
+ * Every note selected by the store's `config.targets` and, inside a git working tree, not ignored by the repository,
9
+ * in walk order.
13
10
  */
11
+ notes: readonly EnumeratedNote[];
14
12
  findings: readonly Finding[];
15
13
  }
16
14
  /**
@@ -19,17 +17,21 @@ export interface CheckResult {
19
17
  * the type-blind per-note lints across them. Frontmatter validity is owned by the record types at write time, so no
20
18
  * frontmatter re-validation runs here.
21
19
  *
22
- * Inside a git working tree the enumeration narrows to what git accounts for, so a note the repository ignores is
23
- * neither checked nor available as a wikilink target; see {@link enumerateNotes} for the rule.
20
+ * A `[[store:Target]]` link resolves against the store named by its prefix rather than this one. Those stores are looked
21
+ * up in the merged `kb.yaml` registry, which is read from `~/.agents/kb.yaml` and from `cwd`'s project-local registry.
22
+ * Because `cwd` defaults to the store root, a caller that supplies none still resolves against the user-global
23
+ * registry. Only the stores that this store's own links name are consulted.
24
24
  *
25
- * Returns the effective config alongside the enumerated notes and findings, so a consumer (e.g. `kb-curate`) can layer
26
- * its own detectors over the same enumeration without walking the tree twice, and can read the resolved
27
- * `targets`/`exclude` without re-loading `.kb/config.yaml`.
25
+ * Returns the effective config alongside the enumerated notes and findings, so that a consumer can layer its own
26
+ * detectors over the same enumeration without walking the tree twice, and can read the resolved `targets`/`exclude`
27
+ * without re-loading `.kb/config.yaml`.
28
28
  *
29
29
  * A structural defect in any loaded file throws a `KbLoaderError` (the loaders' own contract); the caller decides how
30
- * to surface it. Any other error from enumeration or the checks propagates unchanged it is never relabeled as a
30
+ * to report it. Any other error from enumeration or the checks propagates unchanged; it is never relabeled as a
31
31
  * config defect.
32
32
  */
33
33
  export declare function check(input: {
34
34
  kbRoot: string;
35
+ cwd?: string;
36
+ home?: string;
35
37
  }): Promise<CheckResult>;
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { loadKbConfig } from "../config/load-config.js";
3
+ import { tryLoadKbRegistry } from "../discovery/load-registry.js";
3
4
  import { resolveKbDir, TAXONOMY_FILE } from "../layout/index.js";
4
5
  import { pathsFindings } from "../lints/paths.js";
5
6
  import { tagAliasFindings } from "../lints/tag-alias.js";
@@ -8,6 +9,7 @@ import { loadAliases } from "../tags/load-aliases.js";
8
9
  import { loadTaxonomy } from "../taxonomy/load-taxonomy.js";
9
10
  import { checkVaultIntegrity } from "../vault-integrity/check-vault-integrity.js";
10
11
  import { enumerateNotes } from "./enumerate.js";
12
+ import { collectStorePrefixes, resolveForeignStores } from "./resolve-foreign-stores.js";
11
13
  export async function check(input) {
12
14
  const kbRoot = { path: input.kbRoot, kbDir: resolveKbDir(input.kbRoot) };
13
15
  const [config, aliases, taxonomy] = await Promise.all([
@@ -16,10 +18,48 @@ export async function check(input) {
16
18
  loadTaxonomy({ kbRoot }),
17
19
  ]);
18
20
  const notes = await enumerateNotes({ kbRoot: input.kbRoot, config });
21
+ const qualified = await resolveQualifiedStores({
22
+ notes,
23
+ projectDir: input.cwd ?? input.kbRoot,
24
+ sourceVisibility: config.visibility,
25
+ ...(input.home !== undefined && { home: input.home }),
26
+ });
19
27
  const findings = [
20
- ...checkVaultIntegrity(notes),
28
+ ...registryFindings(qualified.registryError),
29
+ ...checkVaultIntegrity(notes, { foreignStores: qualified.foreignStores, sourceVisibility: config.visibility }),
21
30
  ...taxonomyFindings({ notes, taxonomy, config, taxonomyPath: join(input.kbRoot, TAXONOMY_FILE) }),
22
31
  ...notes.flatMap((note) => [...tagAliasFindings(note, aliases), ...pathsFindings(note)]),
23
32
  ];
24
33
  return { config, notes, findings };
25
34
  }
35
+ function registryFindings(error) {
36
+ if (error === undefined)
37
+ return [];
38
+ return [
39
+ {
40
+ path: KB_REGISTRY_LABEL,
41
+ scope: 'vault',
42
+ rule: 'wikilinks.registry-unloadable',
43
+ severity: 'error',
44
+ message: `no store-qualified link can resolve: ${error}`,
45
+ },
46
+ ];
47
+ }
48
+ async function resolveQualifiedStores(input) {
49
+ const prefixes = collectStorePrefixes(input.notes);
50
+ if (prefixes.size === 0)
51
+ return { foreignStores: new Map() };
52
+ const registry = await tryLoadKbRegistry({
53
+ projectDir: input.projectDir,
54
+ ...(input.home !== undefined && { home: input.home }),
55
+ });
56
+ if (registry.error !== undefined)
57
+ return { foreignStores: new Map(), registryError: registry.error };
58
+ const foreignStores = await resolveForeignStores({
59
+ prefixes,
60
+ registry: registry.config,
61
+ sourceVisibility: input.sourceVisibility,
62
+ });
63
+ return { foreignStores };
64
+ }
65
+ const KB_REGISTRY_LABEL = 'kb.yaml';