@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
|
@@ -40,6 +40,14 @@ var kbAnchorSchema = z.object({
|
|
|
40
40
|
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
41
41
|
message: "hash must be sha256:<64 hex chars>"
|
|
42
42
|
}).optional(),
|
|
43
|
+
/**
|
|
44
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
45
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
46
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
47
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
48
|
+
* comments, so reformatting the anchored code is not drift.
|
|
49
|
+
*/
|
|
50
|
+
hash_kind: z.enum(["raw", "ast"]).optional(),
|
|
43
51
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
44
52
|
resolved_at: z.string().min(1).optional(),
|
|
45
53
|
/** Line count of the text the hash was taken over. */
|
|
@@ -524,8 +532,8 @@ function safeSegment(value) {
|
|
|
524
532
|
function revRef(rev) {
|
|
525
533
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
526
534
|
let hash = 5381;
|
|
527
|
-
for (let
|
|
528
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
535
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
536
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
529
537
|
}
|
|
530
538
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
531
539
|
}
|
|
@@ -548,9 +556,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
548
556
|
{ length: Math.min(limit, items.length) },
|
|
549
557
|
async () => {
|
|
550
558
|
while (!failed && next < items.length) {
|
|
551
|
-
const
|
|
559
|
+
const at2 = next++;
|
|
552
560
|
try {
|
|
553
|
-
out[
|
|
561
|
+
out[at2] = await fn(items[at2], at2);
|
|
554
562
|
} catch (error) {
|
|
555
563
|
failed = true;
|
|
556
564
|
throw error;
|
|
@@ -663,8 +671,8 @@ function repoUrlIsSafe(repo) {
|
|
|
663
671
|
if (!scheme?.[1]) return false;
|
|
664
672
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
665
673
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
666
|
-
const
|
|
667
|
-
return
|
|
674
|
+
const at2 = authority.lastIndexOf("@");
|
|
675
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
668
676
|
}
|
|
669
677
|
function protocolArgs() {
|
|
670
678
|
const allowed = allowedProtocols();
|
|
@@ -755,7 +763,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
755
763
|
return new Map([
|
|
756
764
|
...rejected2,
|
|
757
765
|
...wants.map(
|
|
758
|
-
(want,
|
|
766
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
759
767
|
)
|
|
760
768
|
]);
|
|
761
769
|
}
|
|
@@ -966,7 +974,7 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
966
974
|
return { ok: false, reason: "file-unreadable" };
|
|
967
975
|
}
|
|
968
976
|
});
|
|
969
|
-
return new Map(wanted.map((file,
|
|
977
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
970
978
|
}
|
|
971
979
|
|
|
972
980
|
// src/grammars/store.ts
|
|
@@ -1150,8 +1158,8 @@ async function ensureGrammar(language, options = {}) {
|
|
|
1150
1158
|
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1151
1159
|
const parts = [];
|
|
1152
1160
|
const total = pack2.tags.length;
|
|
1153
|
-
for (const [
|
|
1154
|
-
const name = `${language} tags${total > 1 ? ` part ${
|
|
1161
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1162
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1155
1163
|
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1156
1164
|
const held = await ensurePart(path, name, part, options);
|
|
1157
1165
|
if (held !== true) return miss(language, name, held);
|
|
@@ -1300,8 +1308,8 @@ function typeNameIn(receiver) {
|
|
|
1300
1308
|
while (stack.length) {
|
|
1301
1309
|
const node = stack.pop();
|
|
1302
1310
|
if (node.type === "type_identifier") return node.text;
|
|
1303
|
-
for (let
|
|
1304
|
-
const child = node.child(
|
|
1311
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1312
|
+
const child = node.child(at2);
|
|
1305
1313
|
if (child) stack.push(child);
|
|
1306
1314
|
}
|
|
1307
1315
|
}
|
|
@@ -1310,7 +1318,7 @@ function typeNameIn(receiver) {
|
|
|
1310
1318
|
function endsWith(chain, wanted) {
|
|
1311
1319
|
if (wanted.length > chain.length) return false;
|
|
1312
1320
|
const offset = chain.length - wanted.length;
|
|
1313
|
-
return wanted.every((segment,
|
|
1321
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1314
1322
|
}
|
|
1315
1323
|
function width(node) {
|
|
1316
1324
|
return node.endIndex - node.startIndex;
|
|
@@ -1336,6 +1344,26 @@ function spanOf(definition, source) {
|
|
|
1336
1344
|
};
|
|
1337
1345
|
}
|
|
1338
1346
|
|
|
1347
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1348
|
+
function tokens(root) {
|
|
1349
|
+
const out = [];
|
|
1350
|
+
const stack = [root];
|
|
1351
|
+
while (stack.length) {
|
|
1352
|
+
const node = stack.pop();
|
|
1353
|
+
if (node.type.includes("comment")) continue;
|
|
1354
|
+
if (node.childCount === 0) {
|
|
1355
|
+
const text = node.text.trim();
|
|
1356
|
+
if (text) out.push(text);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1360
|
+
const child = node.child(at2);
|
|
1361
|
+
if (child) stack.push(child);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1339
1367
|
// src/tree-sitter-resolver/resolver.ts
|
|
1340
1368
|
var TREE_CACHE_LIMIT = 32;
|
|
1341
1369
|
var TreeSitterResolver = class {
|
|
@@ -1381,7 +1409,7 @@ var TreeSitterResolver = class {
|
|
|
1381
1409
|
(language) => this.load(language)
|
|
1382
1410
|
);
|
|
1383
1411
|
languages.forEach(
|
|
1384
|
-
(language,
|
|
1412
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1385
1413
|
);
|
|
1386
1414
|
}
|
|
1387
1415
|
/**
|
|
@@ -1469,6 +1497,60 @@ var TreeSitterResolver = class {
|
|
|
1469
1497
|
this.trees.set(key, parsed);
|
|
1470
1498
|
return parsed;
|
|
1471
1499
|
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1502
|
+
*
|
|
1503
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1504
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1505
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1506
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1507
|
+
*/
|
|
1508
|
+
spans(source, file) {
|
|
1509
|
+
const language = languageForFile(file);
|
|
1510
|
+
if (!language) return [];
|
|
1511
|
+
const loaded = this.loaded.get(language);
|
|
1512
|
+
if (!loaded) return [];
|
|
1513
|
+
const parsed = this.parse(language, loaded, source);
|
|
1514
|
+
if (!parsed) return [];
|
|
1515
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1516
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1517
|
+
span: spanOf(definition, source)
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1522
|
+
* joined by single spaces.
|
|
1523
|
+
*
|
|
1524
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1525
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1526
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1527
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1528
|
+
* those are leaves.
|
|
1529
|
+
*
|
|
1530
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1531
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1532
|
+
*/
|
|
1533
|
+
normalize(text, file) {
|
|
1534
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1535
|
+
if (!language) return null;
|
|
1536
|
+
const loaded = this.loaded.get(language);
|
|
1537
|
+
if (!loaded) return null;
|
|
1538
|
+
const parser = this.parser;
|
|
1539
|
+
if (!parser) return null;
|
|
1540
|
+
let tree;
|
|
1541
|
+
try {
|
|
1542
|
+
parser.setLanguage(loaded.language);
|
|
1543
|
+
tree = parser.parse(text);
|
|
1544
|
+
} catch {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (!tree) return null;
|
|
1548
|
+
try {
|
|
1549
|
+
return tokens(tree.rootNode).join(" ");
|
|
1550
|
+
} finally {
|
|
1551
|
+
tree.delete();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1472
1554
|
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1473
1555
|
reset() {
|
|
1474
1556
|
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
@@ -1623,7 +1705,7 @@ var regexResolver = {
|
|
|
1623
1705
|
);
|
|
1624
1706
|
const nearest = Math.min(...distances);
|
|
1625
1707
|
if (Number.isFinite(nearest)) {
|
|
1626
|
-
candidates = candidates.filter((_,
|
|
1708
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1627
1709
|
}
|
|
1628
1710
|
}
|
|
1629
1711
|
if (candidates.length !== 1) return null;
|
|
@@ -1638,8 +1720,8 @@ function escapeRegExp(value) {
|
|
|
1638
1720
|
}
|
|
1639
1721
|
function distanceToParent(lines, index2, parent) {
|
|
1640
1722
|
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1641
|
-
for (let
|
|
1642
|
-
if (parent.test(lines[
|
|
1723
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1724
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1643
1725
|
}
|
|
1644
1726
|
return Number.POSITIVE_INFINITY;
|
|
1645
1727
|
}
|
|
@@ -1671,10 +1753,12 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1671
1753
|
if (attempt.reason === "symbol-not-found") continue;
|
|
1672
1754
|
return { ok: false, reason: attempt.reason };
|
|
1673
1755
|
}
|
|
1756
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1674
1757
|
return {
|
|
1675
1758
|
ok: true,
|
|
1676
1759
|
span: attempt.span,
|
|
1677
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
1760
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1761
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1678
1762
|
};
|
|
1679
1763
|
}
|
|
1680
1764
|
return { ok: false, reason: "symbol-not-found" };
|
|
@@ -1702,6 +1786,11 @@ function resolverChanged(source, anchor, produced) {
|
|
|
1702
1786
|
);
|
|
1703
1787
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1704
1788
|
}
|
|
1789
|
+
function anchorHashOf(anchor, outcome) {
|
|
1790
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1791
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1792
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1793
|
+
}
|
|
1705
1794
|
|
|
1706
1795
|
// src/anchor-resolver/drift.ts
|
|
1707
1796
|
async function detectAnchorDrift(records, options = {}) {
|
|
@@ -1781,16 +1870,29 @@ function unresolved(anchor, reason, repo) {
|
|
|
1781
1870
|
state: "unresolved",
|
|
1782
1871
|
diffSize: null,
|
|
1783
1872
|
...reason ? { reason } : {},
|
|
1784
|
-
...repo ? { repo } : {}
|
|
1873
|
+
...repo ? { repo } : {},
|
|
1874
|
+
...classOf(reason)
|
|
1785
1875
|
};
|
|
1786
1876
|
}
|
|
1877
|
+
function provisionalDriftClass(entry) {
|
|
1878
|
+
if (entry.state === "unresolved") {
|
|
1879
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
1880
|
+
}
|
|
1881
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1882
|
+
}
|
|
1883
|
+
function classOf(reason) {
|
|
1884
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1885
|
+
return settled ? { class: settled } : {};
|
|
1886
|
+
}
|
|
1787
1887
|
function hashIn(source, anchor, resolvers) {
|
|
1788
1888
|
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1789
1889
|
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1890
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1790
1891
|
return {
|
|
1791
1892
|
ok: true,
|
|
1792
1893
|
current: {
|
|
1793
|
-
hash
|
|
1894
|
+
hash,
|
|
1895
|
+
kind,
|
|
1794
1896
|
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1795
1897
|
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1796
1898
|
}
|
|
@@ -1803,11 +1905,14 @@ function resolverExtras(source, anchor, current) {
|
|
|
1803
1905
|
};
|
|
1804
1906
|
}
|
|
1805
1907
|
function compared(anchor, current, extra = {}) {
|
|
1908
|
+
const matched = current.hash === anchor.hash;
|
|
1806
1909
|
return {
|
|
1807
1910
|
...base(anchor),
|
|
1808
|
-
state:
|
|
1911
|
+
state: matched ? "match" : "drifted",
|
|
1809
1912
|
currentHash: current.hash,
|
|
1913
|
+
hashKind: current.kind,
|
|
1810
1914
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1915
|
+
...matched ? {} : { class: "changed" },
|
|
1811
1916
|
...extra
|
|
1812
1917
|
};
|
|
1813
1918
|
}
|
|
@@ -2061,14 +2166,17 @@ function asBudgets(value) {
|
|
|
2061
2166
|
if (value === null || typeof value !== "object") return {};
|
|
2062
2167
|
const table2 = value;
|
|
2063
2168
|
const pick = (key, min) => {
|
|
2064
|
-
const
|
|
2065
|
-
return typeof
|
|
2169
|
+
const raw2 = table2[key];
|
|
2170
|
+
return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
|
|
2066
2171
|
};
|
|
2067
2172
|
const budgetTokens = pick("budgetTokens", 1);
|
|
2068
2173
|
const fullUnderTokens = pick("fullUnderTokens", 0);
|
|
2174
|
+
const raw = table2["excludeTags"];
|
|
2175
|
+
const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
2069
2176
|
return {
|
|
2070
2177
|
...budgetTokens ? { budgetTokens } : {},
|
|
2071
|
-
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
|
|
2178
|
+
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
|
|
2179
|
+
...excludeTags ? { excludeTags } : {}
|
|
2072
2180
|
};
|
|
2073
2181
|
}
|
|
2074
2182
|
function contextProfileBudgets(manifest, profile) {
|
|
@@ -2264,7 +2372,7 @@ async function listPins(store, workspaceDir) {
|
|
|
2264
2372
|
}
|
|
2265
2373
|
|
|
2266
2374
|
// src/kb-pins/pin.ts
|
|
2267
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
2375
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
2268
2376
|
const layer = options.layer ?? "project";
|
|
2269
2377
|
const root = layerRoot(workspaceDir, layer);
|
|
2270
2378
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -2292,7 +2400,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2292
2400
|
return {
|
|
2293
2401
|
path: existing.path,
|
|
2294
2402
|
layer,
|
|
2295
|
-
pinnedAt: existing.pinnedAt ??
|
|
2403
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
2296
2404
|
alreadyPinned: true,
|
|
2297
2405
|
...updated.mode ? { mode: updated.mode } : {},
|
|
2298
2406
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -2302,7 +2410,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2302
2410
|
}
|
|
2303
2411
|
const entry = {
|
|
2304
2412
|
path: storablePath(root, bundlePath2),
|
|
2305
|
-
pinnedAt:
|
|
2413
|
+
pinnedAt: at2,
|
|
2306
2414
|
...fields
|
|
2307
2415
|
};
|
|
2308
2416
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -2312,7 +2420,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2312
2420
|
return {
|
|
2313
2421
|
path: entry.path,
|
|
2314
2422
|
layer,
|
|
2315
|
-
pinnedAt:
|
|
2423
|
+
pinnedAt: at2,
|
|
2316
2424
|
alreadyPinned: false,
|
|
2317
2425
|
...fields,
|
|
2318
2426
|
...warning ? { warning } : {}
|
|
@@ -2412,7 +2520,8 @@ function warningAnchor(entry) {
|
|
|
2412
2520
|
diffSize,
|
|
2413
2521
|
...reason !== void 0 ? { reason } : {},
|
|
2414
2522
|
...repo !== void 0 ? { repo } : {},
|
|
2415
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
2523
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
2524
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
2416
2525
|
};
|
|
2417
2526
|
}
|
|
2418
2527
|
function resolveHeads(from, byId) {
|
|
@@ -2464,6 +2573,13 @@ function successors(record, byId) {
|
|
|
2464
2573
|
return { records, missing: missing2 };
|
|
2465
2574
|
}
|
|
2466
2575
|
|
|
2576
|
+
// src/kb-tags.ts
|
|
2577
|
+
function matchesTags(record, filter) {
|
|
2578
|
+
if (!filter.tags?.length && !filter.excludeTags?.length) return true;
|
|
2579
|
+
const carried = new Set(record.frontmatter.tags ?? []);
|
|
2580
|
+
return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2467
2583
|
// src/catalog.ts
|
|
2468
2584
|
var EMPTY_STANDINGS = {
|
|
2469
2585
|
current: 0,
|
|
@@ -2474,7 +2590,7 @@ var EMPTY_STANDINGS = {
|
|
|
2474
2590
|
};
|
|
2475
2591
|
function catalog(bundle, options = {}) {
|
|
2476
2592
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2477
|
-
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
2593
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
|
|
2478
2594
|
conceptId: hit.record.conceptId,
|
|
2479
2595
|
type: hit.record.frontmatter.type,
|
|
2480
2596
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2564,7 +2680,7 @@ function preamble() {
|
|
|
2564
2680
|
"tokens."
|
|
2565
2681
|
].join("\n");
|
|
2566
2682
|
}
|
|
2567
|
-
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
|
|
2683
|
+
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
|
|
2568
2684
|
const bundle = await store.list(absolutePath);
|
|
2569
2685
|
if (bundle.length === 0) {
|
|
2570
2686
|
return {
|
|
@@ -2578,7 +2694,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
2578
2694
|
let degradedFrom;
|
|
2579
2695
|
if (fullCap > 0) {
|
|
2580
2696
|
const full = await store.load(absolutePath, {
|
|
2581
|
-
budgetTokens: fullCap
|
|
2697
|
+
budgetTokens: fullCap,
|
|
2698
|
+
excludeTags
|
|
2582
2699
|
});
|
|
2583
2700
|
if (!full.loaded && pinMode === "full") {
|
|
2584
2701
|
degradedFrom = { approxTokens: full.approxTokens };
|
|
@@ -2608,7 +2725,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
2608
2725
|
};
|
|
2609
2726
|
}
|
|
2610
2727
|
}
|
|
2611
|
-
const adjudicated = adjudicate(bundle, bundle)
|
|
2728
|
+
const adjudicated = adjudicate(bundle, bundle).filter(
|
|
2729
|
+
(hit) => matchesTags(hit.record, { excludeTags })
|
|
2730
|
+
);
|
|
2612
2731
|
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
2613
2732
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
2614
2733
|
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
@@ -2629,6 +2748,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
2629
2748
|
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
2630
2749
|
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
2631
2750
|
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
2751
|
+
const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
|
|
2632
2752
|
const pins = merged.pins.filter(
|
|
2633
2753
|
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
2634
2754
|
);
|
|
@@ -2649,7 +2769,8 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
2649
2769
|
pin.absolutePath,
|
|
2650
2770
|
fullUnderTokens,
|
|
2651
2771
|
pin.mode,
|
|
2652
|
-
budgetTokens
|
|
2772
|
+
budgetTokens,
|
|
2773
|
+
excludeTags
|
|
2653
2774
|
),
|
|
2654
2775
|
frozen: pin.frozen === true
|
|
2655
2776
|
}))
|
|
@@ -2766,6 +2887,405 @@ ${CONTEXT_END}` : null;
|
|
|
2766
2887
|
return { file, action: "appended" };
|
|
2767
2888
|
}
|
|
2768
2889
|
|
|
2890
|
+
// src/drift/git.ts
|
|
2891
|
+
import { execFile as execFile3 } from "child_process";
|
|
2892
|
+
import { promisify as promisify3 } from "util";
|
|
2893
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2894
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
2895
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
2896
|
+
async function git2(cwd, args) {
|
|
2897
|
+
const env = { ...process.env };
|
|
2898
|
+
delete env["GIT_DIR"];
|
|
2899
|
+
delete env["GIT_WORK_TREE"];
|
|
2900
|
+
delete env["GIT_INDEX_FILE"];
|
|
2901
|
+
try {
|
|
2902
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
2903
|
+
timeout: GIT_TIMEOUT_MS,
|
|
2904
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
2905
|
+
env
|
|
2906
|
+
});
|
|
2907
|
+
return { ok: true, stdout };
|
|
2908
|
+
} catch {
|
|
2909
|
+
return { ok: false };
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
async function listRepoFiles(repoRoot) {
|
|
2913
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
2914
|
+
if (!result.ok) return [];
|
|
2915
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
2916
|
+
}
|
|
2917
|
+
async function readOldSource(repoRoot, anchor) {
|
|
2918
|
+
if (!filePathIsSafe(anchor.file))
|
|
2919
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2920
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
2921
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
2922
|
+
if (shown2 !== null) {
|
|
2923
|
+
return {
|
|
2924
|
+
ok: true,
|
|
2925
|
+
source: shown2,
|
|
2926
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
const at2 = anchor.resolved_at;
|
|
2931
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
2932
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2933
|
+
}
|
|
2934
|
+
const found = await git2(repoRoot, [
|
|
2935
|
+
"log",
|
|
2936
|
+
"-1",
|
|
2937
|
+
"--format=%H",
|
|
2938
|
+
`--before=${at2}`,
|
|
2939
|
+
"--end-of-options",
|
|
2940
|
+
"HEAD",
|
|
2941
|
+
"--",
|
|
2942
|
+
anchor.file
|
|
2943
|
+
]);
|
|
2944
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
2945
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
2946
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2947
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
2948
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
2949
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
2950
|
+
}
|
|
2951
|
+
async function showFile(repoRoot, ref, file) {
|
|
2952
|
+
const path = file.replace(/^\.\//, "");
|
|
2953
|
+
const result = await git2(repoRoot, [
|
|
2954
|
+
"show",
|
|
2955
|
+
"--end-of-options",
|
|
2956
|
+
`${ref}:${path}`
|
|
2957
|
+
]);
|
|
2958
|
+
return result.ok ? result.stdout : null;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// src/drift/moved.ts
|
|
2962
|
+
import { stat as stat2 } from "fs/promises";
|
|
2963
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
2964
|
+
var SEARCH_BATCH = 64;
|
|
2965
|
+
function movedSearch(repoRoot, options = {}) {
|
|
2966
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
2967
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
2968
|
+
const resolver = new TreeSitterResolver();
|
|
2969
|
+
let repoFiles;
|
|
2970
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
2971
|
+
const filesForLanguage = async (language) => {
|
|
2972
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
2973
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
2974
|
+
};
|
|
2975
|
+
return {
|
|
2976
|
+
async find(anchor) {
|
|
2977
|
+
const stored = anchor.hash;
|
|
2978
|
+
if (!stored) return void 0;
|
|
2979
|
+
const language = languageForFile(anchor.file);
|
|
2980
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
2981
|
+
const candidates = await filesForLanguage(language);
|
|
2982
|
+
if (!prepared.has(language)) {
|
|
2983
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
2984
|
+
prepared.add(language);
|
|
2985
|
+
}
|
|
2986
|
+
const floor = anchor.lines ?? 0;
|
|
2987
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
2988
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
2989
|
+
const hits = await mapLimit(
|
|
2990
|
+
batch,
|
|
2991
|
+
DEFAULT_IO_CONCURRENCY,
|
|
2992
|
+
async (file) => {
|
|
2993
|
+
const size2 = await sizeOf(file);
|
|
2994
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
2995
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
2996
|
+
}
|
|
2997
|
+
);
|
|
2998
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
2999
|
+
if (found) return found;
|
|
3000
|
+
}
|
|
3001
|
+
return void 0;
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
3006
|
+
const source = await read(file);
|
|
3007
|
+
if (!source.ok) return void 0;
|
|
3008
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
3009
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
3010
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
3011
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
3012
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
3013
|
+
return {
|
|
3014
|
+
file,
|
|
3015
|
+
symbol: found.symbol,
|
|
3016
|
+
startLine: found.span.startLine,
|
|
3017
|
+
endLine: found.span.endLine
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
return void 0;
|
|
3021
|
+
}
|
|
3022
|
+
function diskSize(repoRoot) {
|
|
3023
|
+
return async (file) => {
|
|
3024
|
+
const path = anchorFilePath(repoRoot, file);
|
|
3025
|
+
if (path === null) return null;
|
|
3026
|
+
try {
|
|
3027
|
+
return (await stat2(path)).size;
|
|
3028
|
+
} catch {
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
3034
|
+
const height = anchor.lines;
|
|
3035
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
3036
|
+
const source = await read(anchor.file);
|
|
3037
|
+
if (!source.ok) return void 0;
|
|
3038
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
3039
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
3040
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
return {
|
|
3044
|
+
file: anchor.file,
|
|
3045
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3046
|
+
startLine: at2 + 1,
|
|
3047
|
+
endLine: at2 + height
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
return void 0;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// src/drift/classify.ts
|
|
3054
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
3055
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3056
|
+
(anchor) => anchor.hash
|
|
3057
|
+
);
|
|
3058
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
3059
|
+
const treeSitter = new TreeSitterResolver();
|
|
3060
|
+
const resolvers = [treeSitter, regexResolver];
|
|
3061
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
3062
|
+
const wanted = [];
|
|
3063
|
+
entries.forEach((entry, at2) => {
|
|
3064
|
+
const anchor = anchors[at2];
|
|
3065
|
+
if (!anchor) return;
|
|
3066
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
3067
|
+
wanted.push({ anchor, entry });
|
|
3068
|
+
});
|
|
3069
|
+
if (!wanted.length) return [];
|
|
3070
|
+
await prepareResolvers(
|
|
3071
|
+
resolvers,
|
|
3072
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
3073
|
+
);
|
|
3074
|
+
const out = [];
|
|
3075
|
+
for (const { anchor, entry } of wanted) {
|
|
3076
|
+
const movedTo = await search.find(anchor);
|
|
3077
|
+
if (movedTo) {
|
|
3078
|
+
out.push({
|
|
3079
|
+
anchor,
|
|
3080
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
3081
|
+
class: "moved"
|
|
3082
|
+
});
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
3086
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
3087
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
3088
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
3089
|
+
out.push({
|
|
3090
|
+
anchor,
|
|
3091
|
+
entry: { ...entry, class: settled },
|
|
3092
|
+
class: settled,
|
|
3093
|
+
...newText !== void 0 ? { newText } : {},
|
|
3094
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
3095
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
return out;
|
|
3099
|
+
}
|
|
3100
|
+
function sameTokens(resolver, file, before, after) {
|
|
3101
|
+
if (before === after) return false;
|
|
3102
|
+
const left = resolver.normalize(before, file);
|
|
3103
|
+
const right = resolver.normalize(after, file);
|
|
3104
|
+
return left !== null && left === right;
|
|
3105
|
+
}
|
|
3106
|
+
async function currentText(reader, anchor, resolvers) {
|
|
3107
|
+
const read = await reader(anchor.file);
|
|
3108
|
+
if (!read.ok) return void 0;
|
|
3109
|
+
return spanIn(read.source, anchor, resolvers);
|
|
3110
|
+
}
|
|
3111
|
+
function spanIn(source, anchor, resolvers) {
|
|
3112
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
3113
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
// src/drift/diff.ts
|
|
3117
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
3118
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
3119
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
3120
|
+
function diffBudget(anchors) {
|
|
3121
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
3122
|
+
return Math.min(
|
|
3123
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
3124
|
+
Math.max(
|
|
3125
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
3126
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
3127
|
+
)
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
function unifiedDiff(before, after, options = {}) {
|
|
3131
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
3132
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
3133
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
3134
|
+
const body = [];
|
|
3135
|
+
let added = 0;
|
|
3136
|
+
let removed = 0;
|
|
3137
|
+
for (const edit of edits(left, right)) {
|
|
3138
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
3139
|
+
else if (edit.kind === "remove") {
|
|
3140
|
+
body.push(`-${edit.line}`);
|
|
3141
|
+
removed += 1;
|
|
3142
|
+
} else {
|
|
3143
|
+
body.push(`+${edit.line}`);
|
|
3144
|
+
added += 1;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const truncated = body.length > max;
|
|
3148
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
3149
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3150
|
+
const lines = [header, ...shown];
|
|
3151
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3152
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
3153
|
+
}
|
|
3154
|
+
function edits(left, right) {
|
|
3155
|
+
const rows = left.length;
|
|
3156
|
+
const cols = right.length;
|
|
3157
|
+
const table2 = Array.from(
|
|
3158
|
+
{ length: rows + 1 },
|
|
3159
|
+
() => new Array(cols + 1).fill(0)
|
|
3160
|
+
);
|
|
3161
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
3162
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
3163
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
3164
|
+
table2[row2 + 1][col2],
|
|
3165
|
+
table2[row2][col2 + 1]
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
const out = [];
|
|
3170
|
+
let row = 0;
|
|
3171
|
+
let col = 0;
|
|
3172
|
+
while (row < rows && col < cols) {
|
|
3173
|
+
if (left[row] === right[col]) {
|
|
3174
|
+
out.push({ kind: "same", line: left[row] });
|
|
3175
|
+
row += 1;
|
|
3176
|
+
col += 1;
|
|
3177
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
3178
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3179
|
+
row += 1;
|
|
3180
|
+
} else {
|
|
3181
|
+
out.push({ kind: "add", line: right[col] });
|
|
3182
|
+
col += 1;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
for (; row < rows; row++)
|
|
3186
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3187
|
+
for (; col < cols; col++)
|
|
3188
|
+
out.push({ kind: "add", line: right[col] });
|
|
3189
|
+
return out;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// src/drift/packet.ts
|
|
3193
|
+
var PRESUMED_INVALID = [
|
|
3194
|
+
"fact",
|
|
3195
|
+
"constraint",
|
|
3196
|
+
"contract"
|
|
3197
|
+
];
|
|
3198
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
3199
|
+
var DEFAULT_NOTES = {
|
|
3200
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
3201
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
3202
|
+
review: "re-read the record against the new code"
|
|
3203
|
+
};
|
|
3204
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
3205
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
3206
|
+
...options.reader ? { reader: options.reader } : {},
|
|
3207
|
+
...options.search ? { search: options.search } : {},
|
|
3208
|
+
withHistory: options.withDiff !== false
|
|
3209
|
+
});
|
|
3210
|
+
const open = classified.filter(
|
|
3211
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
3212
|
+
);
|
|
3213
|
+
if (!open.length) return { packet: null, classified };
|
|
3214
|
+
const budget = diffBudget(open.length);
|
|
3215
|
+
const anchors = open.map(
|
|
3216
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3217
|
+
);
|
|
3218
|
+
const type = record.frontmatter.type;
|
|
3219
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
3220
|
+
return {
|
|
3221
|
+
classified,
|
|
3222
|
+
packet: {
|
|
3223
|
+
conceptId: record.conceptId,
|
|
3224
|
+
title: record.frontmatter.title ?? null,
|
|
3225
|
+
type,
|
|
3226
|
+
standing: options.standing ?? "unsettled",
|
|
3227
|
+
why: record.frontmatter.description ?? null,
|
|
3228
|
+
claim: claimOf(record),
|
|
3229
|
+
anchors,
|
|
3230
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
3231
|
+
conceptId: entry.conceptId,
|
|
3232
|
+
title: entry.title,
|
|
3233
|
+
standing: entry.standing,
|
|
3234
|
+
depth: entry.depth
|
|
3235
|
+
})),
|
|
3236
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
3237
|
+
default: fallback,
|
|
3238
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
3239
|
+
}
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
3243
|
+
const { entry } = found;
|
|
3244
|
+
const base2 = {
|
|
3245
|
+
file: entry.file,
|
|
3246
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
3247
|
+
class: found.class,
|
|
3248
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
3249
|
+
storedHash: entry.storedHash,
|
|
3250
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
3251
|
+
diffSize: entry.diffSize,
|
|
3252
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3253
|
+
};
|
|
3254
|
+
if (!withDiff) return base2;
|
|
3255
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3256
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
3257
|
+
}
|
|
3258
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
3259
|
+
maxLines
|
|
3260
|
+
});
|
|
3261
|
+
return {
|
|
3262
|
+
...base2,
|
|
3263
|
+
diff: {
|
|
3264
|
+
status: "ok",
|
|
3265
|
+
source: found.oldOrigin.kind,
|
|
3266
|
+
ref: found.oldOrigin.ref,
|
|
3267
|
+
unified: rendered.text,
|
|
3268
|
+
added: rendered.added,
|
|
3269
|
+
removed: rendered.removed,
|
|
3270
|
+
truncated: rendered.truncated
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
function claimOf(record) {
|
|
3275
|
+
const type = record.frontmatter.type;
|
|
3276
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
3277
|
+
if (!section) return null;
|
|
3278
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
3279
|
+
const start = lines.findIndex(
|
|
3280
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
3281
|
+
);
|
|
3282
|
+
if (start < 0) return null;
|
|
3283
|
+
const rest = lines.slice(start + 1);
|
|
3284
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3285
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3286
|
+
return text ? { section, text } : null;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
2769
3289
|
// src/kb-edges.ts
|
|
2770
3290
|
var KB_EDGE_KINDS = [
|
|
2771
3291
|
"body-link",
|
|
@@ -3025,18 +3545,18 @@ function expired(hits, now) {
|
|
|
3025
3545
|
for (const hit of hits) {
|
|
3026
3546
|
const raw = hit.record.frontmatter.stale_after;
|
|
3027
3547
|
if (!raw) continue;
|
|
3028
|
-
const
|
|
3029
|
-
if (Number.isNaN(
|
|
3548
|
+
const at2 = Date.parse(raw);
|
|
3549
|
+
if (Number.isNaN(at2)) {
|
|
3030
3550
|
findings.push(
|
|
3031
3551
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
3032
3552
|
);
|
|
3033
3553
|
continue;
|
|
3034
3554
|
}
|
|
3035
|
-
if (
|
|
3555
|
+
if (at2 < now.getTime()) {
|
|
3036
3556
|
findings.push(
|
|
3037
3557
|
finding(
|
|
3038
3558
|
hit.record,
|
|
3039
|
-
`stale since ${raw} (${daysBetween(
|
|
3559
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
3040
3560
|
)
|
|
3041
3561
|
);
|
|
3042
3562
|
}
|
|
@@ -3049,12 +3569,12 @@ function expiring(hits, now, withinDays) {
|
|
|
3049
3569
|
for (const hit of hits) {
|
|
3050
3570
|
const raw = hit.record.frontmatter.stale_after;
|
|
3051
3571
|
if (!raw) continue;
|
|
3052
|
-
const
|
|
3053
|
-
if (Number.isNaN(
|
|
3572
|
+
const at2 = Date.parse(raw);
|
|
3573
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
3054
3574
|
findings.push(
|
|
3055
3575
|
finding(
|
|
3056
3576
|
hit.record,
|
|
3057
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
3577
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
3058
3578
|
)
|
|
3059
3579
|
);
|
|
3060
3580
|
}
|
|
@@ -3224,13 +3744,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
3224
3744
|
);
|
|
3225
3745
|
}
|
|
3226
3746
|
function describeAnchor(anchor) {
|
|
3227
|
-
const
|
|
3228
|
-
if (anchor.
|
|
3747
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3748
|
+
if (anchor.class === "gone") {
|
|
3749
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
3750
|
+
}
|
|
3751
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
3229
3752
|
if (anchor.remoteState === "drifted-on-default") {
|
|
3230
|
-
return `${
|
|
3753
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
3231
3754
|
}
|
|
3232
|
-
if (anchor.diffSize === null) return `${
|
|
3233
|
-
return anchor.diffSize === 0 ? `${
|
|
3755
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
3756
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3234
3757
|
}
|
|
3235
3758
|
function replaces(later, earlier) {
|
|
3236
3759
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -3247,9 +3770,9 @@ function daysBetween(from, to) {
|
|
|
3247
3770
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
3248
3771
|
}
|
|
3249
3772
|
function ageInDays(record, now) {
|
|
3250
|
-
const
|
|
3251
|
-
if (!
|
|
3252
|
-
const written = Date.parse(
|
|
3773
|
+
const at2 = record.frontmatter.generated?.at;
|
|
3774
|
+
if (!at2) return null;
|
|
3775
|
+
const written = Date.parse(at2);
|
|
3253
3776
|
if (Number.isNaN(written)) return null;
|
|
3254
3777
|
return daysBetween(written, now.getTime());
|
|
3255
3778
|
}
|
|
@@ -3362,8 +3885,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
3362
3885
|
return [...reached.values()].sort(byGeneratedAt);
|
|
3363
3886
|
}
|
|
3364
3887
|
function byGeneratedAt(left, right) {
|
|
3365
|
-
const
|
|
3366
|
-
return
|
|
3888
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
3889
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
3367
3890
|
}
|
|
3368
3891
|
|
|
3369
3892
|
// src/commands/anchor-resolve.ts
|
|
@@ -3373,6 +3896,9 @@ import { z as z9 } from "zod";
|
|
|
3373
3896
|
import { z as z8 } from "zod";
|
|
3374
3897
|
var bundlePath = z8.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
3375
3898
|
var conceptId = z8.string().min(1).describe("e.g. decision.cursor-v2");
|
|
3899
|
+
var TAGS = z8.array(z8.string().min(1)).optional().describe(
|
|
3900
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
3901
|
+
);
|
|
3376
3902
|
var REPO_ROOT = z8.string().min(1).optional().describe(
|
|
3377
3903
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
3378
3904
|
);
|
|
@@ -3386,14 +3912,49 @@ function argvFlag(argv, name) {
|
|
|
3386
3912
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
3387
3913
|
return value2;
|
|
3388
3914
|
}
|
|
3389
|
-
const
|
|
3390
|
-
if (
|
|
3391
|
-
const value = argv[
|
|
3915
|
+
const at2 = argv.indexOf(name);
|
|
3916
|
+
if (at2 === -1) return void 0;
|
|
3917
|
+
const value = argv[at2 + 1];
|
|
3392
3918
|
if (value === void 0 || value.startsWith("--")) {
|
|
3393
3919
|
throw new KbMissingFlagValueError(name);
|
|
3394
3920
|
}
|
|
3395
3921
|
return value;
|
|
3396
3922
|
}
|
|
3923
|
+
function argvFlags(argv, name) {
|
|
3924
|
+
const values = [];
|
|
3925
|
+
for (const [at2, arg] of argv.entries()) {
|
|
3926
|
+
if (arg.startsWith(`${name}=`)) {
|
|
3927
|
+
const value = arg.slice(name.length + 1);
|
|
3928
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
3929
|
+
values.push(value);
|
|
3930
|
+
} else if (arg === name) {
|
|
3931
|
+
const value = argv[at2 + 1];
|
|
3932
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
3933
|
+
throw new KbMissingFlagValueError(name);
|
|
3934
|
+
}
|
|
3935
|
+
values.push(value);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
return values;
|
|
3939
|
+
}
|
|
3940
|
+
function argvWithout(argv, ...names) {
|
|
3941
|
+
const kept = [];
|
|
3942
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
3943
|
+
const arg = argv[at2];
|
|
3944
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
3945
|
+
if (names.includes(arg)) {
|
|
3946
|
+
at2 += 1;
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
kept.push(arg);
|
|
3950
|
+
}
|
|
3951
|
+
return kept;
|
|
3952
|
+
}
|
|
3953
|
+
function argvPositional(argv, ...names) {
|
|
3954
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
3955
|
+
(arg) => !arg.startsWith("--")
|
|
3956
|
+
);
|
|
3957
|
+
}
|
|
3397
3958
|
|
|
3398
3959
|
// src/commands/anchor-resolve.ts
|
|
3399
3960
|
function resolverSummary(results) {
|
|
@@ -3481,11 +4042,14 @@ var anchorResolveCommand = define({
|
|
|
3481
4042
|
}
|
|
3482
4043
|
const resolved = outcome.span;
|
|
3483
4044
|
const producedBy = outcome.resolver;
|
|
3484
|
-
const currentHash =
|
|
4045
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
3485
4046
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
4047
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
4048
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
3486
4049
|
const stamped = {
|
|
3487
4050
|
...anchor,
|
|
3488
|
-
hash:
|
|
4051
|
+
hash: stampedHash,
|
|
4052
|
+
hash_kind: stampedKind,
|
|
3489
4053
|
lines: currentLines,
|
|
3490
4054
|
resolved_at: now(),
|
|
3491
4055
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -3495,7 +4059,8 @@ var anchorResolveCommand = define({
|
|
|
3495
4059
|
results.push({
|
|
3496
4060
|
...base2,
|
|
3497
4061
|
state: "stamped",
|
|
3498
|
-
currentHash,
|
|
4062
|
+
currentHash: stampedHash,
|
|
4063
|
+
hashKind: stampedKind,
|
|
3499
4064
|
...producedBy ? { resolver: producedBy } : {}
|
|
3500
4065
|
});
|
|
3501
4066
|
updated.push(stamped);
|
|
@@ -3507,6 +4072,7 @@ var anchorResolveCommand = define({
|
|
|
3507
4072
|
...base2,
|
|
3508
4073
|
state: "drifted",
|
|
3509
4074
|
currentHash,
|
|
4075
|
+
hashKind: kind,
|
|
3510
4076
|
diffSize: lineDelta(anchor, currentLines),
|
|
3511
4077
|
...producedBy ? { resolver: producedBy } : {},
|
|
3512
4078
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
@@ -3535,6 +4101,7 @@ var anchorResolveCommand = define({
|
|
|
3535
4101
|
...base2,
|
|
3536
4102
|
state: "match",
|
|
3537
4103
|
currentHash,
|
|
4104
|
+
hashKind: kind,
|
|
3538
4105
|
...producedBy ? { resolver: producedBy } : {},
|
|
3539
4106
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
3540
4107
|
});
|
|
@@ -3704,25 +4271,39 @@ import { z as z12 } from "zod";
|
|
|
3704
4271
|
var catalogCommand = define({
|
|
3705
4272
|
name: "catalog",
|
|
3706
4273
|
tool: "kb_catalog",
|
|
3707
|
-
usage: "catalog [type]",
|
|
4274
|
+
usage: "catalog [type] [--tag T]...",
|
|
3708
4275
|
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.",
|
|
3709
4276
|
input: z12.object({
|
|
3710
4277
|
bundlePath,
|
|
3711
|
-
type: z12.enum(KB_RECORD_TYPES).optional()
|
|
4278
|
+
type: z12.enum(KB_RECORD_TYPES).optional(),
|
|
4279
|
+
tags: TAGS
|
|
3712
4280
|
}),
|
|
3713
|
-
fromArgv: (argv, path) =>
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
4281
|
+
fromArgv: (argv, path) => {
|
|
4282
|
+
const tags = argvFlags(argv, "--tag");
|
|
4283
|
+
const type = argvPositional(argv, "--tag");
|
|
4284
|
+
return {
|
|
4285
|
+
bundlePath: path,
|
|
4286
|
+
...type ? { type } : {},
|
|
4287
|
+
...tags.length ? { tags } : {}
|
|
4288
|
+
};
|
|
4289
|
+
},
|
|
4290
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => render(
|
|
4291
|
+
await store.catalog(path, {
|
|
4292
|
+
...type ? { type } : {},
|
|
4293
|
+
...tags ? { tags } : {}
|
|
4294
|
+
}),
|
|
3719
4295
|
path,
|
|
3720
|
-
type
|
|
4296
|
+
type,
|
|
4297
|
+
tags
|
|
3721
4298
|
)
|
|
3722
4299
|
});
|
|
3723
|
-
function render(result, bundle, type) {
|
|
4300
|
+
function render(result, bundle, type, tags) {
|
|
4301
|
+
const narrowed = [
|
|
4302
|
+
...type ? [type] : [],
|
|
4303
|
+
...tags?.length ? [`tags: ${tags.join(", ")}`] : []
|
|
4304
|
+
].join(" \xB7 ");
|
|
3724
4305
|
const lines = [
|
|
3725
|
-
`# KB Catalog${
|
|
4306
|
+
`# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
|
|
3726
4307
|
`bundle: ${bundle}`,
|
|
3727
4308
|
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
3728
4309
|
];
|
|
@@ -3734,7 +4315,7 @@ function render(result, bundle, type) {
|
|
|
3734
4315
|
lines.push("");
|
|
3735
4316
|
if (!result.entries.length) {
|
|
3736
4317
|
lines.push(
|
|
3737
|
-
|
|
4318
|
+
narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
|
|
3738
4319
|
);
|
|
3739
4320
|
} else {
|
|
3740
4321
|
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
@@ -3767,7 +4348,7 @@ import { z as z13 } from "zod";
|
|
|
3767
4348
|
var contextCommand = define({
|
|
3768
4349
|
name: "context",
|
|
3769
4350
|
tool: "kb_context",
|
|
3770
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
4351
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
3771
4352
|
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.",
|
|
3772
4353
|
input: z13.object({
|
|
3773
4354
|
budgetTokens: z13.number().int().positive().optional().describe(
|
|
@@ -3779,6 +4360,9 @@ var contextCommand = define({
|
|
|
3779
4360
|
profile: z13.string().optional().describe(
|
|
3780
4361
|
"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."
|
|
3781
4362
|
),
|
|
4363
|
+
excludeTags: z13.array(z13.string().min(1)).optional().describe(
|
|
4364
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
4365
|
+
),
|
|
3782
4366
|
format: z13.enum(["markdown", "json"]).optional().describe(
|
|
3783
4367
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3784
4368
|
),
|
|
@@ -3792,19 +4376,22 @@ var contextCommand = define({
|
|
|
3792
4376
|
const profile = argvFlag(argv, "--profile");
|
|
3793
4377
|
const format = argvFlag(argv, "--format");
|
|
3794
4378
|
const event = argvFlag(argv, "--event");
|
|
4379
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
3795
4380
|
return {
|
|
3796
4381
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
3797
4382
|
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
3798
4383
|
...profile ? { profile } : {},
|
|
4384
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
3799
4385
|
...format ? { format } : {},
|
|
3800
4386
|
...event ? { event } : {}
|
|
3801
4387
|
};
|
|
3802
4388
|
},
|
|
3803
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
|
|
4389
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
3804
4390
|
const result = await buildContext(store, process.cwd(), {
|
|
3805
4391
|
...budgetTokens ? { budgetTokens } : {},
|
|
3806
4392
|
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
3807
4393
|
...profile ? { profile } : {},
|
|
4394
|
+
...excludeTags ? { excludeTags } : {},
|
|
3808
4395
|
// Degradations — a full pin that could not fit, a refused block — go
|
|
3809
4396
|
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
3810
4397
|
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
@@ -3818,14 +4405,168 @@ var contextCommand = define({
|
|
|
3818
4405
|
});
|
|
3819
4406
|
|
|
3820
4407
|
// src/commands/doctor.ts
|
|
4408
|
+
import { z as z15 } from "zod";
|
|
4409
|
+
|
|
4410
|
+
// src/commands/reassess.ts
|
|
3821
4411
|
import { z as z14 } from "zod";
|
|
3822
|
-
var
|
|
4412
|
+
var reassessCommand = define({
|
|
4413
|
+
name: "reassess",
|
|
4414
|
+
tool: "kb_reassess",
|
|
4415
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4416
|
+
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.",
|
|
4417
|
+
input: z14.object({
|
|
4418
|
+
bundlePath,
|
|
4419
|
+
conceptId,
|
|
4420
|
+
repoRoot: REPO_ROOT,
|
|
4421
|
+
withDiff: z14.boolean().optional().describe(
|
|
4422
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4423
|
+
)
|
|
4424
|
+
}),
|
|
4425
|
+
fromArgv: (argv, path) => {
|
|
4426
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4427
|
+
return {
|
|
4428
|
+
bundlePath: path,
|
|
4429
|
+
conceptId: argv[1],
|
|
4430
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4431
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4432
|
+
};
|
|
4433
|
+
},
|
|
4434
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
4435
|
+
const root = repoRoot ?? process.cwd();
|
|
4436
|
+
const bundle = await store.list(path);
|
|
4437
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
4438
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
4439
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
4440
|
+
const entries = drift?.get(id) ?? [];
|
|
4441
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
4442
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
4443
|
+
}
|
|
4444
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
4445
|
+
(hit) => hit.record.conceptId === id
|
|
4446
|
+
)?.standing;
|
|
4447
|
+
const impact2 = await store.impact(path, id);
|
|
4448
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
4449
|
+
...withDiff ? { withDiff: true } : {},
|
|
4450
|
+
impact: impact2,
|
|
4451
|
+
...standing ? { standing } : {}
|
|
4452
|
+
});
|
|
4453
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
4454
|
+
let frozen = false;
|
|
4455
|
+
const rebaselined = [];
|
|
4456
|
+
if (moves.length) {
|
|
4457
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
4458
|
+
for (const found of moves) {
|
|
4459
|
+
const to = found.entry.movedTo;
|
|
4460
|
+
if (!to) continue;
|
|
4461
|
+
relocated.set(found.anchor, {
|
|
4462
|
+
...found.anchor,
|
|
4463
|
+
file: to.file,
|
|
4464
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
4465
|
+
});
|
|
4466
|
+
rebaselined.push({
|
|
4467
|
+
file: found.anchor.file,
|
|
4468
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
4469
|
+
toFile: to.file,
|
|
4470
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
4471
|
+
});
|
|
4472
|
+
}
|
|
4473
|
+
try {
|
|
4474
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
4475
|
+
} catch (error) {
|
|
4476
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
4477
|
+
frozen = true;
|
|
4478
|
+
}
|
|
4479
|
+
if (!frozen) {
|
|
4480
|
+
await store.updateAnchors(
|
|
4481
|
+
path,
|
|
4482
|
+
id,
|
|
4483
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
4484
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
4485
|
+
),
|
|
4486
|
+
actor
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
return {
|
|
4491
|
+
conceptId: id,
|
|
4492
|
+
packet,
|
|
4493
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
4494
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
4495
|
+
...frozen ? {
|
|
4496
|
+
frozen: true,
|
|
4497
|
+
note: "base is frozen: nothing was rebaselined"
|
|
4498
|
+
} : {}
|
|
4499
|
+
};
|
|
4500
|
+
},
|
|
4501
|
+
render: (result) => renderReassess(result)
|
|
4502
|
+
});
|
|
4503
|
+
function renderReassess(result) {
|
|
4504
|
+
const lines = [];
|
|
4505
|
+
for (const move of result.rebaselined) {
|
|
4506
|
+
lines.push(
|
|
4507
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
if (result.cosmetic) {
|
|
4511
|
+
lines.push(
|
|
4512
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
4513
|
+
);
|
|
4514
|
+
}
|
|
4515
|
+
if (result.note) lines.push(result.note);
|
|
4516
|
+
const packet = result.packet;
|
|
4517
|
+
if (!packet) {
|
|
4518
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
4519
|
+
return lines.join("\n");
|
|
4520
|
+
}
|
|
4521
|
+
lines.push(
|
|
4522
|
+
"",
|
|
4523
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
4524
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
4525
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
4526
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
4527
|
+
"",
|
|
4528
|
+
`## Anchors (${packet.anchors.length})`
|
|
4529
|
+
);
|
|
4530
|
+
for (const anchor of packet.anchors) {
|
|
4531
|
+
lines.push(
|
|
4532
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
4533
|
+
);
|
|
4534
|
+
if (!anchor.diff) continue;
|
|
4535
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
4536
|
+
lines.push(
|
|
4537
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
4538
|
+
);
|
|
4539
|
+
continue;
|
|
4540
|
+
}
|
|
4541
|
+
lines.push(
|
|
4542
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
4543
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
if (packet.impact.length) {
|
|
4547
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
4548
|
+
for (const entry of packet.impact) {
|
|
4549
|
+
lines.push(
|
|
4550
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
4551
|
+
);
|
|
4552
|
+
}
|
|
4553
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
4554
|
+
}
|
|
4555
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
4556
|
+
return lines.join("\n");
|
|
4557
|
+
}
|
|
4558
|
+
function at(file, symbol) {
|
|
4559
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
// src/commands/doctor.ts
|
|
4563
|
+
var days = (what, fallback) => z15.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3823
4564
|
var doctorCommand = define({
|
|
3824
4565
|
name: "doctor",
|
|
3825
4566
|
tool: "kb_doctor",
|
|
3826
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3827
|
-
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.
|
|
3828
|
-
input:
|
|
4567
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4568
|
+
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.",
|
|
4569
|
+
input: z15.object({
|
|
3829
4570
|
bundlePath,
|
|
3830
4571
|
repoRoot: REPO_ROOT,
|
|
3831
4572
|
expiringDays: days(
|
|
@@ -3840,11 +4581,17 @@ var doctorCommand = define({
|
|
|
3840
4581
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3841
4582
|
DEFAULT_AGING_DAYS
|
|
3842
4583
|
),
|
|
3843
|
-
offline:
|
|
4584
|
+
offline: z15.boolean().optional().describe(
|
|
3844
4585
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3845
4586
|
),
|
|
3846
|
-
strict:
|
|
4587
|
+
strict: z15.boolean().optional().describe(
|
|
3847
4588
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4589
|
+
),
|
|
4590
|
+
drifted: z15.boolean().optional().describe(
|
|
4591
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4592
|
+
),
|
|
4593
|
+
withDiff: z15.boolean().optional().describe(
|
|
4594
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
3848
4595
|
)
|
|
3849
4596
|
}),
|
|
3850
4597
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -3863,7 +4610,9 @@ var doctorCommand = define({
|
|
|
3863
4610
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3864
4611
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3865
4612
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
3866
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
4613
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
4614
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
4615
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
3867
4616
|
};
|
|
3868
4617
|
},
|
|
3869
4618
|
run: async ({ store, now }, {
|
|
@@ -3872,7 +4621,9 @@ var doctorCommand = define({
|
|
|
3872
4621
|
unverifiedDays,
|
|
3873
4622
|
agingDays,
|
|
3874
4623
|
repoRoot,
|
|
3875
|
-
offline
|
|
4624
|
+
offline,
|
|
4625
|
+
drifted: drifted2,
|
|
4626
|
+
withDiff
|
|
3876
4627
|
}) => {
|
|
3877
4628
|
const checkedAt = now();
|
|
3878
4629
|
const records = await store.list(path);
|
|
@@ -3887,10 +4638,51 @@ var doctorCommand = define({
|
|
|
3887
4638
|
now: new Date(checkedAt)
|
|
3888
4639
|
});
|
|
3889
4640
|
const hints = grammarHints();
|
|
4641
|
+
if (!drifted2) {
|
|
4642
|
+
return {
|
|
4643
|
+
bundlePath: path,
|
|
4644
|
+
checkedAt,
|
|
4645
|
+
...report,
|
|
4646
|
+
...hints.length ? { hints } : {}
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
const standings = new Map(
|
|
4650
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4651
|
+
hit.record.conceptId,
|
|
4652
|
+
hit.standing
|
|
4653
|
+
])
|
|
4654
|
+
);
|
|
4655
|
+
const packets = [];
|
|
4656
|
+
const rebaselinable = [];
|
|
4657
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
4658
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
4659
|
+
const record = records.find(
|
|
4660
|
+
(entry) => entry.conceptId === found.conceptId
|
|
4661
|
+
);
|
|
4662
|
+
if (!record) continue;
|
|
4663
|
+
const standing = standings.get(record.conceptId);
|
|
4664
|
+
const built = await reassessPacket(
|
|
4665
|
+
repoRoot ?? process.cwd(),
|
|
4666
|
+
record,
|
|
4667
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
4668
|
+
{
|
|
4669
|
+
...withDiff ? { withDiff: true } : {},
|
|
4670
|
+
impact: await store.impact(path, record.conceptId),
|
|
4671
|
+
...standing ? { standing } : {},
|
|
4672
|
+
search
|
|
4673
|
+
}
|
|
4674
|
+
);
|
|
4675
|
+
if (built.packet) packets.push(built.packet);
|
|
4676
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
4677
|
+
rebaselinable.push(record.conceptId);
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
3890
4680
|
return {
|
|
3891
4681
|
bundlePath: path,
|
|
3892
4682
|
checkedAt,
|
|
3893
4683
|
...report,
|
|
4684
|
+
packets,
|
|
4685
|
+
rebaselinable,
|
|
3894
4686
|
...hints.length ? { hints } : {}
|
|
3895
4687
|
};
|
|
3896
4688
|
},
|
|
@@ -3904,6 +4696,7 @@ var doctorCommand = define({
|
|
|
3904
4696
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
3905
4697
|
});
|
|
3906
4698
|
function render2(result) {
|
|
4699
|
+
if (result.packets) return renderPackets(result);
|
|
3907
4700
|
const { thresholds } = result;
|
|
3908
4701
|
const lines = [
|
|
3909
4702
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -3937,21 +4730,45 @@ function render2(result) {
|
|
|
3937
4730
|
);
|
|
3938
4731
|
return lines.join("\n");
|
|
3939
4732
|
}
|
|
4733
|
+
function renderPackets(result) {
|
|
4734
|
+
const packets = result.packets ?? [];
|
|
4735
|
+
const lines = [
|
|
4736
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
4737
|
+
`checked: ${result.checkedAt}`,
|
|
4738
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
4739
|
+
];
|
|
4740
|
+
if (result.rebaselinable?.length) {
|
|
4741
|
+
lines.push(
|
|
4742
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
4743
|
+
);
|
|
4744
|
+
}
|
|
4745
|
+
for (const packet of packets) {
|
|
4746
|
+
lines.push(
|
|
4747
|
+
renderReassess({
|
|
4748
|
+
conceptId: packet.conceptId,
|
|
4749
|
+
packet,
|
|
4750
|
+
rebaselined: [],
|
|
4751
|
+
cosmetic: 0
|
|
4752
|
+
})
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
return lines.join("\n");
|
|
4756
|
+
}
|
|
3940
4757
|
|
|
3941
4758
|
// src/commands/impact.ts
|
|
3942
|
-
import { z as
|
|
4759
|
+
import { z as z16 } from "zod";
|
|
3943
4760
|
var impactCommand = define({
|
|
3944
4761
|
name: "impact",
|
|
3945
4762
|
tool: "kb_impact",
|
|
3946
4763
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3947
4764
|
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.",
|
|
3948
|
-
input:
|
|
4765
|
+
input: z16.object({
|
|
3949
4766
|
bundlePath,
|
|
3950
4767
|
conceptId,
|
|
3951
|
-
depth:
|
|
4768
|
+
depth: z16.number().int().positive().optional().describe(
|
|
3952
4769
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3953
4770
|
),
|
|
3954
|
-
rels:
|
|
4771
|
+
rels: z16.array(z16.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3955
4772
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3956
4773
|
)
|
|
3957
4774
|
}),
|
|
@@ -3972,35 +4789,49 @@ var impactCommand = define({
|
|
|
3972
4789
|
});
|
|
3973
4790
|
|
|
3974
4791
|
// src/commands/list.ts
|
|
3975
|
-
import { z as
|
|
4792
|
+
import { z as z17 } from "zod";
|
|
3976
4793
|
var listCommand = define({
|
|
3977
4794
|
name: "list",
|
|
3978
4795
|
tool: "kb_list",
|
|
3979
|
-
usage: "list [type]",
|
|
3980
|
-
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3981
|
-
input:
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4796
|
+
usage: "list [type] [--tag T]...",
|
|
4797
|
+
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
4798
|
+
input: z17.object({
|
|
4799
|
+
bundlePath,
|
|
4800
|
+
type: z17.enum(KB_RECORD_TYPES).optional(),
|
|
4801
|
+
tags: TAGS
|
|
4802
|
+
}),
|
|
4803
|
+
fromArgv: (argv, path) => {
|
|
4804
|
+
const tags = argvFlags(argv, "--tag");
|
|
4805
|
+
const type = argvPositional(argv, "--tag");
|
|
4806
|
+
return {
|
|
4807
|
+
bundlePath: path,
|
|
4808
|
+
...type ? { type } : {},
|
|
4809
|
+
...tags.length ? { tags } : {}
|
|
4810
|
+
};
|
|
4811
|
+
},
|
|
4812
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
|
|
4813
|
+
(record) => ({
|
|
4814
|
+
conceptId: record.conceptId,
|
|
4815
|
+
title: record.frontmatter.title ?? null,
|
|
4816
|
+
description: record.frontmatter.description ?? null,
|
|
4817
|
+
status: record.frontmatter.strauss_status,
|
|
4818
|
+
anchors: record.frontmatter.strauss_anchors ?? []
|
|
4819
|
+
})
|
|
4820
|
+
)
|
|
3990
4821
|
});
|
|
3991
4822
|
|
|
3992
4823
|
// src/commands/load.ts
|
|
3993
|
-
import { z as
|
|
4824
|
+
import { z as z18 } from "zod";
|
|
3994
4825
|
var loadCommand = define({
|
|
3995
4826
|
name: "load",
|
|
3996
4827
|
tool: "kb_load",
|
|
3997
4828
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3998
4829
|
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.",
|
|
3999
|
-
input:
|
|
4830
|
+
input: z18.object({
|
|
4000
4831
|
bundlePath,
|
|
4001
|
-
type:
|
|
4002
|
-
budgetTokens:
|
|
4003
|
-
all:
|
|
4832
|
+
type: z18.enum(KB_RECORD_TYPES).optional(),
|
|
4833
|
+
budgetTokens: z18.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4834
|
+
all: z18.boolean().optional().describe(
|
|
4004
4835
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4005
4836
|
),
|
|
4006
4837
|
repoRoot: REPO_ROOT
|
|
@@ -4042,25 +4873,25 @@ var loadCommand = define({
|
|
|
4042
4873
|
});
|
|
4043
4874
|
|
|
4044
4875
|
// src/commands/log.ts
|
|
4045
|
-
import { z as
|
|
4876
|
+
import { z as z19 } from "zod";
|
|
4046
4877
|
var logCommand = define({
|
|
4047
4878
|
name: "log",
|
|
4048
4879
|
tool: "kb_log",
|
|
4049
4880
|
usage: "log",
|
|
4050
4881
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4051
|
-
input:
|
|
4882
|
+
input: z19.object({ bundlePath }),
|
|
4052
4883
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4053
4884
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4054
4885
|
});
|
|
4055
4886
|
|
|
4056
4887
|
// src/commands/no-decision.ts
|
|
4057
|
-
import { z as
|
|
4888
|
+
import { z as z20 } from "zod";
|
|
4058
4889
|
var noDecisionCommand = define({
|
|
4059
4890
|
name: "no-decision",
|
|
4060
4891
|
tool: "kb_no_decision",
|
|
4061
4892
|
usage: "no-decision <reason...>",
|
|
4062
4893
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4063
|
-
input:
|
|
4894
|
+
input: z20.object({ bundlePath, reason: z20.string().min(1) }),
|
|
4064
4895
|
fromArgv: (argv, path) => ({
|
|
4065
4896
|
bundlePath: path,
|
|
4066
4897
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4077,20 +4908,20 @@ var noDecisionCommand = define({
|
|
|
4077
4908
|
});
|
|
4078
4909
|
|
|
4079
4910
|
// src/commands/pack.ts
|
|
4080
|
-
import { z as
|
|
4911
|
+
import { z as z21 } from "zod";
|
|
4081
4912
|
var packCommand = define({
|
|
4082
4913
|
name: "pack",
|
|
4083
4914
|
tool: "kb_pack",
|
|
4084
4915
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4085
4916
|
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.",
|
|
4086
|
-
input:
|
|
4917
|
+
input: z21.object({
|
|
4087
4918
|
bundlePath,
|
|
4088
4919
|
conceptId,
|
|
4089
|
-
hops:
|
|
4090
|
-
maxNodes:
|
|
4920
|
+
hops: z21.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4921
|
+
maxNodes: z21.number().int().positive().optional().describe(
|
|
4091
4922
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4092
4923
|
),
|
|
4093
|
-
budgetTokens:
|
|
4924
|
+
budgetTokens: z21.number().int().positive().optional().describe(
|
|
4094
4925
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4095
4926
|
)
|
|
4096
4927
|
}),
|
|
@@ -4115,12 +4946,12 @@ var packCommand = define({
|
|
|
4115
4946
|
return render3(result, path, now());
|
|
4116
4947
|
}
|
|
4117
4948
|
});
|
|
4118
|
-
function render3(result, bundle,
|
|
4949
|
+
function render3(result, bundle, at2) {
|
|
4119
4950
|
const lines = [
|
|
4120
4951
|
`# KB Pack \u2014 ${result.root}`,
|
|
4121
4952
|
`bundle: ${bundle}`,
|
|
4122
4953
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
4123
|
-
`packed: ${
|
|
4954
|
+
`packed: ${at2}`,
|
|
4124
4955
|
"",
|
|
4125
4956
|
`## Records (${result.records.length})`
|
|
4126
4957
|
];
|
|
@@ -4177,22 +5008,22 @@ function warningLabel(warning) {
|
|
|
4177
5008
|
}
|
|
4178
5009
|
|
|
4179
5010
|
// src/commands/pin.ts
|
|
4180
|
-
import { z as
|
|
5011
|
+
import { z as z22 } from "zod";
|
|
4181
5012
|
var pinCommand = define({
|
|
4182
5013
|
name: "pin",
|
|
4183
5014
|
tool: "kb_pin",
|
|
4184
5015
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4185
5016
|
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.",
|
|
4186
|
-
input:
|
|
5017
|
+
input: z22.object({
|
|
4187
5018
|
bundlePath,
|
|
4188
|
-
mode:
|
|
5019
|
+
mode: z22.enum(["full", "index"]).optional().describe(
|
|
4189
5020
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
4190
5021
|
),
|
|
4191
|
-
profiles:
|
|
4192
|
-
layer:
|
|
5022
|
+
profiles: z22.array(z22.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
5023
|
+
layer: z22.enum(["project", "local", "user"]).optional().describe(
|
|
4193
5024
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4194
5025
|
),
|
|
4195
|
-
frozen:
|
|
5026
|
+
frozen: z22.boolean().optional().describe(
|
|
4196
5027
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4197
5028
|
)
|
|
4198
5029
|
}),
|
|
@@ -4221,47 +5052,49 @@ var pinCommand = define({
|
|
|
4221
5052
|
});
|
|
4222
5053
|
|
|
4223
5054
|
// src/commands/pins.ts
|
|
4224
|
-
import { z as
|
|
5055
|
+
import { z as z23 } from "zod";
|
|
4225
5056
|
var pinsCommand = define({
|
|
4226
5057
|
name: "pins",
|
|
4227
5058
|
tool: "kb_pins",
|
|
4228
5059
|
usage: "pins",
|
|
4229
5060
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4230
|
-
input:
|
|
5061
|
+
input: z23.object({}),
|
|
4231
5062
|
fromArgv: () => ({}),
|
|
4232
5063
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4233
5064
|
});
|
|
4234
5065
|
|
|
4235
5066
|
// src/commands/query.ts
|
|
4236
|
-
import { z as
|
|
5067
|
+
import { z as z24 } from "zod";
|
|
4237
5068
|
var queryCommand = define({
|
|
4238
5069
|
name: "query",
|
|
4239
5070
|
tool: "kb_query",
|
|
4240
|
-
usage: "query <text...> [--repo-root PATH]",
|
|
5071
|
+
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
4241
5072
|
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.",
|
|
4242
|
-
input:
|
|
5073
|
+
input: z24.object({
|
|
4243
5074
|
bundlePath,
|
|
4244
|
-
text:
|
|
4245
|
-
type:
|
|
4246
|
-
includeNonCurrent:
|
|
5075
|
+
text: z24.string().optional(),
|
|
5076
|
+
type: z24.enum(KB_RECORD_TYPES).optional(),
|
|
5077
|
+
includeNonCurrent: z24.boolean().optional(),
|
|
5078
|
+
tags: TAGS,
|
|
4247
5079
|
repoRoot: REPO_ROOT
|
|
4248
5080
|
}),
|
|
4249
|
-
//
|
|
5081
|
+
// Both are flags, so neither's value may fall into the search text.
|
|
4250
5082
|
fromArgv: (argv, path) => {
|
|
4251
5083
|
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4252
|
-
const
|
|
4253
|
-
const
|
|
4254
|
-
if (flag !== -1) words.splice(flag, 2);
|
|
5084
|
+
const tags = argvFlags(argv, "--tag");
|
|
5085
|
+
const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
|
|
4255
5086
|
return {
|
|
4256
5087
|
bundlePath: path,
|
|
4257
5088
|
text: words.join(" ").trim(),
|
|
4258
5089
|
includeNonCurrent: true,
|
|
5090
|
+
...tags.length ? { tags } : {},
|
|
4259
5091
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4260
5092
|
};
|
|
4261
5093
|
},
|
|
4262
|
-
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
5094
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
|
|
4263
5095
|
...type ? { type } : {},
|
|
4264
5096
|
includeNonCurrent: includeNonCurrent === true,
|
|
5097
|
+
...tags ? { tags } : {},
|
|
4265
5098
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4266
5099
|
})).map((hit) => ({
|
|
4267
5100
|
conceptId: hit.record.conceptId,
|
|
@@ -4275,43 +5108,43 @@ var queryCommand = define({
|
|
|
4275
5108
|
});
|
|
4276
5109
|
|
|
4277
5110
|
// src/commands/read-index.ts
|
|
4278
|
-
import { z as
|
|
5111
|
+
import { z as z25 } from "zod";
|
|
4279
5112
|
var readIndexCommand = define({
|
|
4280
5113
|
name: "index",
|
|
4281
5114
|
tool: "kb_index",
|
|
4282
5115
|
usage: "index",
|
|
4283
5116
|
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.",
|
|
4284
|
-
input:
|
|
5117
|
+
input: z25.object({ bundlePath }),
|
|
4285
5118
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4286
5119
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4287
5120
|
});
|
|
4288
5121
|
|
|
4289
5122
|
// src/commands/schema.ts
|
|
4290
|
-
import { z as
|
|
5123
|
+
import { z as z26 } from "zod";
|
|
4291
5124
|
var schemaCommand = define({
|
|
4292
5125
|
name: "schema",
|
|
4293
5126
|
tool: "kb_schema",
|
|
4294
5127
|
usage: "schema",
|
|
4295
5128
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4296
|
-
input:
|
|
5129
|
+
input: z26.object({}),
|
|
4297
5130
|
fromArgv: () => ({}),
|
|
4298
5131
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4299
5132
|
});
|
|
4300
5133
|
|
|
4301
5134
|
// src/commands/stamp.ts
|
|
4302
5135
|
import { readFile as readFile6 } from "fs/promises";
|
|
4303
|
-
import { z as
|
|
5136
|
+
import { z as z27 } from "zod";
|
|
4304
5137
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4305
5138
|
var stampCommand = define({
|
|
4306
5139
|
name: "stamp",
|
|
4307
5140
|
tool: "kb_stamp",
|
|
4308
5141
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
4309
|
-
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
|
|
4310
|
-
input:
|
|
4311
|
-
bundlePath:
|
|
5142
|
+
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.",
|
|
5143
|
+
input: z27.object({
|
|
5144
|
+
bundlePath: z27.string().min(1).optional().describe(
|
|
4312
5145
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
4313
5146
|
),
|
|
4314
|
-
since:
|
|
5147
|
+
since: z27.string().min(1).optional().describe(
|
|
4315
5148
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
4316
5149
|
)
|
|
4317
5150
|
}),
|
|
@@ -4350,7 +5183,7 @@ var stampCommand = define({
|
|
|
4350
5183
|
return reports;
|
|
4351
5184
|
},
|
|
4352
5185
|
render: (result) => result.map((report) => {
|
|
4353
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5186
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
4354
5187
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
4355
5188
|
return report.changed?.length ? `${head}
|
|
4356
5189
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -4397,16 +5230,16 @@ async function readBaseline(since) {
|
|
|
4397
5230
|
}
|
|
4398
5231
|
|
|
4399
5232
|
// src/commands/status.ts
|
|
4400
|
-
import { z as
|
|
5233
|
+
import { z as z28 } from "zod";
|
|
4401
5234
|
var statusCommand = define({
|
|
4402
5235
|
name: "status",
|
|
4403
5236
|
tool: "kb_status",
|
|
4404
5237
|
usage: "status <concept-id> <status>",
|
|
4405
5238
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
4406
|
-
input:
|
|
5239
|
+
input: z28.object({
|
|
4407
5240
|
bundlePath,
|
|
4408
5241
|
conceptId,
|
|
4409
|
-
status:
|
|
5242
|
+
status: z28.enum(KB_RECORD_STATUSES)
|
|
4410
5243
|
}),
|
|
4411
5244
|
fromArgv: (argv, path) => ({
|
|
4412
5245
|
bundlePath: path,
|
|
@@ -4421,13 +5254,13 @@ var statusCommand = define({
|
|
|
4421
5254
|
});
|
|
4422
5255
|
|
|
4423
5256
|
// src/commands/supersede.ts
|
|
4424
|
-
import { z as
|
|
5257
|
+
import { z as z29 } from "zod";
|
|
4425
5258
|
var supersedeCommand = define({
|
|
4426
5259
|
name: "supersede",
|
|
4427
5260
|
tool: "kb_supersede",
|
|
4428
5261
|
usage: "supersede <concept-id> <replacement-id>",
|
|
4429
5262
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
4430
|
-
input:
|
|
5263
|
+
input: z29.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
4431
5264
|
fromArgv: (argv, path) => ({
|
|
4432
5265
|
bundlePath: path,
|
|
4433
5266
|
conceptId: argv[1],
|
|
@@ -4441,16 +5274,16 @@ var supersedeCommand = define({
|
|
|
4441
5274
|
});
|
|
4442
5275
|
|
|
4443
5276
|
// src/commands/sync-instructions.ts
|
|
4444
|
-
import { z as
|
|
5277
|
+
import { z as z30 } from "zod";
|
|
4445
5278
|
var syncInstructionsCommand = define({
|
|
4446
5279
|
name: "sync-instructions",
|
|
4447
5280
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
4448
5281
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
4449
|
-
input:
|
|
4450
|
-
file:
|
|
4451
|
-
budgetTokens:
|
|
4452
|
-
fullUnderTokens:
|
|
4453
|
-
profile:
|
|
5282
|
+
input: z30.object({
|
|
5283
|
+
file: z30.string().min(1).describe("The instruction file to edit in place."),
|
|
5284
|
+
budgetTokens: z30.number().int().positive().optional(),
|
|
5285
|
+
fullUnderTokens: z30.number().int().positive().optional(),
|
|
5286
|
+
profile: z30.string().optional()
|
|
4454
5287
|
}),
|
|
4455
5288
|
fromArgv: (argv) => {
|
|
4456
5289
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -4476,17 +5309,17 @@ var syncInstructionsCommand = define({
|
|
|
4476
5309
|
});
|
|
4477
5310
|
|
|
4478
5311
|
// src/commands/trace.ts
|
|
4479
|
-
import { z as
|
|
5312
|
+
import { z as z31 } from "zod";
|
|
4480
5313
|
var traceCommand = define({
|
|
4481
5314
|
name: "trace",
|
|
4482
5315
|
tool: "kb_trace",
|
|
4483
5316
|
usage: "trace <concept-id> [edges...]",
|
|
4484
5317
|
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".',
|
|
4485
|
-
input:
|
|
5318
|
+
input: z31.object({
|
|
4486
5319
|
bundlePath,
|
|
4487
5320
|
conceptId,
|
|
4488
|
-
edges:
|
|
4489
|
-
depth:
|
|
5321
|
+
edges: z31.array(z31.enum(TRACE_EDGES)).optional(),
|
|
5322
|
+
depth: z31.number().int().positive().optional()
|
|
4490
5323
|
}),
|
|
4491
5324
|
fromArgv: (argv, path) => ({
|
|
4492
5325
|
bundlePath: path,
|
|
@@ -4508,37 +5341,37 @@ var traceCommand = define({
|
|
|
4508
5341
|
});
|
|
4509
5342
|
|
|
4510
5343
|
// src/commands/types.ts
|
|
4511
|
-
import { z as
|
|
5344
|
+
import { z as z32 } from "zod";
|
|
4512
5345
|
var typesCommand = define({
|
|
4513
5346
|
name: "types",
|
|
4514
5347
|
tool: "kb_types",
|
|
4515
5348
|
usage: "types",
|
|
4516
5349
|
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.",
|
|
4517
|
-
input:
|
|
5350
|
+
input: z32.object({}),
|
|
4518
5351
|
fromArgv: () => ({}),
|
|
4519
5352
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
4520
5353
|
});
|
|
4521
5354
|
|
|
4522
5355
|
// src/commands/unpin.ts
|
|
4523
|
-
import { z as
|
|
5356
|
+
import { z as z33 } from "zod";
|
|
4524
5357
|
var unpinCommand = define({
|
|
4525
5358
|
name: "unpin",
|
|
4526
5359
|
tool: "kb_unpin",
|
|
4527
5360
|
usage: "unpin [bundle-path]",
|
|
4528
5361
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
4529
|
-
input:
|
|
5362
|
+
input: z33.object({ bundlePath }),
|
|
4530
5363
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
4531
5364
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
4532
5365
|
});
|
|
4533
5366
|
|
|
4534
5367
|
// src/commands/validate.ts
|
|
4535
|
-
import { z as
|
|
5368
|
+
import { z as z34 } from "zod";
|
|
4536
5369
|
var validateCommand = define({
|
|
4537
5370
|
name: "validate",
|
|
4538
5371
|
tool: "kb_validate",
|
|
4539
5372
|
usage: "validate",
|
|
4540
5373
|
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.",
|
|
4541
|
-
input:
|
|
5374
|
+
input: z34.object({ bundlePath }),
|
|
4542
5375
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4543
5376
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
4544
5377
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -4548,16 +5381,16 @@ var validateCommand = define({
|
|
|
4548
5381
|
});
|
|
4549
5382
|
|
|
4550
5383
|
// src/commands/verify.ts
|
|
4551
|
-
import { z as
|
|
5384
|
+
import { z as z35 } from "zod";
|
|
4552
5385
|
var verifyCommand = define({
|
|
4553
5386
|
name: "verify",
|
|
4554
5387
|
tool: "kb_verify",
|
|
4555
5388
|
usage: "verify <concept-id> --note <text>",
|
|
4556
5389
|
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.",
|
|
4557
|
-
input:
|
|
5390
|
+
input: z35.object({
|
|
4558
5391
|
bundlePath,
|
|
4559
5392
|
conceptId,
|
|
4560
|
-
note:
|
|
5393
|
+
note: z35.string().refine((s) => s.trim().length > 0, {
|
|
4561
5394
|
message: "note must say what the check found"
|
|
4562
5395
|
})
|
|
4563
5396
|
}),
|
|
@@ -4577,15 +5410,15 @@ var verifyCommand = define({
|
|
|
4577
5410
|
});
|
|
4578
5411
|
|
|
4579
5412
|
// src/commands/write.ts
|
|
4580
|
-
import { z as
|
|
5413
|
+
import { z as z36 } from "zod";
|
|
4581
5414
|
var writeCommand = define({
|
|
4582
5415
|
name: "write",
|
|
4583
5416
|
tool: "kb_write",
|
|
4584
5417
|
usage: "write <type> < record.json",
|
|
4585
5418
|
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.",
|
|
4586
|
-
input:
|
|
5419
|
+
input: z36.object({
|
|
4587
5420
|
bundlePath,
|
|
4588
|
-
type:
|
|
5421
|
+
type: z36.enum(KB_RECORD_TYPES),
|
|
4589
5422
|
input: composeInputSchema
|
|
4590
5423
|
}),
|
|
4591
5424
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -4609,13 +5442,13 @@ var writeCommand = define({
|
|
|
4609
5442
|
});
|
|
4610
5443
|
|
|
4611
5444
|
// src/commands/write-decision.ts
|
|
4612
|
-
import { z as
|
|
5445
|
+
import { z as z37 } from "zod";
|
|
4613
5446
|
var writeDecisionCommand = define({
|
|
4614
5447
|
name: "write-decision",
|
|
4615
5448
|
tool: "kb_write_decision",
|
|
4616
5449
|
usage: "write-decision < decision.json",
|
|
4617
5450
|
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.",
|
|
4618
|
-
input:
|
|
5451
|
+
input: z37.object({ bundlePath, input: decisionInputSchema }),
|
|
4619
5452
|
fromArgv: async (_argv, path, stdin) => ({
|
|
4620
5453
|
bundlePath: path,
|
|
4621
5454
|
input: JSON.parse(await stdin())
|
|
@@ -4645,6 +5478,7 @@ var KB_COMMANDS = [
|
|
|
4645
5478
|
answerCommand,
|
|
4646
5479
|
verifyCommand,
|
|
4647
5480
|
anchorResolveCommand,
|
|
5481
|
+
reassessCommand,
|
|
4648
5482
|
loadCommand,
|
|
4649
5483
|
catalogCommand,
|
|
4650
5484
|
packCommand,
|
|
@@ -4696,7 +5530,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4696
5530
|
}
|
|
4697
5531
|
|
|
4698
5532
|
// src/search-index.ts
|
|
4699
|
-
import { stat as
|
|
5533
|
+
import { stat as stat3 } from "fs/promises";
|
|
4700
5534
|
import { join as join6 } from "path";
|
|
4701
5535
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4702
5536
|
var COLLECTION = "kb";
|
|
@@ -4741,7 +5575,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4741
5575
|
}
|
|
4742
5576
|
}
|
|
4743
5577
|
async function isStale(bundlePath2) {
|
|
4744
|
-
const indexAt = await
|
|
5578
|
+
const indexAt = await stat3(join6(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4745
5579
|
if (!indexAt) return true;
|
|
4746
5580
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4747
5581
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4750,8 +5584,8 @@ async function isStale(bundlePath2) {
|
|
|
4750
5584
|
let stale = false;
|
|
4751
5585
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4752
5586
|
if (stale) return;
|
|
4753
|
-
const
|
|
4754
|
-
if (
|
|
5587
|
+
const at2 = await stat3(join6(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5588
|
+
if (at2 > indexAt) stale = true;
|
|
4755
5589
|
});
|
|
4756
5590
|
return stale;
|
|
4757
5591
|
}
|
|
@@ -5066,13 +5900,16 @@ var KbStore = class {
|
|
|
5066
5900
|
return this.parse(conceptId2, raw);
|
|
5067
5901
|
}
|
|
5068
5902
|
/**
|
|
5069
|
-
* Every record in the bundle, optionally narrowed to one type
|
|
5903
|
+
* Every record in the bundle, optionally narrowed to one type and to the
|
|
5904
|
+
* records carrying every tag in `filter.tags`. Selection only — `excludeTags`
|
|
5905
|
+
* is not taken here, because `query`, `catalog` and `load` read through this
|
|
5906
|
+
* and must adjudicate over the whole base.
|
|
5070
5907
|
*
|
|
5071
5908
|
* A file that fails to parse is skipped and logged rather than thrown: one
|
|
5072
5909
|
* malformed record — hand-edited, or written by a producer we don't know —
|
|
5073
5910
|
* must not make the whole bundle unreadable.
|
|
5074
5911
|
*/
|
|
5075
|
-
async list(bundlePath2, type) {
|
|
5912
|
+
async list(bundlePath2, type, filter = {}) {
|
|
5076
5913
|
const root = this.root(bundlePath2);
|
|
5077
5914
|
let names;
|
|
5078
5915
|
try {
|
|
@@ -5086,7 +5923,9 @@ var KbStore = class {
|
|
|
5086
5923
|
DEFAULT_IO_CONCURRENCY,
|
|
5087
5924
|
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile7(join7(root, name), "utf8"))
|
|
5088
5925
|
);
|
|
5089
|
-
return records.filter(
|
|
5926
|
+
return records.filter(
|
|
5927
|
+
(record) => record !== null && matchesTags(record, filter)
|
|
5928
|
+
);
|
|
5090
5929
|
}
|
|
5091
5930
|
/**
|
|
5092
5931
|
* Moves a record's status, preserving everything else.
|
|
@@ -5132,8 +5971,8 @@ var KbStore = class {
|
|
|
5132
5971
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
5133
5972
|
* logs what it publishes.
|
|
5134
5973
|
*/
|
|
5135
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
5136
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
5974
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5975
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
5137
5976
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
5138
5977
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
5139
5978
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -5185,14 +6024,14 @@ var KbStore = class {
|
|
|
5185
6024
|
return superseded;
|
|
5186
6025
|
}
|
|
5187
6026
|
/** Resolves an open question, stamping who answered and when. */
|
|
5188
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
6027
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5189
6028
|
return this.mutate(
|
|
5190
6029
|
bundlePath2,
|
|
5191
6030
|
conceptId2,
|
|
5192
6031
|
(frontmatter) => ({
|
|
5193
6032
|
...frontmatter,
|
|
5194
6033
|
strauss_status: "resolved",
|
|
5195
|
-
strauss_answered: { by: actor, at }
|
|
6034
|
+
strauss_answered: { by: actor, at: at2 }
|
|
5196
6035
|
}),
|
|
5197
6036
|
{ operation: "answer", by: actor },
|
|
5198
6037
|
(body) => `${body.trimEnd()}
|
|
@@ -5226,9 +6065,10 @@ ${answer}
|
|
|
5226
6065
|
/* @__PURE__ */ new Date(),
|
|
5227
6066
|
await this.detectDrift(narrowed, options.repoRoot)
|
|
5228
6067
|
);
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
6068
|
+
const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
|
|
6069
|
+
if (options.includeNonCurrent) return kept;
|
|
6070
|
+
const present = new Set(kept.map((hit) => hit.record.conceptId));
|
|
6071
|
+
return kept.filter(
|
|
5232
6072
|
(hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
|
|
5233
6073
|
);
|
|
5234
6074
|
}
|
|
@@ -5331,14 +6171,17 @@ ${answer}
|
|
|
5331
6171
|
/* @__PURE__ */ new Date(),
|
|
5332
6172
|
await this.detectDrift(wanted, options.repoRoot)
|
|
5333
6173
|
);
|
|
5334
|
-
const
|
|
5335
|
-
|
|
6174
|
+
const kept = adjudicated.filter(
|
|
6175
|
+
(hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
|
|
6176
|
+
);
|
|
6177
|
+
const records = kept.filter((hit) => hit.standing !== "superseded");
|
|
6178
|
+
const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5336
6179
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
5337
6180
|
const bundleDigestValue = bundleDigest(records, superseded);
|
|
5338
6181
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
5339
6182
|
return {
|
|
5340
6183
|
loaded: false,
|
|
5341
|
-
recordCount:
|
|
6184
|
+
recordCount: kept.length,
|
|
5342
6185
|
approxTokens: approxTokens2,
|
|
5343
6186
|
budgetTokens,
|
|
5344
6187
|
message: refusalMessage({
|
|
@@ -5351,7 +6194,7 @@ ${answer}
|
|
|
5351
6194
|
}
|
|
5352
6195
|
return {
|
|
5353
6196
|
loaded: true,
|
|
5354
|
-
recordCount:
|
|
6197
|
+
recordCount: kept.length,
|
|
5355
6198
|
tokensLoaded: approxTokens2,
|
|
5356
6199
|
budgetTokens: options.all ? null : budgetTokens,
|
|
5357
6200
|
records,
|
|
@@ -5361,24 +6204,34 @@ ${answer}
|
|
|
5361
6204
|
}
|
|
5362
6205
|
/**
|
|
5363
6206
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
5364
|
-
* the same way, handed back as a stamp.
|
|
5365
|
-
*
|
|
5366
|
-
*
|
|
6207
|
+
* the same way, handed back as a stamp.
|
|
6208
|
+
*
|
|
6209
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
6210
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
6211
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
6212
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
5367
6213
|
*/
|
|
5368
|
-
async stamp(bundlePath2) {
|
|
6214
|
+
async stamp(bundlePath2, options = {}) {
|
|
5369
6215
|
const bundle = await this.list(bundlePath2);
|
|
5370
6216
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
5371
6217
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
5372
6218
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5373
6219
|
const stamped = bundleStamp(current, superseded);
|
|
5374
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
6220
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
6221
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
6222
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
6223
|
+
(entries) => entries.some(
|
|
6224
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
6225
|
+
)
|
|
6226
|
+
).length;
|
|
5375
6227
|
return {
|
|
5376
6228
|
path: bundlePath2,
|
|
5377
6229
|
digest: stamped.digest,
|
|
5378
6230
|
recordCount: bundle.length,
|
|
5379
6231
|
superseded: superseded.length,
|
|
5380
6232
|
newestAt: dates.at(-1) ?? null,
|
|
5381
|
-
records: stamped.records
|
|
6233
|
+
records: stamped.records,
|
|
6234
|
+
drifted: drifted2
|
|
5382
6235
|
};
|
|
5383
6236
|
}
|
|
5384
6237
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -5786,7 +6639,7 @@ function typeRank(record) {
|
|
|
5786
6639
|
}
|
|
5787
6640
|
|
|
5788
6641
|
// src/version.ts
|
|
5789
|
-
var VERSION = true ? "0.1.
|
|
6642
|
+
var VERSION = true ? "0.1.19" : "0.0.0-dev";
|
|
5790
6643
|
|
|
5791
6644
|
export {
|
|
5792
6645
|
kbSourceSchema,
|
|
@@ -5863,6 +6716,7 @@ export {
|
|
|
5863
6716
|
unpinBase,
|
|
5864
6717
|
adjudicate,
|
|
5865
6718
|
resolveHeads,
|
|
6719
|
+
matchesTags,
|
|
5866
6720
|
catalog,
|
|
5867
6721
|
renderCatalogLine,
|
|
5868
6722
|
INDEX_FILE,
|
|
@@ -5875,6 +6729,9 @@ export {
|
|
|
5875
6729
|
CONTEXT_BEGIN,
|
|
5876
6730
|
CONTEXT_END,
|
|
5877
6731
|
syncInstructions,
|
|
6732
|
+
classifyDrift,
|
|
6733
|
+
unifiedDiff,
|
|
6734
|
+
reassessPacket,
|
|
5878
6735
|
KB_EDGE_KINDS,
|
|
5879
6736
|
DEFAULT_TYPED_LINK_RELS,
|
|
5880
6737
|
neighbours,
|
|
@@ -5912,4 +6769,4 @@ export {
|
|
|
5912
6769
|
KbStore,
|
|
5913
6770
|
VERSION
|
|
5914
6771
|
};
|
|
5915
|
-
//# sourceMappingURL=chunk-
|
|
6772
|
+
//# sourceMappingURL=chunk-MNQNHYWL.js.map
|