@williamthorsen/kb 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -12
- package/bin/kb.js +2 -0
- package/dist/esm/check/enumerate.js +3 -4
- package/dist/esm/cli/commands/check.js +2 -2
- package/dist/esm/cli/commands/create.js +2 -2
- package/dist/esm/cli/commands/set-default.js +3 -3
- package/dist/esm/cli/commands/taxonomy.js +2 -2
- package/dist/esm/cli/index.js +2 -2
- package/dist/esm/cli/run.js +2 -1
- package/dist/esm/cli/select-kb-prompt.js +1 -1
- package/dist/esm/cli/targeting/resolve-changed-paths.js +2 -1
- package/dist/esm/config/kb-loader-error.d.ts +1 -1
- package/dist/esm/config/kb-loader-error.js +2 -2
- package/dist/esm/config/load-config.js +2 -2
- package/dist/esm/discovery/load-registry.js +7 -6
- package/dist/esm/filesystem/write-atomic.d.ts +1 -0
- package/dist/esm/filesystem/write-atomic.js +17 -0
- package/dist/esm/frontmatter/parse-note.js +1 -1
- package/dist/esm/note-io/write-note.js +2 -12
- package/dist/esm/records/event.js +2 -1
- package/dist/esm/tags/load-aliases.js +4 -4
- package/dist/esm/taxonomy/load-taxonomy.js +2 -2
- package/dist/esm/taxonomy/write-taxonomy.js +4 -14
- package/dist/esm/vault-integrity/check-vault-integrity.js +1 -1
- package/package.json +4 -4
- package/dist/esm/lints/index.d.ts +0 -3
- package/dist/esm/lints/index.js +0 -3
package/README.md
CHANGED
|
@@ -4,17 +4,7 @@ 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 -->
|
|
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 -->
|
|
7
|
+
<!-- section:release-notes --><!-- /section:release-notes -->
|
|
18
8
|
|
|
19
9
|
## Exports
|
|
20
10
|
|
|
@@ -284,7 +274,7 @@ Without `--merge`, a store that already declares a taxonomy is left untouched an
|
|
|
284
274
|
|
|
285
275
|
## Error and exception model
|
|
286
276
|
|
|
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.
|
|
277
|
+
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.
|
|
288
278
|
|
|
289
279
|
## MCP wrappability
|
|
290
280
|
|
package/bin/kb.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Imports only node builtins: a top-level import resolves before the gate below runs, so an
|
|
4
|
+
// unresolvable dependency would replace this file's build-first message with ERR_MODULE_NOT_FOUND.
|
|
3
5
|
import { existsSync } from 'node:fs';
|
|
4
6
|
|
|
5
7
|
// Thin wrapper so pnpm can symlink the bin at install time, before `dist/` exists.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
5
|
import { createNoteScopeMatcher } from "../config/note-scope.js";
|
|
5
6
|
import { readNoteContent } from "../note-io/read-note.js";
|
|
6
7
|
import { isGlobSegment } from "./glob-segments.js";
|
|
@@ -26,8 +27,7 @@ export async function enumerateNotes(input) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
|
-
|
|
30
|
-
process.stderr.write(`kb: warning: could not read note ${path}; skipping: ${message}\n`);
|
|
30
|
+
process.stderr.write(`kb: warning: could not read note ${path}; skipping: ${describeError(error)}\n`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
return notes;
|
|
@@ -58,8 +58,7 @@ async function walk(input) {
|
|
|
58
58
|
entries = await readdir(dir, { withFileTypes: true });
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
|
-
|
|
62
|
-
process.stderr.write(`kb: warning: could not read directory ${dir}; skipping: ${message}\n`);
|
|
61
|
+
process.stderr.write(`kb: warning: could not read directory ${dir}; skipping: ${describeError(error)}\n`);
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
65
64
|
const atRoot = dir === root;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
2
|
import { check } from "../../check/check.js";
|
|
2
3
|
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
4
|
import { formatHuman, formatJson, summarize } from "../format.js";
|
|
@@ -119,8 +120,7 @@ export function parseCheckArgs(argv) {
|
|
|
119
120
|
return { kb, json, help, patterns, vs };
|
|
120
121
|
}
|
|
121
122
|
function buildUsageError(error) {
|
|
122
|
-
|
|
123
|
-
return { exitCode: 2, stdout: '', stderr: `kb check: ${message}\n${CHECK_HELP}` };
|
|
123
|
+
return { exitCode: 2, stdout: '', stderr: `kb check: ${describeError(error)}\n${CHECK_HELP}` };
|
|
124
124
|
}
|
|
125
125
|
async function resolveSelection(input) {
|
|
126
126
|
const { options, store, result } = input;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { create } from "../../create/create.js";
|
|
4
5
|
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
5
6
|
import { runSetDefault } from "./set-default.js";
|
|
@@ -109,8 +110,7 @@ export function parseCreateArgs(argv) {
|
|
|
109
110
|
}
|
|
110
111
|
const UNSET_DEFAULT_HINT = 'Multiple knowledge bases are registered and no default is set. Run `kb set-default` to choose one.\n';
|
|
111
112
|
function buildUsageError(error) {
|
|
112
|
-
|
|
113
|
-
return { exitCode: 2, stdout: '', stderr: `kb create: ${message}\n${CREATE_HELP}` };
|
|
113
|
+
return { exitCode: 2, stdout: '', stderr: `kb create: ${describeError(error)}\n${CREATE_HELP}` };
|
|
114
114
|
}
|
|
115
115
|
function formatCreated(created, registryPath) {
|
|
116
116
|
const lines = [`Created knowledge base "${created.name}" at ${created.storePath}`];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { tryLoadKbRegistry } from "../../discovery/load-registry.js";
|
|
4
5
|
import { clearDefaultKb, setDefaultKb } from "../../discovery/set-default-kb.js";
|
|
5
6
|
export const SET_DEFAULT_HELP = `Usage: kb set-default [name] [options]
|
|
@@ -38,11 +39,11 @@ export async function runSetDefault(input) {
|
|
|
38
39
|
if (error !== undefined) {
|
|
39
40
|
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${error}\n` };
|
|
40
41
|
}
|
|
41
|
-
const entries = config.entries;
|
|
42
42
|
if (options.none) {
|
|
43
43
|
await clearDefaultKb({ registryPath });
|
|
44
44
|
return { exitCode: 0, stdout: 'The default knowledge base has been cleared.\n', stderr: '' };
|
|
45
45
|
}
|
|
46
|
+
const entries = config.entries;
|
|
46
47
|
if (options.name !== null) {
|
|
47
48
|
if (entries.length === 0) {
|
|
48
49
|
return { exitCode: 2, stdout: '', stderr: buildNoStoresMessage() };
|
|
@@ -118,6 +119,5 @@ function buildSetConfirmation(name) {
|
|
|
118
119
|
return `Default knowledge base has been set to "${name}".\n`;
|
|
119
120
|
}
|
|
120
121
|
function buildUsageError(error) {
|
|
121
|
-
|
|
122
|
-
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${message}\n${SET_DEFAULT_HELP}` };
|
|
122
|
+
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${describeError(error)}\n${SET_DEFAULT_HELP}` };
|
|
123
123
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
2
|
import { enumerateNotes } from "../../check/enumerate.js";
|
|
2
3
|
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
4
|
import { loadKbConfig } from "../../config/load-config.js";
|
|
@@ -34,8 +35,7 @@ export async function runTaxonomy(input) {
|
|
|
34
35
|
options = parseTaxonomyArgs(input.argv);
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
|
-
|
|
38
|
-
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${message}\n${TAXONOMY_HELP}` };
|
|
38
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${describeError(error)}\n${TAXONOMY_HELP}` };
|
|
39
39
|
}
|
|
40
40
|
if (options.help) {
|
|
41
41
|
return { exitCode: 0, stdout: TAXONOMY_HELP, stderr: '' };
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { run } from "./run.js";
|
|
3
4
|
import { readlineSelectKbPrompt } from "./select-kb-prompt.js";
|
|
4
5
|
async function main() {
|
|
@@ -12,8 +13,7 @@ async function main() {
|
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
|
-
|
|
16
|
-
process.stderr.write(`kb: unexpected error: ${message}\n`);
|
|
16
|
+
process.stderr.write(`kb: unexpected error: ${describeError(error)}\n`);
|
|
17
17
|
process.exit(2);
|
|
18
18
|
}
|
|
19
19
|
if (output.stdout !== '')
|
package/dist/esm/cli/run.js
CHANGED
|
@@ -12,9 +12,10 @@ Commands:
|
|
|
12
12
|
|
|
13
13
|
Run "kb <command> --help" for command options.
|
|
14
14
|
`;
|
|
15
|
+
const HELP_COMMANDS = new Set([undefined, '--help', '-h']);
|
|
15
16
|
export async function run(input) {
|
|
16
17
|
const [command, ...rest] = input.argv;
|
|
17
|
-
if (command
|
|
18
|
+
if (HELP_COMMANDS.has(command)) {
|
|
18
19
|
return { exitCode: 0, stdout: HELP, stderr: '' };
|
|
19
20
|
}
|
|
20
21
|
if (command === 'check') {
|
|
@@ -14,7 +14,7 @@ export function parseSelection(answer, kbCount) {
|
|
|
14
14
|
if (answer === '')
|
|
15
15
|
return { kind: 'cancel' };
|
|
16
16
|
const choice = Number(answer);
|
|
17
|
-
if (!Number.
|
|
17
|
+
if (!Number.isSafeInteger(choice))
|
|
18
18
|
return null;
|
|
19
19
|
if (choice >= 1 && choice <= kbCount)
|
|
20
20
|
return { kind: 'kb', index: choice - 1 };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
5
|
import { isRecord } from "../../type-guards.js";
|
|
5
6
|
export function resolveChangedPaths(input) {
|
|
6
7
|
const { storeRoot, ref } = input;
|
|
@@ -37,7 +38,7 @@ function extractGitErrorMessage(error) {
|
|
|
37
38
|
if (isRecord(error) && typeof error.stderr === 'string' && error.stderr.trim() !== '') {
|
|
38
39
|
return error.stderr.trim();
|
|
39
40
|
}
|
|
40
|
-
return
|
|
41
|
+
return describeError(error);
|
|
41
42
|
}
|
|
42
43
|
function tryGit(storeRoot, args) {
|
|
43
44
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { CONFIG_FILE } from "../layout/index.js";
|
|
5
6
|
import { isEnoent } from "../type-guards.js";
|
|
@@ -22,8 +23,7 @@ export async function loadKbConfig(input) {
|
|
|
22
23
|
parsed = parse(text);
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
|
|
26
|
-
throw new KbLoaderError(`${path}: malformed YAML — ${message}`);
|
|
26
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(error)}`, { cause: error });
|
|
27
27
|
}
|
|
28
28
|
const result = configFileShape.safeParse(parsed ?? {});
|
|
29
29
|
if (!result.success) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
4
6
|
import { parse } from 'yaml';
|
|
5
7
|
import { isEnoent } from "../type-guards.js";
|
|
6
8
|
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
@@ -36,8 +38,7 @@ export async function tryLoadKbRegistry(input = {}) {
|
|
|
36
38
|
return { config: await loadKbRegistry(input) };
|
|
37
39
|
}
|
|
38
40
|
catch (error) {
|
|
39
|
-
|
|
40
|
-
return { config: { entries: [], sources: {} }, error: message };
|
|
41
|
+
return { config: { entries: [], sources: {} }, error: describeError(error) };
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
function expandTilde(value, home) {
|
|
@@ -65,8 +66,7 @@ async function loadRegistryFile(path, source, home) {
|
|
|
65
66
|
parsed = parse(text);
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
|
68
|
-
|
|
69
|
-
throw new Error(`${path}: malformed YAML — ${message}`);
|
|
69
|
+
throw chainError(`${path}: malformed YAML`, error);
|
|
70
70
|
}
|
|
71
71
|
if (parsed === null || parsed === undefined) {
|
|
72
72
|
return { entries: [] };
|
|
@@ -77,7 +77,8 @@ async function loadRegistryFile(path, source, home) {
|
|
|
77
77
|
}
|
|
78
78
|
const configDir = dirname(path);
|
|
79
79
|
const entries = [];
|
|
80
|
-
|
|
80
|
+
const fileEntries = Object.entries(result.data.kbs ?? {});
|
|
81
|
+
for (const [name, fileEntry] of fileEntries) {
|
|
81
82
|
entries.push({
|
|
82
83
|
name,
|
|
83
84
|
path: resolvePath(fileEntry.path, configDir, home),
|
|
@@ -96,7 +97,7 @@ function mergeEntries(userEntries, projectEntries) {
|
|
|
96
97
|
for (const entry of projectEntries) {
|
|
97
98
|
byName.set(entry.name, entry);
|
|
98
99
|
}
|
|
99
|
-
return
|
|
100
|
+
return byName.values().toArray();
|
|
100
101
|
}
|
|
101
102
|
function resolveDefaultKb(entries, name, sourcePath) {
|
|
102
103
|
if (name === undefined) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeAtomic(path: string, content: string): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
export async function writeAtomic(path, content) {
|
|
4
|
+
const tempPath = `${path}.${randomBytes(8).toString('hex')}.tmp`;
|
|
5
|
+
try {
|
|
6
|
+
await writeFile(tempPath, content, 'utf8');
|
|
7
|
+
await rename(tempPath, path);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
try {
|
|
11
|
+
await unlink(tempPath);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -91,7 +91,7 @@ function toFrontmatter(doc) {
|
|
|
91
91
|
tags = stringList(item.value);
|
|
92
92
|
break;
|
|
93
93
|
default:
|
|
94
|
-
extra[key] = key
|
|
94
|
+
extra[key] = Object.hasOwn(plainRecord, key) ? plainRecord[key] : null;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
return { title, recordType, created, updated, tags, extra };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { rename, unlink, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { writeAtomic } from "../filesystem/write-atomic.js";
|
|
3
2
|
import { renderFrontmatterFields } from "./yaml-fields.js";
|
|
4
3
|
const FENCE = '---';
|
|
5
4
|
export function renderNote(fields, body) {
|
|
@@ -8,14 +7,5 @@ export function renderNote(fields, body) {
|
|
|
8
7
|
return `${FENCE}\n${frontmatter}\n${FENCE}\n\n${normalizedBody}`;
|
|
9
8
|
}
|
|
10
9
|
export async function writeNote(path, fields, body) {
|
|
11
|
-
|
|
12
|
-
const tempPath = `${path}.${randomBytes(8).toString('hex')}.tmp`;
|
|
13
|
-
await writeFile(tempPath, content, 'utf8');
|
|
14
|
-
try {
|
|
15
|
-
await rename(tempPath, path);
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
await unlink(tempPath).catch(() => { });
|
|
19
|
-
throw error;
|
|
20
|
-
}
|
|
10
|
+
await writeAtomic(path, renderNote(fields, body));
|
|
21
11
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { asStringList, isValidDate } from "../note-io/field-validators.js";
|
|
2
2
|
export const EVENT_IMPACT_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
3
3
|
const IMPACT_LEVEL_SET = new Set(EVENT_IMPACT_LEVELS);
|
|
4
|
+
const NOT_SUPPLIED_VALUES = new Set([undefined, null, '']);
|
|
4
5
|
export function isEventImpact(value) {
|
|
5
6
|
return typeof value === 'string' && IMPACT_LEVEL_SET.has(value);
|
|
6
7
|
}
|
|
@@ -110,7 +111,7 @@ function readListField(value, field, errors) {
|
|
|
110
111
|
return list;
|
|
111
112
|
}
|
|
112
113
|
function readOptionalNonEmptyString(value, field, errors) {
|
|
113
|
-
if (value
|
|
114
|
+
if (NOT_SUPPLIED_VALUES.has(value)) {
|
|
114
115
|
return undefined;
|
|
115
116
|
}
|
|
116
117
|
if (typeof value === 'string') {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
3
5
|
import { parse } from 'yaml';
|
|
4
6
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
5
7
|
import { ALIASES_FILE } from "../layout/index.js";
|
|
@@ -20,8 +22,7 @@ export async function loadAliases(input) {
|
|
|
20
22
|
return parseAliases(text, path);
|
|
21
23
|
}
|
|
22
24
|
catch (error) {
|
|
23
|
-
|
|
24
|
-
throw new KbLoaderError(message);
|
|
25
|
+
throw new KbLoaderError(describeError(error), { cause: error });
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
export function parseAliases(text, contextLabel = 'tag-aliases') {
|
|
@@ -30,8 +31,7 @@ export function parseAliases(text, contextLabel = 'tag-aliases') {
|
|
|
30
31
|
parsed = parse(text);
|
|
31
32
|
}
|
|
32
33
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
throw new Error(`${contextLabel}: malformed YAML — ${message}`);
|
|
34
|
+
throw chainError(`${contextLabel}: malformed YAML`, error);
|
|
35
35
|
}
|
|
36
36
|
if (!isRecord(parsed)) {
|
|
37
37
|
throw new Error(`${contextLabel}: top-level must be a mapping`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
5
6
|
import { TAXONOMY_FILE } from "../layout/index.js";
|
|
@@ -22,8 +23,7 @@ export async function loadTaxonomy(input) {
|
|
|
22
23
|
parsed = parse(text);
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
|
|
26
|
-
throw new KbLoaderError(`${path}: malformed YAML — ${message}`);
|
|
26
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(error)}`, { cause: error });
|
|
27
27
|
}
|
|
28
28
|
const result = taxonomyFileShape.safeParse(parsed ?? {});
|
|
29
29
|
if (!result.success) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { isMap, isPair, isScalar, parseDocument } from 'yaml';
|
|
5
5
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
6
|
+
import { writeAtomic } from "../filesystem/write-atomic.js";
|
|
6
7
|
import { TAXONOMY_FILE } from "../layout/index.js";
|
|
7
8
|
import { isEnoent, isRecord } from "../type-guards.js";
|
|
8
9
|
import { describeKeyDefect } from "./taxonomy-schema.js";
|
|
@@ -98,7 +99,7 @@ async function readDocument(path) {
|
|
|
98
99
|
const document = parseDocument(text);
|
|
99
100
|
const firstError = document.errors[0];
|
|
100
101
|
if (firstError !== undefined) {
|
|
101
|
-
throw new KbLoaderError(`${path}: malformed YAML
|
|
102
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(firstError)}`, { cause: firstError });
|
|
102
103
|
}
|
|
103
104
|
if (document.contents !== null && !isMap(document.contents)) {
|
|
104
105
|
throw new KbLoaderError(`${path}: top-level must be a mapping`);
|
|
@@ -129,14 +130,3 @@ function sortByPath(declarations) {
|
|
|
129
130
|
return a.path < b.path ? -1 : 1;
|
|
130
131
|
});
|
|
131
132
|
}
|
|
132
|
-
async function writeAtomic(path, content) {
|
|
133
|
-
const tempPath = `${path}.${randomBytes(8).toString('hex')}.tmp`;
|
|
134
|
-
await writeFile(tempPath, content, 'utf8');
|
|
135
|
-
try {
|
|
136
|
-
await rename(tempPath, path);
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
await unlink(tempPath).catch(() => { });
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -6,7 +6,7 @@ export function checkVaultIntegrity(notes) {
|
|
|
6
6
|
}
|
|
7
7
|
function basenameFindings(vaultIndex) {
|
|
8
8
|
const findings = [];
|
|
9
|
-
for (const key of
|
|
9
|
+
for (const key of vaultIndex.keys().toArray().toSorted()) {
|
|
10
10
|
const paths = vaultIndex.get(key);
|
|
11
11
|
if (paths === undefined || paths.size < 2)
|
|
12
12
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/kb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Knowledge-base foundation: discovery, config, frontmatter parsing, records, tags, and vault-integrity checks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontmatter",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
22
|
+
"sideEffects": false,
|
|
22
23
|
"type": "module",
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"dist"
|
|
97
98
|
],
|
|
98
99
|
"dependencies": {
|
|
100
|
+
"@williamthorsen/toolbelt.errors": "0.5.0",
|
|
99
101
|
"picomatch": "4.0.5",
|
|
100
102
|
"yaml": "2.9.0",
|
|
101
103
|
"zod": "4.4.3"
|
|
@@ -109,7 +111,5 @@
|
|
|
109
111
|
"publishConfig": {
|
|
110
112
|
"access": "public"
|
|
111
113
|
},
|
|
112
|
-
"scripts": {
|
|
113
|
-
"build": "nmr compile"
|
|
114
|
-
}
|
|
114
|
+
"scripts": {}
|
|
115
115
|
}
|
package/dist/esm/lints/index.js
DELETED