@williamthorsen/kb 0.6.1 → 0.7.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 +120 -7
- package/dist/esm/check/check.d.ts +24 -0
- 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/cli/commands/check.d.ts +22 -0
- 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 +12 -0
- 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/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 +63 -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 +13 -0
- package/dist/esm/vault-integrity/wikilink-parse.d.ts +22 -0
- package/package.json +14 -5
- package/dist/esm/create/render-seeds.d.ts +0 -2
- package/dist/esm/create/render-seeds.js +0 -29
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomically writes `content` to `path` via a temp file plus `rename`, so a concurrent reader never sees a partial
|
|
3
|
+
* write. The temp file is a sibling of the target, keeping the rename within one filesystem, where it is atomic. On
|
|
4
|
+
* failure the temp file is cleaned up best-effort and the error re-thrown.
|
|
5
|
+
*/
|
|
1
6
|
export declare function writeAtomic(path: string, content: string): Promise<void>;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ParsedNote } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a note from disk and parse it into a `ParsedNote`. I/O errors (e.g. a missing file) are thrown;
|
|
4
|
+
* YAML parse errors are not.
|
|
5
|
+
*/
|
|
2
6
|
export declare function parseNote(input: {
|
|
3
7
|
path: string;
|
|
4
8
|
}): Promise<ParsedNote>;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a note from a literal string into a `ParsedNote` carrying typed frontmatter.
|
|
11
|
+
* Parse errors are recorded in `frontmatterRaw.parseError`, never thrown — the rule layer decides how to report them.
|
|
12
|
+
* `path` defaults to `<string>` and labels the result for diagnostics.
|
|
13
|
+
*/
|
|
5
14
|
export declare function parseNoteContent(input: {
|
|
6
15
|
content: string;
|
|
7
16
|
path?: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns every path git accounts for under `root` as an NFC-normalized set of root-relative, slash-separated paths:
|
|
3
|
+
* tracked files, plus untracked ones that no ignore rule covers. Returns `undefined` where git holds no opinion,
|
|
4
|
+
* because `root` lies outside a working tree or git cannot be run at all, so a caller keeps its own scope instead of
|
|
5
|
+
* reading an empty set as "git ignores everything".
|
|
6
|
+
*
|
|
7
|
+
* A tracked file stays in scope even where an ignore rule matches it, since every clone still sees it. That is why
|
|
8
|
+
* the scope unions the two `ls-files` forms rather than asking `git check-ignore`, which would report it ignored.
|
|
9
|
+
*
|
|
10
|
+
* Paths are normalized because git reports NFC where `readdir` reports NFD on macOS, and comparing the two forms
|
|
11
|
+
* unnormalized drops every path whose name carries a combining mark.
|
|
12
|
+
*/
|
|
13
|
+
export declare function listGitScope(input: {
|
|
14
|
+
root: string;
|
|
15
|
+
}): ReadonlySet<string> | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runGit } from "./run-git.js";
|
|
2
|
+
export function listGitScope(input) {
|
|
3
|
+
const tracked = runGit({ cwd: input.root, args: ['ls-files', '-z'], maxBuffer: GIT_MAX_BUFFER });
|
|
4
|
+
if (!tracked.ok)
|
|
5
|
+
return undefined;
|
|
6
|
+
const unignored = runGit({
|
|
7
|
+
cwd: input.root,
|
|
8
|
+
args: ['ls-files', '-z', '--others', '--exclude-standard'],
|
|
9
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
10
|
+
});
|
|
11
|
+
if (!unignored.ok)
|
|
12
|
+
return undefined;
|
|
13
|
+
const scope = new Set();
|
|
14
|
+
for (const stdout of [tracked.stdout, unignored.stdout]) {
|
|
15
|
+
for (const path of stdout.split('\0')) {
|
|
16
|
+
if (path !== '')
|
|
17
|
+
scope.add(path.normalize('NFC'));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return scope;
|
|
21
|
+
}
|
|
22
|
+
const GIT_MAX_BUFFER = 64 * 1_024 * 1_024;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The outcome of one git invocation: its stdout, or git's failure message. */
|
|
2
|
+
export type GitResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
stdout: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Runs `git -C cwd <args>` and returns its stdout, or a failure carrying git's own stderr. A git that cannot be
|
|
11
|
+
* spawned fails the same way as one that exits non-zero, so both reach the caller as a single "git did not answer".
|
|
12
|
+
*/
|
|
13
|
+
export declare function runGit(input: {
|
|
14
|
+
cwd: string;
|
|
15
|
+
args: readonly string[];
|
|
16
|
+
maxBuffer?: number;
|
|
17
|
+
}): GitResult;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { isRecord } from "../type-guards.js";
|
|
4
|
+
export function runGit(input) {
|
|
5
|
+
try {
|
|
6
|
+
const stdout = execFileSync('git', ['-C', input.cwd, ...input.args], {
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
...(input.maxBuffer !== undefined && { maxBuffer: input.maxBuffer }),
|
|
10
|
+
});
|
|
11
|
+
return { ok: true, stdout };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return { ok: false, message: extractGitErrorMessage(error) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function extractGitErrorMessage(error) {
|
|
18
|
+
if (isRecord(error) && typeof error.stderr === 'string' && error.stderr.trim() !== '') {
|
|
19
|
+
return error.stderr.trim();
|
|
20
|
+
}
|
|
21
|
+
return describeError(error);
|
|
22
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ALIASES_FILE, ASSERTIONS_DIR, ASSERTIONS_SEGMENT, buildEventPath, CONFIG_FILE, CONTENT_DIR, EVENTS_DIR, KB_DIR, resolveAssertionsDir, resolveEventPath, resolveEventsDir, resolveKbDir, TAXONOMY_FILE, } from './store-layout.js';
|
|
1
|
+
export { ALIASES_FILE, ASSERTIONS_DIR, ASSERTIONS_SEGMENT, buildEventPath, CONFIG_FILE, CONTENT_DIR, EDITORCONFIG_FILE, EVENTS_DIR, KB_DIR, PRETTIER_CONFIG_FILE, resolveAssertionsDir, resolveEventPath, resolveEventsDir, resolveKbDir, TAXONOMY_FILE, } from './store-layout.js';
|
package/dist/esm/layout/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ALIASES_FILE, ASSERTIONS_DIR, ASSERTIONS_SEGMENT, buildEventPath, CONFIG_FILE, CONTENT_DIR, EVENTS_DIR, KB_DIR, resolveAssertionsDir, resolveEventPath, resolveEventsDir, resolveKbDir, TAXONOMY_FILE, } from "./store-layout.js";
|
|
1
|
+
export { ALIASES_FILE, ASSERTIONS_DIR, ASSERTIONS_SEGMENT, buildEventPath, CONFIG_FILE, CONTENT_DIR, EDITORCONFIG_FILE, EVENTS_DIR, KB_DIR, PRETTIER_CONFIG_FILE, resolveAssertionsDir, resolveEventPath, resolveEventsDir, resolveKbDir, TAXONOMY_FILE, } from "./store-layout.js";
|
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
/** The store's metadata directory. Its presence marks a directory as a KB root. */
|
|
1
2
|
export declare const KB_DIR = ".kb";
|
|
3
|
+
/** The directory holding the store's notes. */
|
|
2
4
|
export declare const CONTENT_DIR = "content";
|
|
5
|
+
/** The `content/` subdirectory holding assertion records. Named on its own because a caller may need to recognize the segment, not just the path. */
|
|
3
6
|
export declare const ASSERTIONS_SEGMENT = "assertions";
|
|
7
|
+
/** The tag-alias map. */
|
|
4
8
|
export declare const ALIASES_FILE = ".kb/tag-aliases.yaml";
|
|
9
|
+
/** The directory holding the store's assertion records. */
|
|
5
10
|
export declare const ASSERTIONS_DIR = "content/assertions";
|
|
11
|
+
/** The check configuration. */
|
|
6
12
|
export declare const CONFIG_FILE = ".kb/config.yaml";
|
|
13
|
+
/** The editor and formatter settings. Sits at the store root, where every tool that reads it looks. */
|
|
14
|
+
export declare const EDITORCONFIG_FILE = ".editorconfig";
|
|
15
|
+
/** The directory holding the store's event records. */
|
|
7
16
|
export declare const EVENTS_DIR = "content/events";
|
|
17
|
+
/** The formatting configuration. Sits at the store root rather than under `.kb/`, because Prettier discovers it there. */
|
|
18
|
+
export declare const PRETTIER_CONFIG_FILE = ".prettierrc.yaml";
|
|
19
|
+
/** The declared assertion taxonomy. */
|
|
8
20
|
export declare const TAXONOMY_FILE = ".kb/taxonomy.yaml";
|
|
21
|
+
/**
|
|
22
|
+
* Builds an event record's store-relative path. Posix-separated, so it serves as the path half of a git object spec
|
|
23
|
+
* (`@{upstream}:content/events/<id>.md`) as well as an argument to `join`.
|
|
24
|
+
*/
|
|
9
25
|
export declare function buildEventPath(id: string): string;
|
|
26
|
+
/** Resolves the absolute path of a store's assertions directory. */
|
|
10
27
|
export declare function resolveAssertionsDir(storePath: string): string;
|
|
28
|
+
/** Resolves an event record's absolute path within a store. */
|
|
11
29
|
export declare function resolveEventPath(input: {
|
|
12
30
|
storePath: string;
|
|
13
31
|
id: string;
|
|
14
32
|
}): string;
|
|
33
|
+
/** Resolves the absolute path of a store's events directory. */
|
|
15
34
|
export declare function resolveEventsDir(storePath: string): string;
|
|
35
|
+
/** Resolves the absolute path of a store's metadata directory. */
|
|
16
36
|
export declare function resolveKbDir(storePath: string): string;
|
|
@@ -5,7 +5,9 @@ export const ASSERTIONS_SEGMENT = 'assertions';
|
|
|
5
5
|
export const ALIASES_FILE = `${KB_DIR}/tag-aliases.yaml`;
|
|
6
6
|
export const ASSERTIONS_DIR = `${CONTENT_DIR}/${ASSERTIONS_SEGMENT}`;
|
|
7
7
|
export const CONFIG_FILE = `${KB_DIR}/config.yaml`;
|
|
8
|
+
export const EDITORCONFIG_FILE = '.editorconfig';
|
|
8
9
|
export const EVENTS_DIR = `${CONTENT_DIR}/events`;
|
|
10
|
+
export const PRETTIER_CONFIG_FILE = '.prettierrc.yaml';
|
|
9
11
|
export const TAXONOMY_FILE = `${KB_DIR}/taxonomy.yaml`;
|
|
10
12
|
export function buildEventPath(id) {
|
|
11
13
|
return `${EVENTS_DIR}/${id}.md`;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Finding } from '../types.js';
|
|
2
|
+
/** The note fields the paths lint reads: its path and its full raw content. */
|
|
2
3
|
export interface PathsNote {
|
|
3
4
|
path: string;
|
|
4
5
|
content: string;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Flags hardcoded `/Users/{name}/` paths anywhere in the note (`paths.user-home`, error), reporting each occurrence at
|
|
9
|
+
* its line. The convention is `~/` for portability and to avoid leaking local usernames. Scans the full raw content,
|
|
10
|
+
* including frontmatter and code blocks, so a `/Users/{name}/` path inside a fenced or inline code example is flagged
|
|
11
|
+
* too.
|
|
12
|
+
*/
|
|
6
13
|
export declare function pathsFindings(note: PathsNote): Finding[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AliasMap, Finding } from '../types.js';
|
|
2
|
+
/** The note fields the tag-alias lint reads: its path and its raw frontmatter field map. */
|
|
2
3
|
export interface TagAliasNote {
|
|
3
4
|
path: string;
|
|
4
5
|
fields: Record<string, unknown>;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Warns when a note's `tags` list contains a known alias, naming the canonical form, in list order (`tag-alias`,
|
|
9
|
+
* warning). Unknown tags (neither canonical nor alias) are new vocabulary and are not flagged; a `tags` value that is
|
|
10
|
+
* absent or not a list yields no findings. Each finding points at the note, not a specific tag line.
|
|
11
|
+
*/
|
|
6
12
|
export declare function tagAliasFindings(note: TagAliasNote, aliases: AliasMap): Finding[];
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import type { KbConfig } from '../config/config-schema.js';
|
|
2
2
|
import type { Taxonomy } from '../taxonomy/taxonomy-schema.js';
|
|
3
3
|
import type { Finding } from '../types.js';
|
|
4
|
+
/** The note fields the taxonomy rules read. */
|
|
4
5
|
export interface TaxonomyNote {
|
|
6
|
+
/** The note's path relative to the KB root, slash-separated. */
|
|
5
7
|
relativePath: string;
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Reports where a store's assertion folders and its declared taxonomy disagree: `taxonomy.undeclared` for a folder
|
|
11
|
+
* holding notes that no domain declares, `taxonomy.unused` for a declared domain holding no note at or beneath it, and
|
|
12
|
+
* `taxonomy.orphan` for a declared domain whose parent is undeclared. All are warnings, so drift is reported without
|
|
13
|
+
* failing the run, and all are vault-scoped, so a run narrowed to selected notes still sees them.
|
|
14
|
+
*
|
|
15
|
+
* A taxonomy declaring nothing disables all three, whether because the file is absent or because it declares no
|
|
16
|
+
* domains. A store that has not adopted a taxonomy is therefore silent rather than reporting every folder it owns.
|
|
17
|
+
*
|
|
18
|
+
* The observed structure comes from the enumerated notes' own paths rather than a directory listing, so the rules add
|
|
19
|
+
* no filesystem traversal and see exactly the notes the run's `targets` and `exclude` admitted.
|
|
20
|
+
*/
|
|
7
21
|
export declare function taxonomyFindings(input: {
|
|
8
22
|
notes: readonly TaxonomyNote[];
|
|
9
23
|
taxonomy: Taxonomy;
|
|
10
24
|
config: KbConfig;
|
|
25
|
+
/** Absolute path of `.kb/taxonomy.yaml`, which every finding is reported against. */
|
|
11
26
|
taxonomyPath: string;
|
|
12
27
|
}): Finding[];
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerces a frontmatter field value to a string list: the array's string members when it is a sequence, an empty list
|
|
3
|
+
* when the value is absent, or `null` when the value is present but not list-shaped (so a parser can flag it).
|
|
4
|
+
*/
|
|
1
5
|
export declare function asStringList(value: unknown): string[] | null;
|
|
6
|
+
/**
|
|
7
|
+
* Reports whether a value is a real UTC instant in either bare `YYYY-MM-DD` or second-precision `YYYY-MM-DDTHH:MM:SSZ`
|
|
8
|
+
* form. An unmarked or offset timestamp matches neither pattern; an unreal calendar date or clock time fails the
|
|
9
|
+
* `Date.UTC` round-trip.
|
|
10
|
+
*/
|
|
2
11
|
export declare function isValidDate(value: string): boolean;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/** The outcome of splitting a note: its frontmatter field map, its body, the body's start line, and any parse error. */
|
|
1
2
|
export interface ReadNote {
|
|
2
3
|
fields: Record<string, unknown>;
|
|
3
4
|
body: string;
|
|
5
|
+
/** 1-based file line where the body begins (line 1 when there is no frontmatter block). */
|
|
4
6
|
bodyStartLine: number;
|
|
5
7
|
error?: string;
|
|
6
8
|
}
|
|
9
|
+
/** Reads a note file from disk and splits it into a frontmatter field map and body. I/O errors are thrown. */
|
|
7
10
|
export declare function readNote(path: string): Promise<ReadNote>;
|
|
11
|
+
/**
|
|
12
|
+
* Splits note content into a frontmatter field map and body. A missing frontmatter block or a YAML parse error is
|
|
13
|
+
* reported in `error` (never thrown), with the full content returned as the body so callers can still surface it.
|
|
14
|
+
*/
|
|
8
15
|
export declare function readNoteContent(content: string): ReadNote;
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a field map and body to note content: the opening fence, the frontmatter, the closing fence, one blank line,
|
|
3
|
+
* then the body. A single leading newline on the body is dropped so the blank line is not doubled, keeping the output
|
|
4
|
+
* stable across read/write cycles.
|
|
5
|
+
*/
|
|
1
6
|
export declare function renderNote(fields: Record<string, unknown>, body: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Atomically writes a note to `path`, so a concurrent reader never sees a partial write. See {@link writeAtomic} for
|
|
9
|
+
* the guarantee and its failure behavior.
|
|
10
|
+
*/
|
|
2
11
|
export declare function writeNote(path: string, fields: Record<string, unknown>, body: string): Promise<void>;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses frontmatter text (the YAML between the fences) into an insertion-ordered field map. A YAML parse error is
|
|
3
|
+
* returned rather than thrown; a block that is not a map yields an empty field map.
|
|
4
|
+
*/
|
|
1
5
|
export declare function parseFrontmatterFields(text: string): {
|
|
2
6
|
fields: Record<string, unknown>;
|
|
3
7
|
error?: string;
|
|
4
8
|
};
|
|
9
|
+
/** Renders a field map to frontmatter text (no fences), one `key: value` line per entry, in insertion order. */
|
|
5
10
|
export declare function renderFrontmatterFields(fields: Record<string, unknown>): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** A parsed `assertion` record: its typed fields, the body, and any other frontmatter preserved in `extra`. */
|
|
1
2
|
export interface KbAssertion {
|
|
2
3
|
recordType: 'assertion';
|
|
3
4
|
title: string;
|
|
@@ -11,6 +12,7 @@ export interface KbAssertion {
|
|
|
11
12
|
extra: Record<string, unknown>;
|
|
12
13
|
body: string;
|
|
13
14
|
}
|
|
15
|
+
/** The outcome of parsing frontmatter as an assertion: the typed record, or the validation errors that blocked it. */
|
|
14
16
|
export type ParseAssertionResult = {
|
|
15
17
|
ok: true;
|
|
16
18
|
record: KbAssertion;
|
|
@@ -18,7 +20,9 @@ export type ParseAssertionResult = {
|
|
|
18
20
|
ok: false;
|
|
19
21
|
errors: string[];
|
|
20
22
|
};
|
|
23
|
+
/** Validates a frontmatter field map as an assertion and projects it onto a {@link KbAssertion}, accumulating every error. */
|
|
21
24
|
export declare function parseAssertion(fields: Record<string, unknown>, body: string): ParseAssertionResult;
|
|
25
|
+
/** Projects an assertion back to a frontmatter field map (typed fields first, then preserved `extra`) plus its body. */
|
|
22
26
|
export declare function renderAssertion(record: KbAssertion): {
|
|
23
27
|
fields: Record<string, unknown>;
|
|
24
28
|
body: string;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
/** The impact levels an event may carry, ordered lowest to highest. */
|
|
1
2
|
export declare const EVENT_IMPACT_LEVELS: readonly ["low", "medium", "high", "critical"];
|
|
3
|
+
/** An event's impact: the author's subjective, revisable rating of how much addressing the event matters. */
|
|
2
4
|
export type EventImpact = (typeof EVENT_IMPACT_LEVELS)[number];
|
|
5
|
+
/** Reports whether a value is one of the declared {@link EVENT_IMPACT_LEVELS}. */
|
|
3
6
|
export declare function isEventImpact(value: unknown): value is EventImpact;
|
|
7
|
+
/** A parsed `event` record: its declared fields, the body, and any other frontmatter preserved in `extra`. */
|
|
4
8
|
export interface KbEvent {
|
|
5
9
|
recordType: 'event';
|
|
6
10
|
id: string;
|
|
7
11
|
capturedAt: string;
|
|
12
|
+
/** Harness-dependent provenance: absent when the harness that captured the event exposes no session id. */
|
|
8
13
|
session?: string;
|
|
9
14
|
cwd: string;
|
|
10
15
|
summary: string;
|
|
@@ -14,6 +19,7 @@ export interface KbEvent {
|
|
|
14
19
|
extra: Record<string, unknown>;
|
|
15
20
|
body: string;
|
|
16
21
|
}
|
|
22
|
+
/** The outcome of parsing frontmatter as an event: the typed record, or the validation errors that blocked it. */
|
|
17
23
|
export type ParseEventResult = {
|
|
18
24
|
ok: true;
|
|
19
25
|
record: KbEvent;
|
|
@@ -21,7 +27,13 @@ export type ParseEventResult = {
|
|
|
21
27
|
ok: false;
|
|
22
28
|
errors: string[];
|
|
23
29
|
};
|
|
30
|
+
/** Validates a frontmatter field map as an event and projects it onto a {@link KbEvent}, accumulating every error. */
|
|
24
31
|
export declare function parseEvent(fields: Record<string, unknown>, body: string): ParseEventResult;
|
|
32
|
+
/**
|
|
33
|
+
* Projects an event back to a frontmatter field map (declared fields first, then preserved `extra`) plus its body. An
|
|
34
|
+
* empty `session` is omitted like an absent one, mirroring {@link parseEvent}: the two spellings of "no session" have a
|
|
35
|
+
* single representation on both edges of the module, so no record can reacquire the empty field on a write.
|
|
36
|
+
*/
|
|
25
37
|
export declare function renderEvent(record: KbEvent): {
|
|
26
38
|
fields: Record<string, unknown>;
|
|
27
39
|
body: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The formatting option every store carries that `.editorconfig` cannot express. Held here rather than in the seed
|
|
3
|
+
* prose, so that the file that a store receives cannot diverge from the value that kb documents. See
|
|
4
|
+
* {@link renderPrettierSeed}'s header for what it prevents.
|
|
5
|
+
*/
|
|
6
|
+
export declare const canonicalPrettierConfig: {
|
|
7
|
+
embeddedLanguageFormatting: string;
|
|
8
|
+
};
|
|
9
|
+
/** Renders `.kb/tag-aliases.yaml`: an empty `aliases: {}` map under an explanatory header. */
|
|
10
|
+
export declare function renderAliasesSeed(): string;
|
|
11
|
+
/** Renders `.kb/config.yaml`: a fully-commented stub whose example values are the live `defaultKbConfig`. */
|
|
12
|
+
export declare function renderConfigSeed(): string;
|
|
13
|
+
/** Renders `.editorconfig`: the editor and formatter settings every store shares. */
|
|
14
|
+
export declare function renderEditorconfigSeed(): string;
|
|
15
|
+
/** Renders `.prettierrc.yaml`: the canonical formatting option under a header explaining why it is set. */
|
|
16
|
+
export declare function renderPrettierSeed(): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import { defaultKbConfig } from "../config/config-schema.js";
|
|
3
|
+
const ALIASES_HEADER = `# Tag aliases for this knowledge store.
|
|
4
|
+
#
|
|
5
|
+
# Map each canonical tag to the aliases that should resolve to it, for example:
|
|
6
|
+
# aliases:
|
|
7
|
+
# typescript: [ts, type-script]
|
|
8
|
+
# The \`aliases:\` key is required even when empty.
|
|
9
|
+
`;
|
|
10
|
+
const CONFIG_HEADER = `# Check configuration for this knowledge store.
|
|
11
|
+
#
|
|
12
|
+
# Both keys are optional and fall back to the defaults shown below; an absent file uses these defaults too. Uncomment
|
|
13
|
+
# and edit to override. \`targets\` selects which notes \`kb check\` enumerates and \`exclude\` removes matches;
|
|
14
|
+
# patterns are slash-separated and relative to the store root.
|
|
15
|
+
#
|
|
16
|
+
`;
|
|
17
|
+
const EDITORCONFIG_SEED = `root = true
|
|
18
|
+
|
|
19
|
+
[*]
|
|
20
|
+
charset = utf-8
|
|
21
|
+
end_of_line = lf
|
|
22
|
+
indent_size = 2
|
|
23
|
+
indent_style = space
|
|
24
|
+
insert_final_newline = true
|
|
25
|
+
max_line_length = 120
|
|
26
|
+
trim_trailing_whitespace = true
|
|
27
|
+
|
|
28
|
+
[*.md]
|
|
29
|
+
trim_trailing_whitespace = false
|
|
30
|
+
`;
|
|
31
|
+
const PRETTIER_HEADER = `# Formatting configuration for this knowledge store.
|
|
32
|
+
#
|
|
33
|
+
# The single option here is not stylistic. It prevents a specific failure, so read this before removing it.
|
|
34
|
+
#
|
|
35
|
+
# \`embeddedLanguageFormatting: off\` leaves a note's YAML frontmatter unformatted. Formatted, a long \`tags\` or
|
|
36
|
+
# \`addressed-by\` list breaks across several lines, which the note writer puts back onto one the next time it writes
|
|
37
|
+
# the note, so the formatter and the writer would rewrite each other's output without end.
|
|
38
|
+
#
|
|
39
|
+
# Width, indentation, and line endings are set in \`.editorconfig\`, which Prettier reads and every editor reads too.
|
|
40
|
+
#
|
|
41
|
+
`;
|
|
42
|
+
export const canonicalPrettierConfig = {
|
|
43
|
+
embeddedLanguageFormatting: 'off',
|
|
44
|
+
};
|
|
45
|
+
export function renderAliasesSeed() {
|
|
46
|
+
return `${ALIASES_HEADER}aliases: {}\n`;
|
|
47
|
+
}
|
|
48
|
+
export function renderConfigSeed() {
|
|
49
|
+
return `${CONFIG_HEADER}${commentBlock(stringify(defaultKbConfig))}\n`;
|
|
50
|
+
}
|
|
51
|
+
export function renderEditorconfigSeed() {
|
|
52
|
+
return EDITORCONFIG_SEED;
|
|
53
|
+
}
|
|
54
|
+
export function renderPrettierSeed() {
|
|
55
|
+
return `${PRETTIER_HEADER}${stringify(canonicalPrettierConfig)}`;
|
|
56
|
+
}
|
|
57
|
+
function commentBlock(text) {
|
|
58
|
+
return text
|
|
59
|
+
.trimEnd()
|
|
60
|
+
.split('\n')
|
|
61
|
+
.map((line) => (line === '' ? '#' : `# ${line}`))
|
|
62
|
+
.join('\n');
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What {@link scaffold} did about one canonical path: `created` where it was absent, `present` where it was left as
|
|
3
|
+
* found, `replaced` where `force` overwrote it.
|
|
4
|
+
*/
|
|
5
|
+
export type ScaffoldAction = 'created' | 'present' | 'replaced';
|
|
6
|
+
/** One canonical path and what the scaffold did about it. */
|
|
7
|
+
export interface ScaffoldEntry {
|
|
8
|
+
/** The store-relative path, directories carrying a trailing slash. */
|
|
9
|
+
path: string;
|
|
10
|
+
action: ScaffoldAction;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Writes the canonical files and directories every knowledge-base store holds, leaving an existing file as it found it
|
|
14
|
+
* unless `force` is set. Directories are ensured either way, since a directory has no content to replace.
|
|
15
|
+
*
|
|
16
|
+
* The function asserts nothing about the store: a caller that requires one to exist, or requires one not to, checks
|
|
17
|
+
* that itself. `create` calls it on a directory it has just confirmed holds no store, and `kb scaffold` on one it has
|
|
18
|
+
* just confirmed does.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scaffold(input: {
|
|
21
|
+
storePath: string;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}): Promise<readonly ScaffoldEntry[]>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { pathExists } from "../filesystem/exists.js";
|
|
4
|
+
import { writeAtomic } from "../filesystem/write-atomic.js";
|
|
5
|
+
import { ALIASES_FILE, CONFIG_FILE, CONTENT_DIR, EDITORCONFIG_FILE, EVENTS_DIR, PRETTIER_CONFIG_FILE, } from "../layout/index.js";
|
|
6
|
+
import { renderAliasesSeed, renderConfigSeed, renderEditorconfigSeed, renderPrettierSeed } from "./render-seeds.js";
|
|
7
|
+
export async function scaffold(input) {
|
|
8
|
+
const { storePath, force = false } = input;
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const file of CANONICAL_FILES) {
|
|
11
|
+
const absolutePath = join(storePath, file.path);
|
|
12
|
+
const exists = await pathExists(absolutePath);
|
|
13
|
+
if (exists && !force) {
|
|
14
|
+
entries.push({ path: file.path, action: 'present' });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
18
|
+
await writeAtomic(absolutePath, file.render());
|
|
19
|
+
entries.push({ path: file.path, action: exists ? 'replaced' : 'created' });
|
|
20
|
+
}
|
|
21
|
+
for (const directory of CANONICAL_DIRECTORIES) {
|
|
22
|
+
const absolutePath = join(storePath, directory);
|
|
23
|
+
const exists = await pathExists(absolutePath);
|
|
24
|
+
await mkdir(absolutePath, { recursive: true });
|
|
25
|
+
entries.push({ path: `${directory}/`, action: exists ? 'present' : 'created' });
|
|
26
|
+
}
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
const CANONICAL_DIRECTORIES = [CONTENT_DIR, EVENTS_DIR];
|
|
30
|
+
const CANONICAL_FILES = [
|
|
31
|
+
{ path: EDITORCONFIG_FILE, render: renderEditorconfigSeed },
|
|
32
|
+
{ path: CONFIG_FILE, render: renderConfigSeed },
|
|
33
|
+
{ path: ALIASES_FILE, render: renderAliasesSeed },
|
|
34
|
+
{ path: PRETTIER_CONFIG_FILE, render: renderPrettierSeed },
|
|
35
|
+
];
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { AliasMap } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the canonical form of a tag. Lookups are case-insensitive.
|
|
4
|
+
* Unknown tags and tags that are already canonical pass through unchanged.
|
|
5
|
+
*/
|
|
2
6
|
export declare function canonicalize(tag: string, aliases: AliasMap): string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the canonical form of a tag only when the input is a known alias; otherwise `null`.
|
|
9
|
+
* Distinguishes drift (an alias is present) from canonical or new-vocabulary tags.
|
|
10
|
+
*/
|
|
3
11
|
export declare function findAliasFor(tag: string, aliases: AliasMap): string | null;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { AliasMap, KbRoot } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads `.kb/tag-aliases.yaml` from a KB root into a typed `AliasMap`, returning an empty map when the file is absent.
|
|
4
|
+
* The thin I/O wrapper around {@link parseAliases}; structural defects throw with the file path included.
|
|
5
|
+
*/
|
|
2
6
|
export declare function loadAliases(input: {
|
|
3
7
|
kbRoot: KbRoot;
|
|
4
8
|
}): Promise<AliasMap>;
|
|
9
|
+
/**
|
|
10
|
+
* Parses a tag-aliases registry from a string into an `AliasMap`.
|
|
11
|
+
* Aliases are lowercased on insertion so callers can look up case-insensitively.
|
|
12
|
+
* Throws on any structural defect — non-object top level, missing `aliases` key, non-string entries, self-aliases,
|
|
13
|
+
* or cross-canonical collisions — with `contextLabel` prefixed onto every message.
|
|
14
|
+
*/
|
|
5
15
|
export declare function parseAliases(text: string, contextLabel?: string): AliasMap;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives every domain a note set implies: the domain each note sits in, plus each of that domain's ancestors, sorted.
|
|
3
|
+
* A grouping domain that holds only subfolders is included, because the rules treat it as in use and would otherwise
|
|
4
|
+
* report a back-filled taxonomy's own entries as unused.
|
|
5
|
+
*/
|
|
1
6
|
export declare function deriveDomains(relativePaths: Iterable<string>): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Derives the domain a note sits in, or `undefined` when the note is not an assertion or sits at the assertions root.
|
|
9
|
+
* Scoping to the assertions root is what keeps event records from registering as domains.
|
|
10
|
+
*/
|
|
2
11
|
export declare function resolveDomain(relativePath: string): string | undefined;
|
|
12
|
+
/** Derives a slash-path's parent, or `undefined` when it has no separator and so sits at the top level. */
|
|
3
13
|
export declare function resolveParent(path: string): string | undefined;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { KbRoot } from '../types.js';
|
|
2
2
|
import { type Taxonomy } from './taxonomy-schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loads `.kb/taxonomy.yaml` into a single keyed map, returning an empty taxonomy when the file is absent or declares
|
|
5
|
+
* nothing. The two on-disk blocks are a file-format concern: a consumer looks a domain up once and reads `provisional`
|
|
6
|
+
* off the entry it finds.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors {@link loadKbConfig}: structural defects (malformed YAML, a wrong type, a malformed key, a path declared in
|
|
9
|
+
* both blocks) throw a {@link KbLoaderError} naming the file. I/O errors other than a missing file propagate.
|
|
10
|
+
*/
|
|
3
11
|
export declare function loadTaxonomy(input: {
|
|
4
12
|
kbRoot: KbRoot;
|
|
5
13
|
}): Promise<Taxonomy>;
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Describes why a domain key cannot be used, or returns `undefined` when the key is well-formed. Keys are relative to
|
|
4
|
+
* the assertions root, so a restated `content/assertions/` prefix would declare the domain a level deeper than the
|
|
5
|
+
* author meant; `kb-add` refuses the same mistake on the note-write path.
|
|
6
|
+
*/
|
|
2
7
|
export declare function describeKeyDefect(key: string): string | undefined;
|
|
8
|
+
/** A knowledge base's declared assertion structure, keyed by assertions-root-relative slash-path. */
|
|
3
9
|
export type Taxonomy = ReadonlyMap<string, TaxonomyEntry>;
|
|
10
|
+
/** A single declared domain. */
|
|
4
11
|
export interface TaxonomyEntry {
|
|
12
|
+
/** The domain's one-line description; empty when it was declared without one. */
|
|
5
13
|
description: string;
|
|
14
|
+
/** Whether the domain was declared under `provisional:` rather than `domains:`. */
|
|
6
15
|
provisional: boolean;
|
|
7
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The on-disk `.kb/taxonomy.yaml` shape.
|
|
19
|
+
*
|
|
20
|
+
* Both blocks are optional and nullable, so a file may declare only one, and a block header left with nothing under it
|
|
21
|
+
* loads as declaring nothing rather than failing. YAML reads such a header as null, and it is the state promoting the
|
|
22
|
+
* last entry out of a block leaves behind.
|
|
23
|
+
*
|
|
24
|
+
* A description may likewise be null, so a key written bare (`engineering/tooling:`) loads: that is what a hand editor
|
|
25
|
+
* types and what a domain declared without a description round-trips to.
|
|
26
|
+
*/
|
|
8
27
|
export declare const taxonomyFileShape: z.ZodObject<{
|
|
9
28
|
domains: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>>;
|
|
10
29
|
provisional: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>>;
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import type { KbRoot } from '../types.js';
|
|
2
|
+
/** A domain to declare. */
|
|
2
3
|
export interface TaxonomyDeclaration {
|
|
4
|
+
/** The domain's assertions-root-relative slash-path. */
|
|
3
5
|
path: string;
|
|
6
|
+
/** The one-line description; absent or empty writes the key bare, with no description. */
|
|
4
7
|
description?: string;
|
|
8
|
+
/** Whether to declare under `provisional:` rather than `domains:`. */
|
|
5
9
|
provisional: boolean;
|
|
6
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Declares domains in `.kb/taxonomy.yaml`, creating the file and either block as needed, and returns the paths added.
|
|
13
|
+
*
|
|
14
|
+
* Edits the parsed document rather than re-serializing a plain object, so existing comments, key order, and formatting
|
|
15
|
+
* survive; a plain parse-and-stringify round trip would discard every comment in a hand-curated file. New keys append
|
|
16
|
+
* to the end of their block in path order, and a block header left with nothing under it is filled in place rather
|
|
17
|
+
* than moved.
|
|
18
|
+
*
|
|
19
|
+
* A path either block already declares is skipped rather than overwritten, so a repeat call adds nothing; when nothing
|
|
20
|
+
* is left to add, the file is not opened for writing at all. The write goes through the shared `writeAtomic` helper,
|
|
21
|
+
* so an interrupted call cannot truncate the taxonomy.
|
|
22
|
+
*
|
|
23
|
+
* Throws a {@link KbLoaderError} on a malformed key, or on an existing file that cannot be safely appended to: one
|
|
24
|
+
* that fails to parse, one whose top level is not a mapping, and one whose `domains` or `provisional` block holds
|
|
25
|
+
* something other than a mapping. Other I/O errors propagate.
|
|
26
|
+
*/
|
|
7
27
|
export declare function writeTaxonomy(input: {
|
|
8
28
|
kbRoot: KbRoot;
|
|
9
29
|
declarations: readonly TaxonomyDeclaration[];
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether an error is an ENOENT (no such file or directory) filesystem error.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
1
5
|
export declare function isEnoent(error: unknown): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Returns true when `error` carries the given Node `code` string (e.g. `'ENOENT'`, `'EACCES'`).
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
2
10
|
export declare function isErrorCode(error: unknown, code: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Type guard for a non-null, non-array object.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
3
16
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|