@saasontools/strauss-kb 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +26 -0
- package/README.md +31 -3
- package/dist/{chunk-RMJUGTAQ.js → chunk-CQBLH7CE.js} +2 -2
- package/dist/{chunk-ZKIQOBHT.js → chunk-MNQNHYWL.js} +1052 -195
- package/dist/chunk-MNQNHYWL.js.map +1 -0
- package/dist/{chunk-SA3A2SPY.js → chunk-ZWSCLHG6.js} +2 -2
- package/dist/cli-main.cjs +1083 -232
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1062 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +331 -10
- package/dist/index.d.ts +331 -10
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1076 -225
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-ZKIQOBHT.js.map +0 -1
- /package/dist/{chunk-RMJUGTAQ.js.map → chunk-CQBLH7CE.js.map} +0 -0
- /package/dist/{chunk-SA3A2SPY.js.map → chunk-ZWSCLHG6.js.map} +0 -0
package/dist/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,
|
|
@@ -122,6 +123,7 @@ __export(index_exports, {
|
|
|
122
123
|
listPins: () => listPins,
|
|
123
124
|
loadQmd: () => loadQmd,
|
|
124
125
|
matchToDiff: () => matchToDiff,
|
|
126
|
+
matchesTags: () => matchesTags,
|
|
125
127
|
mergedContextBudgets: () => mergedContextBudgets,
|
|
126
128
|
neighbours: () => neighbours,
|
|
127
129
|
pack: () => pack,
|
|
@@ -132,6 +134,7 @@ __export(index_exports, {
|
|
|
132
134
|
readMergedPins: () => readMergedPins,
|
|
133
135
|
readPinsLayer: () => readPinsLayer,
|
|
134
136
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
137
|
+
reassessPacket: () => reassessPacket,
|
|
135
138
|
regexResolver: () => regexResolver,
|
|
136
139
|
renderCatalogLine: () => renderCatalogLine,
|
|
137
140
|
renderIndex: () => renderIndex,
|
|
@@ -153,6 +156,7 @@ __export(index_exports, {
|
|
|
153
156
|
toHookJson: () => toHookJson,
|
|
154
157
|
trace: () => trace,
|
|
155
158
|
treeSitterLanguages: () => treeSitterLanguages,
|
|
159
|
+
unifiedDiff: () => unifiedDiff,
|
|
156
160
|
unpinBase: () => unpinBase,
|
|
157
161
|
validateBundle: () => validateBundle
|
|
158
162
|
});
|
|
@@ -181,9 +185,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
181
185
|
{ length: Math.min(limit, items.length) },
|
|
182
186
|
async () => {
|
|
183
187
|
while (!failed && next < items.length) {
|
|
184
|
-
const
|
|
188
|
+
const at2 = next++;
|
|
185
189
|
try {
|
|
186
|
-
out[
|
|
190
|
+
out[at2] = await fn(items[at2], at2);
|
|
187
191
|
} catch (error) {
|
|
188
192
|
failed = true;
|
|
189
193
|
throw error;
|
|
@@ -262,6 +266,14 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
262
266
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
263
267
|
message: "hash must be sha256:<64 hex chars>"
|
|
264
268
|
}).optional(),
|
|
269
|
+
/**
|
|
270
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
271
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
272
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
273
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
274
|
+
* comments, so reformatting the anchored code is not drift.
|
|
275
|
+
*/
|
|
276
|
+
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
265
277
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
266
278
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
267
279
|
/** Line count of the text the hash was taken over. */
|
|
@@ -676,8 +688,8 @@ function safeSegment(value) {
|
|
|
676
688
|
function revRef(rev) {
|
|
677
689
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
678
690
|
let hash = 5381;
|
|
679
|
-
for (let
|
|
680
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
691
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
692
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
681
693
|
}
|
|
682
694
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
683
695
|
}
|
|
@@ -786,8 +798,8 @@ function repoUrlIsSafe(repo) {
|
|
|
786
798
|
if (!scheme?.[1]) return false;
|
|
787
799
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
788
800
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
789
|
-
const
|
|
790
|
-
return
|
|
801
|
+
const at2 = authority.lastIndexOf("@");
|
|
802
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
791
803
|
}
|
|
792
804
|
function protocolArgs() {
|
|
793
805
|
const allowed = allowedProtocols();
|
|
@@ -878,7 +890,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
878
890
|
return new Map([
|
|
879
891
|
...rejected2,
|
|
880
892
|
...wants.map(
|
|
881
|
-
(want,
|
|
893
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
882
894
|
)
|
|
883
895
|
]);
|
|
884
896
|
}
|
|
@@ -1067,7 +1079,8 @@ function warningAnchor(entry) {
|
|
|
1067
1079
|
diffSize,
|
|
1068
1080
|
...reason !== void 0 ? { reason } : {},
|
|
1069
1081
|
...repo !== void 0 ? { repo } : {},
|
|
1070
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
1082
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
1083
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
1071
1084
|
};
|
|
1072
1085
|
}
|
|
1073
1086
|
function resolveHeads(from, byId) {
|
|
@@ -1238,7 +1251,7 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
1238
1251
|
return { ok: false, reason: "file-unreadable" };
|
|
1239
1252
|
}
|
|
1240
1253
|
});
|
|
1241
|
-
return new Map(wanted.map((file,
|
|
1254
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
1242
1255
|
}
|
|
1243
1256
|
|
|
1244
1257
|
// src/anchor-resolver/resolver.ts
|
|
@@ -1428,8 +1441,8 @@ async function ensureGrammar(language, options = {}) {
|
|
|
1428
1441
|
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1429
1442
|
const parts = [];
|
|
1430
1443
|
const total = pack2.tags.length;
|
|
1431
|
-
for (const [
|
|
1432
|
-
const name = `${language} tags${total > 1 ? ` part ${
|
|
1444
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1445
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1433
1446
|
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1434
1447
|
const held = await ensurePart(path, name, part, options);
|
|
1435
1448
|
if (held !== true) return miss(language, name, held);
|
|
@@ -1577,8 +1590,8 @@ function typeNameIn(receiver) {
|
|
|
1577
1590
|
while (stack.length) {
|
|
1578
1591
|
const node = stack.pop();
|
|
1579
1592
|
if (node.type === "type_identifier") return node.text;
|
|
1580
|
-
for (let
|
|
1581
|
-
const child = node.child(
|
|
1593
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1594
|
+
const child = node.child(at2);
|
|
1582
1595
|
if (child) stack.push(child);
|
|
1583
1596
|
}
|
|
1584
1597
|
}
|
|
@@ -1587,7 +1600,7 @@ function typeNameIn(receiver) {
|
|
|
1587
1600
|
function endsWith(chain, wanted) {
|
|
1588
1601
|
if (wanted.length > chain.length) return false;
|
|
1589
1602
|
const offset = chain.length - wanted.length;
|
|
1590
|
-
return wanted.every((segment,
|
|
1603
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1591
1604
|
}
|
|
1592
1605
|
function width(node) {
|
|
1593
1606
|
return node.endIndex - node.startIndex;
|
|
@@ -1613,6 +1626,26 @@ function spanOf(definition, source) {
|
|
|
1613
1626
|
};
|
|
1614
1627
|
}
|
|
1615
1628
|
|
|
1629
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1630
|
+
function tokens(root) {
|
|
1631
|
+
const out = [];
|
|
1632
|
+
const stack = [root];
|
|
1633
|
+
while (stack.length) {
|
|
1634
|
+
const node = stack.pop();
|
|
1635
|
+
if (node.type.includes("comment")) continue;
|
|
1636
|
+
if (node.childCount === 0) {
|
|
1637
|
+
const text = node.text.trim();
|
|
1638
|
+
if (text) out.push(text);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1642
|
+
const child = node.child(at2);
|
|
1643
|
+
if (child) stack.push(child);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1616
1649
|
// src/tree-sitter-resolver/resolver.ts
|
|
1617
1650
|
var TREE_CACHE_LIMIT = 32;
|
|
1618
1651
|
var TreeSitterResolver = class {
|
|
@@ -1658,7 +1691,7 @@ var TreeSitterResolver = class {
|
|
|
1658
1691
|
(language) => this.load(language)
|
|
1659
1692
|
);
|
|
1660
1693
|
languages.forEach(
|
|
1661
|
-
(language,
|
|
1694
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1662
1695
|
);
|
|
1663
1696
|
}
|
|
1664
1697
|
/**
|
|
@@ -1746,6 +1779,60 @@ var TreeSitterResolver = class {
|
|
|
1746
1779
|
this.trees.set(key2, parsed);
|
|
1747
1780
|
return parsed;
|
|
1748
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1784
|
+
*
|
|
1785
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1786
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1787
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1788
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1789
|
+
*/
|
|
1790
|
+
spans(source, file) {
|
|
1791
|
+
const language = languageForFile(file);
|
|
1792
|
+
if (!language) return [];
|
|
1793
|
+
const loaded = this.loaded.get(language);
|
|
1794
|
+
if (!loaded) return [];
|
|
1795
|
+
const parsed = this.parse(language, loaded, source);
|
|
1796
|
+
if (!parsed) return [];
|
|
1797
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1798
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1799
|
+
span: spanOf(definition, source)
|
|
1800
|
+
}));
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1804
|
+
* joined by single spaces.
|
|
1805
|
+
*
|
|
1806
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1807
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1808
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1809
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1810
|
+
* those are leaves.
|
|
1811
|
+
*
|
|
1812
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1813
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1814
|
+
*/
|
|
1815
|
+
normalize(text, file) {
|
|
1816
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1817
|
+
if (!language) return null;
|
|
1818
|
+
const loaded = this.loaded.get(language);
|
|
1819
|
+
if (!loaded) return null;
|
|
1820
|
+
const parser = this.parser;
|
|
1821
|
+
if (!parser) return null;
|
|
1822
|
+
let tree;
|
|
1823
|
+
try {
|
|
1824
|
+
parser.setLanguage(loaded.language);
|
|
1825
|
+
tree = parser.parse(text);
|
|
1826
|
+
} catch {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
if (!tree) return null;
|
|
1830
|
+
try {
|
|
1831
|
+
return tokens(tree.rootNode).join(" ");
|
|
1832
|
+
} finally {
|
|
1833
|
+
tree.delete();
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1749
1836
|
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1750
1837
|
reset() {
|
|
1751
1838
|
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
@@ -1899,7 +1986,7 @@ var regexResolver = {
|
|
|
1899
1986
|
);
|
|
1900
1987
|
const nearest = Math.min(...distances);
|
|
1901
1988
|
if (Number.isFinite(nearest)) {
|
|
1902
|
-
candidates = candidates.filter((_,
|
|
1989
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1903
1990
|
}
|
|
1904
1991
|
}
|
|
1905
1992
|
if (candidates.length !== 1) return null;
|
|
@@ -1914,8 +2001,8 @@ function escapeRegExp(value) {
|
|
|
1914
2001
|
}
|
|
1915
2002
|
function distanceToParent(lines, index2, parent) {
|
|
1916
2003
|
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1917
|
-
for (let
|
|
1918
|
-
if (parent.test(lines[
|
|
2004
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
2005
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1919
2006
|
}
|
|
1920
2007
|
return Number.POSITIVE_INFINITY;
|
|
1921
2008
|
}
|
|
@@ -1947,10 +2034,12 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1947
2034
|
if (attempt.reason === "symbol-not-found") continue;
|
|
1948
2035
|
return { ok: false, reason: attempt.reason };
|
|
1949
2036
|
}
|
|
2037
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1950
2038
|
return {
|
|
1951
2039
|
ok: true,
|
|
1952
2040
|
span: attempt.span,
|
|
1953
|
-
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
2041
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
2042
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1954
2043
|
};
|
|
1955
2044
|
}
|
|
1956
2045
|
return { ok: false, reason: "symbol-not-found" };
|
|
@@ -1978,6 +2067,11 @@ function resolverChanged(source, anchor, produced) {
|
|
|
1978
2067
|
);
|
|
1979
2068
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1980
2069
|
}
|
|
2070
|
+
function anchorHashOf(anchor, outcome) {
|
|
2071
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2072
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2073
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2074
|
+
}
|
|
1981
2075
|
|
|
1982
2076
|
// src/anchor-resolver/drift.ts
|
|
1983
2077
|
async function detectAnchorDrift(records, options = {}) {
|
|
@@ -2057,16 +2151,29 @@ function unresolved(anchor, reason, repo) {
|
|
|
2057
2151
|
state: "unresolved",
|
|
2058
2152
|
diffSize: null,
|
|
2059
2153
|
...reason ? { reason } : {},
|
|
2060
|
-
...repo ? { repo } : {}
|
|
2154
|
+
...repo ? { repo } : {},
|
|
2155
|
+
...classOf(reason)
|
|
2061
2156
|
};
|
|
2062
2157
|
}
|
|
2158
|
+
function provisionalDriftClass(entry) {
|
|
2159
|
+
if (entry.state === "unresolved") {
|
|
2160
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
2161
|
+
}
|
|
2162
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
2163
|
+
}
|
|
2164
|
+
function classOf(reason) {
|
|
2165
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
2166
|
+
return settled ? { class: settled } : {};
|
|
2167
|
+
}
|
|
2063
2168
|
function hashIn(source, anchor, resolvers) {
|
|
2064
2169
|
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2065
2170
|
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2171
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2066
2172
|
return {
|
|
2067
2173
|
ok: true,
|
|
2068
2174
|
current: {
|
|
2069
|
-
hash
|
|
2175
|
+
hash,
|
|
2176
|
+
kind,
|
|
2070
2177
|
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2071
2178
|
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2072
2179
|
}
|
|
@@ -2079,11 +2186,14 @@ function resolverExtras(source, anchor, current) {
|
|
|
2079
2186
|
};
|
|
2080
2187
|
}
|
|
2081
2188
|
function compared(anchor, current, extra = {}) {
|
|
2189
|
+
const matched = current.hash === anchor.hash;
|
|
2082
2190
|
return {
|
|
2083
2191
|
...base(anchor),
|
|
2084
|
-
state:
|
|
2192
|
+
state: matched ? "match" : "drifted",
|
|
2085
2193
|
currentHash: current.hash,
|
|
2194
|
+
hashKind: current.kind,
|
|
2086
2195
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
2196
|
+
...matched ? {} : { class: "changed" },
|
|
2087
2197
|
...extra
|
|
2088
2198
|
};
|
|
2089
2199
|
}
|
|
@@ -2240,8 +2350,8 @@ async function isStale(bundlePath2) {
|
|
|
2240
2350
|
let stale = false;
|
|
2241
2351
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
2242
2352
|
if (stale) return;
|
|
2243
|
-
const
|
|
2244
|
-
if (
|
|
2353
|
+
const at2 = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2354
|
+
if (at2 > indexAt) stale = true;
|
|
2245
2355
|
});
|
|
2246
2356
|
return stale;
|
|
2247
2357
|
}
|
|
@@ -2536,8 +2646,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
2536
2646
|
return [...reached.values()].sort(byGeneratedAt);
|
|
2537
2647
|
}
|
|
2538
2648
|
function byGeneratedAt(left, right) {
|
|
2539
|
-
const
|
|
2540
|
-
return
|
|
2649
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
2650
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
2541
2651
|
}
|
|
2542
2652
|
|
|
2543
2653
|
// src/pack.ts
|
|
@@ -2624,6 +2734,13 @@ function typeRank(record) {
|
|
|
2624
2734
|
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
2625
2735
|
}
|
|
2626
2736
|
|
|
2737
|
+
// src/kb-tags.ts
|
|
2738
|
+
function matchesTags(record, filter) {
|
|
2739
|
+
if (!filter.tags?.length && !filter.excludeTags?.length) return true;
|
|
2740
|
+
const carried = new Set(record.frontmatter.tags ?? []);
|
|
2741
|
+
return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2627
2744
|
// src/catalog.ts
|
|
2628
2745
|
var EMPTY_STANDINGS = {
|
|
2629
2746
|
current: 0,
|
|
@@ -2634,7 +2751,7 @@ var EMPTY_STANDINGS = {
|
|
|
2634
2751
|
};
|
|
2635
2752
|
function catalog(bundle, options = {}) {
|
|
2636
2753
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2637
|
-
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
2754
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
|
|
2638
2755
|
conceptId: hit.record.conceptId,
|
|
2639
2756
|
type: hit.record.frontmatter.type,
|
|
2640
2757
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2907,13 +3024,16 @@ var KbStore = class {
|
|
|
2907
3024
|
return this.parse(conceptId2, raw);
|
|
2908
3025
|
}
|
|
2909
3026
|
/**
|
|
2910
|
-
* Every record in the bundle, optionally narrowed to one type
|
|
3027
|
+
* Every record in the bundle, optionally narrowed to one type and to the
|
|
3028
|
+
* records carrying every tag in `filter.tags`. Selection only — `excludeTags`
|
|
3029
|
+
* is not taken here, because `query`, `catalog` and `load` read through this
|
|
3030
|
+
* and must adjudicate over the whole base.
|
|
2911
3031
|
*
|
|
2912
3032
|
* A file that fails to parse is skipped and logged rather than thrown: one
|
|
2913
3033
|
* malformed record — hand-edited, or written by a producer we don't know —
|
|
2914
3034
|
* must not make the whole bundle unreadable.
|
|
2915
3035
|
*/
|
|
2916
|
-
async list(bundlePath2, type) {
|
|
3036
|
+
async list(bundlePath2, type, filter = {}) {
|
|
2917
3037
|
const root = this.root(bundlePath2);
|
|
2918
3038
|
let names;
|
|
2919
3039
|
try {
|
|
@@ -2927,7 +3047,9 @@ var KbStore = class {
|
|
|
2927
3047
|
DEFAULT_IO_CONCURRENCY,
|
|
2928
3048
|
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
|
|
2929
3049
|
);
|
|
2930
|
-
return records.filter(
|
|
3050
|
+
return records.filter(
|
|
3051
|
+
(record) => record !== null && matchesTags(record, filter)
|
|
3052
|
+
);
|
|
2931
3053
|
}
|
|
2932
3054
|
/**
|
|
2933
3055
|
* Moves a record's status, preserving everything else.
|
|
@@ -2973,8 +3095,8 @@ var KbStore = class {
|
|
|
2973
3095
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
2974
3096
|
* logs what it publishes.
|
|
2975
3097
|
*/
|
|
2976
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
2977
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
3098
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3099
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
2978
3100
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
2979
3101
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
2980
3102
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -3026,14 +3148,14 @@ var KbStore = class {
|
|
|
3026
3148
|
return superseded;
|
|
3027
3149
|
}
|
|
3028
3150
|
/** Resolves an open question, stamping who answered and when. */
|
|
3029
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
3151
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3030
3152
|
return this.mutate(
|
|
3031
3153
|
bundlePath2,
|
|
3032
3154
|
conceptId2,
|
|
3033
3155
|
(frontmatter) => ({
|
|
3034
3156
|
...frontmatter,
|
|
3035
3157
|
strauss_status: "resolved",
|
|
3036
|
-
strauss_answered: { by: actor, at }
|
|
3158
|
+
strauss_answered: { by: actor, at: at2 }
|
|
3037
3159
|
}),
|
|
3038
3160
|
{ operation: "answer", by: actor },
|
|
3039
3161
|
(body) => `${body.trimEnd()}
|
|
@@ -3067,9 +3189,10 @@ ${answer}
|
|
|
3067
3189
|
/* @__PURE__ */ new Date(),
|
|
3068
3190
|
await this.detectDrift(narrowed, options.repoRoot)
|
|
3069
3191
|
);
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3192
|
+
const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
|
|
3193
|
+
if (options.includeNonCurrent) return kept;
|
|
3194
|
+
const present = new Set(kept.map((hit) => hit.record.conceptId));
|
|
3195
|
+
return kept.filter(
|
|
3073
3196
|
(hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
|
|
3074
3197
|
);
|
|
3075
3198
|
}
|
|
@@ -3172,14 +3295,17 @@ ${answer}
|
|
|
3172
3295
|
/* @__PURE__ */ new Date(),
|
|
3173
3296
|
await this.detectDrift(wanted, options.repoRoot)
|
|
3174
3297
|
);
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3298
|
+
const kept = adjudicated.filter(
|
|
3299
|
+
(hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
|
|
3300
|
+
);
|
|
3301
|
+
const records = kept.filter((hit) => hit.standing !== "superseded");
|
|
3302
|
+
const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3177
3303
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
3178
3304
|
const bundleDigestValue = bundleDigest(records, superseded);
|
|
3179
3305
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
3180
3306
|
return {
|
|
3181
3307
|
loaded: false,
|
|
3182
|
-
recordCount:
|
|
3308
|
+
recordCount: kept.length,
|
|
3183
3309
|
approxTokens: approxTokens2,
|
|
3184
3310
|
budgetTokens,
|
|
3185
3311
|
message: refusalMessage({
|
|
@@ -3192,7 +3318,7 @@ ${answer}
|
|
|
3192
3318
|
}
|
|
3193
3319
|
return {
|
|
3194
3320
|
loaded: true,
|
|
3195
|
-
recordCount:
|
|
3321
|
+
recordCount: kept.length,
|
|
3196
3322
|
tokensLoaded: approxTokens2,
|
|
3197
3323
|
budgetTokens: options.all ? null : budgetTokens,
|
|
3198
3324
|
records,
|
|
@@ -3202,24 +3328,34 @@ ${answer}
|
|
|
3202
3328
|
}
|
|
3203
3329
|
/**
|
|
3204
3330
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
3205
|
-
* the same way, handed back as a stamp.
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3331
|
+
* the same way, handed back as a stamp.
|
|
3332
|
+
*
|
|
3333
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
3334
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
3335
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
3336
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
3208
3337
|
*/
|
|
3209
|
-
async stamp(bundlePath2) {
|
|
3338
|
+
async stamp(bundlePath2, options = {}) {
|
|
3210
3339
|
const bundle = await this.list(bundlePath2);
|
|
3211
3340
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
3212
3341
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
3213
3342
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3214
3343
|
const stamped = bundleStamp(current, superseded);
|
|
3215
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
3344
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
3345
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
3346
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
3347
|
+
(entries) => entries.some(
|
|
3348
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
3349
|
+
)
|
|
3350
|
+
).length;
|
|
3216
3351
|
return {
|
|
3217
3352
|
path: bundlePath2,
|
|
3218
3353
|
digest: stamped.digest,
|
|
3219
3354
|
recordCount: bundle.length,
|
|
3220
3355
|
superseded: superseded.length,
|
|
3221
3356
|
newestAt: dates.at(-1) ?? null,
|
|
3222
|
-
records: stamped.records
|
|
3357
|
+
records: stamped.records,
|
|
3358
|
+
drifted: drifted2
|
|
3223
3359
|
};
|
|
3224
3360
|
}
|
|
3225
3361
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -3669,14 +3805,17 @@ function asBudgets(value) {
|
|
|
3669
3805
|
if (value === null || typeof value !== "object") return {};
|
|
3670
3806
|
const table2 = value;
|
|
3671
3807
|
const pick = (key2, min) => {
|
|
3672
|
-
const
|
|
3673
|
-
return typeof
|
|
3808
|
+
const raw2 = table2[key2];
|
|
3809
|
+
return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
|
|
3674
3810
|
};
|
|
3675
3811
|
const budgetTokens = pick("budgetTokens", 1);
|
|
3676
3812
|
const fullUnderTokens = pick("fullUnderTokens", 0);
|
|
3813
|
+
const raw = table2["excludeTags"];
|
|
3814
|
+
const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
3677
3815
|
return {
|
|
3678
3816
|
...budgetTokens ? { budgetTokens } : {},
|
|
3679
|
-
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
|
|
3817
|
+
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
|
|
3818
|
+
...excludeTags ? { excludeTags } : {}
|
|
3680
3819
|
};
|
|
3681
3820
|
}
|
|
3682
3821
|
function contextProfileBudgets(manifest, profile) {
|
|
@@ -3876,7 +4015,7 @@ async function listPins(store, workspaceDir) {
|
|
|
3876
4015
|
}
|
|
3877
4016
|
|
|
3878
4017
|
// src/kb-pins/pin.ts
|
|
3879
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
4018
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
3880
4019
|
const layer = options.layer ?? "project";
|
|
3881
4020
|
const root = layerRoot(workspaceDir, layer);
|
|
3882
4021
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -3904,7 +4043,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3904
4043
|
return {
|
|
3905
4044
|
path: existing.path,
|
|
3906
4045
|
layer,
|
|
3907
|
-
pinnedAt: existing.pinnedAt ??
|
|
4046
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
3908
4047
|
alreadyPinned: true,
|
|
3909
4048
|
...updated.mode ? { mode: updated.mode } : {},
|
|
3910
4049
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -3914,7 +4053,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3914
4053
|
}
|
|
3915
4054
|
const entry = {
|
|
3916
4055
|
path: storablePath(root, bundlePath2),
|
|
3917
|
-
pinnedAt:
|
|
4056
|
+
pinnedAt: at2,
|
|
3918
4057
|
...fields
|
|
3919
4058
|
};
|
|
3920
4059
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -3924,7 +4063,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3924
4063
|
return {
|
|
3925
4064
|
path: entry.path,
|
|
3926
4065
|
layer,
|
|
3927
|
-
pinnedAt:
|
|
4066
|
+
pinnedAt: at2,
|
|
3928
4067
|
alreadyPinned: false,
|
|
3929
4068
|
...fields,
|
|
3930
4069
|
...warning ? { warning } : {}
|
|
@@ -3992,7 +4131,7 @@ function preamble() {
|
|
|
3992
4131
|
"tokens."
|
|
3993
4132
|
].join("\n");
|
|
3994
4133
|
}
|
|
3995
|
-
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
|
|
4134
|
+
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
|
|
3996
4135
|
const bundle = await store.list(absolutePath);
|
|
3997
4136
|
if (bundle.length === 0) {
|
|
3998
4137
|
return {
|
|
@@ -4006,7 +4145,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
4006
4145
|
let degradedFrom;
|
|
4007
4146
|
if (fullCap > 0) {
|
|
4008
4147
|
const full = await store.load(absolutePath, {
|
|
4009
|
-
budgetTokens: fullCap
|
|
4148
|
+
budgetTokens: fullCap,
|
|
4149
|
+
excludeTags
|
|
4010
4150
|
});
|
|
4011
4151
|
if (!full.loaded && pinMode === "full") {
|
|
4012
4152
|
degradedFrom = { approxTokens: full.approxTokens };
|
|
@@ -4036,7 +4176,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
4036
4176
|
};
|
|
4037
4177
|
}
|
|
4038
4178
|
}
|
|
4039
|
-
const adjudicated = adjudicate(bundle, bundle)
|
|
4179
|
+
const adjudicated = adjudicate(bundle, bundle).filter(
|
|
4180
|
+
(hit) => matchesTags(hit.record, { excludeTags })
|
|
4181
|
+
);
|
|
4040
4182
|
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
4041
4183
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
4042
4184
|
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
@@ -4057,6 +4199,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
4057
4199
|
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
4058
4200
|
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
4059
4201
|
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
4202
|
+
const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
|
|
4060
4203
|
const pins = merged.pins.filter(
|
|
4061
4204
|
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
4062
4205
|
);
|
|
@@ -4077,7 +4220,8 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
4077
4220
|
pin.absolutePath,
|
|
4078
4221
|
fullUnderTokens,
|
|
4079
4222
|
pin.mode,
|
|
4080
|
-
budgetTokens
|
|
4223
|
+
budgetTokens,
|
|
4224
|
+
excludeTags
|
|
4081
4225
|
),
|
|
4082
4226
|
frozen: pin.frozen === true
|
|
4083
4227
|
}))
|
|
@@ -4452,18 +4596,18 @@ function expired(hits, now) {
|
|
|
4452
4596
|
for (const hit of hits) {
|
|
4453
4597
|
const raw = hit.record.frontmatter.stale_after;
|
|
4454
4598
|
if (!raw) continue;
|
|
4455
|
-
const
|
|
4456
|
-
if (Number.isNaN(
|
|
4599
|
+
const at2 = Date.parse(raw);
|
|
4600
|
+
if (Number.isNaN(at2)) {
|
|
4457
4601
|
findings.push(
|
|
4458
4602
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
4459
4603
|
);
|
|
4460
4604
|
continue;
|
|
4461
4605
|
}
|
|
4462
|
-
if (
|
|
4606
|
+
if (at2 < now.getTime()) {
|
|
4463
4607
|
findings.push(
|
|
4464
4608
|
finding(
|
|
4465
4609
|
hit.record,
|
|
4466
|
-
`stale since ${raw} (${daysBetween(
|
|
4610
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
4467
4611
|
)
|
|
4468
4612
|
);
|
|
4469
4613
|
}
|
|
@@ -4476,12 +4620,12 @@ function expiring(hits, now, withinDays) {
|
|
|
4476
4620
|
for (const hit of hits) {
|
|
4477
4621
|
const raw = hit.record.frontmatter.stale_after;
|
|
4478
4622
|
if (!raw) continue;
|
|
4479
|
-
const
|
|
4480
|
-
if (Number.isNaN(
|
|
4623
|
+
const at2 = Date.parse(raw);
|
|
4624
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
4481
4625
|
findings.push(
|
|
4482
4626
|
finding(
|
|
4483
4627
|
hit.record,
|
|
4484
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
4628
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
4485
4629
|
)
|
|
4486
4630
|
);
|
|
4487
4631
|
}
|
|
@@ -4651,13 +4795,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
4651
4795
|
);
|
|
4652
4796
|
}
|
|
4653
4797
|
function describeAnchor(anchor) {
|
|
4654
|
-
const
|
|
4655
|
-
if (anchor.
|
|
4798
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
4799
|
+
if (anchor.class === "gone") {
|
|
4800
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
4801
|
+
}
|
|
4802
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
4656
4803
|
if (anchor.remoteState === "drifted-on-default") {
|
|
4657
|
-
return `${
|
|
4804
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
4658
4805
|
}
|
|
4659
|
-
if (anchor.diffSize === null) return `${
|
|
4660
|
-
return anchor.diffSize === 0 ? `${
|
|
4806
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
4807
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
4661
4808
|
}
|
|
4662
4809
|
function replaces(later, earlier) {
|
|
4663
4810
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -4674,9 +4821,9 @@ function daysBetween(from, to) {
|
|
|
4674
4821
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
4675
4822
|
}
|
|
4676
4823
|
function ageInDays(record, now) {
|
|
4677
|
-
const
|
|
4678
|
-
if (!
|
|
4679
|
-
const written = Date.parse(
|
|
4824
|
+
const at2 = record.frontmatter.generated?.at;
|
|
4825
|
+
if (!at2) return null;
|
|
4826
|
+
const written = Date.parse(at2);
|
|
4680
4827
|
if (Number.isNaN(written)) return null;
|
|
4681
4828
|
return daysBetween(written, now.getTime());
|
|
4682
4829
|
}
|
|
@@ -4735,6 +4882,9 @@ var import_zod9 = require("zod");
|
|
|
4735
4882
|
var import_zod8 = require("zod");
|
|
4736
4883
|
var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
4737
4884
|
var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
4885
|
+
var TAGS = import_zod8.z.array(import_zod8.z.string().min(1)).optional().describe(
|
|
4886
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
4887
|
+
);
|
|
4738
4888
|
var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
|
|
4739
4889
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
4740
4890
|
);
|
|
@@ -4748,14 +4898,49 @@ function argvFlag(argv, name) {
|
|
|
4748
4898
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
4749
4899
|
return value2;
|
|
4750
4900
|
}
|
|
4751
|
-
const
|
|
4752
|
-
if (
|
|
4753
|
-
const value = argv[
|
|
4901
|
+
const at2 = argv.indexOf(name);
|
|
4902
|
+
if (at2 === -1) return void 0;
|
|
4903
|
+
const value = argv[at2 + 1];
|
|
4754
4904
|
if (value === void 0 || value.startsWith("--")) {
|
|
4755
4905
|
throw new KbMissingFlagValueError(name);
|
|
4756
4906
|
}
|
|
4757
4907
|
return value;
|
|
4758
4908
|
}
|
|
4909
|
+
function argvFlags(argv, name) {
|
|
4910
|
+
const values = [];
|
|
4911
|
+
for (const [at2, arg] of argv.entries()) {
|
|
4912
|
+
if (arg.startsWith(`${name}=`)) {
|
|
4913
|
+
const value = arg.slice(name.length + 1);
|
|
4914
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
4915
|
+
values.push(value);
|
|
4916
|
+
} else if (arg === name) {
|
|
4917
|
+
const value = argv[at2 + 1];
|
|
4918
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
4919
|
+
throw new KbMissingFlagValueError(name);
|
|
4920
|
+
}
|
|
4921
|
+
values.push(value);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
return values;
|
|
4925
|
+
}
|
|
4926
|
+
function argvWithout(argv, ...names) {
|
|
4927
|
+
const kept = [];
|
|
4928
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
4929
|
+
const arg = argv[at2];
|
|
4930
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
4931
|
+
if (names.includes(arg)) {
|
|
4932
|
+
at2 += 1;
|
|
4933
|
+
continue;
|
|
4934
|
+
}
|
|
4935
|
+
kept.push(arg);
|
|
4936
|
+
}
|
|
4937
|
+
return kept;
|
|
4938
|
+
}
|
|
4939
|
+
function argvPositional(argv, ...names) {
|
|
4940
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
4941
|
+
(arg) => !arg.startsWith("--")
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4759
4944
|
|
|
4760
4945
|
// src/commands/anchor-resolve.ts
|
|
4761
4946
|
function resolverSummary(results) {
|
|
@@ -4843,11 +5028,14 @@ var anchorResolveCommand = define({
|
|
|
4843
5028
|
}
|
|
4844
5029
|
const resolved = outcome.span;
|
|
4845
5030
|
const producedBy = outcome.resolver;
|
|
4846
|
-
const currentHash =
|
|
5031
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
4847
5032
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
5033
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
5034
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
4848
5035
|
const stamped = {
|
|
4849
5036
|
...anchor,
|
|
4850
|
-
hash:
|
|
5037
|
+
hash: stampedHash,
|
|
5038
|
+
hash_kind: stampedKind,
|
|
4851
5039
|
lines: currentLines,
|
|
4852
5040
|
resolved_at: now(),
|
|
4853
5041
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -4857,7 +5045,8 @@ var anchorResolveCommand = define({
|
|
|
4857
5045
|
results.push({
|
|
4858
5046
|
...base2,
|
|
4859
5047
|
state: "stamped",
|
|
4860
|
-
currentHash,
|
|
5048
|
+
currentHash: stampedHash,
|
|
5049
|
+
hashKind: stampedKind,
|
|
4861
5050
|
...producedBy ? { resolver: producedBy } : {}
|
|
4862
5051
|
});
|
|
4863
5052
|
updated.push(stamped);
|
|
@@ -4869,6 +5058,7 @@ var anchorResolveCommand = define({
|
|
|
4869
5058
|
...base2,
|
|
4870
5059
|
state: "drifted",
|
|
4871
5060
|
currentHash,
|
|
5061
|
+
hashKind: kind,
|
|
4872
5062
|
diffSize: lineDelta(anchor, currentLines),
|
|
4873
5063
|
...producedBy ? { resolver: producedBy } : {},
|
|
4874
5064
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
@@ -4897,6 +5087,7 @@ var anchorResolveCommand = define({
|
|
|
4897
5087
|
...base2,
|
|
4898
5088
|
state: "match",
|
|
4899
5089
|
currentHash,
|
|
5090
|
+
hashKind: kind,
|
|
4900
5091
|
...producedBy ? { resolver: producedBy } : {},
|
|
4901
5092
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4902
5093
|
});
|
|
@@ -5066,25 +5257,39 @@ var import_zod12 = require("zod");
|
|
|
5066
5257
|
var catalogCommand = define({
|
|
5067
5258
|
name: "catalog",
|
|
5068
5259
|
tool: "kb_catalog",
|
|
5069
|
-
usage: "catalog [type]",
|
|
5260
|
+
usage: "catalog [type] [--tag T]...",
|
|
5070
5261
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
5071
5262
|
input: import_zod12.z.object({
|
|
5072
5263
|
bundlePath,
|
|
5073
|
-
type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
|
|
5074
|
-
|
|
5075
|
-
fromArgv: (argv, path) => ({
|
|
5076
|
-
bundlePath: path,
|
|
5077
|
-
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
5264
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
|
|
5265
|
+
tags: TAGS
|
|
5078
5266
|
}),
|
|
5079
|
-
|
|
5080
|
-
|
|
5267
|
+
fromArgv: (argv, path) => {
|
|
5268
|
+
const tags = argvFlags(argv, "--tag");
|
|
5269
|
+
const type = argvPositional(argv, "--tag");
|
|
5270
|
+
return {
|
|
5271
|
+
bundlePath: path,
|
|
5272
|
+
...type ? { type } : {},
|
|
5273
|
+
...tags.length ? { tags } : {}
|
|
5274
|
+
};
|
|
5275
|
+
},
|
|
5276
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => render(
|
|
5277
|
+
await store.catalog(path, {
|
|
5278
|
+
...type ? { type } : {},
|
|
5279
|
+
...tags ? { tags } : {}
|
|
5280
|
+
}),
|
|
5081
5281
|
path,
|
|
5082
|
-
type
|
|
5282
|
+
type,
|
|
5283
|
+
tags
|
|
5083
5284
|
)
|
|
5084
5285
|
});
|
|
5085
|
-
function render(result, bundle, type) {
|
|
5286
|
+
function render(result, bundle, type, tags) {
|
|
5287
|
+
const narrowed = [
|
|
5288
|
+
...type ? [type] : [],
|
|
5289
|
+
...tags?.length ? [`tags: ${tags.join(", ")}`] : []
|
|
5290
|
+
].join(" \xB7 ");
|
|
5086
5291
|
const lines = [
|
|
5087
|
-
`# KB Catalog${
|
|
5292
|
+
`# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
|
|
5088
5293
|
`bundle: ${bundle}`,
|
|
5089
5294
|
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
5090
5295
|
];
|
|
@@ -5096,7 +5301,7 @@ function render(result, bundle, type) {
|
|
|
5096
5301
|
lines.push("");
|
|
5097
5302
|
if (!result.entries.length) {
|
|
5098
5303
|
lines.push(
|
|
5099
|
-
|
|
5304
|
+
narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
|
|
5100
5305
|
);
|
|
5101
5306
|
} else {
|
|
5102
5307
|
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
@@ -5129,7 +5334,7 @@ var import_zod13 = require("zod");
|
|
|
5129
5334
|
var contextCommand = define({
|
|
5130
5335
|
name: "context",
|
|
5131
5336
|
tool: "kb_context",
|
|
5132
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
5337
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
5133
5338
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
5134
5339
|
input: import_zod13.z.object({
|
|
5135
5340
|
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
@@ -5141,6 +5346,9 @@ var contextCommand = define({
|
|
|
5141
5346
|
profile: import_zod13.z.string().optional().describe(
|
|
5142
5347
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
5143
5348
|
),
|
|
5349
|
+
excludeTags: import_zod13.z.array(import_zod13.z.string().min(1)).optional().describe(
|
|
5350
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
5351
|
+
),
|
|
5144
5352
|
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
5145
5353
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
5146
5354
|
),
|
|
@@ -5154,19 +5362,22 @@ var contextCommand = define({
|
|
|
5154
5362
|
const profile = argvFlag(argv, "--profile");
|
|
5155
5363
|
const format = argvFlag(argv, "--format");
|
|
5156
5364
|
const event = argvFlag(argv, "--event");
|
|
5365
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
5157
5366
|
return {
|
|
5158
5367
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
5159
5368
|
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
5160
5369
|
...profile ? { profile } : {},
|
|
5370
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
5161
5371
|
...format ? { format } : {},
|
|
5162
5372
|
...event ? { event } : {}
|
|
5163
5373
|
};
|
|
5164
5374
|
},
|
|
5165
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
|
|
5375
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
5166
5376
|
const result = await buildContext(store, process.cwd(), {
|
|
5167
5377
|
...budgetTokens ? { budgetTokens } : {},
|
|
5168
5378
|
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
5169
5379
|
...profile ? { profile } : {},
|
|
5380
|
+
...excludeTags ? { excludeTags } : {},
|
|
5170
5381
|
// Degradations — a full pin that could not fit, a refused block — go
|
|
5171
5382
|
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
5172
5383
|
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
@@ -5180,14 +5391,567 @@ var contextCommand = define({
|
|
|
5180
5391
|
});
|
|
5181
5392
|
|
|
5182
5393
|
// src/commands/doctor.ts
|
|
5394
|
+
var import_zod15 = require("zod");
|
|
5395
|
+
|
|
5396
|
+
// src/drift/git.ts
|
|
5397
|
+
var import_node_child_process3 = require("child_process");
|
|
5398
|
+
var import_node_util3 = require("util");
|
|
5399
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
5400
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
5401
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
5402
|
+
async function git2(cwd, args) {
|
|
5403
|
+
const env = { ...process.env };
|
|
5404
|
+
delete env["GIT_DIR"];
|
|
5405
|
+
delete env["GIT_WORK_TREE"];
|
|
5406
|
+
delete env["GIT_INDEX_FILE"];
|
|
5407
|
+
try {
|
|
5408
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
5409
|
+
timeout: GIT_TIMEOUT_MS,
|
|
5410
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
5411
|
+
env
|
|
5412
|
+
});
|
|
5413
|
+
return { ok: true, stdout };
|
|
5414
|
+
} catch {
|
|
5415
|
+
return { ok: false };
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
async function listRepoFiles(repoRoot) {
|
|
5419
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
5420
|
+
if (!result.ok) return [];
|
|
5421
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
5422
|
+
}
|
|
5423
|
+
async function readOldSource(repoRoot, anchor) {
|
|
5424
|
+
if (!filePathIsSafe(anchor.file))
|
|
5425
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5426
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
5427
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
5428
|
+
if (shown2 !== null) {
|
|
5429
|
+
return {
|
|
5430
|
+
ok: true,
|
|
5431
|
+
source: shown2,
|
|
5432
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
const at2 = anchor.resolved_at;
|
|
5437
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
5438
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5439
|
+
}
|
|
5440
|
+
const found = await git2(repoRoot, [
|
|
5441
|
+
"log",
|
|
5442
|
+
"-1",
|
|
5443
|
+
"--format=%H",
|
|
5444
|
+
`--before=${at2}`,
|
|
5445
|
+
"--end-of-options",
|
|
5446
|
+
"HEAD",
|
|
5447
|
+
"--",
|
|
5448
|
+
anchor.file
|
|
5449
|
+
]);
|
|
5450
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
5451
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
5452
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5453
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
5454
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
5455
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
5456
|
+
}
|
|
5457
|
+
async function showFile(repoRoot, ref, file) {
|
|
5458
|
+
const path = file.replace(/^\.\//, "");
|
|
5459
|
+
const result = await git2(repoRoot, [
|
|
5460
|
+
"show",
|
|
5461
|
+
"--end-of-options",
|
|
5462
|
+
`${ref}:${path}`
|
|
5463
|
+
]);
|
|
5464
|
+
return result.ok ? result.stdout : null;
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
// src/drift/moved.ts
|
|
5468
|
+
var import_promises9 = require("fs/promises");
|
|
5469
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
5470
|
+
var SEARCH_BATCH = 64;
|
|
5471
|
+
function movedSearch(repoRoot, options = {}) {
|
|
5472
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
5473
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
5474
|
+
const resolver = new TreeSitterResolver();
|
|
5475
|
+
let repoFiles;
|
|
5476
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
5477
|
+
const filesForLanguage = async (language) => {
|
|
5478
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
5479
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
5480
|
+
};
|
|
5481
|
+
return {
|
|
5482
|
+
async find(anchor) {
|
|
5483
|
+
const stored = anchor.hash;
|
|
5484
|
+
if (!stored) return void 0;
|
|
5485
|
+
const language = languageForFile(anchor.file);
|
|
5486
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5487
|
+
const candidates = await filesForLanguage(language);
|
|
5488
|
+
if (!prepared.has(language)) {
|
|
5489
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
5490
|
+
prepared.add(language);
|
|
5491
|
+
}
|
|
5492
|
+
const floor = anchor.lines ?? 0;
|
|
5493
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
5494
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
5495
|
+
const hits = await mapLimit(
|
|
5496
|
+
batch,
|
|
5497
|
+
DEFAULT_IO_CONCURRENCY,
|
|
5498
|
+
async (file) => {
|
|
5499
|
+
const size2 = await sizeOf(file);
|
|
5500
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
5501
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
5502
|
+
}
|
|
5503
|
+
);
|
|
5504
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
5505
|
+
if (found) return found;
|
|
5506
|
+
}
|
|
5507
|
+
return void 0;
|
|
5508
|
+
}
|
|
5509
|
+
};
|
|
5510
|
+
}
|
|
5511
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
5512
|
+
const source = await read(file);
|
|
5513
|
+
if (!source.ok) return void 0;
|
|
5514
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
5515
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
5516
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
5517
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
5518
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
5519
|
+
return {
|
|
5520
|
+
file,
|
|
5521
|
+
symbol: found.symbol,
|
|
5522
|
+
startLine: found.span.startLine,
|
|
5523
|
+
endLine: found.span.endLine
|
|
5524
|
+
};
|
|
5525
|
+
}
|
|
5526
|
+
return void 0;
|
|
5527
|
+
}
|
|
5528
|
+
function diskSize(repoRoot) {
|
|
5529
|
+
return async (file) => {
|
|
5530
|
+
const path = anchorFilePath(repoRoot, file);
|
|
5531
|
+
if (path === null) return null;
|
|
5532
|
+
try {
|
|
5533
|
+
return (await (0, import_promises9.stat)(path)).size;
|
|
5534
|
+
} catch {
|
|
5535
|
+
return null;
|
|
5536
|
+
}
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
5540
|
+
const height = anchor.lines;
|
|
5541
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
5542
|
+
const source = await read(anchor.file);
|
|
5543
|
+
if (!source.ok) return void 0;
|
|
5544
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
5545
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
5546
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
5547
|
+
continue;
|
|
5548
|
+
}
|
|
5549
|
+
return {
|
|
5550
|
+
file: anchor.file,
|
|
5551
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5552
|
+
startLine: at2 + 1,
|
|
5553
|
+
endLine: at2 + height
|
|
5554
|
+
};
|
|
5555
|
+
}
|
|
5556
|
+
return void 0;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
// src/drift/classify.ts
|
|
5560
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
5561
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
5562
|
+
(anchor) => anchor.hash
|
|
5563
|
+
);
|
|
5564
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
5565
|
+
const treeSitter = new TreeSitterResolver();
|
|
5566
|
+
const resolvers = [treeSitter, regexResolver];
|
|
5567
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
5568
|
+
const wanted = [];
|
|
5569
|
+
entries.forEach((entry, at2) => {
|
|
5570
|
+
const anchor = anchors[at2];
|
|
5571
|
+
if (!anchor) return;
|
|
5572
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
5573
|
+
wanted.push({ anchor, entry });
|
|
5574
|
+
});
|
|
5575
|
+
if (!wanted.length) return [];
|
|
5576
|
+
await prepareResolvers(
|
|
5577
|
+
resolvers,
|
|
5578
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
5579
|
+
);
|
|
5580
|
+
const out = [];
|
|
5581
|
+
for (const { anchor, entry } of wanted) {
|
|
5582
|
+
const movedTo = await search.find(anchor);
|
|
5583
|
+
if (movedTo) {
|
|
5584
|
+
out.push({
|
|
5585
|
+
anchor,
|
|
5586
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
5587
|
+
class: "moved"
|
|
5588
|
+
});
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5591
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
5592
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
5593
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
5594
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
5595
|
+
out.push({
|
|
5596
|
+
anchor,
|
|
5597
|
+
entry: { ...entry, class: settled },
|
|
5598
|
+
class: settled,
|
|
5599
|
+
...newText !== void 0 ? { newText } : {},
|
|
5600
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
5601
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
return out;
|
|
5605
|
+
}
|
|
5606
|
+
function sameTokens(resolver, file, before, after) {
|
|
5607
|
+
if (before === after) return false;
|
|
5608
|
+
const left = resolver.normalize(before, file);
|
|
5609
|
+
const right = resolver.normalize(after, file);
|
|
5610
|
+
return left !== null && left === right;
|
|
5611
|
+
}
|
|
5612
|
+
async function currentText(reader, anchor, resolvers) {
|
|
5613
|
+
const read = await reader(anchor.file);
|
|
5614
|
+
if (!read.ok) return void 0;
|
|
5615
|
+
return spanIn(read.source, anchor, resolvers);
|
|
5616
|
+
}
|
|
5617
|
+
function spanIn(source, anchor, resolvers) {
|
|
5618
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
5619
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
// src/drift/diff.ts
|
|
5623
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
5624
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
5625
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
5626
|
+
function diffBudget(anchors) {
|
|
5627
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
5628
|
+
return Math.min(
|
|
5629
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
5630
|
+
Math.max(
|
|
5631
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
5632
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
5633
|
+
)
|
|
5634
|
+
);
|
|
5635
|
+
}
|
|
5636
|
+
function unifiedDiff(before, after, options = {}) {
|
|
5637
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
5638
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
5639
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
5640
|
+
const body = [];
|
|
5641
|
+
let added = 0;
|
|
5642
|
+
let removed = 0;
|
|
5643
|
+
for (const edit of edits(left, right)) {
|
|
5644
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
5645
|
+
else if (edit.kind === "remove") {
|
|
5646
|
+
body.push(`-${edit.line}`);
|
|
5647
|
+
removed += 1;
|
|
5648
|
+
} else {
|
|
5649
|
+
body.push(`+${edit.line}`);
|
|
5650
|
+
added += 1;
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
const truncated = body.length > max;
|
|
5654
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
5655
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
5656
|
+
const lines = [header, ...shown];
|
|
5657
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5658
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
5659
|
+
}
|
|
5660
|
+
function edits(left, right) {
|
|
5661
|
+
const rows = left.length;
|
|
5662
|
+
const cols = right.length;
|
|
5663
|
+
const table2 = Array.from(
|
|
5664
|
+
{ length: rows + 1 },
|
|
5665
|
+
() => new Array(cols + 1).fill(0)
|
|
5666
|
+
);
|
|
5667
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
5668
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
5669
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
5670
|
+
table2[row2 + 1][col2],
|
|
5671
|
+
table2[row2][col2 + 1]
|
|
5672
|
+
);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
const out = [];
|
|
5676
|
+
let row = 0;
|
|
5677
|
+
let col = 0;
|
|
5678
|
+
while (row < rows && col < cols) {
|
|
5679
|
+
if (left[row] === right[col]) {
|
|
5680
|
+
out.push({ kind: "same", line: left[row] });
|
|
5681
|
+
row += 1;
|
|
5682
|
+
col += 1;
|
|
5683
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
5684
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5685
|
+
row += 1;
|
|
5686
|
+
} else {
|
|
5687
|
+
out.push({ kind: "add", line: right[col] });
|
|
5688
|
+
col += 1;
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
for (; row < rows; row++)
|
|
5692
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5693
|
+
for (; col < cols; col++)
|
|
5694
|
+
out.push({ kind: "add", line: right[col] });
|
|
5695
|
+
return out;
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
// src/drift/packet.ts
|
|
5699
|
+
var PRESUMED_INVALID = [
|
|
5700
|
+
"fact",
|
|
5701
|
+
"constraint",
|
|
5702
|
+
"contract"
|
|
5703
|
+
];
|
|
5704
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
5705
|
+
var DEFAULT_NOTES = {
|
|
5706
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
5707
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
5708
|
+
review: "re-read the record against the new code"
|
|
5709
|
+
};
|
|
5710
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
5711
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
5712
|
+
...options.reader ? { reader: options.reader } : {},
|
|
5713
|
+
...options.search ? { search: options.search } : {},
|
|
5714
|
+
withHistory: options.withDiff !== false
|
|
5715
|
+
});
|
|
5716
|
+
const open = classified.filter(
|
|
5717
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
5718
|
+
);
|
|
5719
|
+
if (!open.length) return { packet: null, classified };
|
|
5720
|
+
const budget = diffBudget(open.length);
|
|
5721
|
+
const anchors = open.map(
|
|
5722
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5723
|
+
);
|
|
5724
|
+
const type = record.frontmatter.type;
|
|
5725
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
5726
|
+
return {
|
|
5727
|
+
classified,
|
|
5728
|
+
packet: {
|
|
5729
|
+
conceptId: record.conceptId,
|
|
5730
|
+
title: record.frontmatter.title ?? null,
|
|
5731
|
+
type,
|
|
5732
|
+
standing: options.standing ?? "unsettled",
|
|
5733
|
+
why: record.frontmatter.description ?? null,
|
|
5734
|
+
claim: claimOf(record),
|
|
5735
|
+
anchors,
|
|
5736
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
5737
|
+
conceptId: entry.conceptId,
|
|
5738
|
+
title: entry.title,
|
|
5739
|
+
standing: entry.standing,
|
|
5740
|
+
depth: entry.depth
|
|
5741
|
+
})),
|
|
5742
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
5743
|
+
default: fallback,
|
|
5744
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
5745
|
+
}
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
5749
|
+
const { entry } = found;
|
|
5750
|
+
const base2 = {
|
|
5751
|
+
file: entry.file,
|
|
5752
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
5753
|
+
class: found.class,
|
|
5754
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
5755
|
+
storedHash: entry.storedHash,
|
|
5756
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
5757
|
+
diffSize: entry.diffSize,
|
|
5758
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
5759
|
+
};
|
|
5760
|
+
if (!withDiff) return base2;
|
|
5761
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
5762
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
5763
|
+
}
|
|
5764
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
5765
|
+
maxLines
|
|
5766
|
+
});
|
|
5767
|
+
return {
|
|
5768
|
+
...base2,
|
|
5769
|
+
diff: {
|
|
5770
|
+
status: "ok",
|
|
5771
|
+
source: found.oldOrigin.kind,
|
|
5772
|
+
ref: found.oldOrigin.ref,
|
|
5773
|
+
unified: rendered.text,
|
|
5774
|
+
added: rendered.added,
|
|
5775
|
+
removed: rendered.removed,
|
|
5776
|
+
truncated: rendered.truncated
|
|
5777
|
+
}
|
|
5778
|
+
};
|
|
5779
|
+
}
|
|
5780
|
+
function claimOf(record) {
|
|
5781
|
+
const type = record.frontmatter.type;
|
|
5782
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
5783
|
+
if (!section) return null;
|
|
5784
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
5785
|
+
const start = lines.findIndex(
|
|
5786
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
5787
|
+
);
|
|
5788
|
+
if (start < 0) return null;
|
|
5789
|
+
const rest = lines.slice(start + 1);
|
|
5790
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
5791
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
5792
|
+
return text ? { section, text } : null;
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
// src/commands/reassess.ts
|
|
5183
5796
|
var import_zod14 = require("zod");
|
|
5184
|
-
var
|
|
5797
|
+
var reassessCommand = define({
|
|
5798
|
+
name: "reassess",
|
|
5799
|
+
tool: "kb_reassess",
|
|
5800
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5801
|
+
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.",
|
|
5802
|
+
input: import_zod14.z.object({
|
|
5803
|
+
bundlePath,
|
|
5804
|
+
conceptId,
|
|
5805
|
+
repoRoot: REPO_ROOT,
|
|
5806
|
+
withDiff: import_zod14.z.boolean().optional().describe(
|
|
5807
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5808
|
+
)
|
|
5809
|
+
}),
|
|
5810
|
+
fromArgv: (argv, path) => {
|
|
5811
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
5812
|
+
return {
|
|
5813
|
+
bundlePath: path,
|
|
5814
|
+
conceptId: argv[1],
|
|
5815
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
5816
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
5817
|
+
};
|
|
5818
|
+
},
|
|
5819
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
5820
|
+
const root = repoRoot ?? process.cwd();
|
|
5821
|
+
const bundle = await store.list(path);
|
|
5822
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
5823
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
5824
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
5825
|
+
const entries = drift?.get(id) ?? [];
|
|
5826
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
5827
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
5828
|
+
}
|
|
5829
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
5830
|
+
(hit) => hit.record.conceptId === id
|
|
5831
|
+
)?.standing;
|
|
5832
|
+
const impact2 = await store.impact(path, id);
|
|
5833
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
5834
|
+
...withDiff ? { withDiff: true } : {},
|
|
5835
|
+
impact: impact2,
|
|
5836
|
+
...standing ? { standing } : {}
|
|
5837
|
+
});
|
|
5838
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
5839
|
+
let frozen = false;
|
|
5840
|
+
const rebaselined = [];
|
|
5841
|
+
if (moves.length) {
|
|
5842
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
5843
|
+
for (const found of moves) {
|
|
5844
|
+
const to = found.entry.movedTo;
|
|
5845
|
+
if (!to) continue;
|
|
5846
|
+
relocated.set(found.anchor, {
|
|
5847
|
+
...found.anchor,
|
|
5848
|
+
file: to.file,
|
|
5849
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
5850
|
+
});
|
|
5851
|
+
rebaselined.push({
|
|
5852
|
+
file: found.anchor.file,
|
|
5853
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
5854
|
+
toFile: to.file,
|
|
5855
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
try {
|
|
5859
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
5860
|
+
} catch (error) {
|
|
5861
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
5862
|
+
frozen = true;
|
|
5863
|
+
}
|
|
5864
|
+
if (!frozen) {
|
|
5865
|
+
await store.updateAnchors(
|
|
5866
|
+
path,
|
|
5867
|
+
id,
|
|
5868
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
5869
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
5870
|
+
),
|
|
5871
|
+
actor
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
return {
|
|
5876
|
+
conceptId: id,
|
|
5877
|
+
packet,
|
|
5878
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
5879
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
5880
|
+
...frozen ? {
|
|
5881
|
+
frozen: true,
|
|
5882
|
+
note: "base is frozen: nothing was rebaselined"
|
|
5883
|
+
} : {}
|
|
5884
|
+
};
|
|
5885
|
+
},
|
|
5886
|
+
render: (result) => renderReassess(result)
|
|
5887
|
+
});
|
|
5888
|
+
function renderReassess(result) {
|
|
5889
|
+
const lines = [];
|
|
5890
|
+
for (const move of result.rebaselined) {
|
|
5891
|
+
lines.push(
|
|
5892
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
5893
|
+
);
|
|
5894
|
+
}
|
|
5895
|
+
if (result.cosmetic) {
|
|
5896
|
+
lines.push(
|
|
5897
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
5898
|
+
);
|
|
5899
|
+
}
|
|
5900
|
+
if (result.note) lines.push(result.note);
|
|
5901
|
+
const packet = result.packet;
|
|
5902
|
+
if (!packet) {
|
|
5903
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
5904
|
+
return lines.join("\n");
|
|
5905
|
+
}
|
|
5906
|
+
lines.push(
|
|
5907
|
+
"",
|
|
5908
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
5909
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
5910
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
5911
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
5912
|
+
"",
|
|
5913
|
+
`## Anchors (${packet.anchors.length})`
|
|
5914
|
+
);
|
|
5915
|
+
for (const anchor of packet.anchors) {
|
|
5916
|
+
lines.push(
|
|
5917
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
5918
|
+
);
|
|
5919
|
+
if (!anchor.diff) continue;
|
|
5920
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
5921
|
+
lines.push(
|
|
5922
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
5923
|
+
);
|
|
5924
|
+
continue;
|
|
5925
|
+
}
|
|
5926
|
+
lines.push(
|
|
5927
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
5928
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
if (packet.impact.length) {
|
|
5932
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
5933
|
+
for (const entry of packet.impact) {
|
|
5934
|
+
lines.push(
|
|
5935
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
5936
|
+
);
|
|
5937
|
+
}
|
|
5938
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
5939
|
+
}
|
|
5940
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
5941
|
+
return lines.join("\n");
|
|
5942
|
+
}
|
|
5943
|
+
function at(file, symbol) {
|
|
5944
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
5945
|
+
}
|
|
5946
|
+
|
|
5947
|
+
// src/commands/doctor.ts
|
|
5948
|
+
var days = (what, fallback) => import_zod15.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5185
5949
|
var doctorCommand = define({
|
|
5186
5950
|
name: "doctor",
|
|
5187
5951
|
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:
|
|
5952
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5953
|
+
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.",
|
|
5954
|
+
input: import_zod15.z.object({
|
|
5191
5955
|
bundlePath,
|
|
5192
5956
|
repoRoot: REPO_ROOT,
|
|
5193
5957
|
expiringDays: days(
|
|
@@ -5202,11 +5966,17 @@ var doctorCommand = define({
|
|
|
5202
5966
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5203
5967
|
DEFAULT_AGING_DAYS
|
|
5204
5968
|
),
|
|
5205
|
-
offline:
|
|
5969
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
5206
5970
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5207
5971
|
),
|
|
5208
|
-
strict:
|
|
5972
|
+
strict: import_zod15.z.boolean().optional().describe(
|
|
5209
5973
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5974
|
+
),
|
|
5975
|
+
drifted: import_zod15.z.boolean().optional().describe(
|
|
5976
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5977
|
+
),
|
|
5978
|
+
withDiff: import_zod15.z.boolean().optional().describe(
|
|
5979
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5210
5980
|
)
|
|
5211
5981
|
}),
|
|
5212
5982
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -5225,7 +5995,9 @@ var doctorCommand = define({
|
|
|
5225
5995
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
5226
5996
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
5227
5997
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
5228
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
5998
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
5999
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
6000
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
5229
6001
|
};
|
|
5230
6002
|
},
|
|
5231
6003
|
run: async ({ store, now }, {
|
|
@@ -5234,7 +6006,9 @@ var doctorCommand = define({
|
|
|
5234
6006
|
unverifiedDays,
|
|
5235
6007
|
agingDays,
|
|
5236
6008
|
repoRoot,
|
|
5237
|
-
offline
|
|
6009
|
+
offline,
|
|
6010
|
+
drifted: drifted2,
|
|
6011
|
+
withDiff
|
|
5238
6012
|
}) => {
|
|
5239
6013
|
const checkedAt = now();
|
|
5240
6014
|
const records = await store.list(path);
|
|
@@ -5249,10 +6023,51 @@ var doctorCommand = define({
|
|
|
5249
6023
|
now: new Date(checkedAt)
|
|
5250
6024
|
});
|
|
5251
6025
|
const hints = grammarHints();
|
|
6026
|
+
if (!drifted2) {
|
|
6027
|
+
return {
|
|
6028
|
+
bundlePath: path,
|
|
6029
|
+
checkedAt,
|
|
6030
|
+
...report,
|
|
6031
|
+
...hints.length ? { hints } : {}
|
|
6032
|
+
};
|
|
6033
|
+
}
|
|
6034
|
+
const standings = new Map(
|
|
6035
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
6036
|
+
hit.record.conceptId,
|
|
6037
|
+
hit.standing
|
|
6038
|
+
])
|
|
6039
|
+
);
|
|
6040
|
+
const packets = [];
|
|
6041
|
+
const rebaselinable = [];
|
|
6042
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
6043
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
6044
|
+
const record = records.find(
|
|
6045
|
+
(entry) => entry.conceptId === found.conceptId
|
|
6046
|
+
);
|
|
6047
|
+
if (!record) continue;
|
|
6048
|
+
const standing = standings.get(record.conceptId);
|
|
6049
|
+
const built = await reassessPacket(
|
|
6050
|
+
repoRoot ?? process.cwd(),
|
|
6051
|
+
record,
|
|
6052
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
6053
|
+
{
|
|
6054
|
+
...withDiff ? { withDiff: true } : {},
|
|
6055
|
+
impact: await store.impact(path, record.conceptId),
|
|
6056
|
+
...standing ? { standing } : {},
|
|
6057
|
+
search
|
|
6058
|
+
}
|
|
6059
|
+
);
|
|
6060
|
+
if (built.packet) packets.push(built.packet);
|
|
6061
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
6062
|
+
rebaselinable.push(record.conceptId);
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
5252
6065
|
return {
|
|
5253
6066
|
bundlePath: path,
|
|
5254
6067
|
checkedAt,
|
|
5255
6068
|
...report,
|
|
6069
|
+
packets,
|
|
6070
|
+
rebaselinable,
|
|
5256
6071
|
...hints.length ? { hints } : {}
|
|
5257
6072
|
};
|
|
5258
6073
|
},
|
|
@@ -5266,6 +6081,7 @@ var doctorCommand = define({
|
|
|
5266
6081
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
5267
6082
|
});
|
|
5268
6083
|
function render2(result) {
|
|
6084
|
+
if (result.packets) return renderPackets(result);
|
|
5269
6085
|
const { thresholds } = result;
|
|
5270
6086
|
const lines = [
|
|
5271
6087
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -5299,21 +6115,45 @@ function render2(result) {
|
|
|
5299
6115
|
);
|
|
5300
6116
|
return lines.join("\n");
|
|
5301
6117
|
}
|
|
6118
|
+
function renderPackets(result) {
|
|
6119
|
+
const packets = result.packets ?? [];
|
|
6120
|
+
const lines = [
|
|
6121
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
6122
|
+
`checked: ${result.checkedAt}`,
|
|
6123
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
6124
|
+
];
|
|
6125
|
+
if (result.rebaselinable?.length) {
|
|
6126
|
+
lines.push(
|
|
6127
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
6128
|
+
);
|
|
6129
|
+
}
|
|
6130
|
+
for (const packet of packets) {
|
|
6131
|
+
lines.push(
|
|
6132
|
+
renderReassess({
|
|
6133
|
+
conceptId: packet.conceptId,
|
|
6134
|
+
packet,
|
|
6135
|
+
rebaselined: [],
|
|
6136
|
+
cosmetic: 0
|
|
6137
|
+
})
|
|
6138
|
+
);
|
|
6139
|
+
}
|
|
6140
|
+
return lines.join("\n");
|
|
6141
|
+
}
|
|
5302
6142
|
|
|
5303
6143
|
// src/commands/impact.ts
|
|
5304
|
-
var
|
|
6144
|
+
var import_zod16 = require("zod");
|
|
5305
6145
|
var impactCommand = define({
|
|
5306
6146
|
name: "impact",
|
|
5307
6147
|
tool: "kb_impact",
|
|
5308
6148
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
5309
6149
|
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:
|
|
6150
|
+
input: import_zod16.z.object({
|
|
5311
6151
|
bundlePath,
|
|
5312
6152
|
conceptId,
|
|
5313
|
-
depth:
|
|
6153
|
+
depth: import_zod16.z.number().int().positive().optional().describe(
|
|
5314
6154
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
5315
6155
|
),
|
|
5316
|
-
rels:
|
|
6156
|
+
rels: import_zod16.z.array(import_zod16.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
5317
6157
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
5318
6158
|
)
|
|
5319
6159
|
}),
|
|
@@ -5334,35 +6174,49 @@ var impactCommand = define({
|
|
|
5334
6174
|
});
|
|
5335
6175
|
|
|
5336
6176
|
// src/commands/list.ts
|
|
5337
|
-
var
|
|
6177
|
+
var import_zod17 = require("zod");
|
|
5338
6178
|
var listCommand = define({
|
|
5339
6179
|
name: "list",
|
|
5340
6180
|
tool: "kb_list",
|
|
5341
|
-
usage: "list [type]",
|
|
5342
|
-
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
5343
|
-
input:
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
6181
|
+
usage: "list [type] [--tag T]...",
|
|
6182
|
+
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
6183
|
+
input: import_zod17.z.object({
|
|
6184
|
+
bundlePath,
|
|
6185
|
+
type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
|
|
6186
|
+
tags: TAGS
|
|
6187
|
+
}),
|
|
6188
|
+
fromArgv: (argv, path) => {
|
|
6189
|
+
const tags = argvFlags(argv, "--tag");
|
|
6190
|
+
const type = argvPositional(argv, "--tag");
|
|
6191
|
+
return {
|
|
6192
|
+
bundlePath: path,
|
|
6193
|
+
...type ? { type } : {},
|
|
6194
|
+
...tags.length ? { tags } : {}
|
|
6195
|
+
};
|
|
6196
|
+
},
|
|
6197
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
|
|
6198
|
+
(record) => ({
|
|
6199
|
+
conceptId: record.conceptId,
|
|
6200
|
+
title: record.frontmatter.title ?? null,
|
|
6201
|
+
description: record.frontmatter.description ?? null,
|
|
6202
|
+
status: record.frontmatter.strauss_status,
|
|
6203
|
+
anchors: record.frontmatter.strauss_anchors ?? []
|
|
6204
|
+
})
|
|
6205
|
+
)
|
|
5352
6206
|
});
|
|
5353
6207
|
|
|
5354
6208
|
// src/commands/load.ts
|
|
5355
|
-
var
|
|
6209
|
+
var import_zod18 = require("zod");
|
|
5356
6210
|
var loadCommand = define({
|
|
5357
6211
|
name: "load",
|
|
5358
6212
|
tool: "kb_load",
|
|
5359
6213
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
5360
6214
|
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:
|
|
6215
|
+
input: import_zod18.z.object({
|
|
5362
6216
|
bundlePath,
|
|
5363
|
-
type:
|
|
5364
|
-
budgetTokens:
|
|
5365
|
-
all:
|
|
6217
|
+
type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
|
|
6218
|
+
budgetTokens: import_zod18.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6219
|
+
all: import_zod18.z.boolean().optional().describe(
|
|
5366
6220
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
5367
6221
|
),
|
|
5368
6222
|
repoRoot: REPO_ROOT
|
|
@@ -5404,25 +6258,25 @@ var loadCommand = define({
|
|
|
5404
6258
|
});
|
|
5405
6259
|
|
|
5406
6260
|
// src/commands/log.ts
|
|
5407
|
-
var
|
|
6261
|
+
var import_zod19 = require("zod");
|
|
5408
6262
|
var logCommand = define({
|
|
5409
6263
|
name: "log",
|
|
5410
6264
|
tool: "kb_log",
|
|
5411
6265
|
usage: "log",
|
|
5412
6266
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
5413
|
-
input:
|
|
6267
|
+
input: import_zod19.z.object({ bundlePath }),
|
|
5414
6268
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5415
6269
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
5416
6270
|
});
|
|
5417
6271
|
|
|
5418
6272
|
// src/commands/no-decision.ts
|
|
5419
|
-
var
|
|
6273
|
+
var import_zod20 = require("zod");
|
|
5420
6274
|
var noDecisionCommand = define({
|
|
5421
6275
|
name: "no-decision",
|
|
5422
6276
|
tool: "kb_no_decision",
|
|
5423
6277
|
usage: "no-decision <reason...>",
|
|
5424
6278
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
5425
|
-
input:
|
|
6279
|
+
input: import_zod20.z.object({ bundlePath, reason: import_zod20.z.string().min(1) }),
|
|
5426
6280
|
fromArgv: (argv, path) => ({
|
|
5427
6281
|
bundlePath: path,
|
|
5428
6282
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -5439,20 +6293,20 @@ var noDecisionCommand = define({
|
|
|
5439
6293
|
});
|
|
5440
6294
|
|
|
5441
6295
|
// src/commands/pack.ts
|
|
5442
|
-
var
|
|
6296
|
+
var import_zod21 = require("zod");
|
|
5443
6297
|
var packCommand = define({
|
|
5444
6298
|
name: "pack",
|
|
5445
6299
|
tool: "kb_pack",
|
|
5446
6300
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
5447
6301
|
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:
|
|
6302
|
+
input: import_zod21.z.object({
|
|
5449
6303
|
bundlePath,
|
|
5450
6304
|
conceptId,
|
|
5451
|
-
hops:
|
|
5452
|
-
maxNodes:
|
|
6305
|
+
hops: import_zod21.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6306
|
+
maxNodes: import_zod21.z.number().int().positive().optional().describe(
|
|
5453
6307
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
5454
6308
|
),
|
|
5455
|
-
budgetTokens:
|
|
6309
|
+
budgetTokens: import_zod21.z.number().int().positive().optional().describe(
|
|
5456
6310
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
5457
6311
|
)
|
|
5458
6312
|
}),
|
|
@@ -5477,12 +6331,12 @@ var packCommand = define({
|
|
|
5477
6331
|
return render3(result, path, now());
|
|
5478
6332
|
}
|
|
5479
6333
|
});
|
|
5480
|
-
function render3(result, bundle,
|
|
6334
|
+
function render3(result, bundle, at2) {
|
|
5481
6335
|
const lines = [
|
|
5482
6336
|
`# KB Pack \u2014 ${result.root}`,
|
|
5483
6337
|
`bundle: ${bundle}`,
|
|
5484
6338
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
5485
|
-
`packed: ${
|
|
6339
|
+
`packed: ${at2}`,
|
|
5486
6340
|
"",
|
|
5487
6341
|
`## Records (${result.records.length})`
|
|
5488
6342
|
];
|
|
@@ -5539,22 +6393,22 @@ function warningLabel(warning) {
|
|
|
5539
6393
|
}
|
|
5540
6394
|
|
|
5541
6395
|
// src/commands/pin.ts
|
|
5542
|
-
var
|
|
6396
|
+
var import_zod22 = require("zod");
|
|
5543
6397
|
var pinCommand = define({
|
|
5544
6398
|
name: "pin",
|
|
5545
6399
|
tool: "kb_pin",
|
|
5546
6400
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
5547
6401
|
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:
|
|
6402
|
+
input: import_zod22.z.object({
|
|
5549
6403
|
bundlePath,
|
|
5550
|
-
mode:
|
|
6404
|
+
mode: import_zod22.z.enum(["full", "index"]).optional().describe(
|
|
5551
6405
|
"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
6406
|
),
|
|
5553
|
-
profiles:
|
|
5554
|
-
layer:
|
|
6407
|
+
profiles: import_zod22.z.array(import_zod22.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6408
|
+
layer: import_zod22.z.enum(["project", "local", "user"]).optional().describe(
|
|
5555
6409
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
5556
6410
|
),
|
|
5557
|
-
frozen:
|
|
6411
|
+
frozen: import_zod22.z.boolean().optional().describe(
|
|
5558
6412
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
5559
6413
|
)
|
|
5560
6414
|
}),
|
|
@@ -5583,47 +6437,49 @@ var pinCommand = define({
|
|
|
5583
6437
|
});
|
|
5584
6438
|
|
|
5585
6439
|
// src/commands/pins.ts
|
|
5586
|
-
var
|
|
6440
|
+
var import_zod23 = require("zod");
|
|
5587
6441
|
var pinsCommand = define({
|
|
5588
6442
|
name: "pins",
|
|
5589
6443
|
tool: "kb_pins",
|
|
5590
6444
|
usage: "pins",
|
|
5591
6445
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
5592
|
-
input:
|
|
6446
|
+
input: import_zod23.z.object({}),
|
|
5593
6447
|
fromArgv: () => ({}),
|
|
5594
6448
|
run: ({ store }) => listPins(store, process.cwd())
|
|
5595
6449
|
});
|
|
5596
6450
|
|
|
5597
6451
|
// src/commands/query.ts
|
|
5598
|
-
var
|
|
6452
|
+
var import_zod24 = require("zod");
|
|
5599
6453
|
var queryCommand = define({
|
|
5600
6454
|
name: "query",
|
|
5601
6455
|
tool: "kb_query",
|
|
5602
|
-
usage: "query <text...> [--repo-root PATH]",
|
|
6456
|
+
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
5603
6457
|
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:
|
|
6458
|
+
input: import_zod24.z.object({
|
|
5605
6459
|
bundlePath,
|
|
5606
|
-
text:
|
|
5607
|
-
type:
|
|
5608
|
-
includeNonCurrent:
|
|
6460
|
+
text: import_zod24.z.string().optional(),
|
|
6461
|
+
type: import_zod24.z.enum(KB_RECORD_TYPES).optional(),
|
|
6462
|
+
includeNonCurrent: import_zod24.z.boolean().optional(),
|
|
6463
|
+
tags: TAGS,
|
|
5609
6464
|
repoRoot: REPO_ROOT
|
|
5610
6465
|
}),
|
|
5611
|
-
//
|
|
6466
|
+
// Both are flags, so neither's value may fall into the search text.
|
|
5612
6467
|
fromArgv: (argv, path) => {
|
|
5613
6468
|
const repoRoot = argvFlag(argv, "--repo-root");
|
|
5614
|
-
const
|
|
5615
|
-
const
|
|
5616
|
-
if (flag !== -1) words.splice(flag, 2);
|
|
6469
|
+
const tags = argvFlags(argv, "--tag");
|
|
6470
|
+
const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
|
|
5617
6471
|
return {
|
|
5618
6472
|
bundlePath: path,
|
|
5619
6473
|
text: words.join(" ").trim(),
|
|
5620
6474
|
includeNonCurrent: true,
|
|
6475
|
+
...tags.length ? { tags } : {},
|
|
5621
6476
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
5622
6477
|
};
|
|
5623
6478
|
},
|
|
5624
|
-
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
6479
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
|
|
5625
6480
|
...type ? { type } : {},
|
|
5626
6481
|
includeNonCurrent: includeNonCurrent === true,
|
|
6482
|
+
...tags ? { tags } : {},
|
|
5627
6483
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
5628
6484
|
})).map((hit) => ({
|
|
5629
6485
|
conceptId: hit.record.conceptId,
|
|
@@ -5637,43 +6493,43 @@ var queryCommand = define({
|
|
|
5637
6493
|
});
|
|
5638
6494
|
|
|
5639
6495
|
// src/commands/read-index.ts
|
|
5640
|
-
var
|
|
6496
|
+
var import_zod25 = require("zod");
|
|
5641
6497
|
var readIndexCommand = define({
|
|
5642
6498
|
name: "index",
|
|
5643
6499
|
tool: "kb_index",
|
|
5644
6500
|
usage: "index",
|
|
5645
6501
|
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:
|
|
6502
|
+
input: import_zod25.z.object({ bundlePath }),
|
|
5647
6503
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5648
6504
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
5649
6505
|
});
|
|
5650
6506
|
|
|
5651
6507
|
// src/commands/schema.ts
|
|
5652
|
-
var
|
|
6508
|
+
var import_zod26 = require("zod");
|
|
5653
6509
|
var schemaCommand = define({
|
|
5654
6510
|
name: "schema",
|
|
5655
6511
|
tool: "kb_schema",
|
|
5656
6512
|
usage: "schema",
|
|
5657
6513
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
5658
|
-
input:
|
|
6514
|
+
input: import_zod26.z.object({}),
|
|
5659
6515
|
fromArgv: () => ({}),
|
|
5660
6516
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
5661
6517
|
});
|
|
5662
6518
|
|
|
5663
6519
|
// src/commands/stamp.ts
|
|
5664
|
-
var
|
|
5665
|
-
var
|
|
6520
|
+
var import_promises10 = require("fs/promises");
|
|
6521
|
+
var import_zod27 = require("zod");
|
|
5666
6522
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
5667
6523
|
var stampCommand = define({
|
|
5668
6524
|
name: "stamp",
|
|
5669
6525
|
tool: "kb_stamp",
|
|
5670
6526
|
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:
|
|
6527
|
+
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.",
|
|
6528
|
+
input: import_zod27.z.object({
|
|
6529
|
+
bundlePath: import_zod27.z.string().min(1).optional().describe(
|
|
5674
6530
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5675
6531
|
),
|
|
5676
|
-
since:
|
|
6532
|
+
since: import_zod27.z.string().min(1).optional().describe(
|
|
5677
6533
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5678
6534
|
)
|
|
5679
6535
|
}),
|
|
@@ -5712,7 +6568,7 @@ var stampCommand = define({
|
|
|
5712
6568
|
return reports;
|
|
5713
6569
|
},
|
|
5714
6570
|
render: (result) => result.map((report) => {
|
|
5715
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
6571
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
5716
6572
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
5717
6573
|
return report.changed?.length ? `${head}
|
|
5718
6574
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -5735,7 +6591,7 @@ async function readBaseline(since) {
|
|
|
5735
6591
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5736
6592
|
let parsed;
|
|
5737
6593
|
try {
|
|
5738
|
-
parsed = JSON.parse(await (0,
|
|
6594
|
+
parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
|
|
5739
6595
|
} catch {
|
|
5740
6596
|
throw new KbStampBaselineError(since);
|
|
5741
6597
|
}
|
|
@@ -5759,16 +6615,16 @@ async function readBaseline(since) {
|
|
|
5759
6615
|
}
|
|
5760
6616
|
|
|
5761
6617
|
// src/commands/status.ts
|
|
5762
|
-
var
|
|
6618
|
+
var import_zod28 = require("zod");
|
|
5763
6619
|
var statusCommand = define({
|
|
5764
6620
|
name: "status",
|
|
5765
6621
|
tool: "kb_status",
|
|
5766
6622
|
usage: "status <concept-id> <status>",
|
|
5767
6623
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
5768
|
-
input:
|
|
6624
|
+
input: import_zod28.z.object({
|
|
5769
6625
|
bundlePath,
|
|
5770
6626
|
conceptId,
|
|
5771
|
-
status:
|
|
6627
|
+
status: import_zod28.z.enum(KB_RECORD_STATUSES)
|
|
5772
6628
|
}),
|
|
5773
6629
|
fromArgv: (argv, path) => ({
|
|
5774
6630
|
bundlePath: path,
|
|
@@ -5783,13 +6639,13 @@ var statusCommand = define({
|
|
|
5783
6639
|
});
|
|
5784
6640
|
|
|
5785
6641
|
// src/commands/supersede.ts
|
|
5786
|
-
var
|
|
6642
|
+
var import_zod29 = require("zod");
|
|
5787
6643
|
var supersedeCommand = define({
|
|
5788
6644
|
name: "supersede",
|
|
5789
6645
|
tool: "kb_supersede",
|
|
5790
6646
|
usage: "supersede <concept-id> <replacement-id>",
|
|
5791
6647
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
5792
|
-
input:
|
|
6648
|
+
input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
5793
6649
|
fromArgv: (argv, path) => ({
|
|
5794
6650
|
bundlePath: path,
|
|
5795
6651
|
conceptId: argv[1],
|
|
@@ -5803,16 +6659,16 @@ var supersedeCommand = define({
|
|
|
5803
6659
|
});
|
|
5804
6660
|
|
|
5805
6661
|
// src/commands/sync-instructions.ts
|
|
5806
|
-
var
|
|
6662
|
+
var import_zod30 = require("zod");
|
|
5807
6663
|
var syncInstructionsCommand = define({
|
|
5808
6664
|
name: "sync-instructions",
|
|
5809
6665
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
5810
6666
|
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:
|
|
6667
|
+
input: import_zod30.z.object({
|
|
6668
|
+
file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
|
|
6669
|
+
budgetTokens: import_zod30.z.number().int().positive().optional(),
|
|
6670
|
+
fullUnderTokens: import_zod30.z.number().int().positive().optional(),
|
|
6671
|
+
profile: import_zod30.z.string().optional()
|
|
5816
6672
|
}),
|
|
5817
6673
|
fromArgv: (argv) => {
|
|
5818
6674
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5838,17 +6694,17 @@ var syncInstructionsCommand = define({
|
|
|
5838
6694
|
});
|
|
5839
6695
|
|
|
5840
6696
|
// src/commands/trace.ts
|
|
5841
|
-
var
|
|
6697
|
+
var import_zod31 = require("zod");
|
|
5842
6698
|
var traceCommand = define({
|
|
5843
6699
|
name: "trace",
|
|
5844
6700
|
tool: "kb_trace",
|
|
5845
6701
|
usage: "trace <concept-id> [edges...]",
|
|
5846
6702
|
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:
|
|
6703
|
+
input: import_zod31.z.object({
|
|
5848
6704
|
bundlePath,
|
|
5849
6705
|
conceptId,
|
|
5850
|
-
edges:
|
|
5851
|
-
depth:
|
|
6706
|
+
edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
|
|
6707
|
+
depth: import_zod31.z.number().int().positive().optional()
|
|
5852
6708
|
}),
|
|
5853
6709
|
fromArgv: (argv, path) => ({
|
|
5854
6710
|
bundlePath: path,
|
|
@@ -5870,37 +6726,37 @@ var traceCommand = define({
|
|
|
5870
6726
|
});
|
|
5871
6727
|
|
|
5872
6728
|
// src/commands/types.ts
|
|
5873
|
-
var
|
|
6729
|
+
var import_zod32 = require("zod");
|
|
5874
6730
|
var typesCommand = define({
|
|
5875
6731
|
name: "types",
|
|
5876
6732
|
tool: "kb_types",
|
|
5877
6733
|
usage: "types",
|
|
5878
6734
|
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:
|
|
6735
|
+
input: import_zod32.z.object({}),
|
|
5880
6736
|
fromArgv: () => ({}),
|
|
5881
6737
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5882
6738
|
});
|
|
5883
6739
|
|
|
5884
6740
|
// src/commands/unpin.ts
|
|
5885
|
-
var
|
|
6741
|
+
var import_zod33 = require("zod");
|
|
5886
6742
|
var unpinCommand = define({
|
|
5887
6743
|
name: "unpin",
|
|
5888
6744
|
tool: "kb_unpin",
|
|
5889
6745
|
usage: "unpin [bundle-path]",
|
|
5890
6746
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5891
|
-
input:
|
|
6747
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
5892
6748
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5893
6749
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5894
6750
|
});
|
|
5895
6751
|
|
|
5896
6752
|
// src/commands/validate.ts
|
|
5897
|
-
var
|
|
6753
|
+
var import_zod34 = require("zod");
|
|
5898
6754
|
var validateCommand = define({
|
|
5899
6755
|
name: "validate",
|
|
5900
6756
|
tool: "kb_validate",
|
|
5901
6757
|
usage: "validate",
|
|
5902
6758
|
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:
|
|
6759
|
+
input: import_zod34.z.object({ bundlePath }),
|
|
5904
6760
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5905
6761
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5906
6762
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5910,16 +6766,16 @@ var validateCommand = define({
|
|
|
5910
6766
|
});
|
|
5911
6767
|
|
|
5912
6768
|
// src/commands/verify.ts
|
|
5913
|
-
var
|
|
6769
|
+
var import_zod35 = require("zod");
|
|
5914
6770
|
var verifyCommand = define({
|
|
5915
6771
|
name: "verify",
|
|
5916
6772
|
tool: "kb_verify",
|
|
5917
6773
|
usage: "verify <concept-id> --note <text>",
|
|
5918
6774
|
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:
|
|
6775
|
+
input: import_zod35.z.object({
|
|
5920
6776
|
bundlePath,
|
|
5921
6777
|
conceptId,
|
|
5922
|
-
note:
|
|
6778
|
+
note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
|
|
5923
6779
|
message: "note must say what the check found"
|
|
5924
6780
|
})
|
|
5925
6781
|
}),
|
|
@@ -5939,15 +6795,15 @@ var verifyCommand = define({
|
|
|
5939
6795
|
});
|
|
5940
6796
|
|
|
5941
6797
|
// src/commands/write.ts
|
|
5942
|
-
var
|
|
6798
|
+
var import_zod36 = require("zod");
|
|
5943
6799
|
var writeCommand = define({
|
|
5944
6800
|
name: "write",
|
|
5945
6801
|
tool: "kb_write",
|
|
5946
6802
|
usage: "write <type> < record.json",
|
|
5947
6803
|
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:
|
|
6804
|
+
input: import_zod36.z.object({
|
|
5949
6805
|
bundlePath,
|
|
5950
|
-
type:
|
|
6806
|
+
type: import_zod36.z.enum(KB_RECORD_TYPES),
|
|
5951
6807
|
input: composeInputSchema
|
|
5952
6808
|
}),
|
|
5953
6809
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5971,13 +6827,13 @@ var writeCommand = define({
|
|
|
5971
6827
|
});
|
|
5972
6828
|
|
|
5973
6829
|
// src/commands/write-decision.ts
|
|
5974
|
-
var
|
|
6830
|
+
var import_zod37 = require("zod");
|
|
5975
6831
|
var writeDecisionCommand = define({
|
|
5976
6832
|
name: "write-decision",
|
|
5977
6833
|
tool: "kb_write_decision",
|
|
5978
6834
|
usage: "write-decision < decision.json",
|
|
5979
6835
|
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:
|
|
6836
|
+
input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5981
6837
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5982
6838
|
bundlePath: path,
|
|
5983
6839
|
input: JSON.parse(await stdin())
|
|
@@ -6007,6 +6863,7 @@ var KB_COMMANDS = [
|
|
|
6007
6863
|
answerCommand,
|
|
6008
6864
|
verifyCommand,
|
|
6009
6865
|
anchorResolveCommand,
|
|
6866
|
+
reassessCommand,
|
|
6010
6867
|
loadCommand,
|
|
6011
6868
|
catalogCommand,
|
|
6012
6869
|
packCommand,
|
|
@@ -6037,7 +6894,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
6037
6894
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6038
6895
|
|
|
6039
6896
|
// src/version.ts
|
|
6040
|
-
var VERSION = true ? "0.1.
|
|
6897
|
+
var VERSION = true ? "0.1.19" : "0.0.0-dev";
|
|
6041
6898
|
|
|
6042
6899
|
// src/mcp.ts
|
|
6043
6900
|
function createKbMcpServer() {
|
|
@@ -6131,21 +6988,21 @@ async function runKbCli(argv) {
|
|
|
6131
6988
|
`);
|
|
6132
6989
|
}
|
|
6133
6990
|
function takeLiteral(argv) {
|
|
6134
|
-
const
|
|
6135
|
-
if (
|
|
6136
|
-
return { flags: argv.slice(0,
|
|
6991
|
+
const at2 = argv.indexOf("--");
|
|
6992
|
+
if (at2 === -1) return { flags: argv, literal: [] };
|
|
6993
|
+
return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
|
|
6137
6994
|
}
|
|
6138
6995
|
function takeBundle(argv) {
|
|
6139
|
-
const
|
|
6140
|
-
if (
|
|
6996
|
+
const at2 = argv.indexOf("--bundle");
|
|
6997
|
+
if (at2 === -1) {
|
|
6141
6998
|
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
6142
6999
|
}
|
|
6143
|
-
const bundle = argv[
|
|
7000
|
+
const bundle = argv[at2 + 1];
|
|
6144
7001
|
if (!bundle) die("--bundle requires a path");
|
|
6145
7002
|
return {
|
|
6146
7003
|
bundle,
|
|
6147
7004
|
explicit: true,
|
|
6148
|
-
rest: [...argv.slice(0,
|
|
7005
|
+
rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
|
|
6149
7006
|
};
|
|
6150
7007
|
}
|
|
6151
7008
|
function readStdin() {
|
|
@@ -6243,6 +7100,7 @@ function usage() {
|
|
|
6243
7100
|
backlinks,
|
|
6244
7101
|
buildContext,
|
|
6245
7102
|
catalog,
|
|
7103
|
+
classifyDrift,
|
|
6246
7104
|
composeDecisionRecord,
|
|
6247
7105
|
composeInputSchema,
|
|
6248
7106
|
composeLinkSchema,
|
|
@@ -6280,6 +7138,7 @@ function usage() {
|
|
|
6280
7138
|
listPins,
|
|
6281
7139
|
loadQmd,
|
|
6282
7140
|
matchToDiff,
|
|
7141
|
+
matchesTags,
|
|
6283
7142
|
mergedContextBudgets,
|
|
6284
7143
|
neighbours,
|
|
6285
7144
|
pack,
|
|
@@ -6290,6 +7149,7 @@ function usage() {
|
|
|
6290
7149
|
readMergedPins,
|
|
6291
7150
|
readPinsLayer,
|
|
6292
7151
|
readRemoteAnchors,
|
|
7152
|
+
reassessPacket,
|
|
6293
7153
|
regexResolver,
|
|
6294
7154
|
renderCatalogLine,
|
|
6295
7155
|
renderIndex,
|
|
@@ -6311,6 +7171,7 @@ function usage() {
|
|
|
6311
7171
|
toHookJson,
|
|
6312
7172
|
trace,
|
|
6313
7173
|
treeSitterLanguages,
|
|
7174
|
+
unifiedDiff,
|
|
6314
7175
|
unpinBase,
|
|
6315
7176
|
validateBundle
|
|
6316
7177
|
});
|