@saasontools/strauss-kb 0.1.17 → 0.1.18
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-ZKIQOBHT.js → chunk-GSOTMWZZ.js} +905 -147
- package/dist/chunk-GSOTMWZZ.js.map +1 -0
- package/dist/{chunk-RMJUGTAQ.js → chunk-IOIUS26S.js} +2 -2
- package/dist/{chunk-SA3A2SPY.js → chunk-ROGVYSMV.js} +2 -2
- package/dist/cli-main.cjs +937 -184
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +914 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +291 -5
- package/dist/index.d.ts +291 -5
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +930 -177
- 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-IOIUS26S.js.map} +0 -0
- /package/dist/{chunk-SA3A2SPY.js.map → chunk-ROGVYSMV.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
|
}
|
|
@@ -2264,7 +2369,7 @@ async function listPins(store, workspaceDir) {
|
|
|
2264
2369
|
}
|
|
2265
2370
|
|
|
2266
2371
|
// src/kb-pins/pin.ts
|
|
2267
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
2372
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
2268
2373
|
const layer = options.layer ?? "project";
|
|
2269
2374
|
const root = layerRoot(workspaceDir, layer);
|
|
2270
2375
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -2292,7 +2397,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2292
2397
|
return {
|
|
2293
2398
|
path: existing.path,
|
|
2294
2399
|
layer,
|
|
2295
|
-
pinnedAt: existing.pinnedAt ??
|
|
2400
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
2296
2401
|
alreadyPinned: true,
|
|
2297
2402
|
...updated.mode ? { mode: updated.mode } : {},
|
|
2298
2403
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -2302,7 +2407,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2302
2407
|
}
|
|
2303
2408
|
const entry = {
|
|
2304
2409
|
path: storablePath(root, bundlePath2),
|
|
2305
|
-
pinnedAt:
|
|
2410
|
+
pinnedAt: at2,
|
|
2306
2411
|
...fields
|
|
2307
2412
|
};
|
|
2308
2413
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -2312,7 +2417,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2312
2417
|
return {
|
|
2313
2418
|
path: entry.path,
|
|
2314
2419
|
layer,
|
|
2315
|
-
pinnedAt:
|
|
2420
|
+
pinnedAt: at2,
|
|
2316
2421
|
alreadyPinned: false,
|
|
2317
2422
|
...fields,
|
|
2318
2423
|
...warning ? { warning } : {}
|
|
@@ -2412,7 +2517,8 @@ function warningAnchor(entry) {
|
|
|
2412
2517
|
diffSize,
|
|
2413
2518
|
...reason !== void 0 ? { reason } : {},
|
|
2414
2519
|
...repo !== void 0 ? { repo } : {},
|
|
2415
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
2520
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
2521
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
2416
2522
|
};
|
|
2417
2523
|
}
|
|
2418
2524
|
function resolveHeads(from, byId) {
|
|
@@ -2766,6 +2872,405 @@ ${CONTEXT_END}` : null;
|
|
|
2766
2872
|
return { file, action: "appended" };
|
|
2767
2873
|
}
|
|
2768
2874
|
|
|
2875
|
+
// src/drift/git.ts
|
|
2876
|
+
import { execFile as execFile3 } from "child_process";
|
|
2877
|
+
import { promisify as promisify3 } from "util";
|
|
2878
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2879
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
2880
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
2881
|
+
async function git2(cwd, args) {
|
|
2882
|
+
const env = { ...process.env };
|
|
2883
|
+
delete env["GIT_DIR"];
|
|
2884
|
+
delete env["GIT_WORK_TREE"];
|
|
2885
|
+
delete env["GIT_INDEX_FILE"];
|
|
2886
|
+
try {
|
|
2887
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
2888
|
+
timeout: GIT_TIMEOUT_MS,
|
|
2889
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
2890
|
+
env
|
|
2891
|
+
});
|
|
2892
|
+
return { ok: true, stdout };
|
|
2893
|
+
} catch {
|
|
2894
|
+
return { ok: false };
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
async function listRepoFiles(repoRoot) {
|
|
2898
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
2899
|
+
if (!result.ok) return [];
|
|
2900
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
2901
|
+
}
|
|
2902
|
+
async function readOldSource(repoRoot, anchor) {
|
|
2903
|
+
if (!filePathIsSafe(anchor.file))
|
|
2904
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2905
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
2906
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
2907
|
+
if (shown2 !== null) {
|
|
2908
|
+
return {
|
|
2909
|
+
ok: true,
|
|
2910
|
+
source: shown2,
|
|
2911
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
const at2 = anchor.resolved_at;
|
|
2916
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
2917
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2918
|
+
}
|
|
2919
|
+
const found = await git2(repoRoot, [
|
|
2920
|
+
"log",
|
|
2921
|
+
"-1",
|
|
2922
|
+
"--format=%H",
|
|
2923
|
+
`--before=${at2}`,
|
|
2924
|
+
"--end-of-options",
|
|
2925
|
+
"HEAD",
|
|
2926
|
+
"--",
|
|
2927
|
+
anchor.file
|
|
2928
|
+
]);
|
|
2929
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
2930
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
2931
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2932
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
2933
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
2934
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
2935
|
+
}
|
|
2936
|
+
async function showFile(repoRoot, ref, file) {
|
|
2937
|
+
const path = file.replace(/^\.\//, "");
|
|
2938
|
+
const result = await git2(repoRoot, [
|
|
2939
|
+
"show",
|
|
2940
|
+
"--end-of-options",
|
|
2941
|
+
`${ref}:${path}`
|
|
2942
|
+
]);
|
|
2943
|
+
return result.ok ? result.stdout : null;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/drift/moved.ts
|
|
2947
|
+
import { stat as stat2 } from "fs/promises";
|
|
2948
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
2949
|
+
var SEARCH_BATCH = 64;
|
|
2950
|
+
function movedSearch(repoRoot, options = {}) {
|
|
2951
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
2952
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
2953
|
+
const resolver = new TreeSitterResolver();
|
|
2954
|
+
let repoFiles;
|
|
2955
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
2956
|
+
const filesForLanguage = async (language) => {
|
|
2957
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
2958
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
2959
|
+
};
|
|
2960
|
+
return {
|
|
2961
|
+
async find(anchor) {
|
|
2962
|
+
const stored = anchor.hash;
|
|
2963
|
+
if (!stored) return void 0;
|
|
2964
|
+
const language = languageForFile(anchor.file);
|
|
2965
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
2966
|
+
const candidates = await filesForLanguage(language);
|
|
2967
|
+
if (!prepared.has(language)) {
|
|
2968
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
2969
|
+
prepared.add(language);
|
|
2970
|
+
}
|
|
2971
|
+
const floor = anchor.lines ?? 0;
|
|
2972
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
2973
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
2974
|
+
const hits = await mapLimit(
|
|
2975
|
+
batch,
|
|
2976
|
+
DEFAULT_IO_CONCURRENCY,
|
|
2977
|
+
async (file) => {
|
|
2978
|
+
const size2 = await sizeOf(file);
|
|
2979
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
2980
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
2981
|
+
}
|
|
2982
|
+
);
|
|
2983
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
2984
|
+
if (found) return found;
|
|
2985
|
+
}
|
|
2986
|
+
return void 0;
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
2991
|
+
const source = await read(file);
|
|
2992
|
+
if (!source.ok) return void 0;
|
|
2993
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
2994
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
2995
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
2996
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
2997
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
2998
|
+
return {
|
|
2999
|
+
file,
|
|
3000
|
+
symbol: found.symbol,
|
|
3001
|
+
startLine: found.span.startLine,
|
|
3002
|
+
endLine: found.span.endLine
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
return void 0;
|
|
3006
|
+
}
|
|
3007
|
+
function diskSize(repoRoot) {
|
|
3008
|
+
return async (file) => {
|
|
3009
|
+
const path = anchorFilePath(repoRoot, file);
|
|
3010
|
+
if (path === null) return null;
|
|
3011
|
+
try {
|
|
3012
|
+
return (await stat2(path)).size;
|
|
3013
|
+
} catch {
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
3019
|
+
const height = anchor.lines;
|
|
3020
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
3021
|
+
const source = await read(anchor.file);
|
|
3022
|
+
if (!source.ok) return void 0;
|
|
3023
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
3024
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
3025
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
file: anchor.file,
|
|
3030
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3031
|
+
startLine: at2 + 1,
|
|
3032
|
+
endLine: at2 + height
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
return void 0;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/drift/classify.ts
|
|
3039
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
3040
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3041
|
+
(anchor) => anchor.hash
|
|
3042
|
+
);
|
|
3043
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
3044
|
+
const treeSitter = new TreeSitterResolver();
|
|
3045
|
+
const resolvers = [treeSitter, regexResolver];
|
|
3046
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
3047
|
+
const wanted = [];
|
|
3048
|
+
entries.forEach((entry, at2) => {
|
|
3049
|
+
const anchor = anchors[at2];
|
|
3050
|
+
if (!anchor) return;
|
|
3051
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
3052
|
+
wanted.push({ anchor, entry });
|
|
3053
|
+
});
|
|
3054
|
+
if (!wanted.length) return [];
|
|
3055
|
+
await prepareResolvers(
|
|
3056
|
+
resolvers,
|
|
3057
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
3058
|
+
);
|
|
3059
|
+
const out = [];
|
|
3060
|
+
for (const { anchor, entry } of wanted) {
|
|
3061
|
+
const movedTo = await search.find(anchor);
|
|
3062
|
+
if (movedTo) {
|
|
3063
|
+
out.push({
|
|
3064
|
+
anchor,
|
|
3065
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
3066
|
+
class: "moved"
|
|
3067
|
+
});
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
3071
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
3072
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
3073
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
3074
|
+
out.push({
|
|
3075
|
+
anchor,
|
|
3076
|
+
entry: { ...entry, class: settled },
|
|
3077
|
+
class: settled,
|
|
3078
|
+
...newText !== void 0 ? { newText } : {},
|
|
3079
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
3080
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
return out;
|
|
3084
|
+
}
|
|
3085
|
+
function sameTokens(resolver, file, before, after) {
|
|
3086
|
+
if (before === after) return false;
|
|
3087
|
+
const left = resolver.normalize(before, file);
|
|
3088
|
+
const right = resolver.normalize(after, file);
|
|
3089
|
+
return left !== null && left === right;
|
|
3090
|
+
}
|
|
3091
|
+
async function currentText(reader, anchor, resolvers) {
|
|
3092
|
+
const read = await reader(anchor.file);
|
|
3093
|
+
if (!read.ok) return void 0;
|
|
3094
|
+
return spanIn(read.source, anchor, resolvers);
|
|
3095
|
+
}
|
|
3096
|
+
function spanIn(source, anchor, resolvers) {
|
|
3097
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
3098
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// src/drift/diff.ts
|
|
3102
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
3103
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
3104
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
3105
|
+
function diffBudget(anchors) {
|
|
3106
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
3107
|
+
return Math.min(
|
|
3108
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
3109
|
+
Math.max(
|
|
3110
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
3111
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
3112
|
+
)
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
function unifiedDiff(before, after, options = {}) {
|
|
3116
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
3117
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
3118
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
3119
|
+
const body = [];
|
|
3120
|
+
let added = 0;
|
|
3121
|
+
let removed = 0;
|
|
3122
|
+
for (const edit of edits(left, right)) {
|
|
3123
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
3124
|
+
else if (edit.kind === "remove") {
|
|
3125
|
+
body.push(`-${edit.line}`);
|
|
3126
|
+
removed += 1;
|
|
3127
|
+
} else {
|
|
3128
|
+
body.push(`+${edit.line}`);
|
|
3129
|
+
added += 1;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const truncated = body.length > max;
|
|
3133
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
3134
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3135
|
+
const lines = [header, ...shown];
|
|
3136
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3137
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
3138
|
+
}
|
|
3139
|
+
function edits(left, right) {
|
|
3140
|
+
const rows = left.length;
|
|
3141
|
+
const cols = right.length;
|
|
3142
|
+
const table2 = Array.from(
|
|
3143
|
+
{ length: rows + 1 },
|
|
3144
|
+
() => new Array(cols + 1).fill(0)
|
|
3145
|
+
);
|
|
3146
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
3147
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
3148
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
3149
|
+
table2[row2 + 1][col2],
|
|
3150
|
+
table2[row2][col2 + 1]
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
const out = [];
|
|
3155
|
+
let row = 0;
|
|
3156
|
+
let col = 0;
|
|
3157
|
+
while (row < rows && col < cols) {
|
|
3158
|
+
if (left[row] === right[col]) {
|
|
3159
|
+
out.push({ kind: "same", line: left[row] });
|
|
3160
|
+
row += 1;
|
|
3161
|
+
col += 1;
|
|
3162
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
3163
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3164
|
+
row += 1;
|
|
3165
|
+
} else {
|
|
3166
|
+
out.push({ kind: "add", line: right[col] });
|
|
3167
|
+
col += 1;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
for (; row < rows; row++)
|
|
3171
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3172
|
+
for (; col < cols; col++)
|
|
3173
|
+
out.push({ kind: "add", line: right[col] });
|
|
3174
|
+
return out;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/drift/packet.ts
|
|
3178
|
+
var PRESUMED_INVALID = [
|
|
3179
|
+
"fact",
|
|
3180
|
+
"constraint",
|
|
3181
|
+
"contract"
|
|
3182
|
+
];
|
|
3183
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
3184
|
+
var DEFAULT_NOTES = {
|
|
3185
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
3186
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
3187
|
+
review: "re-read the record against the new code"
|
|
3188
|
+
};
|
|
3189
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
3190
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
3191
|
+
...options.reader ? { reader: options.reader } : {},
|
|
3192
|
+
...options.search ? { search: options.search } : {},
|
|
3193
|
+
withHistory: options.withDiff !== false
|
|
3194
|
+
});
|
|
3195
|
+
const open = classified.filter(
|
|
3196
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
3197
|
+
);
|
|
3198
|
+
if (!open.length) return { packet: null, classified };
|
|
3199
|
+
const budget = diffBudget(open.length);
|
|
3200
|
+
const anchors = open.map(
|
|
3201
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3202
|
+
);
|
|
3203
|
+
const type = record.frontmatter.type;
|
|
3204
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
3205
|
+
return {
|
|
3206
|
+
classified,
|
|
3207
|
+
packet: {
|
|
3208
|
+
conceptId: record.conceptId,
|
|
3209
|
+
title: record.frontmatter.title ?? null,
|
|
3210
|
+
type,
|
|
3211
|
+
standing: options.standing ?? "unsettled",
|
|
3212
|
+
why: record.frontmatter.description ?? null,
|
|
3213
|
+
claim: claimOf(record),
|
|
3214
|
+
anchors,
|
|
3215
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
3216
|
+
conceptId: entry.conceptId,
|
|
3217
|
+
title: entry.title,
|
|
3218
|
+
standing: entry.standing,
|
|
3219
|
+
depth: entry.depth
|
|
3220
|
+
})),
|
|
3221
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
3222
|
+
default: fallback,
|
|
3223
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
3228
|
+
const { entry } = found;
|
|
3229
|
+
const base2 = {
|
|
3230
|
+
file: entry.file,
|
|
3231
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
3232
|
+
class: found.class,
|
|
3233
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
3234
|
+
storedHash: entry.storedHash,
|
|
3235
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
3236
|
+
diffSize: entry.diffSize,
|
|
3237
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3238
|
+
};
|
|
3239
|
+
if (!withDiff) return base2;
|
|
3240
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3241
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
3242
|
+
}
|
|
3243
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
3244
|
+
maxLines
|
|
3245
|
+
});
|
|
3246
|
+
return {
|
|
3247
|
+
...base2,
|
|
3248
|
+
diff: {
|
|
3249
|
+
status: "ok",
|
|
3250
|
+
source: found.oldOrigin.kind,
|
|
3251
|
+
ref: found.oldOrigin.ref,
|
|
3252
|
+
unified: rendered.text,
|
|
3253
|
+
added: rendered.added,
|
|
3254
|
+
removed: rendered.removed,
|
|
3255
|
+
truncated: rendered.truncated
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
function claimOf(record) {
|
|
3260
|
+
const type = record.frontmatter.type;
|
|
3261
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
3262
|
+
if (!section) return null;
|
|
3263
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
3264
|
+
const start = lines.findIndex(
|
|
3265
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
3266
|
+
);
|
|
3267
|
+
if (start < 0) return null;
|
|
3268
|
+
const rest = lines.slice(start + 1);
|
|
3269
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3270
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3271
|
+
return text ? { section, text } : null;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
2769
3274
|
// src/kb-edges.ts
|
|
2770
3275
|
var KB_EDGE_KINDS = [
|
|
2771
3276
|
"body-link",
|
|
@@ -3025,18 +3530,18 @@ function expired(hits, now) {
|
|
|
3025
3530
|
for (const hit of hits) {
|
|
3026
3531
|
const raw = hit.record.frontmatter.stale_after;
|
|
3027
3532
|
if (!raw) continue;
|
|
3028
|
-
const
|
|
3029
|
-
if (Number.isNaN(
|
|
3533
|
+
const at2 = Date.parse(raw);
|
|
3534
|
+
if (Number.isNaN(at2)) {
|
|
3030
3535
|
findings.push(
|
|
3031
3536
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
3032
3537
|
);
|
|
3033
3538
|
continue;
|
|
3034
3539
|
}
|
|
3035
|
-
if (
|
|
3540
|
+
if (at2 < now.getTime()) {
|
|
3036
3541
|
findings.push(
|
|
3037
3542
|
finding(
|
|
3038
3543
|
hit.record,
|
|
3039
|
-
`stale since ${raw} (${daysBetween(
|
|
3544
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
3040
3545
|
)
|
|
3041
3546
|
);
|
|
3042
3547
|
}
|
|
@@ -3049,12 +3554,12 @@ function expiring(hits, now, withinDays) {
|
|
|
3049
3554
|
for (const hit of hits) {
|
|
3050
3555
|
const raw = hit.record.frontmatter.stale_after;
|
|
3051
3556
|
if (!raw) continue;
|
|
3052
|
-
const
|
|
3053
|
-
if (Number.isNaN(
|
|
3557
|
+
const at2 = Date.parse(raw);
|
|
3558
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
3054
3559
|
findings.push(
|
|
3055
3560
|
finding(
|
|
3056
3561
|
hit.record,
|
|
3057
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
3562
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
3058
3563
|
)
|
|
3059
3564
|
);
|
|
3060
3565
|
}
|
|
@@ -3224,13 +3729,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
3224
3729
|
);
|
|
3225
3730
|
}
|
|
3226
3731
|
function describeAnchor(anchor) {
|
|
3227
|
-
const
|
|
3228
|
-
if (anchor.
|
|
3732
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3733
|
+
if (anchor.class === "gone") {
|
|
3734
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
3735
|
+
}
|
|
3736
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
3229
3737
|
if (anchor.remoteState === "drifted-on-default") {
|
|
3230
|
-
return `${
|
|
3738
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
3231
3739
|
}
|
|
3232
|
-
if (anchor.diffSize === null) return `${
|
|
3233
|
-
return anchor.diffSize === 0 ? `${
|
|
3740
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
3741
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3234
3742
|
}
|
|
3235
3743
|
function replaces(later, earlier) {
|
|
3236
3744
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -3247,9 +3755,9 @@ function daysBetween(from, to) {
|
|
|
3247
3755
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
3248
3756
|
}
|
|
3249
3757
|
function ageInDays(record, now) {
|
|
3250
|
-
const
|
|
3251
|
-
if (!
|
|
3252
|
-
const written = Date.parse(
|
|
3758
|
+
const at2 = record.frontmatter.generated?.at;
|
|
3759
|
+
if (!at2) return null;
|
|
3760
|
+
const written = Date.parse(at2);
|
|
3253
3761
|
if (Number.isNaN(written)) return null;
|
|
3254
3762
|
return daysBetween(written, now.getTime());
|
|
3255
3763
|
}
|
|
@@ -3362,8 +3870,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
3362
3870
|
return [...reached.values()].sort(byGeneratedAt);
|
|
3363
3871
|
}
|
|
3364
3872
|
function byGeneratedAt(left, right) {
|
|
3365
|
-
const
|
|
3366
|
-
return
|
|
3873
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
3874
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
3367
3875
|
}
|
|
3368
3876
|
|
|
3369
3877
|
// src/commands/anchor-resolve.ts
|
|
@@ -3386,9 +3894,9 @@ function argvFlag(argv, name) {
|
|
|
3386
3894
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
3387
3895
|
return value2;
|
|
3388
3896
|
}
|
|
3389
|
-
const
|
|
3390
|
-
if (
|
|
3391
|
-
const value = argv[
|
|
3897
|
+
const at2 = argv.indexOf(name);
|
|
3898
|
+
if (at2 === -1) return void 0;
|
|
3899
|
+
const value = argv[at2 + 1];
|
|
3392
3900
|
if (value === void 0 || value.startsWith("--")) {
|
|
3393
3901
|
throw new KbMissingFlagValueError(name);
|
|
3394
3902
|
}
|
|
@@ -3481,11 +3989,14 @@ var anchorResolveCommand = define({
|
|
|
3481
3989
|
}
|
|
3482
3990
|
const resolved = outcome.span;
|
|
3483
3991
|
const producedBy = outcome.resolver;
|
|
3484
|
-
const currentHash =
|
|
3992
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
3485
3993
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3994
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
3995
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
3486
3996
|
const stamped = {
|
|
3487
3997
|
...anchor,
|
|
3488
|
-
hash:
|
|
3998
|
+
hash: stampedHash,
|
|
3999
|
+
hash_kind: stampedKind,
|
|
3489
4000
|
lines: currentLines,
|
|
3490
4001
|
resolved_at: now(),
|
|
3491
4002
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -3495,7 +4006,8 @@ var anchorResolveCommand = define({
|
|
|
3495
4006
|
results.push({
|
|
3496
4007
|
...base2,
|
|
3497
4008
|
state: "stamped",
|
|
3498
|
-
currentHash,
|
|
4009
|
+
currentHash: stampedHash,
|
|
4010
|
+
hashKind: stampedKind,
|
|
3499
4011
|
...producedBy ? { resolver: producedBy } : {}
|
|
3500
4012
|
});
|
|
3501
4013
|
updated.push(stamped);
|
|
@@ -3507,6 +4019,7 @@ var anchorResolveCommand = define({
|
|
|
3507
4019
|
...base2,
|
|
3508
4020
|
state: "drifted",
|
|
3509
4021
|
currentHash,
|
|
4022
|
+
hashKind: kind,
|
|
3510
4023
|
diffSize: lineDelta(anchor, currentLines),
|
|
3511
4024
|
...producedBy ? { resolver: producedBy } : {},
|
|
3512
4025
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
@@ -3535,6 +4048,7 @@ var anchorResolveCommand = define({
|
|
|
3535
4048
|
...base2,
|
|
3536
4049
|
state: "match",
|
|
3537
4050
|
currentHash,
|
|
4051
|
+
hashKind: kind,
|
|
3538
4052
|
...producedBy ? { resolver: producedBy } : {},
|
|
3539
4053
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
3540
4054
|
});
|
|
@@ -3818,14 +4332,168 @@ var contextCommand = define({
|
|
|
3818
4332
|
});
|
|
3819
4333
|
|
|
3820
4334
|
// src/commands/doctor.ts
|
|
4335
|
+
import { z as z15 } from "zod";
|
|
4336
|
+
|
|
4337
|
+
// src/commands/reassess.ts
|
|
3821
4338
|
import { z as z14 } from "zod";
|
|
3822
|
-
var
|
|
4339
|
+
var reassessCommand = define({
|
|
4340
|
+
name: "reassess",
|
|
4341
|
+
tool: "kb_reassess",
|
|
4342
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4343
|
+
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.",
|
|
4344
|
+
input: z14.object({
|
|
4345
|
+
bundlePath,
|
|
4346
|
+
conceptId,
|
|
4347
|
+
repoRoot: REPO_ROOT,
|
|
4348
|
+
withDiff: z14.boolean().optional().describe(
|
|
4349
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4350
|
+
)
|
|
4351
|
+
}),
|
|
4352
|
+
fromArgv: (argv, path) => {
|
|
4353
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4354
|
+
return {
|
|
4355
|
+
bundlePath: path,
|
|
4356
|
+
conceptId: argv[1],
|
|
4357
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4358
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4359
|
+
};
|
|
4360
|
+
},
|
|
4361
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
4362
|
+
const root = repoRoot ?? process.cwd();
|
|
4363
|
+
const bundle = await store.list(path);
|
|
4364
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
4365
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
4366
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
4367
|
+
const entries = drift?.get(id) ?? [];
|
|
4368
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
4369
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
4370
|
+
}
|
|
4371
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
4372
|
+
(hit) => hit.record.conceptId === id
|
|
4373
|
+
)?.standing;
|
|
4374
|
+
const impact2 = await store.impact(path, id);
|
|
4375
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
4376
|
+
...withDiff ? { withDiff: true } : {},
|
|
4377
|
+
impact: impact2,
|
|
4378
|
+
...standing ? { standing } : {}
|
|
4379
|
+
});
|
|
4380
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
4381
|
+
let frozen = false;
|
|
4382
|
+
const rebaselined = [];
|
|
4383
|
+
if (moves.length) {
|
|
4384
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
4385
|
+
for (const found of moves) {
|
|
4386
|
+
const to = found.entry.movedTo;
|
|
4387
|
+
if (!to) continue;
|
|
4388
|
+
relocated.set(found.anchor, {
|
|
4389
|
+
...found.anchor,
|
|
4390
|
+
file: to.file,
|
|
4391
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
4392
|
+
});
|
|
4393
|
+
rebaselined.push({
|
|
4394
|
+
file: found.anchor.file,
|
|
4395
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
4396
|
+
toFile: to.file,
|
|
4397
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
try {
|
|
4401
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
4404
|
+
frozen = true;
|
|
4405
|
+
}
|
|
4406
|
+
if (!frozen) {
|
|
4407
|
+
await store.updateAnchors(
|
|
4408
|
+
path,
|
|
4409
|
+
id,
|
|
4410
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
4411
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
4412
|
+
),
|
|
4413
|
+
actor
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return {
|
|
4418
|
+
conceptId: id,
|
|
4419
|
+
packet,
|
|
4420
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
4421
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
4422
|
+
...frozen ? {
|
|
4423
|
+
frozen: true,
|
|
4424
|
+
note: "base is frozen: nothing was rebaselined"
|
|
4425
|
+
} : {}
|
|
4426
|
+
};
|
|
4427
|
+
},
|
|
4428
|
+
render: (result) => renderReassess(result)
|
|
4429
|
+
});
|
|
4430
|
+
function renderReassess(result) {
|
|
4431
|
+
const lines = [];
|
|
4432
|
+
for (const move of result.rebaselined) {
|
|
4433
|
+
lines.push(
|
|
4434
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
4435
|
+
);
|
|
4436
|
+
}
|
|
4437
|
+
if (result.cosmetic) {
|
|
4438
|
+
lines.push(
|
|
4439
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
if (result.note) lines.push(result.note);
|
|
4443
|
+
const packet = result.packet;
|
|
4444
|
+
if (!packet) {
|
|
4445
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
4446
|
+
return lines.join("\n");
|
|
4447
|
+
}
|
|
4448
|
+
lines.push(
|
|
4449
|
+
"",
|
|
4450
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
4451
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
4452
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
4453
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
4454
|
+
"",
|
|
4455
|
+
`## Anchors (${packet.anchors.length})`
|
|
4456
|
+
);
|
|
4457
|
+
for (const anchor of packet.anchors) {
|
|
4458
|
+
lines.push(
|
|
4459
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
4460
|
+
);
|
|
4461
|
+
if (!anchor.diff) continue;
|
|
4462
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
4463
|
+
lines.push(
|
|
4464
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
4465
|
+
);
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
4468
|
+
lines.push(
|
|
4469
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
4470
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
if (packet.impact.length) {
|
|
4474
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
4475
|
+
for (const entry of packet.impact) {
|
|
4476
|
+
lines.push(
|
|
4477
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4480
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
4481
|
+
}
|
|
4482
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
4483
|
+
return lines.join("\n");
|
|
4484
|
+
}
|
|
4485
|
+
function at(file, symbol) {
|
|
4486
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// src/commands/doctor.ts
|
|
4490
|
+
var days = (what, fallback) => z15.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3823
4491
|
var doctorCommand = define({
|
|
3824
4492
|
name: "doctor",
|
|
3825
4493
|
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:
|
|
4494
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4495
|
+
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.",
|
|
4496
|
+
input: z15.object({
|
|
3829
4497
|
bundlePath,
|
|
3830
4498
|
repoRoot: REPO_ROOT,
|
|
3831
4499
|
expiringDays: days(
|
|
@@ -3840,11 +4508,17 @@ var doctorCommand = define({
|
|
|
3840
4508
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3841
4509
|
DEFAULT_AGING_DAYS
|
|
3842
4510
|
),
|
|
3843
|
-
offline:
|
|
4511
|
+
offline: z15.boolean().optional().describe(
|
|
3844
4512
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3845
4513
|
),
|
|
3846
|
-
strict:
|
|
4514
|
+
strict: z15.boolean().optional().describe(
|
|
3847
4515
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4516
|
+
),
|
|
4517
|
+
drifted: z15.boolean().optional().describe(
|
|
4518
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4519
|
+
),
|
|
4520
|
+
withDiff: z15.boolean().optional().describe(
|
|
4521
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
3848
4522
|
)
|
|
3849
4523
|
}),
|
|
3850
4524
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -3863,7 +4537,9 @@ var doctorCommand = define({
|
|
|
3863
4537
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3864
4538
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3865
4539
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
3866
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
4540
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
4541
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
4542
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
3867
4543
|
};
|
|
3868
4544
|
},
|
|
3869
4545
|
run: async ({ store, now }, {
|
|
@@ -3872,7 +4548,9 @@ var doctorCommand = define({
|
|
|
3872
4548
|
unverifiedDays,
|
|
3873
4549
|
agingDays,
|
|
3874
4550
|
repoRoot,
|
|
3875
|
-
offline
|
|
4551
|
+
offline,
|
|
4552
|
+
drifted: drifted2,
|
|
4553
|
+
withDiff
|
|
3876
4554
|
}) => {
|
|
3877
4555
|
const checkedAt = now();
|
|
3878
4556
|
const records = await store.list(path);
|
|
@@ -3887,10 +4565,51 @@ var doctorCommand = define({
|
|
|
3887
4565
|
now: new Date(checkedAt)
|
|
3888
4566
|
});
|
|
3889
4567
|
const hints = grammarHints();
|
|
4568
|
+
if (!drifted2) {
|
|
4569
|
+
return {
|
|
4570
|
+
bundlePath: path,
|
|
4571
|
+
checkedAt,
|
|
4572
|
+
...report,
|
|
4573
|
+
...hints.length ? { hints } : {}
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
const standings = new Map(
|
|
4577
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4578
|
+
hit.record.conceptId,
|
|
4579
|
+
hit.standing
|
|
4580
|
+
])
|
|
4581
|
+
);
|
|
4582
|
+
const packets = [];
|
|
4583
|
+
const rebaselinable = [];
|
|
4584
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
4585
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
4586
|
+
const record = records.find(
|
|
4587
|
+
(entry) => entry.conceptId === found.conceptId
|
|
4588
|
+
);
|
|
4589
|
+
if (!record) continue;
|
|
4590
|
+
const standing = standings.get(record.conceptId);
|
|
4591
|
+
const built = await reassessPacket(
|
|
4592
|
+
repoRoot ?? process.cwd(),
|
|
4593
|
+
record,
|
|
4594
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
4595
|
+
{
|
|
4596
|
+
...withDiff ? { withDiff: true } : {},
|
|
4597
|
+
impact: await store.impact(path, record.conceptId),
|
|
4598
|
+
...standing ? { standing } : {},
|
|
4599
|
+
search
|
|
4600
|
+
}
|
|
4601
|
+
);
|
|
4602
|
+
if (built.packet) packets.push(built.packet);
|
|
4603
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
4604
|
+
rebaselinable.push(record.conceptId);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
3890
4607
|
return {
|
|
3891
4608
|
bundlePath: path,
|
|
3892
4609
|
checkedAt,
|
|
3893
4610
|
...report,
|
|
4611
|
+
packets,
|
|
4612
|
+
rebaselinable,
|
|
3894
4613
|
...hints.length ? { hints } : {}
|
|
3895
4614
|
};
|
|
3896
4615
|
},
|
|
@@ -3904,6 +4623,7 @@ var doctorCommand = define({
|
|
|
3904
4623
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
3905
4624
|
});
|
|
3906
4625
|
function render2(result) {
|
|
4626
|
+
if (result.packets) return renderPackets(result);
|
|
3907
4627
|
const { thresholds } = result;
|
|
3908
4628
|
const lines = [
|
|
3909
4629
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -3937,21 +4657,45 @@ function render2(result) {
|
|
|
3937
4657
|
);
|
|
3938
4658
|
return lines.join("\n");
|
|
3939
4659
|
}
|
|
4660
|
+
function renderPackets(result) {
|
|
4661
|
+
const packets = result.packets ?? [];
|
|
4662
|
+
const lines = [
|
|
4663
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
4664
|
+
`checked: ${result.checkedAt}`,
|
|
4665
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
4666
|
+
];
|
|
4667
|
+
if (result.rebaselinable?.length) {
|
|
4668
|
+
lines.push(
|
|
4669
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
4670
|
+
);
|
|
4671
|
+
}
|
|
4672
|
+
for (const packet of packets) {
|
|
4673
|
+
lines.push(
|
|
4674
|
+
renderReassess({
|
|
4675
|
+
conceptId: packet.conceptId,
|
|
4676
|
+
packet,
|
|
4677
|
+
rebaselined: [],
|
|
4678
|
+
cosmetic: 0
|
|
4679
|
+
})
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4682
|
+
return lines.join("\n");
|
|
4683
|
+
}
|
|
3940
4684
|
|
|
3941
4685
|
// src/commands/impact.ts
|
|
3942
|
-
import { z as
|
|
4686
|
+
import { z as z16 } from "zod";
|
|
3943
4687
|
var impactCommand = define({
|
|
3944
4688
|
name: "impact",
|
|
3945
4689
|
tool: "kb_impact",
|
|
3946
4690
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3947
4691
|
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:
|
|
4692
|
+
input: z16.object({
|
|
3949
4693
|
bundlePath,
|
|
3950
4694
|
conceptId,
|
|
3951
|
-
depth:
|
|
4695
|
+
depth: z16.number().int().positive().optional().describe(
|
|
3952
4696
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3953
4697
|
),
|
|
3954
|
-
rels:
|
|
4698
|
+
rels: z16.array(z16.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3955
4699
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3956
4700
|
)
|
|
3957
4701
|
}),
|
|
@@ -3972,13 +4716,13 @@ var impactCommand = define({
|
|
|
3972
4716
|
});
|
|
3973
4717
|
|
|
3974
4718
|
// src/commands/list.ts
|
|
3975
|
-
import { z as
|
|
4719
|
+
import { z as z17 } from "zod";
|
|
3976
4720
|
var listCommand = define({
|
|
3977
4721
|
name: "list",
|
|
3978
4722
|
tool: "kb_list",
|
|
3979
4723
|
usage: "list [type]",
|
|
3980
4724
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3981
|
-
input:
|
|
4725
|
+
input: z17.object({ bundlePath, type: z17.enum(KB_RECORD_TYPES).optional() }),
|
|
3982
4726
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3983
4727
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3984
4728
|
conceptId: record.conceptId,
|
|
@@ -3990,17 +4734,17 @@ var listCommand = define({
|
|
|
3990
4734
|
});
|
|
3991
4735
|
|
|
3992
4736
|
// src/commands/load.ts
|
|
3993
|
-
import { z as
|
|
4737
|
+
import { z as z18 } from "zod";
|
|
3994
4738
|
var loadCommand = define({
|
|
3995
4739
|
name: "load",
|
|
3996
4740
|
tool: "kb_load",
|
|
3997
4741
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3998
4742
|
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:
|
|
4743
|
+
input: z18.object({
|
|
4000
4744
|
bundlePath,
|
|
4001
|
-
type:
|
|
4002
|
-
budgetTokens:
|
|
4003
|
-
all:
|
|
4745
|
+
type: z18.enum(KB_RECORD_TYPES).optional(),
|
|
4746
|
+
budgetTokens: z18.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4747
|
+
all: z18.boolean().optional().describe(
|
|
4004
4748
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4005
4749
|
),
|
|
4006
4750
|
repoRoot: REPO_ROOT
|
|
@@ -4042,25 +4786,25 @@ var loadCommand = define({
|
|
|
4042
4786
|
});
|
|
4043
4787
|
|
|
4044
4788
|
// src/commands/log.ts
|
|
4045
|
-
import { z as
|
|
4789
|
+
import { z as z19 } from "zod";
|
|
4046
4790
|
var logCommand = define({
|
|
4047
4791
|
name: "log",
|
|
4048
4792
|
tool: "kb_log",
|
|
4049
4793
|
usage: "log",
|
|
4050
4794
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4051
|
-
input:
|
|
4795
|
+
input: z19.object({ bundlePath }),
|
|
4052
4796
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4053
4797
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4054
4798
|
});
|
|
4055
4799
|
|
|
4056
4800
|
// src/commands/no-decision.ts
|
|
4057
|
-
import { z as
|
|
4801
|
+
import { z as z20 } from "zod";
|
|
4058
4802
|
var noDecisionCommand = define({
|
|
4059
4803
|
name: "no-decision",
|
|
4060
4804
|
tool: "kb_no_decision",
|
|
4061
4805
|
usage: "no-decision <reason...>",
|
|
4062
4806
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4063
|
-
input:
|
|
4807
|
+
input: z20.object({ bundlePath, reason: z20.string().min(1) }),
|
|
4064
4808
|
fromArgv: (argv, path) => ({
|
|
4065
4809
|
bundlePath: path,
|
|
4066
4810
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4077,20 +4821,20 @@ var noDecisionCommand = define({
|
|
|
4077
4821
|
});
|
|
4078
4822
|
|
|
4079
4823
|
// src/commands/pack.ts
|
|
4080
|
-
import { z as
|
|
4824
|
+
import { z as z21 } from "zod";
|
|
4081
4825
|
var packCommand = define({
|
|
4082
4826
|
name: "pack",
|
|
4083
4827
|
tool: "kb_pack",
|
|
4084
4828
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4085
4829
|
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:
|
|
4830
|
+
input: z21.object({
|
|
4087
4831
|
bundlePath,
|
|
4088
4832
|
conceptId,
|
|
4089
|
-
hops:
|
|
4090
|
-
maxNodes:
|
|
4833
|
+
hops: z21.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4834
|
+
maxNodes: z21.number().int().positive().optional().describe(
|
|
4091
4835
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4092
4836
|
),
|
|
4093
|
-
budgetTokens:
|
|
4837
|
+
budgetTokens: z21.number().int().positive().optional().describe(
|
|
4094
4838
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4095
4839
|
)
|
|
4096
4840
|
}),
|
|
@@ -4115,12 +4859,12 @@ var packCommand = define({
|
|
|
4115
4859
|
return render3(result, path, now());
|
|
4116
4860
|
}
|
|
4117
4861
|
});
|
|
4118
|
-
function render3(result, bundle,
|
|
4862
|
+
function render3(result, bundle, at2) {
|
|
4119
4863
|
const lines = [
|
|
4120
4864
|
`# KB Pack \u2014 ${result.root}`,
|
|
4121
4865
|
`bundle: ${bundle}`,
|
|
4122
4866
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
4123
|
-
`packed: ${
|
|
4867
|
+
`packed: ${at2}`,
|
|
4124
4868
|
"",
|
|
4125
4869
|
`## Records (${result.records.length})`
|
|
4126
4870
|
];
|
|
@@ -4177,22 +4921,22 @@ function warningLabel(warning) {
|
|
|
4177
4921
|
}
|
|
4178
4922
|
|
|
4179
4923
|
// src/commands/pin.ts
|
|
4180
|
-
import { z as
|
|
4924
|
+
import { z as z22 } from "zod";
|
|
4181
4925
|
var pinCommand = define({
|
|
4182
4926
|
name: "pin",
|
|
4183
4927
|
tool: "kb_pin",
|
|
4184
4928
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4185
4929
|
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:
|
|
4930
|
+
input: z22.object({
|
|
4187
4931
|
bundlePath,
|
|
4188
|
-
mode:
|
|
4932
|
+
mode: z22.enum(["full", "index"]).optional().describe(
|
|
4189
4933
|
"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
4934
|
),
|
|
4191
|
-
profiles:
|
|
4192
|
-
layer:
|
|
4935
|
+
profiles: z22.array(z22.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4936
|
+
layer: z22.enum(["project", "local", "user"]).optional().describe(
|
|
4193
4937
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4194
4938
|
),
|
|
4195
|
-
frozen:
|
|
4939
|
+
frozen: z22.boolean().optional().describe(
|
|
4196
4940
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4197
4941
|
)
|
|
4198
4942
|
}),
|
|
@@ -4221,29 +4965,29 @@ var pinCommand = define({
|
|
|
4221
4965
|
});
|
|
4222
4966
|
|
|
4223
4967
|
// src/commands/pins.ts
|
|
4224
|
-
import { z as
|
|
4968
|
+
import { z as z23 } from "zod";
|
|
4225
4969
|
var pinsCommand = define({
|
|
4226
4970
|
name: "pins",
|
|
4227
4971
|
tool: "kb_pins",
|
|
4228
4972
|
usage: "pins",
|
|
4229
4973
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4230
|
-
input:
|
|
4974
|
+
input: z23.object({}),
|
|
4231
4975
|
fromArgv: () => ({}),
|
|
4232
4976
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4233
4977
|
});
|
|
4234
4978
|
|
|
4235
4979
|
// src/commands/query.ts
|
|
4236
|
-
import { z as
|
|
4980
|
+
import { z as z24 } from "zod";
|
|
4237
4981
|
var queryCommand = define({
|
|
4238
4982
|
name: "query",
|
|
4239
4983
|
tool: "kb_query",
|
|
4240
4984
|
usage: "query <text...> [--repo-root PATH]",
|
|
4241
4985
|
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:
|
|
4986
|
+
input: z24.object({
|
|
4243
4987
|
bundlePath,
|
|
4244
|
-
text:
|
|
4245
|
-
type:
|
|
4246
|
-
includeNonCurrent:
|
|
4988
|
+
text: z24.string().optional(),
|
|
4989
|
+
type: z24.enum(KB_RECORD_TYPES).optional(),
|
|
4990
|
+
includeNonCurrent: z24.boolean().optional(),
|
|
4247
4991
|
repoRoot: REPO_ROOT
|
|
4248
4992
|
}),
|
|
4249
4993
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -4275,43 +5019,43 @@ var queryCommand = define({
|
|
|
4275
5019
|
});
|
|
4276
5020
|
|
|
4277
5021
|
// src/commands/read-index.ts
|
|
4278
|
-
import { z as
|
|
5022
|
+
import { z as z25 } from "zod";
|
|
4279
5023
|
var readIndexCommand = define({
|
|
4280
5024
|
name: "index",
|
|
4281
5025
|
tool: "kb_index",
|
|
4282
5026
|
usage: "index",
|
|
4283
5027
|
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:
|
|
5028
|
+
input: z25.object({ bundlePath }),
|
|
4285
5029
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4286
5030
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4287
5031
|
});
|
|
4288
5032
|
|
|
4289
5033
|
// src/commands/schema.ts
|
|
4290
|
-
import { z as
|
|
5034
|
+
import { z as z26 } from "zod";
|
|
4291
5035
|
var schemaCommand = define({
|
|
4292
5036
|
name: "schema",
|
|
4293
5037
|
tool: "kb_schema",
|
|
4294
5038
|
usage: "schema",
|
|
4295
5039
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4296
|
-
input:
|
|
5040
|
+
input: z26.object({}),
|
|
4297
5041
|
fromArgv: () => ({}),
|
|
4298
5042
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4299
5043
|
});
|
|
4300
5044
|
|
|
4301
5045
|
// src/commands/stamp.ts
|
|
4302
5046
|
import { readFile as readFile6 } from "fs/promises";
|
|
4303
|
-
import { z as
|
|
5047
|
+
import { z as z27 } from "zod";
|
|
4304
5048
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4305
5049
|
var stampCommand = define({
|
|
4306
5050
|
name: "stamp",
|
|
4307
5051
|
tool: "kb_stamp",
|
|
4308
5052
|
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:
|
|
5053
|
+
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.",
|
|
5054
|
+
input: z27.object({
|
|
5055
|
+
bundlePath: z27.string().min(1).optional().describe(
|
|
4312
5056
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
4313
5057
|
),
|
|
4314
|
-
since:
|
|
5058
|
+
since: z27.string().min(1).optional().describe(
|
|
4315
5059
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
4316
5060
|
)
|
|
4317
5061
|
}),
|
|
@@ -4350,7 +5094,7 @@ var stampCommand = define({
|
|
|
4350
5094
|
return reports;
|
|
4351
5095
|
},
|
|
4352
5096
|
render: (result) => result.map((report) => {
|
|
4353
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5097
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
4354
5098
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
4355
5099
|
return report.changed?.length ? `${head}
|
|
4356
5100
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -4397,16 +5141,16 @@ async function readBaseline(since) {
|
|
|
4397
5141
|
}
|
|
4398
5142
|
|
|
4399
5143
|
// src/commands/status.ts
|
|
4400
|
-
import { z as
|
|
5144
|
+
import { z as z28 } from "zod";
|
|
4401
5145
|
var statusCommand = define({
|
|
4402
5146
|
name: "status",
|
|
4403
5147
|
tool: "kb_status",
|
|
4404
5148
|
usage: "status <concept-id> <status>",
|
|
4405
5149
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
4406
|
-
input:
|
|
5150
|
+
input: z28.object({
|
|
4407
5151
|
bundlePath,
|
|
4408
5152
|
conceptId,
|
|
4409
|
-
status:
|
|
5153
|
+
status: z28.enum(KB_RECORD_STATUSES)
|
|
4410
5154
|
}),
|
|
4411
5155
|
fromArgv: (argv, path) => ({
|
|
4412
5156
|
bundlePath: path,
|
|
@@ -4421,13 +5165,13 @@ var statusCommand = define({
|
|
|
4421
5165
|
});
|
|
4422
5166
|
|
|
4423
5167
|
// src/commands/supersede.ts
|
|
4424
|
-
import { z as
|
|
5168
|
+
import { z as z29 } from "zod";
|
|
4425
5169
|
var supersedeCommand = define({
|
|
4426
5170
|
name: "supersede",
|
|
4427
5171
|
tool: "kb_supersede",
|
|
4428
5172
|
usage: "supersede <concept-id> <replacement-id>",
|
|
4429
5173
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
4430
|
-
input:
|
|
5174
|
+
input: z29.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
4431
5175
|
fromArgv: (argv, path) => ({
|
|
4432
5176
|
bundlePath: path,
|
|
4433
5177
|
conceptId: argv[1],
|
|
@@ -4441,16 +5185,16 @@ var supersedeCommand = define({
|
|
|
4441
5185
|
});
|
|
4442
5186
|
|
|
4443
5187
|
// src/commands/sync-instructions.ts
|
|
4444
|
-
import { z as
|
|
5188
|
+
import { z as z30 } from "zod";
|
|
4445
5189
|
var syncInstructionsCommand = define({
|
|
4446
5190
|
name: "sync-instructions",
|
|
4447
5191
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
4448
5192
|
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:
|
|
5193
|
+
input: z30.object({
|
|
5194
|
+
file: z30.string().min(1).describe("The instruction file to edit in place."),
|
|
5195
|
+
budgetTokens: z30.number().int().positive().optional(),
|
|
5196
|
+
fullUnderTokens: z30.number().int().positive().optional(),
|
|
5197
|
+
profile: z30.string().optional()
|
|
4454
5198
|
}),
|
|
4455
5199
|
fromArgv: (argv) => {
|
|
4456
5200
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -4476,17 +5220,17 @@ var syncInstructionsCommand = define({
|
|
|
4476
5220
|
});
|
|
4477
5221
|
|
|
4478
5222
|
// src/commands/trace.ts
|
|
4479
|
-
import { z as
|
|
5223
|
+
import { z as z31 } from "zod";
|
|
4480
5224
|
var traceCommand = define({
|
|
4481
5225
|
name: "trace",
|
|
4482
5226
|
tool: "kb_trace",
|
|
4483
5227
|
usage: "trace <concept-id> [edges...]",
|
|
4484
5228
|
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:
|
|
5229
|
+
input: z31.object({
|
|
4486
5230
|
bundlePath,
|
|
4487
5231
|
conceptId,
|
|
4488
|
-
edges:
|
|
4489
|
-
depth:
|
|
5232
|
+
edges: z31.array(z31.enum(TRACE_EDGES)).optional(),
|
|
5233
|
+
depth: z31.number().int().positive().optional()
|
|
4490
5234
|
}),
|
|
4491
5235
|
fromArgv: (argv, path) => ({
|
|
4492
5236
|
bundlePath: path,
|
|
@@ -4508,37 +5252,37 @@ var traceCommand = define({
|
|
|
4508
5252
|
});
|
|
4509
5253
|
|
|
4510
5254
|
// src/commands/types.ts
|
|
4511
|
-
import { z as
|
|
5255
|
+
import { z as z32 } from "zod";
|
|
4512
5256
|
var typesCommand = define({
|
|
4513
5257
|
name: "types",
|
|
4514
5258
|
tool: "kb_types",
|
|
4515
5259
|
usage: "types",
|
|
4516
5260
|
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:
|
|
5261
|
+
input: z32.object({}),
|
|
4518
5262
|
fromArgv: () => ({}),
|
|
4519
5263
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
4520
5264
|
});
|
|
4521
5265
|
|
|
4522
5266
|
// src/commands/unpin.ts
|
|
4523
|
-
import { z as
|
|
5267
|
+
import { z as z33 } from "zod";
|
|
4524
5268
|
var unpinCommand = define({
|
|
4525
5269
|
name: "unpin",
|
|
4526
5270
|
tool: "kb_unpin",
|
|
4527
5271
|
usage: "unpin [bundle-path]",
|
|
4528
5272
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
4529
|
-
input:
|
|
5273
|
+
input: z33.object({ bundlePath }),
|
|
4530
5274
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
4531
5275
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
4532
5276
|
});
|
|
4533
5277
|
|
|
4534
5278
|
// src/commands/validate.ts
|
|
4535
|
-
import { z as
|
|
5279
|
+
import { z as z34 } from "zod";
|
|
4536
5280
|
var validateCommand = define({
|
|
4537
5281
|
name: "validate",
|
|
4538
5282
|
tool: "kb_validate",
|
|
4539
5283
|
usage: "validate",
|
|
4540
5284
|
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:
|
|
5285
|
+
input: z34.object({ bundlePath }),
|
|
4542
5286
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4543
5287
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
4544
5288
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -4548,16 +5292,16 @@ var validateCommand = define({
|
|
|
4548
5292
|
});
|
|
4549
5293
|
|
|
4550
5294
|
// src/commands/verify.ts
|
|
4551
|
-
import { z as
|
|
5295
|
+
import { z as z35 } from "zod";
|
|
4552
5296
|
var verifyCommand = define({
|
|
4553
5297
|
name: "verify",
|
|
4554
5298
|
tool: "kb_verify",
|
|
4555
5299
|
usage: "verify <concept-id> --note <text>",
|
|
4556
5300
|
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:
|
|
5301
|
+
input: z35.object({
|
|
4558
5302
|
bundlePath,
|
|
4559
5303
|
conceptId,
|
|
4560
|
-
note:
|
|
5304
|
+
note: z35.string().refine((s) => s.trim().length > 0, {
|
|
4561
5305
|
message: "note must say what the check found"
|
|
4562
5306
|
})
|
|
4563
5307
|
}),
|
|
@@ -4577,15 +5321,15 @@ var verifyCommand = define({
|
|
|
4577
5321
|
});
|
|
4578
5322
|
|
|
4579
5323
|
// src/commands/write.ts
|
|
4580
|
-
import { z as
|
|
5324
|
+
import { z as z36 } from "zod";
|
|
4581
5325
|
var writeCommand = define({
|
|
4582
5326
|
name: "write",
|
|
4583
5327
|
tool: "kb_write",
|
|
4584
5328
|
usage: "write <type> < record.json",
|
|
4585
5329
|
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:
|
|
5330
|
+
input: z36.object({
|
|
4587
5331
|
bundlePath,
|
|
4588
|
-
type:
|
|
5332
|
+
type: z36.enum(KB_RECORD_TYPES),
|
|
4589
5333
|
input: composeInputSchema
|
|
4590
5334
|
}),
|
|
4591
5335
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -4609,13 +5353,13 @@ var writeCommand = define({
|
|
|
4609
5353
|
});
|
|
4610
5354
|
|
|
4611
5355
|
// src/commands/write-decision.ts
|
|
4612
|
-
import { z as
|
|
5356
|
+
import { z as z37 } from "zod";
|
|
4613
5357
|
var writeDecisionCommand = define({
|
|
4614
5358
|
name: "write-decision",
|
|
4615
5359
|
tool: "kb_write_decision",
|
|
4616
5360
|
usage: "write-decision < decision.json",
|
|
4617
5361
|
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:
|
|
5362
|
+
input: z37.object({ bundlePath, input: decisionInputSchema }),
|
|
4619
5363
|
fromArgv: async (_argv, path, stdin) => ({
|
|
4620
5364
|
bundlePath: path,
|
|
4621
5365
|
input: JSON.parse(await stdin())
|
|
@@ -4645,6 +5389,7 @@ var KB_COMMANDS = [
|
|
|
4645
5389
|
answerCommand,
|
|
4646
5390
|
verifyCommand,
|
|
4647
5391
|
anchorResolveCommand,
|
|
5392
|
+
reassessCommand,
|
|
4648
5393
|
loadCommand,
|
|
4649
5394
|
catalogCommand,
|
|
4650
5395
|
packCommand,
|
|
@@ -4696,7 +5441,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4696
5441
|
}
|
|
4697
5442
|
|
|
4698
5443
|
// src/search-index.ts
|
|
4699
|
-
import { stat as
|
|
5444
|
+
import { stat as stat3 } from "fs/promises";
|
|
4700
5445
|
import { join as join6 } from "path";
|
|
4701
5446
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4702
5447
|
var COLLECTION = "kb";
|
|
@@ -4741,7 +5486,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4741
5486
|
}
|
|
4742
5487
|
}
|
|
4743
5488
|
async function isStale(bundlePath2) {
|
|
4744
|
-
const indexAt = await
|
|
5489
|
+
const indexAt = await stat3(join6(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4745
5490
|
if (!indexAt) return true;
|
|
4746
5491
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4747
5492
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4750,8 +5495,8 @@ async function isStale(bundlePath2) {
|
|
|
4750
5495
|
let stale = false;
|
|
4751
5496
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4752
5497
|
if (stale) return;
|
|
4753
|
-
const
|
|
4754
|
-
if (
|
|
5498
|
+
const at2 = await stat3(join6(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5499
|
+
if (at2 > indexAt) stale = true;
|
|
4755
5500
|
});
|
|
4756
5501
|
return stale;
|
|
4757
5502
|
}
|
|
@@ -5132,8 +5877,8 @@ var KbStore = class {
|
|
|
5132
5877
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
5133
5878
|
* logs what it publishes.
|
|
5134
5879
|
*/
|
|
5135
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
5136
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
5880
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5881
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
5137
5882
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
5138
5883
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
5139
5884
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -5185,14 +5930,14 @@ var KbStore = class {
|
|
|
5185
5930
|
return superseded;
|
|
5186
5931
|
}
|
|
5187
5932
|
/** Resolves an open question, stamping who answered and when. */
|
|
5188
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
5933
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5189
5934
|
return this.mutate(
|
|
5190
5935
|
bundlePath2,
|
|
5191
5936
|
conceptId2,
|
|
5192
5937
|
(frontmatter) => ({
|
|
5193
5938
|
...frontmatter,
|
|
5194
5939
|
strauss_status: "resolved",
|
|
5195
|
-
strauss_answered: { by: actor, at }
|
|
5940
|
+
strauss_answered: { by: actor, at: at2 }
|
|
5196
5941
|
}),
|
|
5197
5942
|
{ operation: "answer", by: actor },
|
|
5198
5943
|
(body) => `${body.trimEnd()}
|
|
@@ -5361,24 +6106,34 @@ ${answer}
|
|
|
5361
6106
|
}
|
|
5362
6107
|
/**
|
|
5363
6108
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
5364
|
-
* the same way, handed back as a stamp.
|
|
5365
|
-
*
|
|
5366
|
-
*
|
|
6109
|
+
* the same way, handed back as a stamp.
|
|
6110
|
+
*
|
|
6111
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
6112
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
6113
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
6114
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
5367
6115
|
*/
|
|
5368
|
-
async stamp(bundlePath2) {
|
|
6116
|
+
async stamp(bundlePath2, options = {}) {
|
|
5369
6117
|
const bundle = await this.list(bundlePath2);
|
|
5370
6118
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
5371
6119
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
5372
6120
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5373
6121
|
const stamped = bundleStamp(current, superseded);
|
|
5374
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
6122
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
6123
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
6124
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
6125
|
+
(entries) => entries.some(
|
|
6126
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
6127
|
+
)
|
|
6128
|
+
).length;
|
|
5375
6129
|
return {
|
|
5376
6130
|
path: bundlePath2,
|
|
5377
6131
|
digest: stamped.digest,
|
|
5378
6132
|
recordCount: bundle.length,
|
|
5379
6133
|
superseded: superseded.length,
|
|
5380
6134
|
newestAt: dates.at(-1) ?? null,
|
|
5381
|
-
records: stamped.records
|
|
6135
|
+
records: stamped.records,
|
|
6136
|
+
drifted: drifted2
|
|
5382
6137
|
};
|
|
5383
6138
|
}
|
|
5384
6139
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -5786,7 +6541,7 @@ function typeRank(record) {
|
|
|
5786
6541
|
}
|
|
5787
6542
|
|
|
5788
6543
|
// src/version.ts
|
|
5789
|
-
var VERSION = true ? "0.1.
|
|
6544
|
+
var VERSION = true ? "0.1.18" : "0.0.0-dev";
|
|
5790
6545
|
|
|
5791
6546
|
export {
|
|
5792
6547
|
kbSourceSchema,
|
|
@@ -5875,6 +6630,9 @@ export {
|
|
|
5875
6630
|
CONTEXT_BEGIN,
|
|
5876
6631
|
CONTEXT_END,
|
|
5877
6632
|
syncInstructions,
|
|
6633
|
+
classifyDrift,
|
|
6634
|
+
unifiedDiff,
|
|
6635
|
+
reassessPacket,
|
|
5878
6636
|
KB_EDGE_KINDS,
|
|
5879
6637
|
DEFAULT_TYPED_LINK_RELS,
|
|
5880
6638
|
neighbours,
|
|
@@ -5912,4 +6670,4 @@ export {
|
|
|
5912
6670
|
KbStore,
|
|
5913
6671
|
VERSION
|
|
5914
6672
|
};
|
|
5915
|
-
//# sourceMappingURL=chunk-
|
|
6673
|
+
//# sourceMappingURL=chunk-GSOTMWZZ.js.map
|