@saasontools/strauss-kb 0.1.10 → 0.1.12
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 +17 -0
- package/README.md +96 -27
- package/dist/{chunk-EJQPZWN5.js → chunk-33ZCBEUV.js} +1183 -375
- package/dist/chunk-33ZCBEUV.js.map +1 -0
- package/dist/{chunk-NMTP7V7E.js → chunk-EXKK2KUN.js} +2 -2
- package/dist/{chunk-RGK3K6LN.js → chunk-F2U2YLWV.js} +2 -2
- package/dist/cli-main.cjs +1195 -395
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +991 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -10
- package/dist/index.d.ts +204 -10
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1190 -390
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-EJQPZWN5.js.map +0 -1
- /package/dist/{chunk-NMTP7V7E.js.map → chunk-EXKK2KUN.js.map} +0 -0
- /package/dist/{chunk-RGK3K6LN.js.map → chunk-F2U2YLWV.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -74,6 +74,7 @@ __export(index_exports, {
|
|
|
74
74
|
SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
|
|
75
75
|
TRACE_EDGES: () => TRACE_EDGES,
|
|
76
76
|
adjudicate: () => adjudicate,
|
|
77
|
+
anchorFilePath: () => anchorFilePath,
|
|
77
78
|
assertBaseNotFrozen: () => assertBaseNotFrozen,
|
|
78
79
|
buildContext: () => buildContext,
|
|
79
80
|
catalog: () => catalog,
|
|
@@ -84,8 +85,10 @@ __export(index_exports, {
|
|
|
84
85
|
contextProfileBudgets: () => contextProfileBudgets,
|
|
85
86
|
createKbMcpServer: () => createKbMcpServer,
|
|
86
87
|
decisionInputSchema: () => decisionInputSchema,
|
|
88
|
+
detectAnchorDrift: () => detectAnchorDrift,
|
|
87
89
|
doctor: () => doctor,
|
|
88
90
|
edgeNeighbours: () => edgeNeighbours,
|
|
91
|
+
hashAnchorText: () => hashAnchorText,
|
|
89
92
|
indexIsStale: () => indexIsStale,
|
|
90
93
|
isKbRecordType: () => isKbRecordType,
|
|
91
94
|
isNoDecisionRecord: () => isNoDecisionRecord,
|
|
@@ -108,10 +111,12 @@ __export(index_exports, {
|
|
|
108
111
|
pinBase: () => pinBase,
|
|
109
112
|
readMergedPins: () => readMergedPins,
|
|
110
113
|
readPinsLayer: () => readPinsLayer,
|
|
114
|
+
regexResolver: () => regexResolver,
|
|
111
115
|
renderCatalogLine: () => renderCatalogLine,
|
|
112
116
|
renderIndex: () => renderIndex,
|
|
113
117
|
renderIndexLine: () => renderIndexLine,
|
|
114
118
|
renderLogEntry: () => renderLogEntry,
|
|
119
|
+
resolveAnchor: () => resolveAnchor,
|
|
115
120
|
resolveHeads: () => resolveHeads,
|
|
116
121
|
resolveHits: () => resolveHits,
|
|
117
122
|
resolvePinPath: () => resolvePinPath,
|
|
@@ -130,9 +135,38 @@ __export(index_exports, {
|
|
|
130
135
|
module.exports = __toCommonJS(index_exports);
|
|
131
136
|
|
|
132
137
|
// src/kb-store.ts
|
|
133
|
-
var
|
|
134
|
-
var
|
|
135
|
-
var
|
|
138
|
+
var import_node_crypto2 = require("crypto");
|
|
139
|
+
var import_promises3 = require("fs/promises");
|
|
140
|
+
var import_node_path3 = require("path");
|
|
141
|
+
|
|
142
|
+
// src/concurrency.ts
|
|
143
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
144
|
+
async function mapLimit(items, limit, fn) {
|
|
145
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
146
|
+
throw new RangeError(
|
|
147
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const out = new Array(items.length);
|
|
151
|
+
let next = 0;
|
|
152
|
+
let failed = false;
|
|
153
|
+
const runners = Array.from(
|
|
154
|
+
{ length: Math.min(limit, items.length) },
|
|
155
|
+
async () => {
|
|
156
|
+
while (!failed && next < items.length) {
|
|
157
|
+
const at = next++;
|
|
158
|
+
try {
|
|
159
|
+
out[at] = await fn(items[at], at);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
failed = true;
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
await Promise.all(runners);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
136
170
|
|
|
137
171
|
// src/markdown.ts
|
|
138
172
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -179,7 +213,31 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
179
213
|
});
|
|
180
214
|
var kbAnchorSchema = import_zod.z.object({
|
|
181
215
|
file: import_zod.z.string().min(1),
|
|
182
|
-
symbol: import_zod.z.string().min(1).optional()
|
|
216
|
+
symbol: import_zod.z.string().min(1).optional(),
|
|
217
|
+
/**
|
|
218
|
+
* Which repository the file lives in — a remote URL
|
|
219
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
220
|
+
* own repository, which is what nearly every anchor means.
|
|
221
|
+
*
|
|
222
|
+
* Unvalidated beyond not-blank: one repository has many spellings.
|
|
223
|
+
* Matched after normalisation; see ARCHITECTURE.
|
|
224
|
+
*/
|
|
225
|
+
repo: import_zod.z.string().trim().min(1).optional(),
|
|
226
|
+
/**
|
|
227
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
228
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
229
|
+
* came from wherever that branch happens to be now, which is not a
|
|
230
|
+
* baseline. Recorded and preserved in v1; ref-pinned reads land with
|
|
231
|
+
* SAA-709.
|
|
232
|
+
*/
|
|
233
|
+
ref: import_zod.z.string().trim().min(1).optional(),
|
|
234
|
+
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
235
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
236
|
+
}).optional(),
|
|
237
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
238
|
+
resolved_at: import_zod.z.string().min(1).optional(),
|
|
239
|
+
/** Line count of the text the hash was taken over. */
|
|
240
|
+
lines: import_zod.z.number().int().positive().optional()
|
|
183
241
|
}).strict();
|
|
184
242
|
var KB_RECORD_TYPES = [
|
|
185
243
|
"fact",
|
|
@@ -432,7 +490,7 @@ var STANDING = {
|
|
|
432
490
|
rejected: "rejected",
|
|
433
491
|
superseded: "superseded"
|
|
434
492
|
};
|
|
435
|
-
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
493
|
+
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift) {
|
|
436
494
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
437
495
|
return hits.map((record) => {
|
|
438
496
|
const status = record.frontmatter.strauss_status;
|
|
@@ -462,6 +520,20 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
|
462
520
|
if (!record.frontmatter.verified?.length) {
|
|
463
521
|
warnings.push({ kind: "unverified" });
|
|
464
522
|
}
|
|
523
|
+
const moved = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
524
|
+
(entry) => entry.state !== "match" && entry.reason !== "foreign-repo"
|
|
525
|
+
);
|
|
526
|
+
if (moved.length) {
|
|
527
|
+
warnings.push({
|
|
528
|
+
kind: "drifted",
|
|
529
|
+
anchors: moved.map(({ file, symbol, diffSize, reason }) => ({
|
|
530
|
+
file,
|
|
531
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
532
|
+
diffSize,
|
|
533
|
+
...reason !== void 0 ? { reason } : {}
|
|
534
|
+
}))
|
|
535
|
+
});
|
|
536
|
+
}
|
|
465
537
|
return { record, standing: STANDING[status], heads, warnings };
|
|
466
538
|
});
|
|
467
539
|
}
|
|
@@ -514,9 +586,430 @@ function successors(record, byId) {
|
|
|
514
586
|
return { records, missing };
|
|
515
587
|
}
|
|
516
588
|
|
|
517
|
-
// src/
|
|
589
|
+
// src/anchor-resolver.ts
|
|
590
|
+
var import_node_child_process = require("child_process");
|
|
591
|
+
var import_node_crypto = require("crypto");
|
|
518
592
|
var import_promises = require("fs/promises");
|
|
519
593
|
var import_node_path = require("path");
|
|
594
|
+
var import_node_util = require("util");
|
|
595
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
596
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
597
|
+
var PARENT_SCOPE_LINES = 50;
|
|
598
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
599
|
+
function stripLine(line, state) {
|
|
600
|
+
let out = "";
|
|
601
|
+
let index = 0;
|
|
602
|
+
let { blockComment, template } = state;
|
|
603
|
+
while (index < line.length) {
|
|
604
|
+
const char = line[index];
|
|
605
|
+
const next = line[index + 1];
|
|
606
|
+
if (blockComment) {
|
|
607
|
+
if (char === "*" && next === "/") {
|
|
608
|
+
blockComment = false;
|
|
609
|
+
index += 2;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
index += 1;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (template) {
|
|
616
|
+
if (char === "\\") {
|
|
617
|
+
index += 2;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (char === "`") template = false;
|
|
621
|
+
index += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (char === "/" && next === "*") {
|
|
625
|
+
blockComment = true;
|
|
626
|
+
index += 2;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (char === "/" && next === "/") break;
|
|
630
|
+
if (char === "`") {
|
|
631
|
+
template = true;
|
|
632
|
+
index += 1;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (char === "'" || char === '"') {
|
|
636
|
+
const quote = char;
|
|
637
|
+
index += 1;
|
|
638
|
+
while (index < line.length) {
|
|
639
|
+
if (line[index] === "\\") {
|
|
640
|
+
index += 2;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (line[index] === quote) {
|
|
644
|
+
index += 1;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
index += 1;
|
|
648
|
+
}
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
out += char;
|
|
652
|
+
index += 1;
|
|
653
|
+
}
|
|
654
|
+
return { code: out, state: { blockComment, template } };
|
|
655
|
+
}
|
|
656
|
+
function span(lines, from, to) {
|
|
657
|
+
return {
|
|
658
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
659
|
+
startLine: from + 1,
|
|
660
|
+
endLine: to + 1
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function captureBraceBlock(lines, matchLine) {
|
|
664
|
+
let depth = 0;
|
|
665
|
+
let opened = false;
|
|
666
|
+
let state = CLEAN_STATE;
|
|
667
|
+
for (let index = matchLine; index < lines.length; index++) {
|
|
668
|
+
const stripped = stripLine(lines[index] ?? "", state);
|
|
669
|
+
state = stripped.state;
|
|
670
|
+
for (const char of stripped.code) {
|
|
671
|
+
if (char === "{") {
|
|
672
|
+
depth += 1;
|
|
673
|
+
opened = true;
|
|
674
|
+
} else if (char === "}") {
|
|
675
|
+
depth = Math.max(0, depth - 1);
|
|
676
|
+
} else if (char === ";" && !opened) {
|
|
677
|
+
return span(lines, matchLine, index);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
685
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
686
|
+
const header = lines[matchLine] ?? "";
|
|
687
|
+
const indent = header.length - header.trimStart().length;
|
|
688
|
+
let headerEnd = -1;
|
|
689
|
+
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
690
|
+
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
691
|
+
if (code.endsWith(":")) {
|
|
692
|
+
headerEnd = index;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
if (code.includes(":")) return span(lines, matchLine, index);
|
|
696
|
+
}
|
|
697
|
+
if (headerEnd === -1) return null;
|
|
698
|
+
let end = headerEnd;
|
|
699
|
+
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
700
|
+
const line = lines[index] ?? "";
|
|
701
|
+
if (line.trim() === "") continue;
|
|
702
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
703
|
+
if (lineIndent <= indent) break;
|
|
704
|
+
end = index;
|
|
705
|
+
}
|
|
706
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
707
|
+
}
|
|
708
|
+
var TIERS = [
|
|
709
|
+
(name) => new RegExp(
|
|
710
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
711
|
+
),
|
|
712
|
+
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
713
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
714
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
715
|
+
];
|
|
716
|
+
var regexResolver = {
|
|
717
|
+
name: "regex",
|
|
718
|
+
resolve(source, symbol) {
|
|
719
|
+
const segments = symbol.split(".");
|
|
720
|
+
const name = segments[segments.length - 1];
|
|
721
|
+
if (!name) return null;
|
|
722
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
723
|
+
const escaped = escapeRegExp(name);
|
|
724
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
725
|
+
const lines = source.split("\n");
|
|
726
|
+
for (const tier of TIERS) {
|
|
727
|
+
const pattern = tier(escaped);
|
|
728
|
+
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
729
|
+
if (!candidates.length) continue;
|
|
730
|
+
if (parentPattern && candidates.length > 1) {
|
|
731
|
+
const distances = candidates.map(
|
|
732
|
+
(index) => distanceToParent(lines, index, parentPattern)
|
|
733
|
+
);
|
|
734
|
+
const nearest = Math.min(...distances);
|
|
735
|
+
if (Number.isFinite(nearest)) {
|
|
736
|
+
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (candidates.length !== 1) return null;
|
|
740
|
+
const matchLine = candidates[0];
|
|
741
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function escapeRegExp(value) {
|
|
747
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
748
|
+
}
|
|
749
|
+
function distanceToParent(lines, index, parent) {
|
|
750
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
751
|
+
for (let at = index; at >= floor; at--) {
|
|
752
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
753
|
+
}
|
|
754
|
+
return Number.POSITIVE_INFINITY;
|
|
755
|
+
}
|
|
756
|
+
function hashAnchorText(text) {
|
|
757
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
758
|
+
}
|
|
759
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
760
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
761
|
+
if (!anchor.symbol) {
|
|
762
|
+
const lines = normalized.split("\n");
|
|
763
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
764
|
+
return {
|
|
765
|
+
text: normalized,
|
|
766
|
+
startLine: 1,
|
|
767
|
+
endLine: Math.max(1, lines.length)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
771
|
+
}
|
|
772
|
+
function anchorFilePath(repoRoot, file) {
|
|
773
|
+
const path = (0, import_node_path.resolve)(repoRoot, file.replace(/^\.\//, ""));
|
|
774
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(repoRoot), path);
|
|
775
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(rel)) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
return path;
|
|
779
|
+
}
|
|
780
|
+
function contains(root, path) {
|
|
781
|
+
const rel = (0, import_node_path.relative)(root, path);
|
|
782
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`) && !(0, import_node_path.isAbsolute)(rel);
|
|
783
|
+
}
|
|
784
|
+
function normalizeRepoUrl(value) {
|
|
785
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
786
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
787
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
788
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
789
|
+
url = trimTrailingSlashes(url);
|
|
790
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
791
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
792
|
+
}
|
|
793
|
+
function trimTrailingSlashes(value) {
|
|
794
|
+
let end = value.length;
|
|
795
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
796
|
+
return value.slice(0, end);
|
|
797
|
+
}
|
|
798
|
+
function repoPath(normalized) {
|
|
799
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
800
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
801
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
802
|
+
}
|
|
803
|
+
function repoIdentifies(declared, originUrl) {
|
|
804
|
+
if (!originUrl) return false;
|
|
805
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
806
|
+
const want = normalizeRepoUrl(declared);
|
|
807
|
+
if (!want || !origin) return false;
|
|
808
|
+
if (want === origin) return true;
|
|
809
|
+
const path = repoPath(origin);
|
|
810
|
+
if (!path) return false;
|
|
811
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
812
|
+
}
|
|
813
|
+
async function repoOriginUrl(repoRoot) {
|
|
814
|
+
try {
|
|
815
|
+
const { stdout } = await execFileAsync(
|
|
816
|
+
"git",
|
|
817
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
818
|
+
{ timeout: 5e3 }
|
|
819
|
+
);
|
|
820
|
+
return stdout.trim() || null;
|
|
821
|
+
} catch {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
var LazyOrigin = class {
|
|
826
|
+
constructor(repoRoot) {
|
|
827
|
+
this.repoRoot = repoRoot;
|
|
828
|
+
}
|
|
829
|
+
repoRoot;
|
|
830
|
+
url = null;
|
|
831
|
+
asked = false;
|
|
832
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
833
|
+
async prime() {
|
|
834
|
+
if (this.asked) return;
|
|
835
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
836
|
+
this.asked = true;
|
|
837
|
+
}
|
|
838
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
839
|
+
isForeign(anchor) {
|
|
840
|
+
if (!anchor.repo) return false;
|
|
841
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
842
|
+
}
|
|
843
|
+
async foreign(anchor) {
|
|
844
|
+
if (!anchor.repo) return false;
|
|
845
|
+
await this.prime();
|
|
846
|
+
return this.isForeign(anchor);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function errorCode(error) {
|
|
850
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
851
|
+
}
|
|
852
|
+
function anchorFileReader(repoRoot) {
|
|
853
|
+
let rootOnce;
|
|
854
|
+
const realRoot = () => {
|
|
855
|
+
rootOnce ??= (0, import_promises.realpath)((0, import_node_path.resolve)(repoRoot)).catch((error) => {
|
|
856
|
+
rootOnce = void 0;
|
|
857
|
+
throw error;
|
|
858
|
+
});
|
|
859
|
+
return rootOnce;
|
|
860
|
+
};
|
|
861
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
862
|
+
}
|
|
863
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
864
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
865
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
866
|
+
let root;
|
|
867
|
+
let path;
|
|
868
|
+
try {
|
|
869
|
+
root = await realRoot();
|
|
870
|
+
path = await (0, import_promises.realpath)(lexical);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
const code = errorCode(error);
|
|
873
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
874
|
+
return { ok: false, reason: "file-missing" };
|
|
875
|
+
}
|
|
876
|
+
return { ok: false, reason: "file-unreadable" };
|
|
877
|
+
}
|
|
878
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
879
|
+
try {
|
|
880
|
+
const stats = await (0, import_promises.stat)(path);
|
|
881
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
882
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
883
|
+
return { ok: false, reason: "file-too-large" };
|
|
884
|
+
}
|
|
885
|
+
return { ok: true, source: await (0, import_promises.readFile)(path, "utf8") };
|
|
886
|
+
} catch (error) {
|
|
887
|
+
const code = errorCode(error);
|
|
888
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
889
|
+
return { ok: false, reason: "file-missing" };
|
|
890
|
+
}
|
|
891
|
+
return { ok: false, reason: "file-unreadable" };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
895
|
+
let checked = 0;
|
|
896
|
+
for (const entries of drift.values()) {
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
if (entry.reason === "foreign-repo") continue;
|
|
899
|
+
checked += 1;
|
|
900
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return checked > 0;
|
|
906
|
+
}
|
|
907
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
908
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
909
|
+
throw new RangeError(
|
|
910
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const wanted = [...new Set(files)];
|
|
914
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
915
|
+
try {
|
|
916
|
+
return await read(file);
|
|
917
|
+
} catch {
|
|
918
|
+
return { ok: false, reason: "file-unreadable" };
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
922
|
+
}
|
|
923
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
924
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
925
|
+
const resolver = options.resolver ?? regexResolver;
|
|
926
|
+
const origin = new LazyOrigin(repoRoot);
|
|
927
|
+
const planned = /* @__PURE__ */ new Map();
|
|
928
|
+
let declaresRepo = false;
|
|
929
|
+
for (const record of records) {
|
|
930
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
931
|
+
(anchor) => anchor.hash
|
|
932
|
+
);
|
|
933
|
+
if (!anchors.length) continue;
|
|
934
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
935
|
+
planned.set(
|
|
936
|
+
record.conceptId,
|
|
937
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (declaresRepo) {
|
|
941
|
+
await origin.prime();
|
|
942
|
+
for (const entries of planned.values()) {
|
|
943
|
+
for (const entry of entries)
|
|
944
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const files = [];
|
|
948
|
+
for (const entries of planned.values()) {
|
|
949
|
+
for (const entry of entries) {
|
|
950
|
+
if (!entry.foreign) files.push(entry.anchor.file);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const reads = await readAnchorFiles(
|
|
954
|
+
files,
|
|
955
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
956
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
957
|
+
);
|
|
958
|
+
const drift = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const record of records) {
|
|
960
|
+
const entries = [];
|
|
961
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
962
|
+
const base = {
|
|
963
|
+
file: anchor.file,
|
|
964
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
965
|
+
storedHash: anchor.hash
|
|
966
|
+
};
|
|
967
|
+
if (foreign) {
|
|
968
|
+
entries.push({
|
|
969
|
+
...base,
|
|
970
|
+
state: "unresolved",
|
|
971
|
+
diffSize: null,
|
|
972
|
+
reason: "foreign-repo"
|
|
973
|
+
});
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const read = reads.get(anchor.file);
|
|
977
|
+
if (!read.ok) {
|
|
978
|
+
entries.push({
|
|
979
|
+
...base,
|
|
980
|
+
state: "unresolved",
|
|
981
|
+
diffSize: null,
|
|
982
|
+
reason: read.reason
|
|
983
|
+
});
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
987
|
+
if (!resolved) {
|
|
988
|
+
entries.push({
|
|
989
|
+
...base,
|
|
990
|
+
state: "unresolved",
|
|
991
|
+
diffSize: null,
|
|
992
|
+
reason: "symbol-not-found"
|
|
993
|
+
});
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
997
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
998
|
+
entries.push({
|
|
999
|
+
...base,
|
|
1000
|
+
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
1001
|
+
currentHash,
|
|
1002
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
1006
|
+
}
|
|
1007
|
+
return drift;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/search-index.ts
|
|
1011
|
+
var import_promises2 = require("fs/promises");
|
|
1012
|
+
var import_node_path2 = require("path");
|
|
520
1013
|
|
|
521
1014
|
// src/kb-log.ts
|
|
522
1015
|
var import_zod2 = require("zod");
|
|
@@ -579,7 +1072,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
579
1072
|
let store = null;
|
|
580
1073
|
try {
|
|
581
1074
|
store = await qmd.createStore({
|
|
582
|
-
dbPath: (0,
|
|
1075
|
+
dbPath: (0, import_node_path2.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
583
1076
|
config: {
|
|
584
1077
|
collections: {
|
|
585
1078
|
[COLLECTION]: {
|
|
@@ -614,16 +1107,19 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
614
1107
|
}
|
|
615
1108
|
}
|
|
616
1109
|
async function isStale(bundlePath2) {
|
|
617
|
-
const indexAt = await (0,
|
|
1110
|
+
const indexAt = await (0, import_promises2.stat)((0, import_node_path2.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
618
1111
|
if (!indexAt) return true;
|
|
619
1112
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
620
|
-
const names = await readdir2(bundlePath2).catch(() => [])
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1113
|
+
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
1114
|
+
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
1115
|
+
);
|
|
1116
|
+
let stale = false;
|
|
1117
|
+
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
1118
|
+
if (stale) return;
|
|
1119
|
+
const at = await (0, import_promises2.stat)((0, import_node_path2.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1120
|
+
if (at > indexAt) stale = true;
|
|
1121
|
+
});
|
|
1122
|
+
return stale;
|
|
627
1123
|
}
|
|
628
1124
|
function resolveHits(hits, records) {
|
|
629
1125
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -923,7 +1419,7 @@ function appendUnionMergeLine(contents) {
|
|
|
923
1419
|
}
|
|
924
1420
|
|
|
925
1421
|
// src/kb-store.ts
|
|
926
|
-
var KB_DIR = (0,
|
|
1422
|
+
var KB_DIR = (0, import_node_path3.join)(".strauss", "kb");
|
|
927
1423
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
928
1424
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
929
1425
|
var KbStore = class {
|
|
@@ -954,7 +1450,7 @@ var KbStore = class {
|
|
|
954
1450
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
955
1451
|
const root = this.root(bundlePath2);
|
|
956
1452
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
957
|
-
await (0,
|
|
1453
|
+
await (0, import_promises3.mkdir)(root, { recursive: true });
|
|
958
1454
|
await this.publish(
|
|
959
1455
|
target,
|
|
960
1456
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -993,7 +1489,7 @@ var KbStore = class {
|
|
|
993
1489
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
994
1490
|
let raw;
|
|
995
1491
|
try {
|
|
996
|
-
raw = await (0,
|
|
1492
|
+
raw = await (0, import_promises3.readFile)(target, "utf8");
|
|
997
1493
|
} catch {
|
|
998
1494
|
return null;
|
|
999
1495
|
}
|
|
@@ -1010,15 +1506,15 @@ var KbStore = class {
|
|
|
1010
1506
|
const root = this.root(bundlePath2);
|
|
1011
1507
|
let names;
|
|
1012
1508
|
try {
|
|
1013
|
-
names = await (0,
|
|
1509
|
+
names = await (0, import_promises3.readdir)(root);
|
|
1014
1510
|
} catch {
|
|
1015
1511
|
return [];
|
|
1016
1512
|
}
|
|
1017
1513
|
const wanted = names.sort().filter((name) => name.endsWith(".md") && !STORE_OWNED.has(name)).map((name) => ({ name, conceptId: name.slice(0, -".md".length) })).filter(({ conceptId: conceptId2 }) => !type || conceptId2.startsWith(`${type}.`));
|
|
1018
|
-
const records = await
|
|
1019
|
-
wanted
|
|
1020
|
-
|
|
1021
|
-
)
|
|
1514
|
+
const records = await mapLimit(
|
|
1515
|
+
wanted,
|
|
1516
|
+
DEFAULT_IO_CONCURRENCY,
|
|
1517
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises3.readFile)((0, import_node_path3.join)(root, name), "utf8"))
|
|
1022
1518
|
);
|
|
1023
1519
|
return records.filter((record) => record !== null);
|
|
1024
1520
|
}
|
|
@@ -1040,6 +1536,21 @@ var KbStore = class {
|
|
|
1040
1536
|
{ operation: `status:${status}`, by: actor }
|
|
1041
1537
|
);
|
|
1042
1538
|
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Replaces a record's anchors wholesale, preserving everything else.
|
|
1541
|
+
*
|
|
1542
|
+
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
1543
|
+
* writing, so it holds the complete current set, and a merge would keep
|
|
1544
|
+
* stale entries the resolution pass deliberately dropped.
|
|
1545
|
+
*/
|
|
1546
|
+
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
1547
|
+
return this.mutate(
|
|
1548
|
+
bundlePath2,
|
|
1549
|
+
conceptId2,
|
|
1550
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: anchors }),
|
|
1551
|
+
{ operation: "anchor-resolve", by: actor }
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1043
1554
|
/**
|
|
1044
1555
|
* Appends one `verified[]` event: who checked the record, when, and what the
|
|
1045
1556
|
* check found. Append-only — prior events are history, and are spread into
|
|
@@ -1138,9 +1649,12 @@ ${answer}
|
|
|
1138
1649
|
const bundle = await this.list(bundlePath2);
|
|
1139
1650
|
const needle = text.trim();
|
|
1140
1651
|
const hits = needle ? await this.rank(bundlePath2, needle, bundle) : bundle;
|
|
1652
|
+
const narrowed = options.type ? hits.filter((r) => r.frontmatter.type === options.type) : hits;
|
|
1141
1653
|
const adjudicated = adjudicate(
|
|
1142
|
-
|
|
1143
|
-
bundle
|
|
1654
|
+
narrowed,
|
|
1655
|
+
bundle,
|
|
1656
|
+
/* @__PURE__ */ new Date(),
|
|
1657
|
+
await this.detectDrift(narrowed, options.repoRoot)
|
|
1144
1658
|
);
|
|
1145
1659
|
if (options.includeNonCurrent) return adjudicated;
|
|
1146
1660
|
const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
|
|
@@ -1159,6 +1673,50 @@ ${answer}
|
|
|
1159
1673
|
const lowered = needle.toLowerCase();
|
|
1160
1674
|
return bundle.filter((record) => matches(record, lowered));
|
|
1161
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Anchor drift over the records about to be handed back. Like the search
|
|
1678
|
+
* index, this is an enrichment: a filesystem failure degrades to "no drift
|
|
1679
|
+
* reported" rather than failing the read. Anchors without a stored hash are
|
|
1680
|
+
* skipped inside `detectAnchorDrift`, so a base nobody has stamped pays no
|
|
1681
|
+
* fs cost here. `repoRoot` defaults to the working directory — the CLI runs
|
|
1682
|
+
* at the repo root, and the MCP server's cwd is the workspace.
|
|
1683
|
+
*
|
|
1684
|
+
* Public because `doctor` needs the same map with the same degradation: a
|
|
1685
|
+
* sweep that failed to read the tree should report no drift, not fail.
|
|
1686
|
+
*
|
|
1687
|
+
* When no root was given and not one anchored file was found, the finding is
|
|
1688
|
+
* discarded. A base read from somewhere other than the tree it describes
|
|
1689
|
+
* misses every file at once, and that shape is far likelier to be a wrong
|
|
1690
|
+
* default root than a repository where every anchored file was deleted on
|
|
1691
|
+
* the same day. Reporting it would put a drift warning on every record in
|
|
1692
|
+
* the base, which teaches a reader to ignore the warning — the one outcome
|
|
1693
|
+
* worse than not having it. One file found anywhere makes the root
|
|
1694
|
+
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
1695
|
+
* taken at its word either way.
|
|
1696
|
+
*/
|
|
1697
|
+
async detectDrift(records, repoRoot) {
|
|
1698
|
+
try {
|
|
1699
|
+
const drift = await detectAnchorDrift(records, {
|
|
1700
|
+
repoRoot: repoRoot ?? process.cwd()
|
|
1701
|
+
});
|
|
1702
|
+
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
1703
|
+
this.logger.warn?.({
|
|
1704
|
+
operation: "kb.anchor-drift",
|
|
1705
|
+
outcome: "skipped",
|
|
1706
|
+
reason: "no anchored file found under the default repo root"
|
|
1707
|
+
});
|
|
1708
|
+
return void 0;
|
|
1709
|
+
}
|
|
1710
|
+
return drift;
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
this.logger.warn?.({
|
|
1713
|
+
operation: "kb.anchor-drift",
|
|
1714
|
+
outcome: "skipped",
|
|
1715
|
+
error: error instanceof Error ? error.message : "unknown"
|
|
1716
|
+
});
|
|
1717
|
+
return void 0;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1162
1720
|
/**
|
|
1163
1721
|
* The whole base, adjudicated, when it is small enough to hand over.
|
|
1164
1722
|
*
|
|
@@ -1193,10 +1751,16 @@ ${answer}
|
|
|
1193
1751
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
1194
1752
|
const bundle = await this.list(bundlePath2);
|
|
1195
1753
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
1196
|
-
const adjudicated = adjudicate(
|
|
1754
|
+
const adjudicated = adjudicate(
|
|
1755
|
+
wanted,
|
|
1756
|
+
bundle,
|
|
1757
|
+
/* @__PURE__ */ new Date(),
|
|
1758
|
+
await this.detectDrift(wanted, options.repoRoot)
|
|
1759
|
+
);
|
|
1197
1760
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
1198
1761
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
1199
1762
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
1763
|
+
const bundleDigestValue = bundleDigest(records, superseded);
|
|
1200
1764
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
1201
1765
|
return {
|
|
1202
1766
|
loaded: false,
|
|
@@ -1207,7 +1771,8 @@ ${answer}
|
|
|
1207
1771
|
approxTokens: approxTokens2,
|
|
1208
1772
|
budgetTokens,
|
|
1209
1773
|
type: options.type
|
|
1210
|
-
})
|
|
1774
|
+
}),
|
|
1775
|
+
digest: bundleDigestValue
|
|
1211
1776
|
};
|
|
1212
1777
|
}
|
|
1213
1778
|
return {
|
|
@@ -1216,7 +1781,8 @@ ${answer}
|
|
|
1216
1781
|
tokensLoaded: approxTokens2,
|
|
1217
1782
|
budgetTokens: options.all ? null : budgetTokens,
|
|
1218
1783
|
records,
|
|
1219
|
-
superseded
|
|
1784
|
+
superseded,
|
|
1785
|
+
digest: bundleDigestValue
|
|
1220
1786
|
};
|
|
1221
1787
|
}
|
|
1222
1788
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -1241,11 +1807,11 @@ ${answer}
|
|
|
1241
1807
|
async readIndex(bundlePath2) {
|
|
1242
1808
|
const root = this.root(bundlePath2);
|
|
1243
1809
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
1244
|
-
const stored = await (0,
|
|
1810
|
+
const stored = await (0, import_promises3.readFile)((0, import_node_path3.join)(root, INDEX_FILE), "utf8").catch(
|
|
1245
1811
|
() => null
|
|
1246
1812
|
);
|
|
1247
1813
|
if (indexIsStale(stored, expected)) {
|
|
1248
|
-
await this.publish((0,
|
|
1814
|
+
await this.publish((0, import_node_path3.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
1249
1815
|
this.logger.info?.({
|
|
1250
1816
|
operation: "kb.index.repair",
|
|
1251
1817
|
bundlePath: root,
|
|
@@ -1262,8 +1828,8 @@ ${answer}
|
|
|
1262
1828
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
1263
1829
|
*/
|
|
1264
1830
|
async readLog(bundlePath2) {
|
|
1265
|
-
const raw = await (0,
|
|
1266
|
-
(0,
|
|
1831
|
+
const raw = await (0, import_promises3.readFile)(
|
|
1832
|
+
(0, import_node_path3.join)(this.root(bundlePath2), LOG_FILE),
|
|
1267
1833
|
"utf8"
|
|
1268
1834
|
).catch(() => "");
|
|
1269
1835
|
const result = parseLog(raw);
|
|
@@ -1314,14 +1880,14 @@ ${answer}
|
|
|
1314
1880
|
}
|
|
1315
1881
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
1316
1882
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
1317
|
-
const before = await (0,
|
|
1883
|
+
const before = await (0, import_promises3.readFile)(target, "utf8").catch(() => null);
|
|
1318
1884
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
1319
1885
|
const parsed = this.parse(conceptId2, before);
|
|
1320
1886
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
1321
1887
|
const frontmatter = change(parsed.frontmatter);
|
|
1322
1888
|
const body = changeBody(parsed.body);
|
|
1323
1889
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
1324
|
-
const witness = await (0,
|
|
1890
|
+
const witness = await (0, import_promises3.readFile)(target, "utf8").catch(() => null);
|
|
1325
1891
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
1326
1892
|
throw new KbWriteConflictError(conceptId2);
|
|
1327
1893
|
}
|
|
@@ -1347,20 +1913,20 @@ ${answer}
|
|
|
1347
1913
|
*/
|
|
1348
1914
|
async publish(target, contents, overwrite, conceptId2) {
|
|
1349
1915
|
const staging = `${target}.${process.pid}.tmp`;
|
|
1350
|
-
await (0,
|
|
1916
|
+
await (0, import_promises3.writeFile)(staging, contents, "utf8");
|
|
1351
1917
|
try {
|
|
1352
1918
|
if (overwrite) {
|
|
1353
|
-
await (0,
|
|
1919
|
+
await (0, import_promises3.rename)(staging, target);
|
|
1354
1920
|
return;
|
|
1355
1921
|
}
|
|
1356
|
-
await (0,
|
|
1922
|
+
await (0, import_promises3.link)(staging, target);
|
|
1357
1923
|
} catch (error) {
|
|
1358
1924
|
if (error.code === "EEXIST") {
|
|
1359
1925
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
1360
1926
|
}
|
|
1361
1927
|
throw error;
|
|
1362
1928
|
} finally {
|
|
1363
|
-
await (0,
|
|
1929
|
+
await (0, import_promises3.unlink)(staging).catch(() => void 0);
|
|
1364
1930
|
}
|
|
1365
1931
|
}
|
|
1366
1932
|
/**
|
|
@@ -1404,20 +1970,30 @@ ${answer}
|
|
|
1404
1970
|
* file must not fail the mutation it guards.
|
|
1405
1971
|
*/
|
|
1406
1972
|
async ensureGitattributes(root) {
|
|
1407
|
-
const target = (0,
|
|
1973
|
+
const target = (0, import_node_path3.join)(root, GITATTRIBUTES_FILE);
|
|
1408
1974
|
try {
|
|
1409
1975
|
let existing;
|
|
1410
1976
|
try {
|
|
1411
|
-
existing = await (0,
|
|
1977
|
+
existing = await (0, import_promises3.readFile)(target, "utf8");
|
|
1412
1978
|
} catch (error) {
|
|
1413
1979
|
if (error.code !== "ENOENT") throw error;
|
|
1414
1980
|
existing = null;
|
|
1415
1981
|
}
|
|
1416
1982
|
if (existing === null) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1983
|
+
try {
|
|
1984
|
+
await (0, import_promises3.writeFile)(target, appendUnionMergeLine(""), {
|
|
1985
|
+
encoding: "utf8",
|
|
1986
|
+
flag: "wx"
|
|
1987
|
+
});
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
if (error.code !== "EEXIST") throw error;
|
|
1990
|
+
this.logger.info?.({
|
|
1991
|
+
operation: "kb.gitattributes.ensure",
|
|
1992
|
+
bundlePath: root,
|
|
1993
|
+
outcome: "exists"
|
|
1994
|
+
});
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1421
1997
|
this.logger.info?.({
|
|
1422
1998
|
operation: "kb.gitattributes.ensure",
|
|
1423
1999
|
bundlePath: root,
|
|
@@ -1426,7 +2002,7 @@ ${answer}
|
|
|
1426
2002
|
return;
|
|
1427
2003
|
}
|
|
1428
2004
|
if (!hasMergeDeclaration(existing)) {
|
|
1429
|
-
await (0,
|
|
2005
|
+
await (0, import_promises3.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
1430
2006
|
this.logger.info?.({
|
|
1431
2007
|
operation: "kb.gitattributes.ensure",
|
|
1432
2008
|
bundlePath: root,
|
|
@@ -1445,7 +2021,7 @@ ${answer}
|
|
|
1445
2021
|
async record(root, entry) {
|
|
1446
2022
|
await this.ensureGitattributes(root);
|
|
1447
2023
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
1448
|
-
await (0,
|
|
2024
|
+
await (0, import_promises3.appendFile)((0, import_node_path3.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
1449
2025
|
this.logger.warn?.({
|
|
1450
2026
|
operation: "kb.log.append",
|
|
1451
2027
|
outcome: "failed",
|
|
@@ -1471,18 +2047,18 @@ ${answer}
|
|
|
1471
2047
|
};
|
|
1472
2048
|
}
|
|
1473
2049
|
root(bundlePath2) {
|
|
1474
|
-
return (0,
|
|
2050
|
+
return (0, import_node_path3.resolve)(bundlePath2);
|
|
1475
2051
|
}
|
|
1476
2052
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
1477
2053
|
// bundle root; anything carrying a separator would escape it.
|
|
1478
2054
|
recordPath(bundlePath2, conceptId2) {
|
|
1479
|
-
if (conceptId2.includes(
|
|
2055
|
+
if (conceptId2.includes(import_node_path3.sep) || conceptId2.includes("/")) {
|
|
1480
2056
|
throw new KbInvalidConceptIdError(
|
|
1481
2057
|
"concept id must not contain a path separator",
|
|
1482
2058
|
{ conceptId: conceptId2 }
|
|
1483
2059
|
);
|
|
1484
2060
|
}
|
|
1485
|
-
return (0,
|
|
2061
|
+
return (0, import_node_path3.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
1486
2062
|
}
|
|
1487
2063
|
};
|
|
1488
2064
|
function estimateTokens(record) {
|
|
@@ -1521,7 +2097,23 @@ function normalizeActor(id) {
|
|
|
1521
2097
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
1522
2098
|
}
|
|
1523
2099
|
function digest(contents) {
|
|
1524
|
-
return (0,
|
|
2100
|
+
return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
|
|
2101
|
+
}
|
|
2102
|
+
function bundleDigest(records, superseded) {
|
|
2103
|
+
const entries = [
|
|
2104
|
+
...records.map(
|
|
2105
|
+
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
2106
|
+
stringifyMarkdownWithFrontmatter(
|
|
2107
|
+
hit.record.body,
|
|
2108
|
+
hit.record.frontmatter
|
|
2109
|
+
)
|
|
2110
|
+
)}`
|
|
2111
|
+
),
|
|
2112
|
+
...superseded.map(
|
|
2113
|
+
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
2114
|
+
)
|
|
2115
|
+
].sort();
|
|
2116
|
+
return digest(entries.join("\n"));
|
|
1525
2117
|
}
|
|
1526
2118
|
|
|
1527
2119
|
// src/record-types.ts
|
|
@@ -1732,18 +2324,18 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1732
2324
|
};
|
|
1733
2325
|
|
|
1734
2326
|
// src/kb-pins/frozen.ts
|
|
1735
|
-
var
|
|
2327
|
+
var import_node_path6 = require("path");
|
|
1736
2328
|
|
|
1737
2329
|
// src/kb-pins/layers.ts
|
|
1738
|
-
var
|
|
2330
|
+
var import_promises4 = require("fs/promises");
|
|
1739
2331
|
var import_node_os = require("os");
|
|
1740
|
-
var
|
|
2332
|
+
var import_node_path5 = require("path");
|
|
1741
2333
|
|
|
1742
2334
|
// src/kb-pins/model.ts
|
|
1743
|
-
var
|
|
2335
|
+
var import_node_path4 = require("path");
|
|
1744
2336
|
var import_zod4 = require("zod");
|
|
1745
|
-
var PINS_FILE = (0,
|
|
1746
|
-
var PINS_LOCAL_FILE = (0,
|
|
2337
|
+
var PINS_FILE = (0, import_node_path4.join)(".strauss", "kb-pins.json");
|
|
2338
|
+
var PINS_LOCAL_FILE = (0, import_node_path4.join)(".strauss", "kb-pins.local.json");
|
|
1747
2339
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1748
2340
|
var pinSchema = import_zod4.z.object({
|
|
1749
2341
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -1793,10 +2385,10 @@ function userRoot() {
|
|
|
1793
2385
|
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os.homedir)();
|
|
1794
2386
|
}
|
|
1795
2387
|
function layerRoot(workspaceDir, layer) {
|
|
1796
|
-
return layer === "user" ? userRoot() : (0,
|
|
2388
|
+
return layer === "user" ? userRoot() : (0, import_node_path5.resolve)(workspaceDir);
|
|
1797
2389
|
}
|
|
1798
2390
|
function layerFile(workspaceDir, layer) {
|
|
1799
|
-
return (0,
|
|
2391
|
+
return (0, import_node_path5.join)(
|
|
1800
2392
|
layerRoot(workspaceDir, layer),
|
|
1801
2393
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1802
2394
|
);
|
|
@@ -1805,7 +2397,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1805
2397
|
const file = layerFile(workspaceDir, layer);
|
|
1806
2398
|
let raw;
|
|
1807
2399
|
try {
|
|
1808
|
-
raw = await (0,
|
|
2400
|
+
raw = await (0, import_promises4.readFile)(file, "utf8");
|
|
1809
2401
|
} catch {
|
|
1810
2402
|
return { pins: [] };
|
|
1811
2403
|
}
|
|
@@ -1829,16 +2421,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1829
2421
|
}
|
|
1830
2422
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1831
2423
|
const file = layerFile(workspaceDir, layer);
|
|
1832
|
-
await (0,
|
|
1833
|
-
await (0,
|
|
2424
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(file), { recursive: true });
|
|
2425
|
+
await (0, import_promises4.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
1834
2426
|
`, "utf8");
|
|
1835
2427
|
}
|
|
1836
2428
|
function resolvePinPath(rootDir, path) {
|
|
1837
|
-
return (0,
|
|
2429
|
+
return (0, import_node_path5.isAbsolute)(path) ? (0, import_node_path5.resolve)(path) : (0, import_node_path5.resolve)(rootDir, path.split("/").join(import_node_path5.sep));
|
|
1838
2430
|
}
|
|
1839
2431
|
function storablePath(rootDir, bundlePath2) {
|
|
1840
|
-
const rel = (0,
|
|
1841
|
-
return (rel === "" ? "." : rel).split(
|
|
2432
|
+
const rel = (0, import_node_path5.relative)((0, import_node_path5.resolve)(rootDir), (0, import_node_path5.resolve)(bundlePath2));
|
|
2433
|
+
return (rel === "" ? "." : rel).split(import_node_path5.sep).join("/");
|
|
1842
2434
|
}
|
|
1843
2435
|
async function readMergedPins(workspaceDir) {
|
|
1844
2436
|
const manifests = {};
|
|
@@ -1866,7 +2458,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
1866
2458
|
// src/kb-pins/frozen.ts
|
|
1867
2459
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
1868
2460
|
const merged = await readMergedPins(workspaceDir);
|
|
1869
|
-
const absolute = (0,
|
|
2461
|
+
const absolute = (0, import_node_path6.resolve)(bundlePath2);
|
|
1870
2462
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
1871
2463
|
if (pin?.frozen === true) {
|
|
1872
2464
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -1951,7 +2543,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1951
2543
|
}
|
|
1952
2544
|
|
|
1953
2545
|
// src/kb-pins/unpin.ts
|
|
1954
|
-
var
|
|
2546
|
+
var import_node_path7 = require("path");
|
|
1955
2547
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1956
2548
|
const layers = [];
|
|
1957
2549
|
for (const layer of PIN_LAYERS) {
|
|
@@ -1972,14 +2564,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
1972
2564
|
}
|
|
1973
2565
|
}
|
|
1974
2566
|
return {
|
|
1975
|
-
path: storablePath((0,
|
|
2567
|
+
path: storablePath((0, import_node_path7.resolve)(workspaceDir), bundlePath2),
|
|
1976
2568
|
removed: layers.length > 0,
|
|
1977
2569
|
layers
|
|
1978
2570
|
};
|
|
1979
2571
|
}
|
|
1980
2572
|
|
|
1981
2573
|
// src/kb-context.ts
|
|
1982
|
-
var
|
|
2574
|
+
var import_promises5 = require("fs/promises");
|
|
1983
2575
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
1984
2576
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
1985
2577
|
var CONTEXT_PROFILES = {
|
|
@@ -2183,13 +2775,13 @@ function toHookJson(block, event) {
|
|
|
2183
2775
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2184
2776
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2185
2777
|
async function syncInstructions(file, block) {
|
|
2186
|
-
const existing = await (0,
|
|
2778
|
+
const existing = await (0, import_promises5.readFile)(file, "utf8").catch(() => null);
|
|
2187
2779
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2188
2780
|
${block.trim()}
|
|
2189
2781
|
${CONTEXT_END}` : null;
|
|
2190
2782
|
if (existing === null) {
|
|
2191
2783
|
if (!region) return { file, action: "unchanged" };
|
|
2192
|
-
await (0,
|
|
2784
|
+
await (0, import_promises5.writeFile)(file, `${region}
|
|
2193
2785
|
`, "utf8");
|
|
2194
2786
|
return { file, action: "created" };
|
|
2195
2787
|
}
|
|
@@ -2200,11 +2792,11 @@ ${CONTEXT_END}` : null;
|
|
|
2200
2792
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2201
2793
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2202
2794
|
if (next === existing) return { file, action: "unchanged" };
|
|
2203
|
-
await (0,
|
|
2795
|
+
await (0, import_promises5.writeFile)(file, next, "utf8");
|
|
2204
2796
|
return { file, action: region ? "replaced" : "removed" };
|
|
2205
2797
|
}
|
|
2206
2798
|
if (!region) return { file, action: "unchanged" };
|
|
2207
|
-
await (0,
|
|
2799
|
+
await (0, import_promises5.writeFile)(
|
|
2208
2800
|
file,
|
|
2209
2801
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2210
2802
|
`,
|
|
@@ -2351,7 +2943,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
2351
2943
|
"aging",
|
|
2352
2944
|
"orphaned",
|
|
2353
2945
|
"broken-supersession",
|
|
2354
|
-
"superseded-but-cited"
|
|
2946
|
+
"superseded-but-cited",
|
|
2947
|
+
"drifted"
|
|
2355
2948
|
];
|
|
2356
2949
|
var CHECK_HEADLINES = {
|
|
2357
2950
|
expired: "past its stale_after date",
|
|
@@ -2360,7 +2953,8 @@ var CHECK_HEADLINES = {
|
|
|
2360
2953
|
aging: "still open or still proposed long after it was written",
|
|
2361
2954
|
orphaned: "no other record links to it",
|
|
2362
2955
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
2363
|
-
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
2956
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
2957
|
+
drifted: "the code an anchor points at moved out from under its hash"
|
|
2364
2958
|
};
|
|
2365
2959
|
var DAY_MS = 864e5;
|
|
2366
2960
|
function doctor(bundle, options = {}) {
|
|
@@ -2370,7 +2964,7 @@ function doctor(bundle, options = {}) {
|
|
|
2370
2964
|
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
2371
2965
|
};
|
|
2372
2966
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2373
|
-
const adjudicated = adjudicate(bundle, bundle, now);
|
|
2967
|
+
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
2374
2968
|
const standings = new Map(
|
|
2375
2969
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
2376
2970
|
);
|
|
@@ -2384,7 +2978,8 @@ function doctor(bundle, options = {}) {
|
|
|
2384
2978
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
2385
2979
|
group("orphaned", orphaned(bundle)),
|
|
2386
2980
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
2387
|
-
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
2981
|
+
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
2982
|
+
group("drifted", drifted(inForce))
|
|
2388
2983
|
];
|
|
2389
2984
|
const counts = Object.fromEntries(
|
|
2390
2985
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -2570,6 +3165,29 @@ function supersededButCited(bundle, standings) {
|
|
|
2570
3165
|
}
|
|
2571
3166
|
return findings;
|
|
2572
3167
|
}
|
|
3168
|
+
function drifted(hits) {
|
|
3169
|
+
const findings = [];
|
|
3170
|
+
for (const hit of hits) {
|
|
3171
|
+
const warning = hit.warnings.find((entry) => entry.kind === "drifted");
|
|
3172
|
+
if (!warning) continue;
|
|
3173
|
+
findings.push(
|
|
3174
|
+
finding(
|
|
3175
|
+
hit.record,
|
|
3176
|
+
`${warning.anchors.length} ${warning.anchors.length === 1 ? "anchor no longer matches" : "anchors no longer match"}: ${warning.anchors.map((anchor) => {
|
|
3177
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3178
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
3179
|
+
if (anchor.diffSize === null) {
|
|
3180
|
+
return `${at} (changed, size unrecorded)`;
|
|
3181
|
+
}
|
|
3182
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3183
|
+
}).join(", ")}`
|
|
3184
|
+
)
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
return findings.sort(
|
|
3188
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
2573
3191
|
function replaces(later, earlier) {
|
|
2574
3192
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
2575
3193
|
}
|
|
@@ -2639,13 +3257,16 @@ function selectDecisions(records) {
|
|
|
2639
3257
|
);
|
|
2640
3258
|
}
|
|
2641
3259
|
|
|
2642
|
-
// src/commands/
|
|
3260
|
+
// src/commands/anchor-resolve.ts
|
|
2643
3261
|
var import_zod8 = require("zod");
|
|
2644
3262
|
|
|
2645
3263
|
// src/commands/model.ts
|
|
2646
3264
|
var import_zod7 = require("zod");
|
|
2647
3265
|
var bundlePath = import_zod7.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2648
3266
|
var conceptId = import_zod7.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
3267
|
+
var REPO_ROOT = import_zod7.z.string().min(1).optional().describe(
|
|
3268
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
3269
|
+
);
|
|
2649
3270
|
function define(command) {
|
|
2650
3271
|
return command;
|
|
2651
3272
|
}
|
|
@@ -2665,13 +3286,175 @@ function argvFlag(argv, name) {
|
|
|
2665
3286
|
return value;
|
|
2666
3287
|
}
|
|
2667
3288
|
|
|
3289
|
+
// src/commands/anchor-resolve.ts
|
|
3290
|
+
var anchorResolveCommand = define({
|
|
3291
|
+
name: "anchor-resolve",
|
|
3292
|
+
tool: "kb_anchor_resolve",
|
|
3293
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
3294
|
+
description: "Resolve a record's anchors against the working tree: stamp a hash onto anchors that lack one, report drift where the code moved. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was, not whether the claim still holds. Anchors naming another repository are skipped. Exits non-zero on drift.",
|
|
3295
|
+
input: import_zod8.z.object({
|
|
3296
|
+
bundlePath,
|
|
3297
|
+
conceptId,
|
|
3298
|
+
repoRoot: import_zod8.z.string().min(1).optional(),
|
|
3299
|
+
rebaseline: import_zod8.z.boolean().optional().describe(
|
|
3300
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
3301
|
+
),
|
|
3302
|
+
restamp: import_zod8.z.boolean().optional().describe(
|
|
3303
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
3304
|
+
)
|
|
3305
|
+
}),
|
|
3306
|
+
fromArgv: (argv, path) => ({
|
|
3307
|
+
bundlePath: path,
|
|
3308
|
+
conceptId: argv[1],
|
|
3309
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3310
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
3311
|
+
restamp: argv.includes("--restamp")
|
|
3312
|
+
}),
|
|
3313
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
3314
|
+
const root = repoRoot ?? process.cwd();
|
|
3315
|
+
const record = await store.read(path, id);
|
|
3316
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
3317
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
3318
|
+
if (!anchors.length) {
|
|
3319
|
+
return {
|
|
3320
|
+
conceptId: id,
|
|
3321
|
+
results: [],
|
|
3322
|
+
verified: false,
|
|
3323
|
+
note: "record has no anchors"
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
const results = [];
|
|
3327
|
+
const updated = [];
|
|
3328
|
+
const origin = new LazyOrigin(root);
|
|
3329
|
+
let dirty = false;
|
|
3330
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
3331
|
+
const foreign = new Map(
|
|
3332
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
3333
|
+
);
|
|
3334
|
+
const reads = await readAnchorFiles(
|
|
3335
|
+
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
3336
|
+
anchorFileReader(root)
|
|
3337
|
+
);
|
|
3338
|
+
for (const anchor of anchors) {
|
|
3339
|
+
const base = {
|
|
3340
|
+
file: anchor.file,
|
|
3341
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3342
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
3343
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
3344
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
3345
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
3346
|
+
};
|
|
3347
|
+
if (foreign.get(anchor)) {
|
|
3348
|
+
results.push({ ...base, state: "unresolved", reason: "foreign-repo" });
|
|
3349
|
+
updated.push(anchor);
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
const fileRead = reads.get(anchor.file);
|
|
3353
|
+
if (!fileRead.ok) {
|
|
3354
|
+
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
3355
|
+
updated.push(anchor);
|
|
3356
|
+
continue;
|
|
3357
|
+
}
|
|
3358
|
+
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
3359
|
+
if (!resolved) {
|
|
3360
|
+
results.push({
|
|
3361
|
+
...base,
|
|
3362
|
+
state: "unresolved",
|
|
3363
|
+
reason: "symbol-not-found"
|
|
3364
|
+
});
|
|
3365
|
+
updated.push(anchor);
|
|
3366
|
+
continue;
|
|
3367
|
+
}
|
|
3368
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
3369
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3370
|
+
const stamped = {
|
|
3371
|
+
...anchor,
|
|
3372
|
+
hash: currentHash,
|
|
3373
|
+
lines: currentLines,
|
|
3374
|
+
resolved_at: now()
|
|
3375
|
+
};
|
|
3376
|
+
if (!anchor.hash) {
|
|
3377
|
+
results.push({ ...base, state: "stamped", currentHash });
|
|
3378
|
+
updated.push(stamped);
|
|
3379
|
+
dirty = true;
|
|
3380
|
+
} else if (anchor.hash === currentHash) {
|
|
3381
|
+
results.push({
|
|
3382
|
+
...base,
|
|
3383
|
+
state: "match",
|
|
3384
|
+
currentHash
|
|
3385
|
+
});
|
|
3386
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
3387
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
3388
|
+
if (refresh) dirty = true;
|
|
3389
|
+
} else {
|
|
3390
|
+
results.push({
|
|
3391
|
+
...base,
|
|
3392
|
+
state: "drifted",
|
|
3393
|
+
currentHash,
|
|
3394
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines),
|
|
3395
|
+
...rebaseline ? { rebaselined: true } : {}
|
|
3396
|
+
});
|
|
3397
|
+
updated.push(rebaseline ? stamped : anchor);
|
|
3398
|
+
if (rebaseline) dirty = true;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
let frozen = false;
|
|
3402
|
+
if (dirty) {
|
|
3403
|
+
try {
|
|
3404
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
3407
|
+
frozen = true;
|
|
3408
|
+
}
|
|
3409
|
+
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
3410
|
+
}
|
|
3411
|
+
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
3412
|
+
const checked = results.filter((entry) => entry.reason !== "foreign-repo");
|
|
3413
|
+
const skipped = results.length - checked.length;
|
|
3414
|
+
const matches2 = checked.filter((entry) => entry.state === "match").length;
|
|
3415
|
+
const clean = checked.length > 0 && checked.every((entry) => entry.state === "match");
|
|
3416
|
+
if (clean) {
|
|
3417
|
+
try {
|
|
3418
|
+
await store.verify(
|
|
3419
|
+
path,
|
|
3420
|
+
id,
|
|
3421
|
+
`anchor-resolve: ${matches2}/${checked.length} anchors match${skipped ? `, ${skipped} in another repo` : ""} (regex resolver)`,
|
|
3422
|
+
actor,
|
|
3423
|
+
now()
|
|
3424
|
+
);
|
|
3425
|
+
} catch (error) {
|
|
3426
|
+
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
3427
|
+
return {
|
|
3428
|
+
conceptId: id,
|
|
3429
|
+
results,
|
|
3430
|
+
verified: false,
|
|
3431
|
+
verifyRefused: "self-verification",
|
|
3432
|
+
...frozenNote
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
3436
|
+
}
|
|
3437
|
+
return { conceptId: id, results, verified: false, ...frozenNote };
|
|
3438
|
+
},
|
|
3439
|
+
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
3440
|
+
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
3441
|
+
// let the one edit that destroys an anchor pass the gate that exists to
|
|
3442
|
+
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
3443
|
+
// belonging to another repository was never this run's to check — failing CI
|
|
3444
|
+
// on either would gate on work this command did not do.
|
|
3445
|
+
failsWhen: (result) => result.results.some(
|
|
3446
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason !== "foreign-repo"
|
|
3447
|
+
)
|
|
3448
|
+
});
|
|
3449
|
+
|
|
2668
3450
|
// src/commands/answer.ts
|
|
3451
|
+
var import_zod9 = require("zod");
|
|
2669
3452
|
var answerCommand = define({
|
|
2670
3453
|
name: "answer",
|
|
2671
3454
|
tool: "kb_answer",
|
|
2672
3455
|
usage: "answer <concept-id> <answer...>",
|
|
2673
3456
|
description: "Resolve an open question: sets the status, stamps who answered and when, and appends an Answer section. If the answer overturns an assumption or a decision, that is a supersession \u2014 do it explicitly.",
|
|
2674
|
-
input:
|
|
3457
|
+
input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
|
|
2675
3458
|
fromArgv: (argv, path) => ({
|
|
2676
3459
|
bundlePath: path,
|
|
2677
3460
|
conceptId: argv[1],
|
|
@@ -2685,15 +3468,15 @@ var answerCommand = define({
|
|
|
2685
3468
|
});
|
|
2686
3469
|
|
|
2687
3470
|
// src/commands/catalog.ts
|
|
2688
|
-
var
|
|
3471
|
+
var import_zod10 = require("zod");
|
|
2689
3472
|
var catalogCommand = define({
|
|
2690
3473
|
name: "catalog",
|
|
2691
3474
|
tool: "kb_catalog",
|
|
2692
3475
|
usage: "catalog [type]",
|
|
2693
3476
|
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.",
|
|
2694
|
-
input:
|
|
3477
|
+
input: import_zod10.z.object({
|
|
2695
3478
|
bundlePath,
|
|
2696
|
-
type:
|
|
3479
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
2697
3480
|
}),
|
|
2698
3481
|
fromArgv: (argv, path) => ({
|
|
2699
3482
|
bundlePath: path,
|
|
@@ -2748,26 +3531,26 @@ function count(value, noun) {
|
|
|
2748
3531
|
}
|
|
2749
3532
|
|
|
2750
3533
|
// src/commands/context.ts
|
|
2751
|
-
var
|
|
3534
|
+
var import_zod11 = require("zod");
|
|
2752
3535
|
var contextCommand = define({
|
|
2753
3536
|
name: "context",
|
|
2754
3537
|
tool: "kb_context",
|
|
2755
3538
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2756
3539
|
description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
|
|
2757
|
-
input:
|
|
2758
|
-
budgetTokens:
|
|
3540
|
+
input: import_zod11.z.object({
|
|
3541
|
+
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2759
3542
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2760
3543
|
),
|
|
2761
|
-
fullUnderTokens:
|
|
3544
|
+
fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2762
3545
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
2763
3546
|
),
|
|
2764
|
-
profile:
|
|
3547
|
+
profile: import_zod11.z.string().optional().describe(
|
|
2765
3548
|
"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."
|
|
2766
3549
|
),
|
|
2767
|
-
format:
|
|
3550
|
+
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
2768
3551
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2769
3552
|
),
|
|
2770
|
-
event:
|
|
3553
|
+
event: import_zod11.z.string().optional().describe(
|
|
2771
3554
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2772
3555
|
)
|
|
2773
3556
|
}),
|
|
@@ -2803,15 +3586,16 @@ var contextCommand = define({
|
|
|
2803
3586
|
});
|
|
2804
3587
|
|
|
2805
3588
|
// src/commands/doctor.ts
|
|
2806
|
-
var
|
|
2807
|
-
var days = (what, fallback) =>
|
|
3589
|
+
var import_zod12 = require("zod");
|
|
3590
|
+
var days = (what, fallback) => import_zod12.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2808
3591
|
var doctorCommand = define({
|
|
2809
3592
|
name: "doctor",
|
|
2810
3593
|
tool: "kb_doctor",
|
|
2811
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
2812
|
-
description: "
|
|
2813
|
-
input:
|
|
3594
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
3595
|
+
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
3596
|
+
input: import_zod12.z.object({
|
|
2814
3597
|
bundlePath,
|
|
3598
|
+
repoRoot: REPO_ROOT,
|
|
2815
3599
|
expiringDays: days(
|
|
2816
3600
|
"How far ahead `expiring` looks, in days.",
|
|
2817
3601
|
DEFAULT_EXPIRING_DAYS
|
|
@@ -2824,7 +3608,7 @@ var doctorCommand = define({
|
|
|
2824
3608
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2825
3609
|
DEFAULT_AGING_DAYS
|
|
2826
3610
|
),
|
|
2827
|
-
strict:
|
|
3611
|
+
strict: import_zod12.z.boolean().optional().describe(
|
|
2828
3612
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2829
3613
|
)
|
|
2830
3614
|
}),
|
|
@@ -2836,29 +3620,36 @@ var doctorCommand = define({
|
|
|
2836
3620
|
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
2837
3621
|
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
2838
3622
|
const agingDays = argvFlag(argv, "--aging-days");
|
|
3623
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2839
3624
|
return {
|
|
2840
3625
|
bundlePath: path,
|
|
3626
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
2841
3627
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
2842
3628
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
2843
3629
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
2844
3630
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
2845
3631
|
};
|
|
2846
3632
|
},
|
|
2847
|
-
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
3633
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays, repoRoot }) => {
|
|
2848
3634
|
const checkedAt = now();
|
|
2849
|
-
const
|
|
3635
|
+
const records = await store.list(path);
|
|
3636
|
+
const anchorDrift = await store.detectDrift(records, repoRoot);
|
|
3637
|
+
const report = doctor(records, {
|
|
2850
3638
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
2851
3639
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
2852
3640
|
...agingDays !== void 0 ? { agingDays } : {},
|
|
3641
|
+
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
2853
3642
|
now: new Date(checkedAt)
|
|
2854
3643
|
});
|
|
2855
3644
|
return { bundlePath: path, checkedAt, ...report };
|
|
2856
3645
|
},
|
|
2857
3646
|
render: (result) => render2(result),
|
|
2858
|
-
// Only expiry, and only under --strict. The other
|
|
3647
|
+
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
2859
3648
|
// reader decides about; an expired record is the base asserting something it
|
|
2860
3649
|
// already said it would stop standing behind, which is the one finding a
|
|
2861
|
-
// pipeline can act on without a judgment call.
|
|
3650
|
+
// pipeline can act on without a judgment call. Drift has its own gate —
|
|
3651
|
+
// `anchor-resolve` exits non-zero on it, against a repo root the caller
|
|
3652
|
+
// named, which is the run a CI pipeline should be making anyway.
|
|
2862
3653
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
2863
3654
|
});
|
|
2864
3655
|
function render2(result) {
|
|
@@ -2893,13 +3684,13 @@ function render2(result) {
|
|
|
2893
3684
|
}
|
|
2894
3685
|
|
|
2895
3686
|
// src/commands/list.ts
|
|
2896
|
-
var
|
|
3687
|
+
var import_zod13 = require("zod");
|
|
2897
3688
|
var listCommand = define({
|
|
2898
3689
|
name: "list",
|
|
2899
3690
|
tool: "kb_list",
|
|
2900
3691
|
usage: "list [type]",
|
|
2901
3692
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
2902
|
-
input:
|
|
3693
|
+
input: import_zod13.z.object({ bundlePath, type: import_zod13.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2903
3694
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2904
3695
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2905
3696
|
conceptId: record.conceptId,
|
|
@@ -2911,36 +3702,40 @@ var listCommand = define({
|
|
|
2911
3702
|
});
|
|
2912
3703
|
|
|
2913
3704
|
// src/commands/load.ts
|
|
2914
|
-
var
|
|
3705
|
+
var import_zod14 = require("zod");
|
|
2915
3706
|
var loadCommand = define({
|
|
2916
3707
|
name: "load",
|
|
2917
3708
|
tool: "kb_load",
|
|
2918
|
-
usage: "load [type] [--budget N] [--
|
|
2919
|
-
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs
|
|
2920
|
-
input:
|
|
3709
|
+
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3710
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs; rejected and open records arrive whole. Refuses past the token budget \u2014 call kb_catalog, kb_pack on it; `all` bypasses the budget. Never read record files directly. Cache-stable; `digest` is the base's content stamp \u2014 hooks use it to tell you when to reload.",
|
|
3711
|
+
input: import_zod14.z.object({
|
|
2921
3712
|
bundlePath,
|
|
2922
|
-
type:
|
|
2923
|
-
budgetTokens:
|
|
2924
|
-
all:
|
|
3713
|
+
type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
|
|
3714
|
+
budgetTokens: import_zod14.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
3715
|
+
all: import_zod14.z.boolean().optional().describe(
|
|
2925
3716
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2926
|
-
)
|
|
3717
|
+
),
|
|
3718
|
+
repoRoot: REPO_ROOT
|
|
2927
3719
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
2928
3720
|
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
2929
3721
|
}),
|
|
2930
3722
|
fromArgv: (argv, path) => {
|
|
2931
3723
|
const budget = argvFlag(argv, "--budget");
|
|
3724
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2932
3725
|
return {
|
|
2933
3726
|
bundlePath: path,
|
|
2934
3727
|
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
2935
3728
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
2936
|
-
...argv.includes("--all") ? { all: true } : {}
|
|
3729
|
+
...argv.includes("--all") ? { all: true } : {},
|
|
3730
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2937
3731
|
};
|
|
2938
3732
|
},
|
|
2939
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
3733
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all, repoRoot }) => {
|
|
2940
3734
|
const result = await store.load(path, {
|
|
2941
3735
|
...type ? { type } : {},
|
|
2942
3736
|
...budgetTokens ? { budgetTokens } : {},
|
|
2943
|
-
...all ? { all } : {}
|
|
3737
|
+
...all ? { all } : {},
|
|
3738
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2944
3739
|
});
|
|
2945
3740
|
if (!result.loaded) return result;
|
|
2946
3741
|
return {
|
|
@@ -2959,25 +3754,25 @@ var loadCommand = define({
|
|
|
2959
3754
|
});
|
|
2960
3755
|
|
|
2961
3756
|
// src/commands/log.ts
|
|
2962
|
-
var
|
|
3757
|
+
var import_zod15 = require("zod");
|
|
2963
3758
|
var logCommand = define({
|
|
2964
3759
|
name: "log",
|
|
2965
3760
|
tool: "kb_log",
|
|
2966
3761
|
usage: "log",
|
|
2967
3762
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
2968
|
-
input:
|
|
3763
|
+
input: import_zod15.z.object({ bundlePath }),
|
|
2969
3764
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2970
3765
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2971
3766
|
});
|
|
2972
3767
|
|
|
2973
3768
|
// src/commands/no-decision.ts
|
|
2974
|
-
var
|
|
3769
|
+
var import_zod16 = require("zod");
|
|
2975
3770
|
var noDecisionCommand = define({
|
|
2976
3771
|
name: "no-decision",
|
|
2977
3772
|
tool: "kb_no_decision",
|
|
2978
3773
|
usage: "no-decision <reason...>",
|
|
2979
3774
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
2980
|
-
input:
|
|
3775
|
+
input: import_zod16.z.object({ bundlePath, reason: import_zod16.z.string().min(1) }),
|
|
2981
3776
|
fromArgv: (argv, path) => ({
|
|
2982
3777
|
bundlePath: path,
|
|
2983
3778
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2994,20 +3789,20 @@ var noDecisionCommand = define({
|
|
|
2994
3789
|
});
|
|
2995
3790
|
|
|
2996
3791
|
// src/commands/pack.ts
|
|
2997
|
-
var
|
|
3792
|
+
var import_zod17 = require("zod");
|
|
2998
3793
|
var packCommand = define({
|
|
2999
3794
|
name: "pack",
|
|
3000
3795
|
tool: "kb_pack",
|
|
3001
3796
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3002
3797
|
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
3003
|
-
input:
|
|
3798
|
+
input: import_zod17.z.object({
|
|
3004
3799
|
bundlePath,
|
|
3005
3800
|
conceptId,
|
|
3006
|
-
hops:
|
|
3007
|
-
maxNodes:
|
|
3801
|
+
hops: import_zod17.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
3802
|
+
maxNodes: import_zod17.z.number().int().positive().optional().describe(
|
|
3008
3803
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3009
3804
|
),
|
|
3010
|
-
budgetTokens:
|
|
3805
|
+
budgetTokens: import_zod17.z.number().int().positive().optional().describe(
|
|
3011
3806
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3012
3807
|
)
|
|
3013
3808
|
}),
|
|
@@ -3094,22 +3889,22 @@ function warningLabel(warning) {
|
|
|
3094
3889
|
}
|
|
3095
3890
|
|
|
3096
3891
|
// src/commands/pin.ts
|
|
3097
|
-
var
|
|
3892
|
+
var import_zod18 = require("zod");
|
|
3098
3893
|
var pinCommand = define({
|
|
3099
3894
|
name: "pin",
|
|
3100
3895
|
tool: "kb_pin",
|
|
3101
3896
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3102
3897
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
3103
|
-
input:
|
|
3898
|
+
input: import_zod18.z.object({
|
|
3104
3899
|
bundlePath,
|
|
3105
|
-
mode:
|
|
3900
|
+
mode: import_zod18.z.enum(["full", "index"]).optional().describe(
|
|
3106
3901
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
3107
3902
|
),
|
|
3108
|
-
profiles:
|
|
3109
|
-
layer:
|
|
3903
|
+
profiles: import_zod18.z.array(import_zod18.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
3904
|
+
layer: import_zod18.z.enum(["project", "local", "user"]).optional().describe(
|
|
3110
3905
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3111
3906
|
),
|
|
3112
|
-
frozen:
|
|
3907
|
+
frozen: import_zod18.z.boolean().optional().describe(
|
|
3113
3908
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3114
3909
|
)
|
|
3115
3910
|
}),
|
|
@@ -3138,38 +3933,48 @@ var pinCommand = define({
|
|
|
3138
3933
|
});
|
|
3139
3934
|
|
|
3140
3935
|
// src/commands/pins.ts
|
|
3141
|
-
var
|
|
3936
|
+
var import_zod19 = require("zod");
|
|
3142
3937
|
var pinsCommand = define({
|
|
3143
3938
|
name: "pins",
|
|
3144
3939
|
tool: "kb_pins",
|
|
3145
3940
|
usage: "pins",
|
|
3146
3941
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
3147
|
-
input:
|
|
3942
|
+
input: import_zod19.z.object({}),
|
|
3148
3943
|
fromArgv: () => ({}),
|
|
3149
3944
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3150
3945
|
});
|
|
3151
3946
|
|
|
3152
3947
|
// src/commands/query.ts
|
|
3153
|
-
var
|
|
3948
|
+
var import_zod20 = require("zod");
|
|
3154
3949
|
var queryCommand = define({
|
|
3155
3950
|
name: "query",
|
|
3156
3951
|
tool: "kb_query",
|
|
3157
|
-
usage: "query <text...>",
|
|
3158
|
-
description: "Search
|
|
3159
|
-
input:
|
|
3952
|
+
usage: "query <text...> [--repo-root PATH]",
|
|
3953
|
+
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.",
|
|
3954
|
+
input: import_zod20.z.object({
|
|
3160
3955
|
bundlePath,
|
|
3161
|
-
text:
|
|
3162
|
-
type:
|
|
3163
|
-
includeNonCurrent:
|
|
3956
|
+
text: import_zod20.z.string().optional(),
|
|
3957
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
3958
|
+
includeNonCurrent: import_zod20.z.boolean().optional(),
|
|
3959
|
+
repoRoot: REPO_ROOT
|
|
3164
3960
|
}),
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3961
|
+
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
3962
|
+
fromArgv: (argv, path) => {
|
|
3963
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
3964
|
+
const words = argv.slice(1);
|
|
3965
|
+
const flag = words.indexOf("--repo-root");
|
|
3966
|
+
if (flag !== -1) words.splice(flag, 2);
|
|
3967
|
+
return {
|
|
3968
|
+
bundlePath: path,
|
|
3969
|
+
text: words.join(" ").trim(),
|
|
3970
|
+
includeNonCurrent: true,
|
|
3971
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
3972
|
+
};
|
|
3973
|
+
},
|
|
3974
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
3171
3975
|
...type ? { type } : {},
|
|
3172
|
-
includeNonCurrent: includeNonCurrent === true
|
|
3976
|
+
includeNonCurrent: includeNonCurrent === true,
|
|
3977
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
3173
3978
|
})).map((hit) => ({
|
|
3174
3979
|
conceptId: hit.record.conceptId,
|
|
3175
3980
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -3182,40 +3987,40 @@ var queryCommand = define({
|
|
|
3182
3987
|
});
|
|
3183
3988
|
|
|
3184
3989
|
// src/commands/read-index.ts
|
|
3185
|
-
var
|
|
3990
|
+
var import_zod21 = require("zod");
|
|
3186
3991
|
var readIndexCommand = define({
|
|
3187
3992
|
name: "index",
|
|
3188
3993
|
tool: "kb_index",
|
|
3189
3994
|
usage: "index",
|
|
3190
3995
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
3191
|
-
input:
|
|
3996
|
+
input: import_zod21.z.object({ bundlePath }),
|
|
3192
3997
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3193
3998
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3194
3999
|
});
|
|
3195
4000
|
|
|
3196
4001
|
// src/commands/schema.ts
|
|
3197
|
-
var
|
|
4002
|
+
var import_zod22 = require("zod");
|
|
3198
4003
|
var schemaCommand = define({
|
|
3199
4004
|
name: "schema",
|
|
3200
4005
|
tool: "kb_schema",
|
|
3201
4006
|
usage: "schema",
|
|
3202
4007
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
3203
|
-
input:
|
|
4008
|
+
input: import_zod22.z.object({}),
|
|
3204
4009
|
fromArgv: () => ({}),
|
|
3205
4010
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3206
4011
|
});
|
|
3207
4012
|
|
|
3208
4013
|
// src/commands/status.ts
|
|
3209
|
-
var
|
|
4014
|
+
var import_zod23 = require("zod");
|
|
3210
4015
|
var statusCommand = define({
|
|
3211
4016
|
name: "status",
|
|
3212
4017
|
tool: "kb_status",
|
|
3213
4018
|
usage: "status <concept-id> <status>",
|
|
3214
4019
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
3215
|
-
input:
|
|
4020
|
+
input: import_zod23.z.object({
|
|
3216
4021
|
bundlePath,
|
|
3217
4022
|
conceptId,
|
|
3218
|
-
status:
|
|
4023
|
+
status: import_zod23.z.enum(KB_RECORD_STATUSES)
|
|
3219
4024
|
}),
|
|
3220
4025
|
fromArgv: (argv, path) => ({
|
|
3221
4026
|
bundlePath: path,
|
|
@@ -3230,13 +4035,13 @@ var statusCommand = define({
|
|
|
3230
4035
|
});
|
|
3231
4036
|
|
|
3232
4037
|
// src/commands/supersede.ts
|
|
3233
|
-
var
|
|
4038
|
+
var import_zod24 = require("zod");
|
|
3234
4039
|
var supersedeCommand = define({
|
|
3235
4040
|
name: "supersede",
|
|
3236
4041
|
tool: "kb_supersede",
|
|
3237
4042
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3238
4043
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
3239
|
-
input:
|
|
4044
|
+
input: import_zod24.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3240
4045
|
fromArgv: (argv, path) => ({
|
|
3241
4046
|
bundlePath: path,
|
|
3242
4047
|
conceptId: argv[1],
|
|
@@ -3250,16 +4055,16 @@ var supersedeCommand = define({
|
|
|
3250
4055
|
});
|
|
3251
4056
|
|
|
3252
4057
|
// src/commands/sync-instructions.ts
|
|
3253
|
-
var
|
|
4058
|
+
var import_zod25 = require("zod");
|
|
3254
4059
|
var syncInstructionsCommand = define({
|
|
3255
4060
|
name: "sync-instructions",
|
|
3256
4061
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3257
4062
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
3258
|
-
input:
|
|
3259
|
-
file:
|
|
3260
|
-
budgetTokens:
|
|
3261
|
-
fullUnderTokens:
|
|
3262
|
-
profile:
|
|
4063
|
+
input: import_zod25.z.object({
|
|
4064
|
+
file: import_zod25.z.string().min(1).describe("The instruction file to edit in place."),
|
|
4065
|
+
budgetTokens: import_zod25.z.number().int().positive().optional(),
|
|
4066
|
+
fullUnderTokens: import_zod25.z.number().int().positive().optional(),
|
|
4067
|
+
profile: import_zod25.z.string().optional()
|
|
3263
4068
|
}),
|
|
3264
4069
|
fromArgv: (argv) => {
|
|
3265
4070
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3285,17 +4090,17 @@ var syncInstructionsCommand = define({
|
|
|
3285
4090
|
});
|
|
3286
4091
|
|
|
3287
4092
|
// src/commands/trace.ts
|
|
3288
|
-
var
|
|
4093
|
+
var import_zod26 = require("zod");
|
|
3289
4094
|
var traceCommand = define({
|
|
3290
4095
|
name: "trace",
|
|
3291
4096
|
tool: "kb_trace",
|
|
3292
4097
|
usage: "trace <concept-id> [edges...]",
|
|
3293
4098
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
3294
|
-
input:
|
|
4099
|
+
input: import_zod26.z.object({
|
|
3295
4100
|
bundlePath,
|
|
3296
4101
|
conceptId,
|
|
3297
|
-
edges:
|
|
3298
|
-
depth:
|
|
4102
|
+
edges: import_zod26.z.array(import_zod26.z.enum(TRACE_EDGES)).optional(),
|
|
4103
|
+
depth: import_zod26.z.number().int().positive().optional()
|
|
3299
4104
|
}),
|
|
3300
4105
|
fromArgv: (argv, path) => ({
|
|
3301
4106
|
bundlePath: path,
|
|
@@ -3317,53 +4122,53 @@ var traceCommand = define({
|
|
|
3317
4122
|
});
|
|
3318
4123
|
|
|
3319
4124
|
// src/commands/types.ts
|
|
3320
|
-
var
|
|
4125
|
+
var import_zod27 = require("zod");
|
|
3321
4126
|
var typesCommand = define({
|
|
3322
4127
|
name: "types",
|
|
3323
4128
|
tool: "kb_types",
|
|
3324
4129
|
usage: "types",
|
|
3325
4130
|
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.",
|
|
3326
|
-
input:
|
|
4131
|
+
input: import_zod27.z.object({}),
|
|
3327
4132
|
fromArgv: () => ({}),
|
|
3328
4133
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3329
4134
|
});
|
|
3330
4135
|
|
|
3331
4136
|
// src/commands/unpin.ts
|
|
3332
|
-
var
|
|
4137
|
+
var import_zod28 = require("zod");
|
|
3333
4138
|
var unpinCommand = define({
|
|
3334
4139
|
name: "unpin",
|
|
3335
4140
|
tool: "kb_unpin",
|
|
3336
4141
|
usage: "unpin [bundle-path]",
|
|
3337
4142
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
3338
|
-
input:
|
|
4143
|
+
input: import_zod28.z.object({ bundlePath }),
|
|
3339
4144
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3340
4145
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3341
4146
|
});
|
|
3342
4147
|
|
|
3343
4148
|
// src/commands/validate.ts
|
|
3344
|
-
var
|
|
4149
|
+
var import_zod29 = require("zod");
|
|
3345
4150
|
var validateCommand = define({
|
|
3346
4151
|
name: "validate",
|
|
3347
4152
|
tool: "kb_validate",
|
|
3348
4153
|
usage: "validate",
|
|
3349
4154
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
3350
|
-
input:
|
|
4155
|
+
input: import_zod29.z.object({ bundlePath }),
|
|
3351
4156
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3352
4157
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3353
4158
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
3354
4159
|
});
|
|
3355
4160
|
|
|
3356
4161
|
// src/commands/verify.ts
|
|
3357
|
-
var
|
|
4162
|
+
var import_zod30 = require("zod");
|
|
3358
4163
|
var verifyCommand = define({
|
|
3359
4164
|
name: "verify",
|
|
3360
4165
|
tool: "kb_verify",
|
|
3361
4166
|
usage: "verify <concept-id> --note <text>",
|
|
3362
4167
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
3363
|
-
input:
|
|
4168
|
+
input: import_zod30.z.object({
|
|
3364
4169
|
bundlePath,
|
|
3365
4170
|
conceptId,
|
|
3366
|
-
note:
|
|
4171
|
+
note: import_zod30.z.string().refine((s) => s.trim().length > 0, {
|
|
3367
4172
|
message: "note must say what the check found"
|
|
3368
4173
|
})
|
|
3369
4174
|
}),
|
|
@@ -3383,7 +4188,7 @@ var verifyCommand = define({
|
|
|
3383
4188
|
});
|
|
3384
4189
|
|
|
3385
4190
|
// src/commands/write.ts
|
|
3386
|
-
var
|
|
4191
|
+
var import_zod31 = require("zod");
|
|
3387
4192
|
var writeCommand = define({
|
|
3388
4193
|
name: "write",
|
|
3389
4194
|
tool: "kb_write",
|
|
@@ -3397,9 +4202,9 @@ var writeCommand = define({
|
|
|
3397
4202
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3398
4203
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3399
4204
|
].join("\n"),
|
|
3400
|
-
input:
|
|
4205
|
+
input: import_zod31.z.object({
|
|
3401
4206
|
bundlePath,
|
|
3402
|
-
type:
|
|
4207
|
+
type: import_zod31.z.enum(KB_RECORD_TYPES),
|
|
3403
4208
|
input: composeInputSchema
|
|
3404
4209
|
}),
|
|
3405
4210
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3423,7 +4228,7 @@ var writeCommand = define({
|
|
|
3423
4228
|
});
|
|
3424
4229
|
|
|
3425
4230
|
// src/commands/write-decision.ts
|
|
3426
|
-
var
|
|
4231
|
+
var import_zod32 = require("zod");
|
|
3427
4232
|
var writeDecisionCommand = define({
|
|
3428
4233
|
name: "write-decision",
|
|
3429
4234
|
tool: "kb_write_decision",
|
|
@@ -3436,7 +4241,7 @@ var writeDecisionCommand = define({
|
|
|
3436
4241
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3437
4242
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3438
4243
|
].join("\n"),
|
|
3439
|
-
input:
|
|
4244
|
+
input: import_zod32.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3440
4245
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3441
4246
|
bundlePath: path,
|
|
3442
4247
|
input: JSON.parse(await stdin())
|
|
@@ -3465,6 +4270,7 @@ var KB_COMMANDS = [
|
|
|
3465
4270
|
supersedeCommand,
|
|
3466
4271
|
answerCommand,
|
|
3467
4272
|
verifyCommand,
|
|
4273
|
+
anchorResolveCommand,
|
|
3468
4274
|
loadCommand,
|
|
3469
4275
|
catalogCommand,
|
|
3470
4276
|
packCommand,
|
|
@@ -3492,7 +4298,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
3492
4298
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3493
4299
|
|
|
3494
4300
|
// src/version.ts
|
|
3495
|
-
var VERSION = true ? "0.1.
|
|
4301
|
+
var VERSION = true ? "0.1.12" : "0.0.0-dev";
|
|
3496
4302
|
|
|
3497
4303
|
// src/mcp.ts
|
|
3498
4304
|
function createKbMcpServer() {
|
|
@@ -3531,7 +4337,7 @@ async function runKbMcpServer() {
|
|
|
3531
4337
|
}
|
|
3532
4338
|
|
|
3533
4339
|
// src/cli.ts
|
|
3534
|
-
var
|
|
4340
|
+
var import_node_path8 = require("path");
|
|
3535
4341
|
async function runKbCli(argv) {
|
|
3536
4342
|
const { flags, literal } = takeLiteral(argv);
|
|
3537
4343
|
const { bundle, rest: withFlags } = takeBundle(flags);
|
|
@@ -3588,18 +4394,18 @@ function takeLiteral(argv) {
|
|
|
3588
4394
|
function takeBundle(argv) {
|
|
3589
4395
|
const at = argv.indexOf("--bundle");
|
|
3590
4396
|
if (at === -1) {
|
|
3591
|
-
return { bundle: (0,
|
|
4397
|
+
return { bundle: (0, import_node_path8.join)(process.cwd(), KB_DIR), rest: argv };
|
|
3592
4398
|
}
|
|
3593
4399
|
const bundle = argv[at + 1];
|
|
3594
4400
|
if (!bundle) die("--bundle requires a path");
|
|
3595
4401
|
return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
|
|
3596
4402
|
}
|
|
3597
4403
|
function readStdin() {
|
|
3598
|
-
return new Promise((
|
|
4404
|
+
return new Promise((resolve6, reject) => {
|
|
3599
4405
|
let text = "";
|
|
3600
4406
|
process.stdin.setEncoding("utf8");
|
|
3601
4407
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
3602
|
-
process.stdin.on("end", () =>
|
|
4408
|
+
process.stdin.on("end", () => resolve6(text));
|
|
3603
4409
|
process.stdin.on("error", reject);
|
|
3604
4410
|
});
|
|
3605
4411
|
}
|
|
@@ -3678,6 +4484,7 @@ function usage() {
|
|
|
3678
4484
|
SEARCH_INDEX_FILE,
|
|
3679
4485
|
TRACE_EDGES,
|
|
3680
4486
|
adjudicate,
|
|
4487
|
+
anchorFilePath,
|
|
3681
4488
|
assertBaseNotFrozen,
|
|
3682
4489
|
buildContext,
|
|
3683
4490
|
catalog,
|
|
@@ -3688,8 +4495,10 @@ function usage() {
|
|
|
3688
4495
|
contextProfileBudgets,
|
|
3689
4496
|
createKbMcpServer,
|
|
3690
4497
|
decisionInputSchema,
|
|
4498
|
+
detectAnchorDrift,
|
|
3691
4499
|
doctor,
|
|
3692
4500
|
edgeNeighbours,
|
|
4501
|
+
hashAnchorText,
|
|
3693
4502
|
indexIsStale,
|
|
3694
4503
|
isKbRecordType,
|
|
3695
4504
|
isNoDecisionRecord,
|
|
@@ -3712,10 +4521,12 @@ function usage() {
|
|
|
3712
4521
|
pinBase,
|
|
3713
4522
|
readMergedPins,
|
|
3714
4523
|
readPinsLayer,
|
|
4524
|
+
regexResolver,
|
|
3715
4525
|
renderCatalogLine,
|
|
3716
4526
|
renderIndex,
|
|
3717
4527
|
renderIndexLine,
|
|
3718
4528
|
renderLogEntry,
|
|
4529
|
+
resolveAnchor,
|
|
3719
4530
|
resolveHeads,
|
|
3720
4531
|
resolveHits,
|
|
3721
4532
|
resolvePinPath,
|