@saasontools/strauss-kb 0.1.17 → 0.1.19
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/ARCHITECTURE.md +26 -0
- package/README.md +31 -3
- package/dist/{chunk-RMJUGTAQ.js → chunk-CQBLH7CE.js} +2 -2
- package/dist/{chunk-ZKIQOBHT.js → chunk-MNQNHYWL.js} +1052 -195
- package/dist/chunk-MNQNHYWL.js.map +1 -0
- package/dist/{chunk-SA3A2SPY.js → chunk-ZWSCLHG6.js} +2 -2
- package/dist/cli-main.cjs +1083 -232
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1062 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +331 -10
- package/dist/index.d.ts +331 -10
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1076 -225
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-ZKIQOBHT.js.map +0 -1
- /package/dist/{chunk-RMJUGTAQ.js.map → chunk-CQBLH7CE.js.map} +0 -0
- /package/dist/{chunk-SA3A2SPY.js.map → chunk-ZWSCLHG6.js.map} +0 -0
package/dist/cli-main.cjs
CHANGED
|
@@ -78,6 +78,14 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
78
78
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
79
79
|
message: "hash must be sha256:<64 hex chars>"
|
|
80
80
|
}).optional(),
|
|
81
|
+
/**
|
|
82
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
83
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
84
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
85
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
86
|
+
* comments, so reformatting the anchored code is not drift.
|
|
87
|
+
*/
|
|
88
|
+
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
81
89
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
82
90
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
83
91
|
/** Line count of the text the hash was taken over. */
|
|
@@ -468,9 +476,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
468
476
|
{ length: Math.min(limit, items.length) },
|
|
469
477
|
async () => {
|
|
470
478
|
while (!failed && next < items.length) {
|
|
471
|
-
const
|
|
479
|
+
const at2 = next++;
|
|
472
480
|
try {
|
|
473
|
-
out[
|
|
481
|
+
out[at2] = await fn(items[at2], at2);
|
|
474
482
|
} catch (error) {
|
|
475
483
|
failed = true;
|
|
476
484
|
throw error;
|
|
@@ -586,8 +594,8 @@ function safeSegment(value) {
|
|
|
586
594
|
function revRef(rev) {
|
|
587
595
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
588
596
|
let hash = 5381;
|
|
589
|
-
for (let
|
|
590
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
597
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
598
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
591
599
|
}
|
|
592
600
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
593
601
|
}
|
|
@@ -696,8 +704,8 @@ function repoUrlIsSafe(repo) {
|
|
|
696
704
|
if (!scheme?.[1]) return false;
|
|
697
705
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
698
706
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
699
|
-
const
|
|
700
|
-
return
|
|
707
|
+
const at2 = authority.lastIndexOf("@");
|
|
708
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
701
709
|
}
|
|
702
710
|
function protocolArgs() {
|
|
703
711
|
const allowed = allowedProtocols();
|
|
@@ -788,7 +796,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
788
796
|
return new Map([
|
|
789
797
|
...rejected2,
|
|
790
798
|
...wants.map(
|
|
791
|
-
(want,
|
|
799
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
792
800
|
)
|
|
793
801
|
]);
|
|
794
802
|
}
|
|
@@ -999,7 +1007,7 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
999
1007
|
return { ok: false, reason: "file-unreadable" };
|
|
1000
1008
|
}
|
|
1001
1009
|
});
|
|
1002
|
-
return new Map(wanted.map((file,
|
|
1010
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
1003
1011
|
}
|
|
1004
1012
|
|
|
1005
1013
|
// src/anchor-resolver/resolver.ts
|
|
@@ -1189,8 +1197,8 @@ async function ensureGrammar(language, options = {}) {
|
|
|
1189
1197
|
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1190
1198
|
const parts = [];
|
|
1191
1199
|
const total = pack2.tags.length;
|
|
1192
|
-
for (const [
|
|
1193
|
-
const name = `${language} tags${total > 1 ? ` part ${
|
|
1200
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1201
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1194
1202
|
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1195
1203
|
const held = await ensurePart(path, name, part, options);
|
|
1196
1204
|
if (held !== true) return miss(language, name, held);
|
|
@@ -1335,8 +1343,8 @@ function typeNameIn(receiver) {
|
|
|
1335
1343
|
while (stack.length) {
|
|
1336
1344
|
const node = stack.pop();
|
|
1337
1345
|
if (node.type === "type_identifier") return node.text;
|
|
1338
|
-
for (let
|
|
1339
|
-
const child = node.child(
|
|
1346
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1347
|
+
const child = node.child(at2);
|
|
1340
1348
|
if (child) stack.push(child);
|
|
1341
1349
|
}
|
|
1342
1350
|
}
|
|
@@ -1345,7 +1353,7 @@ function typeNameIn(receiver) {
|
|
|
1345
1353
|
function endsWith(chain, wanted) {
|
|
1346
1354
|
if (wanted.length > chain.length) return false;
|
|
1347
1355
|
const offset = chain.length - wanted.length;
|
|
1348
|
-
return wanted.every((segment,
|
|
1356
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1349
1357
|
}
|
|
1350
1358
|
function width(node) {
|
|
1351
1359
|
return node.endIndex - node.startIndex;
|
|
@@ -1371,6 +1379,26 @@ function spanOf(definition, source) {
|
|
|
1371
1379
|
};
|
|
1372
1380
|
}
|
|
1373
1381
|
|
|
1382
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1383
|
+
function tokens(root) {
|
|
1384
|
+
const out = [];
|
|
1385
|
+
const stack = [root];
|
|
1386
|
+
while (stack.length) {
|
|
1387
|
+
const node = stack.pop();
|
|
1388
|
+
if (node.type.includes("comment")) continue;
|
|
1389
|
+
if (node.childCount === 0) {
|
|
1390
|
+
const text = node.text.trim();
|
|
1391
|
+
if (text) out.push(text);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1395
|
+
const child = node.child(at2);
|
|
1396
|
+
if (child) stack.push(child);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return out;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1374
1402
|
// src/tree-sitter-resolver/resolver.ts
|
|
1375
1403
|
var TREE_CACHE_LIMIT = 32;
|
|
1376
1404
|
var TreeSitterResolver = class {
|
|
@@ -1416,7 +1444,7 @@ var TreeSitterResolver = class {
|
|
|
1416
1444
|
(language) => this.load(language)
|
|
1417
1445
|
);
|
|
1418
1446
|
languages.forEach(
|
|
1419
|
-
(language,
|
|
1447
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1420
1448
|
);
|
|
1421
1449
|
}
|
|
1422
1450
|
/**
|
|
@@ -1504,6 +1532,60 @@ var TreeSitterResolver = class {
|
|
|
1504
1532
|
this.trees.set(key, parsed);
|
|
1505
1533
|
return parsed;
|
|
1506
1534
|
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1537
|
+
*
|
|
1538
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1539
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1540
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1541
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1542
|
+
*/
|
|
1543
|
+
spans(source, file) {
|
|
1544
|
+
const language = languageForFile(file);
|
|
1545
|
+
if (!language) return [];
|
|
1546
|
+
const loaded = this.loaded.get(language);
|
|
1547
|
+
if (!loaded) return [];
|
|
1548
|
+
const parsed = this.parse(language, loaded, source);
|
|
1549
|
+
if (!parsed) return [];
|
|
1550
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1551
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1552
|
+
span: spanOf(definition, source)
|
|
1553
|
+
}));
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1557
|
+
* joined by single spaces.
|
|
1558
|
+
*
|
|
1559
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1560
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1561
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1562
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1563
|
+
* those are leaves.
|
|
1564
|
+
*
|
|
1565
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1566
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1567
|
+
*/
|
|
1568
|
+
normalize(text, file) {
|
|
1569
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1570
|
+
if (!language) return null;
|
|
1571
|
+
const loaded = this.loaded.get(language);
|
|
1572
|
+
if (!loaded) return null;
|
|
1573
|
+
const parser = this.parser;
|
|
1574
|
+
if (!parser) return null;
|
|
1575
|
+
let tree;
|
|
1576
|
+
try {
|
|
1577
|
+
parser.setLanguage(loaded.language);
|
|
1578
|
+
tree = parser.parse(text);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
if (!tree) return null;
|
|
1583
|
+
try {
|
|
1584
|
+
return tokens(tree.rootNode).join(" ");
|
|
1585
|
+
} finally {
|
|
1586
|
+
tree.delete();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1507
1589
|
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1508
1590
|
reset() {
|
|
1509
1591
|
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
@@ -1657,7 +1739,7 @@ var regexResolver = {
|
|
|
1657
1739
|
);
|
|
1658
1740
|
const nearest = Math.min(...distances);
|
|
1659
1741
|
if (Number.isFinite(nearest)) {
|
|
1660
|
-
candidates = candidates.filter((_,
|
|
1742
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1661
1743
|
}
|
|
1662
1744
|
}
|
|
1663
1745
|
if (candidates.length !== 1) return null;
|
|
@@ -1672,8 +1754,8 @@ function escapeRegExp(value) {
|
|
|
1672
1754
|
}
|
|
1673
1755
|
function distanceToParent(lines, index2, parent) {
|
|
1674
1756
|
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1675
|
-
for (let
|
|
1676
|
-
if (parent.test(lines[
|
|
1757
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1758
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1677
1759
|
}
|
|
1678
1760
|
return Number.POSITIVE_INFINITY;
|
|
1679
1761
|
}
|
|
@@ -1701,10 +1783,12 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1701
1783
|
if (attempt.reason === "symbol-not-found") continue;
|
|
1702
1784
|
return { ok: false, reason: attempt.reason };
|
|
1703
1785
|
}
|
|
1786
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1704
1787
|
return {
|
|
1705
1788
|
ok: true,
|
|
1706
1789
|
span: attempt.span,
|
|
1707
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
1790
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1791
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1708
1792
|
};
|
|
1709
1793
|
}
|
|
1710
1794
|
return { ok: false, reason: "symbol-not-found" };
|
|
@@ -1732,6 +1816,11 @@ function resolverChanged(source, anchor, produced) {
|
|
|
1732
1816
|
);
|
|
1733
1817
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1734
1818
|
}
|
|
1819
|
+
function anchorHashOf(anchor, outcome) {
|
|
1820
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1821
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1822
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1823
|
+
}
|
|
1735
1824
|
|
|
1736
1825
|
// src/anchor-resolver/drift.ts
|
|
1737
1826
|
async function detectAnchorDrift(records, options = {}) {
|
|
@@ -1811,16 +1900,29 @@ function unresolved(anchor, reason, repo) {
|
|
|
1811
1900
|
state: "unresolved",
|
|
1812
1901
|
diffSize: null,
|
|
1813
1902
|
...reason ? { reason } : {},
|
|
1814
|
-
...repo ? { repo } : {}
|
|
1903
|
+
...repo ? { repo } : {},
|
|
1904
|
+
...classOf(reason)
|
|
1815
1905
|
};
|
|
1816
1906
|
}
|
|
1907
|
+
function provisionalDriftClass(entry) {
|
|
1908
|
+
if (entry.state === "unresolved") {
|
|
1909
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
1910
|
+
}
|
|
1911
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1912
|
+
}
|
|
1913
|
+
function classOf(reason) {
|
|
1914
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1915
|
+
return settled ? { class: settled } : {};
|
|
1916
|
+
}
|
|
1817
1917
|
function hashIn(source, anchor, resolvers) {
|
|
1818
1918
|
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1819
1919
|
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1920
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1820
1921
|
return {
|
|
1821
1922
|
ok: true,
|
|
1822
1923
|
current: {
|
|
1823
|
-
hash
|
|
1924
|
+
hash,
|
|
1925
|
+
kind,
|
|
1824
1926
|
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1825
1927
|
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1826
1928
|
}
|
|
@@ -1833,11 +1935,14 @@ function resolverExtras(source, anchor, current) {
|
|
|
1833
1935
|
};
|
|
1834
1936
|
}
|
|
1835
1937
|
function compared(anchor, current, extra = {}) {
|
|
1938
|
+
const matched = current.hash === anchor.hash;
|
|
1836
1939
|
return {
|
|
1837
1940
|
...base(anchor),
|
|
1838
|
-
state:
|
|
1941
|
+
state: matched ? "match" : "drifted",
|
|
1839
1942
|
currentHash: current.hash,
|
|
1943
|
+
hashKind: current.kind,
|
|
1840
1944
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1945
|
+
...matched ? {} : { class: "changed" },
|
|
1841
1946
|
...extra
|
|
1842
1947
|
};
|
|
1843
1948
|
}
|
|
@@ -2072,14 +2177,17 @@ function asBudgets(value) {
|
|
|
2072
2177
|
if (value === null || typeof value !== "object") return {};
|
|
2073
2178
|
const table2 = value;
|
|
2074
2179
|
const pick = (key, min) => {
|
|
2075
|
-
const
|
|
2076
|
-
return typeof
|
|
2180
|
+
const raw2 = table2[key];
|
|
2181
|
+
return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
|
|
2077
2182
|
};
|
|
2078
2183
|
const budgetTokens = pick("budgetTokens", 1);
|
|
2079
2184
|
const fullUnderTokens = pick("fullUnderTokens", 0);
|
|
2185
|
+
const raw = table2["excludeTags"];
|
|
2186
|
+
const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
2080
2187
|
return {
|
|
2081
2188
|
...budgetTokens ? { budgetTokens } : {},
|
|
2082
|
-
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
|
|
2189
|
+
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
|
|
2190
|
+
...excludeTags ? { excludeTags } : {}
|
|
2083
2191
|
};
|
|
2084
2192
|
}
|
|
2085
2193
|
function contextProfileBudgets(manifest, profile) {
|
|
@@ -2279,7 +2387,7 @@ async function listPins(store, workspaceDir) {
|
|
|
2279
2387
|
}
|
|
2280
2388
|
|
|
2281
2389
|
// src/kb-pins/pin.ts
|
|
2282
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
2390
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
2283
2391
|
const layer = options.layer ?? "project";
|
|
2284
2392
|
const root = layerRoot(workspaceDir, layer);
|
|
2285
2393
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -2307,7 +2415,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2307
2415
|
return {
|
|
2308
2416
|
path: existing.path,
|
|
2309
2417
|
layer,
|
|
2310
|
-
pinnedAt: existing.pinnedAt ??
|
|
2418
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
2311
2419
|
alreadyPinned: true,
|
|
2312
2420
|
...updated.mode ? { mode: updated.mode } : {},
|
|
2313
2421
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -2317,7 +2425,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2317
2425
|
}
|
|
2318
2426
|
const entry = {
|
|
2319
2427
|
path: storablePath(root, bundlePath2),
|
|
2320
|
-
pinnedAt:
|
|
2428
|
+
pinnedAt: at2,
|
|
2321
2429
|
...fields
|
|
2322
2430
|
};
|
|
2323
2431
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -2327,7 +2435,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2327
2435
|
return {
|
|
2328
2436
|
path: entry.path,
|
|
2329
2437
|
layer,
|
|
2330
|
-
pinnedAt:
|
|
2438
|
+
pinnedAt: at2,
|
|
2331
2439
|
alreadyPinned: false,
|
|
2332
2440
|
...fields,
|
|
2333
2441
|
...warning ? { warning } : {}
|
|
@@ -2366,6 +2474,9 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
2366
2474
|
var import_zod6 = require("zod");
|
|
2367
2475
|
var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2368
2476
|
var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2477
|
+
var TAGS = import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
|
|
2478
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
2479
|
+
);
|
|
2369
2480
|
var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
|
|
2370
2481
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2371
2482
|
);
|
|
@@ -2379,14 +2490,49 @@ function argvFlag(argv, name) {
|
|
|
2379
2490
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2380
2491
|
return value2;
|
|
2381
2492
|
}
|
|
2382
|
-
const
|
|
2383
|
-
if (
|
|
2384
|
-
const value = argv[
|
|
2493
|
+
const at2 = argv.indexOf(name);
|
|
2494
|
+
if (at2 === -1) return void 0;
|
|
2495
|
+
const value = argv[at2 + 1];
|
|
2385
2496
|
if (value === void 0 || value.startsWith("--")) {
|
|
2386
2497
|
throw new KbMissingFlagValueError(name);
|
|
2387
2498
|
}
|
|
2388
2499
|
return value;
|
|
2389
2500
|
}
|
|
2501
|
+
function argvFlags(argv, name) {
|
|
2502
|
+
const values = [];
|
|
2503
|
+
for (const [at2, arg] of argv.entries()) {
|
|
2504
|
+
if (arg.startsWith(`${name}=`)) {
|
|
2505
|
+
const value = arg.slice(name.length + 1);
|
|
2506
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
2507
|
+
values.push(value);
|
|
2508
|
+
} else if (arg === name) {
|
|
2509
|
+
const value = argv[at2 + 1];
|
|
2510
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2511
|
+
throw new KbMissingFlagValueError(name);
|
|
2512
|
+
}
|
|
2513
|
+
values.push(value);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
return values;
|
|
2517
|
+
}
|
|
2518
|
+
function argvWithout(argv, ...names) {
|
|
2519
|
+
const kept = [];
|
|
2520
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
2521
|
+
const arg = argv[at2];
|
|
2522
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
2523
|
+
if (names.includes(arg)) {
|
|
2524
|
+
at2 += 1;
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
kept.push(arg);
|
|
2528
|
+
}
|
|
2529
|
+
return kept;
|
|
2530
|
+
}
|
|
2531
|
+
function argvPositional(argv, ...names) {
|
|
2532
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
2533
|
+
(arg) => !arg.startsWith("--")
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2390
2536
|
|
|
2391
2537
|
// src/commands/anchor-resolve.ts
|
|
2392
2538
|
function resolverSummary(results) {
|
|
@@ -2474,11 +2620,14 @@ var anchorResolveCommand = define({
|
|
|
2474
2620
|
}
|
|
2475
2621
|
const resolved = outcome.span;
|
|
2476
2622
|
const producedBy = outcome.resolver;
|
|
2477
|
-
const currentHash =
|
|
2623
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
2478
2624
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2625
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
2626
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
2479
2627
|
const stamped = {
|
|
2480
2628
|
...anchor,
|
|
2481
|
-
hash:
|
|
2629
|
+
hash: stampedHash,
|
|
2630
|
+
hash_kind: stampedKind,
|
|
2482
2631
|
lines: currentLines,
|
|
2483
2632
|
resolved_at: now(),
|
|
2484
2633
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -2488,7 +2637,8 @@ var anchorResolveCommand = define({
|
|
|
2488
2637
|
results.push({
|
|
2489
2638
|
...base2,
|
|
2490
2639
|
state: "stamped",
|
|
2491
|
-
currentHash,
|
|
2640
|
+
currentHash: stampedHash,
|
|
2641
|
+
hashKind: stampedKind,
|
|
2492
2642
|
...producedBy ? { resolver: producedBy } : {}
|
|
2493
2643
|
});
|
|
2494
2644
|
updated.push(stamped);
|
|
@@ -2500,6 +2650,7 @@ var anchorResolveCommand = define({
|
|
|
2500
2650
|
...base2,
|
|
2501
2651
|
state: "drifted",
|
|
2502
2652
|
currentHash,
|
|
2653
|
+
hashKind: kind,
|
|
2503
2654
|
diffSize: lineDelta(anchor, currentLines),
|
|
2504
2655
|
...producedBy ? { resolver: producedBy } : {},
|
|
2505
2656
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
@@ -2528,6 +2679,7 @@ var anchorResolveCommand = define({
|
|
|
2528
2679
|
...base2,
|
|
2529
2680
|
state: "match",
|
|
2530
2681
|
currentHash,
|
|
2682
|
+
hashKind: kind,
|
|
2531
2683
|
...producedBy ? { resolver: producedBy } : {},
|
|
2532
2684
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
2533
2685
|
});
|
|
@@ -2760,7 +2912,8 @@ function warningAnchor(entry) {
|
|
|
2760
2912
|
diffSize,
|
|
2761
2913
|
...reason !== void 0 ? { reason } : {},
|
|
2762
2914
|
...repo !== void 0 ? { repo } : {},
|
|
2763
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
2915
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
2916
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
2764
2917
|
};
|
|
2765
2918
|
}
|
|
2766
2919
|
function resolveHeads(from, byId) {
|
|
@@ -2812,6 +2965,13 @@ function successors(record, byId) {
|
|
|
2812
2965
|
return { records, missing: missing2 };
|
|
2813
2966
|
}
|
|
2814
2967
|
|
|
2968
|
+
// src/kb-tags.ts
|
|
2969
|
+
function matchesTags(record, filter) {
|
|
2970
|
+
if (!filter.tags?.length && !filter.excludeTags?.length) return true;
|
|
2971
|
+
const carried = new Set(record.frontmatter.tags ?? []);
|
|
2972
|
+
return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2815
2975
|
// src/catalog.ts
|
|
2816
2976
|
var EMPTY_STANDINGS = {
|
|
2817
2977
|
current: 0,
|
|
@@ -2822,7 +2982,7 @@ var EMPTY_STANDINGS = {
|
|
|
2822
2982
|
};
|
|
2823
2983
|
function catalog(bundle, options = {}) {
|
|
2824
2984
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2825
|
-
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
2985
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
|
|
2826
2986
|
conceptId: hit.record.conceptId,
|
|
2827
2987
|
type: hit.record.frontmatter.type,
|
|
2828
2988
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2862,25 +3022,39 @@ function renderCatalogLine(entry) {
|
|
|
2862
3022
|
var catalogCommand = define({
|
|
2863
3023
|
name: "catalog",
|
|
2864
3024
|
tool: "kb_catalog",
|
|
2865
|
-
usage: "catalog [type]",
|
|
3025
|
+
usage: "catalog [type] [--tag T]...",
|
|
2866
3026
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2867
3027
|
input: import_zod10.z.object({
|
|
2868
3028
|
bundlePath,
|
|
2869
|
-
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
2870
|
-
|
|
2871
|
-
fromArgv: (argv, path) => ({
|
|
2872
|
-
bundlePath: path,
|
|
2873
|
-
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
3029
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
|
|
3030
|
+
tags: TAGS
|
|
2874
3031
|
}),
|
|
2875
|
-
|
|
2876
|
-
|
|
3032
|
+
fromArgv: (argv, path) => {
|
|
3033
|
+
const tags = argvFlags(argv, "--tag");
|
|
3034
|
+
const type = argvPositional(argv, "--tag");
|
|
3035
|
+
return {
|
|
3036
|
+
bundlePath: path,
|
|
3037
|
+
...type ? { type } : {},
|
|
3038
|
+
...tags.length ? { tags } : {}
|
|
3039
|
+
};
|
|
3040
|
+
},
|
|
3041
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => render(
|
|
3042
|
+
await store.catalog(path, {
|
|
3043
|
+
...type ? { type } : {},
|
|
3044
|
+
...tags ? { tags } : {}
|
|
3045
|
+
}),
|
|
2877
3046
|
path,
|
|
2878
|
-
type
|
|
3047
|
+
type,
|
|
3048
|
+
tags
|
|
2879
3049
|
)
|
|
2880
3050
|
});
|
|
2881
|
-
function render(result, bundle, type) {
|
|
3051
|
+
function render(result, bundle, type, tags) {
|
|
3052
|
+
const narrowed = [
|
|
3053
|
+
...type ? [type] : [],
|
|
3054
|
+
...tags?.length ? [`tags: ${tags.join(", ")}`] : []
|
|
3055
|
+
].join(" \xB7 ");
|
|
2882
3056
|
const lines = [
|
|
2883
|
-
`# KB Catalog${
|
|
3057
|
+
`# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
|
|
2884
3058
|
`bundle: ${bundle}`,
|
|
2885
3059
|
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
2886
3060
|
];
|
|
@@ -2892,7 +3066,7 @@ function render(result, bundle, type) {
|
|
|
2892
3066
|
lines.push("");
|
|
2893
3067
|
if (!result.entries.length) {
|
|
2894
3068
|
lines.push(
|
|
2895
|
-
|
|
3069
|
+
narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
|
|
2896
3070
|
);
|
|
2897
3071
|
} else {
|
|
2898
3072
|
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
@@ -2979,7 +3153,7 @@ function preamble() {
|
|
|
2979
3153
|
"tokens."
|
|
2980
3154
|
].join("\n");
|
|
2981
3155
|
}
|
|
2982
|
-
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
|
|
3156
|
+
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
|
|
2983
3157
|
const bundle = await store.list(absolutePath);
|
|
2984
3158
|
if (bundle.length === 0) {
|
|
2985
3159
|
return {
|
|
@@ -2993,7 +3167,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
2993
3167
|
let degradedFrom;
|
|
2994
3168
|
if (fullCap > 0) {
|
|
2995
3169
|
const full = await store.load(absolutePath, {
|
|
2996
|
-
budgetTokens: fullCap
|
|
3170
|
+
budgetTokens: fullCap,
|
|
3171
|
+
excludeTags
|
|
2997
3172
|
});
|
|
2998
3173
|
if (!full.loaded && pinMode === "full") {
|
|
2999
3174
|
degradedFrom = { approxTokens: full.approxTokens };
|
|
@@ -3023,7 +3198,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
3023
3198
|
};
|
|
3024
3199
|
}
|
|
3025
3200
|
}
|
|
3026
|
-
const adjudicated = adjudicate(bundle, bundle)
|
|
3201
|
+
const adjudicated = adjudicate(bundle, bundle).filter(
|
|
3202
|
+
(hit) => matchesTags(hit.record, { excludeTags })
|
|
3203
|
+
);
|
|
3027
3204
|
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
3028
3205
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
3029
3206
|
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
@@ -3044,6 +3221,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
3044
3221
|
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
3045
3222
|
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
3046
3223
|
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
3224
|
+
const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
|
|
3047
3225
|
const pins = merged.pins.filter(
|
|
3048
3226
|
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
3049
3227
|
);
|
|
@@ -3064,7 +3242,8 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
3064
3242
|
pin.absolutePath,
|
|
3065
3243
|
fullUnderTokens,
|
|
3066
3244
|
pin.mode,
|
|
3067
|
-
budgetTokens
|
|
3245
|
+
budgetTokens,
|
|
3246
|
+
excludeTags
|
|
3068
3247
|
),
|
|
3069
3248
|
frozen: pin.frozen === true
|
|
3070
3249
|
}))
|
|
@@ -3185,7 +3364,7 @@ ${CONTEXT_END}` : null;
|
|
|
3185
3364
|
var contextCommand = define({
|
|
3186
3365
|
name: "context",
|
|
3187
3366
|
tool: "kb_context",
|
|
3188
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
3367
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
3189
3368
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
3190
3369
|
input: import_zod11.z.object({
|
|
3191
3370
|
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
@@ -3197,6 +3376,9 @@ var contextCommand = define({
|
|
|
3197
3376
|
profile: import_zod11.z.string().optional().describe(
|
|
3198
3377
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
3199
3378
|
),
|
|
3379
|
+
excludeTags: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe(
|
|
3380
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
3381
|
+
),
|
|
3200
3382
|
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
3201
3383
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3202
3384
|
),
|
|
@@ -3210,19 +3392,22 @@ var contextCommand = define({
|
|
|
3210
3392
|
const profile = argvFlag(argv, "--profile");
|
|
3211
3393
|
const format = argvFlag(argv, "--format");
|
|
3212
3394
|
const event = argvFlag(argv, "--event");
|
|
3395
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
3213
3396
|
return {
|
|
3214
3397
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
3215
3398
|
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
3216
3399
|
...profile ? { profile } : {},
|
|
3400
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
3217
3401
|
...format ? { format } : {},
|
|
3218
3402
|
...event ? { event } : {}
|
|
3219
3403
|
};
|
|
3220
3404
|
},
|
|
3221
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
|
|
3405
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
3222
3406
|
const result = await buildContext(store, process.cwd(), {
|
|
3223
3407
|
...budgetTokens ? { budgetTokens } : {},
|
|
3224
3408
|
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
3225
3409
|
...profile ? { profile } : {},
|
|
3410
|
+
...excludeTags ? { excludeTags } : {},
|
|
3226
3411
|
// Degradations — a full pin that could not fit, a refused block — go
|
|
3227
3412
|
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
3228
3413
|
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
@@ -3236,7 +3421,406 @@ var contextCommand = define({
|
|
|
3236
3421
|
});
|
|
3237
3422
|
|
|
3238
3423
|
// src/commands/doctor.ts
|
|
3239
|
-
var
|
|
3424
|
+
var import_zod13 = require("zod");
|
|
3425
|
+
|
|
3426
|
+
// src/drift/git.ts
|
|
3427
|
+
var import_node_child_process3 = require("child_process");
|
|
3428
|
+
var import_node_util3 = require("util");
|
|
3429
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
3430
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
3431
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
3432
|
+
async function git2(cwd, args) {
|
|
3433
|
+
const env = { ...process.env };
|
|
3434
|
+
delete env["GIT_DIR"];
|
|
3435
|
+
delete env["GIT_WORK_TREE"];
|
|
3436
|
+
delete env["GIT_INDEX_FILE"];
|
|
3437
|
+
try {
|
|
3438
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
3439
|
+
timeout: GIT_TIMEOUT_MS,
|
|
3440
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
3441
|
+
env
|
|
3442
|
+
});
|
|
3443
|
+
return { ok: true, stdout };
|
|
3444
|
+
} catch {
|
|
3445
|
+
return { ok: false };
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
async function listRepoFiles(repoRoot) {
|
|
3449
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
3450
|
+
if (!result.ok) return [];
|
|
3451
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
3452
|
+
}
|
|
3453
|
+
async function readOldSource(repoRoot, anchor) {
|
|
3454
|
+
if (!filePathIsSafe(anchor.file))
|
|
3455
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3456
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
3457
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
3458
|
+
if (shown2 !== null) {
|
|
3459
|
+
return {
|
|
3460
|
+
ok: true,
|
|
3461
|
+
source: shown2,
|
|
3462
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
const at2 = anchor.resolved_at;
|
|
3467
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
3468
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3469
|
+
}
|
|
3470
|
+
const found = await git2(repoRoot, [
|
|
3471
|
+
"log",
|
|
3472
|
+
"-1",
|
|
3473
|
+
"--format=%H",
|
|
3474
|
+
`--before=${at2}`,
|
|
3475
|
+
"--end-of-options",
|
|
3476
|
+
"HEAD",
|
|
3477
|
+
"--",
|
|
3478
|
+
anchor.file
|
|
3479
|
+
]);
|
|
3480
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
3481
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
3482
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3483
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
3484
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
3485
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
3486
|
+
}
|
|
3487
|
+
async function showFile(repoRoot, ref, file) {
|
|
3488
|
+
const path = file.replace(/^\.\//, "");
|
|
3489
|
+
const result = await git2(repoRoot, [
|
|
3490
|
+
"show",
|
|
3491
|
+
"--end-of-options",
|
|
3492
|
+
`${ref}:${path}`
|
|
3493
|
+
]);
|
|
3494
|
+
return result.ok ? result.stdout : null;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/drift/moved.ts
|
|
3498
|
+
var import_promises7 = require("fs/promises");
|
|
3499
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
3500
|
+
var SEARCH_BATCH = 64;
|
|
3501
|
+
function movedSearch(repoRoot, options = {}) {
|
|
3502
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
3503
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
3504
|
+
const resolver = new TreeSitterResolver();
|
|
3505
|
+
let repoFiles;
|
|
3506
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
3507
|
+
const filesForLanguage = async (language) => {
|
|
3508
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
3509
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
3510
|
+
};
|
|
3511
|
+
return {
|
|
3512
|
+
async find(anchor) {
|
|
3513
|
+
const stored = anchor.hash;
|
|
3514
|
+
if (!stored) return void 0;
|
|
3515
|
+
const language = languageForFile(anchor.file);
|
|
3516
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
3517
|
+
const candidates = await filesForLanguage(language);
|
|
3518
|
+
if (!prepared.has(language)) {
|
|
3519
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
3520
|
+
prepared.add(language);
|
|
3521
|
+
}
|
|
3522
|
+
const floor = anchor.lines ?? 0;
|
|
3523
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
3524
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
3525
|
+
const hits = await mapLimit(
|
|
3526
|
+
batch,
|
|
3527
|
+
DEFAULT_IO_CONCURRENCY,
|
|
3528
|
+
async (file) => {
|
|
3529
|
+
const size2 = await sizeOf(file);
|
|
3530
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
3531
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
3532
|
+
}
|
|
3533
|
+
);
|
|
3534
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
3535
|
+
if (found) return found;
|
|
3536
|
+
}
|
|
3537
|
+
return void 0;
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
3542
|
+
const source = await read(file);
|
|
3543
|
+
if (!source.ok) return void 0;
|
|
3544
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
3545
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
3546
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
3547
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
3548
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
3549
|
+
return {
|
|
3550
|
+
file,
|
|
3551
|
+
symbol: found.symbol,
|
|
3552
|
+
startLine: found.span.startLine,
|
|
3553
|
+
endLine: found.span.endLine
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
return void 0;
|
|
3557
|
+
}
|
|
3558
|
+
function diskSize(repoRoot) {
|
|
3559
|
+
return async (file) => {
|
|
3560
|
+
const path = anchorFilePath(repoRoot, file);
|
|
3561
|
+
if (path === null) return null;
|
|
3562
|
+
try {
|
|
3563
|
+
return (await (0, import_promises7.stat)(path)).size;
|
|
3564
|
+
} catch {
|
|
3565
|
+
return null;
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
3570
|
+
const height = anchor.lines;
|
|
3571
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
3572
|
+
const source = await read(anchor.file);
|
|
3573
|
+
if (!source.ok) return void 0;
|
|
3574
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
3575
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
3576
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
return {
|
|
3580
|
+
file: anchor.file,
|
|
3581
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3582
|
+
startLine: at2 + 1,
|
|
3583
|
+
endLine: at2 + height
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
// src/drift/classify.ts
|
|
3590
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
3591
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3592
|
+
(anchor) => anchor.hash
|
|
3593
|
+
);
|
|
3594
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
3595
|
+
const treeSitter = new TreeSitterResolver();
|
|
3596
|
+
const resolvers = [treeSitter, regexResolver];
|
|
3597
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
3598
|
+
const wanted = [];
|
|
3599
|
+
entries.forEach((entry, at2) => {
|
|
3600
|
+
const anchor = anchors[at2];
|
|
3601
|
+
if (!anchor) return;
|
|
3602
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
3603
|
+
wanted.push({ anchor, entry });
|
|
3604
|
+
});
|
|
3605
|
+
if (!wanted.length) return [];
|
|
3606
|
+
await prepareResolvers(
|
|
3607
|
+
resolvers,
|
|
3608
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
3609
|
+
);
|
|
3610
|
+
const out = [];
|
|
3611
|
+
for (const { anchor, entry } of wanted) {
|
|
3612
|
+
const movedTo = await search.find(anchor);
|
|
3613
|
+
if (movedTo) {
|
|
3614
|
+
out.push({
|
|
3615
|
+
anchor,
|
|
3616
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
3617
|
+
class: "moved"
|
|
3618
|
+
});
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
3622
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
3623
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
3624
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
3625
|
+
out.push({
|
|
3626
|
+
anchor,
|
|
3627
|
+
entry: { ...entry, class: settled },
|
|
3628
|
+
class: settled,
|
|
3629
|
+
...newText !== void 0 ? { newText } : {},
|
|
3630
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
3631
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
return out;
|
|
3635
|
+
}
|
|
3636
|
+
function sameTokens(resolver, file, before, after) {
|
|
3637
|
+
if (before === after) return false;
|
|
3638
|
+
const left = resolver.normalize(before, file);
|
|
3639
|
+
const right = resolver.normalize(after, file);
|
|
3640
|
+
return left !== null && left === right;
|
|
3641
|
+
}
|
|
3642
|
+
async function currentText(reader, anchor, resolvers) {
|
|
3643
|
+
const read = await reader(anchor.file);
|
|
3644
|
+
if (!read.ok) return void 0;
|
|
3645
|
+
return spanIn(read.source, anchor, resolvers);
|
|
3646
|
+
}
|
|
3647
|
+
function spanIn(source, anchor, resolvers) {
|
|
3648
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
3649
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// src/drift/diff.ts
|
|
3653
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
3654
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
3655
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
3656
|
+
function diffBudget(anchors) {
|
|
3657
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
3658
|
+
return Math.min(
|
|
3659
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
3660
|
+
Math.max(
|
|
3661
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
3662
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
3663
|
+
)
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
function unifiedDiff(before, after, options = {}) {
|
|
3667
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
3668
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
3669
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
3670
|
+
const body = [];
|
|
3671
|
+
let added = 0;
|
|
3672
|
+
let removed = 0;
|
|
3673
|
+
for (const edit of edits(left, right)) {
|
|
3674
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
3675
|
+
else if (edit.kind === "remove") {
|
|
3676
|
+
body.push(`-${edit.line}`);
|
|
3677
|
+
removed += 1;
|
|
3678
|
+
} else {
|
|
3679
|
+
body.push(`+${edit.line}`);
|
|
3680
|
+
added += 1;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
const truncated = body.length > max;
|
|
3684
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
3685
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3686
|
+
const lines = [header, ...shown];
|
|
3687
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3688
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
3689
|
+
}
|
|
3690
|
+
function edits(left, right) {
|
|
3691
|
+
const rows = left.length;
|
|
3692
|
+
const cols = right.length;
|
|
3693
|
+
const table2 = Array.from(
|
|
3694
|
+
{ length: rows + 1 },
|
|
3695
|
+
() => new Array(cols + 1).fill(0)
|
|
3696
|
+
);
|
|
3697
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
3698
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
3699
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
3700
|
+
table2[row2 + 1][col2],
|
|
3701
|
+
table2[row2][col2 + 1]
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const out = [];
|
|
3706
|
+
let row = 0;
|
|
3707
|
+
let col = 0;
|
|
3708
|
+
while (row < rows && col < cols) {
|
|
3709
|
+
if (left[row] === right[col]) {
|
|
3710
|
+
out.push({ kind: "same", line: left[row] });
|
|
3711
|
+
row += 1;
|
|
3712
|
+
col += 1;
|
|
3713
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
3714
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3715
|
+
row += 1;
|
|
3716
|
+
} else {
|
|
3717
|
+
out.push({ kind: "add", line: right[col] });
|
|
3718
|
+
col += 1;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
for (; row < rows; row++)
|
|
3722
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3723
|
+
for (; col < cols; col++)
|
|
3724
|
+
out.push({ kind: "add", line: right[col] });
|
|
3725
|
+
return out;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/drift/packet.ts
|
|
3729
|
+
var PRESUMED_INVALID = [
|
|
3730
|
+
"fact",
|
|
3731
|
+
"constraint",
|
|
3732
|
+
"contract"
|
|
3733
|
+
];
|
|
3734
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
3735
|
+
var DEFAULT_NOTES = {
|
|
3736
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
3737
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
3738
|
+
review: "re-read the record against the new code"
|
|
3739
|
+
};
|
|
3740
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
3741
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
3742
|
+
...options.reader ? { reader: options.reader } : {},
|
|
3743
|
+
...options.search ? { search: options.search } : {},
|
|
3744
|
+
withHistory: options.withDiff !== false
|
|
3745
|
+
});
|
|
3746
|
+
const open = classified.filter(
|
|
3747
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
3748
|
+
);
|
|
3749
|
+
if (!open.length) return { packet: null, classified };
|
|
3750
|
+
const budget = diffBudget(open.length);
|
|
3751
|
+
const anchors = open.map(
|
|
3752
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3753
|
+
);
|
|
3754
|
+
const type = record.frontmatter.type;
|
|
3755
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
3756
|
+
return {
|
|
3757
|
+
classified,
|
|
3758
|
+
packet: {
|
|
3759
|
+
conceptId: record.conceptId,
|
|
3760
|
+
title: record.frontmatter.title ?? null,
|
|
3761
|
+
type,
|
|
3762
|
+
standing: options.standing ?? "unsettled",
|
|
3763
|
+
why: record.frontmatter.description ?? null,
|
|
3764
|
+
claim: claimOf(record),
|
|
3765
|
+
anchors,
|
|
3766
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
3767
|
+
conceptId: entry.conceptId,
|
|
3768
|
+
title: entry.title,
|
|
3769
|
+
standing: entry.standing,
|
|
3770
|
+
depth: entry.depth
|
|
3771
|
+
})),
|
|
3772
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
3773
|
+
default: fallback,
|
|
3774
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
3775
|
+
}
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
3779
|
+
const { entry } = found;
|
|
3780
|
+
const base2 = {
|
|
3781
|
+
file: entry.file,
|
|
3782
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
3783
|
+
class: found.class,
|
|
3784
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
3785
|
+
storedHash: entry.storedHash,
|
|
3786
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
3787
|
+
diffSize: entry.diffSize,
|
|
3788
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3789
|
+
};
|
|
3790
|
+
if (!withDiff) return base2;
|
|
3791
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3792
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
3793
|
+
}
|
|
3794
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
3795
|
+
maxLines
|
|
3796
|
+
});
|
|
3797
|
+
return {
|
|
3798
|
+
...base2,
|
|
3799
|
+
diff: {
|
|
3800
|
+
status: "ok",
|
|
3801
|
+
source: found.oldOrigin.kind,
|
|
3802
|
+
ref: found.oldOrigin.ref,
|
|
3803
|
+
unified: rendered.text,
|
|
3804
|
+
added: rendered.added,
|
|
3805
|
+
removed: rendered.removed,
|
|
3806
|
+
truncated: rendered.truncated
|
|
3807
|
+
}
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
function claimOf(record) {
|
|
3811
|
+
const type = record.frontmatter.type;
|
|
3812
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
3813
|
+
if (!section) return null;
|
|
3814
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
3815
|
+
const start = lines.findIndex(
|
|
3816
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
3817
|
+
);
|
|
3818
|
+
if (start < 0) return null;
|
|
3819
|
+
const rest = lines.slice(start + 1);
|
|
3820
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3821
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3822
|
+
return text ? { section, text } : null;
|
|
3823
|
+
}
|
|
3240
3824
|
|
|
3241
3825
|
// src/kb-edges.ts
|
|
3242
3826
|
var KB_EDGE_KINDS = [
|
|
@@ -3486,18 +4070,18 @@ function expired(hits, now) {
|
|
|
3486
4070
|
for (const hit of hits) {
|
|
3487
4071
|
const raw = hit.record.frontmatter.stale_after;
|
|
3488
4072
|
if (!raw) continue;
|
|
3489
|
-
const
|
|
3490
|
-
if (Number.isNaN(
|
|
4073
|
+
const at2 = Date.parse(raw);
|
|
4074
|
+
if (Number.isNaN(at2)) {
|
|
3491
4075
|
findings.push(
|
|
3492
4076
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
3493
4077
|
);
|
|
3494
4078
|
continue;
|
|
3495
4079
|
}
|
|
3496
|
-
if (
|
|
4080
|
+
if (at2 < now.getTime()) {
|
|
3497
4081
|
findings.push(
|
|
3498
4082
|
finding(
|
|
3499
4083
|
hit.record,
|
|
3500
|
-
`stale since ${raw} (${daysBetween(
|
|
4084
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
3501
4085
|
)
|
|
3502
4086
|
);
|
|
3503
4087
|
}
|
|
@@ -3510,12 +4094,12 @@ function expiring(hits, now, withinDays) {
|
|
|
3510
4094
|
for (const hit of hits) {
|
|
3511
4095
|
const raw = hit.record.frontmatter.stale_after;
|
|
3512
4096
|
if (!raw) continue;
|
|
3513
|
-
const
|
|
3514
|
-
if (Number.isNaN(
|
|
4097
|
+
const at2 = Date.parse(raw);
|
|
4098
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
3515
4099
|
findings.push(
|
|
3516
4100
|
finding(
|
|
3517
4101
|
hit.record,
|
|
3518
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
4102
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
3519
4103
|
)
|
|
3520
4104
|
);
|
|
3521
4105
|
}
|
|
@@ -3685,13 +4269,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
3685
4269
|
);
|
|
3686
4270
|
}
|
|
3687
4271
|
function describeAnchor(anchor) {
|
|
3688
|
-
const
|
|
3689
|
-
if (anchor.
|
|
4272
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
4273
|
+
if (anchor.class === "gone") {
|
|
4274
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
4275
|
+
}
|
|
4276
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
3690
4277
|
if (anchor.remoteState === "drifted-on-default") {
|
|
3691
|
-
return `${
|
|
4278
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
3692
4279
|
}
|
|
3693
|
-
if (anchor.diffSize === null) return `${
|
|
3694
|
-
return anchor.diffSize === 0 ? `${
|
|
4280
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
4281
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3695
4282
|
}
|
|
3696
4283
|
function replaces(later, earlier) {
|
|
3697
4284
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -3708,21 +4295,173 @@ function daysBetween(from, to) {
|
|
|
3708
4295
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
3709
4296
|
}
|
|
3710
4297
|
function ageInDays(record, now) {
|
|
3711
|
-
const
|
|
3712
|
-
if (!
|
|
3713
|
-
const written = Date.parse(
|
|
4298
|
+
const at2 = record.frontmatter.generated?.at;
|
|
4299
|
+
if (!at2) return null;
|
|
4300
|
+
const written = Date.parse(at2);
|
|
3714
4301
|
if (Number.isNaN(written)) return null;
|
|
3715
4302
|
return daysBetween(written, now.getTime());
|
|
3716
4303
|
}
|
|
3717
4304
|
|
|
4305
|
+
// src/commands/reassess.ts
|
|
4306
|
+
var import_zod12 = require("zod");
|
|
4307
|
+
var reassessCommand = define({
|
|
4308
|
+
name: "reassess",
|
|
4309
|
+
tool: "kb_reassess",
|
|
4310
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4311
|
+
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
4312
|
+
input: import_zod12.z.object({
|
|
4313
|
+
bundlePath,
|
|
4314
|
+
conceptId,
|
|
4315
|
+
repoRoot: REPO_ROOT,
|
|
4316
|
+
withDiff: import_zod12.z.boolean().optional().describe(
|
|
4317
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4318
|
+
)
|
|
4319
|
+
}),
|
|
4320
|
+
fromArgv: (argv, path) => {
|
|
4321
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4322
|
+
return {
|
|
4323
|
+
bundlePath: path,
|
|
4324
|
+
conceptId: argv[1],
|
|
4325
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4326
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4327
|
+
};
|
|
4328
|
+
},
|
|
4329
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
4330
|
+
const root = repoRoot ?? process.cwd();
|
|
4331
|
+
const bundle = await store.list(path);
|
|
4332
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
4333
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
4334
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
4335
|
+
const entries = drift?.get(id) ?? [];
|
|
4336
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
4337
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
4338
|
+
}
|
|
4339
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
4340
|
+
(hit) => hit.record.conceptId === id
|
|
4341
|
+
)?.standing;
|
|
4342
|
+
const impact2 = await store.impact(path, id);
|
|
4343
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
4344
|
+
...withDiff ? { withDiff: true } : {},
|
|
4345
|
+
impact: impact2,
|
|
4346
|
+
...standing ? { standing } : {}
|
|
4347
|
+
});
|
|
4348
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
4349
|
+
let frozen = false;
|
|
4350
|
+
const rebaselined = [];
|
|
4351
|
+
if (moves.length) {
|
|
4352
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
4353
|
+
for (const found of moves) {
|
|
4354
|
+
const to = found.entry.movedTo;
|
|
4355
|
+
if (!to) continue;
|
|
4356
|
+
relocated.set(found.anchor, {
|
|
4357
|
+
...found.anchor,
|
|
4358
|
+
file: to.file,
|
|
4359
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
4360
|
+
});
|
|
4361
|
+
rebaselined.push({
|
|
4362
|
+
file: found.anchor.file,
|
|
4363
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
4364
|
+
toFile: to.file,
|
|
4365
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
try {
|
|
4369
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
4370
|
+
} catch (error) {
|
|
4371
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
4372
|
+
frozen = true;
|
|
4373
|
+
}
|
|
4374
|
+
if (!frozen) {
|
|
4375
|
+
await store.updateAnchors(
|
|
4376
|
+
path,
|
|
4377
|
+
id,
|
|
4378
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
4379
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
4380
|
+
),
|
|
4381
|
+
actor
|
|
4382
|
+
);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
return {
|
|
4386
|
+
conceptId: id,
|
|
4387
|
+
packet,
|
|
4388
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
4389
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
4390
|
+
...frozen ? {
|
|
4391
|
+
frozen: true,
|
|
4392
|
+
note: "base is frozen: nothing was rebaselined"
|
|
4393
|
+
} : {}
|
|
4394
|
+
};
|
|
4395
|
+
},
|
|
4396
|
+
render: (result) => renderReassess(result)
|
|
4397
|
+
});
|
|
4398
|
+
function renderReassess(result) {
|
|
4399
|
+
const lines = [];
|
|
4400
|
+
for (const move of result.rebaselined) {
|
|
4401
|
+
lines.push(
|
|
4402
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
4403
|
+
);
|
|
4404
|
+
}
|
|
4405
|
+
if (result.cosmetic) {
|
|
4406
|
+
lines.push(
|
|
4407
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
if (result.note) lines.push(result.note);
|
|
4411
|
+
const packet = result.packet;
|
|
4412
|
+
if (!packet) {
|
|
4413
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
4414
|
+
return lines.join("\n");
|
|
4415
|
+
}
|
|
4416
|
+
lines.push(
|
|
4417
|
+
"",
|
|
4418
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
4419
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
4420
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
4421
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
4422
|
+
"",
|
|
4423
|
+
`## Anchors (${packet.anchors.length})`
|
|
4424
|
+
);
|
|
4425
|
+
for (const anchor of packet.anchors) {
|
|
4426
|
+
lines.push(
|
|
4427
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
4428
|
+
);
|
|
4429
|
+
if (!anchor.diff) continue;
|
|
4430
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
4431
|
+
lines.push(
|
|
4432
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
4433
|
+
);
|
|
4434
|
+
continue;
|
|
4435
|
+
}
|
|
4436
|
+
lines.push(
|
|
4437
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
4438
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
if (packet.impact.length) {
|
|
4442
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
4443
|
+
for (const entry of packet.impact) {
|
|
4444
|
+
lines.push(
|
|
4445
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
4449
|
+
}
|
|
4450
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
4451
|
+
return lines.join("\n");
|
|
4452
|
+
}
|
|
4453
|
+
function at(file, symbol) {
|
|
4454
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
3718
4457
|
// src/commands/doctor.ts
|
|
3719
|
-
var days = (what, fallback) =>
|
|
4458
|
+
var days = (what, fallback) => import_zod13.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3720
4459
|
var doctorCommand = define({
|
|
3721
4460
|
name: "doctor",
|
|
3722
4461
|
tool: "kb_doctor",
|
|
3723
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3724
|
-
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped.
|
|
3725
|
-
input:
|
|
4462
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4463
|
+
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
4464
|
+
input: import_zod13.z.object({
|
|
3726
4465
|
bundlePath,
|
|
3727
4466
|
repoRoot: REPO_ROOT,
|
|
3728
4467
|
expiringDays: days(
|
|
@@ -3737,11 +4476,17 @@ var doctorCommand = define({
|
|
|
3737
4476
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3738
4477
|
DEFAULT_AGING_DAYS
|
|
3739
4478
|
),
|
|
3740
|
-
offline:
|
|
4479
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
3741
4480
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3742
4481
|
),
|
|
3743
|
-
strict:
|
|
4482
|
+
strict: import_zod13.z.boolean().optional().describe(
|
|
3744
4483
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4484
|
+
),
|
|
4485
|
+
drifted: import_zod13.z.boolean().optional().describe(
|
|
4486
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4487
|
+
),
|
|
4488
|
+
withDiff: import_zod13.z.boolean().optional().describe(
|
|
4489
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
3745
4490
|
)
|
|
3746
4491
|
}),
|
|
3747
4492
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -3760,7 +4505,9 @@ var doctorCommand = define({
|
|
|
3760
4505
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3761
4506
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3762
4507
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
3763
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
4508
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
4509
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
4510
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
3764
4511
|
};
|
|
3765
4512
|
},
|
|
3766
4513
|
run: async ({ store, now }, {
|
|
@@ -3769,7 +4516,9 @@ var doctorCommand = define({
|
|
|
3769
4516
|
unverifiedDays,
|
|
3770
4517
|
agingDays,
|
|
3771
4518
|
repoRoot,
|
|
3772
|
-
offline
|
|
4519
|
+
offline,
|
|
4520
|
+
drifted: drifted2,
|
|
4521
|
+
withDiff
|
|
3773
4522
|
}) => {
|
|
3774
4523
|
const checkedAt = now();
|
|
3775
4524
|
const records = await store.list(path);
|
|
@@ -3784,10 +4533,51 @@ var doctorCommand = define({
|
|
|
3784
4533
|
now: new Date(checkedAt)
|
|
3785
4534
|
});
|
|
3786
4535
|
const hints = grammarHints();
|
|
4536
|
+
if (!drifted2) {
|
|
4537
|
+
return {
|
|
4538
|
+
bundlePath: path,
|
|
4539
|
+
checkedAt,
|
|
4540
|
+
...report,
|
|
4541
|
+
...hints.length ? { hints } : {}
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4544
|
+
const standings = new Map(
|
|
4545
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4546
|
+
hit.record.conceptId,
|
|
4547
|
+
hit.standing
|
|
4548
|
+
])
|
|
4549
|
+
);
|
|
4550
|
+
const packets = [];
|
|
4551
|
+
const rebaselinable = [];
|
|
4552
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
4553
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
4554
|
+
const record = records.find(
|
|
4555
|
+
(entry) => entry.conceptId === found.conceptId
|
|
4556
|
+
);
|
|
4557
|
+
if (!record) continue;
|
|
4558
|
+
const standing = standings.get(record.conceptId);
|
|
4559
|
+
const built = await reassessPacket(
|
|
4560
|
+
repoRoot ?? process.cwd(),
|
|
4561
|
+
record,
|
|
4562
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
4563
|
+
{
|
|
4564
|
+
...withDiff ? { withDiff: true } : {},
|
|
4565
|
+
impact: await store.impact(path, record.conceptId),
|
|
4566
|
+
...standing ? { standing } : {},
|
|
4567
|
+
search
|
|
4568
|
+
}
|
|
4569
|
+
);
|
|
4570
|
+
if (built.packet) packets.push(built.packet);
|
|
4571
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
4572
|
+
rebaselinable.push(record.conceptId);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
3787
4575
|
return {
|
|
3788
4576
|
bundlePath: path,
|
|
3789
4577
|
checkedAt,
|
|
3790
4578
|
...report,
|
|
4579
|
+
packets,
|
|
4580
|
+
rebaselinable,
|
|
3791
4581
|
...hints.length ? { hints } : {}
|
|
3792
4582
|
};
|
|
3793
4583
|
},
|
|
@@ -3801,6 +4591,7 @@ var doctorCommand = define({
|
|
|
3801
4591
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
3802
4592
|
});
|
|
3803
4593
|
function render2(result) {
|
|
4594
|
+
if (result.packets) return renderPackets(result);
|
|
3804
4595
|
const { thresholds } = result;
|
|
3805
4596
|
const lines = [
|
|
3806
4597
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -3834,21 +4625,45 @@ function render2(result) {
|
|
|
3834
4625
|
);
|
|
3835
4626
|
return lines.join("\n");
|
|
3836
4627
|
}
|
|
4628
|
+
function renderPackets(result) {
|
|
4629
|
+
const packets = result.packets ?? [];
|
|
4630
|
+
const lines = [
|
|
4631
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
4632
|
+
`checked: ${result.checkedAt}`,
|
|
4633
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
4634
|
+
];
|
|
4635
|
+
if (result.rebaselinable?.length) {
|
|
4636
|
+
lines.push(
|
|
4637
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
for (const packet of packets) {
|
|
4641
|
+
lines.push(
|
|
4642
|
+
renderReassess({
|
|
4643
|
+
conceptId: packet.conceptId,
|
|
4644
|
+
packet,
|
|
4645
|
+
rebaselined: [],
|
|
4646
|
+
cosmetic: 0
|
|
4647
|
+
})
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4650
|
+
return lines.join("\n");
|
|
4651
|
+
}
|
|
3837
4652
|
|
|
3838
4653
|
// src/commands/impact.ts
|
|
3839
|
-
var
|
|
4654
|
+
var import_zod14 = require("zod");
|
|
3840
4655
|
var impactCommand = define({
|
|
3841
4656
|
name: "impact",
|
|
3842
4657
|
tool: "kb_impact",
|
|
3843
4658
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3844
4659
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
3845
|
-
input:
|
|
4660
|
+
input: import_zod14.z.object({
|
|
3846
4661
|
bundlePath,
|
|
3847
4662
|
conceptId,
|
|
3848
|
-
depth:
|
|
4663
|
+
depth: import_zod14.z.number().int().positive().optional().describe(
|
|
3849
4664
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3850
4665
|
),
|
|
3851
|
-
rels:
|
|
4666
|
+
rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3852
4667
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3853
4668
|
)
|
|
3854
4669
|
}),
|
|
@@ -3869,35 +4684,49 @@ var impactCommand = define({
|
|
|
3869
4684
|
});
|
|
3870
4685
|
|
|
3871
4686
|
// src/commands/list.ts
|
|
3872
|
-
var
|
|
4687
|
+
var import_zod15 = require("zod");
|
|
3873
4688
|
var listCommand = define({
|
|
3874
4689
|
name: "list",
|
|
3875
4690
|
tool: "kb_list",
|
|
3876
|
-
usage: "list [type]",
|
|
3877
|
-
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3878
|
-
input:
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
4691
|
+
usage: "list [type] [--tag T]...",
|
|
4692
|
+
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
4693
|
+
input: import_zod15.z.object({
|
|
4694
|
+
bundlePath,
|
|
4695
|
+
type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
|
|
4696
|
+
tags: TAGS
|
|
4697
|
+
}),
|
|
4698
|
+
fromArgv: (argv, path) => {
|
|
4699
|
+
const tags = argvFlags(argv, "--tag");
|
|
4700
|
+
const type = argvPositional(argv, "--tag");
|
|
4701
|
+
return {
|
|
4702
|
+
bundlePath: path,
|
|
4703
|
+
...type ? { type } : {},
|
|
4704
|
+
...tags.length ? { tags } : {}
|
|
4705
|
+
};
|
|
4706
|
+
},
|
|
4707
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
|
|
4708
|
+
(record) => ({
|
|
4709
|
+
conceptId: record.conceptId,
|
|
4710
|
+
title: record.frontmatter.title ?? null,
|
|
4711
|
+
description: record.frontmatter.description ?? null,
|
|
4712
|
+
status: record.frontmatter.strauss_status,
|
|
4713
|
+
anchors: record.frontmatter.strauss_anchors ?? []
|
|
4714
|
+
})
|
|
4715
|
+
)
|
|
3887
4716
|
});
|
|
3888
4717
|
|
|
3889
4718
|
// src/commands/load.ts
|
|
3890
|
-
var
|
|
4719
|
+
var import_zod16 = require("zod");
|
|
3891
4720
|
var loadCommand = define({
|
|
3892
4721
|
name: "load",
|
|
3893
4722
|
tool: "kb_load",
|
|
3894
4723
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3895
4724
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
3896
|
-
input:
|
|
4725
|
+
input: import_zod16.z.object({
|
|
3897
4726
|
bundlePath,
|
|
3898
|
-
type:
|
|
3899
|
-
budgetTokens:
|
|
3900
|
-
all:
|
|
4727
|
+
type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
|
|
4728
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4729
|
+
all: import_zod16.z.boolean().optional().describe(
|
|
3901
4730
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3902
4731
|
),
|
|
3903
4732
|
repoRoot: REPO_ROOT
|
|
@@ -3939,25 +4768,25 @@ var loadCommand = define({
|
|
|
3939
4768
|
});
|
|
3940
4769
|
|
|
3941
4770
|
// src/commands/log.ts
|
|
3942
|
-
var
|
|
4771
|
+
var import_zod17 = require("zod");
|
|
3943
4772
|
var logCommand = define({
|
|
3944
4773
|
name: "log",
|
|
3945
4774
|
tool: "kb_log",
|
|
3946
4775
|
usage: "log",
|
|
3947
4776
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3948
|
-
input:
|
|
4777
|
+
input: import_zod17.z.object({ bundlePath }),
|
|
3949
4778
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3950
4779
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3951
4780
|
});
|
|
3952
4781
|
|
|
3953
4782
|
// src/commands/no-decision.ts
|
|
3954
|
-
var
|
|
4783
|
+
var import_zod18 = require("zod");
|
|
3955
4784
|
var noDecisionCommand = define({
|
|
3956
4785
|
name: "no-decision",
|
|
3957
4786
|
tool: "kb_no_decision",
|
|
3958
4787
|
usage: "no-decision <reason...>",
|
|
3959
4788
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3960
|
-
input:
|
|
4789
|
+
input: import_zod18.z.object({ bundlePath, reason: import_zod18.z.string().min(1) }),
|
|
3961
4790
|
fromArgv: (argv, path) => ({
|
|
3962
4791
|
bundlePath: path,
|
|
3963
4792
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3974,20 +4803,20 @@ var noDecisionCommand = define({
|
|
|
3974
4803
|
});
|
|
3975
4804
|
|
|
3976
4805
|
// src/commands/pack.ts
|
|
3977
|
-
var
|
|
4806
|
+
var import_zod19 = require("zod");
|
|
3978
4807
|
var packCommand = define({
|
|
3979
4808
|
name: "pack",
|
|
3980
4809
|
tool: "kb_pack",
|
|
3981
4810
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3982
4811
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
3983
|
-
input:
|
|
4812
|
+
input: import_zod19.z.object({
|
|
3984
4813
|
bundlePath,
|
|
3985
4814
|
conceptId,
|
|
3986
|
-
hops:
|
|
3987
|
-
maxNodes:
|
|
4815
|
+
hops: import_zod19.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4816
|
+
maxNodes: import_zod19.z.number().int().positive().optional().describe(
|
|
3988
4817
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3989
4818
|
),
|
|
3990
|
-
budgetTokens:
|
|
4819
|
+
budgetTokens: import_zod19.z.number().int().positive().optional().describe(
|
|
3991
4820
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3992
4821
|
)
|
|
3993
4822
|
}),
|
|
@@ -4012,12 +4841,12 @@ var packCommand = define({
|
|
|
4012
4841
|
return render3(result, path, now());
|
|
4013
4842
|
}
|
|
4014
4843
|
});
|
|
4015
|
-
function render3(result, bundle,
|
|
4844
|
+
function render3(result, bundle, at2) {
|
|
4016
4845
|
const lines = [
|
|
4017
4846
|
`# KB Pack \u2014 ${result.root}`,
|
|
4018
4847
|
`bundle: ${bundle}`,
|
|
4019
4848
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
4020
|
-
`packed: ${
|
|
4849
|
+
`packed: ${at2}`,
|
|
4021
4850
|
"",
|
|
4022
4851
|
`## Records (${result.records.length})`
|
|
4023
4852
|
];
|
|
@@ -4074,22 +4903,22 @@ function warningLabel(warning) {
|
|
|
4074
4903
|
}
|
|
4075
4904
|
|
|
4076
4905
|
// src/commands/pin.ts
|
|
4077
|
-
var
|
|
4906
|
+
var import_zod20 = require("zod");
|
|
4078
4907
|
var pinCommand = define({
|
|
4079
4908
|
name: "pin",
|
|
4080
4909
|
tool: "kb_pin",
|
|
4081
4910
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4082
4911
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
4083
|
-
input:
|
|
4912
|
+
input: import_zod20.z.object({
|
|
4084
4913
|
bundlePath,
|
|
4085
|
-
mode:
|
|
4914
|
+
mode: import_zod20.z.enum(["full", "index"]).optional().describe(
|
|
4086
4915
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
4087
4916
|
),
|
|
4088
|
-
profiles:
|
|
4089
|
-
layer:
|
|
4917
|
+
profiles: import_zod20.z.array(import_zod20.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4918
|
+
layer: import_zod20.z.enum(["project", "local", "user"]).optional().describe(
|
|
4090
4919
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4091
4920
|
),
|
|
4092
|
-
frozen:
|
|
4921
|
+
frozen: import_zod20.z.boolean().optional().describe(
|
|
4093
4922
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4094
4923
|
)
|
|
4095
4924
|
}),
|
|
@@ -4118,47 +4947,49 @@ var pinCommand = define({
|
|
|
4118
4947
|
});
|
|
4119
4948
|
|
|
4120
4949
|
// src/commands/pins.ts
|
|
4121
|
-
var
|
|
4950
|
+
var import_zod21 = require("zod");
|
|
4122
4951
|
var pinsCommand = define({
|
|
4123
4952
|
name: "pins",
|
|
4124
4953
|
tool: "kb_pins",
|
|
4125
4954
|
usage: "pins",
|
|
4126
4955
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4127
|
-
input:
|
|
4956
|
+
input: import_zod21.z.object({}),
|
|
4128
4957
|
fromArgv: () => ({}),
|
|
4129
4958
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4130
4959
|
});
|
|
4131
4960
|
|
|
4132
4961
|
// src/commands/query.ts
|
|
4133
|
-
var
|
|
4962
|
+
var import_zod22 = require("zod");
|
|
4134
4963
|
var queryCommand = define({
|
|
4135
4964
|
name: "query",
|
|
4136
4965
|
tool: "kb_query",
|
|
4137
|
-
usage: "query <text...> [--repo-root PATH]",
|
|
4966
|
+
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
4138
4967
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
4139
|
-
input:
|
|
4968
|
+
input: import_zod22.z.object({
|
|
4140
4969
|
bundlePath,
|
|
4141
|
-
text:
|
|
4142
|
-
type:
|
|
4143
|
-
includeNonCurrent:
|
|
4970
|
+
text: import_zod22.z.string().optional(),
|
|
4971
|
+
type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
|
|
4972
|
+
includeNonCurrent: import_zod22.z.boolean().optional(),
|
|
4973
|
+
tags: TAGS,
|
|
4144
4974
|
repoRoot: REPO_ROOT
|
|
4145
4975
|
}),
|
|
4146
|
-
//
|
|
4976
|
+
// Both are flags, so neither's value may fall into the search text.
|
|
4147
4977
|
fromArgv: (argv, path) => {
|
|
4148
4978
|
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4149
|
-
const
|
|
4150
|
-
const
|
|
4151
|
-
if (flag !== -1) words.splice(flag, 2);
|
|
4979
|
+
const tags = argvFlags(argv, "--tag");
|
|
4980
|
+
const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
|
|
4152
4981
|
return {
|
|
4153
4982
|
bundlePath: path,
|
|
4154
4983
|
text: words.join(" ").trim(),
|
|
4155
4984
|
includeNonCurrent: true,
|
|
4985
|
+
...tags.length ? { tags } : {},
|
|
4156
4986
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4157
4987
|
};
|
|
4158
4988
|
},
|
|
4159
|
-
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
4989
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
|
|
4160
4990
|
...type ? { type } : {},
|
|
4161
4991
|
includeNonCurrent: includeNonCurrent === true,
|
|
4992
|
+
...tags ? { tags } : {},
|
|
4162
4993
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4163
4994
|
})).map((hit) => ({
|
|
4164
4995
|
conceptId: hit.record.conceptId,
|
|
@@ -4172,27 +5003,27 @@ var queryCommand = define({
|
|
|
4172
5003
|
});
|
|
4173
5004
|
|
|
4174
5005
|
// src/commands/read-index.ts
|
|
4175
|
-
var
|
|
5006
|
+
var import_zod23 = require("zod");
|
|
4176
5007
|
var readIndexCommand = define({
|
|
4177
5008
|
name: "index",
|
|
4178
5009
|
tool: "kb_index",
|
|
4179
5010
|
usage: "index",
|
|
4180
5011
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
4181
|
-
input:
|
|
5012
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
4182
5013
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4183
5014
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4184
5015
|
});
|
|
4185
5016
|
|
|
4186
5017
|
// src/commands/schema.ts
|
|
4187
|
-
var
|
|
5018
|
+
var import_zod26 = require("zod");
|
|
4188
5019
|
|
|
4189
5020
|
// src/json-schema.ts
|
|
4190
|
-
var
|
|
5021
|
+
var import_zod25 = require("zod");
|
|
4191
5022
|
|
|
4192
5023
|
// src/kb-log.ts
|
|
4193
|
-
var
|
|
5024
|
+
var import_zod24 = require("zod");
|
|
4194
5025
|
var LOG_FILE = "log.jsonl";
|
|
4195
|
-
var kbLogEntrySchema =
|
|
5026
|
+
var kbLogEntrySchema = import_zod24.z.object({
|
|
4196
5027
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
4197
5028
|
// below), and a value that isn't actually chronological — a Unix
|
|
4198
5029
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -4201,12 +5032,12 @@ var kbLogEntrySchema = import_zod23.z.object({
|
|
|
4201
5032
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
4202
5033
|
// malformed `at` is reported the same way a malformed line already is,
|
|
4203
5034
|
// rather than silently sorting into the wrong place.
|
|
4204
|
-
at:
|
|
4205
|
-
by:
|
|
4206
|
-
operation:
|
|
4207
|
-
conceptId:
|
|
5035
|
+
at: import_zod24.z.iso.datetime(),
|
|
5036
|
+
by: import_zod24.z.string().min(1),
|
|
5037
|
+
operation: import_zod24.z.string().min(1),
|
|
5038
|
+
conceptId: import_zod24.z.string().min(1),
|
|
4208
5039
|
/** Second concept id, where the operation relates two — supersession. */
|
|
4209
|
-
target:
|
|
5040
|
+
target: import_zod24.z.string().min(1).optional()
|
|
4210
5041
|
}).strict();
|
|
4211
5042
|
function renderLogEntry(entry) {
|
|
4212
5043
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -4244,11 +5075,11 @@ function parseLog(raw) {
|
|
|
4244
5075
|
// src/json-schema.ts
|
|
4245
5076
|
function kbJsonSchemas() {
|
|
4246
5077
|
return {
|
|
4247
|
-
recordFrontmatter:
|
|
5078
|
+
recordFrontmatter: import_zod25.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
4248
5079
|
io: "input"
|
|
4249
5080
|
}),
|
|
4250
|
-
composeInput:
|
|
4251
|
-
logEntry:
|
|
5081
|
+
composeInput: import_zod25.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
5082
|
+
logEntry: import_zod25.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
4252
5083
|
};
|
|
4253
5084
|
}
|
|
4254
5085
|
|
|
@@ -4258,25 +5089,25 @@ var schemaCommand = define({
|
|
|
4258
5089
|
tool: "kb_schema",
|
|
4259
5090
|
usage: "schema",
|
|
4260
5091
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4261
|
-
input:
|
|
5092
|
+
input: import_zod26.z.object({}),
|
|
4262
5093
|
fromArgv: () => ({}),
|
|
4263
5094
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4264
5095
|
});
|
|
4265
5096
|
|
|
4266
5097
|
// src/commands/stamp.ts
|
|
4267
|
-
var
|
|
4268
|
-
var
|
|
5098
|
+
var import_promises8 = require("fs/promises");
|
|
5099
|
+
var import_zod27 = require("zod");
|
|
4269
5100
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4270
5101
|
var stampCommand = define({
|
|
4271
5102
|
name: "stamp",
|
|
4272
5103
|
tool: "kb_stamp",
|
|
4273
5104
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
4274
|
-
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids
|
|
4275
|
-
input:
|
|
4276
|
-
bundlePath:
|
|
5105
|
+
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
5106
|
+
input: import_zod27.z.object({
|
|
5107
|
+
bundlePath: import_zod27.z.string().min(1).optional().describe(
|
|
4277
5108
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
4278
5109
|
),
|
|
4279
|
-
since:
|
|
5110
|
+
since: import_zod27.z.string().min(1).optional().describe(
|
|
4280
5111
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
4281
5112
|
)
|
|
4282
5113
|
}),
|
|
@@ -4315,7 +5146,7 @@ var stampCommand = define({
|
|
|
4315
5146
|
return reports;
|
|
4316
5147
|
},
|
|
4317
5148
|
render: (result) => result.map((report) => {
|
|
4318
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5149
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
4319
5150
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
4320
5151
|
return report.changed?.length ? `${head}
|
|
4321
5152
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -4338,7 +5169,7 @@ async function readBaseline(since) {
|
|
|
4338
5169
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
4339
5170
|
let parsed;
|
|
4340
5171
|
try {
|
|
4341
|
-
parsed = JSON.parse(await (0,
|
|
5172
|
+
parsed = JSON.parse(await (0, import_promises8.readFile)(since, "utf8"));
|
|
4342
5173
|
} catch {
|
|
4343
5174
|
throw new KbStampBaselineError(since);
|
|
4344
5175
|
}
|
|
@@ -4362,16 +5193,16 @@ async function readBaseline(since) {
|
|
|
4362
5193
|
}
|
|
4363
5194
|
|
|
4364
5195
|
// src/commands/status.ts
|
|
4365
|
-
var
|
|
5196
|
+
var import_zod28 = require("zod");
|
|
4366
5197
|
var statusCommand = define({
|
|
4367
5198
|
name: "status",
|
|
4368
5199
|
tool: "kb_status",
|
|
4369
5200
|
usage: "status <concept-id> <status>",
|
|
4370
5201
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
4371
|
-
input:
|
|
5202
|
+
input: import_zod28.z.object({
|
|
4372
5203
|
bundlePath,
|
|
4373
5204
|
conceptId,
|
|
4374
|
-
status:
|
|
5205
|
+
status: import_zod28.z.enum(KB_RECORD_STATUSES)
|
|
4375
5206
|
}),
|
|
4376
5207
|
fromArgv: (argv, path) => ({
|
|
4377
5208
|
bundlePath: path,
|
|
@@ -4386,13 +5217,13 @@ var statusCommand = define({
|
|
|
4386
5217
|
});
|
|
4387
5218
|
|
|
4388
5219
|
// src/commands/supersede.ts
|
|
4389
|
-
var
|
|
5220
|
+
var import_zod29 = require("zod");
|
|
4390
5221
|
var supersedeCommand = define({
|
|
4391
5222
|
name: "supersede",
|
|
4392
5223
|
tool: "kb_supersede",
|
|
4393
5224
|
usage: "supersede <concept-id> <replacement-id>",
|
|
4394
5225
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
4395
|
-
input:
|
|
5226
|
+
input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
4396
5227
|
fromArgv: (argv, path) => ({
|
|
4397
5228
|
bundlePath: path,
|
|
4398
5229
|
conceptId: argv[1],
|
|
@@ -4406,16 +5237,16 @@ var supersedeCommand = define({
|
|
|
4406
5237
|
});
|
|
4407
5238
|
|
|
4408
5239
|
// src/commands/sync-instructions.ts
|
|
4409
|
-
var
|
|
5240
|
+
var import_zod30 = require("zod");
|
|
4410
5241
|
var syncInstructionsCommand = define({
|
|
4411
5242
|
name: "sync-instructions",
|
|
4412
5243
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
4413
5244
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
4414
|
-
input:
|
|
4415
|
-
file:
|
|
4416
|
-
budgetTokens:
|
|
4417
|
-
fullUnderTokens:
|
|
4418
|
-
profile:
|
|
5245
|
+
input: import_zod30.z.object({
|
|
5246
|
+
file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
|
|
5247
|
+
budgetTokens: import_zod30.z.number().int().positive().optional(),
|
|
5248
|
+
fullUnderTokens: import_zod30.z.number().int().positive().optional(),
|
|
5249
|
+
profile: import_zod30.z.string().optional()
|
|
4419
5250
|
}),
|
|
4420
5251
|
fromArgv: (argv) => {
|
|
4421
5252
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -4441,7 +5272,7 @@ var syncInstructionsCommand = define({
|
|
|
4441
5272
|
});
|
|
4442
5273
|
|
|
4443
5274
|
// src/commands/trace.ts
|
|
4444
|
-
var
|
|
5275
|
+
var import_zod31 = require("zod");
|
|
4445
5276
|
|
|
4446
5277
|
// src/trace.ts
|
|
4447
5278
|
var TRACE_EDGES = [
|
|
@@ -4487,8 +5318,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
4487
5318
|
return [...reached.values()].sort(byGeneratedAt);
|
|
4488
5319
|
}
|
|
4489
5320
|
function byGeneratedAt(left, right) {
|
|
4490
|
-
const
|
|
4491
|
-
return
|
|
5321
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
5322
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
4492
5323
|
}
|
|
4493
5324
|
|
|
4494
5325
|
// src/commands/trace.ts
|
|
@@ -4497,11 +5328,11 @@ var traceCommand = define({
|
|
|
4497
5328
|
tool: "kb_trace",
|
|
4498
5329
|
usage: "trace <concept-id> [edges...]",
|
|
4499
5330
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
4500
|
-
input:
|
|
5331
|
+
input: import_zod31.z.object({
|
|
4501
5332
|
bundlePath,
|
|
4502
5333
|
conceptId,
|
|
4503
|
-
edges:
|
|
4504
|
-
depth:
|
|
5334
|
+
edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
|
|
5335
|
+
depth: import_zod31.z.number().int().positive().optional()
|
|
4505
5336
|
}),
|
|
4506
5337
|
fromArgv: (argv, path) => ({
|
|
4507
5338
|
bundlePath: path,
|
|
@@ -4523,37 +5354,37 @@ var traceCommand = define({
|
|
|
4523
5354
|
});
|
|
4524
5355
|
|
|
4525
5356
|
// src/commands/types.ts
|
|
4526
|
-
var
|
|
5357
|
+
var import_zod32 = require("zod");
|
|
4527
5358
|
var typesCommand = define({
|
|
4528
5359
|
name: "types",
|
|
4529
5360
|
tool: "kb_types",
|
|
4530
5361
|
usage: "types",
|
|
4531
5362
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
4532
|
-
input:
|
|
5363
|
+
input: import_zod32.z.object({}),
|
|
4533
5364
|
fromArgv: () => ({}),
|
|
4534
5365
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
4535
5366
|
});
|
|
4536
5367
|
|
|
4537
5368
|
// src/commands/unpin.ts
|
|
4538
|
-
var
|
|
5369
|
+
var import_zod33 = require("zod");
|
|
4539
5370
|
var unpinCommand = define({
|
|
4540
5371
|
name: "unpin",
|
|
4541
5372
|
tool: "kb_unpin",
|
|
4542
5373
|
usage: "unpin [bundle-path]",
|
|
4543
5374
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
4544
|
-
input:
|
|
5375
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
4545
5376
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
4546
5377
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
4547
5378
|
});
|
|
4548
5379
|
|
|
4549
5380
|
// src/commands/validate.ts
|
|
4550
|
-
var
|
|
5381
|
+
var import_zod34 = require("zod");
|
|
4551
5382
|
var validateCommand = define({
|
|
4552
5383
|
name: "validate",
|
|
4553
5384
|
tool: "kb_validate",
|
|
4554
5385
|
usage: "validate",
|
|
4555
5386
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
4556
|
-
input:
|
|
5387
|
+
input: import_zod34.z.object({ bundlePath }),
|
|
4557
5388
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4558
5389
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
4559
5390
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -4563,16 +5394,16 @@ var validateCommand = define({
|
|
|
4563
5394
|
});
|
|
4564
5395
|
|
|
4565
5396
|
// src/commands/verify.ts
|
|
4566
|
-
var
|
|
5397
|
+
var import_zod35 = require("zod");
|
|
4567
5398
|
var verifyCommand = define({
|
|
4568
5399
|
name: "verify",
|
|
4569
5400
|
tool: "kb_verify",
|
|
4570
5401
|
usage: "verify <concept-id> --note <text>",
|
|
4571
5402
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
4572
|
-
input:
|
|
5403
|
+
input: import_zod35.z.object({
|
|
4573
5404
|
bundlePath,
|
|
4574
5405
|
conceptId,
|
|
4575
|
-
note:
|
|
5406
|
+
note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
|
|
4576
5407
|
message: "note must say what the check found"
|
|
4577
5408
|
})
|
|
4578
5409
|
}),
|
|
@@ -4592,15 +5423,15 @@ var verifyCommand = define({
|
|
|
4592
5423
|
});
|
|
4593
5424
|
|
|
4594
5425
|
// src/commands/write.ts
|
|
4595
|
-
var
|
|
5426
|
+
var import_zod36 = require("zod");
|
|
4596
5427
|
var writeCommand = define({
|
|
4597
5428
|
name: "write",
|
|
4598
5429
|
tool: "kb_write",
|
|
4599
5430
|
usage: "write <type> < record.json",
|
|
4600
5431
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
4601
|
-
input:
|
|
5432
|
+
input: import_zod36.z.object({
|
|
4602
5433
|
bundlePath,
|
|
4603
|
-
type:
|
|
5434
|
+
type: import_zod36.z.enum(KB_RECORD_TYPES),
|
|
4604
5435
|
input: composeInputSchema
|
|
4605
5436
|
}),
|
|
4606
5437
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -4624,13 +5455,13 @@ var writeCommand = define({
|
|
|
4624
5455
|
});
|
|
4625
5456
|
|
|
4626
5457
|
// src/commands/write-decision.ts
|
|
4627
|
-
var
|
|
5458
|
+
var import_zod37 = require("zod");
|
|
4628
5459
|
var writeDecisionCommand = define({
|
|
4629
5460
|
name: "write-decision",
|
|
4630
5461
|
tool: "kb_write_decision",
|
|
4631
5462
|
usage: "write-decision < decision.json",
|
|
4632
5463
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
4633
|
-
input:
|
|
5464
|
+
input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
|
|
4634
5465
|
fromArgv: async (_argv, path, stdin) => ({
|
|
4635
5466
|
bundlePath: path,
|
|
4636
5467
|
input: JSON.parse(await stdin())
|
|
@@ -4660,6 +5491,7 @@ var KB_COMMANDS = [
|
|
|
4660
5491
|
answerCommand,
|
|
4661
5492
|
verifyCommand,
|
|
4662
5493
|
anchorResolveCommand,
|
|
5494
|
+
reassessCommand,
|
|
4663
5495
|
loadCommand,
|
|
4664
5496
|
catalogCommand,
|
|
4665
5497
|
packCommand,
|
|
@@ -4686,7 +5518,7 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
4686
5518
|
);
|
|
4687
5519
|
|
|
4688
5520
|
// src/kb-store.ts
|
|
4689
|
-
var
|
|
5521
|
+
var import_promises10 = require("fs/promises");
|
|
4690
5522
|
var import_node_path11 = require("path");
|
|
4691
5523
|
|
|
4692
5524
|
// src/markdown.ts
|
|
@@ -4747,7 +5579,7 @@ function bundleDigest(records, superseded) {
|
|
|
4747
5579
|
}
|
|
4748
5580
|
|
|
4749
5581
|
// src/search-index.ts
|
|
4750
|
-
var
|
|
5582
|
+
var import_promises9 = require("fs/promises");
|
|
4751
5583
|
var import_node_path10 = require("path");
|
|
4752
5584
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4753
5585
|
var COLLECTION = "kb";
|
|
@@ -4792,7 +5624,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4792
5624
|
}
|
|
4793
5625
|
}
|
|
4794
5626
|
async function isStale(bundlePath2) {
|
|
4795
|
-
const indexAt = await (0,
|
|
5627
|
+
const indexAt = await (0, import_promises9.stat)((0, import_node_path10.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4796
5628
|
if (!indexAt) return true;
|
|
4797
5629
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4798
5630
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4801,8 +5633,8 @@ async function isStale(bundlePath2) {
|
|
|
4801
5633
|
let stale = false;
|
|
4802
5634
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4803
5635
|
if (stale) return;
|
|
4804
|
-
const
|
|
4805
|
-
if (
|
|
5636
|
+
const at2 = await (0, import_promises9.stat)((0, import_node_path10.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5637
|
+
if (at2 > indexAt) stale = true;
|
|
4806
5638
|
});
|
|
4807
5639
|
return stale;
|
|
4808
5640
|
}
|
|
@@ -5110,7 +5942,7 @@ var KbStore = class {
|
|
|
5110
5942
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
5111
5943
|
const root = this.root(bundlePath2);
|
|
5112
5944
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
5113
|
-
await (0,
|
|
5945
|
+
await (0, import_promises10.mkdir)(root, { recursive: true });
|
|
5114
5946
|
await this.publish(
|
|
5115
5947
|
target,
|
|
5116
5948
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -5149,24 +5981,27 @@ var KbStore = class {
|
|
|
5149
5981
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
5150
5982
|
let raw;
|
|
5151
5983
|
try {
|
|
5152
|
-
raw = await (0,
|
|
5984
|
+
raw = await (0, import_promises10.readFile)(target, "utf8");
|
|
5153
5985
|
} catch {
|
|
5154
5986
|
return null;
|
|
5155
5987
|
}
|
|
5156
5988
|
return this.parse(conceptId2, raw);
|
|
5157
5989
|
}
|
|
5158
5990
|
/**
|
|
5159
|
-
* Every record in the bundle, optionally narrowed to one type
|
|
5991
|
+
* Every record in the bundle, optionally narrowed to one type and to the
|
|
5992
|
+
* records carrying every tag in `filter.tags`. Selection only — `excludeTags`
|
|
5993
|
+
* is not taken here, because `query`, `catalog` and `load` read through this
|
|
5994
|
+
* and must adjudicate over the whole base.
|
|
5160
5995
|
*
|
|
5161
5996
|
* A file that fails to parse is skipped and logged rather than thrown: one
|
|
5162
5997
|
* malformed record — hand-edited, or written by a producer we don't know —
|
|
5163
5998
|
* must not make the whole bundle unreadable.
|
|
5164
5999
|
*/
|
|
5165
|
-
async list(bundlePath2, type) {
|
|
6000
|
+
async list(bundlePath2, type, filter = {}) {
|
|
5166
6001
|
const root = this.root(bundlePath2);
|
|
5167
6002
|
let names;
|
|
5168
6003
|
try {
|
|
5169
|
-
names = await (0,
|
|
6004
|
+
names = await (0, import_promises10.readdir)(root);
|
|
5170
6005
|
} catch {
|
|
5171
6006
|
return [];
|
|
5172
6007
|
}
|
|
@@ -5174,9 +6009,11 @@ var KbStore = class {
|
|
|
5174
6009
|
const records = await mapLimit(
|
|
5175
6010
|
wanted,
|
|
5176
6011
|
DEFAULT_IO_CONCURRENCY,
|
|
5177
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
6012
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises10.readFile)((0, import_node_path11.join)(root, name), "utf8"))
|
|
6013
|
+
);
|
|
6014
|
+
return records.filter(
|
|
6015
|
+
(record) => record !== null && matchesTags(record, filter)
|
|
5178
6016
|
);
|
|
5179
|
-
return records.filter((record) => record !== null);
|
|
5180
6017
|
}
|
|
5181
6018
|
/**
|
|
5182
6019
|
* Moves a record's status, preserving everything else.
|
|
@@ -5222,8 +6059,8 @@ var KbStore = class {
|
|
|
5222
6059
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
5223
6060
|
* logs what it publishes.
|
|
5224
6061
|
*/
|
|
5225
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
5226
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
6062
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6063
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
5227
6064
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
5228
6065
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
5229
6066
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -5275,14 +6112,14 @@ var KbStore = class {
|
|
|
5275
6112
|
return superseded;
|
|
5276
6113
|
}
|
|
5277
6114
|
/** Resolves an open question, stamping who answered and when. */
|
|
5278
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
6115
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5279
6116
|
return this.mutate(
|
|
5280
6117
|
bundlePath2,
|
|
5281
6118
|
conceptId2,
|
|
5282
6119
|
(frontmatter) => ({
|
|
5283
6120
|
...frontmatter,
|
|
5284
6121
|
strauss_status: "resolved",
|
|
5285
|
-
strauss_answered: { by: actor, at }
|
|
6122
|
+
strauss_answered: { by: actor, at: at2 }
|
|
5286
6123
|
}),
|
|
5287
6124
|
{ operation: "answer", by: actor },
|
|
5288
6125
|
(body) => `${body.trimEnd()}
|
|
@@ -5316,9 +6153,10 @@ ${answer}
|
|
|
5316
6153
|
/* @__PURE__ */ new Date(),
|
|
5317
6154
|
await this.detectDrift(narrowed, options.repoRoot)
|
|
5318
6155
|
);
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
6156
|
+
const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
|
|
6157
|
+
if (options.includeNonCurrent) return kept;
|
|
6158
|
+
const present = new Set(kept.map((hit) => hit.record.conceptId));
|
|
6159
|
+
return kept.filter(
|
|
5322
6160
|
(hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
|
|
5323
6161
|
);
|
|
5324
6162
|
}
|
|
@@ -5421,14 +6259,17 @@ ${answer}
|
|
|
5421
6259
|
/* @__PURE__ */ new Date(),
|
|
5422
6260
|
await this.detectDrift(wanted, options.repoRoot)
|
|
5423
6261
|
);
|
|
5424
|
-
const
|
|
5425
|
-
|
|
6262
|
+
const kept = adjudicated.filter(
|
|
6263
|
+
(hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
|
|
6264
|
+
);
|
|
6265
|
+
const records = kept.filter((hit) => hit.standing !== "superseded");
|
|
6266
|
+
const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5426
6267
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
5427
6268
|
const bundleDigestValue = bundleDigest(records, superseded);
|
|
5428
6269
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
5429
6270
|
return {
|
|
5430
6271
|
loaded: false,
|
|
5431
|
-
recordCount:
|
|
6272
|
+
recordCount: kept.length,
|
|
5432
6273
|
approxTokens: approxTokens2,
|
|
5433
6274
|
budgetTokens,
|
|
5434
6275
|
message: refusalMessage({
|
|
@@ -5441,7 +6282,7 @@ ${answer}
|
|
|
5441
6282
|
}
|
|
5442
6283
|
return {
|
|
5443
6284
|
loaded: true,
|
|
5444
|
-
recordCount:
|
|
6285
|
+
recordCount: kept.length,
|
|
5445
6286
|
tokensLoaded: approxTokens2,
|
|
5446
6287
|
budgetTokens: options.all ? null : budgetTokens,
|
|
5447
6288
|
records,
|
|
@@ -5451,24 +6292,34 @@ ${answer}
|
|
|
5451
6292
|
}
|
|
5452
6293
|
/**
|
|
5453
6294
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
5454
|
-
* the same way, handed back as a stamp.
|
|
5455
|
-
*
|
|
5456
|
-
*
|
|
6295
|
+
* the same way, handed back as a stamp.
|
|
6296
|
+
*
|
|
6297
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
6298
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
6299
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
6300
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
5457
6301
|
*/
|
|
5458
|
-
async stamp(bundlePath2) {
|
|
6302
|
+
async stamp(bundlePath2, options = {}) {
|
|
5459
6303
|
const bundle = await this.list(bundlePath2);
|
|
5460
6304
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
5461
6305
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
5462
6306
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5463
6307
|
const stamped = bundleStamp(current, superseded);
|
|
5464
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
6308
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
6309
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
6310
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
6311
|
+
(entries) => entries.some(
|
|
6312
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
6313
|
+
)
|
|
6314
|
+
).length;
|
|
5465
6315
|
return {
|
|
5466
6316
|
path: bundlePath2,
|
|
5467
6317
|
digest: stamped.digest,
|
|
5468
6318
|
recordCount: bundle.length,
|
|
5469
6319
|
superseded: superseded.length,
|
|
5470
6320
|
newestAt: dates.at(-1) ?? null,
|
|
5471
|
-
records: stamped.records
|
|
6321
|
+
records: stamped.records,
|
|
6322
|
+
drifted: drifted2
|
|
5472
6323
|
};
|
|
5473
6324
|
}
|
|
5474
6325
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -5501,7 +6352,7 @@ ${answer}
|
|
|
5501
6352
|
async readIndex(bundlePath2) {
|
|
5502
6353
|
const root = this.root(bundlePath2);
|
|
5503
6354
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
5504
|
-
const stored = await (0,
|
|
6355
|
+
const stored = await (0, import_promises10.readFile)((0, import_node_path11.join)(root, INDEX_FILE), "utf8").catch(
|
|
5505
6356
|
() => null
|
|
5506
6357
|
);
|
|
5507
6358
|
if (indexIsStale(stored, expected)) {
|
|
@@ -5522,7 +6373,7 @@ ${answer}
|
|
|
5522
6373
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
5523
6374
|
*/
|
|
5524
6375
|
async readLog(bundlePath2) {
|
|
5525
|
-
const raw = await (0,
|
|
6376
|
+
const raw = await (0, import_promises10.readFile)(
|
|
5526
6377
|
(0, import_node_path11.join)(this.root(bundlePath2), LOG_FILE),
|
|
5527
6378
|
"utf8"
|
|
5528
6379
|
).catch(() => "");
|
|
@@ -5574,14 +6425,14 @@ ${answer}
|
|
|
5574
6425
|
}
|
|
5575
6426
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
5576
6427
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
5577
|
-
const before = await (0,
|
|
6428
|
+
const before = await (0, import_promises10.readFile)(target, "utf8").catch(() => null);
|
|
5578
6429
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
5579
6430
|
const parsed = this.parse(conceptId2, before);
|
|
5580
6431
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
5581
6432
|
const frontmatter = change(parsed.frontmatter);
|
|
5582
6433
|
const body = changeBody(parsed.body);
|
|
5583
6434
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
5584
|
-
const witness = await (0,
|
|
6435
|
+
const witness = await (0, import_promises10.readFile)(target, "utf8").catch(() => null);
|
|
5585
6436
|
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
5586
6437
|
throw new KbWriteConflictError(conceptId2);
|
|
5587
6438
|
}
|
|
@@ -5607,20 +6458,20 @@ ${answer}
|
|
|
5607
6458
|
*/
|
|
5608
6459
|
async publish(target, contents, overwrite, conceptId2) {
|
|
5609
6460
|
const staging = `${target}.${process.pid}.tmp`;
|
|
5610
|
-
await (0,
|
|
6461
|
+
await (0, import_promises10.writeFile)(staging, contents, "utf8");
|
|
5611
6462
|
try {
|
|
5612
6463
|
if (overwrite) {
|
|
5613
|
-
await (0,
|
|
6464
|
+
await (0, import_promises10.rename)(staging, target);
|
|
5614
6465
|
return;
|
|
5615
6466
|
}
|
|
5616
|
-
await (0,
|
|
6467
|
+
await (0, import_promises10.link)(staging, target);
|
|
5617
6468
|
} catch (error) {
|
|
5618
6469
|
if (error.code === "EEXIST") {
|
|
5619
6470
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
5620
6471
|
}
|
|
5621
6472
|
throw error;
|
|
5622
6473
|
} finally {
|
|
5623
|
-
await (0,
|
|
6474
|
+
await (0, import_promises10.unlink)(staging).catch(() => void 0);
|
|
5624
6475
|
}
|
|
5625
6476
|
}
|
|
5626
6477
|
/**
|
|
@@ -5668,14 +6519,14 @@ ${answer}
|
|
|
5668
6519
|
try {
|
|
5669
6520
|
let existing;
|
|
5670
6521
|
try {
|
|
5671
|
-
existing = await (0,
|
|
6522
|
+
existing = await (0, import_promises10.readFile)(target, "utf8");
|
|
5672
6523
|
} catch (error) {
|
|
5673
6524
|
if (error.code !== "ENOENT") throw error;
|
|
5674
6525
|
existing = null;
|
|
5675
6526
|
}
|
|
5676
6527
|
if (existing === null) {
|
|
5677
6528
|
try {
|
|
5678
|
-
await (0,
|
|
6529
|
+
await (0, import_promises10.writeFile)(target, appendUnionMergeLine(""), {
|
|
5679
6530
|
encoding: "utf8",
|
|
5680
6531
|
flag: "wx"
|
|
5681
6532
|
});
|
|
@@ -5696,7 +6547,7 @@ ${answer}
|
|
|
5696
6547
|
return;
|
|
5697
6548
|
}
|
|
5698
6549
|
if (!hasMergeDeclaration(existing)) {
|
|
5699
|
-
await (0,
|
|
6550
|
+
await (0, import_promises10.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
5700
6551
|
this.logger.info?.({
|
|
5701
6552
|
operation: "kb.gitattributes.ensure",
|
|
5702
6553
|
bundlePath: root,
|
|
@@ -5715,7 +6566,7 @@ ${answer}
|
|
|
5715
6566
|
async record(root, entry) {
|
|
5716
6567
|
await this.ensureGitattributes(root);
|
|
5717
6568
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
5718
|
-
await (0,
|
|
6569
|
+
await (0, import_promises10.appendFile)((0, import_node_path11.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
5719
6570
|
this.logger.warn?.({
|
|
5720
6571
|
operation: "kb.log.append",
|
|
5721
6572
|
outcome: "failed",
|
|
@@ -5792,7 +6643,7 @@ function normalizeActor(id) {
|
|
|
5792
6643
|
}
|
|
5793
6644
|
|
|
5794
6645
|
// src/version.ts
|
|
5795
|
-
var VERSION = true ? "0.1.
|
|
6646
|
+
var VERSION = true ? "0.1.19" : "0.0.0-dev";
|
|
5796
6647
|
|
|
5797
6648
|
// src/cli.ts
|
|
5798
6649
|
async function runKbCli(argv) {
|
|
@@ -5849,21 +6700,21 @@ async function runKbCli(argv) {
|
|
|
5849
6700
|
`);
|
|
5850
6701
|
}
|
|
5851
6702
|
function takeLiteral(argv) {
|
|
5852
|
-
const
|
|
5853
|
-
if (
|
|
5854
|
-
return { flags: argv.slice(0,
|
|
6703
|
+
const at2 = argv.indexOf("--");
|
|
6704
|
+
if (at2 === -1) return { flags: argv, literal: [] };
|
|
6705
|
+
return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
|
|
5855
6706
|
}
|
|
5856
6707
|
function takeBundle(argv) {
|
|
5857
|
-
const
|
|
5858
|
-
if (
|
|
6708
|
+
const at2 = argv.indexOf("--bundle");
|
|
6709
|
+
if (at2 === -1) {
|
|
5859
6710
|
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5860
6711
|
}
|
|
5861
|
-
const bundle = argv[
|
|
6712
|
+
const bundle = argv[at2 + 1];
|
|
5862
6713
|
if (!bundle) die("--bundle requires a path");
|
|
5863
6714
|
return {
|
|
5864
6715
|
bundle,
|
|
5865
6716
|
explicit: true,
|
|
5866
|
-
rest: [...argv.slice(0,
|
|
6717
|
+
rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
|
|
5867
6718
|
};
|
|
5868
6719
|
}
|
|
5869
6720
|
function readStdin() {
|