@williamthorsen/kb 0.3.1 → 0.5.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 +78 -15
- package/dist/esm/check/check.js +10 -2
- package/dist/esm/check/enumerate.d.ts +4 -0
- package/dist/esm/check/enumerate.js +32 -20
- package/dist/esm/check/index.d.ts +1 -1
- package/dist/esm/check/index.js +1 -1
- package/dist/esm/cli/commands/check.js +3 -35
- package/dist/esm/cli/commands/create.d.ts +2 -1
- package/dist/esm/cli/commands/create.js +32 -15
- package/dist/esm/cli/commands/taxonomy.d.ts +15 -0
- package/dist/esm/cli/commands/taxonomy.js +130 -0
- package/dist/esm/cli/format.js +4 -1
- package/dist/esm/cli/parse-flag-value.d.ts +2 -0
- package/dist/esm/cli/parse-flag-value.js +14 -0
- package/dist/esm/cli/resolve-store.d.ts +14 -0
- package/dist/esm/cli/resolve-store.js +25 -0
- package/dist/esm/cli/run.d.ts +1 -1
- package/dist/esm/cli/run.js +5 -0
- package/dist/esm/create/create.d.ts +2 -0
- package/dist/esm/create/create.js +3 -2
- package/dist/esm/discovery/register-store.js +23 -4
- 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 +1 -0
- package/dist/esm/layout/store-layout.js +1 -0
- package/dist/esm/lints/index.d.ts +1 -0
- package/dist/esm/lints/index.js +1 -0
- package/dist/esm/lints/taxonomy.d.ts +12 -0
- package/dist/esm/lints/taxonomy.js +52 -0
- package/dist/esm/taxonomy/domain-paths.d.ts +3 -0
- package/dist/esm/taxonomy/domain-paths.js +23 -0
- package/dist/esm/taxonomy/index.d.ts +4 -0
- package/dist/esm/taxonomy/index.js +4 -0
- package/dist/esm/taxonomy/load-taxonomy.d.ts +5 -0
- package/dist/esm/taxonomy/load-taxonomy.js +60 -0
- package/dist/esm/taxonomy/taxonomy-schema.d.ts +11 -0
- package/dist/esm/taxonomy/taxonomy-schema.js +28 -0
- package/dist/esm/taxonomy/write-taxonomy.d.ts +12 -0
- package/dist/esm/taxonomy/write-taxonomy.js +142 -0
- package/dist/esm/types.d.ts +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -4,11 +4,21 @@ Foundation library for knowledge-base tooling.
|
|
|
4
4
|
Provides knowledge-base discovery, registry loading, frontmatter parsing and writing, tag canonicalization, and type-blind vault-integrity checks.
|
|
5
5
|
It underpins the knowledge-base skills — among them `kb-retrieve` (assertion recall) and `kb-retrieve-events` (event recall), `kb-add`, `kb-curate`, `capture-event`, and `kb-update-events` — and the planned `@williamthorsen/kb-mcp` server.
|
|
6
6
|
|
|
7
|
-
<!-- section:release-notes
|
|
7
|
+
<!-- section:release-notes -->
|
|
8
|
+
## Release notes — v0.5.0 (2026-08-08)
|
|
9
|
+
|
|
10
|
+
### 🎉 Features
|
|
11
|
+
|
|
12
|
+
- Register a new store with a description and keep the registry sorted (#1237)
|
|
13
|
+
|
|
14
|
+
Adds a `--description` flag to `kb create`, so that a new knowledge base can be given a description as it is created. Alphabetical ordering of keys in `kb.yaml` is now enforced on every write.
|
|
15
|
+
|
|
16
|
+
Also fixes an issue where creating a knowledge base under an empty name could leave a stray registry entry. Across the CLI, a flag given an empty value is now refused.
|
|
17
|
+
<!-- /section:release-notes -->
|
|
8
18
|
|
|
9
19
|
## Exports
|
|
10
20
|
|
|
11
|
-
The package exposes
|
|
21
|
+
The package exposes twelve subpath entries plus a root barrel:
|
|
12
22
|
|
|
13
23
|
| Entry | Description |
|
|
14
24
|
| ------------------- | ------------------------------------------------------------------------------ |
|
|
@@ -19,9 +29,11 @@ The package exposes ten subpath entries plus a root barrel:
|
|
|
19
29
|
| `./discovery` | KB root discovery and `kb.yaml` registry loading, merging, and writing |
|
|
20
30
|
| `./filesystem` | Filesystem-existence helpers with an explicit absence policy |
|
|
21
31
|
| `./frontmatter` | Note parsing into typed frontmatter and writing it back to YAML |
|
|
32
|
+
| `./layout` | The store's on-disk layout: every path inside a `.kb/` store derives from here |
|
|
22
33
|
| `./note-io` | Type-blind note read/write as an ordered frontmatter field map |
|
|
23
34
|
| `./records` | The typed `assertion`/`event` record parsers and renderers |
|
|
24
35
|
| `./tags` | `.kb/tag-aliases.yaml` loading and tag canonicalization |
|
|
36
|
+
| `./taxonomy` | `.kb/taxonomy.yaml` loading, comment-preserving declaration, and path mapping |
|
|
25
37
|
| `./vault-integrity` | Type-blind `[[link]]` resolution and basename-uniqueness over a note set |
|
|
26
38
|
|
|
27
39
|
Every public function takes a single plain-object input so a future MCP wrapper can mechanically bind Zod-validated payloads.
|
|
@@ -112,6 +124,8 @@ missing files (when a path is given) throw.
|
|
|
112
124
|
|
|
113
125
|
The type-blind per-note lints — `tagAliasFindings(note, aliases)` (`tag-alias`, warning) and `pathsFindings(note)` (`paths.user-home`, error) — catch what write-time record validation can't: alias-vocabulary drift and hardcoded `/Users/{name}/` paths in captured content.
|
|
114
126
|
|
|
127
|
+
`taxonomyFindings({ notes, taxonomy, config, taxonomyPath })` reports where a store's assertion folders and its declared taxonomy disagree (see [`.kb/taxonomy.yaml`](#the-declared-structure-kbtaxonomyyaml)). Its findings carry `scope: 'vault'`: they describe the store rather than any one note, so a consumer that narrows a report to selected notes must keep them rather than filter them out by path.
|
|
128
|
+
|
|
115
129
|
```ts
|
|
116
130
|
import { checkVaultIntegrity } from '@williamthorsen/kb/vault-integrity';
|
|
117
131
|
|
|
@@ -120,7 +134,7 @@ const findings = checkVaultIntegrity(notes);
|
|
|
120
134
|
|
|
121
135
|
## Checking a store
|
|
122
136
|
|
|
123
|
-
`check({ kbRoot })` runs a store's full check in one call: it loads `.kb/config.yaml
|
|
137
|
+
`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.
|
|
124
138
|
|
|
125
139
|
```ts
|
|
126
140
|
import { check } from '@williamthorsen/kb/check';
|
|
@@ -128,7 +142,9 @@ import { check } from '@williamthorsen/kb/check';
|
|
|
128
142
|
const { notes, findings } = await check({ kbRoot });
|
|
129
143
|
```
|
|
130
144
|
|
|
131
|
-
A structural defect in
|
|
145
|
+
A structural defect in any loaded file throws a `KbLoaderError` (see below). Any other error from enumeration or the checks propagates unchanged.
|
|
146
|
+
|
|
147
|
+
`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.
|
|
132
148
|
|
|
133
149
|
### Which notes are checked: `.kb/config.yaml`
|
|
134
150
|
|
|
@@ -149,18 +165,49 @@ exclude:
|
|
|
149
165
|
|
|
150
166
|
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`.
|
|
151
167
|
|
|
168
|
+
### The declared structure: `.kb/taxonomy.yaml`
|
|
169
|
+
|
|
170
|
+
`.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.
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
# .kb/taxonomy.yaml
|
|
174
|
+
domains:
|
|
175
|
+
engineering: Software engineering practice
|
|
176
|
+
engineering/tooling: Build, test, and development tooling
|
|
177
|
+
provisional:
|
|
178
|
+
engineering/tooling/versioning: Release and version management
|
|
179
|
+
languages:
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Two disjoint maps of domain path to one-line description. `domains` holds reviewed declarations and `provisional` holds those declared but not yet reviewed; promotion is writing a description and moving the line up. A domain may be declared without a description, as `languages` is above.
|
|
183
|
+
|
|
184
|
+
Keys are relative to `content/assertions/` and may nest to any depth. Parents are not implied: declaring `engineering/tooling` does not declare `engineering`. A path declared in both maps fails the load, as does a malformed key — one restating the `content/assertions/` prefix, or carrying a leading or trailing slash, an empty segment, or a `.`/`..` segment.
|
|
185
|
+
|
|
186
|
+
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:
|
|
187
|
+
|
|
188
|
+
| Rule | Meaning |
|
|
189
|
+
| --------------------- | ----------------------------------------------- |
|
|
190
|
+
| `taxonomy.undeclared` | A folder holds notes but no domain declares it. |
|
|
191
|
+
| `taxonomy.unused` | A declared domain has no note at or beneath it. |
|
|
192
|
+
| `taxonomy.orphan` | A declared domain's parent is undeclared. |
|
|
193
|
+
|
|
194
|
+
A domain counts as used when any note lives at or beneath it, so a grouping domain that holds only subfolders is not reported unused. A domain inside a `config.exclude` subtree is exempt from `taxonomy.unused`, since its notes never enumerate.
|
|
195
|
+
|
|
196
|
+
`loadTaxonomy({ kbRoot })` reads both blocks into one map of domain path to `{ description, provisional }`, and `writeTaxonomy({ kbRoot, declarations })` declares domains while preserving the file's existing comments, key order, and formatting. `resolveDomain(relativePath)` maps a store-root-relative note path to the domain it sits in (`undefined` for a non-assertion or a note at the assertions root), and `resolveParent(path)` yields a domain's parent (`undefined` at the top level); the drift rules and the back-fill both derive their answers from this pair, so a consumer that classifies notes against the taxonomy stays in agreement with what `kb check` reports. All four are exported from `@williamthorsen/kb/taxonomy`.
|
|
197
|
+
|
|
152
198
|
## The `kb` command
|
|
153
199
|
|
|
154
|
-
The package ships a `kb` bin with
|
|
200
|
+
The package ships a `kb` bin with four subcommands: `check`, `create`, `set-default`, and `taxonomy`.
|
|
155
201
|
|
|
156
202
|
### kb create
|
|
157
203
|
|
|
158
204
|
`kb create` scaffolds a new knowledge base in the current directory and registers it in the user-global `~/.agents/kb.yaml`.
|
|
159
205
|
|
|
160
206
|
```bash
|
|
161
|
-
kb create
|
|
162
|
-
kb create --name coding
|
|
163
|
-
kb create --
|
|
207
|
+
kb create # scaffold the current directory, register under its name
|
|
208
|
+
kb create --name coding # register under an explicit name
|
|
209
|
+
kb create --description "Coding notes" # describe the registry entry
|
|
210
|
+
kb create --no-register # scaffold without writing the registry
|
|
164
211
|
```
|
|
165
212
|
|
|
166
213
|
It creates these files and directories:
|
|
@@ -173,7 +220,7 @@ It creates these files and directories:
|
|
|
173
220
|
|
|
174
221
|
The config seed is serialized from the in-package `defaultKbConfig`, so a new store cannot drift from the bundled default.
|
|
175
222
|
|
|
176
|
-
The name defaults to the directory's base name; `--name` overrides it and `--no-register` scaffolds without writing the registry. The registry write preserves any existing comments in `kb.yaml
|
|
223
|
+
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.
|
|
177
224
|
|
|
178
225
|
`kb create` also keeps a default knowledge base set. When the registry's top-level `default_kb` pointer is unset and the new store is the only registered KB, it becomes the default. When other KBs are already registered with no default, `kb create` prompts you to choose one on an interactive terminal — or, when stdin is not interactive, points you to `kb set-default`. An existing `default_kb` is never overwritten.
|
|
179
226
|
|
|
@@ -213,15 +260,31 @@ Because the exit code reflects only the selected notes, a per-batch or pre-commi
|
|
|
213
260
|
|
|
214
261
|
Exit codes:
|
|
215
262
|
|
|
216
|
-
| Code | Meaning
|
|
217
|
-
| ---- |
|
|
218
|
-
| `0` | No error-severity findings in the checked notes (warnings are allowed). A run that selects no notes also exits 0.
|
|
219
|
-
| `1` | One or more error-severity findings in the checked notes.
|
|
220
|
-
| `2` | A usage error, an unresolvable store or `--vs` ref, a path matching no note, or a malformed `config
|
|
263
|
+
| Code | Meaning |
|
|
264
|
+
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
265
|
+
| `0` | No error-severity findings in the checked notes (warnings are allowed). A run that selects no notes also exits 0. |
|
|
266
|
+
| `1` | One or more error-severity findings in the checked notes. |
|
|
267
|
+
| `2` | A usage error, an unresolvable store or `--vs` ref, a path matching no note, or a malformed `config`, `tag-aliases`, or `taxonomy` file. |
|
|
268
|
+
|
|
269
|
+
A finding carrying `scope: 'vault'` describes the store rather than any one note, so it is reported under every run, including a targeted one, a `--vs` one, and one that matched no notes at all. The taxonomy rules are the ones that produce them.
|
|
270
|
+
|
|
271
|
+
### kb taxonomy
|
|
272
|
+
|
|
273
|
+
`kb taxonomy init` derives a starting taxonomy from the notes a store already holds, so a taxonomy can be introduced to a populated store without every folder reporting as undeclared.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
kb taxonomy init # declare every folder holding notes, and its ancestors
|
|
277
|
+
kb taxonomy init --kb coding # back-fill the named store from the kb.yaml registry
|
|
278
|
+
kb taxonomy init --merge # add only the domains an existing taxonomy omits
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Every derived domain lands under `provisional:` with no description: the command cannot invent descriptions, and provisional already means "declared, not yet reviewed". Because the derivation reads the same enumeration `kb check` does, a back-filled store reports no taxonomy drift.
|
|
282
|
+
|
|
283
|
+
Without `--merge`, a store that already declares a taxonomy is left untouched and the command exits 2.
|
|
221
284
|
|
|
222
285
|
## Error and exception model
|
|
223
286
|
|
|
224
|
-
The checks **return** findings; they never throw. Loaders (`loadKbConfig`, `loadAliases`) **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. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
|
|
287
|
+
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. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
|
|
225
288
|
|
|
226
289
|
## MCP wrappability
|
|
227
290
|
|
package/dist/esm/check/check.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import { loadKbConfig } from "../config/load-config.js";
|
|
2
|
-
import { resolveKbDir } from "../layout/index.js";
|
|
3
|
+
import { resolveKbDir, TAXONOMY_FILE } from "../layout/index.js";
|
|
3
4
|
import { pathsFindings } from "../lints/paths.js";
|
|
4
5
|
import { tagAliasFindings } from "../lints/tag-alias.js";
|
|
6
|
+
import { taxonomyFindings } from "../lints/taxonomy.js";
|
|
5
7
|
import { loadAliases } from "../tags/load-aliases.js";
|
|
8
|
+
import { loadTaxonomy } from "../taxonomy/load-taxonomy.js";
|
|
6
9
|
import { checkVaultIntegrity } from "../vault-integrity/check-vault-integrity.js";
|
|
7
10
|
import { enumerateNotes } from "./enumerate.js";
|
|
8
11
|
export async function check(input) {
|
|
9
12
|
const kbRoot = { path: input.kbRoot, kbDir: resolveKbDir(input.kbRoot) };
|
|
10
|
-
const [config, aliases] = await Promise.all([
|
|
13
|
+
const [config, aliases, taxonomy] = await Promise.all([
|
|
14
|
+
loadKbConfig({ kbRoot }),
|
|
15
|
+
loadAliases({ kbRoot }),
|
|
16
|
+
loadTaxonomy({ kbRoot }),
|
|
17
|
+
]);
|
|
11
18
|
const notes = await enumerateNotes({ kbRoot: input.kbRoot, config });
|
|
12
19
|
const findings = [
|
|
13
20
|
...checkVaultIntegrity(notes),
|
|
21
|
+
...taxonomyFindings({ notes, taxonomy, config, taxonomyPath: join(input.kbRoot, TAXONOMY_FILE) }),
|
|
14
22
|
...notes.flatMap((note) => [...tagAliasFindings(note, aliases), ...pathsFindings(note)]),
|
|
15
23
|
];
|
|
16
24
|
return { config, notes, findings };
|
|
@@ -8,6 +8,10 @@ export interface EnumeratedNote {
|
|
|
8
8
|
bodyStartLine: number;
|
|
9
9
|
error?: string;
|
|
10
10
|
}
|
|
11
|
+
export declare function enumerateNotePaths(input: {
|
|
12
|
+
kbRoot: string;
|
|
13
|
+
config: KbConfig;
|
|
14
|
+
}): Promise<string[]>;
|
|
11
15
|
export declare function enumerateNotes(input: {
|
|
12
16
|
kbRoot: string;
|
|
13
17
|
config: KbConfig;
|
|
@@ -4,13 +4,41 @@ import process from 'node:process';
|
|
|
4
4
|
import { createNoteScopeMatcher } from "../config/note-scope.js";
|
|
5
5
|
import { readNoteContent } from "../note-io/read-note.js";
|
|
6
6
|
import { isGlobSegment } from "./glob-segments.js";
|
|
7
|
+
export async function enumerateNotePaths(input) {
|
|
8
|
+
const locations = await collectNoteLocations(input);
|
|
9
|
+
return locations.map((location) => location.relativePath);
|
|
10
|
+
}
|
|
7
11
|
export async function enumerateNotes(input) {
|
|
12
|
+
const locations = await collectNoteLocations(input);
|
|
13
|
+
const notes = [];
|
|
14
|
+
for (const { path, relativePath } of locations) {
|
|
15
|
+
try {
|
|
16
|
+
const content = await readFile(path, 'utf8');
|
|
17
|
+
const { fields, body, bodyStartLine, error: parseError } = readNoteContent(content);
|
|
18
|
+
notes.push({
|
|
19
|
+
path,
|
|
20
|
+
relativePath,
|
|
21
|
+
fields,
|
|
22
|
+
body,
|
|
23
|
+
content,
|
|
24
|
+
bodyStartLine,
|
|
25
|
+
...(parseError !== undefined && { error: parseError }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
process.stderr.write(`kb: warning: could not read note ${path}; skipping: ${message}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return notes;
|
|
34
|
+
}
|
|
35
|
+
async function collectNoteLocations(input) {
|
|
8
36
|
const { kbRoot, config } = input;
|
|
9
37
|
const matcher = createNoteScopeMatcher(config);
|
|
10
38
|
const topLevelDirs = leadingLiteralSegments(config.targets);
|
|
11
|
-
const
|
|
12
|
-
await walk({ root: kbRoot, dir: kbRoot, matcher, topLevelDirs, out:
|
|
13
|
-
return
|
|
39
|
+
const locations = [];
|
|
40
|
+
await walk({ root: kbRoot, dir: kbRoot, matcher, topLevelDirs, out: locations });
|
|
41
|
+
return locations;
|
|
14
42
|
}
|
|
15
43
|
function leadingLiteralSegments(targets) {
|
|
16
44
|
const dirs = new Set();
|
|
@@ -50,22 +78,6 @@ async function walk(input) {
|
|
|
50
78
|
continue;
|
|
51
79
|
if (!matcher.isNote(relativePath))
|
|
52
80
|
continue;
|
|
53
|
-
|
|
54
|
-
const content = await readFile(absolutePath, 'utf8');
|
|
55
|
-
const { fields, body, bodyStartLine, error: parseError } = readNoteContent(content);
|
|
56
|
-
out.push({
|
|
57
|
-
path: absolutePath,
|
|
58
|
-
relativePath,
|
|
59
|
-
fields,
|
|
60
|
-
body,
|
|
61
|
-
content,
|
|
62
|
-
bodyStartLine,
|
|
63
|
-
...(parseError !== undefined && { error: parseError }),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
-
process.stderr.write(`kb: warning: could not read note ${absolutePath}; skipping: ${message}\n`);
|
|
69
|
-
}
|
|
81
|
+
out.push({ path: absolutePath, relativePath });
|
|
70
82
|
}
|
|
71
83
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { check, type CheckResult } from './check.js';
|
|
2
|
-
export { type EnumeratedNote, enumerateNotes } from './enumerate.js';
|
|
2
|
+
export { type EnumeratedNote, enumerateNotePaths, enumerateNotes } from './enumerate.js';
|
package/dist/esm/check/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { check } from "./check.js";
|
|
2
|
-
export { enumerateNotes } from "./enumerate.js";
|
|
2
|
+
export { enumerateNotePaths, enumerateNotes } from "./enumerate.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { check } from "../../check/check.js";
|
|
2
2
|
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
|
-
import { findKbRoot } from "../../discovery/find-kb-root.js";
|
|
4
|
-
import { tryLoadKbRegistry } from "../../discovery/load-registry.js";
|
|
5
3
|
import { formatHuman, formatJson, summarize } from "../format.js";
|
|
4
|
+
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
5
|
+
import { resolveStore } from "../resolve-store.js";
|
|
6
6
|
import { resolveChangedPaths } from "../targeting/resolve-changed-paths.js";
|
|
7
7
|
import { selectNotes } from "../targeting/select-notes.js";
|
|
8
8
|
export const CHECK_HELP = `Usage: kb check [paths...] [options]
|
|
@@ -142,38 +142,6 @@ async function resolveSelection(input) {
|
|
|
142
142
|
return { ok: false, message: `no notes matched: ${selection.unmatched.join(', ')}` };
|
|
143
143
|
}
|
|
144
144
|
const selectedPaths = new Set(selection.selected.map((entry) => entry.path));
|
|
145
|
-
const findings = result.findings.filter((finding) => selectedPaths.has(finding.path));
|
|
145
|
+
const findings = result.findings.filter((finding) => finding.scope === 'vault' || selectedPaths.has(finding.path));
|
|
146
146
|
return { ok: true, scope, notes: selection.selected, findings };
|
|
147
147
|
}
|
|
148
|
-
async function resolveStore(input) {
|
|
149
|
-
if (input.explicitKb !== null) {
|
|
150
|
-
const { config } = await tryLoadKbRegistry({
|
|
151
|
-
projectDir: input.cwd,
|
|
152
|
-
...(input.home !== undefined && { home: input.home }),
|
|
153
|
-
});
|
|
154
|
-
const match = config.entries.find((entry) => entry.name === input.explicitKb);
|
|
155
|
-
if (match === undefined) {
|
|
156
|
-
return { ok: false, message: `--kb "${input.explicitKb}" does not match any registered knowledge base` };
|
|
157
|
-
}
|
|
158
|
-
return { ok: true, store: { name: match.name, path: match.path } };
|
|
159
|
-
}
|
|
160
|
-
const discovered = await findKbRoot({ startDir: input.cwd });
|
|
161
|
-
if (discovered === null) {
|
|
162
|
-
return { ok: false, message: 'no .kb/ directory found in the current directory or any ancestor' };
|
|
163
|
-
}
|
|
164
|
-
return { ok: true, store: { name: null, path: discovered.path } };
|
|
165
|
-
}
|
|
166
|
-
function takeInlineValue(arg, prefix) {
|
|
167
|
-
const value = arg.slice(prefix.length);
|
|
168
|
-
if (value === '') {
|
|
169
|
-
throw new Error(`${prefix.replace(/=$/, '')} requires a value`);
|
|
170
|
-
}
|
|
171
|
-
return value;
|
|
172
|
-
}
|
|
173
|
-
function takeValue(argv, index, flag) {
|
|
174
|
-
const next = argv[index + 1] ?? null;
|
|
175
|
-
if (next === null || next.startsWith('--')) {
|
|
176
|
-
throw new Error(`${flag} requires a value`);
|
|
177
|
-
}
|
|
178
|
-
return next;
|
|
179
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SelectKbPrompt } from '../select-kb-prompt.js';
|
|
2
2
|
import type { CommandOutput } from './check.js';
|
|
3
|
-
export declare const CREATE_HELP = "Usage: kb create [options]\n\nScaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.\n\nWhen the registry has no default knowledge base, the new store becomes the default.\nIf other knowledge bases are already registered, you are prompted to choose one (or set it later with \"kb set-default\").\n\nCreates:\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n content/, content/events/\n\nOptions:\n --name <name>
|
|
3
|
+
export declare const CREATE_HELP = "Usage: kb create [options]\n\nScaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.\n\nRegistering leaves the registry's entries in alphabetical order, preserving its comments and formatting.\n\nWhen the registry has no default knowledge base, the new store becomes the default.\nIf other knowledge bases are already registered, you are prompted to choose one (or set it later with \"kb set-default\").\n\nCreates:\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n content/, content/events/\n\nOptions:\n --description <text> Description for the registry entry; cannot be combined with --no-register.\n --name <name> Registry name for the store. Defaults to the directory name.\n --no-register Scaffold without writing the kb.yaml registry entry.\n -h, --help Show this help.\n\nExit codes:\n 0 store created\n 2 usage error, an existing .kb/ in the directory, or an already-registered name\n";
|
|
4
4
|
export declare function runCreate(input: {
|
|
5
5
|
argv: readonly string[];
|
|
6
6
|
cwd: string;
|
|
@@ -8,6 +8,7 @@ export declare function runCreate(input: {
|
|
|
8
8
|
selectKb?: SelectKbPrompt;
|
|
9
9
|
}): Promise<CommandOutput>;
|
|
10
10
|
interface CreateOptions {
|
|
11
|
+
description: string | null;
|
|
11
12
|
name: string | null;
|
|
12
13
|
noRegister: boolean;
|
|
13
14
|
help: boolean;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { create } from "../../create/create.js";
|
|
4
|
+
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
4
5
|
import { runSetDefault } from "./set-default.js";
|
|
5
6
|
export const CREATE_HELP = `Usage: kb create [options]
|
|
6
7
|
|
|
7
8
|
Scaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.
|
|
8
9
|
|
|
10
|
+
Registering leaves the registry's entries in alphabetical order, preserving its comments and formatting.
|
|
11
|
+
|
|
9
12
|
When the registry has no default knowledge base, the new store becomes the default.
|
|
10
13
|
If other knowledge bases are already registered, you are prompted to choose one (or set it later with "kb set-default").
|
|
11
14
|
|
|
@@ -15,9 +18,10 @@ Creates:
|
|
|
15
18
|
content/, content/events/
|
|
16
19
|
|
|
17
20
|
Options:
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
-
|
|
21
|
+
--description <text> Description for the registry entry; cannot be combined with --no-register.
|
|
22
|
+
--name <name> Registry name for the store. Defaults to the directory name.
|
|
23
|
+
--no-register Scaffold without writing the kb.yaml registry entry.
|
|
24
|
+
-h, --help Show this help.
|
|
21
25
|
|
|
22
26
|
Exit codes:
|
|
23
27
|
0 store created
|
|
@@ -38,7 +42,12 @@ export async function runCreate(input) {
|
|
|
38
42
|
const base = { targetDir: input.cwd, ...(options.name !== null && { name: options.name }) };
|
|
39
43
|
const outcome = options.noRegister
|
|
40
44
|
? await create({ ...base, register: false })
|
|
41
|
-
: await create({
|
|
45
|
+
: await create({
|
|
46
|
+
...base,
|
|
47
|
+
register: true,
|
|
48
|
+
registryPath,
|
|
49
|
+
...(options.description !== null && { description: options.description }),
|
|
50
|
+
});
|
|
42
51
|
if (!outcome.ok) {
|
|
43
52
|
return { exitCode: 2, stdout: '', stderr: `kb create: ${outcome.message}\n` };
|
|
44
53
|
}
|
|
@@ -57,6 +66,7 @@ export async function runCreate(input) {
|
|
|
57
66
|
return { exitCode: 0, stdout: summary + selection.stdout, stderr: selection.stderr };
|
|
58
67
|
}
|
|
59
68
|
export function parseCreateArgs(argv) {
|
|
69
|
+
let description = null;
|
|
60
70
|
let name = null;
|
|
61
71
|
let noRegister = false;
|
|
62
72
|
let help = false;
|
|
@@ -68,30 +78,34 @@ export function parseCreateArgs(argv) {
|
|
|
68
78
|
help = true;
|
|
69
79
|
continue;
|
|
70
80
|
}
|
|
81
|
+
if (arg === '--description') {
|
|
82
|
+
description = takeValue(argv, index, '--description');
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith('--description=')) {
|
|
87
|
+
description = takeInlineValue(arg, '--description=');
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
71
90
|
if (arg === '--no-register') {
|
|
72
91
|
noRegister = true;
|
|
73
92
|
continue;
|
|
74
93
|
}
|
|
75
94
|
if (arg === '--name') {
|
|
76
|
-
|
|
77
|
-
if (next === null || next.startsWith('--')) {
|
|
78
|
-
throw new Error('--name requires a value');
|
|
79
|
-
}
|
|
80
|
-
name = next;
|
|
95
|
+
name = takeValue(argv, index, '--name');
|
|
81
96
|
index += 1;
|
|
82
97
|
continue;
|
|
83
98
|
}
|
|
84
99
|
if (arg.startsWith('--name=')) {
|
|
85
|
-
|
|
86
|
-
if (value === '') {
|
|
87
|
-
throw new Error('--name requires a value');
|
|
88
|
-
}
|
|
89
|
-
name = value;
|
|
100
|
+
name = takeInlineValue(arg, '--name=');
|
|
90
101
|
continue;
|
|
91
102
|
}
|
|
92
103
|
throw new Error(`unknown flag: ${arg}`);
|
|
93
104
|
}
|
|
94
|
-
|
|
105
|
+
if (noRegister && description !== null) {
|
|
106
|
+
throw new Error('--description cannot be combined with --no-register');
|
|
107
|
+
}
|
|
108
|
+
return { description, name, noRegister, help };
|
|
95
109
|
}
|
|
96
110
|
const UNSET_DEFAULT_HINT = 'Multiple knowledge bases are registered and no default is set. Run `kb set-default` to choose one.\n';
|
|
97
111
|
function buildUsageError(error) {
|
|
@@ -104,6 +118,9 @@ function formatCreated(created, registryPath) {
|
|
|
104
118
|
lines.push(` ${path}`);
|
|
105
119
|
}
|
|
106
120
|
lines.push(created.registered ? `Registered in ${registryPath}` : 'Not registered (--no-register).');
|
|
121
|
+
if (created.description !== undefined) {
|
|
122
|
+
lines.push(`Description: ${created.description}`);
|
|
123
|
+
}
|
|
107
124
|
if (created.defaultKb === 'set') {
|
|
108
125
|
lines.push('Set as the default knowledge base.');
|
|
109
126
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CommandOutput } from './check.js';
|
|
2
|
+
export declare const TAXONOMY_HELP = "Usage: kb taxonomy init [options]\n\nDerive a starting taxonomy from the notes a knowledge base already holds, so a\ntaxonomy can be introduced to a populated store without every folder reporting\nas undeclared. Every folder holding notes is declared, along with each of its\nancestors, under \"provisional:\" with no description: the command cannot invent\ndescriptions, and provisional already means \"declared, not yet reviewed\".\n\nOptions:\n --kb <name> Use the named store from the kb.yaml registry. Without it, the\n nearest ancestor .kb/ directory is used.\n --merge Add only the domains an existing taxonomy does not declare.\n Without it, a store that already has a taxonomy is left\n untouched.\n -h, --help Show this help.\n\nExit codes:\n 0 the taxonomy was written, or already declared every derived domain\n 2 usage error, unresolvable store, a store marked readonly in kb.yaml,\n malformed config or taxonomy, or an existing taxonomy without --merge\n";
|
|
3
|
+
export declare function runTaxonomy(input: {
|
|
4
|
+
argv: readonly string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
home?: string;
|
|
7
|
+
}): Promise<CommandOutput>;
|
|
8
|
+
interface TaxonomyOptions {
|
|
9
|
+
subcommand: 'init' | null;
|
|
10
|
+
kb: string | null;
|
|
11
|
+
merge: boolean;
|
|
12
|
+
help: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseTaxonomyArgs(argv: readonly string[]): TaxonomyOptions;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { enumerateNotes } from "../../check/enumerate.js";
|
|
2
|
+
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
|
+
import { loadKbConfig } from "../../config/load-config.js";
|
|
4
|
+
import { resolveKbDir, TAXONOMY_FILE } from "../../layout/index.js";
|
|
5
|
+
import { deriveDomains } from "../../taxonomy/domain-paths.js";
|
|
6
|
+
import { loadTaxonomy } from "../../taxonomy/load-taxonomy.js";
|
|
7
|
+
import { writeTaxonomy } from "../../taxonomy/write-taxonomy.js";
|
|
8
|
+
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
9
|
+
import { resolveStore } from "../resolve-store.js";
|
|
10
|
+
export const TAXONOMY_HELP = `Usage: kb taxonomy init [options]
|
|
11
|
+
|
|
12
|
+
Derive a starting taxonomy from the notes a knowledge base already holds, so a
|
|
13
|
+
taxonomy can be introduced to a populated store without every folder reporting
|
|
14
|
+
as undeclared. Every folder holding notes is declared, along with each of its
|
|
15
|
+
ancestors, under "provisional:" with no description: the command cannot invent
|
|
16
|
+
descriptions, and provisional already means "declared, not yet reviewed".
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--kb <name> Use the named store from the kb.yaml registry. Without it, the
|
|
20
|
+
nearest ancestor .kb/ directory is used.
|
|
21
|
+
--merge Add only the domains an existing taxonomy does not declare.
|
|
22
|
+
Without it, a store that already has a taxonomy is left
|
|
23
|
+
untouched.
|
|
24
|
+
-h, --help Show this help.
|
|
25
|
+
|
|
26
|
+
Exit codes:
|
|
27
|
+
0 the taxonomy was written, or already declared every derived domain
|
|
28
|
+
2 usage error, unresolvable store, a store marked readonly in kb.yaml,
|
|
29
|
+
malformed config or taxonomy, or an existing taxonomy without --merge
|
|
30
|
+
`;
|
|
31
|
+
export async function runTaxonomy(input) {
|
|
32
|
+
let options;
|
|
33
|
+
try {
|
|
34
|
+
options = parseTaxonomyArgs(input.argv);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${message}\n${TAXONOMY_HELP}` };
|
|
39
|
+
}
|
|
40
|
+
if (options.help) {
|
|
41
|
+
return { exitCode: 0, stdout: TAXONOMY_HELP, stderr: '' };
|
|
42
|
+
}
|
|
43
|
+
if (options.subcommand === null) {
|
|
44
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: no subcommand given\n${TAXONOMY_HELP}` };
|
|
45
|
+
}
|
|
46
|
+
const resolved = await resolveStore({
|
|
47
|
+
explicitKb: options.kb,
|
|
48
|
+
cwd: input.cwd,
|
|
49
|
+
...(input.home !== undefined && { home: input.home }),
|
|
50
|
+
});
|
|
51
|
+
if (!resolved.ok) {
|
|
52
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${resolved.message}\n` };
|
|
53
|
+
}
|
|
54
|
+
if (resolved.readonly) {
|
|
55
|
+
const name = resolved.store.name ?? resolved.store.path;
|
|
56
|
+
const message = `knowledge base "${name}" is marked readonly in kb.yaml; taxonomy init is refused`;
|
|
57
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${message}\n` };
|
|
58
|
+
}
|
|
59
|
+
const kbRoot = { path: resolved.store.path, kbDir: resolveKbDir(resolved.store.path) };
|
|
60
|
+
try {
|
|
61
|
+
return await initTaxonomy({ kbRoot, merge: options.merge });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (isKbLoaderError(error)) {
|
|
65
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${error.message}\n` };
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function parseTaxonomyArgs(argv) {
|
|
71
|
+
let subcommand = null;
|
|
72
|
+
let kb = null;
|
|
73
|
+
let merge = false;
|
|
74
|
+
let help = false;
|
|
75
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
76
|
+
const arg = argv[index];
|
|
77
|
+
if (arg === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (arg === '--help' || arg === '-h') {
|
|
80
|
+
help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg === '--merge') {
|
|
84
|
+
merge = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === '--kb') {
|
|
88
|
+
kb = takeValue(argv, index, '--kb');
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg.startsWith('--kb=')) {
|
|
93
|
+
kb = takeInlineValue(arg, '--kb=');
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg.startsWith('-')) {
|
|
97
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
98
|
+
}
|
|
99
|
+
if (arg !== 'init') {
|
|
100
|
+
throw new Error(`unknown subcommand: ${arg}`);
|
|
101
|
+
}
|
|
102
|
+
if (subcommand !== null) {
|
|
103
|
+
throw new Error('only one subcommand may be given');
|
|
104
|
+
}
|
|
105
|
+
subcommand = arg;
|
|
106
|
+
}
|
|
107
|
+
return { subcommand, kb, merge, help };
|
|
108
|
+
}
|
|
109
|
+
async function initTaxonomy(input) {
|
|
110
|
+
const { kbRoot, merge } = input;
|
|
111
|
+
const existing = await loadTaxonomy({ kbRoot });
|
|
112
|
+
if (existing.size > 0 && !merge) {
|
|
113
|
+
const message = `${TAXONOMY_FILE} already declares ${existing.size} domains; pass --merge to add the missing ones`;
|
|
114
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${message}\n` };
|
|
115
|
+
}
|
|
116
|
+
const config = await loadKbConfig({ kbRoot });
|
|
117
|
+
const notes = await enumerateNotes({ kbRoot: kbRoot.path, config });
|
|
118
|
+
const domains = deriveDomains(notes.map((note) => note.relativePath));
|
|
119
|
+
if (domains.length === 0) {
|
|
120
|
+
return { exitCode: 0, stdout: `no assertion folders hold notes; ${TAXONOMY_FILE} not written\n`, stderr: '' };
|
|
121
|
+
}
|
|
122
|
+
const { added } = await writeTaxonomy({
|
|
123
|
+
kbRoot,
|
|
124
|
+
declarations: domains.map((path) => ({ path, provisional: true })),
|
|
125
|
+
});
|
|
126
|
+
if (added.length === 0) {
|
|
127
|
+
return { exitCode: 0, stdout: `${TAXONOMY_FILE} already declares every derived domain\n`, stderr: '' };
|
|
128
|
+
}
|
|
129
|
+
return { exitCode: 0, stdout: `declared ${added.length} domains in ${TAXONOMY_FILE}\n`, stderr: '' };
|
|
130
|
+
}
|
package/dist/esm/cli/format.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export function formatHuman(input) {
|
|
2
2
|
const { summary, findings, targets, scope } = input;
|
|
3
|
-
if (summary.notes === 0) {
|
|
3
|
+
if (summary.notes === 0 && findings.length === 0) {
|
|
4
4
|
return `${zeroMatchLine(scope, targets)}\n`;
|
|
5
5
|
}
|
|
6
6
|
if (findings.length === 0) {
|
|
7
7
|
return `✓ no findings (${summary.notes} notes checked)\n`;
|
|
8
8
|
}
|
|
9
9
|
const lines = [];
|
|
10
|
+
if (summary.notes === 0) {
|
|
11
|
+
lines.push(zeroMatchLine(scope, targets), '');
|
|
12
|
+
}
|
|
10
13
|
for (const [path, group] of groupByPath(findings)) {
|
|
11
14
|
lines.push(path);
|
|
12
15
|
for (const finding of group) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function takeInlineValue(arg, prefix) {
|
|
2
|
+
const value = arg.slice(prefix.length);
|
|
3
|
+
if (value === '') {
|
|
4
|
+
throw new Error(`${prefix.replace(/=$/, '')} requires a value`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function takeValue(argv, index, flag) {
|
|
9
|
+
const next = argv[index + 1] ?? null;
|
|
10
|
+
if (next === null || next === '' || next.startsWith('--')) {
|
|
11
|
+
throw new Error(`${flag} requires a value`);
|
|
12
|
+
}
|
|
13
|
+
return next;
|
|
14
|
+
}
|