@saasontools/strauss-kb 0.1.9 → 0.1.11
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 +155 -62
- package/dist/{chunk-MWWDD23L.js → chunk-CWWXMD35.js} +2 -2
- package/dist/{chunk-KVEEISYQ.js → chunk-I3WW4F6X.js} +2 -2
- package/dist/{chunk-OFDWRMY6.js → chunk-OVRQCQ6P.js} +1257 -304
- package/dist/chunk-OVRQCQ6P.js.map +1 -0
- package/dist/cli-main.cjs +1266 -321
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1145 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -13
- package/dist/index.d.ts +294 -13
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1262 -317
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OFDWRMY6.js.map +0 -1
- /package/dist/{chunk-MWWDD23L.js.map → chunk-CWWXMD35.js.map} +0 -0
- /package/dist/{chunk-KVEEISYQ.js.map → chunk-I3WW4F6X.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
KB_SLUG_PATTERN: () => KB_SLUG_PATTERN,
|
|
58
58
|
KbBaseFrozenError: () => KbBaseFrozenError,
|
|
59
59
|
KbInvalidConceptIdError: () => KbInvalidConceptIdError,
|
|
60
|
+
KbMissingFlagValueError: () => KbMissingFlagValueError,
|
|
60
61
|
KbPackBudgetExceededError: () => KbPackBudgetExceededError,
|
|
61
62
|
KbPinsMalformedError: () => KbPinsMalformedError,
|
|
62
63
|
KbRecordAlreadyExistsError: () => KbRecordAlreadyExistsError,
|
|
@@ -73,8 +74,10 @@ __export(index_exports, {
|
|
|
73
74
|
SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
|
|
74
75
|
TRACE_EDGES: () => TRACE_EDGES,
|
|
75
76
|
adjudicate: () => adjudicate,
|
|
77
|
+
anchorFilePath: () => anchorFilePath,
|
|
76
78
|
assertBaseNotFrozen: () => assertBaseNotFrozen,
|
|
77
79
|
buildContext: () => buildContext,
|
|
80
|
+
catalog: () => catalog,
|
|
78
81
|
composeDecisionRecord: () => composeDecisionRecord,
|
|
79
82
|
composeInputSchema: () => composeInputSchema,
|
|
80
83
|
composeNoDecisionRecord: () => composeNoDecisionRecord,
|
|
@@ -82,8 +85,10 @@ __export(index_exports, {
|
|
|
82
85
|
contextProfileBudgets: () => contextProfileBudgets,
|
|
83
86
|
createKbMcpServer: () => createKbMcpServer,
|
|
84
87
|
decisionInputSchema: () => decisionInputSchema,
|
|
88
|
+
detectAnchorDrift: () => detectAnchorDrift,
|
|
85
89
|
doctor: () => doctor,
|
|
86
90
|
edgeNeighbours: () => edgeNeighbours,
|
|
91
|
+
hashAnchorText: () => hashAnchorText,
|
|
87
92
|
indexIsStale: () => indexIsStale,
|
|
88
93
|
isKbRecordType: () => isKbRecordType,
|
|
89
94
|
isNoDecisionRecord: () => isNoDecisionRecord,
|
|
@@ -106,9 +111,12 @@ __export(index_exports, {
|
|
|
106
111
|
pinBase: () => pinBase,
|
|
107
112
|
readMergedPins: () => readMergedPins,
|
|
108
113
|
readPinsLayer: () => readPinsLayer,
|
|
114
|
+
regexResolver: () => regexResolver,
|
|
115
|
+
renderCatalogLine: () => renderCatalogLine,
|
|
109
116
|
renderIndex: () => renderIndex,
|
|
110
117
|
renderIndexLine: () => renderIndexLine,
|
|
111
118
|
renderLogEntry: () => renderLogEntry,
|
|
119
|
+
resolveAnchor: () => resolveAnchor,
|
|
112
120
|
resolveHeads: () => resolveHeads,
|
|
113
121
|
resolveHits: () => resolveHits,
|
|
114
122
|
resolvePinPath: () => resolvePinPath,
|
|
@@ -127,9 +135,38 @@ __export(index_exports, {
|
|
|
127
135
|
module.exports = __toCommonJS(index_exports);
|
|
128
136
|
|
|
129
137
|
// src/kb-store.ts
|
|
130
|
-
var
|
|
131
|
-
var
|
|
132
|
-
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
|
+
}
|
|
133
170
|
|
|
134
171
|
// src/markdown.ts
|
|
135
172
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -176,7 +213,31 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
176
213
|
});
|
|
177
214
|
var kbAnchorSchema = import_zod.z.object({
|
|
178
215
|
file: import_zod.z.string().min(1),
|
|
179
|
-
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()
|
|
180
241
|
}).strict();
|
|
181
242
|
var KB_RECORD_TYPES = [
|
|
182
243
|
"fact",
|
|
@@ -257,6 +318,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
257
318
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
258
319
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
259
320
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
321
|
+
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
260
322
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
261
323
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
262
324
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
@@ -368,6 +430,21 @@ var KbPackBudgetExceededError = class extends BaseError {
|
|
|
368
430
|
budgetTokens;
|
|
369
431
|
excluded;
|
|
370
432
|
};
|
|
433
|
+
var KbMissingFlagValueError = class extends BaseError {
|
|
434
|
+
constructor(flag) {
|
|
435
|
+
super({
|
|
436
|
+
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
437
|
+
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
438
|
+
code: 400,
|
|
439
|
+
fault: "User" /* User */,
|
|
440
|
+
retriable: false,
|
|
441
|
+
reportToUser: true,
|
|
442
|
+
details: { flag }
|
|
443
|
+
});
|
|
444
|
+
this.flag = flag;
|
|
445
|
+
}
|
|
446
|
+
flag;
|
|
447
|
+
};
|
|
371
448
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
372
449
|
constructor(message, details) {
|
|
373
450
|
super({
|
|
@@ -413,7 +490,7 @@ var STANDING = {
|
|
|
413
490
|
rejected: "rejected",
|
|
414
491
|
superseded: "superseded"
|
|
415
492
|
};
|
|
416
|
-
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
493
|
+
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift) {
|
|
417
494
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
418
495
|
return hits.map((record) => {
|
|
419
496
|
const status = record.frontmatter.strauss_status;
|
|
@@ -443,6 +520,20 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
|
443
520
|
if (!record.frontmatter.verified?.length) {
|
|
444
521
|
warnings.push({ kind: "unverified" });
|
|
445
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
|
+
}
|
|
446
537
|
return { record, standing: STANDING[status], heads, warnings };
|
|
447
538
|
});
|
|
448
539
|
}
|
|
@@ -495,9 +586,430 @@ function successors(record, byId) {
|
|
|
495
586
|
return { records, missing };
|
|
496
587
|
}
|
|
497
588
|
|
|
498
|
-
// src/
|
|
589
|
+
// src/anchor-resolver.ts
|
|
590
|
+
var import_node_child_process = require("child_process");
|
|
591
|
+
var import_node_crypto = require("crypto");
|
|
499
592
|
var import_promises = require("fs/promises");
|
|
500
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");
|
|
501
1013
|
|
|
502
1014
|
// src/kb-log.ts
|
|
503
1015
|
var import_zod2 = require("zod");
|
|
@@ -560,7 +1072,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
560
1072
|
let store = null;
|
|
561
1073
|
try {
|
|
562
1074
|
store = await qmd.createStore({
|
|
563
|
-
dbPath: (0,
|
|
1075
|
+
dbPath: (0, import_node_path2.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
564
1076
|
config: {
|
|
565
1077
|
collections: {
|
|
566
1078
|
[COLLECTION]: {
|
|
@@ -595,16 +1107,19 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
595
1107
|
}
|
|
596
1108
|
}
|
|
597
1109
|
async function isStale(bundlePath2) {
|
|
598
|
-
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);
|
|
599
1111
|
if (!indexAt) return true;
|
|
600
1112
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
601
|
-
const names = await readdir2(bundlePath2).catch(() => [])
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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;
|
|
608
1123
|
}
|
|
609
1124
|
function resolveHits(hits, records) {
|
|
610
1125
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -833,6 +1348,52 @@ function typeRank(record) {
|
|
|
833
1348
|
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
834
1349
|
}
|
|
835
1350
|
|
|
1351
|
+
// src/catalog.ts
|
|
1352
|
+
var EMPTY_STANDINGS = {
|
|
1353
|
+
current: 0,
|
|
1354
|
+
superseded: 0,
|
|
1355
|
+
rejected: 0,
|
|
1356
|
+
unsettled: 0,
|
|
1357
|
+
open: 0
|
|
1358
|
+
};
|
|
1359
|
+
function catalog(bundle, options = {}) {
|
|
1360
|
+
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
1361
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
1362
|
+
conceptId: hit.record.conceptId,
|
|
1363
|
+
type: hit.record.frontmatter.type,
|
|
1364
|
+
title: hit.record.frontmatter.title ?? null,
|
|
1365
|
+
standing: hit.standing,
|
|
1366
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
1367
|
+
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
1368
|
+
})).sort(byTypeThenTitle);
|
|
1369
|
+
const standings = { ...EMPTY_STANDINGS };
|
|
1370
|
+
for (const entry of entries) standings[entry.standing] += 1;
|
|
1371
|
+
return {
|
|
1372
|
+
entries,
|
|
1373
|
+
recordCount: entries.length,
|
|
1374
|
+
standings,
|
|
1375
|
+
currentCount: standings.current,
|
|
1376
|
+
supersededCount: standings.superseded,
|
|
1377
|
+
staleCount: entries.filter((entry) => entry.stale).length
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function byTypeThenTitle(left, right) {
|
|
1381
|
+
return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
|
|
1382
|
+
}
|
|
1383
|
+
function byCodeUnit(left, right) {
|
|
1384
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1385
|
+
}
|
|
1386
|
+
function renderCatalogLine(entry) {
|
|
1387
|
+
const parts = [
|
|
1388
|
+
entry.conceptId,
|
|
1389
|
+
entry.type,
|
|
1390
|
+
entry.title ?? "(untitled)",
|
|
1391
|
+
entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
|
|
1392
|
+
];
|
|
1393
|
+
if (entry.stale) parts.push("stale");
|
|
1394
|
+
return `- ${parts.join(" \xB7 ")}`;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
836
1397
|
// src/kb-gitattributes.ts
|
|
837
1398
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
838
1399
|
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
|
|
@@ -858,7 +1419,7 @@ function appendUnionMergeLine(contents) {
|
|
|
858
1419
|
}
|
|
859
1420
|
|
|
860
1421
|
// src/kb-store.ts
|
|
861
|
-
var KB_DIR = (0,
|
|
1422
|
+
var KB_DIR = (0, import_node_path3.join)(".strauss", "kb");
|
|
862
1423
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
863
1424
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
864
1425
|
var KbStore = class {
|
|
@@ -889,7 +1450,7 @@ var KbStore = class {
|
|
|
889
1450
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
890
1451
|
const root = this.root(bundlePath2);
|
|
891
1452
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
892
|
-
await (0,
|
|
1453
|
+
await (0, import_promises3.mkdir)(root, { recursive: true });
|
|
893
1454
|
await this.publish(
|
|
894
1455
|
target,
|
|
895
1456
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -928,7 +1489,7 @@ var KbStore = class {
|
|
|
928
1489
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
929
1490
|
let raw;
|
|
930
1491
|
try {
|
|
931
|
-
raw = await (0,
|
|
1492
|
+
raw = await (0, import_promises3.readFile)(target, "utf8");
|
|
932
1493
|
} catch {
|
|
933
1494
|
return null;
|
|
934
1495
|
}
|
|
@@ -945,15 +1506,15 @@ var KbStore = class {
|
|
|
945
1506
|
const root = this.root(bundlePath2);
|
|
946
1507
|
let names;
|
|
947
1508
|
try {
|
|
948
|
-
names = await (0,
|
|
1509
|
+
names = await (0, import_promises3.readdir)(root);
|
|
949
1510
|
} catch {
|
|
950
1511
|
return [];
|
|
951
1512
|
}
|
|
952
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}.`));
|
|
953
|
-
const records = await
|
|
954
|
-
wanted
|
|
955
|
-
|
|
956
|
-
)
|
|
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"))
|
|
957
1518
|
);
|
|
958
1519
|
return records.filter((record) => record !== null);
|
|
959
1520
|
}
|
|
@@ -975,6 +1536,21 @@ var KbStore = class {
|
|
|
975
1536
|
{ operation: `status:${status}`, by: actor }
|
|
976
1537
|
);
|
|
977
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
|
+
}
|
|
978
1554
|
/**
|
|
979
1555
|
* Appends one `verified[]` event: who checked the record, when, and what the
|
|
980
1556
|
* check found. Append-only — prior events are history, and are spread into
|
|
@@ -1073,9 +1649,12 @@ ${answer}
|
|
|
1073
1649
|
const bundle = await this.list(bundlePath2);
|
|
1074
1650
|
const needle = text.trim();
|
|
1075
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;
|
|
1076
1653
|
const adjudicated = adjudicate(
|
|
1077
|
-
|
|
1078
|
-
bundle
|
|
1654
|
+
narrowed,
|
|
1655
|
+
bundle,
|
|
1656
|
+
/* @__PURE__ */ new Date(),
|
|
1657
|
+
await this.detectDrift(narrowed, options.repoRoot)
|
|
1079
1658
|
);
|
|
1080
1659
|
if (options.includeNonCurrent) return adjudicated;
|
|
1081
1660
|
const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
|
|
@@ -1094,6 +1673,50 @@ ${answer}
|
|
|
1094
1673
|
const lowered = needle.toLowerCase();
|
|
1095
1674
|
return bundle.filter((record) => matches(record, lowered));
|
|
1096
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
|
+
}
|
|
1097
1720
|
/**
|
|
1098
1721
|
* The whole base, adjudicated, when it is small enough to hand over.
|
|
1099
1722
|
*
|
|
@@ -1113,15 +1736,27 @@ ${answer}
|
|
|
1113
1736
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
1114
1737
|
* was never decided" from a slice it did not know was a slice.
|
|
1115
1738
|
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1739
|
+
* A token budget decides that, measured over what is actually handed back.
|
|
1740
|
+
* The refusal names the estimate and the budget, because a caller told only
|
|
1741
|
+
* "too big" cannot tell whether to narrow the type filter, raise the budget,
|
|
1742
|
+
* or stop loading the base whole altogether. Past the budget the answer is
|
|
1743
|
+
* the catalog and then a pack, which is what the refusal says.
|
|
1744
|
+
*
|
|
1745
|
+
* That refusal is the default guardrail. `all` bypasses the budget outright
|
|
1746
|
+
* and always hands back the whole bundle: an explicit, never-accidental
|
|
1747
|
+
* escape hatch for an operator who has the budget to spend, not a wider
|
|
1748
|
+
* default.
|
|
1119
1749
|
*/
|
|
1120
1750
|
async load(bundlePath2, options = {}) {
|
|
1121
1751
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
1122
1752
|
const bundle = await this.list(bundlePath2);
|
|
1123
1753
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
1124
|
-
const adjudicated = adjudicate(
|
|
1754
|
+
const adjudicated = adjudicate(
|
|
1755
|
+
wanted,
|
|
1756
|
+
bundle,
|
|
1757
|
+
/* @__PURE__ */ new Date(),
|
|
1758
|
+
await this.detectDrift(wanted, options.repoRoot)
|
|
1759
|
+
);
|
|
1125
1760
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
1126
1761
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
1127
1762
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
@@ -1130,7 +1765,12 @@ ${answer}
|
|
|
1130
1765
|
loaded: false,
|
|
1131
1766
|
recordCount: wanted.length,
|
|
1132
1767
|
approxTokens: approxTokens2,
|
|
1133
|
-
budgetTokens
|
|
1768
|
+
budgetTokens,
|
|
1769
|
+
message: refusalMessage({
|
|
1770
|
+
approxTokens: approxTokens2,
|
|
1771
|
+
budgetTokens,
|
|
1772
|
+
type: options.type
|
|
1773
|
+
})
|
|
1134
1774
|
};
|
|
1135
1775
|
}
|
|
1136
1776
|
return {
|
|
@@ -1146,6 +1786,10 @@ ${answer}
|
|
|
1146
1786
|
async trace(bundlePath2, seedId, options = {}) {
|
|
1147
1787
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
1148
1788
|
}
|
|
1789
|
+
/** Every record named in one line each. See `catalog.ts`. */
|
|
1790
|
+
async catalog(bundlePath2, options = {}) {
|
|
1791
|
+
return catalog(await this.list(bundlePath2), options);
|
|
1792
|
+
}
|
|
1149
1793
|
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
1150
1794
|
async pack(bundlePath2, rootId, options = {}) {
|
|
1151
1795
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
@@ -1160,11 +1804,11 @@ ${answer}
|
|
|
1160
1804
|
async readIndex(bundlePath2) {
|
|
1161
1805
|
const root = this.root(bundlePath2);
|
|
1162
1806
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
1163
|
-
const stored = await (0,
|
|
1807
|
+
const stored = await (0, import_promises3.readFile)((0, import_node_path3.join)(root, INDEX_FILE), "utf8").catch(
|
|
1164
1808
|
() => null
|
|
1165
1809
|
);
|
|
1166
1810
|
if (indexIsStale(stored, expected)) {
|
|
1167
|
-
await this.publish((0,
|
|
1811
|
+
await this.publish((0, import_node_path3.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
1168
1812
|
this.logger.info?.({
|
|
1169
1813
|
operation: "kb.index.repair",
|
|
1170
1814
|
bundlePath: root,
|
|
@@ -1181,8 +1825,8 @@ ${answer}
|
|
|
1181
1825
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
1182
1826
|
*/
|
|
1183
1827
|
async readLog(bundlePath2) {
|
|
1184
|
-
const raw = await (0,
|
|
1185
|
-
(0,
|
|
1828
|
+
const raw = await (0, import_promises3.readFile)(
|
|
1829
|
+
(0, import_node_path3.join)(this.root(bundlePath2), LOG_FILE),
|
|
1186
1830
|
"utf8"
|
|
1187
1831
|
).catch(() => "");
|
|
1188
1832
|
const result = parseLog(raw);
|
|
@@ -1233,14 +1877,14 @@ ${answer}
|
|
|
1233
1877
|
}
|
|
1234
1878
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
1235
1879
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
1236
|
-
const before = await (0,
|
|
1880
|
+
const before = await (0, import_promises3.readFile)(target, "utf8").catch(() => null);
|
|
1237
1881
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
1238
1882
|
const parsed = this.parse(conceptId2, before);
|
|
1239
1883
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
1240
1884
|
const frontmatter = change(parsed.frontmatter);
|
|
1241
1885
|
const body = changeBody(parsed.body);
|
|
1242
1886
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
1243
|
-
const witness = await (0,
|
|
1887
|
+
const witness = await (0, import_promises3.readFile)(target, "utf8").catch(() => null);
|
|
1244
1888
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
1245
1889
|
throw new KbWriteConflictError(conceptId2);
|
|
1246
1890
|
}
|
|
@@ -1266,20 +1910,20 @@ ${answer}
|
|
|
1266
1910
|
*/
|
|
1267
1911
|
async publish(target, contents, overwrite, conceptId2) {
|
|
1268
1912
|
const staging = `${target}.${process.pid}.tmp`;
|
|
1269
|
-
await (0,
|
|
1913
|
+
await (0, import_promises3.writeFile)(staging, contents, "utf8");
|
|
1270
1914
|
try {
|
|
1271
1915
|
if (overwrite) {
|
|
1272
|
-
await (0,
|
|
1916
|
+
await (0, import_promises3.rename)(staging, target);
|
|
1273
1917
|
return;
|
|
1274
1918
|
}
|
|
1275
|
-
await (0,
|
|
1919
|
+
await (0, import_promises3.link)(staging, target);
|
|
1276
1920
|
} catch (error) {
|
|
1277
1921
|
if (error.code === "EEXIST") {
|
|
1278
1922
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
1279
1923
|
}
|
|
1280
1924
|
throw error;
|
|
1281
1925
|
} finally {
|
|
1282
|
-
await (0,
|
|
1926
|
+
await (0, import_promises3.unlink)(staging).catch(() => void 0);
|
|
1283
1927
|
}
|
|
1284
1928
|
}
|
|
1285
1929
|
/**
|
|
@@ -1323,20 +1967,30 @@ ${answer}
|
|
|
1323
1967
|
* file must not fail the mutation it guards.
|
|
1324
1968
|
*/
|
|
1325
1969
|
async ensureGitattributes(root) {
|
|
1326
|
-
const target = (0,
|
|
1970
|
+
const target = (0, import_node_path3.join)(root, GITATTRIBUTES_FILE);
|
|
1327
1971
|
try {
|
|
1328
1972
|
let existing;
|
|
1329
1973
|
try {
|
|
1330
|
-
existing = await (0,
|
|
1974
|
+
existing = await (0, import_promises3.readFile)(target, "utf8");
|
|
1331
1975
|
} catch (error) {
|
|
1332
1976
|
if (error.code !== "ENOENT") throw error;
|
|
1333
1977
|
existing = null;
|
|
1334
1978
|
}
|
|
1335
1979
|
if (existing === null) {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1980
|
+
try {
|
|
1981
|
+
await (0, import_promises3.writeFile)(target, appendUnionMergeLine(""), {
|
|
1982
|
+
encoding: "utf8",
|
|
1983
|
+
flag: "wx"
|
|
1984
|
+
});
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
if (error.code !== "EEXIST") throw error;
|
|
1987
|
+
this.logger.info?.({
|
|
1988
|
+
operation: "kb.gitattributes.ensure",
|
|
1989
|
+
bundlePath: root,
|
|
1990
|
+
outcome: "exists"
|
|
1991
|
+
});
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1340
1994
|
this.logger.info?.({
|
|
1341
1995
|
operation: "kb.gitattributes.ensure",
|
|
1342
1996
|
bundlePath: root,
|
|
@@ -1345,7 +1999,7 @@ ${answer}
|
|
|
1345
1999
|
return;
|
|
1346
2000
|
}
|
|
1347
2001
|
if (!hasMergeDeclaration(existing)) {
|
|
1348
|
-
await (0,
|
|
2002
|
+
await (0, import_promises3.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
1349
2003
|
this.logger.info?.({
|
|
1350
2004
|
operation: "kb.gitattributes.ensure",
|
|
1351
2005
|
bundlePath: root,
|
|
@@ -1364,7 +2018,7 @@ ${answer}
|
|
|
1364
2018
|
async record(root, entry) {
|
|
1365
2019
|
await this.ensureGitattributes(root);
|
|
1366
2020
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
1367
|
-
await (0,
|
|
2021
|
+
await (0, import_promises3.appendFile)((0, import_node_path3.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
1368
2022
|
this.logger.warn?.({
|
|
1369
2023
|
operation: "kb.log.append",
|
|
1370
2024
|
outcome: "failed",
|
|
@@ -1390,18 +2044,18 @@ ${answer}
|
|
|
1390
2044
|
};
|
|
1391
2045
|
}
|
|
1392
2046
|
root(bundlePath2) {
|
|
1393
|
-
return (0,
|
|
2047
|
+
return (0, import_node_path3.resolve)(bundlePath2);
|
|
1394
2048
|
}
|
|
1395
2049
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
1396
2050
|
// bundle root; anything carrying a separator would escape it.
|
|
1397
2051
|
recordPath(bundlePath2, conceptId2) {
|
|
1398
|
-
if (conceptId2.includes(
|
|
2052
|
+
if (conceptId2.includes(import_node_path3.sep) || conceptId2.includes("/")) {
|
|
1399
2053
|
throw new KbInvalidConceptIdError(
|
|
1400
2054
|
"concept id must not contain a path separator",
|
|
1401
2055
|
{ conceptId: conceptId2 }
|
|
1402
2056
|
);
|
|
1403
2057
|
}
|
|
1404
|
-
return (0,
|
|
2058
|
+
return (0, import_node_path3.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
1405
2059
|
}
|
|
1406
2060
|
};
|
|
1407
2061
|
function estimateTokens(record) {
|
|
@@ -1412,6 +2066,14 @@ function estimateTokens(record) {
|
|
|
1412
2066
|
function estimateStubTokens(entry) {
|
|
1413
2067
|
return Math.ceil(JSON.stringify(entry).length / 4);
|
|
1414
2068
|
}
|
|
2069
|
+
function refusalMessage(refusal) {
|
|
2070
|
+
const scope = refusal.type ? ` of type ${refusal.type}` : "";
|
|
2071
|
+
return [
|
|
2072
|
+
`Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
|
|
2073
|
+
`Call kb_catalog for one line per record${scope} (id, type, title, standing), then kb_pack on the record that matters; kb_query works for a lookup by wording.`,
|
|
2074
|
+
`To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
|
|
2075
|
+
].join(" ");
|
|
2076
|
+
}
|
|
1415
2077
|
function stub(hit) {
|
|
1416
2078
|
return {
|
|
1417
2079
|
conceptId: hit.record.conceptId,
|
|
@@ -1432,7 +2094,7 @@ function normalizeActor(id) {
|
|
|
1432
2094
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
1433
2095
|
}
|
|
1434
2096
|
function digest(contents) {
|
|
1435
|
-
return (0,
|
|
2097
|
+
return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
|
|
1436
2098
|
}
|
|
1437
2099
|
|
|
1438
2100
|
// src/record-types.ts
|
|
@@ -1643,18 +2305,18 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1643
2305
|
};
|
|
1644
2306
|
|
|
1645
2307
|
// src/kb-pins/frozen.ts
|
|
1646
|
-
var
|
|
2308
|
+
var import_node_path6 = require("path");
|
|
1647
2309
|
|
|
1648
2310
|
// src/kb-pins/layers.ts
|
|
1649
|
-
var
|
|
2311
|
+
var import_promises4 = require("fs/promises");
|
|
1650
2312
|
var import_node_os = require("os");
|
|
1651
|
-
var
|
|
2313
|
+
var import_node_path5 = require("path");
|
|
1652
2314
|
|
|
1653
2315
|
// src/kb-pins/model.ts
|
|
1654
|
-
var
|
|
2316
|
+
var import_node_path4 = require("path");
|
|
1655
2317
|
var import_zod4 = require("zod");
|
|
1656
|
-
var PINS_FILE = (0,
|
|
1657
|
-
var PINS_LOCAL_FILE = (0,
|
|
2318
|
+
var PINS_FILE = (0, import_node_path4.join)(".strauss", "kb-pins.json");
|
|
2319
|
+
var PINS_LOCAL_FILE = (0, import_node_path4.join)(".strauss", "kb-pins.local.json");
|
|
1658
2320
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1659
2321
|
var pinSchema = import_zod4.z.object({
|
|
1660
2322
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -1704,10 +2366,10 @@ function userRoot() {
|
|
|
1704
2366
|
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os.homedir)();
|
|
1705
2367
|
}
|
|
1706
2368
|
function layerRoot(workspaceDir, layer) {
|
|
1707
|
-
return layer === "user" ? userRoot() : (0,
|
|
2369
|
+
return layer === "user" ? userRoot() : (0, import_node_path5.resolve)(workspaceDir);
|
|
1708
2370
|
}
|
|
1709
2371
|
function layerFile(workspaceDir, layer) {
|
|
1710
|
-
return (0,
|
|
2372
|
+
return (0, import_node_path5.join)(
|
|
1711
2373
|
layerRoot(workspaceDir, layer),
|
|
1712
2374
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1713
2375
|
);
|
|
@@ -1716,7 +2378,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1716
2378
|
const file = layerFile(workspaceDir, layer);
|
|
1717
2379
|
let raw;
|
|
1718
2380
|
try {
|
|
1719
|
-
raw = await (0,
|
|
2381
|
+
raw = await (0, import_promises4.readFile)(file, "utf8");
|
|
1720
2382
|
} catch {
|
|
1721
2383
|
return { pins: [] };
|
|
1722
2384
|
}
|
|
@@ -1740,16 +2402,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1740
2402
|
}
|
|
1741
2403
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1742
2404
|
const file = layerFile(workspaceDir, layer);
|
|
1743
|
-
await (0,
|
|
1744
|
-
await (0,
|
|
2405
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(file), { recursive: true });
|
|
2406
|
+
await (0, import_promises4.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
1745
2407
|
`, "utf8");
|
|
1746
2408
|
}
|
|
1747
2409
|
function resolvePinPath(rootDir, path) {
|
|
1748
|
-
return (0,
|
|
2410
|
+
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));
|
|
1749
2411
|
}
|
|
1750
2412
|
function storablePath(rootDir, bundlePath2) {
|
|
1751
|
-
const rel = (0,
|
|
1752
|
-
return (rel === "" ? "." : rel).split(
|
|
2413
|
+
const rel = (0, import_node_path5.relative)((0, import_node_path5.resolve)(rootDir), (0, import_node_path5.resolve)(bundlePath2));
|
|
2414
|
+
return (rel === "" ? "." : rel).split(import_node_path5.sep).join("/");
|
|
1753
2415
|
}
|
|
1754
2416
|
async function readMergedPins(workspaceDir) {
|
|
1755
2417
|
const manifests = {};
|
|
@@ -1777,7 +2439,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
1777
2439
|
// src/kb-pins/frozen.ts
|
|
1778
2440
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
1779
2441
|
const merged = await readMergedPins(workspaceDir);
|
|
1780
|
-
const absolute = (0,
|
|
2442
|
+
const absolute = (0, import_node_path6.resolve)(bundlePath2);
|
|
1781
2443
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
1782
2444
|
if (pin?.frozen === true) {
|
|
1783
2445
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -1862,7 +2524,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1862
2524
|
}
|
|
1863
2525
|
|
|
1864
2526
|
// src/kb-pins/unpin.ts
|
|
1865
|
-
var
|
|
2527
|
+
var import_node_path7 = require("path");
|
|
1866
2528
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1867
2529
|
const layers = [];
|
|
1868
2530
|
for (const layer of PIN_LAYERS) {
|
|
@@ -1883,14 +2545,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
1883
2545
|
}
|
|
1884
2546
|
}
|
|
1885
2547
|
return {
|
|
1886
|
-
path: storablePath((0,
|
|
2548
|
+
path: storablePath((0, import_node_path7.resolve)(workspaceDir), bundlePath2),
|
|
1887
2549
|
removed: layers.length > 0,
|
|
1888
2550
|
layers
|
|
1889
2551
|
};
|
|
1890
2552
|
}
|
|
1891
2553
|
|
|
1892
2554
|
// src/kb-context.ts
|
|
1893
|
-
var
|
|
2555
|
+
var import_promises5 = require("fs/promises");
|
|
1894
2556
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
1895
2557
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
1896
2558
|
var CONTEXT_PROFILES = {
|
|
@@ -2094,13 +2756,13 @@ function toHookJson(block, event) {
|
|
|
2094
2756
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2095
2757
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2096
2758
|
async function syncInstructions(file, block) {
|
|
2097
|
-
const existing = await (0,
|
|
2759
|
+
const existing = await (0, import_promises5.readFile)(file, "utf8").catch(() => null);
|
|
2098
2760
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2099
2761
|
${block.trim()}
|
|
2100
2762
|
${CONTEXT_END}` : null;
|
|
2101
2763
|
if (existing === null) {
|
|
2102
2764
|
if (!region) return { file, action: "unchanged" };
|
|
2103
|
-
await (0,
|
|
2765
|
+
await (0, import_promises5.writeFile)(file, `${region}
|
|
2104
2766
|
`, "utf8");
|
|
2105
2767
|
return { file, action: "created" };
|
|
2106
2768
|
}
|
|
@@ -2111,11 +2773,11 @@ ${CONTEXT_END}` : null;
|
|
|
2111
2773
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2112
2774
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2113
2775
|
if (next === existing) return { file, action: "unchanged" };
|
|
2114
|
-
await (0,
|
|
2776
|
+
await (0, import_promises5.writeFile)(file, next, "utf8");
|
|
2115
2777
|
return { file, action: region ? "replaced" : "removed" };
|
|
2116
2778
|
}
|
|
2117
2779
|
if (!region) return { file, action: "unchanged" };
|
|
2118
|
-
await (0,
|
|
2780
|
+
await (0, import_promises5.writeFile)(
|
|
2119
2781
|
file,
|
|
2120
2782
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2121
2783
|
`,
|
|
@@ -2262,7 +2924,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
2262
2924
|
"aging",
|
|
2263
2925
|
"orphaned",
|
|
2264
2926
|
"broken-supersession",
|
|
2265
|
-
"superseded-but-cited"
|
|
2927
|
+
"superseded-but-cited",
|
|
2928
|
+
"drifted"
|
|
2266
2929
|
];
|
|
2267
2930
|
var CHECK_HEADLINES = {
|
|
2268
2931
|
expired: "past its stale_after date",
|
|
@@ -2271,7 +2934,8 @@ var CHECK_HEADLINES = {
|
|
|
2271
2934
|
aging: "still open or still proposed long after it was written",
|
|
2272
2935
|
orphaned: "no other record links to it",
|
|
2273
2936
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
2274
|
-
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
2937
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
2938
|
+
drifted: "the code an anchor points at moved out from under its hash"
|
|
2275
2939
|
};
|
|
2276
2940
|
var DAY_MS = 864e5;
|
|
2277
2941
|
function doctor(bundle, options = {}) {
|
|
@@ -2281,7 +2945,7 @@ function doctor(bundle, options = {}) {
|
|
|
2281
2945
|
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
2282
2946
|
};
|
|
2283
2947
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2284
|
-
const adjudicated = adjudicate(bundle, bundle, now);
|
|
2948
|
+
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
2285
2949
|
const standings = new Map(
|
|
2286
2950
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
2287
2951
|
);
|
|
@@ -2295,7 +2959,8 @@ function doctor(bundle, options = {}) {
|
|
|
2295
2959
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
2296
2960
|
group("orphaned", orphaned(bundle)),
|
|
2297
2961
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
2298
|
-
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
2962
|
+
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
2963
|
+
group("drifted", drifted(inForce))
|
|
2299
2964
|
];
|
|
2300
2965
|
const counts = Object.fromEntries(
|
|
2301
2966
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -2481,6 +3146,29 @@ function supersededButCited(bundle, standings) {
|
|
|
2481
3146
|
}
|
|
2482
3147
|
return findings;
|
|
2483
3148
|
}
|
|
3149
|
+
function drifted(hits) {
|
|
3150
|
+
const findings = [];
|
|
3151
|
+
for (const hit of hits) {
|
|
3152
|
+
const warning = hit.warnings.find((entry) => entry.kind === "drifted");
|
|
3153
|
+
if (!warning) continue;
|
|
3154
|
+
findings.push(
|
|
3155
|
+
finding(
|
|
3156
|
+
hit.record,
|
|
3157
|
+
`${warning.anchors.length} ${warning.anchors.length === 1 ? "anchor no longer matches" : "anchors no longer match"}: ${warning.anchors.map((anchor) => {
|
|
3158
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3159
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
3160
|
+
if (anchor.diffSize === null) {
|
|
3161
|
+
return `${at} (changed, size unrecorded)`;
|
|
3162
|
+
}
|
|
3163
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3164
|
+
}).join(", ")}`
|
|
3165
|
+
)
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
return findings.sort(
|
|
3169
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
2484
3172
|
function replaces(later, earlier) {
|
|
2485
3173
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
2486
3174
|
}
|
|
@@ -2550,28 +3238,204 @@ function selectDecisions(records) {
|
|
|
2550
3238
|
);
|
|
2551
3239
|
}
|
|
2552
3240
|
|
|
2553
|
-
// src/commands/
|
|
3241
|
+
// src/commands/anchor-resolve.ts
|
|
2554
3242
|
var import_zod8 = require("zod");
|
|
2555
3243
|
|
|
2556
3244
|
// src/commands/model.ts
|
|
2557
3245
|
var import_zod7 = require("zod");
|
|
2558
3246
|
var bundlePath = import_zod7.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2559
3247
|
var conceptId = import_zod7.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
3248
|
+
var REPO_ROOT = import_zod7.z.string().min(1).optional().describe(
|
|
3249
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
3250
|
+
);
|
|
2560
3251
|
function define(command) {
|
|
2561
3252
|
return command;
|
|
2562
3253
|
}
|
|
2563
3254
|
function argvFlag(argv, name) {
|
|
3255
|
+
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
3256
|
+
if (joined !== void 0) {
|
|
3257
|
+
const value2 = joined.slice(name.length + 1);
|
|
3258
|
+
if (!value2) throw new KbMissingFlagValueError(name);
|
|
3259
|
+
return value2;
|
|
3260
|
+
}
|
|
2564
3261
|
const at = argv.indexOf(name);
|
|
2565
|
-
|
|
3262
|
+
if (at === -1) return void 0;
|
|
3263
|
+
const value = argv[at + 1];
|
|
3264
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
3265
|
+
throw new KbMissingFlagValueError(name);
|
|
3266
|
+
}
|
|
3267
|
+
return value;
|
|
2566
3268
|
}
|
|
2567
3269
|
|
|
3270
|
+
// src/commands/anchor-resolve.ts
|
|
3271
|
+
var anchorResolveCommand = define({
|
|
3272
|
+
name: "anchor-resolve",
|
|
3273
|
+
tool: "kb_anchor_resolve",
|
|
3274
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
3275
|
+
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.",
|
|
3276
|
+
input: import_zod8.z.object({
|
|
3277
|
+
bundlePath,
|
|
3278
|
+
conceptId,
|
|
3279
|
+
repoRoot: import_zod8.z.string().min(1).optional(),
|
|
3280
|
+
rebaseline: import_zod8.z.boolean().optional().describe(
|
|
3281
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
3282
|
+
),
|
|
3283
|
+
restamp: import_zod8.z.boolean().optional().describe(
|
|
3284
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
3285
|
+
)
|
|
3286
|
+
}),
|
|
3287
|
+
fromArgv: (argv, path) => ({
|
|
3288
|
+
bundlePath: path,
|
|
3289
|
+
conceptId: argv[1],
|
|
3290
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3291
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
3292
|
+
restamp: argv.includes("--restamp")
|
|
3293
|
+
}),
|
|
3294
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
3295
|
+
const root = repoRoot ?? process.cwd();
|
|
3296
|
+
const record = await store.read(path, id);
|
|
3297
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
3298
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
3299
|
+
if (!anchors.length) {
|
|
3300
|
+
return {
|
|
3301
|
+
conceptId: id,
|
|
3302
|
+
results: [],
|
|
3303
|
+
verified: false,
|
|
3304
|
+
note: "record has no anchors"
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
const results = [];
|
|
3308
|
+
const updated = [];
|
|
3309
|
+
const origin = new LazyOrigin(root);
|
|
3310
|
+
let dirty = false;
|
|
3311
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
3312
|
+
const foreign = new Map(
|
|
3313
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
3314
|
+
);
|
|
3315
|
+
const reads = await readAnchorFiles(
|
|
3316
|
+
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
3317
|
+
anchorFileReader(root)
|
|
3318
|
+
);
|
|
3319
|
+
for (const anchor of anchors) {
|
|
3320
|
+
const base = {
|
|
3321
|
+
file: anchor.file,
|
|
3322
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3323
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
3324
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
3325
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
3326
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
3327
|
+
};
|
|
3328
|
+
if (foreign.get(anchor)) {
|
|
3329
|
+
results.push({ ...base, state: "unresolved", reason: "foreign-repo" });
|
|
3330
|
+
updated.push(anchor);
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
const fileRead = reads.get(anchor.file);
|
|
3334
|
+
if (!fileRead.ok) {
|
|
3335
|
+
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
3336
|
+
updated.push(anchor);
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
3340
|
+
if (!resolved) {
|
|
3341
|
+
results.push({
|
|
3342
|
+
...base,
|
|
3343
|
+
state: "unresolved",
|
|
3344
|
+
reason: "symbol-not-found"
|
|
3345
|
+
});
|
|
3346
|
+
updated.push(anchor);
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
3350
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3351
|
+
const stamped = {
|
|
3352
|
+
...anchor,
|
|
3353
|
+
hash: currentHash,
|
|
3354
|
+
lines: currentLines,
|
|
3355
|
+
resolved_at: now()
|
|
3356
|
+
};
|
|
3357
|
+
if (!anchor.hash) {
|
|
3358
|
+
results.push({ ...base, state: "stamped", currentHash });
|
|
3359
|
+
updated.push(stamped);
|
|
3360
|
+
dirty = true;
|
|
3361
|
+
} else if (anchor.hash === currentHash) {
|
|
3362
|
+
results.push({
|
|
3363
|
+
...base,
|
|
3364
|
+
state: "match",
|
|
3365
|
+
currentHash
|
|
3366
|
+
});
|
|
3367
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
3368
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
3369
|
+
if (refresh) dirty = true;
|
|
3370
|
+
} else {
|
|
3371
|
+
results.push({
|
|
3372
|
+
...base,
|
|
3373
|
+
state: "drifted",
|
|
3374
|
+
currentHash,
|
|
3375
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines),
|
|
3376
|
+
...rebaseline ? { rebaselined: true } : {}
|
|
3377
|
+
});
|
|
3378
|
+
updated.push(rebaseline ? stamped : anchor);
|
|
3379
|
+
if (rebaseline) dirty = true;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
let frozen = false;
|
|
3383
|
+
if (dirty) {
|
|
3384
|
+
try {
|
|
3385
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
3388
|
+
frozen = true;
|
|
3389
|
+
}
|
|
3390
|
+
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
3391
|
+
}
|
|
3392
|
+
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
3393
|
+
const checked = results.filter((entry) => entry.reason !== "foreign-repo");
|
|
3394
|
+
const skipped = results.length - checked.length;
|
|
3395
|
+
const matches2 = checked.filter((entry) => entry.state === "match").length;
|
|
3396
|
+
const clean = checked.length > 0 && checked.every((entry) => entry.state === "match");
|
|
3397
|
+
if (clean) {
|
|
3398
|
+
try {
|
|
3399
|
+
await store.verify(
|
|
3400
|
+
path,
|
|
3401
|
+
id,
|
|
3402
|
+
`anchor-resolve: ${matches2}/${checked.length} anchors match${skipped ? `, ${skipped} in another repo` : ""} (regex resolver)`,
|
|
3403
|
+
actor,
|
|
3404
|
+
now()
|
|
3405
|
+
);
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
3408
|
+
return {
|
|
3409
|
+
conceptId: id,
|
|
3410
|
+
results,
|
|
3411
|
+
verified: false,
|
|
3412
|
+
verifyRefused: "self-verification",
|
|
3413
|
+
...frozenNote
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
3417
|
+
}
|
|
3418
|
+
return { conceptId: id, results, verified: false, ...frozenNote };
|
|
3419
|
+
},
|
|
3420
|
+
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
3421
|
+
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
3422
|
+
// let the one edit that destroys an anchor pass the gate that exists to
|
|
3423
|
+
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
3424
|
+
// belonging to another repository was never this run's to check — failing CI
|
|
3425
|
+
// on either would gate on work this command did not do.
|
|
3426
|
+
failsWhen: (result) => result.results.some(
|
|
3427
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason !== "foreign-repo"
|
|
3428
|
+
)
|
|
3429
|
+
});
|
|
3430
|
+
|
|
2568
3431
|
// src/commands/answer.ts
|
|
3432
|
+
var import_zod9 = require("zod");
|
|
2569
3433
|
var answerCommand = define({
|
|
2570
3434
|
name: "answer",
|
|
2571
3435
|
tool: "kb_answer",
|
|
2572
3436
|
usage: "answer <concept-id> <answer...>",
|
|
2573
3437
|
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.",
|
|
2574
|
-
input:
|
|
3438
|
+
input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
|
|
2575
3439
|
fromArgv: (argv, path) => ({
|
|
2576
3440
|
bundlePath: path,
|
|
2577
3441
|
conceptId: argv[1],
|
|
@@ -2584,27 +3448,90 @@ var answerCommand = define({
|
|
|
2584
3448
|
}
|
|
2585
3449
|
});
|
|
2586
3450
|
|
|
3451
|
+
// src/commands/catalog.ts
|
|
3452
|
+
var import_zod10 = require("zod");
|
|
3453
|
+
var catalogCommand = define({
|
|
3454
|
+
name: "catalog",
|
|
3455
|
+
tool: "kb_catalog",
|
|
3456
|
+
usage: "catalog [type]",
|
|
3457
|
+
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.",
|
|
3458
|
+
input: import_zod10.z.object({
|
|
3459
|
+
bundlePath,
|
|
3460
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
3461
|
+
}),
|
|
3462
|
+
fromArgv: (argv, path) => ({
|
|
3463
|
+
bundlePath: path,
|
|
3464
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
3465
|
+
}),
|
|
3466
|
+
run: async ({ store }, { bundlePath: path, type }) => render(
|
|
3467
|
+
await store.catalog(path, { ...type ? { type } : {} }),
|
|
3468
|
+
path,
|
|
3469
|
+
type
|
|
3470
|
+
)
|
|
3471
|
+
});
|
|
3472
|
+
function render(result, bundle, type) {
|
|
3473
|
+
const lines = [
|
|
3474
|
+
`# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
|
|
3475
|
+
`bundle: ${bundle}`,
|
|
3476
|
+
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
3477
|
+
];
|
|
3478
|
+
if (result.staleCount) {
|
|
3479
|
+
lines.push(
|
|
3480
|
+
`${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
lines.push("");
|
|
3484
|
+
if (!result.entries.length) {
|
|
3485
|
+
lines.push(
|
|
3486
|
+
type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
|
|
3487
|
+
);
|
|
3488
|
+
} else {
|
|
3489
|
+
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
3490
|
+
}
|
|
3491
|
+
lines.push(
|
|
3492
|
+
"",
|
|
3493
|
+
"Bodies are not here: kb_pack <conceptId> for the neighbourhood around one record, kb_load for the whole base when it fits the budget, kb_query for a lookup by wording, kb_trace <conceptId> for how a position was arrived at."
|
|
3494
|
+
);
|
|
3495
|
+
return lines.join("\n");
|
|
3496
|
+
}
|
|
3497
|
+
function standingCounts(result) {
|
|
3498
|
+
const ORDER = [
|
|
3499
|
+
"current",
|
|
3500
|
+
"open",
|
|
3501
|
+
"unsettled",
|
|
3502
|
+
"rejected",
|
|
3503
|
+
"superseded"
|
|
3504
|
+
];
|
|
3505
|
+
const parts = ORDER.filter((standing) => result.standings[standing]).map(
|
|
3506
|
+
(standing) => `${result.standings[standing]} ${standing}`
|
|
3507
|
+
);
|
|
3508
|
+
return parts.length ? parts.join(" \xB7 ") : "none";
|
|
3509
|
+
}
|
|
3510
|
+
function count(value, noun) {
|
|
3511
|
+
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
2587
3514
|
// src/commands/context.ts
|
|
2588
|
-
var
|
|
3515
|
+
var import_zod11 = require("zod");
|
|
2589
3516
|
var contextCommand = define({
|
|
2590
3517
|
name: "context",
|
|
2591
3518
|
tool: "kb_context",
|
|
2592
3519
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2593
3520
|
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.",
|
|
2594
|
-
input:
|
|
2595
|
-
budgetTokens:
|
|
3521
|
+
input: import_zod11.z.object({
|
|
3522
|
+
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2596
3523
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2597
3524
|
),
|
|
2598
|
-
fullUnderTokens:
|
|
3525
|
+
fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2599
3526
|
"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."
|
|
2600
3527
|
),
|
|
2601
|
-
profile:
|
|
3528
|
+
profile: import_zod11.z.string().optional().describe(
|
|
2602
3529
|
"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."
|
|
2603
3530
|
),
|
|
2604
|
-
format:
|
|
3531
|
+
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
2605
3532
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2606
3533
|
),
|
|
2607
|
-
event:
|
|
3534
|
+
event: import_zod11.z.string().optional().describe(
|
|
2608
3535
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2609
3536
|
)
|
|
2610
3537
|
}),
|
|
@@ -2640,15 +3567,16 @@ var contextCommand = define({
|
|
|
2640
3567
|
});
|
|
2641
3568
|
|
|
2642
3569
|
// src/commands/doctor.ts
|
|
2643
|
-
var
|
|
2644
|
-
var days = (what, fallback) =>
|
|
3570
|
+
var import_zod12 = require("zod");
|
|
3571
|
+
var days = (what, fallback) => import_zod12.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2645
3572
|
var doctorCommand = define({
|
|
2646
3573
|
name: "doctor",
|
|
2647
3574
|
tool: "kb_doctor",
|
|
2648
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
2649
|
-
description: "
|
|
2650
|
-
input:
|
|
3575
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
3576
|
+
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.",
|
|
3577
|
+
input: import_zod12.z.object({
|
|
2651
3578
|
bundlePath,
|
|
3579
|
+
repoRoot: REPO_ROOT,
|
|
2652
3580
|
expiringDays: days(
|
|
2653
3581
|
"How far ahead `expiring` looks, in days.",
|
|
2654
3582
|
DEFAULT_EXPIRING_DAYS
|
|
@@ -2661,7 +3589,7 @@ var doctorCommand = define({
|
|
|
2661
3589
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2662
3590
|
DEFAULT_AGING_DAYS
|
|
2663
3591
|
),
|
|
2664
|
-
strict:
|
|
3592
|
+
strict: import_zod12.z.boolean().optional().describe(
|
|
2665
3593
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2666
3594
|
)
|
|
2667
3595
|
}),
|
|
@@ -2673,32 +3601,39 @@ var doctorCommand = define({
|
|
|
2673
3601
|
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
2674
3602
|
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
2675
3603
|
const agingDays = argvFlag(argv, "--aging-days");
|
|
3604
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2676
3605
|
return {
|
|
2677
3606
|
bundlePath: path,
|
|
3607
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
2678
3608
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
2679
3609
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
2680
3610
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
2681
3611
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
2682
3612
|
};
|
|
2683
3613
|
},
|
|
2684
|
-
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
3614
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays, repoRoot }) => {
|
|
2685
3615
|
const checkedAt = now();
|
|
2686
|
-
const
|
|
3616
|
+
const records = await store.list(path);
|
|
3617
|
+
const anchorDrift = await store.detectDrift(records, repoRoot);
|
|
3618
|
+
const report = doctor(records, {
|
|
2687
3619
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
2688
3620
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
2689
3621
|
...agingDays !== void 0 ? { agingDays } : {},
|
|
3622
|
+
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
2690
3623
|
now: new Date(checkedAt)
|
|
2691
3624
|
});
|
|
2692
3625
|
return { bundlePath: path, checkedAt, ...report };
|
|
2693
3626
|
},
|
|
2694
|
-
render: (result) =>
|
|
2695
|
-
// Only expiry, and only under --strict. The other
|
|
3627
|
+
render: (result) => render2(result),
|
|
3628
|
+
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
2696
3629
|
// reader decides about; an expired record is the base asserting something it
|
|
2697
3630
|
// already said it would stop standing behind, which is the one finding a
|
|
2698
|
-
// pipeline can act on without a judgment call.
|
|
3631
|
+
// pipeline can act on without a judgment call. Drift has its own gate —
|
|
3632
|
+
// `anchor-resolve` exits non-zero on it, against a repo root the caller
|
|
3633
|
+
// named, which is the run a CI pipeline should be making anyway.
|
|
2699
3634
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
2700
3635
|
});
|
|
2701
|
-
function
|
|
3636
|
+
function render2(result) {
|
|
2702
3637
|
const { thresholds } = result;
|
|
2703
3638
|
const lines = [
|
|
2704
3639
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -2730,13 +3665,13 @@ function render(result) {
|
|
|
2730
3665
|
}
|
|
2731
3666
|
|
|
2732
3667
|
// src/commands/list.ts
|
|
2733
|
-
var
|
|
3668
|
+
var import_zod13 = require("zod");
|
|
2734
3669
|
var listCommand = define({
|
|
2735
3670
|
name: "list",
|
|
2736
3671
|
tool: "kb_list",
|
|
2737
3672
|
usage: "list [type]",
|
|
2738
3673
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
2739
|
-
input:
|
|
3674
|
+
input: import_zod13.z.object({ bundlePath, type: import_zod13.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2740
3675
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2741
3676
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2742
3677
|
conceptId: record.conceptId,
|
|
@@ -2748,36 +3683,40 @@ var listCommand = define({
|
|
|
2748
3683
|
});
|
|
2749
3684
|
|
|
2750
3685
|
// src/commands/load.ts
|
|
2751
|
-
var
|
|
3686
|
+
var import_zod14 = require("zod");
|
|
2752
3687
|
var loadCommand = define({
|
|
2753
3688
|
name: "load",
|
|
2754
3689
|
tool: "kb_load",
|
|
2755
|
-
usage: "load [type] [--budget N | --all]",
|
|
2756
|
-
description: "
|
|
2757
|
-
input:
|
|
3690
|
+
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3691
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs (name, replacement, date); rejected and open records arrive whole. Refuses past the token budget rather than truncating \u2014 call kb_catalog, then kb_pack on the record that matters, or narrow with `type`; kb_query for a lookup by wording. `all` bypasses the budget.",
|
|
3692
|
+
input: import_zod14.z.object({
|
|
2758
3693
|
bundlePath,
|
|
2759
|
-
type:
|
|
2760
|
-
budgetTokens:
|
|
2761
|
-
all:
|
|
2762
|
-
"
|
|
2763
|
-
)
|
|
3694
|
+
type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
|
|
3695
|
+
budgetTokens: import_zod14.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
3696
|
+
all: import_zod14.z.boolean().optional().describe(
|
|
3697
|
+
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3698
|
+
),
|
|
3699
|
+
repoRoot: REPO_ROOT
|
|
2764
3700
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
2765
|
-
message: "all
|
|
3701
|
+
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
2766
3702
|
}),
|
|
2767
3703
|
fromArgv: (argv, path) => {
|
|
2768
3704
|
const budget = argvFlag(argv, "--budget");
|
|
3705
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2769
3706
|
return {
|
|
2770
3707
|
bundlePath: path,
|
|
2771
3708
|
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
2772
3709
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
2773
|
-
...argv.includes("--all") ? { all: true } : {}
|
|
3710
|
+
...argv.includes("--all") ? { all: true } : {},
|
|
3711
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2774
3712
|
};
|
|
2775
3713
|
},
|
|
2776
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
3714
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all, repoRoot }) => {
|
|
2777
3715
|
const result = await store.load(path, {
|
|
2778
3716
|
...type ? { type } : {},
|
|
2779
3717
|
...budgetTokens ? { budgetTokens } : {},
|
|
2780
|
-
...all ? { all } : {}
|
|
3718
|
+
...all ? { all } : {},
|
|
3719
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2781
3720
|
});
|
|
2782
3721
|
if (!result.loaded) return result;
|
|
2783
3722
|
return {
|
|
@@ -2796,25 +3735,25 @@ var loadCommand = define({
|
|
|
2796
3735
|
});
|
|
2797
3736
|
|
|
2798
3737
|
// src/commands/log.ts
|
|
2799
|
-
var
|
|
3738
|
+
var import_zod15 = require("zod");
|
|
2800
3739
|
var logCommand = define({
|
|
2801
3740
|
name: "log",
|
|
2802
3741
|
tool: "kb_log",
|
|
2803
3742
|
usage: "log",
|
|
2804
3743
|
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.",
|
|
2805
|
-
input:
|
|
3744
|
+
input: import_zod15.z.object({ bundlePath }),
|
|
2806
3745
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2807
3746
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2808
3747
|
});
|
|
2809
3748
|
|
|
2810
3749
|
// src/commands/no-decision.ts
|
|
2811
|
-
var
|
|
3750
|
+
var import_zod16 = require("zod");
|
|
2812
3751
|
var noDecisionCommand = define({
|
|
2813
3752
|
name: "no-decision",
|
|
2814
3753
|
tool: "kb_no_decision",
|
|
2815
3754
|
usage: "no-decision <reason...>",
|
|
2816
3755
|
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.',
|
|
2817
|
-
input:
|
|
3756
|
+
input: import_zod16.z.object({ bundlePath, reason: import_zod16.z.string().min(1) }),
|
|
2818
3757
|
fromArgv: (argv, path) => ({
|
|
2819
3758
|
bundlePath: path,
|
|
2820
3759
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2831,20 +3770,20 @@ var noDecisionCommand = define({
|
|
|
2831
3770
|
});
|
|
2832
3771
|
|
|
2833
3772
|
// src/commands/pack.ts
|
|
2834
|
-
var
|
|
3773
|
+
var import_zod17 = require("zod");
|
|
2835
3774
|
var packCommand = define({
|
|
2836
3775
|
name: "pack",
|
|
2837
3776
|
tool: "kb_pack",
|
|
2838
3777
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2839
3778
|
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.",
|
|
2840
|
-
input:
|
|
3779
|
+
input: import_zod17.z.object({
|
|
2841
3780
|
bundlePath,
|
|
2842
3781
|
conceptId,
|
|
2843
|
-
hops:
|
|
2844
|
-
maxNodes:
|
|
3782
|
+
hops: import_zod17.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
3783
|
+
maxNodes: import_zod17.z.number().int().positive().optional().describe(
|
|
2845
3784
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
2846
3785
|
),
|
|
2847
|
-
budgetTokens:
|
|
3786
|
+
budgetTokens: import_zod17.z.number().int().positive().optional().describe(
|
|
2848
3787
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
2849
3788
|
)
|
|
2850
3789
|
}),
|
|
@@ -2866,10 +3805,10 @@ var packCommand = define({
|
|
|
2866
3805
|
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
2867
3806
|
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
2868
3807
|
});
|
|
2869
|
-
return
|
|
3808
|
+
return render3(result, path, now());
|
|
2870
3809
|
}
|
|
2871
3810
|
});
|
|
2872
|
-
function
|
|
3811
|
+
function render3(result, bundle, at) {
|
|
2873
3812
|
const lines = [
|
|
2874
3813
|
`# KB Pack \u2014 ${result.root}`,
|
|
2875
3814
|
`bundle: ${bundle}`,
|
|
@@ -2931,22 +3870,22 @@ function warningLabel(warning) {
|
|
|
2931
3870
|
}
|
|
2932
3871
|
|
|
2933
3872
|
// src/commands/pin.ts
|
|
2934
|
-
var
|
|
3873
|
+
var import_zod18 = require("zod");
|
|
2935
3874
|
var pinCommand = define({
|
|
2936
3875
|
name: "pin",
|
|
2937
3876
|
tool: "kb_pin",
|
|
2938
3877
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2939
3878
|
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.",
|
|
2940
|
-
input:
|
|
3879
|
+
input: import_zod18.z.object({
|
|
2941
3880
|
bundlePath,
|
|
2942
|
-
mode:
|
|
3881
|
+
mode: import_zod18.z.enum(["full", "index"]).optional().describe(
|
|
2943
3882
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2944
3883
|
),
|
|
2945
|
-
profiles:
|
|
2946
|
-
layer:
|
|
3884
|
+
profiles: import_zod18.z.array(import_zod18.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
3885
|
+
layer: import_zod18.z.enum(["project", "local", "user"]).optional().describe(
|
|
2947
3886
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2948
3887
|
),
|
|
2949
|
-
frozen:
|
|
3888
|
+
frozen: import_zod18.z.boolean().optional().describe(
|
|
2950
3889
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2951
3890
|
)
|
|
2952
3891
|
}),
|
|
@@ -2975,38 +3914,48 @@ var pinCommand = define({
|
|
|
2975
3914
|
});
|
|
2976
3915
|
|
|
2977
3916
|
// src/commands/pins.ts
|
|
2978
|
-
var
|
|
3917
|
+
var import_zod19 = require("zod");
|
|
2979
3918
|
var pinsCommand = define({
|
|
2980
3919
|
name: "pins",
|
|
2981
3920
|
tool: "kb_pins",
|
|
2982
3921
|
usage: "pins",
|
|
2983
3922
|
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.",
|
|
2984
|
-
input:
|
|
3923
|
+
input: import_zod19.z.object({}),
|
|
2985
3924
|
fromArgv: () => ({}),
|
|
2986
3925
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2987
3926
|
});
|
|
2988
3927
|
|
|
2989
3928
|
// src/commands/query.ts
|
|
2990
|
-
var
|
|
3929
|
+
var import_zod20 = require("zod");
|
|
2991
3930
|
var queryCommand = define({
|
|
2992
3931
|
name: "query",
|
|
2993
3932
|
tool: "kb_query",
|
|
2994
|
-
usage: "query <text...>",
|
|
2995
|
-
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted.
|
|
2996
|
-
input:
|
|
3933
|
+
usage: "query <text...> [--repo-root PATH]",
|
|
3934
|
+
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. This is the lookup-by-wording rung, and the narrowest of the three: use it when you know roughly what the record says. The decision rule around it \u2014 while the base fits kb_load's token budget, kb_load it whole, because on this package's measurements a reader holding the whole base answered eight of nine questions whose wording appears in no record where embedding search answered four; once kb_load refuses, kb_catalog for one line per record and then kb_pack on the record the work centres on; and kb_query when the question is a point lookup rather than a neighbourhood. A query cannot tell you that nothing was decided \u2014 it returns its nearest hit whatever the distance \u2014 so reach for kb_catalog when the question is what exists. Never read record files directly: this tool (with kb_load, kb_catalog, kb_pack and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
|
|
3935
|
+
input: import_zod20.z.object({
|
|
2997
3936
|
bundlePath,
|
|
2998
|
-
text:
|
|
2999
|
-
type:
|
|
3000
|
-
includeNonCurrent:
|
|
3937
|
+
text: import_zod20.z.string().optional(),
|
|
3938
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
3939
|
+
includeNonCurrent: import_zod20.z.boolean().optional(),
|
|
3940
|
+
repoRoot: REPO_ROOT
|
|
3001
3941
|
}),
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3942
|
+
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
3943
|
+
fromArgv: (argv, path) => {
|
|
3944
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
3945
|
+
const words = argv.slice(1);
|
|
3946
|
+
const flag = words.indexOf("--repo-root");
|
|
3947
|
+
if (flag !== -1) words.splice(flag, 2);
|
|
3948
|
+
return {
|
|
3949
|
+
bundlePath: path,
|
|
3950
|
+
text: words.join(" ").trim(),
|
|
3951
|
+
includeNonCurrent: true,
|
|
3952
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
3953
|
+
};
|
|
3954
|
+
},
|
|
3955
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
3008
3956
|
...type ? { type } : {},
|
|
3009
|
-
includeNonCurrent: includeNonCurrent === true
|
|
3957
|
+
includeNonCurrent: includeNonCurrent === true,
|
|
3958
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
3010
3959
|
})).map((hit) => ({
|
|
3011
3960
|
conceptId: hit.record.conceptId,
|
|
3012
3961
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -3019,40 +3968,40 @@ var queryCommand = define({
|
|
|
3019
3968
|
});
|
|
3020
3969
|
|
|
3021
3970
|
// src/commands/read-index.ts
|
|
3022
|
-
var
|
|
3971
|
+
var import_zod21 = require("zod");
|
|
3023
3972
|
var readIndexCommand = define({
|
|
3024
3973
|
name: "index",
|
|
3025
3974
|
tool: "kb_index",
|
|
3026
3975
|
usage: "index",
|
|
3027
3976
|
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.",
|
|
3028
|
-
input:
|
|
3977
|
+
input: import_zod21.z.object({ bundlePath }),
|
|
3029
3978
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3030
3979
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3031
3980
|
});
|
|
3032
3981
|
|
|
3033
3982
|
// src/commands/schema.ts
|
|
3034
|
-
var
|
|
3983
|
+
var import_zod22 = require("zod");
|
|
3035
3984
|
var schemaCommand = define({
|
|
3036
3985
|
name: "schema",
|
|
3037
3986
|
tool: "kb_schema",
|
|
3038
3987
|
usage: "schema",
|
|
3039
3988
|
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.",
|
|
3040
|
-
input:
|
|
3989
|
+
input: import_zod22.z.object({}),
|
|
3041
3990
|
fromArgv: () => ({}),
|
|
3042
3991
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3043
3992
|
});
|
|
3044
3993
|
|
|
3045
3994
|
// src/commands/status.ts
|
|
3046
|
-
var
|
|
3995
|
+
var import_zod23 = require("zod");
|
|
3047
3996
|
var statusCommand = define({
|
|
3048
3997
|
name: "status",
|
|
3049
3998
|
tool: "kb_status",
|
|
3050
3999
|
usage: "status <concept-id> <status>",
|
|
3051
4000
|
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.",
|
|
3052
|
-
input:
|
|
4001
|
+
input: import_zod23.z.object({
|
|
3053
4002
|
bundlePath,
|
|
3054
4003
|
conceptId,
|
|
3055
|
-
status:
|
|
4004
|
+
status: import_zod23.z.enum(KB_RECORD_STATUSES)
|
|
3056
4005
|
}),
|
|
3057
4006
|
fromArgv: (argv, path) => ({
|
|
3058
4007
|
bundlePath: path,
|
|
@@ -3067,13 +4016,13 @@ var statusCommand = define({
|
|
|
3067
4016
|
});
|
|
3068
4017
|
|
|
3069
4018
|
// src/commands/supersede.ts
|
|
3070
|
-
var
|
|
4019
|
+
var import_zod24 = require("zod");
|
|
3071
4020
|
var supersedeCommand = define({
|
|
3072
4021
|
name: "supersede",
|
|
3073
4022
|
tool: "kb_supersede",
|
|
3074
4023
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3075
4024
|
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.",
|
|
3076
|
-
input:
|
|
4025
|
+
input: import_zod24.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3077
4026
|
fromArgv: (argv, path) => ({
|
|
3078
4027
|
bundlePath: path,
|
|
3079
4028
|
conceptId: argv[1],
|
|
@@ -3087,16 +4036,16 @@ var supersedeCommand = define({
|
|
|
3087
4036
|
});
|
|
3088
4037
|
|
|
3089
4038
|
// src/commands/sync-instructions.ts
|
|
3090
|
-
var
|
|
4039
|
+
var import_zod25 = require("zod");
|
|
3091
4040
|
var syncInstructionsCommand = define({
|
|
3092
4041
|
name: "sync-instructions",
|
|
3093
4042
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3094
4043
|
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.",
|
|
3095
|
-
input:
|
|
3096
|
-
file:
|
|
3097
|
-
budgetTokens:
|
|
3098
|
-
fullUnderTokens:
|
|
3099
|
-
profile:
|
|
4044
|
+
input: import_zod25.z.object({
|
|
4045
|
+
file: import_zod25.z.string().min(1).describe("The instruction file to edit in place."),
|
|
4046
|
+
budgetTokens: import_zod25.z.number().int().positive().optional(),
|
|
4047
|
+
fullUnderTokens: import_zod25.z.number().int().positive().optional(),
|
|
4048
|
+
profile: import_zod25.z.string().optional()
|
|
3100
4049
|
}),
|
|
3101
4050
|
fromArgv: (argv) => {
|
|
3102
4051
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3122,17 +4071,17 @@ var syncInstructionsCommand = define({
|
|
|
3122
4071
|
});
|
|
3123
4072
|
|
|
3124
4073
|
// src/commands/trace.ts
|
|
3125
|
-
var
|
|
4074
|
+
var import_zod26 = require("zod");
|
|
3126
4075
|
var traceCommand = define({
|
|
3127
4076
|
name: "trace",
|
|
3128
4077
|
tool: "kb_trace",
|
|
3129
4078
|
usage: "trace <concept-id> [edges...]",
|
|
3130
4079
|
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.',
|
|
3131
|
-
input:
|
|
4080
|
+
input: import_zod26.z.object({
|
|
3132
4081
|
bundlePath,
|
|
3133
4082
|
conceptId,
|
|
3134
|
-
edges:
|
|
3135
|
-
depth:
|
|
4083
|
+
edges: import_zod26.z.array(import_zod26.z.enum(TRACE_EDGES)).optional(),
|
|
4084
|
+
depth: import_zod26.z.number().int().positive().optional()
|
|
3136
4085
|
}),
|
|
3137
4086
|
fromArgv: (argv, path) => ({
|
|
3138
4087
|
bundlePath: path,
|
|
@@ -3154,53 +4103,53 @@ var traceCommand = define({
|
|
|
3154
4103
|
});
|
|
3155
4104
|
|
|
3156
4105
|
// src/commands/types.ts
|
|
3157
|
-
var
|
|
4106
|
+
var import_zod27 = require("zod");
|
|
3158
4107
|
var typesCommand = define({
|
|
3159
4108
|
name: "types",
|
|
3160
4109
|
tool: "kb_types",
|
|
3161
4110
|
usage: "types",
|
|
3162
4111
|
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.",
|
|
3163
|
-
input:
|
|
4112
|
+
input: import_zod27.z.object({}),
|
|
3164
4113
|
fromArgv: () => ({}),
|
|
3165
4114
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3166
4115
|
});
|
|
3167
4116
|
|
|
3168
4117
|
// src/commands/unpin.ts
|
|
3169
|
-
var
|
|
4118
|
+
var import_zod28 = require("zod");
|
|
3170
4119
|
var unpinCommand = define({
|
|
3171
4120
|
name: "unpin",
|
|
3172
4121
|
tool: "kb_unpin",
|
|
3173
4122
|
usage: "unpin [bundle-path]",
|
|
3174
4123
|
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.",
|
|
3175
|
-
input:
|
|
4124
|
+
input: import_zod28.z.object({ bundlePath }),
|
|
3176
4125
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3177
4126
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3178
4127
|
});
|
|
3179
4128
|
|
|
3180
4129
|
// src/commands/validate.ts
|
|
3181
|
-
var
|
|
4130
|
+
var import_zod29 = require("zod");
|
|
3182
4131
|
var validateCommand = define({
|
|
3183
4132
|
name: "validate",
|
|
3184
4133
|
tool: "kb_validate",
|
|
3185
4134
|
usage: "validate",
|
|
3186
4135
|
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.",
|
|
3187
|
-
input:
|
|
4136
|
+
input: import_zod29.z.object({ bundlePath }),
|
|
3188
4137
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3189
4138
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3190
4139
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
3191
4140
|
});
|
|
3192
4141
|
|
|
3193
4142
|
// src/commands/verify.ts
|
|
3194
|
-
var
|
|
4143
|
+
var import_zod30 = require("zod");
|
|
3195
4144
|
var verifyCommand = define({
|
|
3196
4145
|
name: "verify",
|
|
3197
4146
|
tool: "kb_verify",
|
|
3198
4147
|
usage: "verify <concept-id> --note <text>",
|
|
3199
4148
|
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.",
|
|
3200
|
-
input:
|
|
4149
|
+
input: import_zod30.z.object({
|
|
3201
4150
|
bundlePath,
|
|
3202
4151
|
conceptId,
|
|
3203
|
-
note:
|
|
4152
|
+
note: import_zod30.z.string().refine((s) => s.trim().length > 0, {
|
|
3204
4153
|
message: "note must say what the check found"
|
|
3205
4154
|
})
|
|
3206
4155
|
}),
|
|
@@ -3220,7 +4169,7 @@ var verifyCommand = define({
|
|
|
3220
4169
|
});
|
|
3221
4170
|
|
|
3222
4171
|
// src/commands/write.ts
|
|
3223
|
-
var
|
|
4172
|
+
var import_zod31 = require("zod");
|
|
3224
4173
|
var writeCommand = define({
|
|
3225
4174
|
name: "write",
|
|
3226
4175
|
tool: "kb_write",
|
|
@@ -3234,9 +4183,9 @@ var writeCommand = define({
|
|
|
3234
4183
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3235
4184
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3236
4185
|
].join("\n"),
|
|
3237
|
-
input:
|
|
4186
|
+
input: import_zod31.z.object({
|
|
3238
4187
|
bundlePath,
|
|
3239
|
-
type:
|
|
4188
|
+
type: import_zod31.z.enum(KB_RECORD_TYPES),
|
|
3240
4189
|
input: composeInputSchema
|
|
3241
4190
|
}),
|
|
3242
4191
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3260,7 +4209,7 @@ var writeCommand = define({
|
|
|
3260
4209
|
});
|
|
3261
4210
|
|
|
3262
4211
|
// src/commands/write-decision.ts
|
|
3263
|
-
var
|
|
4212
|
+
var import_zod32 = require("zod");
|
|
3264
4213
|
var writeDecisionCommand = define({
|
|
3265
4214
|
name: "write-decision",
|
|
3266
4215
|
tool: "kb_write_decision",
|
|
@@ -3273,7 +4222,7 @@ var writeDecisionCommand = define({
|
|
|
3273
4222
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3274
4223
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3275
4224
|
].join("\n"),
|
|
3276
|
-
input:
|
|
4225
|
+
input: import_zod32.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3277
4226
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3278
4227
|
bundlePath: path,
|
|
3279
4228
|
input: JSON.parse(await stdin())
|
|
@@ -3302,7 +4251,9 @@ var KB_COMMANDS = [
|
|
|
3302
4251
|
supersedeCommand,
|
|
3303
4252
|
answerCommand,
|
|
3304
4253
|
verifyCommand,
|
|
4254
|
+
anchorResolveCommand,
|
|
3305
4255
|
loadCommand,
|
|
4256
|
+
catalogCommand,
|
|
3306
4257
|
packCommand,
|
|
3307
4258
|
queryCommand,
|
|
3308
4259
|
traceCommand,
|
|
@@ -3328,7 +4279,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
3328
4279
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3329
4280
|
|
|
3330
4281
|
// src/version.ts
|
|
3331
|
-
var VERSION = true ? "0.1.
|
|
4282
|
+
var VERSION = true ? "0.1.11" : "0.0.0-dev";
|
|
3332
4283
|
|
|
3333
4284
|
// src/mcp.ts
|
|
3334
4285
|
function createKbMcpServer() {
|
|
@@ -3367,7 +4318,7 @@ async function runKbMcpServer() {
|
|
|
3367
4318
|
}
|
|
3368
4319
|
|
|
3369
4320
|
// src/cli.ts
|
|
3370
|
-
var
|
|
4321
|
+
var import_node_path8 = require("path");
|
|
3371
4322
|
async function runKbCli(argv) {
|
|
3372
4323
|
const { flags, literal } = takeLiteral(argv);
|
|
3373
4324
|
const { bundle, rest: withFlags } = takeBundle(flags);
|
|
@@ -3424,18 +4375,18 @@ function takeLiteral(argv) {
|
|
|
3424
4375
|
function takeBundle(argv) {
|
|
3425
4376
|
const at = argv.indexOf("--bundle");
|
|
3426
4377
|
if (at === -1) {
|
|
3427
|
-
return { bundle: (0,
|
|
4378
|
+
return { bundle: (0, import_node_path8.join)(process.cwd(), KB_DIR), rest: argv };
|
|
3428
4379
|
}
|
|
3429
4380
|
const bundle = argv[at + 1];
|
|
3430
4381
|
if (!bundle) die("--bundle requires a path");
|
|
3431
4382
|
return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
|
|
3432
4383
|
}
|
|
3433
4384
|
function readStdin() {
|
|
3434
|
-
return new Promise((
|
|
4385
|
+
return new Promise((resolve6, reject) => {
|
|
3435
4386
|
let text = "";
|
|
3436
4387
|
process.stdin.setEncoding("utf8");
|
|
3437
4388
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
3438
|
-
process.stdin.on("end", () =>
|
|
4389
|
+
process.stdin.on("end", () => resolve6(text));
|
|
3439
4390
|
process.stdin.on("error", reject);
|
|
3440
4391
|
});
|
|
3441
4392
|
}
|
|
@@ -3497,6 +4448,7 @@ function usage() {
|
|
|
3497
4448
|
KB_SLUG_PATTERN,
|
|
3498
4449
|
KbBaseFrozenError,
|
|
3499
4450
|
KbInvalidConceptIdError,
|
|
4451
|
+
KbMissingFlagValueError,
|
|
3500
4452
|
KbPackBudgetExceededError,
|
|
3501
4453
|
KbPinsMalformedError,
|
|
3502
4454
|
KbRecordAlreadyExistsError,
|
|
@@ -3513,8 +4465,10 @@ function usage() {
|
|
|
3513
4465
|
SEARCH_INDEX_FILE,
|
|
3514
4466
|
TRACE_EDGES,
|
|
3515
4467
|
adjudicate,
|
|
4468
|
+
anchorFilePath,
|
|
3516
4469
|
assertBaseNotFrozen,
|
|
3517
4470
|
buildContext,
|
|
4471
|
+
catalog,
|
|
3518
4472
|
composeDecisionRecord,
|
|
3519
4473
|
composeInputSchema,
|
|
3520
4474
|
composeNoDecisionRecord,
|
|
@@ -3522,8 +4476,10 @@ function usage() {
|
|
|
3522
4476
|
contextProfileBudgets,
|
|
3523
4477
|
createKbMcpServer,
|
|
3524
4478
|
decisionInputSchema,
|
|
4479
|
+
detectAnchorDrift,
|
|
3525
4480
|
doctor,
|
|
3526
4481
|
edgeNeighbours,
|
|
4482
|
+
hashAnchorText,
|
|
3527
4483
|
indexIsStale,
|
|
3528
4484
|
isKbRecordType,
|
|
3529
4485
|
isNoDecisionRecord,
|
|
@@ -3546,9 +4502,12 @@ function usage() {
|
|
|
3546
4502
|
pinBase,
|
|
3547
4503
|
readMergedPins,
|
|
3548
4504
|
readPinsLayer,
|
|
4505
|
+
regexResolver,
|
|
4506
|
+
renderCatalogLine,
|
|
3549
4507
|
renderIndex,
|
|
3550
4508
|
renderIndexLine,
|
|
3551
4509
|
renderLogEntry,
|
|
4510
|
+
resolveAnchor,
|
|
3552
4511
|
resolveHeads,
|
|
3553
4512
|
resolveHits,
|
|
3554
4513
|
resolvePinPath,
|