@msareen/knowledge-hub-builder 0.2.1 → 0.2.3
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/AGENTS.md +4 -2
- package/README.md +69 -11
- package/SPEC.md +35 -8
- package/package.json +3 -1
- package/scripts/cli.ts +46 -25
- package/scripts/config.ts +229 -0
- package/scripts/doctor.ts +206 -0
- package/scripts/export.ts +8 -4
- package/scripts/hubs.ts +185 -93
- package/scripts/ingest/acquire.ts +1 -1
- package/scripts/ingest/folder.ts +2 -1
- package/scripts/ingest/index.ts +21 -13
- package/scripts/init.ts +29 -13
- package/scripts/lib/color.ts +75 -0
- package/scripts/lib/config-check.ts +364 -0
- package/scripts/lib/extract.ts +36 -4
- package/scripts/lib/log.ts +4 -2
- package/scripts/lib/registry.ts +20 -1
- package/scripts/lib/relocate.ts +1 -1
- package/scripts/lib/upgrade.ts +17 -5
- package/scripts/lib/util.ts +1 -1
- package/scripts/lint.ts +185 -77
- package/scripts/new-bundle.ts +9 -4
- package/scripts/visualize.ts +7 -6
- package/skills/ingest/SKILL.md +1 -1
- package/skills/lint/SKILL.md +24 -2
- package/templates/hub/gitignore +2 -2
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// khb doctor — one read-only report on the state of a hub.
|
|
2
|
+
//
|
|
3
|
+
// Every check here already existed, scattered across the preambles of commands that each
|
|
4
|
+
// knew one of them: cli.ts announces a move and a version drift, `khb upgrade` prints the
|
|
5
|
+
// `khb update` hint, `khb ingest` counts the uncurated rows on its way out, and the
|
|
6
|
+
// transcriber probe only ever spoke during a run that needed it. So the answer to "what
|
|
7
|
+
// state is this hub in?" was: run several commands that change things and read their
|
|
8
|
+
// margins. This command asks nothing of the hub but to look at it.
|
|
9
|
+
//
|
|
10
|
+
// It writes nothing. That is the point, and it is also the boundary: `doctor` reports and
|
|
11
|
+
// names the command that repairs, but never repairs. `khb lint` stays the structural
|
|
12
|
+
// validator — doctor counts and points at it rather than duplicating a rule.
|
|
13
|
+
import { HUB, BUNDLES, listBundles, read, join, existsSync } from "./lib/util";
|
|
14
|
+
import { readLedger } from "./lib/ledger";
|
|
15
|
+
import { staleLocations, hubVersion } from "./lib/upgrade";
|
|
16
|
+
import { diffSourcesYamlAll } from "./lib/schema";
|
|
17
|
+
import { transcriberStatus } from "./lib/extract";
|
|
18
|
+
import { version, MARKER, markerIn } from "./lib/paths";
|
|
19
|
+
import { listHubs, canonical, loadConfig, agentFor, isAlive } from "./lib/registry";
|
|
20
|
+
import { checkConfig } from "./lib/config-check";
|
|
21
|
+
import { section, detail, totalElapsed } from "./lib/log";
|
|
22
|
+
import { rejectUnknownFlags } from "./lib/args";
|
|
23
|
+
import { paint } from "./lib/color";
|
|
24
|
+
import { readdirSync, statSync } from "node:fs";
|
|
25
|
+
import { relative } from "node:path";
|
|
26
|
+
|
|
27
|
+
rejectUnknownFlags(process.argv.slice(2), "khb doctor");
|
|
28
|
+
|
|
29
|
+
/** Findings are advisory: doctor's exit code reports whether it ran, not what it found. */
|
|
30
|
+
const findings: string[] = [];
|
|
31
|
+
const flag = (msg: string, fix?: string) =>
|
|
32
|
+
findings.push(fix ? `${msg}\n ${paint.dim("fix:")} ${paint.cmd(fix)}` : msg);
|
|
33
|
+
|
|
34
|
+
console.log(`${paint.head("khb doctor")} → ${paint.path(HUB)}`);
|
|
35
|
+
|
|
36
|
+
// ---- Hub identity -----------------------------------------------------------------------
|
|
37
|
+
const marker = (() => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(read(join(HUB, markerIn(HUB) ?? MARKER))) as Record<string, unknown>;
|
|
40
|
+
} catch {
|
|
41
|
+
return {} as Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
section("Hub");
|
|
46
|
+
detail(`name ${(marker.name as string) || "(unset — khb.json 'name')"}`);
|
|
47
|
+
detail(`description ${(marker.description as string) || "(unset — khb.json 'description')"}`);
|
|
48
|
+
|
|
49
|
+
// Drift is normally self-healing: cli.ts refreshes a hub before any in-hub command, this one
|
|
50
|
+
// included, so a mismatch here means the refresh was suppressed rather than that it is due.
|
|
51
|
+
const stamped = hubVersion(HUB);
|
|
52
|
+
const installed = version();
|
|
53
|
+
detail(
|
|
54
|
+
`khb version ${stamped ?? "unstamped"}` +
|
|
55
|
+
(stamped === installed ? ` (matches installed)` : ` — installed is ${installed}`),
|
|
56
|
+
);
|
|
57
|
+
if (stamped !== installed)
|
|
58
|
+
flag(
|
|
59
|
+
`hub is stamped ${stamped ?? "unstamped"} but khb is ${installed}; its contract docs may be a version behind.`,
|
|
60
|
+
process.env.KHB_NO_AUTO_UPGRADE ? "unset KHB_NO_AUTO_UPGRADE, or run: khb upgrade" : "khb upgrade",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ---- Location and registry --------------------------------------------------------------
|
|
64
|
+
// A hub moved more than once before anyone repaired it carries every former home, and
|
|
65
|
+
// `khb update --path` rewrites them all in one pass — so say how many there are rather than
|
|
66
|
+
// showing the most recent and implying it is the only one.
|
|
67
|
+
const stale = staleLocations(HUB);
|
|
68
|
+
detail(
|
|
69
|
+
`location ${
|
|
70
|
+
stale.length
|
|
71
|
+
? `moved from ${stale[stale.length - 1]}${stale.length > 1 ? ` (+${stale.length - 1} earlier)` : ""}`
|
|
72
|
+
: "matches the marker"
|
|
73
|
+
}`,
|
|
74
|
+
);
|
|
75
|
+
if (stale.length)
|
|
76
|
+
flag(
|
|
77
|
+
`this hub has moved; absolute paths recorded inside it still name ${stale.length > 1 ? "former locations" : "its former location"}.`,
|
|
78
|
+
"khb update --path (khb update --path --dry-run to preview)",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const registered = listHubs().some((entry) => canonical(entry.path) === canonical(HUB));
|
|
82
|
+
detail(`registered ${registered ? "yes" : "no — 'khb list' and 'khb go' will not offer it"}`);
|
|
83
|
+
|
|
84
|
+
// ---- Machine config ---------------------------------------------------------------------
|
|
85
|
+
// Not about this hub — about the file that lists every hub on the machine and names the
|
|
86
|
+
// agent `khb go` launches. It belongs in doctor because it fails the same way a hub does:
|
|
87
|
+
// quietly. `loadConfig` ignores what it cannot read, so a hand edit that broke the JSON
|
|
88
|
+
// costs you every shortcut with no message anywhere. The rules live in lib/config-check.ts;
|
|
89
|
+
// doctor reports them and `khb config fix` is the half that writes.
|
|
90
|
+
section("Machine config");
|
|
91
|
+
const configReport = checkConfig();
|
|
92
|
+
detail(`file ${configReport.path}`);
|
|
93
|
+
if (!configReport.exists) detail(`state not written yet — created the first time khb registers a hub`);
|
|
94
|
+
else {
|
|
95
|
+
const machineConfig = loadConfig();
|
|
96
|
+
const agent = agentFor(machineConfig);
|
|
97
|
+
detail(`agent ${agent ? `${agent.name} (${agent.spec.command})` : "none — khb go prints the path"}`);
|
|
98
|
+
const hubs = listHubs();
|
|
99
|
+
const missing = hubs.filter((entry) => !isAlive(entry)).length;
|
|
100
|
+
detail(`hubs ${hubs.length} registered${missing ? `, ${missing} missing` : ""}`);
|
|
101
|
+
detail(
|
|
102
|
+
`schema ${configReport.findings.length ? `${configReport.findings.length} finding(s)` : "clean"}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
for (const finding of configReport.findings) flag(`config: ${finding.what}`, finding.fix);
|
|
106
|
+
|
|
107
|
+
// ---- sources.yaml schema ----------------------------------------------------------------
|
|
108
|
+
const schemaDiffs = diffSourcesYamlAll(HUB);
|
|
109
|
+
if (schemaDiffs.length) {
|
|
110
|
+
const fields = schemaDiffs.reduce((total, diff) => total + diff.changes.length, 0);
|
|
111
|
+
flag(
|
|
112
|
+
`${fields} sources.yaml field(s) across ${schemaDiffs.length} bundle(s) predate the current schema.`,
|
|
113
|
+
"khb update --schema (khb update --schema --dry-run to preview)",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- Bundles ----------------------------------------------------------------------------
|
|
118
|
+
const RESERVED = ["index.md", "log.md", "refs.md"];
|
|
119
|
+
|
|
120
|
+
/** Concept docs: every .md in the bundle outside raw/ that is not a reserved filename. */
|
|
121
|
+
function conceptCount(dir: string): number {
|
|
122
|
+
const walk = (current: string): string[] =>
|
|
123
|
+
readdirSync(current).flatMap((entry: string) => {
|
|
124
|
+
const path = join(current, entry);
|
|
125
|
+
if (statSync(path).isDirectory()) return entry === "raw" ? [] : walk(path);
|
|
126
|
+
return [relative(dir, path).replaceAll("\\", "/")];
|
|
127
|
+
});
|
|
128
|
+
return walk(dir).filter(
|
|
129
|
+
(file) => file.endsWith(".md") && !RESERVED.includes(file.split("/").pop()!),
|
|
130
|
+
).length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bundles = listBundles();
|
|
134
|
+
section(`Bundles (${bundles.length})`);
|
|
135
|
+
|
|
136
|
+
if (!bundles.length) {
|
|
137
|
+
detail(`none yet — ${paint.cmd('khb new-bundle <name> "scope"')}`);
|
|
138
|
+
} else {
|
|
139
|
+
const summaries = bundles.map((bundle) => {
|
|
140
|
+
const dir = join(BUNDLES, bundle);
|
|
141
|
+
const ledger = readLedger(dir);
|
|
142
|
+
const rawDir = join(dir, "raw");
|
|
143
|
+
const rawFiles = existsSync(rawDir)
|
|
144
|
+
? (readdirSync(rawDir, { recursive: true }) as string[]).filter((file) => file.endsWith(".md"))
|
|
145
|
+
.length
|
|
146
|
+
: 0;
|
|
147
|
+
const rows = [...ledger.values()];
|
|
148
|
+
return {
|
|
149
|
+
bundle,
|
|
150
|
+
concepts: conceptCount(dir),
|
|
151
|
+
rawFiles,
|
|
152
|
+
rows: rows.length,
|
|
153
|
+
// The catalog backlog in the ledger's own terms — "in raw/ but not yet distilled into
|
|
154
|
+
// a concept doc" — so a row must have a raw file to be part of it. A row with neither
|
|
155
|
+
// is *pending*, a different state with a different fix, and counting it in both would
|
|
156
|
+
// overstate the work cataloging can actually pick up.
|
|
157
|
+
backlog: rows.filter((row) => row.raw && !row.curated).length,
|
|
158
|
+
pending: rows.filter((row) => !row.raw).length,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const nameWidth = Math.max(6, ...summaries.map((summary) => summary.bundle.length));
|
|
163
|
+
detail(`${"bundle".padEnd(nameWidth)} concepts raw/ rows backlog pending`);
|
|
164
|
+
for (const summary of summaries)
|
|
165
|
+
detail(
|
|
166
|
+
`${summary.bundle.padEnd(nameWidth)} ${String(summary.concepts).padStart(8)} ` +
|
|
167
|
+
`${String(summary.rawFiles).padStart(4)} ${String(summary.rows).padStart(4)} ` +
|
|
168
|
+
`${String(summary.backlog).padStart(7)} ${String(summary.pending).padStart(7)}`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const backlog = summaries.reduce((total, summary) => total + summary.backlog, 0);
|
|
172
|
+
const pending = summaries.reduce((total, summary) => total + summary.pending, 0);
|
|
173
|
+
if (backlog)
|
|
174
|
+
flag(
|
|
175
|
+
`${backlog} row(s) in raw/ but not yet cataloged, across ` +
|
|
176
|
+
`${summaries.filter((summary) => summary.backlog).length} bundle(s).`,
|
|
177
|
+
"ask an agent to catalog the bundle (skills/catalog/SKILL.md)",
|
|
178
|
+
);
|
|
179
|
+
// An empty `raw` is a source khb saw and could not convert — a missing extractor, a
|
|
180
|
+
// protected file, or a --skip flag. It is not a failed run, but it is work still owed.
|
|
181
|
+
if (pending)
|
|
182
|
+
flag(
|
|
183
|
+
`${pending} source(s) acquired but not extracted (empty 'raw' in log.md).`,
|
|
184
|
+
"khb ingest <bundle> — after installing whatever the row's reason names",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- Extraction -------------------------------------------------------------------------
|
|
189
|
+
section("Extraction");
|
|
190
|
+
detail("bundled text, PDF, DOCX, ODT, XLSX, PPTX, OCR (images + scanned PDFs), captions");
|
|
191
|
+
const transcriber = await transcriberStatus();
|
|
192
|
+
detail(`transcriber ${transcriber.detail}`);
|
|
193
|
+
if (!transcriber.ready)
|
|
194
|
+
flag(`no transcriber is ready, so audio and video will pend.`, transcriber.fix);
|
|
195
|
+
|
|
196
|
+
// ---- Findings ---------------------------------------------------------------------------
|
|
197
|
+
section(findings.length ? `Findings (${findings.length})` : "Findings");
|
|
198
|
+
if (!findings.length) detail(paint.ok("none — nothing here needs attention."));
|
|
199
|
+
else for (const finding of findings) detail(`${paint.warn("-")} ${finding}`);
|
|
200
|
+
|
|
201
|
+
section("Next");
|
|
202
|
+
detail(`${paint.cmd("khb lint")} structural and OKF validation (doctor does not duplicate it)`);
|
|
203
|
+
console.log(
|
|
204
|
+
`\n${paint.head("doctor")}: ${findings.length ? paint.warn(`${findings.length} finding(s)`) : paint.ok("no findings")} ` +
|
|
205
|
+
`across ${bundles.length} bundle(s) in ${totalElapsed()}`,
|
|
206
|
+
);
|
package/scripts/export.ts
CHANGED
|
@@ -5,19 +5,20 @@ import { cpSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node
|
|
|
5
5
|
import { HUB, bundleDir, join } from "./lib/util";
|
|
6
6
|
import { detail, totalElapsed } from "./lib/log";
|
|
7
7
|
import { rejectUnknownFlags } from "./lib/args";
|
|
8
|
+
import { paint, paintErr } from "./lib/color";
|
|
8
9
|
|
|
9
10
|
const argv = process.argv.slice(2);
|
|
10
11
|
// Before reading positionals: an unrecognized flag would otherwise become the destination,
|
|
11
12
|
// and `khb export mybundle --force` would export into a folder named `--force`.
|
|
12
13
|
rejectUnknownFlags(argv, "khb export <bundle> [dest]");
|
|
13
14
|
const [name, destArg] = argv;
|
|
14
|
-
if (!name) { console.error(
|
|
15
|
+
if (!name) { console.error(`Usage: ${paintErr.cmd("khb export <bundle> [dest]")}`); process.exit(1); }
|
|
15
16
|
|
|
16
17
|
const src = bundleDir(name);
|
|
17
18
|
const dest = destArg ?? join(HUB, "export", name);
|
|
18
|
-
if (existsSync(dest)) { console.error(
|
|
19
|
+
if (existsSync(dest)) { console.error(`${paintErr.bad("Destination exists:")} ${paintErr.path(dest)}`); process.exit(1); }
|
|
19
20
|
|
|
20
|
-
console.log(
|
|
21
|
+
console.log(`${paint.head("khb export")} → ${paint.name(name)}`);
|
|
21
22
|
detail(`from: ${src}`);
|
|
22
23
|
detail(`to: ${dest}`);
|
|
23
24
|
|
|
@@ -44,4 +45,7 @@ writeFileSync(join(dest, "outer.index.md"),
|
|
|
44
45
|
writeFileSync(join(dest, "README.md"),
|
|
45
46
|
`# ${name} (exported KHB bundle)\n\nExported: ${new Date().toISOString()}\nOrigin: KHB bundle-of-bundles repo.\n\nStandalone unit: start at AGENTS.md → outer.index.md → bundle/index.md.\nWorkflow protocols (query, ingest, lint, …) live in skills/<name>/SKILL.md and are discoverable by Claude and Codex.\nNote: refs.md entries pointing at other bundles will not resolve here.\n`);
|
|
46
47
|
|
|
47
|
-
console.log(
|
|
48
|
+
console.log(
|
|
49
|
+
`\n${paint.ok("Exported to")} ${paint.path(dest)} in ${totalElapsed()} ` +
|
|
50
|
+
paint.dim("(bundle + agent contracts, skills, single-bundle router)"),
|
|
51
|
+
);
|