@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.
- package/README.md +101 -96
- package/bin/kb.js +2 -4
- package/dist/esm/check/check.d.ts +14 -12
- package/dist/esm/check/check.js +41 -1
- package/dist/esm/check/enumerate.d.ts +13 -16
- package/dist/esm/check/glob-segments.d.ts +1 -1
- package/dist/esm/check/resolve-foreign-stores.d.ts +25 -0
- package/dist/esm/check/resolve-foreign-stores.js +50 -0
- package/dist/esm/cli/commands/check.d.ts +4 -10
- package/dist/esm/cli/commands/check.js +8 -3
- package/dist/esm/cli/commands/create.d.ts +3 -7
- package/dist/esm/cli/commands/create.js +1 -1
- package/dist/esm/cli/commands/scaffold.d.ts +5 -9
- package/dist/esm/cli/commands/scaffold.js +5 -5
- package/dist/esm/cli/commands/set-default.d.ts +1 -5
- package/dist/esm/cli/commands/set-default.js +1 -1
- package/dist/esm/cli/commands/taxonomy.d.ts +5 -10
- package/dist/esm/cli/commands/taxonomy.js +10 -9
- package/dist/esm/cli/format.d.ts +8 -10
- package/dist/esm/cli/resolve-store.d.ts +8 -8
- package/dist/esm/cli/run.d.ts +4 -6
- package/dist/esm/cli/run.js +1 -1
- package/dist/esm/cli/select-kb-prompt.d.ts +6 -5
- package/dist/esm/cli/select-kb-prompt.js +1 -1
- package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +3 -3
- package/dist/esm/cli/targeting/select-notes.d.ts +2 -2
- package/dist/esm/config/config-schema.d.ts +24 -6
- package/dist/esm/config/config-schema.js +9 -0
- package/dist/esm/config/index.d.ts +1 -1
- package/dist/esm/config/index.js +1 -1
- package/dist/esm/config/kb-loader-error.d.ts +3 -3
- package/dist/esm/config/load-config.d.ts +3 -3
- package/dist/esm/config/load-config.js +2 -1
- package/dist/esm/config/note-scope.d.ts +7 -7
- package/dist/esm/create/create.d.ts +8 -8
- package/dist/esm/discovery/kb-registry-schema.d.ts +0 -2
- package/dist/esm/discovery/load-registry.d.ts +7 -9
- package/dist/esm/discovery/load-registry.js +1 -1
- package/dist/esm/discovery/register-store.d.ts +3 -4
- package/dist/esm/discovery/register-store.js +2 -2
- package/dist/esm/discovery/registry-document.d.ts +2 -2
- package/dist/esm/discovery/set-default-kb.d.ts +5 -5
- package/dist/esm/discovery/set-default-kb.js +3 -3
- package/dist/esm/filesystem/exists.d.ts +4 -4
- package/dist/esm/frontmatter/parse-note.d.ts +3 -3
- package/dist/esm/git/list-git-scope.d.ts +7 -7
- package/dist/esm/git/run-git.d.ts +2 -2
- package/dist/esm/layout/store-layout.d.ts +5 -5
- package/dist/esm/lints/paths.d.ts +1 -1
- package/dist/esm/lints/paths.js +1 -1
- package/dist/esm/lints/tag-alias.d.ts +1 -1
- package/dist/esm/lints/tag-alias.js +1 -1
- package/dist/esm/lints/taxonomy.d.ts +9 -8
- package/dist/esm/lints/taxonomy.js +2 -2
- package/dist/esm/note-io/field-validators.d.ts +1 -1
- package/dist/esm/note-io/read-note.d.ts +2 -2
- package/dist/esm/note-io/write-note.d.ts +3 -6
- package/dist/esm/records/event.d.ts +3 -3
- package/dist/esm/scaffold/render-seeds.d.ts +3 -3
- package/dist/esm/scaffold/render-seeds.js +5 -3
- package/dist/esm/scaffold/scaffold.d.ts +7 -8
- package/dist/esm/tags/load-aliases.d.ts +3 -4
- package/dist/esm/taxonomy/domain-paths.d.ts +6 -6
- package/dist/esm/taxonomy/load-taxonomy.d.ts +3 -3
- package/dist/esm/taxonomy/load-taxonomy.js +2 -2
- package/dist/esm/taxonomy/taxonomy-schema.d.ts +5 -6
- package/dist/esm/taxonomy/write-taxonomy.d.ts +7 -8
- package/dist/esm/type-guards.d.ts +1 -1
- package/dist/esm/types.d.ts +6 -9
- package/dist/esm/vault-integrity/build-vault-index.d.ts +3 -4
- package/dist/esm/vault-integrity/check-vault-integrity.d.ts +46 -12
- package/dist/esm/vault-integrity/check-vault-integrity.js +59 -20
- package/dist/esm/vault-integrity/index.d.ts +2 -2
- package/dist/esm/vault-integrity/index.js +2 -2
- package/dist/esm/vault-integrity/wikilink-parse.d.ts +45 -19
- package/dist/esm/vault-integrity/wikilink-parse.js +37 -9
- 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
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
|
|
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`:
|
|
44
|
-
| `./config` | `.kb/config.yaml` loading and the typed `KbLoaderError` the loaders throw
|
|
45
|
-
| `./create` | `create`:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
122
|
+
## Vault integrity
|
|
140
123
|
|
|
141
|
-
`checkVaultIntegrity(notes)` runs whole-vault, type-blind checks over a `{ path, body, bodyStartLine }[]` note set:
|
|
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
|
-
|
|
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
|
-
`
|
|
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:
|
|
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.
|
|
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
|
|
180
|
-
|
|
|
181
|
-
| `targets`
|
|
182
|
-
| `exclude`
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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...]`**:
|
|
299
|
-
- **`--vs <ref>`**:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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`)
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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>;
|
package/dist/esm/check/check.js
CHANGED
|
@@ -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
|
-
...
|
|
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';
|