@williamthorsen/kb 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -27
- package/dist/esm/check/check.d.ts +34 -0
- package/dist/esm/check/check.js +41 -1
- package/dist/esm/check/enumerate.d.ts +37 -0
- package/dist/esm/check/enumerate.js +12 -4
- package/dist/esm/check/glob-segments.d.ts +6 -0
- package/dist/esm/check/resolve-foreign-stores.d.ts +24 -0
- package/dist/esm/check/resolve-foreign-stores.js +50 -0
- package/dist/esm/cli/commands/check.d.ts +23 -1
- package/dist/esm/cli/commands/check.js +7 -2
- package/dist/esm/cli/commands/create.d.ts +19 -1
- package/dist/esm/cli/commands/create.js +2 -0
- package/dist/esm/cli/commands/scaffold.d.ts +30 -0
- package/dist/esm/cli/commands/scaffold.js +108 -0
- package/dist/esm/cli/commands/set-default.d.ts +15 -0
- package/dist/esm/cli/commands/taxonomy.d.ts +18 -0
- package/dist/esm/cli/format.d.ts +21 -0
- package/dist/esm/cli/parse-flag-value.d.ts +5 -0
- package/dist/esm/cli/resolve-store.d.ts +12 -0
- package/dist/esm/cli/run.d.ts +9 -1
- package/dist/esm/cli/run.js +5 -0
- package/dist/esm/cli/select-kb-prompt.d.ts +18 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +10 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.js +7 -33
- package/dist/esm/cli/targeting/select-notes.d.ts +13 -0
- package/dist/esm/cli/targeting/select-notes.js +4 -3
- package/dist/esm/config/config-schema.d.ts +30 -1
- 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 +8 -0
- package/dist/esm/config/load-config.d.ts +7 -0
- package/dist/esm/config/load-config.js +1 -0
- package/dist/esm/config/note-scope.d.ts +13 -0
- package/dist/esm/create/create.d.ts +25 -0
- package/dist/esm/create/create.js +8 -12
- package/dist/esm/create/index.d.ts +0 -1
- package/dist/esm/create/index.js +0 -1
- package/dist/esm/discovery/find-kb-root.d.ts +4 -0
- package/dist/esm/discovery/kb-registry-schema.d.ts +4 -0
- package/dist/esm/discovery/load-registry.d.ts +23 -0
- package/dist/esm/discovery/register-store.d.ts +11 -0
- package/dist/esm/discovery/registry-document.d.ts +5 -0
- package/dist/esm/discovery/set-default-kb.d.ts +12 -0
- package/dist/esm/filesystem/exists.d.ts +13 -0
- package/dist/esm/filesystem/write-atomic.d.ts +5 -0
- package/dist/esm/frontmatter/parse-note.d.ts +9 -0
- package/dist/esm/git/list-git-scope.d.ts +15 -0
- package/dist/esm/git/list-git-scope.js +22 -0
- package/dist/esm/git/run-git.d.ts +17 -0
- package/dist/esm/git/run-git.js +22 -0
- package/dist/esm/layout/index.d.ts +1 -1
- package/dist/esm/layout/index.js +1 -1
- package/dist/esm/layout/store-layout.d.ts +20 -0
- package/dist/esm/layout/store-layout.js +2 -0
- package/dist/esm/lints/paths.d.ts +7 -0
- package/dist/esm/lints/tag-alias.d.ts +6 -0
- package/dist/esm/lints/taxonomy.d.ts +15 -0
- package/dist/esm/note-io/field-validators.d.ts +9 -0
- package/dist/esm/note-io/read-note.d.ts +7 -0
- package/dist/esm/note-io/write-note.d.ts +9 -0
- package/dist/esm/note-io/yaml-fields.d.ts +5 -0
- package/dist/esm/records/assertion.d.ts +4 -0
- package/dist/esm/records/event.d.ts +12 -0
- package/dist/esm/scaffold/index.d.ts +2 -0
- package/dist/esm/scaffold/index.js +2 -0
- package/dist/esm/scaffold/render-seeds.d.ts +16 -0
- package/dist/esm/scaffold/render-seeds.js +65 -0
- package/dist/esm/scaffold/scaffold.d.ts +23 -0
- package/dist/esm/scaffold/scaffold.js +35 -0
- package/dist/esm/tags/canonicalize.d.ts +8 -0
- package/dist/esm/tags/load-aliases.d.ts +10 -0
- package/dist/esm/taxonomy/domain-paths.d.ts +10 -0
- package/dist/esm/taxonomy/load-taxonomy.d.ts +8 -0
- package/dist/esm/taxonomy/taxonomy-schema.d.ts +19 -0
- package/dist/esm/taxonomy/write-taxonomy.d.ts +20 -0
- package/dist/esm/type-guards.d.ts +13 -0
- package/dist/esm/types.d.ts +41 -0
- package/dist/esm/vault-integrity/build-vault-index.d.ts +11 -0
- package/dist/esm/vault-integrity/check-vault-integrity.d.ts +48 -1
- 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 +47 -4
- package/dist/esm/vault-integrity/wikilink-parse.js +37 -9
- package/package.json +10 -4
- package/dist/esm/create/render-seeds.d.ts +0 -2
- package/dist/esm/create/render-seeds.js +0 -29
package/README.md
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
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
7
|
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.
|
|
6
8
|
|
|
7
|
-
<!-- section:release-notes
|
|
9
|
+
<!-- section:release-notes -->
|
|
10
|
+
## Release notes — v0.8.0 (2026-09-13)
|
|
11
|
+
|
|
12
|
+
### 🎉 Features
|
|
13
|
+
|
|
14
|
+
- Resolve wikilinks from a private store into a shared one (#1617)
|
|
15
|
+
|
|
16
|
+
- Adds the store-qualified wikilink `[[store:Note title]]`, whose prefix names an entry in the merged `kb.yaml` registry, so that `kb check` resolves the link against that store rather than reporting `wikilinks.unresolved`.
|
|
17
|
+
- Adds `visibility` to `.kb/config.yaml`, taking `shared` or `private` and defaulting to `private`, which sets the permitted direction: a link may point at a store as shareable as its own or more so.
|
|
18
|
+
- Adds three findings for a qualified link that does not resolve, `wikilinks.unknown-store` and `wikilinks.disallowed-store` as errors and `wikilinks.store-unavailable` as a warning, so that a correct link into a store that this machine has not cloned is reported as unverifiable rather than broken.
|
|
19
|
+
|
|
20
|
+
- Add a second-person rule to the writing preferences (#1634)
|
|
21
|
+
|
|
22
|
+
- Bans the use of the 2nd person in user- and developer-facing documentation prose.
|
|
23
|
+
- Adds `second-person` to the rule names that `revise-prose` and `prose-reviser` accept, alongside `plain-speech`, so a sweep repairs a site or records a rejection under it though no detector reports its sites.
|
|
24
|
+
- Moves the rulebook's `version` to `3`, so that a bound repository re-sweeps and re-adjudicates the rejections recorded under the old version.
|
|
25
|
+
|
|
26
|
+
### 🪦 Removed
|
|
27
|
+
|
|
28
|
+
- 🚨 **Breaking:** Remove the wikilink-parse primitives from the vault-integrity subpath (#1680)
|
|
29
|
+
|
|
30
|
+
- Removes `countNewlines`, `extractTarget`, `hasNonMarkdownExtension`, `maskFencedCode`, `maskInlineCode`, and `WIKILINK` from `@williamthorsen/kb/vault-integrity`.
|
|
31
|
+
|
|
32
|
+
Migration: Call `scanWikilinks` to find the wikilinks in a note body and their targets, in place of a scan assembled from the removed primitives.
|
|
33
|
+
<!-- /section:release-notes -->
|
|
8
34
|
|
|
9
35
|
## Exports
|
|
10
36
|
|
|
11
|
-
The package exposes
|
|
37
|
+
The package exposes thirteen subpath entries plus a root barrel:
|
|
12
38
|
|
|
13
39
|
| Entry | Description |
|
|
14
40
|
| ------------------- | ------------------------------------------------------------------------------ |
|
|
@@ -18,16 +44,14 @@ The package exposes twelve subpath entries plus a root barrel:
|
|
|
18
44
|
| `./create` | `create`: scaffold a new store and register it in `kb.yaml` |
|
|
19
45
|
| `./discovery` | KB root discovery and `kb.yaml` registry loading, merging, and writing |
|
|
20
46
|
| `./filesystem` | Filesystem-existence helpers with an explicit absence policy |
|
|
21
|
-
| `./frontmatter` | Note parsing into typed frontmatter
|
|
47
|
+
| `./frontmatter` | Note parsing into typed frontmatter |
|
|
22
48
|
| `./layout` | The store's on-disk layout: every path inside a `.kb/` store derives from here |
|
|
23
49
|
| `./note-io` | Type-blind note read/write as an ordered frontmatter field map |
|
|
24
50
|
| `./records` | The typed `assertion`/`event` record parsers and renderers |
|
|
51
|
+
| `./scaffold` | The canonical set held by a store, and the idempotent writer over it |
|
|
25
52
|
| `./tags` | `.kb/tag-aliases.yaml` loading and tag canonicalization |
|
|
26
53
|
| `./taxonomy` | `.kb/taxonomy.yaml` loading, comment-preserving declaration, and path mapping |
|
|
27
|
-
| `./vault-integrity` | Type-blind `[[link]]` resolution and basename-uniqueness
|
|
28
|
-
|
|
29
|
-
Every public function takes a single plain-object input so a future MCP wrapper can mechanically bind Zod-validated payloads.
|
|
30
|
-
The library throws on errors; success/failure shaping is left to consumers.
|
|
54
|
+
| `./vault-integrity` | Type-blind `[[link]]` scanning, resolution, and basename-uniqueness checks |
|
|
31
55
|
|
|
32
56
|
## Knowledge-base discovery
|
|
33
57
|
|
|
@@ -69,6 +93,8 @@ Configuration keys, per KB entry under `kbs.<name>`:
|
|
|
69
93
|
| `description` | no | Human-readable description |
|
|
70
94
|
| `readonly` | no | Marks the KB as read-only |
|
|
71
95
|
|
|
96
|
+
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`.
|
|
97
|
+
|
|
72
98
|
### Merge semantics
|
|
73
99
|
|
|
74
100
|
`loadKbRegistry` merges the two registries by KB name:
|
|
@@ -98,23 +124,24 @@ The relation is many-to-many (one response can address many problems, and one pr
|
|
|
98
124
|
|
|
99
125
|
`parseNote({ path })` (or `parseNoteContent({ content })`) parses a note into a `ParsedNote` carrying typed `Frontmatter`:
|
|
100
126
|
The `title`, `recordType`, `created`, `updated`, and `tags` fields are strongly typed and any other fields are preserved in an `extra` map.
|
|
101
|
-
`writeFrontmatter({ frontmatter, body })` renders it back to a note string with a fixed field order and flow-style tags; the round trip is idempotent.
|
|
102
127
|
|
|
103
128
|
Date fields surface as strings, never JS `Date` objects. YAML parse errors are recorded in `ParsedNote.frontmatterRaw.parseError` rather than thrown;
|
|
104
129
|
missing files (when a path is given) throw.
|
|
105
130
|
|
|
131
|
+
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.
|
|
132
|
+
|
|
106
133
|
## Tags
|
|
107
134
|
|
|
108
135
|
`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.
|
|
109
136
|
`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.
|
|
110
137
|
|
|
111
|
-
## Vault integrity
|
|
138
|
+
## Vault integrity
|
|
112
139
|
|
|
113
140
|
`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.
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
A second argument, `{ foreignStores, sourceVisibility }`, resolves [store-qualified links](#linking-into-another-store) against the stores they name; supplied none, the layer treats every target as store-local, which is what `buildVaultIndex`'s other consumers get.
|
|
116
143
|
|
|
117
|
-
`
|
|
144
|
+
`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`.
|
|
118
145
|
|
|
119
146
|
```ts
|
|
120
147
|
import { checkVaultIntegrity } from '@williamthorsen/kb/vault-integrity';
|
|
@@ -124,7 +151,7 @@ const findings = checkVaultIntegrity(notes);
|
|
|
124
151
|
|
|
125
152
|
## Checking a store
|
|
126
153
|
|
|
127
|
-
`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 the config selects, 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.
|
|
154
|
+
`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 a consumer can layer its own detectors over the same enumeration without walking the store twice.
|
|
128
155
|
|
|
129
156
|
```ts
|
|
130
157
|
import { check } from '@williamthorsen/kb/check';
|
|
@@ -132,13 +159,15 @@ import { check } from '@williamthorsen/kb/check';
|
|
|
132
159
|
const { notes, findings } = await check({ kbRoot });
|
|
133
160
|
```
|
|
134
161
|
|
|
162
|
+
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 where a store's assertion folders and its declared taxonomy disagree (see [`.kb/taxonomy.yaml`](#the-declared-structure-kbtaxonomyyaml)). Their 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.
|
|
163
|
+
|
|
135
164
|
A structural defect in any loaded file throws a `KbLoaderError` (see below). Any other error from enumeration or the checks propagates unchanged.
|
|
136
165
|
|
|
137
166
|
`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.
|
|
138
167
|
|
|
139
168
|
### Which notes are checked: `.kb/config.yaml`
|
|
140
169
|
|
|
141
|
-
`.kb/config.yaml` configures which notes a check enumerates.
|
|
170
|
+
`.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.
|
|
142
171
|
|
|
143
172
|
```yaml
|
|
144
173
|
# .kb/config.yaml
|
|
@@ -146,15 +175,45 @@ targets:
|
|
|
146
175
|
- 'content/**/*.md'
|
|
147
176
|
exclude:
|
|
148
177
|
- '**/node_modules/**'
|
|
178
|
+
visibility: private
|
|
149
179
|
```
|
|
150
180
|
|
|
151
|
-
| Key
|
|
152
|
-
|
|
|
153
|
-
| `targets`
|
|
154
|
-
| `exclude`
|
|
181
|
+
| Key | Default | Meaning |
|
|
182
|
+
| ------------ | ------------------------ | ---------------------------------------------------------------------------- |
|
|
183
|
+
| `targets` | `['content/**/*.md']` | Glob patterns (store-root-relative) selecting which notes a check enumerates |
|
|
184
|
+
| `exclude` | `['**/node_modules/**']` | Glob patterns excluded from enumeration even when a target matches |
|
|
185
|
+
| `visibility` | `private` | `shared` or `private`; decides which stores may link into this one |
|
|
186
|
+
|
|
187
|
+
`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, where a per-machine declaration could let the two disagree silently. It governs [linking into another store](#linking-into-another-store).
|
|
155
188
|
|
|
156
189
|
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`.
|
|
157
190
|
|
|
191
|
+
#### Ignored notes
|
|
192
|
+
|
|
193
|
+
Where the store sits 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 is what lets a store gitignore a scratch area (`local/`, `*.local.md`) and keep uncommitted notes there without the store's lints gating them.
|
|
194
|
+
|
|
195
|
+
A store outside a git working tree, or a machine carrying no git, keeps the filesystem walk alone. Where 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 a clean bill over an empty note set.
|
|
196
|
+
|
|
197
|
+
### Linking into another store
|
|
198
|
+
|
|
199
|
+
A wikilink names a store by qualifying its target: `[[fde:Note title]]` resolves `Note title` in the store registered as `fde`, where 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 carries no whitespace and no `/`, so a title that happens to contain a colon resolves whole.
|
|
200
|
+
|
|
201
|
+
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 its detail was stripped from, 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.
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
| Rule | Severity | Reported when |
|
|
206
|
+
| ------------------------------- | -------- | ----------------------------------------------------------------------------------- |
|
|
207
|
+
| `wikilinks.disallowed-store` | error | The named store is less shareable than the one being checked |
|
|
208
|
+
| `wikilinks.registry-unloadable` | error | A run with qualified links could not load `kb.yaml`; vault-scoped, reported once |
|
|
209
|
+
| `wikilinks.store-unavailable` | warning | The named store is registered but could not be read here, so the link is unverified |
|
|
210
|
+
| `wikilinks.unknown-store` | error | No `kb.yaml` entry declares the named store |
|
|
211
|
+
| `wikilinks.unresolved` | error | The named store carries no note of that basename |
|
|
212
|
+
|
|
213
|
+
`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.
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
158
217
|
### The declared structure: `.kb/taxonomy.yaml`
|
|
159
218
|
|
|
160
219
|
`.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.
|
|
@@ -187,7 +246,7 @@ A domain counts as used when any note lives at or beneath it, so a grouping doma
|
|
|
187
246
|
|
|
188
247
|
## The `kb` command
|
|
189
248
|
|
|
190
|
-
The package ships a `kb` bin with
|
|
249
|
+
The package ships a `kb` bin with five subcommands: `check`, `create`, `scaffold`, `set-default`, and `taxonomy`.
|
|
191
250
|
|
|
192
251
|
### kb create
|
|
193
252
|
|
|
@@ -204,15 +263,33 @@ It creates these files and directories:
|
|
|
204
263
|
|
|
205
264
|
| Path | Contents |
|
|
206
265
|
| ----------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
266
|
+
| `.editorconfig` | Width, indentation, and line endings; see [Formatting a knowledge base](#formatting-a-knowledge-base) |
|
|
207
267
|
| `.kb/config.yaml` | A fully-commented check config; the bundled defaults apply as-is |
|
|
208
268
|
| `.kb/tag-aliases.yaml` | An empty `aliases: {}` map |
|
|
269
|
+
| `.prettierrc.yaml` | The canonical formatting config; see [Formatting a knowledge base](#formatting-a-knowledge-base) |
|
|
209
270
|
| `content/`, `content/events/` | The note tree; `capture-event` writes events to `content/events/`, `kb-update-events` edits them in place |
|
|
210
271
|
|
|
211
|
-
The config
|
|
272
|
+
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.
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
`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` where stdin is not interactive. An existing `default_kb` is never overwritten.
|
|
277
|
+
|
|
278
|
+
### kb scaffold
|
|
279
|
+
|
|
280
|
+
`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.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
kb scaffold # back-fill the nearest ancestor .kb/ store
|
|
284
|
+
kb scaffold --kb coding # back-fill the named store from the kb.yaml registry
|
|
285
|
+
kb scaffold --force # replace every canonical file with a fresh seed
|
|
286
|
+
```
|
|
212
287
|
|
|
213
|
-
The
|
|
288
|
+
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.
|
|
214
289
|
|
|
215
|
-
|
|
290
|
+
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`.
|
|
291
|
+
|
|
292
|
+
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.
|
|
216
293
|
|
|
217
294
|
### kb set-default
|
|
218
295
|
|
|
@@ -241,9 +318,9 @@ kb check --vs=main # check only the notes changed since a ref
|
|
|
241
318
|
|
|
242
319
|
`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.
|
|
243
320
|
|
|
244
|
-
**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
|
|
321
|
+
**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 it names, so a targeted run never false-flags a link to an unselected note; only the report and the exit code narrow to the selection.
|
|
245
322
|
|
|
246
|
-
- **`[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
|
|
323
|
+
- **`[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.
|
|
247
324
|
- **`--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.
|
|
248
325
|
|
|
249
326
|
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.
|
|
@@ -272,10 +349,67 @@ Every derived domain lands under `provisional:` with no description: the command
|
|
|
272
349
|
|
|
273
350
|
Without `--merge`, a store that already declares a taxonomy is left untouched and the command exits 2.
|
|
274
351
|
|
|
275
|
-
##
|
|
352
|
+
## Formatting a knowledge base
|
|
276
353
|
|
|
277
|
-
|
|
354
|
+
A knowledge base is formatted by Prettier, invoked directly. kb owns the configuration and scaffolds it; it does not run the formatter and does not bundle one. A store therefore needs Prettier available on the machine and the two config files that `kb create` and `kb scaffold` write, and nothing else: no `package.json`, no lockfile, and no linter or TypeScript configuration.
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
prettier --write . # format the store
|
|
358
|
+
prettier --check . # report drift without writing
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
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`.
|
|
362
|
+
|
|
363
|
+
### The two config files
|
|
364
|
+
|
|
365
|
+
`.editorconfig` carries everything that more than one tool reads:
|
|
366
|
+
|
|
367
|
+
```ini
|
|
368
|
+
root = true
|
|
369
|
+
|
|
370
|
+
[*]
|
|
371
|
+
charset = utf-8
|
|
372
|
+
end_of_line = lf
|
|
373
|
+
indent_size = 2
|
|
374
|
+
indent_style = space
|
|
375
|
+
insert_final_newline = true
|
|
376
|
+
max_line_length = 120
|
|
377
|
+
trim_trailing_whitespace = true
|
|
378
|
+
|
|
379
|
+
[*.md]
|
|
380
|
+
trim_trailing_whitespace = false
|
|
381
|
+
```
|
|
278
382
|
|
|
279
|
-
|
|
383
|
+
Prettier reads this file, and so does every editor that supports EditorConfig, which is why the width and the indentation live here rather than in the Prettier config. `max_line_length` becomes Prettier's `printWidth` and `indent_size` its `tabWidth`, so a store that left them unset would format at Prettier's default width of 80 and would tell an editor nothing at all.
|
|
280
384
|
|
|
281
|
-
|
|
385
|
+
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.
|
|
386
|
+
|
|
387
|
+
`.prettierrc.yaml` carries the one setting that `.editorconfig` has no key for:
|
|
388
|
+
|
|
389
|
+
```yaml
|
|
390
|
+
embeddedLanguageFormatting: off
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
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 `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 carrying a list past the print width churns on each pass.
|
|
394
|
+
|
|
395
|
+
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.
|
|
396
|
+
|
|
397
|
+
### Migrating a store off a `package.json` toolchain
|
|
398
|
+
|
|
399
|
+
A store that carries a `package.json` only to obtain Prettier can retire it.
|
|
400
|
+
|
|
401
|
+
1. Install Prettier where the store is edited, if it is not already there: `pnpm add --global prettier`.
|
|
402
|
+
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.
|
|
403
|
+
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.
|
|
404
|
+
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.
|
|
405
|
+
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`.
|
|
406
|
+
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.
|
|
407
|
+
7. Format once: `prettier --write .`. Commit the result on its own, so the reformatting does not obscure later diffs.
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
411
|
+
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.
|
|
412
|
+
|
|
413
|
+
## Error and exception model
|
|
414
|
+
|
|
415
|
+
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.
|
|
@@ -1,11 +1,45 @@
|
|
|
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. */
|
|
4
5
|
export interface CheckResult {
|
|
6
|
+
/** The effective `KbConfig` the run used — loaded from `.kb/config.yaml`, or `defaultKbConfig` when absent. */
|
|
5
7
|
config: KbConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Every note selected by the store's `config.targets` and, inside a git working tree, not ignored by the repository,
|
|
10
|
+
* in walk order.
|
|
11
|
+
*/
|
|
6
12
|
notes: readonly EnumeratedNote[];
|
|
13
|
+
/**
|
|
14
|
+
* Findings from whole-vault integrity (unresolved links, basename collisions), taxonomy drift, and the tag-alias and
|
|
15
|
+
* paths lints.
|
|
16
|
+
*/
|
|
7
17
|
findings: readonly Finding[];
|
|
8
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Runs the store's config-driven check: load `.kb/config.yaml`, `.kb/tag-aliases.yaml`, and `.kb/taxonomy.yaml`,
|
|
21
|
+
* enumerate notes under the config's `targets`/`exclude`, and compose whole-vault integrity and taxonomy drift with
|
|
22
|
+
* the type-blind per-note lints across them. Frontmatter validity is owned by the record types at write time, so no
|
|
23
|
+
* frontmatter re-validation runs here.
|
|
24
|
+
*
|
|
25
|
+
* Inside a git working tree the enumeration also drops the notes that the repository ignores, so such a note is
|
|
26
|
+
* neither checked nor available as a wikilink target; see {@link enumerateNotes} for the rule.
|
|
27
|
+
*
|
|
28
|
+
* A `[[store:Target]]` link resolves against the store its prefix names rather than this one. Those stores are looked
|
|
29
|
+
* up in the merged `kb.yaml` registry, which is read from `~/.agents/kb.yaml` and from `cwd`'s project-local registry;
|
|
30
|
+
* `cwd` defaults to the store root, so a caller that supplies none still resolves against the user-global registry.
|
|
31
|
+
* Only the stores this store's own links name are consulted.
|
|
32
|
+
*
|
|
33
|
+
* Returns the effective config alongside the enumerated notes and findings, so a consumer (e.g. `kb-curate`) can layer
|
|
34
|
+
* its own detectors over the same enumeration without walking the tree twice, and can read the resolved
|
|
35
|
+
* `targets`/`exclude` without re-loading `.kb/config.yaml`.
|
|
36
|
+
*
|
|
37
|
+
* A structural defect in any loaded file throws a `KbLoaderError` (the loaders' own contract); the caller decides how
|
|
38
|
+
* to surface it. Any other error from enumeration or the checks propagates unchanged — it is never relabeled as a
|
|
39
|
+
* config defect.
|
|
40
|
+
*/
|
|
9
41
|
export declare function check(input: {
|
|
10
42
|
kbRoot: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
home?: string;
|
|
11
45
|
}): 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';
|
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
import type { KbConfig } from '../config/config-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* A note reduced to the fields the check pipeline and curate consume: its paths, its frontmatter field map, its body
|
|
4
|
+
* and body-start line (for file-absolute link lines), its full content (for the paths lint), and any parse error.
|
|
5
|
+
*/
|
|
2
6
|
export interface EnumeratedNote {
|
|
7
|
+
/** Absolute path the note was read from. */
|
|
3
8
|
path: string;
|
|
9
|
+
/** The note's path relative to the KB root, slash-separated. */
|
|
4
10
|
relativePath: string;
|
|
11
|
+
/** The frontmatter field map, empty when the block is absent or unparseable. */
|
|
5
12
|
fields: Record<string, unknown>;
|
|
13
|
+
/** The note body (everything after the frontmatter block). */
|
|
6
14
|
body: string;
|
|
15
|
+
/** The full original note content. */
|
|
7
16
|
content: string;
|
|
17
|
+
/** 1-based file line where the body begins. */
|
|
8
18
|
bodyStartLine: number;
|
|
19
|
+
/** Frontmatter parse or absence diagnostic, when the block was missing or could not be parsed. */
|
|
9
20
|
error?: string;
|
|
10
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks a KB root and returns the KB-root-relative path of every note {@link enumerateNotes} selects, opening none of
|
|
24
|
+
* them. Scope selection runs through the same matcher and the same pruning, so a caller that needs only the note set's
|
|
25
|
+
* shape — which folders hold notes, and how many — sees exactly what the check pipeline admits.
|
|
26
|
+
*
|
|
27
|
+
* A note whose content cannot be read still contributes its path here, where `enumerateNotes` drops it with a warning.
|
|
28
|
+
*/
|
|
11
29
|
export declare function enumerateNotePaths(input: {
|
|
12
30
|
kbRoot: string;
|
|
13
31
|
config: KbConfig;
|
|
14
32
|
}): Promise<string[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Walks a KB root and parses every note whose KB-root-relative path matches a `config.targets` glob and no
|
|
35
|
+
* `config.exclude` glob into an {@link EnumeratedNote}.
|
|
36
|
+
*
|
|
37
|
+
* Matching uses `picomatch` with `dot:false`, so dot-prefixed directories (`.kb`, `.git`, `.agents`) are excluded
|
|
38
|
+
* implicitly without naming them in `exclude`. The walk prunes the tree to each target's leading literal segment
|
|
39
|
+
* (`content/**` descends only into `content/`); a target with no leading literal (e.g. `**\/*.md`) falls back to a
|
|
40
|
+
* full walk. Excludes are honored during descent so an excluded subtree is never entered.
|
|
41
|
+
*
|
|
42
|
+
* If the store sits in a git working tree, the enumeration keeps only the notes that git tracks and the untracked
|
|
43
|
+
* notes that no ignore rule covers. A note that the repository ignores is therefore neither enumerated nor available
|
|
44
|
+
* as a wikilink target, so a link pointing at one resolves to nothing. A store outside a working tree, or a machine
|
|
45
|
+
* carrying no git, keeps what the walk alone found.
|
|
46
|
+
*
|
|
47
|
+
* Notes with malformed or absent frontmatter are kept — `readNoteContent` records the parse error in `error` and
|
|
48
|
+
* returns an empty field map rather than throwing, so they remain valid wikilink targets. A note that cannot be read,
|
|
49
|
+
* or a directory that cannot be listed, is skipped with a `kb:` stderr warning rather than aborting the walk. Each
|
|
50
|
+
* note's `path` is absolute; `relativePath` is the slash-separated path from the KB root.
|
|
51
|
+
*/
|
|
15
52
|
export declare function enumerateNotes(input: {
|
|
16
53
|
kbRoot: string;
|
|
17
54
|
config: KbConfig;
|
|
@@ -3,6 +3,7 @@ import { join, relative, sep } from 'node:path';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
5
|
import { createNoteScopeMatcher } from "../config/note-scope.js";
|
|
6
|
+
import { listGitScope } from "../git/list-git-scope.js";
|
|
6
7
|
import { readNoteContent } from "../note-io/read-note.js";
|
|
7
8
|
import { isGlobSegment } from "./glob-segments.js";
|
|
8
9
|
export async function enumerateNotePaths(input) {
|
|
@@ -36,8 +37,15 @@ async function collectNoteLocations(input) {
|
|
|
36
37
|
const { kbRoot, config } = input;
|
|
37
38
|
const matcher = createNoteScopeMatcher(config);
|
|
38
39
|
const topLevelDirs = leadingLiteralSegments(config.targets);
|
|
39
|
-
const
|
|
40
|
-
await walk({ root: kbRoot, dir: kbRoot, matcher, topLevelDirs, out:
|
|
40
|
+
const candidates = [];
|
|
41
|
+
await walk({ root: kbRoot, dir: kbRoot, matcher, topLevelDirs, out: candidates });
|
|
42
|
+
const gitScope = listGitScope({ root: kbRoot });
|
|
43
|
+
if (gitScope === undefined)
|
|
44
|
+
return candidates;
|
|
45
|
+
const locations = candidates.filter((candidate) => gitScope.has(candidate.relativePath.normalize('NFC')));
|
|
46
|
+
if (candidates.length > 0 && locations.length === 0) {
|
|
47
|
+
process.stderr.write(`kb: warning: git ignores every note under ${kbRoot}, so nothing was checked\n`);
|
|
48
|
+
}
|
|
41
49
|
return locations;
|
|
42
50
|
}
|
|
43
51
|
function leadingLiteralSegments(targets) {
|
|
@@ -45,7 +53,7 @@ function leadingLiteralSegments(targets) {
|
|
|
45
53
|
for (const target of targets) {
|
|
46
54
|
const firstSegment = target.split('/', 1)[0] ?? '';
|
|
47
55
|
if (firstSegment === '' || isGlobSegment(firstSegment)) {
|
|
48
|
-
return
|
|
56
|
+
return undefined;
|
|
49
57
|
}
|
|
50
58
|
dirs.add(firstSegment);
|
|
51
59
|
}
|
|
@@ -66,7 +74,7 @@ async function walk(input) {
|
|
|
66
74
|
const absolutePath = join(dir, entry.name);
|
|
67
75
|
const relativePath = relative(root, absolutePath).split(sep).join('/');
|
|
68
76
|
if (entry.isDirectory()) {
|
|
69
|
-
if (atRoot && topLevelDirs !==
|
|
77
|
+
if (atRoot && topLevelDirs !== undefined && !topLevelDirs.has(entry.name))
|
|
70
78
|
continue;
|
|
71
79
|
if (matcher.isExcluded(relativePath))
|
|
72
80
|
continue;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/** Reports whether a path segment contains a glob metacharacter, making it non-literal. */
|
|
1
2
|
export declare function isGlobSegment(segment: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the leading run of literal (non-glob) segments of a slash-separated pattern, slash-joined. A pattern whose
|
|
5
|
+
* first segment is a glob yields the empty string; a fully literal pattern yields itself. Locates the deepest concrete
|
|
6
|
+
* path a glob is anchored to, e.g. `content/notes/**` yields `content/notes`.
|
|
7
|
+
*/
|
|
2
8
|
export declare function leadingLiteralPrefix(pattern: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type StoreVisibility } from '../config/config-schema.js';
|
|
2
|
+
import type { KbRegistry } from '../types.js';
|
|
3
|
+
import type { ForeignStore } from '../vault-integrity/check-vault-integrity.js';
|
|
4
|
+
/**
|
|
5
|
+
* Collects the distinct store names that a note set's wikilinks qualify, so a check run consults only the stores its
|
|
6
|
+
* own links reach. It reads {@link scanWikilinks}, the same walk that later evaluates the links, so a store-shaped
|
|
7
|
+
* prefix inside a code sample pulls in no store and the two cannot disagree on what counts as a qualified link.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collectStorePrefixes(notes: readonly {
|
|
10
|
+
body: string;
|
|
11
|
+
}[]): Set<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves each store name a run's links qualify against the merged registry, reading only note paths and each store's
|
|
14
|
+
* own `.kb/config.yaml`: a foreign store is enumerated under its own `targets`/`exclude` and its own repository's
|
|
15
|
+
* ignore rules, as it would be under its own check run, and no foreign note is opened.
|
|
16
|
+
*
|
|
17
|
+
* A store that cannot be read — absent from this machine, or carrying a config file that will not load — resolves
|
|
18
|
+
* `unavailable` rather than throwing, so one unrelated store cannot fail the run.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveForeignStores(input: {
|
|
21
|
+
prefixes: Iterable<string>;
|
|
22
|
+
registry: KbRegistry;
|
|
23
|
+
sourceVisibility: StoreVisibility;
|
|
24
|
+
}): Promise<Map<string, ForeignStore>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { isAtLeastAsShareable } from "../config/config-schema.js";
|
|
4
|
+
import { loadKbConfig } from "../config/load-config.js";
|
|
5
|
+
import { resolveKbDir } from "../layout/index.js";
|
|
6
|
+
import { buildVaultIndex } from "../vault-integrity/build-vault-index.js";
|
|
7
|
+
import { scanWikilinks } from "../vault-integrity/wikilink-parse.js";
|
|
8
|
+
import { enumerateNotePaths } from "./enumerate.js";
|
|
9
|
+
export function collectStorePrefixes(notes) {
|
|
10
|
+
const prefixes = new Set();
|
|
11
|
+
for (const note of notes) {
|
|
12
|
+
for (const { store } of scanWikilinks(note.body)) {
|
|
13
|
+
if (store !== undefined)
|
|
14
|
+
prefixes.add(store);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return prefixes;
|
|
18
|
+
}
|
|
19
|
+
export async function resolveForeignStores(input) {
|
|
20
|
+
const resolved = new Map();
|
|
21
|
+
for (const prefix of input.prefixes) {
|
|
22
|
+
resolved.set(prefix, await resolveOne(prefix, input.registry, input.sourceVisibility));
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
async function resolveOne(prefix, registry, sourceVisibility) {
|
|
27
|
+
const entry = registry.entries.find((candidate) => candidate.name === prefix);
|
|
28
|
+
if (entry === undefined)
|
|
29
|
+
return { status: 'unknown' };
|
|
30
|
+
try {
|
|
31
|
+
const stats = await stat(entry.path);
|
|
32
|
+
if (!stats.isDirectory())
|
|
33
|
+
return { status: 'unavailable', reason: `${entry.path} is not a directory` };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return { status: 'unavailable', reason: `${entry.path} could not be read: ${describeError(error)}` };
|
|
37
|
+
}
|
|
38
|
+
let config;
|
|
39
|
+
try {
|
|
40
|
+
config = await loadKbConfig({ kbRoot: { path: entry.path, kbDir: resolveKbDir(entry.path) } });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { status: 'unavailable', reason: describeError(error) };
|
|
44
|
+
}
|
|
45
|
+
if (!isAtLeastAsShareable({ source: sourceVisibility, target: config.visibility })) {
|
|
46
|
+
return { status: 'disallowed', visibility: config.visibility };
|
|
47
|
+
}
|
|
48
|
+
const paths = await enumerateNotePaths({ kbRoot: entry.path, config });
|
|
49
|
+
return { status: 'resolved', index: buildVaultIndex(paths.map((path) => ({ path }))) };
|
|
50
|
+
}
|