@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
package/dist/index.cjs
CHANGED
|
@@ -85,6 +85,7 @@ __export(index_exports, {
|
|
|
85
85
|
backlinks: () => backlinks,
|
|
86
86
|
buildContext: () => buildContext,
|
|
87
87
|
catalog: () => catalog,
|
|
88
|
+
classifyDrift: () => classifyDrift,
|
|
88
89
|
composeDecisionRecord: () => composeDecisionRecord,
|
|
89
90
|
composeInputSchema: () => composeInputSchema,
|
|
90
91
|
composeLinkSchema: () => composeLinkSchema,
|
|
@@ -132,6 +133,7 @@ __export(index_exports, {
|
|
|
132
133
|
readMergedPins: () => readMergedPins,
|
|
133
134
|
readPinsLayer: () => readPinsLayer,
|
|
134
135
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
136
|
+
reassessPacket: () => reassessPacket,
|
|
135
137
|
regexResolver: () => regexResolver,
|
|
136
138
|
renderCatalogLine: () => renderCatalogLine,
|
|
137
139
|
renderIndex: () => renderIndex,
|
|
@@ -153,6 +155,7 @@ __export(index_exports, {
|
|
|
153
155
|
toHookJson: () => toHookJson,
|
|
154
156
|
trace: () => trace,
|
|
155
157
|
treeSitterLanguages: () => treeSitterLanguages,
|
|
158
|
+
unifiedDiff: () => unifiedDiff,
|
|
156
159
|
unpinBase: () => unpinBase,
|
|
157
160
|
validateBundle: () => validateBundle
|
|
158
161
|
});
|
|
@@ -181,9 +184,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
181
184
|
{ length: Math.min(limit, items.length) },
|
|
182
185
|
async () => {
|
|
183
186
|
while (!failed && next < items.length) {
|
|
184
|
-
const
|
|
187
|
+
const at2 = next++;
|
|
185
188
|
try {
|
|
186
|
-
out[
|
|
189
|
+
out[at2] = await fn(items[at2], at2);
|
|
187
190
|
} catch (error) {
|
|
188
191
|
failed = true;
|
|
189
192
|
throw error;
|
|
@@ -262,6 +265,14 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
262
265
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
263
266
|
message: "hash must be sha256:<64 hex chars>"
|
|
264
267
|
}).optional(),
|
|
268
|
+
/**
|
|
269
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
270
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
271
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
272
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
273
|
+
* comments, so reformatting the anchored code is not drift.
|
|
274
|
+
*/
|
|
275
|
+
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
265
276
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
266
277
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
267
278
|
/** Line count of the text the hash was taken over. */
|
|
@@ -676,8 +687,8 @@ function safeSegment(value) {
|
|
|
676
687
|
function revRef(rev) {
|
|
677
688
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
678
689
|
let hash = 5381;
|
|
679
|
-
for (let
|
|
680
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
690
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
691
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
681
692
|
}
|
|
682
693
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
683
694
|
}
|
|
@@ -786,8 +797,8 @@ function repoUrlIsSafe(repo) {
|
|
|
786
797
|
if (!scheme?.[1]) return false;
|
|
787
798
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
788
799
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
789
|
-
const
|
|
790
|
-
return
|
|
800
|
+
const at2 = authority.lastIndexOf("@");
|
|
801
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
791
802
|
}
|
|
792
803
|
function protocolArgs() {
|
|
793
804
|
const allowed = allowedProtocols();
|
|
@@ -878,7 +889,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
878
889
|
return new Map([
|
|
879
890
|
...rejected2,
|
|
880
891
|
...wants.map(
|
|
881
|
-
(want,
|
|
892
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
882
893
|
)
|
|
883
894
|
]);
|
|
884
895
|
}
|
|
@@ -1067,7 +1078,8 @@ function warningAnchor(entry) {
|
|
|
1067
1078
|
diffSize,
|
|
1068
1079
|
...reason !== void 0 ? { reason } : {},
|
|
1069
1080
|
...repo !== void 0 ? { repo } : {},
|
|
1070
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
1081
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
1082
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
1071
1083
|
};
|
|
1072
1084
|
}
|
|
1073
1085
|
function resolveHeads(from, byId) {
|
|
@@ -1238,7 +1250,7 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
1238
1250
|
return { ok: false, reason: "file-unreadable" };
|
|
1239
1251
|
}
|
|
1240
1252
|
});
|
|
1241
|
-
return new Map(wanted.map((file,
|
|
1253
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
1242
1254
|
}
|
|
1243
1255
|
|
|
1244
1256
|
// src/anchor-resolver/resolver.ts
|
|
@@ -1428,8 +1440,8 @@ async function ensureGrammar(language, options = {}) {
|
|
|
1428
1440
|
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1429
1441
|
const parts = [];
|
|
1430
1442
|
const total = pack2.tags.length;
|
|
1431
|
-
for (const [
|
|
1432
|
-
const name = `${language} tags${total > 1 ? ` part ${
|
|
1443
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1444
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1433
1445
|
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1434
1446
|
const held = await ensurePart(path, name, part, options);
|
|
1435
1447
|
if (held !== true) return miss(language, name, held);
|
|
@@ -1577,8 +1589,8 @@ function typeNameIn(receiver) {
|
|
|
1577
1589
|
while (stack.length) {
|
|
1578
1590
|
const node = stack.pop();
|
|
1579
1591
|
if (node.type === "type_identifier") return node.text;
|
|
1580
|
-
for (let
|
|
1581
|
-
const child = node.child(
|
|
1592
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1593
|
+
const child = node.child(at2);
|
|
1582
1594
|
if (child) stack.push(child);
|
|
1583
1595
|
}
|
|
1584
1596
|
}
|
|
@@ -1587,7 +1599,7 @@ function typeNameIn(receiver) {
|
|
|
1587
1599
|
function endsWith(chain, wanted) {
|
|
1588
1600
|
if (wanted.length > chain.length) return false;
|
|
1589
1601
|
const offset = chain.length - wanted.length;
|
|
1590
|
-
return wanted.every((segment,
|
|
1602
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1591
1603
|
}
|
|
1592
1604
|
function width(node) {
|
|
1593
1605
|
return node.endIndex - node.startIndex;
|
|
@@ -1613,6 +1625,26 @@ function spanOf(definition, source) {
|
|
|
1613
1625
|
};
|
|
1614
1626
|
}
|
|
1615
1627
|
|
|
1628
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1629
|
+
function tokens(root) {
|
|
1630
|
+
const out = [];
|
|
1631
|
+
const stack = [root];
|
|
1632
|
+
while (stack.length) {
|
|
1633
|
+
const node = stack.pop();
|
|
1634
|
+
if (node.type.includes("comment")) continue;
|
|
1635
|
+
if (node.childCount === 0) {
|
|
1636
|
+
const text = node.text.trim();
|
|
1637
|
+
if (text) out.push(text);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1641
|
+
const child = node.child(at2);
|
|
1642
|
+
if (child) stack.push(child);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1616
1648
|
// src/tree-sitter-resolver/resolver.ts
|
|
1617
1649
|
var TREE_CACHE_LIMIT = 32;
|
|
1618
1650
|
var TreeSitterResolver = class {
|
|
@@ -1658,7 +1690,7 @@ var TreeSitterResolver = class {
|
|
|
1658
1690
|
(language) => this.load(language)
|
|
1659
1691
|
);
|
|
1660
1692
|
languages.forEach(
|
|
1661
|
-
(language,
|
|
1693
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1662
1694
|
);
|
|
1663
1695
|
}
|
|
1664
1696
|
/**
|
|
@@ -1746,6 +1778,60 @@ var TreeSitterResolver = class {
|
|
|
1746
1778
|
this.trees.set(key2, parsed);
|
|
1747
1779
|
return parsed;
|
|
1748
1780
|
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1783
|
+
*
|
|
1784
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1785
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1786
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1787
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1788
|
+
*/
|
|
1789
|
+
spans(source, file) {
|
|
1790
|
+
const language = languageForFile(file);
|
|
1791
|
+
if (!language) return [];
|
|
1792
|
+
const loaded = this.loaded.get(language);
|
|
1793
|
+
if (!loaded) return [];
|
|
1794
|
+
const parsed = this.parse(language, loaded, source);
|
|
1795
|
+
if (!parsed) return [];
|
|
1796
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1797
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1798
|
+
span: spanOf(definition, source)
|
|
1799
|
+
}));
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1803
|
+
* joined by single spaces.
|
|
1804
|
+
*
|
|
1805
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1806
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1807
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1808
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1809
|
+
* those are leaves.
|
|
1810
|
+
*
|
|
1811
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1812
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1813
|
+
*/
|
|
1814
|
+
normalize(text, file) {
|
|
1815
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1816
|
+
if (!language) return null;
|
|
1817
|
+
const loaded = this.loaded.get(language);
|
|
1818
|
+
if (!loaded) return null;
|
|
1819
|
+
const parser = this.parser;
|
|
1820
|
+
if (!parser) return null;
|
|
1821
|
+
let tree;
|
|
1822
|
+
try {
|
|
1823
|
+
parser.setLanguage(loaded.language);
|
|
1824
|
+
tree = parser.parse(text);
|
|
1825
|
+
} catch {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
if (!tree) return null;
|
|
1829
|
+
try {
|
|
1830
|
+
return tokens(tree.rootNode).join(" ");
|
|
1831
|
+
} finally {
|
|
1832
|
+
tree.delete();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1749
1835
|
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1750
1836
|
reset() {
|
|
1751
1837
|
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
@@ -1899,7 +1985,7 @@ var regexResolver = {
|
|
|
1899
1985
|
);
|
|
1900
1986
|
const nearest = Math.min(...distances);
|
|
1901
1987
|
if (Number.isFinite(nearest)) {
|
|
1902
|
-
candidates = candidates.filter((_,
|
|
1988
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1903
1989
|
}
|
|
1904
1990
|
}
|
|
1905
1991
|
if (candidates.length !== 1) return null;
|
|
@@ -1914,8 +2000,8 @@ function escapeRegExp(value) {
|
|
|
1914
2000
|
}
|
|
1915
2001
|
function distanceToParent(lines, index2, parent) {
|
|
1916
2002
|
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1917
|
-
for (let
|
|
1918
|
-
if (parent.test(lines[
|
|
2003
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
2004
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1919
2005
|
}
|
|
1920
2006
|
return Number.POSITIVE_INFINITY;
|
|
1921
2007
|
}
|
|
@@ -1947,10 +2033,12 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1947
2033
|
if (attempt.reason === "symbol-not-found") continue;
|
|
1948
2034
|
return { ok: false, reason: attempt.reason };
|
|
1949
2035
|
}
|
|
2036
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1950
2037
|
return {
|
|
1951
2038
|
ok: true,
|
|
1952
2039
|
span: attempt.span,
|
|
1953
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
2040
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
2041
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1954
2042
|
};
|
|
1955
2043
|
}
|
|
1956
2044
|
return { ok: false, reason: "symbol-not-found" };
|
|
@@ -1978,6 +2066,11 @@ function resolverChanged(source, anchor, produced) {
|
|
|
1978
2066
|
);
|
|
1979
2067
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1980
2068
|
}
|
|
2069
|
+
function anchorHashOf(anchor, outcome) {
|
|
2070
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2071
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2072
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2073
|
+
}
|
|
1981
2074
|
|
|
1982
2075
|
// src/anchor-resolver/drift.ts
|
|
1983
2076
|
async function detectAnchorDrift(records, options = {}) {
|
|
@@ -2057,16 +2150,29 @@ function unresolved(anchor, reason, repo) {
|
|
|
2057
2150
|
state: "unresolved",
|
|
2058
2151
|
diffSize: null,
|
|
2059
2152
|
...reason ? { reason } : {},
|
|
2060
|
-
...repo ? { repo } : {}
|
|
2153
|
+
...repo ? { repo } : {},
|
|
2154
|
+
...classOf(reason)
|
|
2061
2155
|
};
|
|
2062
2156
|
}
|
|
2157
|
+
function provisionalDriftClass(entry) {
|
|
2158
|
+
if (entry.state === "unresolved") {
|
|
2159
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
2160
|
+
}
|
|
2161
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
2162
|
+
}
|
|
2163
|
+
function classOf(reason) {
|
|
2164
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
2165
|
+
return settled ? { class: settled } : {};
|
|
2166
|
+
}
|
|
2063
2167
|
function hashIn(source, anchor, resolvers) {
|
|
2064
2168
|
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2065
2169
|
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2170
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2066
2171
|
return {
|
|
2067
2172
|
ok: true,
|
|
2068
2173
|
current: {
|
|
2069
|
-
hash
|
|
2174
|
+
hash,
|
|
2175
|
+
kind,
|
|
2070
2176
|
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2071
2177
|
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2072
2178
|
}
|
|
@@ -2079,11 +2185,14 @@ function resolverExtras(source, anchor, current) {
|
|
|
2079
2185
|
};
|
|
2080
2186
|
}
|
|
2081
2187
|
function compared(anchor, current, extra = {}) {
|
|
2188
|
+
const matched = current.hash === anchor.hash;
|
|
2082
2189
|
return {
|
|
2083
2190
|
...base(anchor),
|
|
2084
|
-
state:
|
|
2191
|
+
state: matched ? "match" : "drifted",
|
|
2085
2192
|
currentHash: current.hash,
|
|
2193
|
+
hashKind: current.kind,
|
|
2086
2194
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
2195
|
+
...matched ? {} : { class: "changed" },
|
|
2087
2196
|
...extra
|
|
2088
2197
|
};
|
|
2089
2198
|
}
|
|
@@ -2240,8 +2349,8 @@ async function isStale(bundlePath2) {
|
|
|
2240
2349
|
let stale = false;
|
|
2241
2350
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
2242
2351
|
if (stale) return;
|
|
2243
|
-
const
|
|
2244
|
-
if (
|
|
2352
|
+
const at2 = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2353
|
+
if (at2 > indexAt) stale = true;
|
|
2245
2354
|
});
|
|
2246
2355
|
return stale;
|
|
2247
2356
|
}
|
|
@@ -2536,8 +2645,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
2536
2645
|
return [...reached.values()].sort(byGeneratedAt);
|
|
2537
2646
|
}
|
|
2538
2647
|
function byGeneratedAt(left, right) {
|
|
2539
|
-
const
|
|
2540
|
-
return
|
|
2648
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
2649
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
2541
2650
|
}
|
|
2542
2651
|
|
|
2543
2652
|
// src/pack.ts
|
|
@@ -2973,8 +3082,8 @@ var KbStore = class {
|
|
|
2973
3082
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
2974
3083
|
* logs what it publishes.
|
|
2975
3084
|
*/
|
|
2976
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
2977
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
3085
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3086
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
2978
3087
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
2979
3088
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
2980
3089
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -3026,14 +3135,14 @@ var KbStore = class {
|
|
|
3026
3135
|
return superseded;
|
|
3027
3136
|
}
|
|
3028
3137
|
/** Resolves an open question, stamping who answered and when. */
|
|
3029
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
3138
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3030
3139
|
return this.mutate(
|
|
3031
3140
|
bundlePath2,
|
|
3032
3141
|
conceptId2,
|
|
3033
3142
|
(frontmatter) => ({
|
|
3034
3143
|
...frontmatter,
|
|
3035
3144
|
strauss_status: "resolved",
|
|
3036
|
-
strauss_answered: { by: actor, at }
|
|
3145
|
+
strauss_answered: { by: actor, at: at2 }
|
|
3037
3146
|
}),
|
|
3038
3147
|
{ operation: "answer", by: actor },
|
|
3039
3148
|
(body) => `${body.trimEnd()}
|
|
@@ -3202,24 +3311,34 @@ ${answer}
|
|
|
3202
3311
|
}
|
|
3203
3312
|
/**
|
|
3204
3313
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
3205
|
-
* the same way, handed back as a stamp.
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3314
|
+
* the same way, handed back as a stamp.
|
|
3315
|
+
*
|
|
3316
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
3317
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
3318
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
3319
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
3208
3320
|
*/
|
|
3209
|
-
async stamp(bundlePath2) {
|
|
3321
|
+
async stamp(bundlePath2, options = {}) {
|
|
3210
3322
|
const bundle = await this.list(bundlePath2);
|
|
3211
3323
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
3212
3324
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
3213
3325
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3214
3326
|
const stamped = bundleStamp(current, superseded);
|
|
3215
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
3327
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
3328
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
3329
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
3330
|
+
(entries) => entries.some(
|
|
3331
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
3332
|
+
)
|
|
3333
|
+
).length;
|
|
3216
3334
|
return {
|
|
3217
3335
|
path: bundlePath2,
|
|
3218
3336
|
digest: stamped.digest,
|
|
3219
3337
|
recordCount: bundle.length,
|
|
3220
3338
|
superseded: superseded.length,
|
|
3221
3339
|
newestAt: dates.at(-1) ?? null,
|
|
3222
|
-
records: stamped.records
|
|
3340
|
+
records: stamped.records,
|
|
3341
|
+
drifted: drifted2
|
|
3223
3342
|
};
|
|
3224
3343
|
}
|
|
3225
3344
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -3876,7 +3995,7 @@ async function listPins(store, workspaceDir) {
|
|
|
3876
3995
|
}
|
|
3877
3996
|
|
|
3878
3997
|
// src/kb-pins/pin.ts
|
|
3879
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
3998
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
3880
3999
|
const layer = options.layer ?? "project";
|
|
3881
4000
|
const root = layerRoot(workspaceDir, layer);
|
|
3882
4001
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -3904,7 +4023,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3904
4023
|
return {
|
|
3905
4024
|
path: existing.path,
|
|
3906
4025
|
layer,
|
|
3907
|
-
pinnedAt: existing.pinnedAt ??
|
|
4026
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
3908
4027
|
alreadyPinned: true,
|
|
3909
4028
|
...updated.mode ? { mode: updated.mode } : {},
|
|
3910
4029
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -3914,7 +4033,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3914
4033
|
}
|
|
3915
4034
|
const entry = {
|
|
3916
4035
|
path: storablePath(root, bundlePath2),
|
|
3917
|
-
pinnedAt:
|
|
4036
|
+
pinnedAt: at2,
|
|
3918
4037
|
...fields
|
|
3919
4038
|
};
|
|
3920
4039
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -3924,7 +4043,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3924
4043
|
return {
|
|
3925
4044
|
path: entry.path,
|
|
3926
4045
|
layer,
|
|
3927
|
-
pinnedAt:
|
|
4046
|
+
pinnedAt: at2,
|
|
3928
4047
|
alreadyPinned: false,
|
|
3929
4048
|
...fields,
|
|
3930
4049
|
...warning ? { warning } : {}
|
|
@@ -4452,18 +4571,18 @@ function expired(hits, now) {
|
|
|
4452
4571
|
for (const hit of hits) {
|
|
4453
4572
|
const raw = hit.record.frontmatter.stale_after;
|
|
4454
4573
|
if (!raw) continue;
|
|
4455
|
-
const
|
|
4456
|
-
if (Number.isNaN(
|
|
4574
|
+
const at2 = Date.parse(raw);
|
|
4575
|
+
if (Number.isNaN(at2)) {
|
|
4457
4576
|
findings.push(
|
|
4458
4577
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
4459
4578
|
);
|
|
4460
4579
|
continue;
|
|
4461
4580
|
}
|
|
4462
|
-
if (
|
|
4581
|
+
if (at2 < now.getTime()) {
|
|
4463
4582
|
findings.push(
|
|
4464
4583
|
finding(
|
|
4465
4584
|
hit.record,
|
|
4466
|
-
`stale since ${raw} (${daysBetween(
|
|
4585
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
4467
4586
|
)
|
|
4468
4587
|
);
|
|
4469
4588
|
}
|
|
@@ -4476,12 +4595,12 @@ function expiring(hits, now, withinDays) {
|
|
|
4476
4595
|
for (const hit of hits) {
|
|
4477
4596
|
const raw = hit.record.frontmatter.stale_after;
|
|
4478
4597
|
if (!raw) continue;
|
|
4479
|
-
const
|
|
4480
|
-
if (Number.isNaN(
|
|
4598
|
+
const at2 = Date.parse(raw);
|
|
4599
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
4481
4600
|
findings.push(
|
|
4482
4601
|
finding(
|
|
4483
4602
|
hit.record,
|
|
4484
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
4603
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
4485
4604
|
)
|
|
4486
4605
|
);
|
|
4487
4606
|
}
|
|
@@ -4651,13 +4770,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
4651
4770
|
);
|
|
4652
4771
|
}
|
|
4653
4772
|
function describeAnchor(anchor) {
|
|
4654
|
-
const
|
|
4655
|
-
if (anchor.
|
|
4773
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
4774
|
+
if (anchor.class === "gone") {
|
|
4775
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
4776
|
+
}
|
|
4777
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
4656
4778
|
if (anchor.remoteState === "drifted-on-default") {
|
|
4657
|
-
return `${
|
|
4779
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
4658
4780
|
}
|
|
4659
|
-
if (anchor.diffSize === null) return `${
|
|
4660
|
-
return anchor.diffSize === 0 ? `${
|
|
4781
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
4782
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
4661
4783
|
}
|
|
4662
4784
|
function replaces(later, earlier) {
|
|
4663
4785
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -4674,9 +4796,9 @@ function daysBetween(from, to) {
|
|
|
4674
4796
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
4675
4797
|
}
|
|
4676
4798
|
function ageInDays(record, now) {
|
|
4677
|
-
const
|
|
4678
|
-
if (!
|
|
4679
|
-
const written = Date.parse(
|
|
4799
|
+
const at2 = record.frontmatter.generated?.at;
|
|
4800
|
+
if (!at2) return null;
|
|
4801
|
+
const written = Date.parse(at2);
|
|
4680
4802
|
if (Number.isNaN(written)) return null;
|
|
4681
4803
|
return daysBetween(written, now.getTime());
|
|
4682
4804
|
}
|
|
@@ -4748,9 +4870,9 @@ function argvFlag(argv, name) {
|
|
|
4748
4870
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
4749
4871
|
return value2;
|
|
4750
4872
|
}
|
|
4751
|
-
const
|
|
4752
|
-
if (
|
|
4753
|
-
const value = argv[
|
|
4873
|
+
const at2 = argv.indexOf(name);
|
|
4874
|
+
if (at2 === -1) return void 0;
|
|
4875
|
+
const value = argv[at2 + 1];
|
|
4754
4876
|
if (value === void 0 || value.startsWith("--")) {
|
|
4755
4877
|
throw new KbMissingFlagValueError(name);
|
|
4756
4878
|
}
|
|
@@ -4843,11 +4965,14 @@ var anchorResolveCommand = define({
|
|
|
4843
4965
|
}
|
|
4844
4966
|
const resolved = outcome.span;
|
|
4845
4967
|
const producedBy = outcome.resolver;
|
|
4846
|
-
const currentHash =
|
|
4968
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
4847
4969
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
4970
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
4971
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
4848
4972
|
const stamped = {
|
|
4849
4973
|
...anchor,
|
|
4850
|
-
hash:
|
|
4974
|
+
hash: stampedHash,
|
|
4975
|
+
hash_kind: stampedKind,
|
|
4851
4976
|
lines: currentLines,
|
|
4852
4977
|
resolved_at: now(),
|
|
4853
4978
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -4857,7 +4982,8 @@ var anchorResolveCommand = define({
|
|
|
4857
4982
|
results.push({
|
|
4858
4983
|
...base2,
|
|
4859
4984
|
state: "stamped",
|
|
4860
|
-
currentHash,
|
|
4985
|
+
currentHash: stampedHash,
|
|
4986
|
+
hashKind: stampedKind,
|
|
4861
4987
|
...producedBy ? { resolver: producedBy } : {}
|
|
4862
4988
|
});
|
|
4863
4989
|
updated.push(stamped);
|
|
@@ -4869,6 +4995,7 @@ var anchorResolveCommand = define({
|
|
|
4869
4995
|
...base2,
|
|
4870
4996
|
state: "drifted",
|
|
4871
4997
|
currentHash,
|
|
4998
|
+
hashKind: kind,
|
|
4872
4999
|
diffSize: lineDelta(anchor, currentLines),
|
|
4873
5000
|
...producedBy ? { resolver: producedBy } : {},
|
|
4874
5001
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
@@ -4897,6 +5024,7 @@ var anchorResolveCommand = define({
|
|
|
4897
5024
|
...base2,
|
|
4898
5025
|
state: "match",
|
|
4899
5026
|
currentHash,
|
|
5027
|
+
hashKind: kind,
|
|
4900
5028
|
...producedBy ? { resolver: producedBy } : {},
|
|
4901
5029
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4902
5030
|
});
|
|
@@ -5180,14 +5308,567 @@ var contextCommand = define({
|
|
|
5180
5308
|
});
|
|
5181
5309
|
|
|
5182
5310
|
// src/commands/doctor.ts
|
|
5311
|
+
var import_zod15 = require("zod");
|
|
5312
|
+
|
|
5313
|
+
// src/drift/git.ts
|
|
5314
|
+
var import_node_child_process3 = require("child_process");
|
|
5315
|
+
var import_node_util3 = require("util");
|
|
5316
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
5317
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
5318
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
5319
|
+
async function git2(cwd, args) {
|
|
5320
|
+
const env = { ...process.env };
|
|
5321
|
+
delete env["GIT_DIR"];
|
|
5322
|
+
delete env["GIT_WORK_TREE"];
|
|
5323
|
+
delete env["GIT_INDEX_FILE"];
|
|
5324
|
+
try {
|
|
5325
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
5326
|
+
timeout: GIT_TIMEOUT_MS,
|
|
5327
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
5328
|
+
env
|
|
5329
|
+
});
|
|
5330
|
+
return { ok: true, stdout };
|
|
5331
|
+
} catch {
|
|
5332
|
+
return { ok: false };
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
async function listRepoFiles(repoRoot) {
|
|
5336
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
5337
|
+
if (!result.ok) return [];
|
|
5338
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
5339
|
+
}
|
|
5340
|
+
async function readOldSource(repoRoot, anchor) {
|
|
5341
|
+
if (!filePathIsSafe(anchor.file))
|
|
5342
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5343
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
5344
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
5345
|
+
if (shown2 !== null) {
|
|
5346
|
+
return {
|
|
5347
|
+
ok: true,
|
|
5348
|
+
source: shown2,
|
|
5349
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
5350
|
+
};
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
const at2 = anchor.resolved_at;
|
|
5354
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
5355
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5356
|
+
}
|
|
5357
|
+
const found = await git2(repoRoot, [
|
|
5358
|
+
"log",
|
|
5359
|
+
"-1",
|
|
5360
|
+
"--format=%H",
|
|
5361
|
+
`--before=${at2}`,
|
|
5362
|
+
"--end-of-options",
|
|
5363
|
+
"HEAD",
|
|
5364
|
+
"--",
|
|
5365
|
+
anchor.file
|
|
5366
|
+
]);
|
|
5367
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
5368
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
5369
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5370
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
5371
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
5372
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
5373
|
+
}
|
|
5374
|
+
async function showFile(repoRoot, ref, file) {
|
|
5375
|
+
const path = file.replace(/^\.\//, "");
|
|
5376
|
+
const result = await git2(repoRoot, [
|
|
5377
|
+
"show",
|
|
5378
|
+
"--end-of-options",
|
|
5379
|
+
`${ref}:${path}`
|
|
5380
|
+
]);
|
|
5381
|
+
return result.ok ? result.stdout : null;
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
// src/drift/moved.ts
|
|
5385
|
+
var import_promises9 = require("fs/promises");
|
|
5386
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
5387
|
+
var SEARCH_BATCH = 64;
|
|
5388
|
+
function movedSearch(repoRoot, options = {}) {
|
|
5389
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
5390
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
5391
|
+
const resolver = new TreeSitterResolver();
|
|
5392
|
+
let repoFiles;
|
|
5393
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
5394
|
+
const filesForLanguage = async (language) => {
|
|
5395
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
5396
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
5397
|
+
};
|
|
5398
|
+
return {
|
|
5399
|
+
async find(anchor) {
|
|
5400
|
+
const stored = anchor.hash;
|
|
5401
|
+
if (!stored) return void 0;
|
|
5402
|
+
const language = languageForFile(anchor.file);
|
|
5403
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5404
|
+
const candidates = await filesForLanguage(language);
|
|
5405
|
+
if (!prepared.has(language)) {
|
|
5406
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
5407
|
+
prepared.add(language);
|
|
5408
|
+
}
|
|
5409
|
+
const floor = anchor.lines ?? 0;
|
|
5410
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
5411
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
5412
|
+
const hits = await mapLimit(
|
|
5413
|
+
batch,
|
|
5414
|
+
DEFAULT_IO_CONCURRENCY,
|
|
5415
|
+
async (file) => {
|
|
5416
|
+
const size2 = await sizeOf(file);
|
|
5417
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
5418
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
5419
|
+
}
|
|
5420
|
+
);
|
|
5421
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
5422
|
+
if (found) return found;
|
|
5423
|
+
}
|
|
5424
|
+
return void 0;
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
5429
|
+
const source = await read(file);
|
|
5430
|
+
if (!source.ok) return void 0;
|
|
5431
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
5432
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
5433
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
5434
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
5435
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
5436
|
+
return {
|
|
5437
|
+
file,
|
|
5438
|
+
symbol: found.symbol,
|
|
5439
|
+
startLine: found.span.startLine,
|
|
5440
|
+
endLine: found.span.endLine
|
|
5441
|
+
};
|
|
5442
|
+
}
|
|
5443
|
+
return void 0;
|
|
5444
|
+
}
|
|
5445
|
+
function diskSize(repoRoot) {
|
|
5446
|
+
return async (file) => {
|
|
5447
|
+
const path = anchorFilePath(repoRoot, file);
|
|
5448
|
+
if (path === null) return null;
|
|
5449
|
+
try {
|
|
5450
|
+
return (await (0, import_promises9.stat)(path)).size;
|
|
5451
|
+
} catch {
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
5457
|
+
const height = anchor.lines;
|
|
5458
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
5459
|
+
const source = await read(anchor.file);
|
|
5460
|
+
if (!source.ok) return void 0;
|
|
5461
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
5462
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
5463
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
5464
|
+
continue;
|
|
5465
|
+
}
|
|
5466
|
+
return {
|
|
5467
|
+
file: anchor.file,
|
|
5468
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5469
|
+
startLine: at2 + 1,
|
|
5470
|
+
endLine: at2 + height
|
|
5471
|
+
};
|
|
5472
|
+
}
|
|
5473
|
+
return void 0;
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// src/drift/classify.ts
|
|
5477
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
5478
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
5479
|
+
(anchor) => anchor.hash
|
|
5480
|
+
);
|
|
5481
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
5482
|
+
const treeSitter = new TreeSitterResolver();
|
|
5483
|
+
const resolvers = [treeSitter, regexResolver];
|
|
5484
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
5485
|
+
const wanted = [];
|
|
5486
|
+
entries.forEach((entry, at2) => {
|
|
5487
|
+
const anchor = anchors[at2];
|
|
5488
|
+
if (!anchor) return;
|
|
5489
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
5490
|
+
wanted.push({ anchor, entry });
|
|
5491
|
+
});
|
|
5492
|
+
if (!wanted.length) return [];
|
|
5493
|
+
await prepareResolvers(
|
|
5494
|
+
resolvers,
|
|
5495
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
5496
|
+
);
|
|
5497
|
+
const out = [];
|
|
5498
|
+
for (const { anchor, entry } of wanted) {
|
|
5499
|
+
const movedTo = await search.find(anchor);
|
|
5500
|
+
if (movedTo) {
|
|
5501
|
+
out.push({
|
|
5502
|
+
anchor,
|
|
5503
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
5504
|
+
class: "moved"
|
|
5505
|
+
});
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
5509
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
5510
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
5511
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
5512
|
+
out.push({
|
|
5513
|
+
anchor,
|
|
5514
|
+
entry: { ...entry, class: settled },
|
|
5515
|
+
class: settled,
|
|
5516
|
+
...newText !== void 0 ? { newText } : {},
|
|
5517
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
5518
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
5519
|
+
});
|
|
5520
|
+
}
|
|
5521
|
+
return out;
|
|
5522
|
+
}
|
|
5523
|
+
function sameTokens(resolver, file, before, after) {
|
|
5524
|
+
if (before === after) return false;
|
|
5525
|
+
const left = resolver.normalize(before, file);
|
|
5526
|
+
const right = resolver.normalize(after, file);
|
|
5527
|
+
return left !== null && left === right;
|
|
5528
|
+
}
|
|
5529
|
+
async function currentText(reader, anchor, resolvers) {
|
|
5530
|
+
const read = await reader(anchor.file);
|
|
5531
|
+
if (!read.ok) return void 0;
|
|
5532
|
+
return spanIn(read.source, anchor, resolvers);
|
|
5533
|
+
}
|
|
5534
|
+
function spanIn(source, anchor, resolvers) {
|
|
5535
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
5536
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
// src/drift/diff.ts
|
|
5540
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
5541
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
5542
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
5543
|
+
function diffBudget(anchors) {
|
|
5544
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
5545
|
+
return Math.min(
|
|
5546
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
5547
|
+
Math.max(
|
|
5548
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
5549
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
5550
|
+
)
|
|
5551
|
+
);
|
|
5552
|
+
}
|
|
5553
|
+
function unifiedDiff(before, after, options = {}) {
|
|
5554
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
5555
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
5556
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
5557
|
+
const body = [];
|
|
5558
|
+
let added = 0;
|
|
5559
|
+
let removed = 0;
|
|
5560
|
+
for (const edit of edits(left, right)) {
|
|
5561
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
5562
|
+
else if (edit.kind === "remove") {
|
|
5563
|
+
body.push(`-${edit.line}`);
|
|
5564
|
+
removed += 1;
|
|
5565
|
+
} else {
|
|
5566
|
+
body.push(`+${edit.line}`);
|
|
5567
|
+
added += 1;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const truncated = body.length > max;
|
|
5571
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
5572
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
5573
|
+
const lines = [header, ...shown];
|
|
5574
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5575
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
5576
|
+
}
|
|
5577
|
+
function edits(left, right) {
|
|
5578
|
+
const rows = left.length;
|
|
5579
|
+
const cols = right.length;
|
|
5580
|
+
const table2 = Array.from(
|
|
5581
|
+
{ length: rows + 1 },
|
|
5582
|
+
() => new Array(cols + 1).fill(0)
|
|
5583
|
+
);
|
|
5584
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
5585
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
5586
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
5587
|
+
table2[row2 + 1][col2],
|
|
5588
|
+
table2[row2][col2 + 1]
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
const out = [];
|
|
5593
|
+
let row = 0;
|
|
5594
|
+
let col = 0;
|
|
5595
|
+
while (row < rows && col < cols) {
|
|
5596
|
+
if (left[row] === right[col]) {
|
|
5597
|
+
out.push({ kind: "same", line: left[row] });
|
|
5598
|
+
row += 1;
|
|
5599
|
+
col += 1;
|
|
5600
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
5601
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5602
|
+
row += 1;
|
|
5603
|
+
} else {
|
|
5604
|
+
out.push({ kind: "add", line: right[col] });
|
|
5605
|
+
col += 1;
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
for (; row < rows; row++)
|
|
5609
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5610
|
+
for (; col < cols; col++)
|
|
5611
|
+
out.push({ kind: "add", line: right[col] });
|
|
5612
|
+
return out;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
// src/drift/packet.ts
|
|
5616
|
+
var PRESUMED_INVALID = [
|
|
5617
|
+
"fact",
|
|
5618
|
+
"constraint",
|
|
5619
|
+
"contract"
|
|
5620
|
+
];
|
|
5621
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
5622
|
+
var DEFAULT_NOTES = {
|
|
5623
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
5624
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
5625
|
+
review: "re-read the record against the new code"
|
|
5626
|
+
};
|
|
5627
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
5628
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
5629
|
+
...options.reader ? { reader: options.reader } : {},
|
|
5630
|
+
...options.search ? { search: options.search } : {},
|
|
5631
|
+
withHistory: options.withDiff !== false
|
|
5632
|
+
});
|
|
5633
|
+
const open = classified.filter(
|
|
5634
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
5635
|
+
);
|
|
5636
|
+
if (!open.length) return { packet: null, classified };
|
|
5637
|
+
const budget = diffBudget(open.length);
|
|
5638
|
+
const anchors = open.map(
|
|
5639
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5640
|
+
);
|
|
5641
|
+
const type = record.frontmatter.type;
|
|
5642
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
5643
|
+
return {
|
|
5644
|
+
classified,
|
|
5645
|
+
packet: {
|
|
5646
|
+
conceptId: record.conceptId,
|
|
5647
|
+
title: record.frontmatter.title ?? null,
|
|
5648
|
+
type,
|
|
5649
|
+
standing: options.standing ?? "unsettled",
|
|
5650
|
+
why: record.frontmatter.description ?? null,
|
|
5651
|
+
claim: claimOf(record),
|
|
5652
|
+
anchors,
|
|
5653
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
5654
|
+
conceptId: entry.conceptId,
|
|
5655
|
+
title: entry.title,
|
|
5656
|
+
standing: entry.standing,
|
|
5657
|
+
depth: entry.depth
|
|
5658
|
+
})),
|
|
5659
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
5660
|
+
default: fallback,
|
|
5661
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
5662
|
+
}
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
5666
|
+
const { entry } = found;
|
|
5667
|
+
const base2 = {
|
|
5668
|
+
file: entry.file,
|
|
5669
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
5670
|
+
class: found.class,
|
|
5671
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
5672
|
+
storedHash: entry.storedHash,
|
|
5673
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
5674
|
+
diffSize: entry.diffSize,
|
|
5675
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
5676
|
+
};
|
|
5677
|
+
if (!withDiff) return base2;
|
|
5678
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
5679
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
5680
|
+
}
|
|
5681
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
5682
|
+
maxLines
|
|
5683
|
+
});
|
|
5684
|
+
return {
|
|
5685
|
+
...base2,
|
|
5686
|
+
diff: {
|
|
5687
|
+
status: "ok",
|
|
5688
|
+
source: found.oldOrigin.kind,
|
|
5689
|
+
ref: found.oldOrigin.ref,
|
|
5690
|
+
unified: rendered.text,
|
|
5691
|
+
added: rendered.added,
|
|
5692
|
+
removed: rendered.removed,
|
|
5693
|
+
truncated: rendered.truncated
|
|
5694
|
+
}
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5697
|
+
function claimOf(record) {
|
|
5698
|
+
const type = record.frontmatter.type;
|
|
5699
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
5700
|
+
if (!section) return null;
|
|
5701
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
5702
|
+
const start = lines.findIndex(
|
|
5703
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
5704
|
+
);
|
|
5705
|
+
if (start < 0) return null;
|
|
5706
|
+
const rest = lines.slice(start + 1);
|
|
5707
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
5708
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
5709
|
+
return text ? { section, text } : null;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
// src/commands/reassess.ts
|
|
5183
5713
|
var import_zod14 = require("zod");
|
|
5184
|
-
var
|
|
5714
|
+
var reassessCommand = define({
|
|
5715
|
+
name: "reassess",
|
|
5716
|
+
tool: "kb_reassess",
|
|
5717
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5718
|
+
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.",
|
|
5719
|
+
input: import_zod14.z.object({
|
|
5720
|
+
bundlePath,
|
|
5721
|
+
conceptId,
|
|
5722
|
+
repoRoot: REPO_ROOT,
|
|
5723
|
+
withDiff: import_zod14.z.boolean().optional().describe(
|
|
5724
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5725
|
+
)
|
|
5726
|
+
}),
|
|
5727
|
+
fromArgv: (argv, path) => {
|
|
5728
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
5729
|
+
return {
|
|
5730
|
+
bundlePath: path,
|
|
5731
|
+
conceptId: argv[1],
|
|
5732
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
5733
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
5734
|
+
};
|
|
5735
|
+
},
|
|
5736
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
5737
|
+
const root = repoRoot ?? process.cwd();
|
|
5738
|
+
const bundle = await store.list(path);
|
|
5739
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
5740
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
5741
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
5742
|
+
const entries = drift?.get(id) ?? [];
|
|
5743
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
5744
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
5745
|
+
}
|
|
5746
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
5747
|
+
(hit) => hit.record.conceptId === id
|
|
5748
|
+
)?.standing;
|
|
5749
|
+
const impact2 = await store.impact(path, id);
|
|
5750
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
5751
|
+
...withDiff ? { withDiff: true } : {},
|
|
5752
|
+
impact: impact2,
|
|
5753
|
+
...standing ? { standing } : {}
|
|
5754
|
+
});
|
|
5755
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
5756
|
+
let frozen = false;
|
|
5757
|
+
const rebaselined = [];
|
|
5758
|
+
if (moves.length) {
|
|
5759
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
5760
|
+
for (const found of moves) {
|
|
5761
|
+
const to = found.entry.movedTo;
|
|
5762
|
+
if (!to) continue;
|
|
5763
|
+
relocated.set(found.anchor, {
|
|
5764
|
+
...found.anchor,
|
|
5765
|
+
file: to.file,
|
|
5766
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
5767
|
+
});
|
|
5768
|
+
rebaselined.push({
|
|
5769
|
+
file: found.anchor.file,
|
|
5770
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
5771
|
+
toFile: to.file,
|
|
5772
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
try {
|
|
5776
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
5779
|
+
frozen = true;
|
|
5780
|
+
}
|
|
5781
|
+
if (!frozen) {
|
|
5782
|
+
await store.updateAnchors(
|
|
5783
|
+
path,
|
|
5784
|
+
id,
|
|
5785
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
5786
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
5787
|
+
),
|
|
5788
|
+
actor
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
return {
|
|
5793
|
+
conceptId: id,
|
|
5794
|
+
packet,
|
|
5795
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
5796
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
5797
|
+
...frozen ? {
|
|
5798
|
+
frozen: true,
|
|
5799
|
+
note: "base is frozen: nothing was rebaselined"
|
|
5800
|
+
} : {}
|
|
5801
|
+
};
|
|
5802
|
+
},
|
|
5803
|
+
render: (result) => renderReassess(result)
|
|
5804
|
+
});
|
|
5805
|
+
function renderReassess(result) {
|
|
5806
|
+
const lines = [];
|
|
5807
|
+
for (const move of result.rebaselined) {
|
|
5808
|
+
lines.push(
|
|
5809
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
5810
|
+
);
|
|
5811
|
+
}
|
|
5812
|
+
if (result.cosmetic) {
|
|
5813
|
+
lines.push(
|
|
5814
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
5815
|
+
);
|
|
5816
|
+
}
|
|
5817
|
+
if (result.note) lines.push(result.note);
|
|
5818
|
+
const packet = result.packet;
|
|
5819
|
+
if (!packet) {
|
|
5820
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
5821
|
+
return lines.join("\n");
|
|
5822
|
+
}
|
|
5823
|
+
lines.push(
|
|
5824
|
+
"",
|
|
5825
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
5826
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
5827
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
5828
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
5829
|
+
"",
|
|
5830
|
+
`## Anchors (${packet.anchors.length})`
|
|
5831
|
+
);
|
|
5832
|
+
for (const anchor of packet.anchors) {
|
|
5833
|
+
lines.push(
|
|
5834
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
5835
|
+
);
|
|
5836
|
+
if (!anchor.diff) continue;
|
|
5837
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
5838
|
+
lines.push(
|
|
5839
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
5840
|
+
);
|
|
5841
|
+
continue;
|
|
5842
|
+
}
|
|
5843
|
+
lines.push(
|
|
5844
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
5845
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
5846
|
+
);
|
|
5847
|
+
}
|
|
5848
|
+
if (packet.impact.length) {
|
|
5849
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
5850
|
+
for (const entry of packet.impact) {
|
|
5851
|
+
lines.push(
|
|
5852
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
5853
|
+
);
|
|
5854
|
+
}
|
|
5855
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
5856
|
+
}
|
|
5857
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
5858
|
+
return lines.join("\n");
|
|
5859
|
+
}
|
|
5860
|
+
function at(file, symbol) {
|
|
5861
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5864
|
+
// src/commands/doctor.ts
|
|
5865
|
+
var days = (what, fallback) => import_zod15.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5185
5866
|
var doctorCommand = define({
|
|
5186
5867
|
name: "doctor",
|
|
5187
5868
|
tool: "kb_doctor",
|
|
5188
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
5189
|
-
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.
|
|
5190
|
-
input:
|
|
5869
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5870
|
+
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.",
|
|
5871
|
+
input: import_zod15.z.object({
|
|
5191
5872
|
bundlePath,
|
|
5192
5873
|
repoRoot: REPO_ROOT,
|
|
5193
5874
|
expiringDays: days(
|
|
@@ -5202,11 +5883,17 @@ var doctorCommand = define({
|
|
|
5202
5883
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5203
5884
|
DEFAULT_AGING_DAYS
|
|
5204
5885
|
),
|
|
5205
|
-
offline:
|
|
5886
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
5206
5887
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5207
5888
|
),
|
|
5208
|
-
strict:
|
|
5889
|
+
strict: import_zod15.z.boolean().optional().describe(
|
|
5209
5890
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5891
|
+
),
|
|
5892
|
+
drifted: import_zod15.z.boolean().optional().describe(
|
|
5893
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5894
|
+
),
|
|
5895
|
+
withDiff: import_zod15.z.boolean().optional().describe(
|
|
5896
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5210
5897
|
)
|
|
5211
5898
|
}),
|
|
5212
5899
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -5225,7 +5912,9 @@ var doctorCommand = define({
|
|
|
5225
5912
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
5226
5913
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
5227
5914
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
5228
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
5915
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
5916
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
5917
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
5229
5918
|
};
|
|
5230
5919
|
},
|
|
5231
5920
|
run: async ({ store, now }, {
|
|
@@ -5234,7 +5923,9 @@ var doctorCommand = define({
|
|
|
5234
5923
|
unverifiedDays,
|
|
5235
5924
|
agingDays,
|
|
5236
5925
|
repoRoot,
|
|
5237
|
-
offline
|
|
5926
|
+
offline,
|
|
5927
|
+
drifted: drifted2,
|
|
5928
|
+
withDiff
|
|
5238
5929
|
}) => {
|
|
5239
5930
|
const checkedAt = now();
|
|
5240
5931
|
const records = await store.list(path);
|
|
@@ -5249,10 +5940,51 @@ var doctorCommand = define({
|
|
|
5249
5940
|
now: new Date(checkedAt)
|
|
5250
5941
|
});
|
|
5251
5942
|
const hints = grammarHints();
|
|
5943
|
+
if (!drifted2) {
|
|
5944
|
+
return {
|
|
5945
|
+
bundlePath: path,
|
|
5946
|
+
checkedAt,
|
|
5947
|
+
...report,
|
|
5948
|
+
...hints.length ? { hints } : {}
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
const standings = new Map(
|
|
5952
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
5953
|
+
hit.record.conceptId,
|
|
5954
|
+
hit.standing
|
|
5955
|
+
])
|
|
5956
|
+
);
|
|
5957
|
+
const packets = [];
|
|
5958
|
+
const rebaselinable = [];
|
|
5959
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
5960
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
5961
|
+
const record = records.find(
|
|
5962
|
+
(entry) => entry.conceptId === found.conceptId
|
|
5963
|
+
);
|
|
5964
|
+
if (!record) continue;
|
|
5965
|
+
const standing = standings.get(record.conceptId);
|
|
5966
|
+
const built = await reassessPacket(
|
|
5967
|
+
repoRoot ?? process.cwd(),
|
|
5968
|
+
record,
|
|
5969
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
5970
|
+
{
|
|
5971
|
+
...withDiff ? { withDiff: true } : {},
|
|
5972
|
+
impact: await store.impact(path, record.conceptId),
|
|
5973
|
+
...standing ? { standing } : {},
|
|
5974
|
+
search
|
|
5975
|
+
}
|
|
5976
|
+
);
|
|
5977
|
+
if (built.packet) packets.push(built.packet);
|
|
5978
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
5979
|
+
rebaselinable.push(record.conceptId);
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5252
5982
|
return {
|
|
5253
5983
|
bundlePath: path,
|
|
5254
5984
|
checkedAt,
|
|
5255
5985
|
...report,
|
|
5986
|
+
packets,
|
|
5987
|
+
rebaselinable,
|
|
5256
5988
|
...hints.length ? { hints } : {}
|
|
5257
5989
|
};
|
|
5258
5990
|
},
|
|
@@ -5266,6 +5998,7 @@ var doctorCommand = define({
|
|
|
5266
5998
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
5267
5999
|
});
|
|
5268
6000
|
function render2(result) {
|
|
6001
|
+
if (result.packets) return renderPackets(result);
|
|
5269
6002
|
const { thresholds } = result;
|
|
5270
6003
|
const lines = [
|
|
5271
6004
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -5299,21 +6032,45 @@ function render2(result) {
|
|
|
5299
6032
|
);
|
|
5300
6033
|
return lines.join("\n");
|
|
5301
6034
|
}
|
|
6035
|
+
function renderPackets(result) {
|
|
6036
|
+
const packets = result.packets ?? [];
|
|
6037
|
+
const lines = [
|
|
6038
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
6039
|
+
`checked: ${result.checkedAt}`,
|
|
6040
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
6041
|
+
];
|
|
6042
|
+
if (result.rebaselinable?.length) {
|
|
6043
|
+
lines.push(
|
|
6044
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
6045
|
+
);
|
|
6046
|
+
}
|
|
6047
|
+
for (const packet of packets) {
|
|
6048
|
+
lines.push(
|
|
6049
|
+
renderReassess({
|
|
6050
|
+
conceptId: packet.conceptId,
|
|
6051
|
+
packet,
|
|
6052
|
+
rebaselined: [],
|
|
6053
|
+
cosmetic: 0
|
|
6054
|
+
})
|
|
6055
|
+
);
|
|
6056
|
+
}
|
|
6057
|
+
return lines.join("\n");
|
|
6058
|
+
}
|
|
5302
6059
|
|
|
5303
6060
|
// src/commands/impact.ts
|
|
5304
|
-
var
|
|
6061
|
+
var import_zod16 = require("zod");
|
|
5305
6062
|
var impactCommand = define({
|
|
5306
6063
|
name: "impact",
|
|
5307
6064
|
tool: "kb_impact",
|
|
5308
6065
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
5309
6066
|
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.",
|
|
5310
|
-
input:
|
|
6067
|
+
input: import_zod16.z.object({
|
|
5311
6068
|
bundlePath,
|
|
5312
6069
|
conceptId,
|
|
5313
|
-
depth:
|
|
6070
|
+
depth: import_zod16.z.number().int().positive().optional().describe(
|
|
5314
6071
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
5315
6072
|
),
|
|
5316
|
-
rels:
|
|
6073
|
+
rels: import_zod16.z.array(import_zod16.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
5317
6074
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
5318
6075
|
)
|
|
5319
6076
|
}),
|
|
@@ -5334,13 +6091,13 @@ var impactCommand = define({
|
|
|
5334
6091
|
});
|
|
5335
6092
|
|
|
5336
6093
|
// src/commands/list.ts
|
|
5337
|
-
var
|
|
6094
|
+
var import_zod17 = require("zod");
|
|
5338
6095
|
var listCommand = define({
|
|
5339
6096
|
name: "list",
|
|
5340
6097
|
tool: "kb_list",
|
|
5341
6098
|
usage: "list [type]",
|
|
5342
6099
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
5343
|
-
input:
|
|
6100
|
+
input: import_zod17.z.object({ bundlePath, type: import_zod17.z.enum(KB_RECORD_TYPES).optional() }),
|
|
5344
6101
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
5345
6102
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
5346
6103
|
conceptId: record.conceptId,
|
|
@@ -5352,17 +6109,17 @@ var listCommand = define({
|
|
|
5352
6109
|
});
|
|
5353
6110
|
|
|
5354
6111
|
// src/commands/load.ts
|
|
5355
|
-
var
|
|
6112
|
+
var import_zod18 = require("zod");
|
|
5356
6113
|
var loadCommand = define({
|
|
5357
6114
|
name: "load",
|
|
5358
6115
|
tool: "kb_load",
|
|
5359
6116
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
5360
6117
|
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.",
|
|
5361
|
-
input:
|
|
6118
|
+
input: import_zod18.z.object({
|
|
5362
6119
|
bundlePath,
|
|
5363
|
-
type:
|
|
5364
|
-
budgetTokens:
|
|
5365
|
-
all:
|
|
6120
|
+
type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
|
|
6121
|
+
budgetTokens: import_zod18.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6122
|
+
all: import_zod18.z.boolean().optional().describe(
|
|
5366
6123
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
5367
6124
|
),
|
|
5368
6125
|
repoRoot: REPO_ROOT
|
|
@@ -5404,25 +6161,25 @@ var loadCommand = define({
|
|
|
5404
6161
|
});
|
|
5405
6162
|
|
|
5406
6163
|
// src/commands/log.ts
|
|
5407
|
-
var
|
|
6164
|
+
var import_zod19 = require("zod");
|
|
5408
6165
|
var logCommand = define({
|
|
5409
6166
|
name: "log",
|
|
5410
6167
|
tool: "kb_log",
|
|
5411
6168
|
usage: "log",
|
|
5412
6169
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
5413
|
-
input:
|
|
6170
|
+
input: import_zod19.z.object({ bundlePath }),
|
|
5414
6171
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5415
6172
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
5416
6173
|
});
|
|
5417
6174
|
|
|
5418
6175
|
// src/commands/no-decision.ts
|
|
5419
|
-
var
|
|
6176
|
+
var import_zod20 = require("zod");
|
|
5420
6177
|
var noDecisionCommand = define({
|
|
5421
6178
|
name: "no-decision",
|
|
5422
6179
|
tool: "kb_no_decision",
|
|
5423
6180
|
usage: "no-decision <reason...>",
|
|
5424
6181
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
5425
|
-
input:
|
|
6182
|
+
input: import_zod20.z.object({ bundlePath, reason: import_zod20.z.string().min(1) }),
|
|
5426
6183
|
fromArgv: (argv, path) => ({
|
|
5427
6184
|
bundlePath: path,
|
|
5428
6185
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -5439,20 +6196,20 @@ var noDecisionCommand = define({
|
|
|
5439
6196
|
});
|
|
5440
6197
|
|
|
5441
6198
|
// src/commands/pack.ts
|
|
5442
|
-
var
|
|
6199
|
+
var import_zod21 = require("zod");
|
|
5443
6200
|
var packCommand = define({
|
|
5444
6201
|
name: "pack",
|
|
5445
6202
|
tool: "kb_pack",
|
|
5446
6203
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
5447
6204
|
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.",
|
|
5448
|
-
input:
|
|
6205
|
+
input: import_zod21.z.object({
|
|
5449
6206
|
bundlePath,
|
|
5450
6207
|
conceptId,
|
|
5451
|
-
hops:
|
|
5452
|
-
maxNodes:
|
|
6208
|
+
hops: import_zod21.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6209
|
+
maxNodes: import_zod21.z.number().int().positive().optional().describe(
|
|
5453
6210
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
5454
6211
|
),
|
|
5455
|
-
budgetTokens:
|
|
6212
|
+
budgetTokens: import_zod21.z.number().int().positive().optional().describe(
|
|
5456
6213
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
5457
6214
|
)
|
|
5458
6215
|
}),
|
|
@@ -5477,12 +6234,12 @@ var packCommand = define({
|
|
|
5477
6234
|
return render3(result, path, now());
|
|
5478
6235
|
}
|
|
5479
6236
|
});
|
|
5480
|
-
function render3(result, bundle,
|
|
6237
|
+
function render3(result, bundle, at2) {
|
|
5481
6238
|
const lines = [
|
|
5482
6239
|
`# KB Pack \u2014 ${result.root}`,
|
|
5483
6240
|
`bundle: ${bundle}`,
|
|
5484
6241
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
5485
|
-
`packed: ${
|
|
6242
|
+
`packed: ${at2}`,
|
|
5486
6243
|
"",
|
|
5487
6244
|
`## Records (${result.records.length})`
|
|
5488
6245
|
];
|
|
@@ -5539,22 +6296,22 @@ function warningLabel(warning) {
|
|
|
5539
6296
|
}
|
|
5540
6297
|
|
|
5541
6298
|
// src/commands/pin.ts
|
|
5542
|
-
var
|
|
6299
|
+
var import_zod22 = require("zod");
|
|
5543
6300
|
var pinCommand = define({
|
|
5544
6301
|
name: "pin",
|
|
5545
6302
|
tool: "kb_pin",
|
|
5546
6303
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
5547
6304
|
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.",
|
|
5548
|
-
input:
|
|
6305
|
+
input: import_zod22.z.object({
|
|
5549
6306
|
bundlePath,
|
|
5550
|
-
mode:
|
|
6307
|
+
mode: import_zod22.z.enum(["full", "index"]).optional().describe(
|
|
5551
6308
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
5552
6309
|
),
|
|
5553
|
-
profiles:
|
|
5554
|
-
layer:
|
|
6310
|
+
profiles: import_zod22.z.array(import_zod22.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6311
|
+
layer: import_zod22.z.enum(["project", "local", "user"]).optional().describe(
|
|
5555
6312
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
5556
6313
|
),
|
|
5557
|
-
frozen:
|
|
6314
|
+
frozen: import_zod22.z.boolean().optional().describe(
|
|
5558
6315
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
5559
6316
|
)
|
|
5560
6317
|
}),
|
|
@@ -5583,29 +6340,29 @@ var pinCommand = define({
|
|
|
5583
6340
|
});
|
|
5584
6341
|
|
|
5585
6342
|
// src/commands/pins.ts
|
|
5586
|
-
var
|
|
6343
|
+
var import_zod23 = require("zod");
|
|
5587
6344
|
var pinsCommand = define({
|
|
5588
6345
|
name: "pins",
|
|
5589
6346
|
tool: "kb_pins",
|
|
5590
6347
|
usage: "pins",
|
|
5591
6348
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
5592
|
-
input:
|
|
6349
|
+
input: import_zod23.z.object({}),
|
|
5593
6350
|
fromArgv: () => ({}),
|
|
5594
6351
|
run: ({ store }) => listPins(store, process.cwd())
|
|
5595
6352
|
});
|
|
5596
6353
|
|
|
5597
6354
|
// src/commands/query.ts
|
|
5598
|
-
var
|
|
6355
|
+
var import_zod24 = require("zod");
|
|
5599
6356
|
var queryCommand = define({
|
|
5600
6357
|
name: "query",
|
|
5601
6358
|
tool: "kb_query",
|
|
5602
6359
|
usage: "query <text...> [--repo-root PATH]",
|
|
5603
6360
|
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.",
|
|
5604
|
-
input:
|
|
6361
|
+
input: import_zod24.z.object({
|
|
5605
6362
|
bundlePath,
|
|
5606
|
-
text:
|
|
5607
|
-
type:
|
|
5608
|
-
includeNonCurrent:
|
|
6363
|
+
text: import_zod24.z.string().optional(),
|
|
6364
|
+
type: import_zod24.z.enum(KB_RECORD_TYPES).optional(),
|
|
6365
|
+
includeNonCurrent: import_zod24.z.boolean().optional(),
|
|
5609
6366
|
repoRoot: REPO_ROOT
|
|
5610
6367
|
}),
|
|
5611
6368
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -5637,43 +6394,43 @@ var queryCommand = define({
|
|
|
5637
6394
|
});
|
|
5638
6395
|
|
|
5639
6396
|
// src/commands/read-index.ts
|
|
5640
|
-
var
|
|
6397
|
+
var import_zod25 = require("zod");
|
|
5641
6398
|
var readIndexCommand = define({
|
|
5642
6399
|
name: "index",
|
|
5643
6400
|
tool: "kb_index",
|
|
5644
6401
|
usage: "index",
|
|
5645
6402
|
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.",
|
|
5646
|
-
input:
|
|
6403
|
+
input: import_zod25.z.object({ bundlePath }),
|
|
5647
6404
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5648
6405
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
5649
6406
|
});
|
|
5650
6407
|
|
|
5651
6408
|
// src/commands/schema.ts
|
|
5652
|
-
var
|
|
6409
|
+
var import_zod26 = require("zod");
|
|
5653
6410
|
var schemaCommand = define({
|
|
5654
6411
|
name: "schema",
|
|
5655
6412
|
tool: "kb_schema",
|
|
5656
6413
|
usage: "schema",
|
|
5657
6414
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
5658
|
-
input:
|
|
6415
|
+
input: import_zod26.z.object({}),
|
|
5659
6416
|
fromArgv: () => ({}),
|
|
5660
6417
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
5661
6418
|
});
|
|
5662
6419
|
|
|
5663
6420
|
// src/commands/stamp.ts
|
|
5664
|
-
var
|
|
5665
|
-
var
|
|
6421
|
+
var import_promises10 = require("fs/promises");
|
|
6422
|
+
var import_zod27 = require("zod");
|
|
5666
6423
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
5667
6424
|
var stampCommand = define({
|
|
5668
6425
|
name: "stamp",
|
|
5669
6426
|
tool: "kb_stamp",
|
|
5670
6427
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
5671
|
-
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
|
|
5672
|
-
input:
|
|
5673
|
-
bundlePath:
|
|
6428
|
+
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.",
|
|
6429
|
+
input: import_zod27.z.object({
|
|
6430
|
+
bundlePath: import_zod27.z.string().min(1).optional().describe(
|
|
5674
6431
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5675
6432
|
),
|
|
5676
|
-
since:
|
|
6433
|
+
since: import_zod27.z.string().min(1).optional().describe(
|
|
5677
6434
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5678
6435
|
)
|
|
5679
6436
|
}),
|
|
@@ -5712,7 +6469,7 @@ var stampCommand = define({
|
|
|
5712
6469
|
return reports;
|
|
5713
6470
|
},
|
|
5714
6471
|
render: (result) => result.map((report) => {
|
|
5715
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
6472
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
5716
6473
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
5717
6474
|
return report.changed?.length ? `${head}
|
|
5718
6475
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -5735,7 +6492,7 @@ async function readBaseline(since) {
|
|
|
5735
6492
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5736
6493
|
let parsed;
|
|
5737
6494
|
try {
|
|
5738
|
-
parsed = JSON.parse(await (0,
|
|
6495
|
+
parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
|
|
5739
6496
|
} catch {
|
|
5740
6497
|
throw new KbStampBaselineError(since);
|
|
5741
6498
|
}
|
|
@@ -5759,16 +6516,16 @@ async function readBaseline(since) {
|
|
|
5759
6516
|
}
|
|
5760
6517
|
|
|
5761
6518
|
// src/commands/status.ts
|
|
5762
|
-
var
|
|
6519
|
+
var import_zod28 = require("zod");
|
|
5763
6520
|
var statusCommand = define({
|
|
5764
6521
|
name: "status",
|
|
5765
6522
|
tool: "kb_status",
|
|
5766
6523
|
usage: "status <concept-id> <status>",
|
|
5767
6524
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
5768
|
-
input:
|
|
6525
|
+
input: import_zod28.z.object({
|
|
5769
6526
|
bundlePath,
|
|
5770
6527
|
conceptId,
|
|
5771
|
-
status:
|
|
6528
|
+
status: import_zod28.z.enum(KB_RECORD_STATUSES)
|
|
5772
6529
|
}),
|
|
5773
6530
|
fromArgv: (argv, path) => ({
|
|
5774
6531
|
bundlePath: path,
|
|
@@ -5783,13 +6540,13 @@ var statusCommand = define({
|
|
|
5783
6540
|
});
|
|
5784
6541
|
|
|
5785
6542
|
// src/commands/supersede.ts
|
|
5786
|
-
var
|
|
6543
|
+
var import_zod29 = require("zod");
|
|
5787
6544
|
var supersedeCommand = define({
|
|
5788
6545
|
name: "supersede",
|
|
5789
6546
|
tool: "kb_supersede",
|
|
5790
6547
|
usage: "supersede <concept-id> <replacement-id>",
|
|
5791
6548
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
5792
|
-
input:
|
|
6549
|
+
input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
5793
6550
|
fromArgv: (argv, path) => ({
|
|
5794
6551
|
bundlePath: path,
|
|
5795
6552
|
conceptId: argv[1],
|
|
@@ -5803,16 +6560,16 @@ var supersedeCommand = define({
|
|
|
5803
6560
|
});
|
|
5804
6561
|
|
|
5805
6562
|
// src/commands/sync-instructions.ts
|
|
5806
|
-
var
|
|
6563
|
+
var import_zod30 = require("zod");
|
|
5807
6564
|
var syncInstructionsCommand = define({
|
|
5808
6565
|
name: "sync-instructions",
|
|
5809
6566
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
5810
6567
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
5811
|
-
input:
|
|
5812
|
-
file:
|
|
5813
|
-
budgetTokens:
|
|
5814
|
-
fullUnderTokens:
|
|
5815
|
-
profile:
|
|
6568
|
+
input: import_zod30.z.object({
|
|
6569
|
+
file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
|
|
6570
|
+
budgetTokens: import_zod30.z.number().int().positive().optional(),
|
|
6571
|
+
fullUnderTokens: import_zod30.z.number().int().positive().optional(),
|
|
6572
|
+
profile: import_zod30.z.string().optional()
|
|
5816
6573
|
}),
|
|
5817
6574
|
fromArgv: (argv) => {
|
|
5818
6575
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5838,17 +6595,17 @@ var syncInstructionsCommand = define({
|
|
|
5838
6595
|
});
|
|
5839
6596
|
|
|
5840
6597
|
// src/commands/trace.ts
|
|
5841
|
-
var
|
|
6598
|
+
var import_zod31 = require("zod");
|
|
5842
6599
|
var traceCommand = define({
|
|
5843
6600
|
name: "trace",
|
|
5844
6601
|
tool: "kb_trace",
|
|
5845
6602
|
usage: "trace <concept-id> [edges...]",
|
|
5846
6603
|
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".',
|
|
5847
|
-
input:
|
|
6604
|
+
input: import_zod31.z.object({
|
|
5848
6605
|
bundlePath,
|
|
5849
6606
|
conceptId,
|
|
5850
|
-
edges:
|
|
5851
|
-
depth:
|
|
6607
|
+
edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
|
|
6608
|
+
depth: import_zod31.z.number().int().positive().optional()
|
|
5852
6609
|
}),
|
|
5853
6610
|
fromArgv: (argv, path) => ({
|
|
5854
6611
|
bundlePath: path,
|
|
@@ -5870,37 +6627,37 @@ var traceCommand = define({
|
|
|
5870
6627
|
});
|
|
5871
6628
|
|
|
5872
6629
|
// src/commands/types.ts
|
|
5873
|
-
var
|
|
6630
|
+
var import_zod32 = require("zod");
|
|
5874
6631
|
var typesCommand = define({
|
|
5875
6632
|
name: "types",
|
|
5876
6633
|
tool: "kb_types",
|
|
5877
6634
|
usage: "types",
|
|
5878
6635
|
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.",
|
|
5879
|
-
input:
|
|
6636
|
+
input: import_zod32.z.object({}),
|
|
5880
6637
|
fromArgv: () => ({}),
|
|
5881
6638
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5882
6639
|
});
|
|
5883
6640
|
|
|
5884
6641
|
// src/commands/unpin.ts
|
|
5885
|
-
var
|
|
6642
|
+
var import_zod33 = require("zod");
|
|
5886
6643
|
var unpinCommand = define({
|
|
5887
6644
|
name: "unpin",
|
|
5888
6645
|
tool: "kb_unpin",
|
|
5889
6646
|
usage: "unpin [bundle-path]",
|
|
5890
6647
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5891
|
-
input:
|
|
6648
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
5892
6649
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5893
6650
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5894
6651
|
});
|
|
5895
6652
|
|
|
5896
6653
|
// src/commands/validate.ts
|
|
5897
|
-
var
|
|
6654
|
+
var import_zod34 = require("zod");
|
|
5898
6655
|
var validateCommand = define({
|
|
5899
6656
|
name: "validate",
|
|
5900
6657
|
tool: "kb_validate",
|
|
5901
6658
|
usage: "validate",
|
|
5902
6659
|
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.",
|
|
5903
|
-
input:
|
|
6660
|
+
input: import_zod34.z.object({ bundlePath }),
|
|
5904
6661
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5905
6662
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5906
6663
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5910,16 +6667,16 @@ var validateCommand = define({
|
|
|
5910
6667
|
});
|
|
5911
6668
|
|
|
5912
6669
|
// src/commands/verify.ts
|
|
5913
|
-
var
|
|
6670
|
+
var import_zod35 = require("zod");
|
|
5914
6671
|
var verifyCommand = define({
|
|
5915
6672
|
name: "verify",
|
|
5916
6673
|
tool: "kb_verify",
|
|
5917
6674
|
usage: "verify <concept-id> --note <text>",
|
|
5918
6675
|
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.",
|
|
5919
|
-
input:
|
|
6676
|
+
input: import_zod35.z.object({
|
|
5920
6677
|
bundlePath,
|
|
5921
6678
|
conceptId,
|
|
5922
|
-
note:
|
|
6679
|
+
note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
|
|
5923
6680
|
message: "note must say what the check found"
|
|
5924
6681
|
})
|
|
5925
6682
|
}),
|
|
@@ -5939,15 +6696,15 @@ var verifyCommand = define({
|
|
|
5939
6696
|
});
|
|
5940
6697
|
|
|
5941
6698
|
// src/commands/write.ts
|
|
5942
|
-
var
|
|
6699
|
+
var import_zod36 = require("zod");
|
|
5943
6700
|
var writeCommand = define({
|
|
5944
6701
|
name: "write",
|
|
5945
6702
|
tool: "kb_write",
|
|
5946
6703
|
usage: "write <type> < record.json",
|
|
5947
6704
|
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.",
|
|
5948
|
-
input:
|
|
6705
|
+
input: import_zod36.z.object({
|
|
5949
6706
|
bundlePath,
|
|
5950
|
-
type:
|
|
6707
|
+
type: import_zod36.z.enum(KB_RECORD_TYPES),
|
|
5951
6708
|
input: composeInputSchema
|
|
5952
6709
|
}),
|
|
5953
6710
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5971,13 +6728,13 @@ var writeCommand = define({
|
|
|
5971
6728
|
});
|
|
5972
6729
|
|
|
5973
6730
|
// src/commands/write-decision.ts
|
|
5974
|
-
var
|
|
6731
|
+
var import_zod37 = require("zod");
|
|
5975
6732
|
var writeDecisionCommand = define({
|
|
5976
6733
|
name: "write-decision",
|
|
5977
6734
|
tool: "kb_write_decision",
|
|
5978
6735
|
usage: "write-decision < decision.json",
|
|
5979
6736
|
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.",
|
|
5980
|
-
input:
|
|
6737
|
+
input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5981
6738
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5982
6739
|
bundlePath: path,
|
|
5983
6740
|
input: JSON.parse(await stdin())
|
|
@@ -6007,6 +6764,7 @@ var KB_COMMANDS = [
|
|
|
6007
6764
|
answerCommand,
|
|
6008
6765
|
verifyCommand,
|
|
6009
6766
|
anchorResolveCommand,
|
|
6767
|
+
reassessCommand,
|
|
6010
6768
|
loadCommand,
|
|
6011
6769
|
catalogCommand,
|
|
6012
6770
|
packCommand,
|
|
@@ -6037,7 +6795,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
6037
6795
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6038
6796
|
|
|
6039
6797
|
// src/version.ts
|
|
6040
|
-
var VERSION = true ? "0.1.
|
|
6798
|
+
var VERSION = true ? "0.1.18" : "0.0.0-dev";
|
|
6041
6799
|
|
|
6042
6800
|
// src/mcp.ts
|
|
6043
6801
|
function createKbMcpServer() {
|
|
@@ -6131,21 +6889,21 @@ async function runKbCli(argv) {
|
|
|
6131
6889
|
`);
|
|
6132
6890
|
}
|
|
6133
6891
|
function takeLiteral(argv) {
|
|
6134
|
-
const
|
|
6135
|
-
if (
|
|
6136
|
-
return { flags: argv.slice(0,
|
|
6892
|
+
const at2 = argv.indexOf("--");
|
|
6893
|
+
if (at2 === -1) return { flags: argv, literal: [] };
|
|
6894
|
+
return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
|
|
6137
6895
|
}
|
|
6138
6896
|
function takeBundle(argv) {
|
|
6139
|
-
const
|
|
6140
|
-
if (
|
|
6897
|
+
const at2 = argv.indexOf("--bundle");
|
|
6898
|
+
if (at2 === -1) {
|
|
6141
6899
|
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
6142
6900
|
}
|
|
6143
|
-
const bundle = argv[
|
|
6901
|
+
const bundle = argv[at2 + 1];
|
|
6144
6902
|
if (!bundle) die("--bundle requires a path");
|
|
6145
6903
|
return {
|
|
6146
6904
|
bundle,
|
|
6147
6905
|
explicit: true,
|
|
6148
|
-
rest: [...argv.slice(0,
|
|
6906
|
+
rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
|
|
6149
6907
|
};
|
|
6150
6908
|
}
|
|
6151
6909
|
function readStdin() {
|
|
@@ -6243,6 +7001,7 @@ function usage() {
|
|
|
6243
7001
|
backlinks,
|
|
6244
7002
|
buildContext,
|
|
6245
7003
|
catalog,
|
|
7004
|
+
classifyDrift,
|
|
6246
7005
|
composeDecisionRecord,
|
|
6247
7006
|
composeInputSchema,
|
|
6248
7007
|
composeLinkSchema,
|
|
@@ -6290,6 +7049,7 @@ function usage() {
|
|
|
6290
7049
|
readMergedPins,
|
|
6291
7050
|
readPinsLayer,
|
|
6292
7051
|
readRemoteAnchors,
|
|
7052
|
+
reassessPacket,
|
|
6293
7053
|
regexResolver,
|
|
6294
7054
|
renderCatalogLine,
|
|
6295
7055
|
renderIndex,
|
|
@@ -6311,6 +7071,7 @@ function usage() {
|
|
|
6311
7071
|
toHookJson,
|
|
6312
7072
|
trace,
|
|
6313
7073
|
treeSitterLanguages,
|
|
7074
|
+
unifiedDiff,
|
|
6314
7075
|
unpinBase,
|
|
6315
7076
|
validateBundle
|
|
6316
7077
|
});
|