@williamthorsen/kb 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -27
- package/dist/esm/check/check.d.ts +34 -0
- package/dist/esm/check/check.js +41 -1
- package/dist/esm/check/enumerate.d.ts +37 -0
- package/dist/esm/check/enumerate.js +12 -4
- package/dist/esm/check/glob-segments.d.ts +6 -0
- package/dist/esm/check/resolve-foreign-stores.d.ts +24 -0
- package/dist/esm/check/resolve-foreign-stores.js +50 -0
- package/dist/esm/cli/commands/check.d.ts +23 -1
- package/dist/esm/cli/commands/check.js +7 -2
- package/dist/esm/cli/commands/create.d.ts +19 -1
- package/dist/esm/cli/commands/create.js +2 -0
- package/dist/esm/cli/commands/scaffold.d.ts +30 -0
- package/dist/esm/cli/commands/scaffold.js +108 -0
- package/dist/esm/cli/commands/set-default.d.ts +15 -0
- package/dist/esm/cli/commands/taxonomy.d.ts +18 -0
- package/dist/esm/cli/format.d.ts +21 -0
- package/dist/esm/cli/parse-flag-value.d.ts +5 -0
- package/dist/esm/cli/resolve-store.d.ts +12 -0
- package/dist/esm/cli/run.d.ts +9 -1
- package/dist/esm/cli/run.js +5 -0
- package/dist/esm/cli/select-kb-prompt.d.ts +18 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +10 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.js +7 -33
- package/dist/esm/cli/targeting/select-notes.d.ts +13 -0
- package/dist/esm/cli/targeting/select-notes.js +4 -3
- package/dist/esm/config/config-schema.d.ts +30 -1
- package/dist/esm/config/config-schema.js +9 -0
- package/dist/esm/config/index.d.ts +1 -1
- package/dist/esm/config/index.js +1 -1
- package/dist/esm/config/kb-loader-error.d.ts +8 -0
- package/dist/esm/config/load-config.d.ts +7 -0
- package/dist/esm/config/load-config.js +1 -0
- package/dist/esm/config/note-scope.d.ts +13 -0
- package/dist/esm/create/create.d.ts +25 -0
- package/dist/esm/create/create.js +8 -12
- package/dist/esm/create/index.d.ts +0 -1
- package/dist/esm/create/index.js +0 -1
- package/dist/esm/discovery/find-kb-root.d.ts +4 -0
- package/dist/esm/discovery/kb-registry-schema.d.ts +4 -0
- package/dist/esm/discovery/load-registry.d.ts +23 -0
- package/dist/esm/discovery/register-store.d.ts +11 -0
- package/dist/esm/discovery/registry-document.d.ts +5 -0
- package/dist/esm/discovery/set-default-kb.d.ts +12 -0
- package/dist/esm/filesystem/exists.d.ts +13 -0
- package/dist/esm/filesystem/write-atomic.d.ts +5 -0
- package/dist/esm/frontmatter/parse-note.d.ts +9 -0
- package/dist/esm/git/list-git-scope.d.ts +15 -0
- package/dist/esm/git/list-git-scope.js +22 -0
- package/dist/esm/git/run-git.d.ts +17 -0
- package/dist/esm/git/run-git.js +22 -0
- package/dist/esm/layout/index.d.ts +1 -1
- package/dist/esm/layout/index.js +1 -1
- package/dist/esm/layout/store-layout.d.ts +20 -0
- package/dist/esm/layout/store-layout.js +2 -0
- package/dist/esm/lints/paths.d.ts +7 -0
- package/dist/esm/lints/tag-alias.d.ts +6 -0
- package/dist/esm/lints/taxonomy.d.ts +15 -0
- package/dist/esm/note-io/field-validators.d.ts +9 -0
- package/dist/esm/note-io/read-note.d.ts +7 -0
- package/dist/esm/note-io/write-note.d.ts +9 -0
- package/dist/esm/note-io/yaml-fields.d.ts +5 -0
- package/dist/esm/records/assertion.d.ts +4 -0
- package/dist/esm/records/event.d.ts +12 -0
- package/dist/esm/scaffold/index.d.ts +2 -0
- package/dist/esm/scaffold/index.js +2 -0
- package/dist/esm/scaffold/render-seeds.d.ts +16 -0
- package/dist/esm/scaffold/render-seeds.js +65 -0
- package/dist/esm/scaffold/scaffold.d.ts +23 -0
- package/dist/esm/scaffold/scaffold.js +35 -0
- package/dist/esm/tags/canonicalize.d.ts +8 -0
- package/dist/esm/tags/load-aliases.d.ts +10 -0
- package/dist/esm/taxonomy/domain-paths.d.ts +10 -0
- package/dist/esm/taxonomy/load-taxonomy.d.ts +8 -0
- package/dist/esm/taxonomy/taxonomy-schema.d.ts +19 -0
- package/dist/esm/taxonomy/write-taxonomy.d.ts +20 -0
- package/dist/esm/type-guards.d.ts +13 -0
- package/dist/esm/types.d.ts +41 -0
- package/dist/esm/vault-integrity/build-vault-index.d.ts +11 -0
- package/dist/esm/vault-integrity/check-vault-integrity.d.ts +48 -1
- package/dist/esm/vault-integrity/check-vault-integrity.js +59 -20
- package/dist/esm/vault-integrity/index.d.ts +2 -2
- package/dist/esm/vault-integrity/index.js +2 -2
- package/dist/esm/vault-integrity/wikilink-parse.d.ts +47 -4
- package/dist/esm/vault-integrity/wikilink-parse.js +37 -9
- package/package.json +10 -4
- package/dist/esm/create/render-seeds.d.ts +0 -2
- package/dist/esm/create/render-seeds.js +0 -29
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The on-disk `.kb/config.yaml` shape. Every field is optional so a file may override only the key it cares about; an
|
|
4
|
+
* absent field falls back to {@link defaultKbConfig}.
|
|
5
|
+
*/
|
|
2
6
|
export declare const configFileShape: z.ZodObject<{
|
|
3
7
|
targets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
4
8
|
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
9
|
+
visibility: z.ZodOptional<z.ZodEnum<{
|
|
10
|
+
private: "private";
|
|
11
|
+
shared: "shared";
|
|
12
|
+
}>>;
|
|
5
13
|
}, z.core.$strip>;
|
|
14
|
+
/**
|
|
15
|
+
* The configuration applied when `.kb/config.yaml` is absent or omits a field. Targets the `content/`-scoped layout
|
|
16
|
+
* the owner's stores use; with `picomatch` `dot:false`, dot-directories (`.kb`, `.git`, `.agents`) are excluded
|
|
17
|
+
* implicitly, so the default exclude only names `node_modules`. Visibility defaults to the safer of the two values, so
|
|
18
|
+
* a store that has not declared itself never widens what may link into it.
|
|
19
|
+
*/
|
|
20
|
+
export declare const defaultKbConfig: KbConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a link from a store of `source` visibility may resolve into one of `target` visibility. A link never
|
|
23
|
+
* increases disclosure: it may point at a store as shareable as its own or more so, never at a less shareable one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAtLeastAsShareable(input: {
|
|
26
|
+
source: StoreVisibility;
|
|
27
|
+
target: StoreVisibility;
|
|
28
|
+
}): boolean;
|
|
29
|
+
/** The effective check configuration: the glob target set, the glob exclude set, and the store's visibility. */
|
|
6
30
|
export interface KbConfig {
|
|
31
|
+
/** Glob patterns (slash-separated, kbRoot-relative) selecting which notes `check` enumerates. */
|
|
7
32
|
targets: readonly string[];
|
|
33
|
+
/** Glob patterns excluded from enumeration even when a target matches. */
|
|
8
34
|
exclude: readonly string[];
|
|
35
|
+
/** How widely the store is published, which decides what may link into it. */
|
|
36
|
+
visibility: StoreVisibility;
|
|
9
37
|
}
|
|
10
|
-
|
|
38
|
+
/** How widely a store is published: `shared` reaches collaborators through a remote, `private` reaches nobody else. */
|
|
39
|
+
export type StoreVisibility = NonNullable<z.infer<typeof configFileShape>['visibility']>;
|
|
@@ -3,8 +3,17 @@ import { CONTENT_DIR } from "../layout/index.js";
|
|
|
3
3
|
export const configFileShape = z.object({
|
|
4
4
|
targets: z.array(z.string()).optional(),
|
|
5
5
|
exclude: z.array(z.string()).optional(),
|
|
6
|
+
visibility: z.enum(['private', 'shared']).optional(),
|
|
6
7
|
});
|
|
7
8
|
export const defaultKbConfig = {
|
|
8
9
|
targets: [`${CONTENT_DIR}/**/*.md`],
|
|
9
10
|
exclude: ['**/node_modules/**'],
|
|
11
|
+
visibility: 'private',
|
|
12
|
+
};
|
|
13
|
+
export function isAtLeastAsShareable(input) {
|
|
14
|
+
return VISIBILITY_RANK[input.target] >= VISIBILITY_RANK[input.source];
|
|
15
|
+
}
|
|
16
|
+
const VISIBILITY_RANK = {
|
|
17
|
+
private: 0,
|
|
18
|
+
shared: 1,
|
|
10
19
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { configFileShape, defaultKbConfig, type KbConfig } from './config-schema.js';
|
|
1
|
+
export { configFileShape, defaultKbConfig, isAtLeastAsShareable, type KbConfig, type StoreVisibility, } from './config-schema.js';
|
|
2
2
|
export { isKbLoaderError, KbLoaderError } from './kb-loader-error.js';
|
|
3
3
|
export { loadKbConfig } from './load-config.js';
|
|
4
4
|
export { createNoteScopeMatcher, type NoteScopeMatcher } from './note-scope.js';
|
package/dist/esm/config/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { configFileShape, defaultKbConfig } from "./config-schema.js";
|
|
1
|
+
export { configFileShape, defaultKbConfig, isAtLeastAsShareable, } from "./config-schema.js";
|
|
2
2
|
export { isKbLoaderError, KbLoaderError } from "./kb-loader-error.js";
|
|
3
3
|
export { loadKbConfig } from "./load-config.js";
|
|
4
4
|
export { createNoteScopeMatcher } from "./note-scope.js";
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed error thrown by the KB loaders (`loadKbConfig`, `loadAliases`) on a structural defect: malformed YAML or
|
|
3
|
+
* wrong types. The `kind` discriminant lets a caller distinguish a recoverable config or alias defect from any other
|
|
4
|
+
* throw (an enumeration or detection crash) without matching on message text or relying on `instanceof` surviving a
|
|
5
|
+
* bundle boundary.
|
|
6
|
+
*/
|
|
1
7
|
export declare class KbLoaderError extends Error {
|
|
8
|
+
/** Discriminant for narrow catch boundaries; survives serialization and bundling unlike `instanceof`. */
|
|
2
9
|
readonly kind: "KbLoaderError";
|
|
3
10
|
constructor(message: string, options?: ErrorOptions);
|
|
4
11
|
}
|
|
12
|
+
/** Type guard narrowing an unknown throw to a {@link KbLoaderError} via its `kind` discriminant. */
|
|
5
13
|
export declare function isKbLoaderError(error: unknown): error is KbLoaderError;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { KbRoot } from '../types.js';
|
|
2
2
|
import { type KbConfig } from './config-schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Loads the effective check configuration for a KB root. Returns {@link defaultKbConfig} verbatim when no
|
|
5
|
+
* `.kb/config.yaml` exists; a file present but omitting a field inherits that field's default.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors {@link loadAliases}: a single plain-object input, structural defects (malformed YAML, wrong types) throw a
|
|
8
|
+
* {@link KbLoaderError} naming the file. I/O errors other than a missing file propagate.
|
|
9
|
+
*/
|
|
3
10
|
export declare function loadKbConfig(input: {
|
|
4
11
|
kbRoot: KbRoot;
|
|
5
12
|
}): Promise<KbConfig>;
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { KbConfig } from './config-schema.js';
|
|
2
|
+
/** The note-membership predicates a {@link KbConfig} defines, all matching KB-root-relative, slash-separated paths. */
|
|
2
3
|
export interface NoteScopeMatcher {
|
|
4
|
+
/** True when the path matches a `targets` glob. */
|
|
3
5
|
isTarget(relativePath: string): boolean;
|
|
6
|
+
/** True when the path matches an `exclude` glob. */
|
|
4
7
|
isExcluded(relativePath: string): boolean;
|
|
8
|
+
/** True when the path is a note: matched by `targets` and not `exclude`d. The `.md` gate is the caller's. */
|
|
5
9
|
isNote(relativePath: string): boolean;
|
|
6
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds the note-membership predicates for a KB config — the single definition of "a note" shared by
|
|
13
|
+
* `enumerateNotes` (which `kb check`/`kb-curate` drive) and `kb-retrieve`. Both axes match with `picomatch`'s
|
|
14
|
+
* `dot:false`, so dot-directories (`.kb`, `.git`, `.agents`) are excluded implicitly without naming them in `exclude`.
|
|
15
|
+
*
|
|
16
|
+
* The `.md` extension gate is deliberately left to the caller: `enumerateNotes` applies its own `.endsWith('.md')`
|
|
17
|
+
* during the walk, and `kb-retrieve` constrains ripgrep with `--glob '*.md'`. Keeping it out of `isNote` lets this
|
|
18
|
+
* matcher govern `targets`/`exclude` alone, the one place the two tools previously disagreed.
|
|
19
|
+
*/
|
|
7
20
|
export declare function createNoteScopeMatcher(config: KbConfig): NoteScopeMatcher;
|
|
@@ -1,12 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `create` did about the registry's `default_kb` pointer when registering a store:
|
|
3
|
+
* `set` — it was unset and the new store was the only KB, so the store became the default;
|
|
4
|
+
* `unchanged` — a default was already set and left untouched;
|
|
5
|
+
* `needs-selection` — it was unset but other KBs exist, so the caller should prompt for a choice.
|
|
6
|
+
*/
|
|
1
7
|
export type DefaultKbOutcome = 'set' | 'unchanged' | 'needs-selection';
|
|
8
|
+
/** A successfully created store and a record of what was written. */
|
|
2
9
|
export interface CreatedStore {
|
|
10
|
+
/** The store's registry name (the directory's base name unless overridden). */
|
|
3
11
|
name: string;
|
|
12
|
+
/** Absolute path to the store root. */
|
|
4
13
|
storePath: string;
|
|
14
|
+
/** The description written into the registry entry; absent when none was supplied. */
|
|
5
15
|
description?: string;
|
|
16
|
+
/** Whether the store was registered in the kb.yaml registry. */
|
|
6
17
|
registered: boolean;
|
|
18
|
+
/** Store-relative paths created by the scaffold. */
|
|
7
19
|
created: readonly string[];
|
|
20
|
+
/** What happened to the registry's `default_kb` pointer; absent when the store was not registered. */
|
|
8
21
|
defaultKb?: DefaultKbOutcome;
|
|
9
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Inputs for {@link create}. `registryPath` and `description` belong to the registering form alone: a description is
|
|
25
|
+
* only ever written into a registry entry, so supplying one without registering is a type error rather than a value
|
|
26
|
+
* that would be silently discarded.
|
|
27
|
+
*/
|
|
10
28
|
export type CreateInput = {
|
|
11
29
|
targetDir: string;
|
|
12
30
|
name?: string;
|
|
@@ -17,6 +35,7 @@ export type CreateInput = {
|
|
|
17
35
|
registryPath: string;
|
|
18
36
|
description?: string;
|
|
19
37
|
});
|
|
38
|
+
/** The outcome of a {@link create} call: a created store, or a categorical precondition failure. */
|
|
20
39
|
export type CreateOutcome = {
|
|
21
40
|
ok: true;
|
|
22
41
|
created: CreatedStore;
|
|
@@ -25,4 +44,10 @@ export type CreateOutcome = {
|
|
|
25
44
|
reason: 'kb-exists' | 'name-registered';
|
|
26
45
|
message: string;
|
|
27
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Scaffolds a new knowledge-base store in `targetDir` and, unless `register` is false, registers it in the kb.yaml
|
|
49
|
+
* registry. Both preconditions — an existing `.kb/`, and (when registering) an already-registered name — are checked
|
|
50
|
+
* before anything is written, so a precondition failure leaves the filesystem untouched. Genuine I/O failures
|
|
51
|
+
* propagate.
|
|
52
|
+
*/
|
|
28
53
|
export declare function create(input: CreateInput): Promise<CreateOutcome>;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, join, resolve } from 'node:path';
|
|
1
|
+
import { basename, resolve } from 'node:path';
|
|
3
2
|
import { loadKbRegistry } from "../discovery/load-registry.js";
|
|
4
3
|
import { registerStore } from "../discovery/register-store.js";
|
|
5
4
|
import { setDefaultKb } from "../discovery/set-default-kb.js";
|
|
6
5
|
import { pathExists } from "../filesystem/exists.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { KB_DIR, resolveKbDir } from "../layout/index.js";
|
|
7
|
+
import { scaffold } from "../scaffold/scaffold.js";
|
|
9
8
|
export async function create(input) {
|
|
10
9
|
const storePath = resolve(input.targetDir);
|
|
11
10
|
const name = input.name ?? basename(storePath);
|
|
@@ -13,7 +12,7 @@ export async function create(input) {
|
|
|
13
12
|
return { ok: false, reason: 'kb-exists', message: `a ${KB_DIR}/ store already exists at ${storePath}` };
|
|
14
13
|
}
|
|
15
14
|
if (!input.register) {
|
|
16
|
-
const created = await
|
|
15
|
+
const created = await writeCanonicalSet(storePath);
|
|
17
16
|
return { ok: true, created: { name, storePath, registered: false, created } };
|
|
18
17
|
}
|
|
19
18
|
const { registryPath } = input;
|
|
@@ -21,7 +20,7 @@ export async function create(input) {
|
|
|
21
20
|
if (before.entries.some((entry) => entry.name === name)) {
|
|
22
21
|
return { ok: false, reason: 'name-registered', message: nameRegisteredMessage(name, registryPath) };
|
|
23
22
|
}
|
|
24
|
-
const created = await
|
|
23
|
+
const created = await writeCanonicalSet(storePath);
|
|
25
24
|
const described = input.description !== undefined && { description: input.description };
|
|
26
25
|
const result = await registerStore({ registryPath, name, storePath, ...described });
|
|
27
26
|
if (result.status === 'already-present') {
|
|
@@ -43,10 +42,7 @@ async function ensureDefaultKb(input) {
|
|
|
43
42
|
function nameRegisteredMessage(name, registryPath) {
|
|
44
43
|
return `a store named "${name}" is already registered in ${registryPath}`;
|
|
45
44
|
}
|
|
46
|
-
async function
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
await writeFile(join(storePath, ALIASES_FILE), renderAliasesSeed(), 'utf8');
|
|
50
|
-
await mkdir(resolveEventsDir(storePath), { recursive: true });
|
|
51
|
-
return [CONFIG_FILE, ALIASES_FILE, `${CONTENT_DIR}/`, `${EVENTS_DIR}/`];
|
|
45
|
+
async function writeCanonicalSet(storePath) {
|
|
46
|
+
const entries = await scaffold({ storePath });
|
|
47
|
+
return entries.filter((entry) => entry.action !== 'present').map((entry) => entry.path);
|
|
52
48
|
}
|
package/dist/esm/create/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { KbRoot } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Walks up the directory tree from `startDir` & returns the first ancestor containing a `.kb/` directory as a `KbRoot`.
|
|
4
|
+
* Returns `null` when the walk reaches the filesystem root without a hit.
|
|
5
|
+
*/
|
|
2
6
|
export declare function findKbRoot(input: {
|
|
3
7
|
startDir: string;
|
|
4
8
|
}): Promise<KbRoot | null>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/** Schema for a single KB entry as written in `kb.yaml`. */
|
|
2
3
|
export declare const kbRegistryFileEntrySchema: z.ZodObject<{
|
|
3
4
|
path: z.ZodString;
|
|
4
5
|
description: z.ZodOptional<z.ZodString>;
|
|
5
6
|
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
6
7
|
}, z.core.$strip>;
|
|
8
|
+
/** Schema for the full `kb.yaml` file. */
|
|
7
9
|
export declare const kbRegistryFileSchema: z.ZodObject<{
|
|
8
10
|
default_kb: z.ZodOptional<z.ZodString>;
|
|
9
11
|
kbs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -12,5 +14,7 @@ export declare const kbRegistryFileSchema: z.ZodObject<{
|
|
|
12
14
|
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
13
15
|
}, z.core.$strip>>>;
|
|
14
16
|
}, z.core.$strip>;
|
|
17
|
+
/** The validated on-disk entry shape. */
|
|
15
18
|
export type KbRegistryFileEntry = z.infer<typeof kbRegistryFileEntrySchema>;
|
|
19
|
+
/** The validated on-disk file shape. */
|
|
16
20
|
export type KbRegistryFile = z.infer<typeof kbRegistryFileSchema>;
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import type { KbRegistry } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load and merge the user-global (`~/.agents/kb.yaml`) and project-local
|
|
4
|
+
* (`.agents/kb.yaml`) KB registries into a normalized `KbRegistry`.
|
|
5
|
+
*
|
|
6
|
+
* Project entries replace user entries by name on collision and append new names. The top-level `default_kb`
|
|
7
|
+
* pointer resolves by name against the merged entries (the project's value overriding the user's); the resolved
|
|
8
|
+
* entry is exposed as `defaultKb`.
|
|
9
|
+
* Within a single file, relative `path` values resolve against that file's directory and a leading `~/` expands
|
|
10
|
+
* against `$HOME`. Both files are optional; when neither exists the result has no entries.
|
|
11
|
+
* Malformed YAML, a structural defect, or a `default_kb` that names no registered KB throw.
|
|
12
|
+
*/
|
|
2
13
|
export declare function loadKbRegistry(input?: {
|
|
3
14
|
userConfigPath?: string;
|
|
4
15
|
projectDir?: string;
|
|
5
16
|
home?: string;
|
|
6
17
|
}): Promise<KbRegistry>;
|
|
18
|
+
/** The outcome of a no-throw registry load: the resolved config plus a captured error message when loading failed. */
|
|
7
19
|
export interface KbRegistryLoadResult {
|
|
20
|
+
/** The merged registry, or an empty config when loading threw. */
|
|
8
21
|
config: KbRegistry;
|
|
22
|
+
/** The thrown error's message, present only when `loadKbRegistry` failed. */
|
|
9
23
|
error?: string;
|
|
10
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Load the merged `kb.yaml` registry without throwing, capturing any failure message instead of presenting it.
|
|
27
|
+
*
|
|
28
|
+
* On success — including the legitimate "no registry files present" case, which `loadKbRegistry` already returns as an
|
|
29
|
+
* empty config — `error` is absent. On a malformed file, a schema violation, an unresolvable `default_kb`, or a
|
|
30
|
+
* non-ENOENT read failure, the result degrades to an empty config and carries the thrown message in `error`. Each
|
|
31
|
+
* caller decides whether and how to surface that message; this wrapper neither writes to stderr nor builds a
|
|
32
|
+
* diagnostic.
|
|
33
|
+
*/
|
|
11
34
|
export declare function tryLoadKbRegistry(input?: {
|
|
12
35
|
userConfigPath?: string;
|
|
13
36
|
projectDir?: string;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
/** The outcome of a registry write. */
|
|
1
2
|
export interface RegisterStoreResult {
|
|
3
|
+
/** `added` when a new entry was written; `already-present` when an entry of the same name already existed. */
|
|
2
4
|
status: 'added' | 'already-present';
|
|
3
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Inserts a knowledge-base entry under `kbs:` in a `kb.yaml` registry, creating the file and its parent directory when
|
|
8
|
+
* absent and preserving any existing comments and formatting. Every write leaves the `kbs:` entries alphabetically
|
|
9
|
+
* ordered, so a registry that has already drifted converges rather than merely staying sorted. An existing entry of the
|
|
10
|
+
* same name is left untouched and reported as `already-present`; that path writes nothing, so re-registering never
|
|
11
|
+
* reorders the file. The registry is validated against its schema both before mutation (so an already-corrupt file
|
|
12
|
+
* throws rather than being silently extended) and after (so an invalid entry, such as an empty `storePath` from a
|
|
13
|
+
* direct caller, throws rather than being written), never producing a corrupt file.
|
|
14
|
+
*/
|
|
4
15
|
export declare function registerStore(input: {
|
|
5
16
|
registryPath: string;
|
|
6
17
|
name: string;
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { Document } from 'yaml';
|
|
2
|
+
/**
|
|
3
|
+
* Reads and parses a `kb.yaml` registry as a `yaml` Document, returning a fresh empty document when the file is absent
|
|
4
|
+
* or empty. Parsing through `parseDocument` preserves comments and formatting so registry writers can mutate in place
|
|
5
|
+
* without discarding them. Shared by every registry writer (`registerStore`, `setDefaultKb`, `clearDefaultKb`).
|
|
6
|
+
*/
|
|
2
7
|
export declare function loadRegistryDocument(path: string): Promise<Document>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the top-level `default_kb` pointer from a `kb.yaml` registry, preserving existing comments and formatting. A
|
|
3
|
+
* no-op when no default is set (or the file is absent): the file is left untouched rather than rewritten, so clearing is
|
|
4
|
+
* idempotent and never reformats. Validates an existing registry against its schema first, so a corrupt file throws
|
|
5
|
+
* rather than being rewritten.
|
|
6
|
+
*/
|
|
1
7
|
export declare function clearDefaultKb(input: {
|
|
2
8
|
registryPath: string;
|
|
3
9
|
}): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Sets the top-level `default_kb` pointer in a `kb.yaml` registry to `name`, creating the file and its parent directory
|
|
12
|
+
* when absent and preserving existing comments and formatting. Validates the registry against its schema before mutating
|
|
13
|
+
* (so an already-corrupt file throws rather than being rewritten) and asserts `name` is registered under `kbs` in the
|
|
14
|
+
* file (a `default_kb` naming no registered KB would fail every subsequent load), then re-validates the result.
|
|
15
|
+
*/
|
|
4
16
|
export declare function setDefaultKb(input: {
|
|
5
17
|
registryPath: string;
|
|
6
18
|
name: string;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
/** Controls which `stat` failures a filesystem-existence check treats as "absent". */
|
|
1
2
|
export interface ExistsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Stat-error codes treated as "absent" (the helper returns `false`). Any other failure is re-thrown so a genuine
|
|
5
|
+
* error — most importantly a permission denial on a path that does exist — is not silently read as absence.
|
|
6
|
+
* Defaults to `['ENOENT']`.
|
|
7
|
+
*/
|
|
2
8
|
absentCodes?: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* When `true`, every `stat` failure is treated as "absent" (`false`), including permission errors.
|
|
11
|
+
* Reserved for best-effort probes such as an ancestor walk, where an unreadable path should be skipped rather than
|
|
12
|
+
* abort the operation. Takes precedence over `absentCodes`.
|
|
13
|
+
*/
|
|
3
14
|
treatErrorsAsAbsent?: boolean;
|
|
4
15
|
}
|
|
16
|
+
/** Returns true when `path` exists and is a directory. See {@link ExistsOptions} for the absence policy. */
|
|
5
17
|
export declare function directoryExists(path: string, options?: ExistsOptions): Promise<boolean>;
|
|
18
|
+
/** Returns true when something exists at `path`. See {@link ExistsOptions} for the absence policy. */
|
|
6
19
|
export declare function pathExists(path: string, options?: ExistsOptions): Promise<boolean>;
|
|
@@ -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 that git tracks under `root`, plus every untracked path that no ignore rule covers, as an
|
|
3
|
+
* NFC-normalized set of root-relative, slash-separated paths. 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;
|