@saasontools/strauss-kb 0.1.18 → 0.1.20
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 +5 -5
- package/README.md +11 -4
- package/dist/{chunk-ROGVYSMV.js → chunk-EXZQZJU2.js} +2 -2
- package/dist/{chunk-GSOTMWZZ.js → chunk-QQLPJO4R.js} +3196 -1273
- package/dist/chunk-QQLPJO4R.js.map +1 -0
- package/dist/{chunk-IOIUS26S.js → chunk-XDH6J6CH.js} +2 -2
- package/dist/cli-main.cjs +2841 -954
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +2272 -417
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +510 -101
- package/dist/index.d.ts +510 -101
- package/dist/index.js +40 -83
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +2837 -950
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-GSOTMWZZ.js.map +0 -1
- /package/dist/{chunk-ROGVYSMV.js.map → chunk-EXZQZJU2.js.map} +0 -0
- /package/dist/{chunk-IOIUS26S.js.map → chunk-XDH6J6CH.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -40,12 +40,14 @@ __export(index_exports, {
|
|
|
40
40
|
DEFAULT_LOAD_BUDGET: () => DEFAULT_LOAD_BUDGET,
|
|
41
41
|
DEFAULT_PACK_HOPS: () => DEFAULT_PACK_HOPS,
|
|
42
42
|
DEFAULT_PACK_MAX_NODES: () => DEFAULT_PACK_MAX_NODES,
|
|
43
|
+
DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
|
|
43
44
|
DEFAULT_TYPED_LINK_RELS: () => DEFAULT_TYPED_LINK_RELS,
|
|
44
45
|
DEFAULT_UNVERIFIED_DAYS: () => DEFAULT_UNVERIFIED_DAYS,
|
|
45
46
|
ErrorTypes: () => ErrorTypes,
|
|
46
47
|
Fault: () => Fault,
|
|
47
48
|
INDEX_FILE: () => INDEX_FILE,
|
|
48
49
|
KB_CAUSAL_LINK_RELS: () => KB_CAUSAL_LINK_RELS,
|
|
50
|
+
KB_CLASSES: () => KB_CLASSES,
|
|
49
51
|
KB_COMMANDS: () => KB_COMMANDS,
|
|
50
52
|
KB_COMMANDS_BY_NAME: () => KB_COMMANDS_BY_NAME,
|
|
51
53
|
KB_CONCEPT_ID_PATTERN: () => KB_CONCEPT_ID_PATTERN,
|
|
@@ -59,10 +61,15 @@ __export(index_exports, {
|
|
|
59
61
|
KB_RECORD_TYPES: () => KB_RECORD_TYPES,
|
|
60
62
|
KB_SLUG_PATTERN: () => KB_SLUG_PATTERN,
|
|
61
63
|
KbBaseFrozenError: () => KbBaseFrozenError,
|
|
64
|
+
KbClassifyInputError: () => KbClassifyInputError,
|
|
62
65
|
KbInvalidConceptIdError: () => KbInvalidConceptIdError,
|
|
63
66
|
KbMissingFlagValueError: () => KbMissingFlagValueError,
|
|
64
67
|
KbPackBudgetExceededError: () => KbPackBudgetExceededError,
|
|
65
68
|
KbPinsMalformedError: () => KbPinsMalformedError,
|
|
69
|
+
KbPromoteCollisionError: () => KbPromoteCollisionError,
|
|
70
|
+
KbPromoteSelfError: () => KbPromoteSelfError,
|
|
71
|
+
KbPromoteStandingError: () => KbPromoteStandingError,
|
|
72
|
+
KbPromoteStoppedError: () => KbPromoteStoppedError,
|
|
66
73
|
KbRecordAlreadyExistsError: () => KbRecordAlreadyExistsError,
|
|
67
74
|
KbRecordNotFoundError: () => KbRecordNotFoundError,
|
|
68
75
|
KbSelfVerificationError: () => KbSelfVerificationError,
|
|
@@ -75,16 +82,20 @@ __export(index_exports, {
|
|
|
75
82
|
PINS_FILE: () => PINS_FILE,
|
|
76
83
|
PINS_LOCAL_FILE: () => PINS_LOCAL_FILE,
|
|
77
84
|
PIN_LAYERS: () => PIN_LAYERS,
|
|
85
|
+
PROMOTION_SOURCE_ID: () => PROMOTION_SOURCE_ID,
|
|
78
86
|
RECORD_TYPES: () => RECORD_TYPES,
|
|
79
87
|
SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
|
|
80
88
|
TRACE_EDGES: () => TRACE_EDGES,
|
|
81
89
|
TreeSitterResolver: () => TreeSitterResolver,
|
|
82
90
|
adjudicate: () => adjudicate,
|
|
83
91
|
anchorFilePath: () => anchorFilePath,
|
|
92
|
+
anchorOnHunk: () => anchorOnHunk,
|
|
84
93
|
assertBaseNotFrozen: () => assertBaseNotFrozen,
|
|
85
94
|
backlinks: () => backlinks,
|
|
86
95
|
buildContext: () => buildContext,
|
|
96
|
+
carry: () => carry,
|
|
87
97
|
catalog: () => catalog,
|
|
98
|
+
classifyDiff: () => classifyDiff,
|
|
88
99
|
classifyDrift: () => classifyDrift,
|
|
89
100
|
composeDecisionRecord: () => composeDecisionRecord,
|
|
90
101
|
composeInputSchema: () => composeInputSchema,
|
|
@@ -110,8 +121,11 @@ __export(index_exports, {
|
|
|
110
121
|
isKbLinkRel: () => isKbLinkRel,
|
|
111
122
|
isKbRecordType: () => isKbRecordType,
|
|
112
123
|
isNoDecisionRecord: () => isNoDecisionRecord,
|
|
124
|
+
isReviewTag: () => isReviewTag,
|
|
113
125
|
kbActorStampSchema: () => kbActorStampSchema,
|
|
114
126
|
kbAnchorSchema: () => kbAnchorSchema,
|
|
127
|
+
kbAnchorSpanSchema: () => kbAnchorSpanSchema,
|
|
128
|
+
kbAnchorWriteSchema: () => kbAnchorWriteSchema,
|
|
115
129
|
kbConceptIdSchema: () => kbConceptIdSchema,
|
|
116
130
|
kbJsonSchemas: () => kbJsonSchemas,
|
|
117
131
|
kbLinkSchema: () => kbLinkSchema,
|
|
@@ -123,6 +137,7 @@ __export(index_exports, {
|
|
|
123
137
|
listPins: () => listPins,
|
|
124
138
|
loadQmd: () => loadQmd,
|
|
125
139
|
matchToDiff: () => matchToDiff,
|
|
140
|
+
matchesTags: () => matchesTags,
|
|
126
141
|
mergedContextBudgets: () => mergedContextBudgets,
|
|
127
142
|
neighbours: () => neighbours,
|
|
128
143
|
pack: () => pack,
|
|
@@ -130,6 +145,8 @@ __export(index_exports, {
|
|
|
130
145
|
parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
|
|
131
146
|
pinBase: () => pinBase,
|
|
132
147
|
prepareResolvers: () => prepareResolvers,
|
|
148
|
+
promoteCandidates: () => promoteCandidates,
|
|
149
|
+
promoteInputSchema: () => promoteInputSchema,
|
|
133
150
|
readMergedPins: () => readMergedPins,
|
|
134
151
|
readPinsLayer: () => readPinsLayer,
|
|
135
152
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
@@ -151,6 +168,7 @@ __export(index_exports, {
|
|
|
151
168
|
selectDecisions: () => selectDecisions,
|
|
152
169
|
splitMarkdownFrontmatter: () => splitMarkdownFrontmatter,
|
|
153
170
|
stringifyMarkdownWithFrontmatter: () => stringifyMarkdownWithFrontmatter,
|
|
171
|
+
symbolRangeIndex: () => symbolRangeIndex,
|
|
154
172
|
syncInstructions: () => syncInstructions,
|
|
155
173
|
toHookJson: () => toHookJson,
|
|
156
174
|
trace: () => trace,
|
|
@@ -241,9 +259,24 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
241
259
|
message: "note must say what the check found"
|
|
242
260
|
})
|
|
243
261
|
});
|
|
262
|
+
var kbAnchorSpanSchema = import_zod.z.object({
|
|
263
|
+
start: import_zod.z.number().int().positive(),
|
|
264
|
+
end: import_zod.z.number().int().positive()
|
|
265
|
+
}).strict();
|
|
244
266
|
var kbAnchorSchema = import_zod.z.object({
|
|
245
267
|
file: import_zod.z.string().min(1),
|
|
246
268
|
symbol: import_zod.z.string().min(1).optional(),
|
|
269
|
+
/**
|
|
270
|
+
* The lines the concept names, when no symbol covers them — deleted code,
|
|
271
|
+
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
272
|
+
*/
|
|
273
|
+
span: kbAnchorSpanSchema.optional(),
|
|
274
|
+
/**
|
|
275
|
+
* Which side of the change the anchor describes. `old` is code as it was
|
|
276
|
+
* committed at `ref`, which is the only way to anchor something deleted;
|
|
277
|
+
* absent means the working tree.
|
|
278
|
+
*/
|
|
279
|
+
side: import_zod.z.enum(["old", "new"]).optional(),
|
|
247
280
|
/**
|
|
248
281
|
* Which repository the file lives in — a remote URL
|
|
249
282
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
@@ -282,8 +315,38 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
282
315
|
* before resolvers were named, which is read as `regex` — the only one
|
|
283
316
|
* there was. A hash from a different resolver is drift, not a match.
|
|
284
317
|
*/
|
|
285
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
318
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
286
319
|
}).strict();
|
|
320
|
+
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
321
|
+
if (anchor.span && anchor.symbol) {
|
|
322
|
+
ctx.addIssue({
|
|
323
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
324
|
+
path: ["span"],
|
|
325
|
+
message: "an anchor names a symbol or a span, not both"
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
329
|
+
ctx.addIssue({
|
|
330
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
331
|
+
path: ["span", "end"],
|
|
332
|
+
message: "span end must not precede start"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
336
|
+
ctx.addIssue({
|
|
337
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
338
|
+
path: ["hash_kind"],
|
|
339
|
+
message: "a span is hashed raw, never ast"
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
343
|
+
ctx.addIssue({
|
|
344
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
345
|
+
path: ["ref"],
|
|
346
|
+
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
287
350
|
var kbLinkSchema = import_zod.z.object({
|
|
288
351
|
target: import_zod.z.string().min(1),
|
|
289
352
|
rel: import_zod.z.string().min(1)
|
|
@@ -370,9 +433,15 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
370
433
|
})(Fault || {});
|
|
371
434
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
372
435
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
436
|
+
ErrorTypes2["KbClassifyInput"] = "KbClassifyInput";
|
|
373
437
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
438
|
+
ErrorTypes2["KbMatchInput"] = "KbMatchInput";
|
|
374
439
|
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
375
440
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
441
|
+
ErrorTypes2["KbPromoteCollision"] = "KbPromoteCollision";
|
|
442
|
+
ErrorTypes2["KbPromoteSelf"] = "KbPromoteSelf";
|
|
443
|
+
ErrorTypes2["KbPromoteStanding"] = "KbPromoteStanding";
|
|
444
|
+
ErrorTypes2["KbPromoteStopped"] = "KbPromoteStopped";
|
|
376
445
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
377
446
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
378
447
|
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
@@ -518,6 +587,89 @@ var KbMissingFlagValueError = class extends BaseError {
|
|
|
518
587
|
}
|
|
519
588
|
flag;
|
|
520
589
|
};
|
|
590
|
+
var KbClassifyInputError = class extends BaseError {
|
|
591
|
+
constructor(reason) {
|
|
592
|
+
super({
|
|
593
|
+
message: `classify: ${reason}`,
|
|
594
|
+
errorType: "KbClassifyInput" /* KbClassifyInput */,
|
|
595
|
+
code: 400,
|
|
596
|
+
fault: "User" /* User */,
|
|
597
|
+
retriable: false,
|
|
598
|
+
reportToUser: true,
|
|
599
|
+
details: { reason }
|
|
600
|
+
});
|
|
601
|
+
this.reason = reason;
|
|
602
|
+
}
|
|
603
|
+
reason;
|
|
604
|
+
};
|
|
605
|
+
var KbPromoteCollisionError = class extends BaseError {
|
|
606
|
+
constructor(conceptId2, to) {
|
|
607
|
+
super({
|
|
608
|
+
message: `kb: ${to} already holds ${conceptId2} \u2014 re-run with force to overwrite it`,
|
|
609
|
+
errorType: "KbPromoteCollision" /* KbPromoteCollision */,
|
|
610
|
+
code: 409,
|
|
611
|
+
fault: "User" /* User */,
|
|
612
|
+
retriable: false,
|
|
613
|
+
reportToUser: true,
|
|
614
|
+
details: { conceptId: conceptId2, to, action: "refused" }
|
|
615
|
+
});
|
|
616
|
+
this.conceptId = conceptId2;
|
|
617
|
+
this.to = to;
|
|
618
|
+
}
|
|
619
|
+
conceptId;
|
|
620
|
+
to;
|
|
621
|
+
};
|
|
622
|
+
var KbPromoteStandingError = class extends BaseError {
|
|
623
|
+
constructor(conceptId2, standing) {
|
|
624
|
+
super({
|
|
625
|
+
message: `kb: ${conceptId2} is ${standing} \u2014 only a record that still stands can be promoted`,
|
|
626
|
+
errorType: "KbPromoteStanding" /* KbPromoteStanding */,
|
|
627
|
+
code: 409,
|
|
628
|
+
fault: "User" /* User */,
|
|
629
|
+
retriable: false,
|
|
630
|
+
reportToUser: true,
|
|
631
|
+
details: { conceptId: conceptId2, standing, action: "refused" }
|
|
632
|
+
});
|
|
633
|
+
this.conceptId = conceptId2;
|
|
634
|
+
this.standing = standing;
|
|
635
|
+
}
|
|
636
|
+
conceptId;
|
|
637
|
+
standing;
|
|
638
|
+
};
|
|
639
|
+
var KbPromoteSelfError = class extends BaseError {
|
|
640
|
+
constructor(to) {
|
|
641
|
+
super({
|
|
642
|
+
message: `kb: ${to} is the base being promoted from \u2014 name a different target`,
|
|
643
|
+
errorType: "KbPromoteSelf" /* KbPromoteSelf */,
|
|
644
|
+
code: 400,
|
|
645
|
+
fault: "User" /* User */,
|
|
646
|
+
retriable: false,
|
|
647
|
+
reportToUser: true,
|
|
648
|
+
details: { to, action: "refused" }
|
|
649
|
+
});
|
|
650
|
+
this.to = to;
|
|
651
|
+
}
|
|
652
|
+
to;
|
|
653
|
+
};
|
|
654
|
+
var KbPromoteStoppedError = class extends BaseError {
|
|
655
|
+
constructor(conceptId2, landed, reason) {
|
|
656
|
+
super({
|
|
657
|
+
message: `kb: promotion stopped at ${conceptId2} (${reason}) \u2014 landed: ${landed.length ? landed.join(", ") : "nothing"}`,
|
|
658
|
+
errorType: "KbPromoteStopped" /* KbPromoteStopped */,
|
|
659
|
+
code: 500,
|
|
660
|
+
fault: "System" /* System */,
|
|
661
|
+
retriable: false,
|
|
662
|
+
reportToUser: true,
|
|
663
|
+
details: { conceptId: conceptId2, landed, reason, action: "stopped" }
|
|
664
|
+
});
|
|
665
|
+
this.conceptId = conceptId2;
|
|
666
|
+
this.landed = landed;
|
|
667
|
+
this.reason = reason;
|
|
668
|
+
}
|
|
669
|
+
conceptId;
|
|
670
|
+
landed;
|
|
671
|
+
reason;
|
|
672
|
+
};
|
|
521
673
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
522
674
|
constructor(message, details) {
|
|
523
675
|
super({
|
|
@@ -697,7 +849,9 @@ function revRef(rev) {
|
|
|
697
849
|
var UNCHECKED_REASONS = [
|
|
698
850
|
"remote-unreachable",
|
|
699
851
|
"repo-unauthorized",
|
|
700
|
-
"default-branch-unknown"
|
|
852
|
+
"default-branch-unknown",
|
|
853
|
+
/** Local, but the same finding: a shallow clone has no rev to read. */
|
|
854
|
+
"ref-unavailable"
|
|
701
855
|
];
|
|
702
856
|
function isUncheckedReason(reason) {
|
|
703
857
|
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
@@ -765,6 +919,11 @@ function refShapeIsSafe(ref) {
|
|
|
765
919
|
if (ref.includes("..")) return false;
|
|
766
920
|
return REF_SHAPE.test(ref);
|
|
767
921
|
}
|
|
922
|
+
function localRevShapeIsSafe(rev) {
|
|
923
|
+
if (!rev || rev.length > MAX_REF_LENGTH) return false;
|
|
924
|
+
if (rev.includes("..")) return false;
|
|
925
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/^~-]*$/.test(rev);
|
|
926
|
+
}
|
|
768
927
|
async function refIsWellFormed(ref) {
|
|
769
928
|
if (!refShapeIsSafe(ref)) return false;
|
|
770
929
|
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
@@ -1163,6 +1322,142 @@ function bundleDigest(records, superseded) {
|
|
|
1163
1322
|
return bundleStamp(records, superseded).digest;
|
|
1164
1323
|
}
|
|
1165
1324
|
|
|
1325
|
+
// src/drift/git.ts
|
|
1326
|
+
var import_node_child_process3 = require("child_process");
|
|
1327
|
+
var import_node_util3 = require("util");
|
|
1328
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
1329
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
1330
|
+
var MAX_RANGE_DIFF_BYTES = 8 * 1048576;
|
|
1331
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
1332
|
+
var RANGE_DIFF_TIMEOUT_MS = 2e4;
|
|
1333
|
+
async function git2(cwd, args, limits = {}) {
|
|
1334
|
+
const env = { ...process.env };
|
|
1335
|
+
delete env["GIT_DIR"];
|
|
1336
|
+
delete env["GIT_WORK_TREE"];
|
|
1337
|
+
delete env["GIT_INDEX_FILE"];
|
|
1338
|
+
try {
|
|
1339
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
1340
|
+
timeout: limits.timeoutMs ?? GIT_TIMEOUT_MS,
|
|
1341
|
+
maxBuffer: limits.maxBytes ?? MAX_GIT_OUTPUT_BYTES,
|
|
1342
|
+
env
|
|
1343
|
+
});
|
|
1344
|
+
return { ok: true, stdout };
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
return { ok: false, reason: failureOf(error) };
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function failureOf(error) {
|
|
1350
|
+
const { code, killed } = error;
|
|
1351
|
+
if (code === "ENOENT") return "git-missing";
|
|
1352
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "too-large";
|
|
1353
|
+
if (killed === true) return "timeout";
|
|
1354
|
+
return "failed";
|
|
1355
|
+
}
|
|
1356
|
+
async function readFileAtRef(repoRoot, anchor) {
|
|
1357
|
+
if (!filePathIsSafe(anchor.file))
|
|
1358
|
+
return { ok: false, reason: "outside-repo" };
|
|
1359
|
+
if (!anchor.ref || !refShapeIsSafe(anchor.ref)) {
|
|
1360
|
+
return { ok: false, reason: "ref-unreadable" };
|
|
1361
|
+
}
|
|
1362
|
+
const blob = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
1363
|
+
if (blob !== null) return { ok: true, source: blob };
|
|
1364
|
+
return {
|
|
1365
|
+
ok: false,
|
|
1366
|
+
reason: await hasCommit(repoRoot, anchor.ref) ? "ref-unreadable" : "ref-unavailable"
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async function hasCommit(repoRoot, ref) {
|
|
1370
|
+
const found = await git2(repoRoot, [
|
|
1371
|
+
"cat-file",
|
|
1372
|
+
"-e",
|
|
1373
|
+
"--end-of-options",
|
|
1374
|
+
`${ref}^{commit}`
|
|
1375
|
+
]);
|
|
1376
|
+
return found.ok;
|
|
1377
|
+
}
|
|
1378
|
+
async function listRepoFiles(repoRoot) {
|
|
1379
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
1380
|
+
if (!result.ok) return [];
|
|
1381
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
1382
|
+
}
|
|
1383
|
+
var DIFF_RANGE = /^(.+?)(\.{2,3})(.+)$/;
|
|
1384
|
+
async function readRangeDiff(repoRoot, range, maxBytes = MAX_RANGE_DIFF_BYTES) {
|
|
1385
|
+
const parts = DIFF_RANGE.exec(range);
|
|
1386
|
+
if (!parts) return { ok: false, reason: "bad-range" };
|
|
1387
|
+
const [, base2 = "", dots = "", head = ""] = parts;
|
|
1388
|
+
if (!localRevShapeIsSafe(base2) || !localRevShapeIsSafe(head)) {
|
|
1389
|
+
return { ok: false, reason: "bad-range" };
|
|
1390
|
+
}
|
|
1391
|
+
const result = await git2(
|
|
1392
|
+
repoRoot,
|
|
1393
|
+
[
|
|
1394
|
+
"-c",
|
|
1395
|
+
"core.quotePath=false",
|
|
1396
|
+
"diff",
|
|
1397
|
+
"--unified=0",
|
|
1398
|
+
"--no-color",
|
|
1399
|
+
"--no-ext-diff",
|
|
1400
|
+
"--no-textconv",
|
|
1401
|
+
"--find-renames",
|
|
1402
|
+
"--src-prefix=a/",
|
|
1403
|
+
"--dst-prefix=b/",
|
|
1404
|
+
"--end-of-options",
|
|
1405
|
+
`${base2}${dots}${head}`,
|
|
1406
|
+
"--"
|
|
1407
|
+
],
|
|
1408
|
+
{ maxBytes, timeoutMs: RANGE_DIFF_TIMEOUT_MS }
|
|
1409
|
+
);
|
|
1410
|
+
if (result.ok) return { ok: true, text: result.stdout };
|
|
1411
|
+
return {
|
|
1412
|
+
ok: false,
|
|
1413
|
+
reason: result.reason === "failed" ? "bad-range" : result.reason
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
async function readOldSource(repoRoot, anchor) {
|
|
1417
|
+
if (!filePathIsSafe(anchor.file))
|
|
1418
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1419
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
1420
|
+
const shown2 = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
1421
|
+
if (shown2 !== null) {
|
|
1422
|
+
return {
|
|
1423
|
+
ok: true,
|
|
1424
|
+
source: shown2,
|
|
1425
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const at2 = anchor.resolved_at;
|
|
1430
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
1431
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1432
|
+
}
|
|
1433
|
+
const found = await git2(repoRoot, [
|
|
1434
|
+
"log",
|
|
1435
|
+
"-1",
|
|
1436
|
+
"--format=%H",
|
|
1437
|
+
`--before=${at2}`,
|
|
1438
|
+
"--end-of-options",
|
|
1439
|
+
"HEAD",
|
|
1440
|
+
"--",
|
|
1441
|
+
anchor.file
|
|
1442
|
+
]);
|
|
1443
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
1444
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
1445
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1446
|
+
const shown = await catBlob(repoRoot, sha, anchor.file);
|
|
1447
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
1448
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
1449
|
+
}
|
|
1450
|
+
async function catBlob(repoRoot, ref, file) {
|
|
1451
|
+
const path = file.replace(/^\.\//, "");
|
|
1452
|
+
const result = await git2(repoRoot, [
|
|
1453
|
+
"cat-file",
|
|
1454
|
+
"blob",
|
|
1455
|
+
"--end-of-options",
|
|
1456
|
+
`${ref}:${path}`
|
|
1457
|
+
]);
|
|
1458
|
+
return result.ok ? result.stdout : null;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1166
1461
|
// src/anchor-resolver/read.ts
|
|
1167
1462
|
var import_promises2 = require("fs/promises");
|
|
1168
1463
|
var import_node_path2 = require("path");
|
|
@@ -1228,6 +1523,7 @@ function looksLikeWrongRepoRoot(drift) {
|
|
|
1228
1523
|
for (const entries of drift.values()) {
|
|
1229
1524
|
for (const entry of entries) {
|
|
1230
1525
|
if (entry.repo !== void 0) continue;
|
|
1526
|
+
if (entry.side === "old") continue;
|
|
1231
1527
|
checked += 1;
|
|
1232
1528
|
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
1233
1529
|
return false;
|
|
@@ -1363,7 +1659,7 @@ function size(bytes) {
|
|
|
1363
1659
|
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1364
1660
|
}
|
|
1365
1661
|
function pause(ms) {
|
|
1366
|
-
return new Promise((
|
|
1662
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1367
1663
|
}
|
|
1368
1664
|
|
|
1369
1665
|
// src/grammars/manifest.ts
|
|
@@ -1935,8 +2231,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1935
2231
|
}
|
|
1936
2232
|
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1937
2233
|
function captureIndentedBlock(lines, matchLine) {
|
|
1938
|
-
const
|
|
1939
|
-
const indent =
|
|
2234
|
+
const header2 = lines[matchLine] ?? "";
|
|
2235
|
+
const indent = header2.length - header2.trimStart().length;
|
|
1940
2236
|
let headerEnd = -1;
|
|
1941
2237
|
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1942
2238
|
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
@@ -1957,42 +2253,55 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1957
2253
|
}
|
|
1958
2254
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1959
2255
|
}
|
|
1960
|
-
var
|
|
1961
|
-
(name
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2256
|
+
var declarationTier = (name) => new RegExp(
|
|
2257
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
2258
|
+
);
|
|
2259
|
+
var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
|
|
2260
|
+
var anchoredAssignmentTier = (name) => new RegExp(
|
|
2261
|
+
`^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
|
|
2262
|
+
);
|
|
2263
|
+
var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
|
|
2264
|
+
var MENTION_TIERS = [
|
|
1965
2265
|
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1966
2266
|
(name) => new RegExp(`\\b${name}\\b`)
|
|
1967
2267
|
];
|
|
2268
|
+
var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
|
|
2269
|
+
function resolveWith(tiers, source, symbol) {
|
|
2270
|
+
const segments = symbol.split(".");
|
|
2271
|
+
const name = segments[segments.length - 1];
|
|
2272
|
+
if (!name) return null;
|
|
2273
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
2274
|
+
const escaped = escapeRegExp(name);
|
|
2275
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
2276
|
+
const lines = source.split("\n");
|
|
2277
|
+
for (const tier of tiers) {
|
|
2278
|
+
const pattern = tier(escaped);
|
|
2279
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
2280
|
+
if (!candidates.length) continue;
|
|
2281
|
+
if (parentPattern && candidates.length > 1) {
|
|
2282
|
+
const distances = candidates.map(
|
|
2283
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
2284
|
+
);
|
|
2285
|
+
const nearest = Math.min(...distances);
|
|
2286
|
+
if (Number.isFinite(nearest)) {
|
|
2287
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (candidates.length !== 1) return null;
|
|
2291
|
+
const matchLine = candidates[0];
|
|
2292
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
2293
|
+
}
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
1968
2296
|
var regexResolver = {
|
|
1969
2297
|
name: "regex",
|
|
1970
2298
|
resolve(source, symbol) {
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
const
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
const lines = source.split("\n");
|
|
1978
|
-
for (const tier of TIERS) {
|
|
1979
|
-
const pattern = tier(escaped);
|
|
1980
|
-
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1981
|
-
if (!candidates.length) continue;
|
|
1982
|
-
if (parentPattern && candidates.length > 1) {
|
|
1983
|
-
const distances = candidates.map(
|
|
1984
|
-
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1985
|
-
);
|
|
1986
|
-
const nearest = Math.min(...distances);
|
|
1987
|
-
if (Number.isFinite(nearest)) {
|
|
1988
|
-
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
if (candidates.length !== 1) return null;
|
|
1992
|
-
const matchLine = candidates[0];
|
|
1993
|
-
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
1994
|
-
}
|
|
1995
|
-
return null;
|
|
2299
|
+
return resolveWith(TIERS, source, symbol);
|
|
2300
|
+
},
|
|
2301
|
+
attempt(source, symbol, _file, options) {
|
|
2302
|
+
const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
|
|
2303
|
+
const span2 = resolveWith(tiers, source, symbol);
|
|
2304
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1996
2305
|
}
|
|
1997
2306
|
};
|
|
1998
2307
|
function escapeRegExp(value) {
|
|
@@ -2014,6 +2323,7 @@ function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
|
2014
2323
|
}
|
|
2015
2324
|
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
2016
2325
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
2326
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
2017
2327
|
if (!anchor.symbol) {
|
|
2018
2328
|
const lines = normalized.split("\n");
|
|
2019
2329
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
@@ -2026,11 +2336,17 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2026
2336
|
}
|
|
2027
2337
|
};
|
|
2028
2338
|
}
|
|
2339
|
+
let afterParsedMiss = false;
|
|
2029
2340
|
for (const resolver of resolvers) {
|
|
2030
|
-
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file
|
|
2341
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
|
|
2342
|
+
afterParsedMiss
|
|
2343
|
+
}) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
2031
2344
|
if (attempt.kind === "abstain") continue;
|
|
2032
2345
|
if (attempt.kind === "unresolved") {
|
|
2033
|
-
if (attempt.reason === "symbol-not-found")
|
|
2346
|
+
if (attempt.reason === "symbol-not-found") {
|
|
2347
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2034
2350
|
return { ok: false, reason: attempt.reason };
|
|
2035
2351
|
}
|
|
2036
2352
|
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
@@ -2043,12 +2359,28 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2043
2359
|
}
|
|
2044
2360
|
return { ok: false, reason: "symbol-not-found" };
|
|
2045
2361
|
}
|
|
2362
|
+
function sliceSpan(source, range) {
|
|
2363
|
+
const lines = source.split("\n");
|
|
2364
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
2365
|
+
if (range.end > lines.length) {
|
|
2366
|
+
return { ok: false, reason: "span-out-of-range" };
|
|
2367
|
+
}
|
|
2368
|
+
return {
|
|
2369
|
+
ok: true,
|
|
2370
|
+
span: {
|
|
2371
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
2372
|
+
startLine: range.start,
|
|
2373
|
+
endLine: range.end
|
|
2374
|
+
},
|
|
2375
|
+
resolver: "span"
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2046
2378
|
function fromResolve(resolver, source, symbol, file) {
|
|
2047
2379
|
const span2 = resolver.resolve(source, symbol, file);
|
|
2048
2380
|
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2049
2381
|
}
|
|
2050
2382
|
function isResolverName(name) {
|
|
2051
|
-
return name === "tree-sitter" || name === "regex";
|
|
2383
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2052
2384
|
}
|
|
2053
2385
|
async function prepareResolvers(resolvers, files) {
|
|
2054
2386
|
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
@@ -2067,6 +2399,9 @@ function resolverChanged(source, anchor, produced) {
|
|
|
2067
2399
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
2068
2400
|
}
|
|
2069
2401
|
function anchorHashOf(anchor, outcome) {
|
|
2402
|
+
if (outcome.resolver === "span") {
|
|
2403
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2404
|
+
}
|
|
2070
2405
|
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2071
2406
|
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2072
2407
|
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
@@ -2100,37 +2435,60 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
2100
2435
|
}
|
|
2101
2436
|
}
|
|
2102
2437
|
const files = [];
|
|
2438
|
+
const committedWants = [];
|
|
2103
2439
|
const wants = [];
|
|
2104
2440
|
for (const entries of planned.values()) {
|
|
2105
2441
|
for (const { anchor, foreign } of entries) {
|
|
2106
|
-
if (
|
|
2107
|
-
else
|
|
2442
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
2443
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2444
|
+
else files.push(anchor.file);
|
|
2108
2445
|
}
|
|
2109
2446
|
}
|
|
2110
|
-
const [reads, remote] = await Promise.all([
|
|
2447
|
+
const [reads, committed, remote] = await Promise.all([
|
|
2111
2448
|
readAnchorFiles(
|
|
2112
2449
|
files,
|
|
2113
2450
|
options.reader ?? anchorFileReader(repoRoot),
|
|
2114
2451
|
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
2115
2452
|
),
|
|
2453
|
+
readCommitted(repoRoot, committedWants, options),
|
|
2116
2454
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
2117
2455
|
]);
|
|
2118
2456
|
await prepareResolvers(resolvers, [
|
|
2119
2457
|
...files,
|
|
2458
|
+
...committedWants.map((anchor) => anchor.file),
|
|
2120
2459
|
...wants.map((want) => want.file)
|
|
2121
2460
|
]);
|
|
2122
2461
|
const drift = /* @__PURE__ */ new Map();
|
|
2123
2462
|
for (const record of records) {
|
|
2124
2463
|
const entries = [];
|
|
2125
2464
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2465
|
+
if (foreign) {
|
|
2466
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2470
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
2129
2471
|
}
|
|
2130
2472
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
2131
2473
|
}
|
|
2132
2474
|
return drift;
|
|
2133
2475
|
}
|
|
2476
|
+
function atRefKey(anchor) {
|
|
2477
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
2478
|
+
}
|
|
2479
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
2480
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
2481
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
2482
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2483
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
2484
|
+
const keys = [...byKey.keys()];
|
|
2485
|
+
const results = await mapLimit(
|
|
2486
|
+
keys,
|
|
2487
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2488
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
2489
|
+
);
|
|
2490
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2491
|
+
}
|
|
2134
2492
|
function remoteWants(anchor) {
|
|
2135
2493
|
const repo = anchor.repo;
|
|
2136
2494
|
const wants = [{ repo, file: anchor.file }];
|
|
@@ -2141,6 +2499,7 @@ function base(anchor) {
|
|
|
2141
2499
|
return {
|
|
2142
2500
|
file: anchor.file,
|
|
2143
2501
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2502
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
2144
2503
|
storedHash: anchor.hash
|
|
2145
2504
|
};
|
|
2146
2505
|
}
|
|
@@ -2154,9 +2513,15 @@ function unresolved(anchor, reason, repo) {
|
|
|
2154
2513
|
...classOf(reason)
|
|
2155
2514
|
};
|
|
2156
2515
|
}
|
|
2516
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2517
|
+
"file-missing",
|
|
2518
|
+
"symbol-not-found",
|
|
2519
|
+
"span-out-of-range",
|
|
2520
|
+
"ref-unreadable"
|
|
2521
|
+
]);
|
|
2157
2522
|
function provisionalDriftClass(entry) {
|
|
2158
2523
|
if (entry.state === "unresolved") {
|
|
2159
|
-
return
|
|
2524
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
2160
2525
|
}
|
|
2161
2526
|
return entry.state === "drifted" ? "changed" : void 0;
|
|
2162
2527
|
}
|
|
@@ -2225,6 +2590,13 @@ function remoteEntry(anchor, remote, resolvers) {
|
|
|
2225
2590
|
remoteState: "drifted-from-ref"
|
|
2226
2591
|
});
|
|
2227
2592
|
}
|
|
2593
|
+
if (anchor.side === "old") {
|
|
2594
|
+
return compared(anchor, current, {
|
|
2595
|
+
repo,
|
|
2596
|
+
...extras,
|
|
2597
|
+
remoteState: "matches-ref"
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2228
2600
|
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2229
2601
|
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2230
2602
|
...compared(anchor, head.current, {
|
|
@@ -2260,19 +2632,28 @@ var kbLogEntrySchema = import_zod3.z.object({
|
|
|
2260
2632
|
by: import_zod3.z.string().min(1),
|
|
2261
2633
|
operation: import_zod3.z.string().min(1),
|
|
2262
2634
|
conceptId: import_zod3.z.string().min(1),
|
|
2263
|
-
/**
|
|
2635
|
+
/**
|
|
2636
|
+
* The operation's other end, where it has one: a second concept id for
|
|
2637
|
+
* supersession, the other base's path for promotion.
|
|
2638
|
+
*/
|
|
2264
2639
|
target: import_zod3.z.string().min(1).optional()
|
|
2265
2640
|
}).strict();
|
|
2266
2641
|
function renderLogEntry(entry) {
|
|
2267
2642
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
2268
2643
|
`;
|
|
2269
2644
|
}
|
|
2645
|
+
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
2270
2646
|
function parseLog(raw) {
|
|
2271
2647
|
const entries = [];
|
|
2272
2648
|
const malformed = [];
|
|
2273
2649
|
const seen = /* @__PURE__ */ new Set();
|
|
2650
|
+
let conflicted = false;
|
|
2274
2651
|
raw.split("\n").forEach((text, index2) => {
|
|
2275
2652
|
if (!text.trim()) return;
|
|
2653
|
+
if (CONFLICT_MARKER.test(text)) {
|
|
2654
|
+
conflicted = true;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2276
2657
|
let value;
|
|
2277
2658
|
try {
|
|
2278
2659
|
value = JSON.parse(text);
|
|
@@ -2293,7 +2674,7 @@ function parseLog(raw) {
|
|
|
2293
2674
|
entries.sort(
|
|
2294
2675
|
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
|
|
2295
2676
|
);
|
|
2296
|
-
return { entries, malformed };
|
|
2677
|
+
return { entries, malformed, conflicted };
|
|
2297
2678
|
}
|
|
2298
2679
|
|
|
2299
2680
|
// src/search-index.ts
|
|
@@ -2342,8 +2723,8 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2342
2723
|
async function isStale(bundlePath2) {
|
|
2343
2724
|
const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2344
2725
|
if (!indexAt) return true;
|
|
2345
|
-
const { readdir:
|
|
2346
|
-
const names = (await
|
|
2726
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
2727
|
+
const names = (await readdir3(bundlePath2).catch(() => [])).filter(
|
|
2347
2728
|
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
2348
2729
|
);
|
|
2349
2730
|
let stale = false;
|
|
@@ -2733,6 +3114,13 @@ function typeRank(record) {
|
|
|
2733
3114
|
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
2734
3115
|
}
|
|
2735
3116
|
|
|
3117
|
+
// src/kb-tags.ts
|
|
3118
|
+
function matchesTags(record, filter) {
|
|
3119
|
+
if (!filter.tags?.length && !filter.excludeTags?.length) return true;
|
|
3120
|
+
const carried = new Set(record.frontmatter.tags ?? []);
|
|
3121
|
+
return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
|
|
3122
|
+
}
|
|
3123
|
+
|
|
2736
3124
|
// src/catalog.ts
|
|
2737
3125
|
var EMPTY_STANDINGS = {
|
|
2738
3126
|
current: 0,
|
|
@@ -2743,7 +3131,7 @@ var EMPTY_STANDINGS = {
|
|
|
2743
3131
|
};
|
|
2744
3132
|
function catalog(bundle, options = {}) {
|
|
2745
3133
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2746
|
-
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
3134
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
|
|
2747
3135
|
conceptId: hit.record.conceptId,
|
|
2748
3136
|
type: hit.record.frontmatter.type,
|
|
2749
3137
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2751,14 +3139,14 @@ function catalog(bundle, options = {}) {
|
|
|
2751
3139
|
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2752
3140
|
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
2753
3141
|
})).sort(byTypeThenTitle);
|
|
2754
|
-
const
|
|
2755
|
-
for (const entry of entries)
|
|
3142
|
+
const standings2 = { ...EMPTY_STANDINGS };
|
|
3143
|
+
for (const entry of entries) standings2[entry.standing] += 1;
|
|
2756
3144
|
return {
|
|
2757
3145
|
entries,
|
|
2758
3146
|
recordCount: entries.length,
|
|
2759
|
-
standings,
|
|
2760
|
-
currentCount:
|
|
2761
|
-
supersededCount:
|
|
3147
|
+
standings: standings2,
|
|
3148
|
+
currentCount: standings2.current,
|
|
3149
|
+
supersededCount: standings2.superseded,
|
|
2762
3150
|
staleCount: entries.filter((entry) => entry.stale).length
|
|
2763
3151
|
};
|
|
2764
3152
|
}
|
|
@@ -2914,9 +3302,13 @@ function hasEdge(edges, edge) {
|
|
|
2914
3302
|
);
|
|
2915
3303
|
}
|
|
2916
3304
|
|
|
3305
|
+
// src/kb-files.ts
|
|
3306
|
+
var STORE_OWNED_FILES = [INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE];
|
|
3307
|
+
|
|
2917
3308
|
// src/kb-gitattributes.ts
|
|
2918
3309
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
2919
|
-
var
|
|
3310
|
+
var GENERATED = "linguist-generated";
|
|
3311
|
+
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union ${GENERATED}=true`;
|
|
2920
3312
|
function parseLine(line) {
|
|
2921
3313
|
const trimmed = line.trim();
|
|
2922
3314
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -2924,23 +3316,39 @@ function parseLine(line) {
|
|
|
2924
3316
|
return pattern === void 0 ? null : { pattern, attrs };
|
|
2925
3317
|
}
|
|
2926
3318
|
function hasMergeDeclaration(contents) {
|
|
3319
|
+
return declares(contents, LOG_FILE, "merge");
|
|
3320
|
+
}
|
|
3321
|
+
function declares(contents, pattern, attribute) {
|
|
2927
3322
|
return contents.split("\n").some((line) => {
|
|
2928
3323
|
const parsed = parseLine(line);
|
|
2929
|
-
if (!parsed || parsed.pattern !==
|
|
3324
|
+
if (!parsed || parsed.pattern !== pattern) return false;
|
|
2930
3325
|
return parsed.attrs.some(
|
|
2931
|
-
(attr) => attr ===
|
|
3326
|
+
(attr) => attr === attribute || attr === `-${attribute}` || attr.startsWith(`${attribute}=`)
|
|
2932
3327
|
);
|
|
2933
3328
|
});
|
|
2934
3329
|
}
|
|
2935
|
-
function
|
|
3330
|
+
function missingGitattributesLines(contents) {
|
|
3331
|
+
const needsMerge = !hasMergeDeclaration(contents);
|
|
3332
|
+
const generated = STORE_OWNED_FILES.filter(
|
|
3333
|
+
// The union-merge line carries the log's `linguist-generated` too, so the
|
|
3334
|
+
// log needs its own line only where that line is already there without it.
|
|
3335
|
+
(file) => !(needsMerge && file === LOG_FILE)
|
|
3336
|
+
).filter((file) => !declares(contents, file, GENERATED)).map((file) => `${file} ${GENERATED}=true`);
|
|
3337
|
+
return needsMerge ? [UNION_MERGE_LINE, ...generated] : generated;
|
|
3338
|
+
}
|
|
3339
|
+
var GITATTRIBUTES_BLOCK = `${missingGitattributesLines("").join("\n")}
|
|
3340
|
+
`;
|
|
3341
|
+
function appendGitattributesLines(contents) {
|
|
3342
|
+
const lines = missingGitattributesLines(contents);
|
|
3343
|
+
if (lines.length === 0) return "";
|
|
2936
3344
|
const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
2937
|
-
return `${separator}${
|
|
3345
|
+
return `${separator}${lines.join("\n")}
|
|
2938
3346
|
`;
|
|
2939
3347
|
}
|
|
2940
3348
|
|
|
2941
3349
|
// src/kb-store.ts
|
|
2942
3350
|
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2943
|
-
var STORE_OWNED =
|
|
3351
|
+
var STORE_OWNED = new Set(STORE_OWNED_FILES);
|
|
2944
3352
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2945
3353
|
var KbStore = class {
|
|
2946
3354
|
constructor(logger = {}) {
|
|
@@ -3016,13 +3424,16 @@ var KbStore = class {
|
|
|
3016
3424
|
return this.parse(conceptId2, raw);
|
|
3017
3425
|
}
|
|
3018
3426
|
/**
|
|
3019
|
-
* Every record in the bundle, optionally narrowed to one type
|
|
3427
|
+
* Every record in the bundle, optionally narrowed to one type and to the
|
|
3428
|
+
* records carrying every tag in `filter.tags`. Selection only — `excludeTags`
|
|
3429
|
+
* is not taken here, because `query`, `catalog` and `load` read through this
|
|
3430
|
+
* and must adjudicate over the whole base.
|
|
3020
3431
|
*
|
|
3021
3432
|
* A file that fails to parse is skipped and logged rather than thrown: one
|
|
3022
3433
|
* malformed record — hand-edited, or written by a producer we don't know —
|
|
3023
3434
|
* must not make the whole bundle unreadable.
|
|
3024
3435
|
*/
|
|
3025
|
-
async list(bundlePath2, type) {
|
|
3436
|
+
async list(bundlePath2, type, filter = {}) {
|
|
3026
3437
|
const root = this.root(bundlePath2);
|
|
3027
3438
|
let names;
|
|
3028
3439
|
try {
|
|
@@ -3036,7 +3447,9 @@ var KbStore = class {
|
|
|
3036
3447
|
DEFAULT_IO_CONCURRENCY,
|
|
3037
3448
|
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
|
|
3038
3449
|
);
|
|
3039
|
-
return records.filter(
|
|
3450
|
+
return records.filter(
|
|
3451
|
+
(record) => record !== null && matchesTags(record, filter)
|
|
3452
|
+
);
|
|
3040
3453
|
}
|
|
3041
3454
|
/**
|
|
3042
3455
|
* Moves a record's status, preserving everything else.
|
|
@@ -3062,12 +3475,16 @@ var KbStore = class {
|
|
|
3062
3475
|
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3063
3476
|
* writing, so it holds the complete current set, and a merge would keep
|
|
3064
3477
|
* stale entries the resolution pass deliberately dropped.
|
|
3478
|
+
*
|
|
3479
|
+
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
3480
|
+
* the frontmatter must not be published back out under an actor stamp.
|
|
3065
3481
|
*/
|
|
3066
3482
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3483
|
+
const checked = anchors.map((anchor) => kbAnchorWriteSchema.parse(anchor));
|
|
3067
3484
|
return this.mutate(
|
|
3068
3485
|
bundlePath2,
|
|
3069
3486
|
conceptId2,
|
|
3070
|
-
(frontmatter) => ({ ...frontmatter, strauss_anchors:
|
|
3487
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: checked }),
|
|
3071
3488
|
{ operation: "anchor-resolve", by: actor }
|
|
3072
3489
|
);
|
|
3073
3490
|
}
|
|
@@ -3153,6 +3570,35 @@ ${answer}
|
|
|
3153
3570
|
`
|
|
3154
3571
|
);
|
|
3155
3572
|
}
|
|
3573
|
+
/**
|
|
3574
|
+
* Removes one record, logged as `sweep`. The only path in this store that
|
|
3575
|
+
* deletes — see the specification for the scope that makes it safe.
|
|
3576
|
+
*
|
|
3577
|
+
* `expected` is re-read and re-checked immediately before the unlink, the
|
|
3578
|
+
* compare-and-swap `mutate` makes: a record retagged or moved out of a
|
|
3579
|
+
* terminal status since the caller listed it is reported, not removed.
|
|
3580
|
+
*/
|
|
3581
|
+
async deleteRecord(bundlePath2, conceptId2, expected, actor = "unknown") {
|
|
3582
|
+
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3583
|
+
const witness = await this.read(bundlePath2, conceptId2);
|
|
3584
|
+
if (!witness) throw new KbRecordNotFoundError(conceptId2);
|
|
3585
|
+
const { tags, strauss_status } = witness.frontmatter;
|
|
3586
|
+
if (!(tags ?? []).includes(expected.tag) || !expected.statuses.includes(strauss_status)) {
|
|
3587
|
+
return "changed-since-listing";
|
|
3588
|
+
}
|
|
3589
|
+
try {
|
|
3590
|
+
await (0, import_promises6.unlink)(target);
|
|
3591
|
+
} catch (error) {
|
|
3592
|
+
if (error.code !== "ENOENT") throw error;
|
|
3593
|
+
throw new KbRecordNotFoundError(conceptId2);
|
|
3594
|
+
}
|
|
3595
|
+
await this.record(this.root(bundlePath2), {
|
|
3596
|
+
operation: "sweep",
|
|
3597
|
+
by: actor,
|
|
3598
|
+
conceptId: conceptId2
|
|
3599
|
+
});
|
|
3600
|
+
return "deleted";
|
|
3601
|
+
}
|
|
3156
3602
|
/**
|
|
3157
3603
|
* Records matching a text query, each carrying its standing.
|
|
3158
3604
|
*
|
|
@@ -3176,9 +3622,10 @@ ${answer}
|
|
|
3176
3622
|
/* @__PURE__ */ new Date(),
|
|
3177
3623
|
await this.detectDrift(narrowed, options.repoRoot)
|
|
3178
3624
|
);
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3625
|
+
const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
|
|
3626
|
+
if (options.includeNonCurrent) return kept;
|
|
3627
|
+
const present = new Set(kept.map((hit) => hit.record.conceptId));
|
|
3628
|
+
return kept.filter(
|
|
3182
3629
|
(hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
|
|
3183
3630
|
);
|
|
3184
3631
|
}
|
|
@@ -3281,14 +3728,17 @@ ${answer}
|
|
|
3281
3728
|
/* @__PURE__ */ new Date(),
|
|
3282
3729
|
await this.detectDrift(wanted, options.repoRoot)
|
|
3283
3730
|
);
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3731
|
+
const kept = adjudicated.filter(
|
|
3732
|
+
(hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
|
|
3733
|
+
);
|
|
3734
|
+
const records = kept.filter((hit) => hit.standing !== "superseded");
|
|
3735
|
+
const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3286
3736
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
3287
3737
|
const bundleDigestValue = bundleDigest(records, superseded);
|
|
3288
3738
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
3289
3739
|
return {
|
|
3290
3740
|
loaded: false,
|
|
3291
|
-
recordCount:
|
|
3741
|
+
recordCount: kept.length,
|
|
3292
3742
|
approxTokens: approxTokens2,
|
|
3293
3743
|
budgetTokens,
|
|
3294
3744
|
message: refusalMessage({
|
|
@@ -3301,7 +3751,7 @@ ${answer}
|
|
|
3301
3751
|
}
|
|
3302
3752
|
return {
|
|
3303
3753
|
loaded: true,
|
|
3304
|
-
recordCount:
|
|
3754
|
+
recordCount: kept.length,
|
|
3305
3755
|
tokensLoaded: approxTokens2,
|
|
3306
3756
|
budgetTokens: options.all ? null : budgetTokens,
|
|
3307
3757
|
records,
|
|
@@ -3384,12 +3834,25 @@ ${answer}
|
|
|
3384
3834
|
}
|
|
3385
3835
|
return expected;
|
|
3386
3836
|
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Drops the derived search index, so the next search rebuilds it.
|
|
3839
|
+
*
|
|
3840
|
+
* `searchBase` re-indexes when a record is newer than the index, which no
|
|
3841
|
+
* deletion makes true — a swept record would stay findable until some other
|
|
3842
|
+
* record was written.
|
|
3843
|
+
*/
|
|
3844
|
+
async dropSearchIndex(bundlePath2) {
|
|
3845
|
+
await (0, import_promises6.unlink)((0, import_node_path7.join)(this.root(bundlePath2), SEARCH_INDEX_FILE)).catch(
|
|
3846
|
+
() => void 0
|
|
3847
|
+
);
|
|
3848
|
+
}
|
|
3387
3849
|
/**
|
|
3388
3850
|
* The log, with unparseable lines reported rather than repaired.
|
|
3389
3851
|
*
|
|
3390
3852
|
* The log is the bundle's only artifact that cannot be reconstructed — the
|
|
3391
3853
|
* records rebuild the index, and the code outlives both, but nothing else
|
|
3392
3854
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
3855
|
+
* Conflict markers are read past rather than reported per line.
|
|
3393
3856
|
*/
|
|
3394
3857
|
async readLog(bundlePath2) {
|
|
3395
3858
|
const raw = await (0, import_promises6.readFile)(
|
|
@@ -3397,6 +3860,13 @@ ${answer}
|
|
|
3397
3860
|
"utf8"
|
|
3398
3861
|
).catch(() => "");
|
|
3399
3862
|
const result = parseLog(raw);
|
|
3863
|
+
if (result.conflicted) {
|
|
3864
|
+
this.logger.warn?.({
|
|
3865
|
+
operation: "kb.log.parse",
|
|
3866
|
+
bundlePath: this.root(bundlePath2),
|
|
3867
|
+
outcome: "conflicted"
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3400
3870
|
for (const bad of result.malformed) {
|
|
3401
3871
|
this.logger.warn?.({
|
|
3402
3872
|
operation: "kb.log.parse",
|
|
@@ -3406,6 +3876,14 @@ ${answer}
|
|
|
3406
3876
|
}
|
|
3407
3877
|
return result;
|
|
3408
3878
|
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Appends one log entry for a move the store cannot see from one base.
|
|
3881
|
+
* Promotion writes into a target base and has to be legible from the source
|
|
3882
|
+
* base too, where nothing was written.
|
|
3883
|
+
*/
|
|
3884
|
+
async note(bundlePath2, entry) {
|
|
3885
|
+
await this.record(this.root(bundlePath2), entry);
|
|
3886
|
+
}
|
|
3409
3887
|
/**
|
|
3410
3888
|
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
3411
3889
|
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
@@ -3496,7 +3974,8 @@ ${answer}
|
|
|
3496
3974
|
/**
|
|
3497
3975
|
* Declares union merge for the log, so two worktrees writing the same
|
|
3498
3976
|
* bundle interleave their `log.jsonl` lines on merge rather than one
|
|
3499
|
-
* side's appends silently losing to git's ordinary line-level merge
|
|
3977
|
+
* side's appends silently losing to git's ordinary line-level merge — and
|
|
3978
|
+
* marks every store-owned file generated, so GitHub collapses it in a diff.
|
|
3500
3979
|
*
|
|
3501
3980
|
* Called from `record` — every path that appends a log line, not just
|
|
3502
3981
|
* `write` — so a bundle only ever mutated through `setStatus`/`verify`/
|
|
@@ -3509,10 +3988,9 @@ ${answer}
|
|
|
3509
3988
|
* race and created the file between the `readFile` below and this call,
|
|
3510
3989
|
* `wx` fails instead of truncating what that writer just wrote, and the
|
|
3511
3990
|
* failure is swallowed by the catch below same as any other best-effort
|
|
3512
|
-
* miss. A file that exists
|
|
3513
|
-
*
|
|
3514
|
-
*
|
|
3515
|
-
* entirely (see `hasMergeDeclaration`).
|
|
3991
|
+
* miss. A file that exists gets only the lines it lacks appended, never a
|
|
3992
|
+
* wholesale rewrite; an attribute it already sets — this one's value or a
|
|
3993
|
+
* user's own — is left alone (see `missingGitattributesLines`).
|
|
3516
3994
|
*
|
|
3517
3995
|
* `readFile` failing is `existing === null` only for `ENOENT` — genuinely
|
|
3518
3996
|
* missing. Any other error (a permission problem, a transient `EMFILE`,
|
|
@@ -3523,12 +4001,12 @@ ${answer}
|
|
|
3523
4001
|
* therefore left untouched and reported as a failure like any other.
|
|
3524
4002
|
*
|
|
3525
4003
|
* Two processes racing the append branch — both read a file without the
|
|
3526
|
-
*
|
|
3527
|
-
* `O_APPEND`, so the result is two copies of the same
|
|
3528
|
-
* torn write, and
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
4004
|
+
* lines, both append them — is possible and left unguarded: `appendFile` is
|
|
4005
|
+
* `O_APPEND`, so the result is two copies of the same lines rather than a
|
|
4006
|
+
* torn write, and the next call sees a duplicate declaration as "already
|
|
4007
|
+
* declared". A cheap-to-detect, harmless-to-leave residue, not a reason to
|
|
4008
|
+
* add a cross-process lock (see `ARCHITECTURE.md`'s rejection of one for
|
|
4009
|
+
* the same trade on records).
|
|
3532
4010
|
*
|
|
3533
4011
|
* Best-effort, like the log append it precedes: failing to write this
|
|
3534
4012
|
* file must not fail the mutation it guards.
|
|
@@ -3545,7 +4023,7 @@ ${answer}
|
|
|
3545
4023
|
}
|
|
3546
4024
|
if (existing === null) {
|
|
3547
4025
|
try {
|
|
3548
|
-
await (0, import_promises6.writeFile)(target,
|
|
4026
|
+
await (0, import_promises6.writeFile)(target, appendGitattributesLines(""), {
|
|
3549
4027
|
encoding: "utf8",
|
|
3550
4028
|
flag: "wx"
|
|
3551
4029
|
});
|
|
@@ -3565,8 +4043,9 @@ ${answer}
|
|
|
3565
4043
|
});
|
|
3566
4044
|
return;
|
|
3567
4045
|
}
|
|
3568
|
-
|
|
3569
|
-
|
|
4046
|
+
const addition = appendGitattributesLines(existing);
|
|
4047
|
+
if (addition) {
|
|
4048
|
+
await (0, import_promises6.appendFile)(target, addition, "utf8");
|
|
3570
4049
|
this.logger.info?.({
|
|
3571
4050
|
operation: "kb.gitattributes.ensure",
|
|
3572
4051
|
bundlePath: root,
|
|
@@ -3675,7 +4154,7 @@ var composeInputSchema = import_zod4.z.object({
|
|
|
3675
4154
|
why: import_zod4.z.string().min(1),
|
|
3676
4155
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
3677
4156
|
sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
|
|
3678
|
-
anchors: import_zod4.z.array(
|
|
4157
|
+
anchors: import_zod4.z.array(kbAnchorWriteSchema).optional(),
|
|
3679
4158
|
sources: import_zod4.z.array(kbSourceSchema).optional(),
|
|
3680
4159
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
3681
4160
|
assumption: import_zod4.z.boolean().optional(),
|
|
@@ -3788,14 +4267,17 @@ function asBudgets(value) {
|
|
|
3788
4267
|
if (value === null || typeof value !== "object") return {};
|
|
3789
4268
|
const table2 = value;
|
|
3790
4269
|
const pick = (key2, min) => {
|
|
3791
|
-
const
|
|
3792
|
-
return typeof
|
|
4270
|
+
const raw2 = table2[key2];
|
|
4271
|
+
return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
|
|
3793
4272
|
};
|
|
3794
4273
|
const budgetTokens = pick("budgetTokens", 1);
|
|
3795
4274
|
const fullUnderTokens = pick("fullUnderTokens", 0);
|
|
4275
|
+
const raw = table2["excludeTags"];
|
|
4276
|
+
const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
3796
4277
|
return {
|
|
3797
4278
|
...budgetTokens ? { budgetTokens } : {},
|
|
3798
|
-
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
|
|
4279
|
+
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
|
|
4280
|
+
...excludeTags ? { excludeTags } : {}
|
|
3799
4281
|
};
|
|
3800
4282
|
}
|
|
3801
4283
|
function contextProfileBudgets(manifest, profile) {
|
|
@@ -4111,7 +4593,7 @@ function preamble() {
|
|
|
4111
4593
|
"tokens."
|
|
4112
4594
|
].join("\n");
|
|
4113
4595
|
}
|
|
4114
|
-
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
|
|
4596
|
+
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
|
|
4115
4597
|
const bundle = await store.list(absolutePath);
|
|
4116
4598
|
if (bundle.length === 0) {
|
|
4117
4599
|
return {
|
|
@@ -4125,7 +4607,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
4125
4607
|
let degradedFrom;
|
|
4126
4608
|
if (fullCap > 0) {
|
|
4127
4609
|
const full = await store.load(absolutePath, {
|
|
4128
|
-
budgetTokens: fullCap
|
|
4610
|
+
budgetTokens: fullCap,
|
|
4611
|
+
excludeTags
|
|
4129
4612
|
});
|
|
4130
4613
|
if (!full.loaded && pinMode === "full") {
|
|
4131
4614
|
degradedFrom = { approxTokens: full.approxTokens };
|
|
@@ -4155,7 +4638,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
|
|
|
4155
4638
|
};
|
|
4156
4639
|
}
|
|
4157
4640
|
}
|
|
4158
|
-
const adjudicated = adjudicate(bundle, bundle)
|
|
4641
|
+
const adjudicated = adjudicate(bundle, bundle).filter(
|
|
4642
|
+
(hit) => matchesTags(hit.record, { excludeTags })
|
|
4643
|
+
);
|
|
4159
4644
|
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
4160
4645
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
4161
4646
|
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
@@ -4176,6 +4661,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
4176
4661
|
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
4177
4662
|
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
4178
4663
|
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
4664
|
+
const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
|
|
4179
4665
|
const pins = merged.pins.filter(
|
|
4180
4666
|
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
4181
4667
|
);
|
|
@@ -4196,7 +4682,8 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
4196
4682
|
pin.absolutePath,
|
|
4197
4683
|
fullUnderTokens,
|
|
4198
4684
|
pin.mode,
|
|
4199
|
-
budgetTokens
|
|
4685
|
+
budgetTokens,
|
|
4686
|
+
excludeTags
|
|
4200
4687
|
),
|
|
4201
4688
|
frozen: pin.frozen === true
|
|
4202
4689
|
}))
|
|
@@ -4327,7 +4814,7 @@ function kbJsonSchemas() {
|
|
|
4327
4814
|
|
|
4328
4815
|
// src/match-diff.ts
|
|
4329
4816
|
function matchToDiff(files, records, options = {}) {
|
|
4330
|
-
const ranges =
|
|
4817
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
4331
4818
|
const anchored = records.filter(
|
|
4332
4819
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
4333
4820
|
);
|
|
@@ -4345,8 +4832,8 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4345
4832
|
let precision = "symbol";
|
|
4346
4833
|
for (const { record, anchors } of candidates) {
|
|
4347
4834
|
const placement = place(anchors, file.filePath, hunk, ranges);
|
|
4348
|
-
if (placement === "miss") continue;
|
|
4349
|
-
if (placement === "file") precision = "file";
|
|
4835
|
+
if (placement.kind === "miss") continue;
|
|
4836
|
+
if (placement.kind === "file") precision = "file";
|
|
4350
4837
|
hits.push(record);
|
|
4351
4838
|
}
|
|
4352
4839
|
if (!hits.length) continue;
|
|
@@ -4360,19 +4847,51 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4360
4847
|
}
|
|
4361
4848
|
return matches3;
|
|
4362
4849
|
}
|
|
4850
|
+
function placeOnHunk(record, filePath, hunk, symbolRanges = []) {
|
|
4851
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
4852
|
+
(anchor) => normalize(anchor.file) === normalize(filePath)
|
|
4853
|
+
);
|
|
4854
|
+
return place(anchors, filePath, hunk, asIndex(symbolRanges));
|
|
4855
|
+
}
|
|
4856
|
+
function asIndex(ranges) {
|
|
4857
|
+
return isIndex(ranges) ? ranges : symbolRangeIndex(ranges);
|
|
4858
|
+
}
|
|
4859
|
+
function isIndex(ranges) {
|
|
4860
|
+
return !Array.isArray(ranges);
|
|
4861
|
+
}
|
|
4862
|
+
function anchorOnHunk(record, filePath, hunk, symbolRanges = []) {
|
|
4863
|
+
return placeOnHunk(record, filePath, hunk, symbolRanges).anchor;
|
|
4864
|
+
}
|
|
4363
4865
|
function place(anchors, filePath, hunk, ranges) {
|
|
4364
|
-
let fallback = "miss";
|
|
4866
|
+
let fallback = { kind: "miss" };
|
|
4365
4867
|
for (const anchor of anchors) {
|
|
4366
|
-
if (
|
|
4367
|
-
|
|
4868
|
+
if (side(anchor.side) !== side(hunk.side)) continue;
|
|
4869
|
+
if (anchor.span) {
|
|
4870
|
+
if (overlaps(
|
|
4871
|
+
{ startLine: anchor.span.start, endLine: anchor.span.end },
|
|
4872
|
+
hunk
|
|
4873
|
+
)) {
|
|
4874
|
+
return { kind: "symbol", anchor };
|
|
4875
|
+
}
|
|
4876
|
+
continue;
|
|
4877
|
+
}
|
|
4878
|
+
if (!anchor.symbol) return { kind: "file", anchor };
|
|
4879
|
+
const resolved = ranges.get(
|
|
4880
|
+
key(filePath, anchor.symbol, side(anchor.side))
|
|
4881
|
+
);
|
|
4368
4882
|
if (!resolved?.length) {
|
|
4369
|
-
fallback = "file";
|
|
4883
|
+
if (fallback.kind === "miss") fallback = { kind: "file", anchor };
|
|
4370
4884
|
continue;
|
|
4371
4885
|
}
|
|
4372
|
-
if (resolved.some((range) => overlaps(range, hunk)))
|
|
4886
|
+
if (resolved.some((range) => overlaps(range, hunk))) {
|
|
4887
|
+
return { kind: "symbol", anchor };
|
|
4888
|
+
}
|
|
4373
4889
|
}
|
|
4374
4890
|
return fallback;
|
|
4375
4891
|
}
|
|
4892
|
+
function side(value) {
|
|
4893
|
+
return value ?? "new";
|
|
4894
|
+
}
|
|
4376
4895
|
function overlaps(range, hunk) {
|
|
4377
4896
|
return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;
|
|
4378
4897
|
}
|
|
@@ -4390,42 +4909,211 @@ function order(records) {
|
|
|
4390
4909
|
)
|
|
4391
4910
|
);
|
|
4392
4911
|
}
|
|
4393
|
-
function
|
|
4912
|
+
function symbolRangeIndex(ranges) {
|
|
4394
4913
|
const byKey = /* @__PURE__ */ new Map();
|
|
4395
4914
|
for (const range of ranges) {
|
|
4396
|
-
const id = key(range.file, range.symbol);
|
|
4915
|
+
const id = key(range.file, range.symbol, side(range.side));
|
|
4397
4916
|
byKey.set(id, [...byKey.get(id) ?? [], range]);
|
|
4398
4917
|
}
|
|
4399
4918
|
return byKey;
|
|
4400
4919
|
}
|
|
4401
|
-
function key(file, symbol) {
|
|
4402
|
-
return `${normalize(file)}#${symbol}`;
|
|
4920
|
+
function key(file, symbol, at2) {
|
|
4921
|
+
return `${normalize(file)}#${symbol}#${at2}`;
|
|
4403
4922
|
}
|
|
4404
4923
|
function normalize(path) {
|
|
4405
4924
|
return path.replace(/^\.\//, "");
|
|
4406
4925
|
}
|
|
4407
4926
|
|
|
4408
|
-
// src/
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4927
|
+
// src/classify/model.ts
|
|
4928
|
+
var KB_CLASSES = [
|
|
4929
|
+
"test",
|
|
4930
|
+
"config",
|
|
4931
|
+
"ci",
|
|
4932
|
+
"docs",
|
|
4933
|
+
"lockfile",
|
|
4934
|
+
"generated",
|
|
4935
|
+
"boilerplate",
|
|
4936
|
+
"rename",
|
|
4937
|
+
"source"
|
|
4938
|
+
];
|
|
4939
|
+
var DEFAULT_THRESHOLDS = {
|
|
4940
|
+
boilerplate: 0.8,
|
|
4941
|
+
rename: 90
|
|
4942
|
+
};
|
|
4943
|
+
|
|
4944
|
+
// src/classify/rules.ts
|
|
4945
|
+
var PATH_RULES = [
|
|
4946
|
+
{
|
|
4947
|
+
name: "test-path",
|
|
4948
|
+
class: "test",
|
|
4949
|
+
test: /(^|\/)(__tests__|__mocks__|tests?)\/|\.(spec|test)\.[^/]+$/
|
|
4950
|
+
},
|
|
4951
|
+
{
|
|
4952
|
+
name: "ci-path",
|
|
4953
|
+
class: "ci",
|
|
4954
|
+
test: /(^|\/)\.github\/|(^|\/)(\.circleci|\.buildkite|\.gitlab|ci)\/[^/]*\.ya?ml$|(^|\/)Dockerfile(\.[^/]*)?$|\.tf$/
|
|
4955
|
+
},
|
|
4956
|
+
{
|
|
4957
|
+
name: "docs-path",
|
|
4958
|
+
class: "docs",
|
|
4959
|
+
test: /\.md$|(^|\/)docs\/|(^|\/)LICENSE(\.(md|txt|rst))?$/
|
|
4960
|
+
},
|
|
4961
|
+
{
|
|
4962
|
+
name: "lockfile-path",
|
|
4963
|
+
class: "lockfile",
|
|
4964
|
+
test: /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|Cargo\.lock|go\.sum)$/
|
|
4965
|
+
},
|
|
4966
|
+
{
|
|
4967
|
+
name: "config-path",
|
|
4968
|
+
class: "config",
|
|
4969
|
+
// `.jsonl` rides with `.json`: an append-only log of JSON is configuration
|
|
4970
|
+
// data too, and calling it source would send a reviewer to read it. Every
|
|
4971
|
+
// arm is anchored at both ends: `src/tsconfig-loader.ts` and `report.env.ts`
|
|
4972
|
+
// are source, not config.
|
|
4973
|
+
test: /\.(jsonc?|jsonl|ya?ml|toml|ini)$|(^|\/)\.env(?![^/]*\.[cm]?[jt]sx?$)([.-][^/]*)?$|[^/]+\.env$|(^|\/)tsconfig[^/]*\.json$|\.config\.[^/]+$|(^|\/)\.(eslintrc|prettierrc)[^/]*$/
|
|
4974
|
+
}
|
|
4975
|
+
];
|
|
4976
|
+
var HEADER_LINES = 20;
|
|
4977
|
+
var GENERATED_MARKERS = [
|
|
4978
|
+
/@generated\b/i,
|
|
4979
|
+
/\bdo not edit\b/i,
|
|
4980
|
+
/\bcode generated by\b/i,
|
|
4981
|
+
/\bthis file was automatically generated\b/i
|
|
4982
|
+
];
|
|
4983
|
+
var BOILERPLATE_SHAPES = [
|
|
4984
|
+
{ name: "import", test: /^import\b|^\}\s*from\s+["']/ },
|
|
4985
|
+
{ name: "re-export", test: /^export\s+(\*|\{|type\s*[{*])/ },
|
|
4986
|
+
{
|
|
4987
|
+
name: "class-shell",
|
|
4988
|
+
test: /^(export\s+)?(default\s+)?(abstract\s+)?class\s+[\w$]+[^{]*\{\s*\}$/
|
|
4989
|
+
},
|
|
4990
|
+
{ name: "punctuation", test: /^[{}()[\],;]+$/ }
|
|
4991
|
+
];
|
|
4992
|
+
function generatedMarker(lines) {
|
|
4993
|
+
for (const line of lines.slice(0, HEADER_LINES)) {
|
|
4994
|
+
const hit = GENERATED_MARKERS.find((marker) => marker.test(line));
|
|
4995
|
+
if (hit) return hit.source.replaceAll("\\b", "");
|
|
4996
|
+
}
|
|
4997
|
+
return void 0;
|
|
4998
|
+
}
|
|
4999
|
+
function isBoilerplateLine(line) {
|
|
5000
|
+
return BOILERPLATE_SHAPES.some((shape) => shape.test.test(line));
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
// src/classify/classify.ts
|
|
5004
|
+
function classifyDiff(files, options = {}) {
|
|
5005
|
+
const overrides = currentOverrides(options.records ?? [], options.now);
|
|
5006
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
5007
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
|
|
5008
|
+
return files.map((file) => classifyFile(file, overrides, ranges, thresholds));
|
|
5009
|
+
}
|
|
5010
|
+
var OVERRIDE_CLASS = /* @__PURE__ */ new Map([
|
|
5011
|
+
["review:generated", "generated"],
|
|
5012
|
+
["review:boilerplate", "boilerplate"],
|
|
5013
|
+
["review:move", "rename"]
|
|
5014
|
+
]);
|
|
5015
|
+
var WHOLE_FILE = {
|
|
5016
|
+
startLine: 1,
|
|
5017
|
+
endLine: Number.MAX_SAFE_INTEGER
|
|
5018
|
+
};
|
|
5019
|
+
function classifyFile(file, overrides, ranges, thresholds) {
|
|
5020
|
+
const whole = overrides.find(
|
|
5021
|
+
({ record }) => placeOnHunk(record, file.filePath, WHOLE_FILE, ranges).kind === "file"
|
|
5022
|
+
);
|
|
5023
|
+
const verdict = whole ? verdictOf(whole) : heuristic(file, thresholds);
|
|
5024
|
+
const hunks = file.hunks.map((hunk) => {
|
|
5025
|
+
const hit = whole ?? overrides.find(
|
|
5026
|
+
({ record }) => placeOnHunk(record, file.filePath, hunk, ranges).kind !== "miss"
|
|
5027
|
+
);
|
|
5028
|
+
return {
|
|
5029
|
+
startLine: hunk.startLine,
|
|
5030
|
+
endLine: hunk.endLine,
|
|
5031
|
+
...hit ? verdictOf(hit) : verdict
|
|
5032
|
+
};
|
|
5033
|
+
});
|
|
5034
|
+
return {
|
|
5035
|
+
filePath: file.filePath,
|
|
5036
|
+
...verdict,
|
|
5037
|
+
...file.renamedFrom ? { renamedFrom: file.renamedFrom } : {},
|
|
5038
|
+
...hunks.some((hunk) => hunk.class !== verdict.class) ? { hunks } : {}
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
function verdictOf(override) {
|
|
5042
|
+
return {
|
|
5043
|
+
class: override.class,
|
|
5044
|
+
reason: `kb-override ${override.record.conceptId}`
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
function heuristic(file, thresholds) {
|
|
5048
|
+
const marker = generatedMarker(file.header ?? headOfDiff(file));
|
|
5049
|
+
if (marker)
|
|
5050
|
+
return { class: "generated", reason: `generated-header ${marker}` };
|
|
5051
|
+
const rule = PATH_RULES.find((entry) => entry.test.test(file.filePath));
|
|
5052
|
+
if (rule) return { class: rule.class, reason: rule.name };
|
|
5053
|
+
if (file.renamedFrom && !file.hunks.length && (file.similarity ?? 100) >= thresholds.rename) {
|
|
5054
|
+
return { class: "rename", reason: `rename ${file.renamedFrom}` };
|
|
5055
|
+
}
|
|
5056
|
+
const share = boilerplateShare(file);
|
|
5057
|
+
if (share !== void 0 && share >= thresholds.boilerplate) {
|
|
5058
|
+
return {
|
|
5059
|
+
class: "boilerplate",
|
|
5060
|
+
reason: `boilerplate ${Math.round(share * 100)}%`
|
|
5061
|
+
};
|
|
5062
|
+
}
|
|
5063
|
+
return { class: "source", reason: "default" };
|
|
5064
|
+
}
|
|
5065
|
+
function headOfDiff(file) {
|
|
5066
|
+
return file.hunks.flatMap(
|
|
5067
|
+
(hunk) => (hunk.side ?? "new") === "new" && hunk.startLine <= HEADER_LINES ? (hunk.lines ?? []).slice(0, HEADER_LINES - hunk.startLine + 1) : []
|
|
5068
|
+
);
|
|
5069
|
+
}
|
|
5070
|
+
function boilerplateShare(file) {
|
|
5071
|
+
const lines = file.hunks.flatMap((hunk) => hunk.lines ?? []).map((line) => line.trim()).filter(Boolean);
|
|
5072
|
+
if (!lines.length) return void 0;
|
|
5073
|
+
return lines.filter(isBoilerplateLine).length / lines.length;
|
|
5074
|
+
}
|
|
5075
|
+
function currentOverrides(records, now) {
|
|
5076
|
+
const tagged = records.flatMap((record) => {
|
|
5077
|
+
if (record.frontmatter.type !== "fact") return [];
|
|
5078
|
+
const tag = (record.frontmatter.tags ?? []).find(
|
|
5079
|
+
(entry) => OVERRIDE_CLASS.has(entry)
|
|
5080
|
+
);
|
|
5081
|
+
const asserted = tag && OVERRIDE_CLASS.get(tag);
|
|
5082
|
+
return asserted ? [{ record, class: asserted }] : [];
|
|
5083
|
+
});
|
|
5084
|
+
const current = new Set(
|
|
5085
|
+
adjudicate(
|
|
5086
|
+
tagged.map(({ record }) => record),
|
|
5087
|
+
records,
|
|
5088
|
+
now
|
|
5089
|
+
).filter((entry) => entry.standing === "current").map((entry) => entry.record.conceptId)
|
|
5090
|
+
);
|
|
5091
|
+
return tagged.filter(({ record }) => current.has(record.conceptId)).sort(
|
|
5092
|
+
(left, right) => left.record.conceptId.localeCompare(right.record.conceptId)
|
|
5093
|
+
);
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
// src/validate.ts
|
|
5097
|
+
function validateBundle(records) {
|
|
5098
|
+
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
5099
|
+
const problems = [];
|
|
5100
|
+
const report = (check, conceptId2, note, severity = "error") => problems.push({ check, conceptId: conceptId2, note, severity });
|
|
5101
|
+
for (const record of records) {
|
|
5102
|
+
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
5103
|
+
if (!isKbRecordType(fm.type)) {
|
|
5104
|
+
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
5105
|
+
}
|
|
5106
|
+
if (fm.strauss_status === "superseded") {
|
|
5107
|
+
const by = fm.strauss_superseded_by;
|
|
5108
|
+
if (!by) {
|
|
5109
|
+
report("superseded_by", conceptId2, "superseded with no replacement");
|
|
5110
|
+
} else if (!byId.has(by)) {
|
|
5111
|
+
report("superseded_by", conceptId2, `replacement ${by} is missing`);
|
|
5112
|
+
} else if (!byId.get(by)?.frontmatter.strauss_supersedes?.includes(conceptId2)) {
|
|
5113
|
+
report("backlink", by, `does not list ${conceptId2} in supersedes`);
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
for (const old of fm.strauss_supersedes ?? []) {
|
|
4429
5117
|
const previous = byId.get(old);
|
|
4430
5118
|
if (!previous) {
|
|
4431
5119
|
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
@@ -4464,6 +5152,34 @@ function validateBundle(records) {
|
|
|
4464
5152
|
}
|
|
4465
5153
|
}
|
|
4466
5154
|
for (const anchor of fm.strauss_anchors ?? []) {
|
|
5155
|
+
if (anchor.span && anchor.symbol) {
|
|
5156
|
+
report(
|
|
5157
|
+
"anchor_span",
|
|
5158
|
+
conceptId2,
|
|
5159
|
+
`anchor ${anchor.file} names both a symbol and a span \u2014 one or the other`
|
|
5160
|
+
);
|
|
5161
|
+
}
|
|
5162
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
5163
|
+
report(
|
|
5164
|
+
"anchor_span",
|
|
5165
|
+
conceptId2,
|
|
5166
|
+
`anchor ${anchor.file} span ${anchor.span.start}-${anchor.span.end} ends before it starts`
|
|
5167
|
+
);
|
|
5168
|
+
}
|
|
5169
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
5170
|
+
report(
|
|
5171
|
+
"anchor_span",
|
|
5172
|
+
conceptId2,
|
|
5173
|
+
`anchor ${anchor.file} is a span with hash_kind: "ast" \u2014 a span is hashed raw`
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
5177
|
+
report(
|
|
5178
|
+
"anchor_side",
|
|
5179
|
+
conceptId2,
|
|
5180
|
+
`anchor ${anchor.file} is side: "old" with no ref`
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
4467
5183
|
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
4468
5184
|
report(
|
|
4469
5185
|
"anchor_repo",
|
|
@@ -4510,14 +5226,19 @@ var DAY_MS = 864e5;
|
|
|
4510
5226
|
function anchorResolverCounts(bundle) {
|
|
4511
5227
|
let treeSitter = 0;
|
|
4512
5228
|
let regex = 0;
|
|
5229
|
+
let span2 = 0;
|
|
5230
|
+
let oldSide = 0;
|
|
4513
5231
|
for (const record of bundle) {
|
|
4514
5232
|
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4515
|
-
if (!anchor.hash
|
|
4516
|
-
if (anchor.
|
|
5233
|
+
if (!anchor.hash) continue;
|
|
5234
|
+
if (anchor.side === "old") oldSide += 1;
|
|
5235
|
+
if (anchor.span) span2 += 1;
|
|
5236
|
+
else if (!anchor.symbol) continue;
|
|
5237
|
+
else if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
4517
5238
|
else regex += 1;
|
|
4518
5239
|
}
|
|
4519
5240
|
}
|
|
4520
|
-
return { total: treeSitter + regex, treeSitter, regex };
|
|
5241
|
+
return { total: treeSitter + regex + span2, treeSitter, regex, span: span2, oldSide };
|
|
4521
5242
|
}
|
|
4522
5243
|
function doctor(bundle, options = {}) {
|
|
4523
5244
|
const thresholds = {
|
|
@@ -4527,7 +5248,7 @@ function doctor(bundle, options = {}) {
|
|
|
4527
5248
|
};
|
|
4528
5249
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
4529
5250
|
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
4530
|
-
const
|
|
5251
|
+
const standings2 = new Map(
|
|
4531
5252
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
4532
5253
|
);
|
|
4533
5254
|
const inForce = adjudicated.filter(
|
|
@@ -4540,7 +5261,7 @@ function doctor(bundle, options = {}) {
|
|
|
4540
5261
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
4541
5262
|
group("orphaned", orphaned(bundle)),
|
|
4542
5263
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
4543
|
-
group("superseded-but-cited", supersededButCited(bundle,
|
|
5264
|
+
group("superseded-but-cited", supersededButCited(bundle, standings2)),
|
|
4544
5265
|
group("drifted", drifted(inForce)),
|
|
4545
5266
|
group("unchecked", unchecked(inForce))
|
|
4546
5267
|
];
|
|
@@ -4706,14 +5427,14 @@ function brokenSupersession(bundle, adjudicated) {
|
|
|
4706
5427
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
4707
5428
|
);
|
|
4708
5429
|
}
|
|
4709
|
-
function supersededButCited(bundle,
|
|
5430
|
+
function supersededButCited(bundle, standings2) {
|
|
4710
5431
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
4711
5432
|
const findings = [];
|
|
4712
5433
|
for (const record of bundle) {
|
|
4713
|
-
const standing =
|
|
5434
|
+
const standing = standings2.get(record.conceptId);
|
|
4714
5435
|
if (standing === "superseded" || standing === "rejected") continue;
|
|
4715
5436
|
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
4716
|
-
const targetStanding =
|
|
5437
|
+
const targetStanding = standings2.get(target.conceptId);
|
|
4717
5438
|
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
4718
5439
|
continue;
|
|
4719
5440
|
}
|
|
@@ -4857,6 +5578,9 @@ var import_zod9 = require("zod");
|
|
|
4857
5578
|
var import_zod8 = require("zod");
|
|
4858
5579
|
var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
4859
5580
|
var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
5581
|
+
var TAGS = import_zod8.z.array(import_zod8.z.string().min(1)).optional().describe(
|
|
5582
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
5583
|
+
);
|
|
4860
5584
|
var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
|
|
4861
5585
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
4862
5586
|
);
|
|
@@ -4878,6 +5602,41 @@ function argvFlag(argv, name) {
|
|
|
4878
5602
|
}
|
|
4879
5603
|
return value;
|
|
4880
5604
|
}
|
|
5605
|
+
function argvFlags(argv, name) {
|
|
5606
|
+
const values = [];
|
|
5607
|
+
for (const [at2, arg] of argv.entries()) {
|
|
5608
|
+
if (arg.startsWith(`${name}=`)) {
|
|
5609
|
+
const value = arg.slice(name.length + 1);
|
|
5610
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
5611
|
+
values.push(value);
|
|
5612
|
+
} else if (arg === name) {
|
|
5613
|
+
const value = argv[at2 + 1];
|
|
5614
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
5615
|
+
throw new KbMissingFlagValueError(name);
|
|
5616
|
+
}
|
|
5617
|
+
values.push(value);
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
return values;
|
|
5621
|
+
}
|
|
5622
|
+
function argvWithout(argv, ...names) {
|
|
5623
|
+
const kept = [];
|
|
5624
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
5625
|
+
const arg = argv[at2];
|
|
5626
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
5627
|
+
if (names.includes(arg)) {
|
|
5628
|
+
at2 += 1;
|
|
5629
|
+
continue;
|
|
5630
|
+
}
|
|
5631
|
+
kept.push(arg);
|
|
5632
|
+
}
|
|
5633
|
+
return kept;
|
|
5634
|
+
}
|
|
5635
|
+
function argvPositional(argv, ...names) {
|
|
5636
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
5637
|
+
(arg) => !arg.startsWith("--")
|
|
5638
|
+
);
|
|
5639
|
+
}
|
|
4881
5640
|
|
|
4882
5641
|
// src/commands/anchor-resolve.ts
|
|
4883
5642
|
function resolverSummary(results) {
|
|
@@ -4941,6 +5700,7 @@ var anchorResolveCommand = define({
|
|
|
4941
5700
|
const base2 = {
|
|
4942
5701
|
file: anchor.file,
|
|
4943
5702
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5703
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
4944
5704
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
4945
5705
|
// and now resolves to nothing is a broken anchor, and the exit code
|
|
4946
5706
|
// has to be able to tell it from one nobody ever stamped.
|
|
@@ -5117,12 +5877,18 @@ async function readSources(anchors, root, offline) {
|
|
|
5117
5877
|
const foreign = new Map(
|
|
5118
5878
|
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
5119
5879
|
);
|
|
5120
|
-
const local = anchors.filter(
|
|
5880
|
+
const local = anchors.filter(
|
|
5881
|
+
(anchor) => !foreign.get(anchor) && anchor.side !== "old"
|
|
5882
|
+
);
|
|
5883
|
+
const committed = anchors.filter(
|
|
5884
|
+
(anchor) => !foreign.get(anchor) && anchor.side === "old"
|
|
5885
|
+
);
|
|
5121
5886
|
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
5122
5887
|
const reads = await readAnchorFiles(
|
|
5123
5888
|
local.map((anchor) => anchor.file),
|
|
5124
5889
|
anchorFileReader(root)
|
|
5125
5890
|
);
|
|
5891
|
+
const atRef = await readCommitted(root, committed);
|
|
5126
5892
|
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
5127
5893
|
offline
|
|
5128
5894
|
});
|
|
@@ -5134,6 +5900,13 @@ async function readSources(anchors, root, offline) {
|
|
|
5134
5900
|
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
5135
5901
|
);
|
|
5136
5902
|
}
|
|
5903
|
+
for (const anchor of committed) {
|
|
5904
|
+
const read = atRef.get(atRefKey(anchor));
|
|
5905
|
+
sources.set(
|
|
5906
|
+
anchor,
|
|
5907
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
5908
|
+
);
|
|
5909
|
+
}
|
|
5137
5910
|
for (const anchor of remote) {
|
|
5138
5911
|
const repo = anchor.repo;
|
|
5139
5912
|
const key2 = normalizeRepoUrl(repo);
|
|
@@ -5194,25 +5967,39 @@ var import_zod12 = require("zod");
|
|
|
5194
5967
|
var catalogCommand = define({
|
|
5195
5968
|
name: "catalog",
|
|
5196
5969
|
tool: "kb_catalog",
|
|
5197
|
-
usage: "catalog [type]",
|
|
5970
|
+
usage: "catalog [type] [--tag T]...",
|
|
5198
5971
|
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.",
|
|
5199
5972
|
input: import_zod12.z.object({
|
|
5200
5973
|
bundlePath,
|
|
5201
|
-
type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
|
|
5974
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
|
|
5975
|
+
tags: TAGS
|
|
5202
5976
|
}),
|
|
5203
|
-
fromArgv: (argv, path) =>
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5977
|
+
fromArgv: (argv, path) => {
|
|
5978
|
+
const tags = argvFlags(argv, "--tag");
|
|
5979
|
+
const type = argvPositional(argv, "--tag");
|
|
5980
|
+
return {
|
|
5981
|
+
bundlePath: path,
|
|
5982
|
+
...type ? { type } : {},
|
|
5983
|
+
...tags.length ? { tags } : {}
|
|
5984
|
+
};
|
|
5985
|
+
},
|
|
5986
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => render(
|
|
5987
|
+
await store.catalog(path, {
|
|
5988
|
+
...type ? { type } : {},
|
|
5989
|
+
...tags ? { tags } : {}
|
|
5990
|
+
}),
|
|
5209
5991
|
path,
|
|
5210
|
-
type
|
|
5992
|
+
type,
|
|
5993
|
+
tags
|
|
5211
5994
|
)
|
|
5212
5995
|
});
|
|
5213
|
-
function render(result, bundle, type) {
|
|
5996
|
+
function render(result, bundle, type, tags) {
|
|
5997
|
+
const narrowed = [
|
|
5998
|
+
...type ? [type] : [],
|
|
5999
|
+
...tags?.length ? [`tags: ${tags.join(", ")}`] : []
|
|
6000
|
+
].join(" \xB7 ");
|
|
5214
6001
|
const lines = [
|
|
5215
|
-
`# KB Catalog${
|
|
6002
|
+
`# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
|
|
5216
6003
|
`bundle: ${bundle}`,
|
|
5217
6004
|
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
5218
6005
|
];
|
|
@@ -5224,7 +6011,7 @@ function render(result, bundle, type) {
|
|
|
5224
6011
|
lines.push("");
|
|
5225
6012
|
if (!result.entries.length) {
|
|
5226
6013
|
lines.push(
|
|
5227
|
-
|
|
6014
|
+
narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
|
|
5228
6015
|
);
|
|
5229
6016
|
} else {
|
|
5230
6017
|
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
@@ -5252,135 +6039,12 @@ function count(value, noun) {
|
|
|
5252
6039
|
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
5253
6040
|
}
|
|
5254
6041
|
|
|
5255
|
-
// src/commands/
|
|
5256
|
-
var
|
|
5257
|
-
var
|
|
5258
|
-
|
|
5259
|
-
tool: "kb_context",
|
|
5260
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
5261
|
-
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
5262
|
-
input: import_zod13.z.object({
|
|
5263
|
-
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
5264
|
-
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
5265
|
-
),
|
|
5266
|
-
fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
5267
|
-
"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."
|
|
5268
|
-
),
|
|
5269
|
-
profile: import_zod13.z.string().optional().describe(
|
|
5270
|
-
"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."
|
|
5271
|
-
),
|
|
5272
|
-
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
5273
|
-
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
5274
|
-
),
|
|
5275
|
-
event: import_zod13.z.string().optional().describe(
|
|
5276
|
-
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
5277
|
-
)
|
|
5278
|
-
}),
|
|
5279
|
-
fromArgv: (argv) => {
|
|
5280
|
-
const budget = argvFlag(argv, "--budget");
|
|
5281
|
-
const fullUnder = argvFlag(argv, "--full-under");
|
|
5282
|
-
const profile = argvFlag(argv, "--profile");
|
|
5283
|
-
const format = argvFlag(argv, "--format");
|
|
5284
|
-
const event = argvFlag(argv, "--event");
|
|
5285
|
-
return {
|
|
5286
|
-
...budget ? { budgetTokens: Number(budget) } : {},
|
|
5287
|
-
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
5288
|
-
...profile ? { profile } : {},
|
|
5289
|
-
...format ? { format } : {},
|
|
5290
|
-
...event ? { event } : {}
|
|
5291
|
-
};
|
|
5292
|
-
},
|
|
5293
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
|
|
5294
|
-
const result = await buildContext(store, process.cwd(), {
|
|
5295
|
-
...budgetTokens ? { budgetTokens } : {},
|
|
5296
|
-
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
5297
|
-
...profile ? { profile } : {},
|
|
5298
|
-
// Degradations — a full pin that could not fit, a refused block — go
|
|
5299
|
-
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
5300
|
-
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
5301
|
-
// see budget pressure without reading injected context.
|
|
5302
|
-
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
5303
|
-
`)
|
|
5304
|
-
});
|
|
5305
|
-
if (!result.block) return "";
|
|
5306
|
-
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
5307
|
-
}
|
|
5308
|
-
});
|
|
5309
|
-
|
|
5310
|
-
// src/commands/doctor.ts
|
|
6042
|
+
// src/commands/classify.ts
|
|
6043
|
+
var import_node_buffer = require("buffer");
|
|
6044
|
+
var import_promises10 = require("fs/promises");
|
|
6045
|
+
var import_node_path12 = require("path");
|
|
5311
6046
|
var import_zod15 = require("zod");
|
|
5312
6047
|
|
|
5313
|
-
// src/drift/git.ts
|
|
5314
|
-
var import_node_child_process3 = require("child_process");
|
|
5315
|
-
var import_node_util3 = require("util");
|
|
5316
|
-
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
5317
|
-
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
5318
|
-
var GIT_TIMEOUT_MS = 5e3;
|
|
5319
|
-
async function git2(cwd, args) {
|
|
5320
|
-
const env = { ...process.env };
|
|
5321
|
-
delete env["GIT_DIR"];
|
|
5322
|
-
delete env["GIT_WORK_TREE"];
|
|
5323
|
-
delete env["GIT_INDEX_FILE"];
|
|
5324
|
-
try {
|
|
5325
|
-
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
5326
|
-
timeout: GIT_TIMEOUT_MS,
|
|
5327
|
-
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
5328
|
-
env
|
|
5329
|
-
});
|
|
5330
|
-
return { ok: true, stdout };
|
|
5331
|
-
} catch {
|
|
5332
|
-
return { ok: false };
|
|
5333
|
-
}
|
|
5334
|
-
}
|
|
5335
|
-
async function listRepoFiles(repoRoot) {
|
|
5336
|
-
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
5337
|
-
if (!result.ok) return [];
|
|
5338
|
-
return result.stdout.split("\0").filter(Boolean);
|
|
5339
|
-
}
|
|
5340
|
-
async function readOldSource(repoRoot, anchor) {
|
|
5341
|
-
if (!filePathIsSafe(anchor.file))
|
|
5342
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5343
|
-
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
5344
|
-
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
5345
|
-
if (shown2 !== null) {
|
|
5346
|
-
return {
|
|
5347
|
-
ok: true,
|
|
5348
|
-
source: shown2,
|
|
5349
|
-
origin: { kind: "ref", ref: anchor.ref }
|
|
5350
|
-
};
|
|
5351
|
-
}
|
|
5352
|
-
}
|
|
5353
|
-
const at2 = anchor.resolved_at;
|
|
5354
|
-
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
5355
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5356
|
-
}
|
|
5357
|
-
const found = await git2(repoRoot, [
|
|
5358
|
-
"log",
|
|
5359
|
-
"-1",
|
|
5360
|
-
"--format=%H",
|
|
5361
|
-
`--before=${at2}`,
|
|
5362
|
-
"--end-of-options",
|
|
5363
|
-
"HEAD",
|
|
5364
|
-
"--",
|
|
5365
|
-
anchor.file
|
|
5366
|
-
]);
|
|
5367
|
-
const sha = found.ok ? found.stdout.trim() : "";
|
|
5368
|
-
if (!sha || !refShapeIsSafe(sha))
|
|
5369
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5370
|
-
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
5371
|
-
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
5372
|
-
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
5373
|
-
}
|
|
5374
|
-
async function showFile(repoRoot, ref, file) {
|
|
5375
|
-
const path = file.replace(/^\.\//, "");
|
|
5376
|
-
const result = await git2(repoRoot, [
|
|
5377
|
-
"show",
|
|
5378
|
-
"--end-of-options",
|
|
5379
|
-
`${ref}:${path}`
|
|
5380
|
-
]);
|
|
5381
|
-
return result.ok ? result.stdout : null;
|
|
5382
|
-
}
|
|
5383
|
-
|
|
5384
6048
|
// src/drift/moved.ts
|
|
5385
6049
|
var import_promises9 = require("fs/promises");
|
|
5386
6050
|
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
@@ -5399,6 +6063,7 @@ function movedSearch(repoRoot, options = {}) {
|
|
|
5399
6063
|
async find(anchor) {
|
|
5400
6064
|
const stored = anchor.hash;
|
|
5401
6065
|
if (!stored) return void 0;
|
|
6066
|
+
if (anchor.span) return sameFileWindow(anchor, read, stored);
|
|
5402
6067
|
const language = languageForFile(anchor.file);
|
|
5403
6068
|
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5404
6069
|
const candidates = await filesForLanguage(language);
|
|
@@ -5496,6 +6161,11 @@ async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
|
5496
6161
|
);
|
|
5497
6162
|
const out = [];
|
|
5498
6163
|
for (const { anchor, entry } of wanted) {
|
|
6164
|
+
if (anchor.side === "old") {
|
|
6165
|
+
const settled2 = entry.class ?? "changed";
|
|
6166
|
+
out.push({ anchor, entry: { ...entry, class: settled2 }, class: settled2 });
|
|
6167
|
+
continue;
|
|
6168
|
+
}
|
|
5499
6169
|
const movedTo = await search.find(anchor);
|
|
5500
6170
|
if (movedTo) {
|
|
5501
6171
|
out.push({
|
|
@@ -5569,8 +6239,8 @@ function unifiedDiff(before, after, options = {}) {
|
|
|
5569
6239
|
}
|
|
5570
6240
|
const truncated = body.length > max;
|
|
5571
6241
|
const shown = truncated ? body.slice(0, max) : body;
|
|
5572
|
-
const
|
|
5573
|
-
const lines = [
|
|
6242
|
+
const header2 = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
6243
|
+
const lines = [header2, ...shown];
|
|
5574
6244
|
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5575
6245
|
return { text: lines.join("\n"), added, removed, truncated };
|
|
5576
6246
|
}
|
|
@@ -5630,12 +6300,12 @@ async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
|
5630
6300
|
...options.search ? { search: options.search } : {},
|
|
5631
6301
|
withHistory: options.withDiff !== false
|
|
5632
6302
|
});
|
|
5633
|
-
const
|
|
6303
|
+
const open2 = classified.filter(
|
|
5634
6304
|
(found) => found.class === "changed" || found.class === "gone"
|
|
5635
6305
|
);
|
|
5636
|
-
if (!
|
|
5637
|
-
const budget = diffBudget(
|
|
5638
|
-
const anchors =
|
|
6306
|
+
if (!open2.length) return { packet: null, classified };
|
|
6307
|
+
const budget = diffBudget(open2.length);
|
|
6308
|
+
const anchors = open2.map(
|
|
5639
6309
|
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5640
6310
|
);
|
|
5641
6311
|
const type = record.frontmatter.type;
|
|
@@ -5709,37 +6379,624 @@ function claimOf(record) {
|
|
|
5709
6379
|
return text ? { section, text } : null;
|
|
5710
6380
|
}
|
|
5711
6381
|
|
|
5712
|
-
// src/commands/
|
|
6382
|
+
// src/commands/match/command.ts
|
|
5713
6383
|
var import_zod14 = require("zod");
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
6384
|
+
|
|
6385
|
+
// src/commands/match/errors.ts
|
|
6386
|
+
var KbMatchInputError = class extends BaseError {
|
|
6387
|
+
constructor(reason) {
|
|
6388
|
+
super({
|
|
6389
|
+
message: `match: ${reason}`,
|
|
6390
|
+
errorType: "KbMatchInput" /* KbMatchInput */,
|
|
6391
|
+
code: 400,
|
|
6392
|
+
fault: "User" /* User */,
|
|
6393
|
+
retriable: false,
|
|
6394
|
+
reportToUser: true,
|
|
6395
|
+
details: { reason }
|
|
6396
|
+
});
|
|
6397
|
+
this.reason = reason;
|
|
6398
|
+
}
|
|
6399
|
+
reason;
|
|
6400
|
+
};
|
|
6401
|
+
|
|
6402
|
+
// src/commands/match/model.ts
|
|
6403
|
+
var import_zod13 = require("zod");
|
|
6404
|
+
var diffHunkSchema = import_zod13.z.object({
|
|
6405
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
6406
|
+
endLine: import_zod13.z.number().int().positive(),
|
|
6407
|
+
side: import_zod13.z.enum(["old", "new"]).optional()
|
|
6408
|
+
}).passthrough();
|
|
6409
|
+
var diffFileSchema = import_zod13.z.object({
|
|
6410
|
+
filePath: import_zod13.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
6411
|
+
hunks: import_zod13.z.array(diffHunkSchema)
|
|
6412
|
+
});
|
|
6413
|
+
var symbolRangeSchema = import_zod13.z.object({
|
|
6414
|
+
file: import_zod13.z.string().min(1),
|
|
6415
|
+
symbol: import_zod13.z.string().min(1),
|
|
6416
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
6417
|
+
endLine: import_zod13.z.number().int().positive()
|
|
6418
|
+
});
|
|
6419
|
+
|
|
6420
|
+
// src/commands/match/parse-unified-diff.ts
|
|
6421
|
+
var FILE_HEADER = /^diff --git (.+)$/;
|
|
6422
|
+
var HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
6423
|
+
var SIMILARITY = /^similarity index (\d+)%$/;
|
|
6424
|
+
var KNOWN_PREFIX = /^[ab]\//;
|
|
6425
|
+
function parseUnifiedDiff(patch, options = {}) {
|
|
6426
|
+
const files = [];
|
|
6427
|
+
let current;
|
|
6428
|
+
let listed = false;
|
|
6429
|
+
let shared;
|
|
6430
|
+
let oldPath;
|
|
6431
|
+
let rename4 = {};
|
|
6432
|
+
let inHeader = false;
|
|
6433
|
+
let added;
|
|
6434
|
+
let removed;
|
|
6435
|
+
const list = () => {
|
|
6436
|
+
if (!current || listed) return;
|
|
6437
|
+
files.push(current);
|
|
6438
|
+
listed = true;
|
|
6439
|
+
};
|
|
6440
|
+
const open2 = (filePath) => {
|
|
6441
|
+
current = { filePath, hunks: [], ...rename4 };
|
|
6442
|
+
listed = false;
|
|
6443
|
+
};
|
|
6444
|
+
const amend = () => {
|
|
6445
|
+
if (current) Object.assign(current, rename4);
|
|
6446
|
+
};
|
|
6447
|
+
const close = () => {
|
|
6448
|
+
if (options.keepEmpty) list();
|
|
6449
|
+
current = void 0;
|
|
6450
|
+
listed = false;
|
|
6451
|
+
added = void 0;
|
|
6452
|
+
removed = void 0;
|
|
6453
|
+
};
|
|
6454
|
+
for (const raw of patch.split("\n")) {
|
|
6455
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
6456
|
+
const start = FILE_HEADER.exec(line);
|
|
6457
|
+
if (start) {
|
|
6458
|
+
close();
|
|
6459
|
+
oldPath = void 0;
|
|
6460
|
+
rename4 = {};
|
|
6461
|
+
shared = sharedHeaderPath(start[1]);
|
|
6462
|
+
inHeader = true;
|
|
6463
|
+
if (shared) open2(shared);
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
if (inHeader) {
|
|
6467
|
+
const similarity = SIMILARITY.exec(line);
|
|
6468
|
+
if (similarity) {
|
|
6469
|
+
rename4.similarity = Number(similarity[1]);
|
|
6470
|
+
amend();
|
|
6471
|
+
continue;
|
|
6472
|
+
}
|
|
6473
|
+
if (line.startsWith("rename from ")) {
|
|
6474
|
+
rename4.renamedFrom = unquote(line.slice(12).trim());
|
|
6475
|
+
amend();
|
|
6476
|
+
continue;
|
|
6477
|
+
}
|
|
6478
|
+
if (line.startsWith("rename to ")) {
|
|
6479
|
+
open2(unquote(line.slice(10).trim()));
|
|
6480
|
+
continue;
|
|
6481
|
+
}
|
|
6482
|
+
if (line.startsWith("--- ")) {
|
|
6483
|
+
oldPath = sidePath(line.slice(4), shared);
|
|
6484
|
+
continue;
|
|
6485
|
+
}
|
|
6486
|
+
if (line.startsWith("+++ ")) {
|
|
6487
|
+
const path = sidePath(line.slice(4), shared) ?? oldPath;
|
|
6488
|
+
if (path) open2(path);
|
|
6489
|
+
else current = void 0;
|
|
6490
|
+
continue;
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
const hunk = HUNK.exec(line);
|
|
6494
|
+
if (!hunk) {
|
|
6495
|
+
if (!options.withLines || inHeader) continue;
|
|
6496
|
+
if (line.startsWith("+")) added?.lines?.push(line.slice(1));
|
|
6497
|
+
else if (line.startsWith("-")) removed?.lines?.push(line.slice(1));
|
|
6498
|
+
continue;
|
|
6499
|
+
}
|
|
6500
|
+
inHeader = false;
|
|
6501
|
+
added = void 0;
|
|
6502
|
+
removed = void 0;
|
|
6503
|
+
if (!current) continue;
|
|
6504
|
+
const [next, before] = hunksOf(hunk, options.withLines === true);
|
|
6505
|
+
added = next;
|
|
6506
|
+
removed = before;
|
|
6507
|
+
list();
|
|
6508
|
+
current.hunks.push(...before ? [next, before] : [next]);
|
|
6509
|
+
}
|
|
6510
|
+
close();
|
|
6511
|
+
return files;
|
|
6512
|
+
}
|
|
6513
|
+
function hunksOf(hunk, withLines) {
|
|
6514
|
+
const oldStart = Number(hunk[1]);
|
|
6515
|
+
const oldCount = hunk[2] === void 0 ? 1 : Number(hunk[2]);
|
|
6516
|
+
const newStart = Number(hunk[3]);
|
|
6517
|
+
const newCount = hunk[4] === void 0 ? 1 : Number(hunk[4]);
|
|
6518
|
+
const lines = withLines ? { lines: [] } : {};
|
|
6519
|
+
const added = newCount === 0 ? { ...point(newStart), ...lines } : { startLine: newStart, endLine: newStart + newCount - 1, ...lines };
|
|
6520
|
+
if (oldCount === 0) return [added];
|
|
6521
|
+
return [
|
|
6522
|
+
added,
|
|
6523
|
+
{
|
|
6524
|
+
startLine: oldStart,
|
|
6525
|
+
endLine: oldStart + oldCount - 1,
|
|
6526
|
+
side: "old",
|
|
6527
|
+
...withLines ? { lines: [] } : {}
|
|
6528
|
+
}
|
|
6529
|
+
];
|
|
6530
|
+
}
|
|
6531
|
+
function point(start) {
|
|
6532
|
+
const at2 = Math.max(1, start);
|
|
6533
|
+
return { startLine: at2, endLine: at2 };
|
|
6534
|
+
}
|
|
6535
|
+
function sidePath(raw, shared) {
|
|
6536
|
+
const text = unquote(raw.replace(/\t.*$/, "").trim());
|
|
6537
|
+
if (text === "/dev/null") return void 0;
|
|
6538
|
+
if (KNOWN_PREFIX.test(text)) return text.slice(2);
|
|
6539
|
+
return shared ?? text;
|
|
6540
|
+
}
|
|
6541
|
+
function sharedHeaderPath(rest) {
|
|
6542
|
+
const pair = splitHeaderPair(rest);
|
|
6543
|
+
if (!pair || pair[0] === pair[1]) return void 0;
|
|
6544
|
+
const from = unquote(pair[0]).split("/");
|
|
6545
|
+
const to = unquote(pair[1]).split("/");
|
|
6546
|
+
const shared = [];
|
|
6547
|
+
while (from.length > 1 && to.length > 1 && from.at(-1) === to.at(-1)) {
|
|
6548
|
+
shared.unshift(from.pop());
|
|
6549
|
+
to.pop();
|
|
6550
|
+
}
|
|
6551
|
+
return shared.length ? shared.join("/") : void 0;
|
|
6552
|
+
}
|
|
6553
|
+
function splitHeaderPair(rest) {
|
|
6554
|
+
if (rest.startsWith('"')) {
|
|
6555
|
+
const end = endOfQuoted(rest);
|
|
6556
|
+
if (end < 0 || rest[end + 1] !== " ") return void 0;
|
|
6557
|
+
return [rest.slice(0, end + 1), rest.slice(end + 2)];
|
|
6558
|
+
}
|
|
6559
|
+
const mid = (rest.length - 1) / 2;
|
|
6560
|
+
if (Number.isInteger(mid) && rest[mid] === " ") {
|
|
6561
|
+
return [rest.slice(0, mid), rest.slice(mid + 1)];
|
|
6562
|
+
}
|
|
6563
|
+
const at2 = rest.indexOf(" ");
|
|
6564
|
+
return at2 === -1 ? void 0 : [rest.slice(0, at2), rest.slice(at2 + 1)];
|
|
6565
|
+
}
|
|
6566
|
+
function endOfQuoted(text) {
|
|
6567
|
+
for (let at2 = 1; at2 < text.length; at2 += 1) {
|
|
6568
|
+
if (text[at2] === "\\") {
|
|
6569
|
+
at2 += 1;
|
|
6570
|
+
continue;
|
|
6571
|
+
}
|
|
6572
|
+
if (text[at2] === '"') return at2;
|
|
6573
|
+
}
|
|
6574
|
+
return -1;
|
|
6575
|
+
}
|
|
6576
|
+
var ESCAPES = {
|
|
6577
|
+
a: 7,
|
|
6578
|
+
b: 8,
|
|
6579
|
+
f: 12,
|
|
6580
|
+
n: 10,
|
|
6581
|
+
r: 13,
|
|
6582
|
+
t: 9,
|
|
6583
|
+
v: 11,
|
|
6584
|
+
'"': 34,
|
|
6585
|
+
"\\": 92
|
|
6586
|
+
};
|
|
6587
|
+
var OCTAL = /^[0-7]{3}/;
|
|
6588
|
+
var utf8 = new TextEncoder();
|
|
6589
|
+
function unquote(text) {
|
|
6590
|
+
if (text.length < 2 || !text.startsWith('"') || !text.endsWith('"')) {
|
|
6591
|
+
return text;
|
|
6592
|
+
}
|
|
6593
|
+
const body = text.slice(1, -1);
|
|
6594
|
+
const bytes = [];
|
|
6595
|
+
for (let at2 = 0; at2 < body.length; ) {
|
|
6596
|
+
const slash = body.indexOf("\\", at2);
|
|
6597
|
+
if (slash < 0) {
|
|
6598
|
+
bytes.push(...utf8.encode(body.slice(at2)));
|
|
6599
|
+
break;
|
|
6600
|
+
}
|
|
6601
|
+
if (slash > at2) bytes.push(...utf8.encode(body.slice(at2, slash)));
|
|
6602
|
+
const octal = OCTAL.exec(body.slice(slash + 1, slash + 4));
|
|
6603
|
+
if (octal) {
|
|
6604
|
+
bytes.push(Number.parseInt(octal[0], 8));
|
|
6605
|
+
at2 = slash + 4;
|
|
6606
|
+
continue;
|
|
6607
|
+
}
|
|
6608
|
+
const next = body[slash + 1];
|
|
6609
|
+
if (next === void 0) {
|
|
6610
|
+
bytes.push(ESCAPES["\\"]);
|
|
6611
|
+
break;
|
|
6612
|
+
}
|
|
6613
|
+
const mapped = ESCAPES[next];
|
|
6614
|
+
if (mapped === void 0) bytes.push(...utf8.encode(next));
|
|
6615
|
+
else bytes.push(mapped);
|
|
6616
|
+
at2 = slash + 2;
|
|
6617
|
+
}
|
|
6618
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
// src/commands/match/symbol-ranges.ts
|
|
6622
|
+
async function resolveSymbolRanges(repoRoot, files, records, offline = false) {
|
|
6623
|
+
const changed = new Set(files.map((file) => strip(file.filePath)));
|
|
6624
|
+
const wanted = [];
|
|
6625
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6626
|
+
for (const record of records) {
|
|
6627
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
6628
|
+
if (!anchor.symbol || anchor.repo) continue;
|
|
6629
|
+
if (!changed.has(strip(anchor.file))) continue;
|
|
6630
|
+
const key2 = `${strip(anchor.file)}#${anchor.symbol}`;
|
|
6631
|
+
if (seen.has(key2)) continue;
|
|
6632
|
+
seen.add(key2);
|
|
6633
|
+
wanted.push(anchor);
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
if (!wanted.length) return [];
|
|
6637
|
+
const paths = [...new Set(wanted.map((anchor) => anchor.file))];
|
|
6638
|
+
const sources = await readAnchorFiles(paths, anchorFileReader(repoRoot));
|
|
6639
|
+
const resolvers = defaultAnchorResolvers({ offline });
|
|
6640
|
+
await prepareResolvers(resolvers, paths);
|
|
6641
|
+
const ranges = [];
|
|
6642
|
+
for (const anchor of wanted) {
|
|
6643
|
+
const read = sources.get(anchor.file);
|
|
6644
|
+
if (!read?.ok) continue;
|
|
6645
|
+
const outcome = resolveAnchorSpan(read.source, anchor, resolvers);
|
|
6646
|
+
if (!outcome.ok) continue;
|
|
6647
|
+
ranges.push({
|
|
6648
|
+
file: anchor.file,
|
|
6649
|
+
symbol: anchor.symbol,
|
|
6650
|
+
startLine: outcome.span.startLine,
|
|
6651
|
+
endLine: outcome.span.endLine
|
|
6652
|
+
});
|
|
6653
|
+
}
|
|
6654
|
+
return ranges;
|
|
6655
|
+
}
|
|
6656
|
+
function strip(path) {
|
|
6657
|
+
return path.replace(/^\.\//, "");
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
// src/commands/match/command.ts
|
|
6661
|
+
var matchCommand = define({
|
|
6662
|
+
name: "match",
|
|
6663
|
+
tool: "kb_match",
|
|
6664
|
+
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
6665
|
+
description: "Which records sit on each changed hunk: the anchored records per file range, current first, each with its standing and the anchor that matched. kb_load hands over a whole base; this narrows a diff. Symbol ranges resolve from repoRoot when omitted; non-current records need includeNonCurrent.",
|
|
6666
|
+
input: import_zod14.z.object({
|
|
6667
|
+
bundlePath,
|
|
6668
|
+
files: import_zod14.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
6669
|
+
symbolRanges: import_zod14.z.array(symbolRangeSchema).optional().describe(
|
|
6670
|
+
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
6671
|
+
),
|
|
6672
|
+
repoRoot: REPO_ROOT,
|
|
6673
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
6674
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
6675
|
+
),
|
|
6676
|
+
includeNonCurrent: import_zod14.z.boolean().optional().describe(
|
|
6677
|
+
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
6678
|
+
)
|
|
6679
|
+
}),
|
|
6680
|
+
fromArgv: async (argv, path, stdin) => {
|
|
6681
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6682
|
+
const range = argvFlag(argv, "--git");
|
|
6683
|
+
const base2 = {
|
|
6684
|
+
bundlePath: path,
|
|
6685
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
6686
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
6687
|
+
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
6688
|
+
};
|
|
6689
|
+
if (range !== void 0) {
|
|
6690
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
6691
|
+
if (!diff.ok) {
|
|
6692
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff.reason]}`);
|
|
6693
|
+
}
|
|
6694
|
+
return { ...base2, files: parseUnifiedDiff(diff.text) };
|
|
6695
|
+
}
|
|
6696
|
+
if (!argv.includes("--stdin")) {
|
|
6697
|
+
throw new KbMatchInputError(
|
|
6698
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
6699
|
+
);
|
|
6700
|
+
}
|
|
6701
|
+
return { ...base2, ...fromStdin(await stdin()) };
|
|
6702
|
+
},
|
|
6703
|
+
run: async ({ store }, {
|
|
6704
|
+
bundlePath: path,
|
|
6705
|
+
files,
|
|
6706
|
+
symbolRanges,
|
|
6707
|
+
repoRoot,
|
|
6708
|
+
offline,
|
|
6709
|
+
includeNonCurrent
|
|
6710
|
+
}) => {
|
|
6711
|
+
const records = await store.list(path);
|
|
6712
|
+
const ranges = symbolRanges ?? await resolveSymbolRanges(
|
|
6713
|
+
repoRoot ?? process.cwd(),
|
|
6714
|
+
files,
|
|
6715
|
+
records,
|
|
6716
|
+
offline === true
|
|
6717
|
+
);
|
|
6718
|
+
const index2 = symbolRangeIndex(ranges);
|
|
6719
|
+
return matchToDiff(files, records, { symbolRanges: ranges }).flatMap(
|
|
6720
|
+
(match) => project(match, index2, includeNonCurrent === true)
|
|
6721
|
+
);
|
|
6722
|
+
}
|
|
6723
|
+
});
|
|
6724
|
+
var REFUSED = {
|
|
6725
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
6726
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
6727
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
6728
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
6729
|
+
};
|
|
6730
|
+
function fromStdin(text) {
|
|
6731
|
+
let payload;
|
|
6732
|
+
try {
|
|
6733
|
+
payload = JSON.parse(text);
|
|
6734
|
+
} catch {
|
|
6735
|
+
throw new KbMatchInputError("stdin is not JSON");
|
|
6736
|
+
}
|
|
6737
|
+
if (!Array.isArray(payload?.files)) {
|
|
6738
|
+
throw new KbMatchInputError("stdin needs a files array");
|
|
6739
|
+
}
|
|
6740
|
+
return {
|
|
6741
|
+
files: payload.files,
|
|
6742
|
+
...payload.symbolRanges !== void 0 ? { symbolRanges: payload.symbolRanges } : {}
|
|
6743
|
+
};
|
|
6744
|
+
}
|
|
6745
|
+
function project(match, ranges, all) {
|
|
6746
|
+
const kept = all ? match.records : match.records.filter((hit) => hit.standing === "current");
|
|
6747
|
+
if (!kept.length) return [];
|
|
6748
|
+
const placed = kept.map((hit) => ({
|
|
6749
|
+
hit,
|
|
6750
|
+
at: placeOnHunk(hit.record, match.filePath, match.hunk, ranges)
|
|
6751
|
+
}));
|
|
6752
|
+
return [
|
|
6753
|
+
{
|
|
6754
|
+
filePath: match.filePath,
|
|
6755
|
+
hunk: match.hunk,
|
|
6756
|
+
// Over the records returned, not the ones matched: a hunk holding only
|
|
6757
|
+
// symbol-placed records is not `file` because a dropped one was.
|
|
6758
|
+
precision: placed.every(({ at: at2 }) => at2.kind === "symbol") ? "symbol" : "file",
|
|
6759
|
+
records: placed.map(({ hit, at: { anchor } }) => {
|
|
6760
|
+
const { frontmatter } = hit.record;
|
|
6761
|
+
return {
|
|
6762
|
+
conceptId: hit.record.conceptId,
|
|
6763
|
+
type: frontmatter.type,
|
|
6764
|
+
title: frontmatter.title ?? null,
|
|
6765
|
+
standing: hit.standing,
|
|
6766
|
+
status: frontmatter.strauss_status,
|
|
6767
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
6768
|
+
...frontmatter.strauss_materiality ? { materiality: frontmatter.strauss_materiality } : {},
|
|
6769
|
+
...frontmatter.strauss_confidence ? { confidence: frontmatter.strauss_confidence } : {},
|
|
6770
|
+
...frontmatter.tags?.length ? { tags: frontmatter.tags } : {},
|
|
6771
|
+
...anchor ? { anchor } : {}
|
|
6772
|
+
};
|
|
6773
|
+
})
|
|
6774
|
+
}
|
|
6775
|
+
];
|
|
6776
|
+
}
|
|
6777
|
+
|
|
6778
|
+
// src/commands/classify.ts
|
|
6779
|
+
var classifyFileSchema = diffFileSchema.extend({
|
|
6780
|
+
hunks: import_zod15.z.array(
|
|
6781
|
+
diffHunkSchema.extend({ lines: import_zod15.z.array(import_zod15.z.string()).optional() })
|
|
6782
|
+
),
|
|
6783
|
+
renamedFrom: import_zod15.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
6784
|
+
similarity: import_zod15.z.number().min(0).max(100).optional()
|
|
6785
|
+
});
|
|
6786
|
+
var classifyCommand = define({
|
|
6787
|
+
name: "classify",
|
|
6788
|
+
tool: "kb_classify",
|
|
6789
|
+
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
6790
|
+
description: "What kind of change each file carries: test, config, ci, docs, lockfile, generated, boilerplate, rename or source, with the rule that decided it. Derived from the diff and never stored; a `review:generated`, `review:boilerplate` or `review:move` fact anchored on a file overrides the heuristic. kb_match says what sits on a hunk; this says whether to read it.",
|
|
6791
|
+
input: import_zod15.z.object({
|
|
6792
|
+
bundlePath,
|
|
6793
|
+
files: import_zod15.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
6794
|
+
repoRoot: REPO_ROOT,
|
|
6795
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
6796
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
6797
|
+
)
|
|
6798
|
+
}),
|
|
6799
|
+
fromArgv: async (argv, path, stdin) => {
|
|
6800
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6801
|
+
const range = argvFlag(argv, "--git");
|
|
6802
|
+
const base2 = {
|
|
6803
|
+
bundlePath: path,
|
|
6804
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
6805
|
+
...argv.includes("--offline") ? { offline: true } : {}
|
|
6806
|
+
};
|
|
6807
|
+
if (range !== void 0) {
|
|
6808
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
6809
|
+
if (!diff.ok) {
|
|
6810
|
+
throw new KbClassifyInputError(
|
|
6811
|
+
`--git ${range} ${REFUSED2[diff.reason]}`
|
|
6812
|
+
);
|
|
6813
|
+
}
|
|
6814
|
+
return {
|
|
6815
|
+
...base2,
|
|
6816
|
+
files: parseUnifiedDiff(diff.text, {
|
|
6817
|
+
keepEmpty: true,
|
|
6818
|
+
withLines: true
|
|
6819
|
+
})
|
|
6820
|
+
};
|
|
6821
|
+
}
|
|
6822
|
+
if (!argv.includes("--stdin")) {
|
|
6823
|
+
throw new KbClassifyInputError(
|
|
6824
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
6825
|
+
);
|
|
6826
|
+
}
|
|
6827
|
+
return { ...base2, files: fromStdin2(await stdin()) };
|
|
6828
|
+
},
|
|
6829
|
+
run: async ({ store }, { bundlePath: path, files, repoRoot, offline }) => {
|
|
6830
|
+
const records = await store.list(path);
|
|
6831
|
+
const root = repoRoot ?? process.cwd();
|
|
6832
|
+
const withHeaders = await mapLimit2(files, READERS, async (file) => ({
|
|
6833
|
+
...file,
|
|
6834
|
+
header: await header(root, file)
|
|
6835
|
+
}));
|
|
6836
|
+
const symbolRanges = await resolveSymbolRanges(
|
|
6837
|
+
root,
|
|
6838
|
+
files,
|
|
6839
|
+
records,
|
|
6840
|
+
offline === true
|
|
6841
|
+
);
|
|
6842
|
+
return { files: classifyDiff(withHeaders, { records, symbolRanges }) };
|
|
6843
|
+
},
|
|
6844
|
+
render: (result) => renderClassify(result)
|
|
6845
|
+
});
|
|
6846
|
+
var HEADER_BYTES = 65536;
|
|
6847
|
+
var READERS = 16;
|
|
6848
|
+
async function header(root, file) {
|
|
6849
|
+
if (!filePathIsSafe(file.filePath)) return void 0;
|
|
6850
|
+
let handle;
|
|
6851
|
+
try {
|
|
6852
|
+
handle = await (0, import_promises10.open)((0, import_node_path12.join)(root, file.filePath), "r");
|
|
6853
|
+
const buffer = import_node_buffer.Buffer.alloc(HEADER_BYTES);
|
|
6854
|
+
const { bytesRead } = await handle.read(buffer, 0, HEADER_BYTES, 0);
|
|
6855
|
+
return buffer.toString("utf8", 0, bytesRead).split("\n").slice(0, HEADER_LINES);
|
|
6856
|
+
} catch {
|
|
6857
|
+
return void 0;
|
|
6858
|
+
} finally {
|
|
6859
|
+
await handle?.close();
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
async function mapLimit2(items, limit, run) {
|
|
6863
|
+
const out = Array.from({ length: items.length });
|
|
6864
|
+
let next = 0;
|
|
6865
|
+
const worker = async () => {
|
|
6866
|
+
while (next < items.length) {
|
|
6867
|
+
const at2 = next;
|
|
6868
|
+
next += 1;
|
|
6869
|
+
out[at2] = await run(items[at2]);
|
|
6870
|
+
}
|
|
6871
|
+
};
|
|
6872
|
+
await Promise.all(
|
|
6873
|
+
Array.from({ length: Math.min(limit, items.length) }, () => worker())
|
|
6874
|
+
);
|
|
6875
|
+
return out;
|
|
6876
|
+
}
|
|
6877
|
+
var REFUSED2 = {
|
|
6878
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
6879
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
6880
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
6881
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
6882
|
+
};
|
|
6883
|
+
function fromStdin2(text) {
|
|
6884
|
+
let payload;
|
|
6885
|
+
try {
|
|
6886
|
+
payload = JSON.parse(text);
|
|
6887
|
+
} catch {
|
|
6888
|
+
throw new KbClassifyInputError("stdin is not JSON");
|
|
6889
|
+
}
|
|
6890
|
+
if (!Array.isArray(payload?.files)) {
|
|
6891
|
+
throw new KbClassifyInputError("stdin needs a files array");
|
|
6892
|
+
}
|
|
6893
|
+
return payload.files;
|
|
6894
|
+
}
|
|
6895
|
+
function renderClassify(result) {
|
|
6896
|
+
const width2 = Math.max(
|
|
6897
|
+
0,
|
|
6898
|
+
...result.files.map((file) => file.class.length)
|
|
6899
|
+
);
|
|
6900
|
+
return result.files.map(
|
|
6901
|
+
(file) => `${file.class.padEnd(width2)} ${file.filePath} (${file.reason})`
|
|
6902
|
+
).join("\n");
|
|
6903
|
+
}
|
|
6904
|
+
|
|
6905
|
+
// src/commands/context.ts
|
|
6906
|
+
var import_zod16 = require("zod");
|
|
6907
|
+
var contextCommand = define({
|
|
6908
|
+
name: "context",
|
|
6909
|
+
tool: "kb_context",
|
|
6910
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
6911
|
+
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
6912
|
+
input: import_zod16.z.object({
|
|
6913
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
6914
|
+
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
6915
|
+
),
|
|
6916
|
+
fullUnderTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
6917
|
+
"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."
|
|
6918
|
+
),
|
|
6919
|
+
profile: import_zod16.z.string().optional().describe(
|
|
6920
|
+
"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."
|
|
6921
|
+
),
|
|
6922
|
+
excludeTags: import_zod16.z.array(import_zod16.z.string().min(1)).optional().describe(
|
|
6923
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
6924
|
+
),
|
|
6925
|
+
format: import_zod16.z.enum(["markdown", "json"]).optional().describe(
|
|
6926
|
+
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
6927
|
+
),
|
|
6928
|
+
event: import_zod16.z.string().optional().describe(
|
|
6929
|
+
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
6930
|
+
)
|
|
6931
|
+
}),
|
|
6932
|
+
fromArgv: (argv) => {
|
|
6933
|
+
const budget = argvFlag(argv, "--budget");
|
|
6934
|
+
const fullUnder = argvFlag(argv, "--full-under");
|
|
6935
|
+
const profile = argvFlag(argv, "--profile");
|
|
6936
|
+
const format = argvFlag(argv, "--format");
|
|
6937
|
+
const event = argvFlag(argv, "--event");
|
|
6938
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
6939
|
+
return {
|
|
6940
|
+
...budget ? { budgetTokens: Number(budget) } : {},
|
|
6941
|
+
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
6942
|
+
...profile ? { profile } : {},
|
|
6943
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
6944
|
+
...format ? { format } : {},
|
|
6945
|
+
...event ? { event } : {}
|
|
6946
|
+
};
|
|
6947
|
+
},
|
|
6948
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
6949
|
+
const result = await buildContext(store, process.cwd(), {
|
|
6950
|
+
...budgetTokens ? { budgetTokens } : {},
|
|
6951
|
+
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
6952
|
+
...profile ? { profile } : {},
|
|
6953
|
+
...excludeTags ? { excludeTags } : {},
|
|
6954
|
+
// Degradations — a full pin that could not fit, a refused block — go
|
|
6955
|
+
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
6956
|
+
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
6957
|
+
// see budget pressure without reading injected context.
|
|
6958
|
+
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
6959
|
+
`)
|
|
6960
|
+
});
|
|
6961
|
+
if (!result.block) return "";
|
|
6962
|
+
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
6963
|
+
}
|
|
6964
|
+
});
|
|
6965
|
+
|
|
6966
|
+
// src/commands/doctor.ts
|
|
6967
|
+
var import_zod18 = require("zod");
|
|
6968
|
+
|
|
6969
|
+
// src/commands/reassess.ts
|
|
6970
|
+
var import_zod17 = require("zod");
|
|
6971
|
+
var reassessCommand = define({
|
|
6972
|
+
name: "reassess",
|
|
6973
|
+
tool: "kb_reassess",
|
|
6974
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
6975
|
+
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
6976
|
+
input: import_zod17.z.object({
|
|
6977
|
+
bundlePath,
|
|
6978
|
+
conceptId,
|
|
6979
|
+
repoRoot: REPO_ROOT,
|
|
6980
|
+
withDiff: import_zod17.z.boolean().optional().describe(
|
|
6981
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
6982
|
+
)
|
|
6983
|
+
}),
|
|
6984
|
+
fromArgv: (argv, path) => {
|
|
6985
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6986
|
+
return {
|
|
6987
|
+
bundlePath: path,
|
|
6988
|
+
conceptId: argv[1],
|
|
6989
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
6990
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
6991
|
+
};
|
|
6992
|
+
},
|
|
6993
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
6994
|
+
const root = repoRoot ?? process.cwd();
|
|
6995
|
+
const bundle = await store.list(path);
|
|
6996
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
6997
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
6998
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
6999
|
+
const entries = drift?.get(id) ?? [];
|
|
5743
7000
|
if (!entries.some((entry) => entry.state !== "match")) {
|
|
5744
7001
|
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
5745
7002
|
}
|
|
@@ -5763,7 +7020,10 @@ var reassessCommand = define({
|
|
|
5763
7020
|
relocated.set(found.anchor, {
|
|
5764
7021
|
...found.anchor,
|
|
5765
7022
|
file: to.file,
|
|
5766
|
-
...to.symbol ? { symbol: to.symbol } : {}
|
|
7023
|
+
...to.symbol ? { symbol: to.symbol } : {},
|
|
7024
|
+
// A span is the anchor's whole address, so relocating it means
|
|
7025
|
+
// moving the line range the same code now occupies.
|
|
7026
|
+
...found.anchor.span ? { span: { start: to.startLine, end: to.endLine } } : {}
|
|
5767
7027
|
});
|
|
5768
7028
|
rebaselined.push({
|
|
5769
7029
|
file: found.anchor.file,
|
|
@@ -5862,13 +7122,13 @@ function at(file, symbol) {
|
|
|
5862
7122
|
}
|
|
5863
7123
|
|
|
5864
7124
|
// src/commands/doctor.ts
|
|
5865
|
-
var days = (what, fallback) =>
|
|
7125
|
+
var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5866
7126
|
var doctorCommand = define({
|
|
5867
7127
|
name: "doctor",
|
|
5868
7128
|
tool: "kb_doctor",
|
|
5869
7129
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5870
7130
|
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
5871
|
-
input:
|
|
7131
|
+
input: import_zod18.z.object({
|
|
5872
7132
|
bundlePath,
|
|
5873
7133
|
repoRoot: REPO_ROOT,
|
|
5874
7134
|
expiringDays: days(
|
|
@@ -5883,16 +7143,16 @@ var doctorCommand = define({
|
|
|
5883
7143
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5884
7144
|
DEFAULT_AGING_DAYS
|
|
5885
7145
|
),
|
|
5886
|
-
offline:
|
|
7146
|
+
offline: import_zod18.z.boolean().optional().describe(
|
|
5887
7147
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5888
7148
|
),
|
|
5889
|
-
strict:
|
|
7149
|
+
strict: import_zod18.z.boolean().optional().describe(
|
|
5890
7150
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5891
7151
|
),
|
|
5892
|
-
drifted:
|
|
7152
|
+
drifted: import_zod18.z.boolean().optional().describe(
|
|
5893
7153
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5894
7154
|
),
|
|
5895
|
-
withDiff:
|
|
7155
|
+
withDiff: import_zod18.z.boolean().optional().describe(
|
|
5896
7156
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5897
7157
|
)
|
|
5898
7158
|
}),
|
|
@@ -5948,7 +7208,7 @@ var doctorCommand = define({
|
|
|
5948
7208
|
...hints.length ? { hints } : {}
|
|
5949
7209
|
};
|
|
5950
7210
|
}
|
|
5951
|
-
const
|
|
7211
|
+
const standings2 = new Map(
|
|
5952
7212
|
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
5953
7213
|
hit.record.conceptId,
|
|
5954
7214
|
hit.standing
|
|
@@ -5962,7 +7222,7 @@ var doctorCommand = define({
|
|
|
5962
7222
|
(entry) => entry.conceptId === found.conceptId
|
|
5963
7223
|
);
|
|
5964
7224
|
if (!record) continue;
|
|
5965
|
-
const standing =
|
|
7225
|
+
const standing = standings2.get(record.conceptId);
|
|
5966
7226
|
const built = await reassessPacket(
|
|
5967
7227
|
repoRoot ?? process.cwd(),
|
|
5968
7228
|
record,
|
|
@@ -5999,15 +7259,16 @@ var doctorCommand = define({
|
|
|
5999
7259
|
});
|
|
6000
7260
|
function render2(result) {
|
|
6001
7261
|
if (result.packets) return renderPackets(result);
|
|
6002
|
-
const { thresholds } = result;
|
|
7262
|
+
const { thresholds, anchorResolvers: counts } = result;
|
|
6003
7263
|
const lines = [
|
|
6004
7264
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
6005
7265
|
`records: ${result.recordCount}`,
|
|
6006
7266
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
6007
7267
|
`checked: ${result.checkedAt}`,
|
|
6008
|
-
...
|
|
6009
|
-
|
|
6010
|
-
|
|
7268
|
+
...counts.total ? [anchorLine(counts)] : [],
|
|
7269
|
+
// Its own line: an old-side anchor may name a whole file, which no
|
|
7270
|
+
// resolver bucket and no `total` counts.
|
|
7271
|
+
...counts.oldSide ? [`old-side anchors: ${counts.oldSide}`] : [],
|
|
6011
7272
|
""
|
|
6012
7273
|
];
|
|
6013
7274
|
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
@@ -6032,6 +7293,14 @@ function render2(result) {
|
|
|
6032
7293
|
);
|
|
6033
7294
|
return lines.join("\n");
|
|
6034
7295
|
}
|
|
7296
|
+
function anchorLine(counts) {
|
|
7297
|
+
const parts = [
|
|
7298
|
+
`${counts.treeSitter} tree-sitter`,
|
|
7299
|
+
`${counts.regex} regex`,
|
|
7300
|
+
...counts.span ? [`${counts.span} span`] : []
|
|
7301
|
+
];
|
|
7302
|
+
return `anchors: ${counts.total} hashed \u2014 ${parts.join(", ")}`;
|
|
7303
|
+
}
|
|
6035
7304
|
function renderPackets(result) {
|
|
6036
7305
|
const packets = result.packets ?? [];
|
|
6037
7306
|
const lines = [
|
|
@@ -6057,20 +7326,155 @@ function renderPackets(result) {
|
|
|
6057
7326
|
return lines.join("\n");
|
|
6058
7327
|
}
|
|
6059
7328
|
|
|
7329
|
+
// src/commands/export.ts
|
|
7330
|
+
var import_promises11 = require("fs/promises");
|
|
7331
|
+
var import_node_path13 = require("path");
|
|
7332
|
+
var import_zod19 = require("zod");
|
|
7333
|
+
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
7334
|
+
var MARKER = "<!-- strauss-kb export: ";
|
|
7335
|
+
var exportCommand = define({
|
|
7336
|
+
name: "export",
|
|
7337
|
+
tool: "kb_export",
|
|
7338
|
+
usage: "export --format madr --to <dir>",
|
|
7339
|
+
description: "Write the base's decisions out as numbered MADR files, one per decision, for a repository that keeps ADRs of its own. Numbering is by slug, so a re-run rewrites its own files in place. A superseded decision is exported with what replaced it.",
|
|
7340
|
+
input: import_zod19.z.object({
|
|
7341
|
+
bundlePath,
|
|
7342
|
+
format: import_zod19.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
7343
|
+
to: import_zod19.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
7344
|
+
}),
|
|
7345
|
+
fromArgv: (argv, path) => ({
|
|
7346
|
+
bundlePath: path,
|
|
7347
|
+
format: argvFlag(argv, "--format"),
|
|
7348
|
+
to: argvFlag(argv, "--to")
|
|
7349
|
+
}),
|
|
7350
|
+
run: async ({ store }, { bundlePath: path, to }) => {
|
|
7351
|
+
const bundle = await store.list(path);
|
|
7352
|
+
const decisions = selectDecisions(bundle).sort(
|
|
7353
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
7354
|
+
);
|
|
7355
|
+
const adjudicated = new Map(
|
|
7356
|
+
adjudicate(decisions, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
7357
|
+
);
|
|
7358
|
+
await (0, import_promises11.mkdir)(to, { recursive: true });
|
|
7359
|
+
const taken = await existingFiles(to);
|
|
7360
|
+
let next = Math.max(0, ...[...taken.values()].map((row) => row.number)) + 1;
|
|
7361
|
+
const exported = [];
|
|
7362
|
+
const foreign = [];
|
|
7363
|
+
for (const record of decisions) {
|
|
7364
|
+
const slug = record.conceptId.slice(record.conceptId.indexOf(".") + 1);
|
|
7365
|
+
const held = taken.get(slug);
|
|
7366
|
+
if (held && !held.ours) {
|
|
7367
|
+
foreign.push({ conceptId: record.conceptId, file: held.file });
|
|
7368
|
+
continue;
|
|
7369
|
+
}
|
|
7370
|
+
const number = held?.number ?? next++;
|
|
7371
|
+
const file = `${String(number).padStart(4, "0")}-${slug}.md`;
|
|
7372
|
+
const status = statusLine(adjudicated.get(record.conceptId));
|
|
7373
|
+
await publish((0, import_node_path13.join)(to, file), renderMadr(record, status));
|
|
7374
|
+
exported.push({ conceptId: record.conceptId, file, status });
|
|
7375
|
+
}
|
|
7376
|
+
return { to, format: "madr", exported, foreign };
|
|
7377
|
+
},
|
|
7378
|
+
render: (result) => {
|
|
7379
|
+
const { exported, foreign, to } = result;
|
|
7380
|
+
return [
|
|
7381
|
+
`Wrote ${exported.length} MADR file${exported.length === 1 ? "" : "s"} to ${to}.`,
|
|
7382
|
+
...exported.map(
|
|
7383
|
+
(entry) => `- ${entry.file} ${entry.conceptId} [${entry.status}]`
|
|
7384
|
+
),
|
|
7385
|
+
...foreign.map(
|
|
7386
|
+
(entry) => `- skipped ${entry.conceptId}: ${entry.file} was not written by export`
|
|
7387
|
+
)
|
|
7388
|
+
].join("\n");
|
|
7389
|
+
}
|
|
7390
|
+
});
|
|
7391
|
+
async function publish(target, contents) {
|
|
7392
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
7393
|
+
await (0, import_promises11.writeFile)(staging, contents, "utf8");
|
|
7394
|
+
try {
|
|
7395
|
+
await (0, import_promises11.rename)(staging, target);
|
|
7396
|
+
} catch (error) {
|
|
7397
|
+
await (0, import_promises11.unlink)(staging).catch(() => void 0);
|
|
7398
|
+
throw error;
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7401
|
+
async function existingFiles(to) {
|
|
7402
|
+
const names = await (0, import_promises11.readdir)(to).catch(() => []);
|
|
7403
|
+
const taken = /* @__PURE__ */ new Map();
|
|
7404
|
+
for (const name of names.sort()) {
|
|
7405
|
+
const [, number, slug] = NUMBERED.exec(name) ?? [];
|
|
7406
|
+
if (!number || !slug) continue;
|
|
7407
|
+
const text = await (0, import_promises11.readFile)((0, import_node_path13.join)(to, name), "utf8").catch(() => "");
|
|
7408
|
+
taken.set(slug, {
|
|
7409
|
+
file: name,
|
|
7410
|
+
number: Number(number),
|
|
7411
|
+
ours: text.includes(MARKER)
|
|
7412
|
+
});
|
|
7413
|
+
}
|
|
7414
|
+
return taken;
|
|
7415
|
+
}
|
|
7416
|
+
function statusLine(hit) {
|
|
7417
|
+
const status = hit?.record.frontmatter.strauss_status ?? "draft";
|
|
7418
|
+
if (status !== "superseded") return status;
|
|
7419
|
+
const by = (hit?.heads ?? []).map((head) => head.conceptId);
|
|
7420
|
+
return by.length ? `superseded by ${by.join(", ")}` : "superseded";
|
|
7421
|
+
}
|
|
7422
|
+
function renderMadr(record, status) {
|
|
7423
|
+
const sections = bodySections(record.body);
|
|
7424
|
+
const blocks = [
|
|
7425
|
+
`# ${record.frontmatter.title ?? record.conceptId}`,
|
|
7426
|
+
"## Status",
|
|
7427
|
+
status
|
|
7428
|
+
];
|
|
7429
|
+
push(blocks, "Context and Problem Statement", record.frontmatter.description);
|
|
7430
|
+
push(blocks, "Considered Options", sections.get("Rejected"));
|
|
7431
|
+
push(blocks, "Decision Outcome", sections.get("Decision"));
|
|
7432
|
+
push(blocks, "Consequences", sections.get("Impact"));
|
|
7433
|
+
blocks.push(`${MARKER}${record.conceptId} -->`);
|
|
7434
|
+
return `${blocks.join("\n\n")}
|
|
7435
|
+
`;
|
|
7436
|
+
}
|
|
7437
|
+
function push(blocks, heading, text) {
|
|
7438
|
+
if (text?.trim()) blocks.push(`## ${heading}`, text.trim());
|
|
7439
|
+
}
|
|
7440
|
+
function bodySections(body) {
|
|
7441
|
+
const generated = new RegExp(
|
|
7442
|
+
`^(?:(?:${Object.values(LINK_RELS).map((spec) => spec.phrase).join("|")}) \\[[^\\]]+\\]\\([^)]+\\.md\\)\\.|\\[\\^[^\\]]+\\]: .*)$`
|
|
7443
|
+
);
|
|
7444
|
+
const sections = /* @__PURE__ */ new Map();
|
|
7445
|
+
let heading = null;
|
|
7446
|
+
let lines = [];
|
|
7447
|
+
const flush = () => {
|
|
7448
|
+
if (heading) sections.set(heading, lines.join("\n").trim());
|
|
7449
|
+
};
|
|
7450
|
+
for (const line of body.split("\n")) {
|
|
7451
|
+
const match = /^## (.+?)\s*$/.exec(line);
|
|
7452
|
+
if (match) {
|
|
7453
|
+
flush();
|
|
7454
|
+
heading = match[1] ?? null;
|
|
7455
|
+
lines = [];
|
|
7456
|
+
} else if (heading && !generated.test(line)) {
|
|
7457
|
+
lines.push(line);
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
flush();
|
|
7461
|
+
return sections;
|
|
7462
|
+
}
|
|
7463
|
+
|
|
6060
7464
|
// src/commands/impact.ts
|
|
6061
|
-
var
|
|
7465
|
+
var import_zod20 = require("zod");
|
|
6062
7466
|
var impactCommand = define({
|
|
6063
7467
|
name: "impact",
|
|
6064
7468
|
tool: "kb_impact",
|
|
6065
7469
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
6066
7470
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
6067
|
-
input:
|
|
7471
|
+
input: import_zod20.z.object({
|
|
6068
7472
|
bundlePath,
|
|
6069
7473
|
conceptId,
|
|
6070
|
-
depth:
|
|
7474
|
+
depth: import_zod20.z.number().int().positive().optional().describe(
|
|
6071
7475
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
6072
7476
|
),
|
|
6073
|
-
rels:
|
|
7477
|
+
rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
6074
7478
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
6075
7479
|
)
|
|
6076
7480
|
}),
|
|
@@ -6091,35 +7495,49 @@ var impactCommand = define({
|
|
|
6091
7495
|
});
|
|
6092
7496
|
|
|
6093
7497
|
// src/commands/list.ts
|
|
6094
|
-
var
|
|
7498
|
+
var import_zod21 = require("zod");
|
|
6095
7499
|
var listCommand = define({
|
|
6096
7500
|
name: "list",
|
|
6097
7501
|
tool: "kb_list",
|
|
6098
|
-
usage: "list [type]",
|
|
6099
|
-
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
6100
|
-
input:
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
7502
|
+
usage: "list [type] [--tag T]...",
|
|
7503
|
+
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
7504
|
+
input: import_zod21.z.object({
|
|
7505
|
+
bundlePath,
|
|
7506
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
7507
|
+
tags: TAGS
|
|
7508
|
+
}),
|
|
7509
|
+
fromArgv: (argv, path) => {
|
|
7510
|
+
const tags = argvFlags(argv, "--tag");
|
|
7511
|
+
const type = argvPositional(argv, "--tag");
|
|
7512
|
+
return {
|
|
7513
|
+
bundlePath: path,
|
|
7514
|
+
...type ? { type } : {},
|
|
7515
|
+
...tags.length ? { tags } : {}
|
|
7516
|
+
};
|
|
7517
|
+
},
|
|
7518
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
|
|
7519
|
+
(record) => ({
|
|
7520
|
+
conceptId: record.conceptId,
|
|
7521
|
+
title: record.frontmatter.title ?? null,
|
|
7522
|
+
description: record.frontmatter.description ?? null,
|
|
7523
|
+
status: record.frontmatter.strauss_status,
|
|
7524
|
+
anchors: record.frontmatter.strauss_anchors ?? []
|
|
7525
|
+
})
|
|
7526
|
+
)
|
|
6109
7527
|
});
|
|
6110
7528
|
|
|
6111
7529
|
// src/commands/load.ts
|
|
6112
|
-
var
|
|
7530
|
+
var import_zod22 = require("zod");
|
|
6113
7531
|
var loadCommand = define({
|
|
6114
7532
|
name: "load",
|
|
6115
7533
|
tool: "kb_load",
|
|
6116
7534
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6117
7535
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
6118
|
-
input:
|
|
7536
|
+
input: import_zod22.z.object({
|
|
6119
7537
|
bundlePath,
|
|
6120
|
-
type:
|
|
6121
|
-
budgetTokens:
|
|
6122
|
-
all:
|
|
7538
|
+
type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
|
|
7539
|
+
budgetTokens: import_zod22.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
7540
|
+
all: import_zod22.z.boolean().optional().describe(
|
|
6123
7541
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6124
7542
|
),
|
|
6125
7543
|
repoRoot: REPO_ROOT
|
|
@@ -6161,25 +7579,25 @@ var loadCommand = define({
|
|
|
6161
7579
|
});
|
|
6162
7580
|
|
|
6163
7581
|
// src/commands/log.ts
|
|
6164
|
-
var
|
|
7582
|
+
var import_zod23 = require("zod");
|
|
6165
7583
|
var logCommand = define({
|
|
6166
7584
|
name: "log",
|
|
6167
7585
|
tool: "kb_log",
|
|
6168
7586
|
usage: "log",
|
|
6169
7587
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6170
|
-
input:
|
|
7588
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
6171
7589
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6172
7590
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6173
7591
|
});
|
|
6174
7592
|
|
|
6175
7593
|
// src/commands/no-decision.ts
|
|
6176
|
-
var
|
|
7594
|
+
var import_zod24 = require("zod");
|
|
6177
7595
|
var noDecisionCommand = define({
|
|
6178
7596
|
name: "no-decision",
|
|
6179
7597
|
tool: "kb_no_decision",
|
|
6180
7598
|
usage: "no-decision <reason...>",
|
|
6181
7599
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6182
|
-
input:
|
|
7600
|
+
input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
|
|
6183
7601
|
fromArgv: (argv, path) => ({
|
|
6184
7602
|
bundlePath: path,
|
|
6185
7603
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6196,20 +7614,20 @@ var noDecisionCommand = define({
|
|
|
6196
7614
|
});
|
|
6197
7615
|
|
|
6198
7616
|
// src/commands/pack.ts
|
|
6199
|
-
var
|
|
7617
|
+
var import_zod25 = require("zod");
|
|
6200
7618
|
var packCommand = define({
|
|
6201
7619
|
name: "pack",
|
|
6202
7620
|
tool: "kb_pack",
|
|
6203
7621
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6204
7622
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
6205
|
-
input:
|
|
7623
|
+
input: import_zod25.z.object({
|
|
6206
7624
|
bundlePath,
|
|
6207
7625
|
conceptId,
|
|
6208
|
-
hops:
|
|
6209
|
-
maxNodes:
|
|
7626
|
+
hops: import_zod25.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
7627
|
+
maxNodes: import_zod25.z.number().int().positive().optional().describe(
|
|
6210
7628
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6211
7629
|
),
|
|
6212
|
-
budgetTokens:
|
|
7630
|
+
budgetTokens: import_zod25.z.number().int().positive().optional().describe(
|
|
6213
7631
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6214
7632
|
)
|
|
6215
7633
|
}),
|
|
@@ -6296,22 +7714,22 @@ function warningLabel(warning) {
|
|
|
6296
7714
|
}
|
|
6297
7715
|
|
|
6298
7716
|
// src/commands/pin.ts
|
|
6299
|
-
var
|
|
7717
|
+
var import_zod26 = require("zod");
|
|
6300
7718
|
var pinCommand = define({
|
|
6301
7719
|
name: "pin",
|
|
6302
7720
|
tool: "kb_pin",
|
|
6303
7721
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6304
7722
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
6305
|
-
input:
|
|
7723
|
+
input: import_zod26.z.object({
|
|
6306
7724
|
bundlePath,
|
|
6307
|
-
mode:
|
|
7725
|
+
mode: import_zod26.z.enum(["full", "index"]).optional().describe(
|
|
6308
7726
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
6309
7727
|
),
|
|
6310
|
-
profiles:
|
|
6311
|
-
layer:
|
|
7728
|
+
profiles: import_zod26.z.array(import_zod26.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
7729
|
+
layer: import_zod26.z.enum(["project", "local", "user"]).optional().describe(
|
|
6312
7730
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6313
7731
|
),
|
|
6314
|
-
frozen:
|
|
7732
|
+
frozen: import_zod26.z.boolean().optional().describe(
|
|
6315
7733
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6316
7734
|
)
|
|
6317
7735
|
}),
|
|
@@ -6340,47 +7758,325 @@ var pinCommand = define({
|
|
|
6340
7758
|
});
|
|
6341
7759
|
|
|
6342
7760
|
// src/commands/pins.ts
|
|
6343
|
-
var
|
|
7761
|
+
var import_zod27 = require("zod");
|
|
6344
7762
|
var pinsCommand = define({
|
|
6345
7763
|
name: "pins",
|
|
6346
7764
|
tool: "kb_pins",
|
|
6347
7765
|
usage: "pins",
|
|
6348
7766
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6349
|
-
input:
|
|
7767
|
+
input: import_zod27.z.object({}),
|
|
6350
7768
|
fromArgv: () => ({}),
|
|
6351
7769
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6352
7770
|
});
|
|
6353
7771
|
|
|
7772
|
+
// src/commands/promote/command.ts
|
|
7773
|
+
var import_node_path14 = require("path");
|
|
7774
|
+
|
|
7775
|
+
// src/commands/promote/carry.ts
|
|
7776
|
+
var PROMOTION_SOURCE_ID = "promoted";
|
|
7777
|
+
var CARRIED_STATUS = {
|
|
7778
|
+
draft: "accepted",
|
|
7779
|
+
proposed: "accepted",
|
|
7780
|
+
accepted: "accepted",
|
|
7781
|
+
open: "open",
|
|
7782
|
+
resolved: "resolved",
|
|
7783
|
+
rejected: "rejected",
|
|
7784
|
+
superseded: "superseded"
|
|
7785
|
+
};
|
|
7786
|
+
function carry(record, promoted, source) {
|
|
7787
|
+
const {
|
|
7788
|
+
type: _type,
|
|
7789
|
+
// Both name records in the source base, and supersession in the target is
|
|
7790
|
+
// a separate question from whether this record belongs there at all.
|
|
7791
|
+
strauss_supersedes: _supersedes,
|
|
7792
|
+
strauss_superseded_by: _supersededBy,
|
|
7793
|
+
// A check run against the source repository, which the target never saw.
|
|
7794
|
+
verified: _verified,
|
|
7795
|
+
...rest
|
|
7796
|
+
} = record.frontmatter;
|
|
7797
|
+
const links = rest.strauss_links ?? [];
|
|
7798
|
+
const kept = links.filter((link2) => promoted.has(link2.target));
|
|
7799
|
+
const dropped = links.filter((link2) => !promoted.has(link2.target));
|
|
7800
|
+
const tags = (rest.tags ?? []).filter((tag) => !isReviewTag(tag));
|
|
7801
|
+
const frontmatter = {
|
|
7802
|
+
...rest,
|
|
7803
|
+
strauss_status: CARRIED_STATUS[rest.strauss_status]
|
|
7804
|
+
};
|
|
7805
|
+
setOrDrop(frontmatter, "tags", tags);
|
|
7806
|
+
setOrDrop(frontmatter, "strauss_links", kept);
|
|
7807
|
+
let body = withoutLinkSentences(record.body, dropped);
|
|
7808
|
+
if (source) {
|
|
7809
|
+
frontmatter.sources = [
|
|
7810
|
+
...(rest.sources ?? []).filter(
|
|
7811
|
+
(entry) => entry.id !== PROMOTION_SOURCE_ID
|
|
7812
|
+
),
|
|
7813
|
+
{ id: PROMOTION_SOURCE_ID, resource: source }
|
|
7814
|
+
];
|
|
7815
|
+
body = `${stripFootnote(body).trimEnd()}
|
|
7816
|
+
|
|
7817
|
+
[^${PROMOTION_SOURCE_ID}]: ${source}
|
|
7818
|
+
`;
|
|
7819
|
+
}
|
|
7820
|
+
return {
|
|
7821
|
+
frontmatter,
|
|
7822
|
+
body,
|
|
7823
|
+
droppedLinks: dropped.map(({ target, rel }) => ({ target, rel }))
|
|
7824
|
+
};
|
|
7825
|
+
}
|
|
7826
|
+
function isReviewTag(tag) {
|
|
7827
|
+
return tag === "review" || tag.startsWith("review:");
|
|
7828
|
+
}
|
|
7829
|
+
function withoutLinkSentences(body, dropped) {
|
|
7830
|
+
const sentences = new Set(
|
|
7831
|
+
dropped.filter((link2) => isKbLinkRel(link2.rel)).map(
|
|
7832
|
+
(link2) => `${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
7833
|
+
)
|
|
7834
|
+
);
|
|
7835
|
+
if (!sentences.size) return body;
|
|
7836
|
+
return body.split("\n\n").filter((block) => !sentences.has(block.trim())).join("\n\n");
|
|
7837
|
+
}
|
|
7838
|
+
function stripFootnote(body) {
|
|
7839
|
+
return body.split("\n").filter((line) => !line.startsWith(`[^${PROMOTION_SOURCE_ID}]: `)).join("\n");
|
|
7840
|
+
}
|
|
7841
|
+
function setOrDrop(frontmatter, key2, value) {
|
|
7842
|
+
if (value.length) frontmatter[key2] = value;
|
|
7843
|
+
else delete frontmatter[key2];
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7846
|
+
// src/commands/promote/standing.ts
|
|
7847
|
+
var WITHDRAWN = ["superseded", "rejected"];
|
|
7848
|
+
function standings(bundle) {
|
|
7849
|
+
return new Map(
|
|
7850
|
+
adjudicate(bundle, bundle).map((hit) => [
|
|
7851
|
+
hit.record.conceptId,
|
|
7852
|
+
hit.standing
|
|
7853
|
+
])
|
|
7854
|
+
);
|
|
7855
|
+
}
|
|
7856
|
+
function isWithdrawn(standing) {
|
|
7857
|
+
return standing !== void 0 && WITHDRAWN.includes(standing);
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7860
|
+
// src/commands/promote/candidates.ts
|
|
7861
|
+
var REVIEW_TAG = "review";
|
|
7862
|
+
var SETTLED = ["resolved"];
|
|
7863
|
+
function promoteCandidates(bundle) {
|
|
7864
|
+
const inbound = inboundIndex(bundle);
|
|
7865
|
+
const standing = standings(bundle);
|
|
7866
|
+
const rows = [];
|
|
7867
|
+
for (const record of bundle) {
|
|
7868
|
+
if (isWithdrawn(standing.get(record.conceptId))) continue;
|
|
7869
|
+
const why2 = candidateReason(record, inbound.get(record.conceptId) ?? []);
|
|
7870
|
+
if (!why2) continue;
|
|
7871
|
+
rows.push({
|
|
7872
|
+
conceptId: record.conceptId,
|
|
7873
|
+
type: recordType(record.conceptId),
|
|
7874
|
+
title: record.frontmatter.title ?? null,
|
|
7875
|
+
why: why2
|
|
7876
|
+
});
|
|
7877
|
+
}
|
|
7878
|
+
return rows;
|
|
7879
|
+
}
|
|
7880
|
+
function candidateReason(record, inbound) {
|
|
7881
|
+
const { strauss_status: status, tags } = record.frontmatter;
|
|
7882
|
+
switch (recordType(record.conceptId)) {
|
|
7883
|
+
case "decision":
|
|
7884
|
+
if (isNoDecisionRecord(record)) return null;
|
|
7885
|
+
return tags?.includes(REVIEW_TAG) ? null : "decision no longer under review";
|
|
7886
|
+
case "constraint":
|
|
7887
|
+
return status === "proposed" ? "constraint still proposed \u2014 the target base is where it settles" : null;
|
|
7888
|
+
case "contract":
|
|
7889
|
+
return "contract \u2014 it outlives the change that introduced it";
|
|
7890
|
+
case "requirement":
|
|
7891
|
+
return inbound.some((edge) => edge.rel === "satisfies") ? "requirement something in the base satisfies" : null;
|
|
7892
|
+
case "risk":
|
|
7893
|
+
return record.frontmatter.strauss_materiality === "blocking" && !SETTLED.includes(status) ? "blocking risk still open" : null;
|
|
7894
|
+
default:
|
|
7895
|
+
return null;
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
function recordType(conceptId2) {
|
|
7899
|
+
return conceptId2.slice(0, conceptId2.indexOf("."));
|
|
7900
|
+
}
|
|
7901
|
+
|
|
7902
|
+
// src/commands/promote/model.ts
|
|
7903
|
+
var import_zod28 = require("zod");
|
|
7904
|
+
var promoteInputSchema = import_zod28.z.object({
|
|
7905
|
+
bundlePath,
|
|
7906
|
+
conceptIds: import_zod28.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
7907
|
+
to: import_zod28.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
7908
|
+
source: import_zod28.z.string().min(1).optional().describe(
|
|
7909
|
+
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
7910
|
+
),
|
|
7911
|
+
force: import_zod28.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
7912
|
+
list: import_zod28.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
7913
|
+
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
7914
|
+
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
7915
|
+
path: ["to"]
|
|
7916
|
+
}).refine(
|
|
7917
|
+
(input) => input.list === true || (input.conceptIds?.length ?? 0) > 0,
|
|
7918
|
+
{
|
|
7919
|
+
message: "name at least one concept id to promote, or pass --list",
|
|
7920
|
+
path: ["conceptIds"]
|
|
7921
|
+
}
|
|
7922
|
+
);
|
|
7923
|
+
|
|
7924
|
+
// src/commands/promote/command.ts
|
|
7925
|
+
var promoteCommand = define({
|
|
7926
|
+
name: "promote",
|
|
7927
|
+
tool: "kb_promote",
|
|
7928
|
+
usage: "promote <concept-id...> --to <bundle> [--source <url>] [--force] | --list",
|
|
7929
|
+
description: "Copy records into another base at the same slug, with the review tags dropped and a source naming where the promotion came from. Use at merge, to lift what a review base settled into the base that outlives it. `list` names the candidates instead. The originals stay put.",
|
|
7930
|
+
input: promoteInputSchema,
|
|
7931
|
+
fromArgv: (argv, path) => {
|
|
7932
|
+
const to = argvFlag(argv, "--to");
|
|
7933
|
+
const source = argvFlag(argv, "--source");
|
|
7934
|
+
const words = argv.slice(1);
|
|
7935
|
+
for (const flag of ["--to", "--source"]) {
|
|
7936
|
+
const at2 = words.indexOf(flag);
|
|
7937
|
+
if (at2 !== -1) words.splice(at2, 2);
|
|
7938
|
+
}
|
|
7939
|
+
const conceptIds = words.filter((word) => !word.startsWith("--"));
|
|
7940
|
+
return {
|
|
7941
|
+
bundlePath: path,
|
|
7942
|
+
...conceptIds.length ? { conceptIds } : {},
|
|
7943
|
+
...to !== void 0 ? { to } : {},
|
|
7944
|
+
...source !== void 0 ? { source } : {},
|
|
7945
|
+
...argv.includes("--force") ? { force: true } : {},
|
|
7946
|
+
...argv.includes("--list") ? { list: true } : {}
|
|
7947
|
+
};
|
|
7948
|
+
},
|
|
7949
|
+
run: async ({ store, actor }, { bundlePath: path, conceptIds, to, source, force, list }) => {
|
|
7950
|
+
const from = (0, import_node_path14.resolve)(path);
|
|
7951
|
+
const bundle = await store.list(from);
|
|
7952
|
+
if (list) {
|
|
7953
|
+
return { mode: "list", candidates: promoteCandidates(bundle) };
|
|
7954
|
+
}
|
|
7955
|
+
const target = (0, import_node_path14.resolve)(to);
|
|
7956
|
+
if (target === from) throw new KbPromoteSelfError(target);
|
|
7957
|
+
const named = (conceptIds ?? []).map(namedRecord);
|
|
7958
|
+
const wanted = named.map(({ conceptId: conceptId2, type, slug }) => {
|
|
7959
|
+
const record = bundle.find((entry) => entry.conceptId === conceptId2);
|
|
7960
|
+
if (!record) throw new KbRecordNotFoundError(conceptId2);
|
|
7961
|
+
return { record, type, slug };
|
|
7962
|
+
});
|
|
7963
|
+
await assertBaseNotFrozen(process.cwd(), from);
|
|
7964
|
+
await assertBaseNotFrozen(process.cwd(), target);
|
|
7965
|
+
const standing = standings(bundle);
|
|
7966
|
+
for (const { record } of wanted) {
|
|
7967
|
+
const where = standing.get(record.conceptId);
|
|
7968
|
+
if (isWithdrawn(where)) {
|
|
7969
|
+
throw new KbPromoteStandingError(record.conceptId, where);
|
|
7970
|
+
}
|
|
7971
|
+
if (!force && await store.read(target, record.conceptId)) {
|
|
7972
|
+
throw new KbPromoteCollisionError(record.conceptId, target);
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
const promotedIds = new Set(wanted.map(({ record }) => record.conceptId));
|
|
7976
|
+
const promoted = [];
|
|
7977
|
+
for (const { record, type, slug } of wanted) {
|
|
7978
|
+
const { frontmatter, body, droppedLinks } = carry(
|
|
7979
|
+
record,
|
|
7980
|
+
promotedIds,
|
|
7981
|
+
source
|
|
7982
|
+
);
|
|
7983
|
+
try {
|
|
7984
|
+
await store.write(
|
|
7985
|
+
target,
|
|
7986
|
+
{ type, slug, frontmatter, body, overwrite: force === true },
|
|
7987
|
+
actor
|
|
7988
|
+
);
|
|
7989
|
+
} catch (error) {
|
|
7990
|
+
throw new KbPromoteStoppedError(
|
|
7991
|
+
record.conceptId,
|
|
7992
|
+
promoted.map((entry) => entry.conceptId),
|
|
7993
|
+
error instanceof Error ? error.message : "unknown"
|
|
7994
|
+
);
|
|
7995
|
+
}
|
|
7996
|
+
await store.note(target, {
|
|
7997
|
+
by: actor,
|
|
7998
|
+
operation: "promote-in",
|
|
7999
|
+
conceptId: record.conceptId,
|
|
8000
|
+
target: from
|
|
8001
|
+
});
|
|
8002
|
+
await store.note(from, {
|
|
8003
|
+
by: actor,
|
|
8004
|
+
operation: "promote-out",
|
|
8005
|
+
conceptId: record.conceptId,
|
|
8006
|
+
target
|
|
8007
|
+
});
|
|
8008
|
+
promoted.push({ conceptId: record.conceptId, droppedLinks });
|
|
8009
|
+
}
|
|
8010
|
+
return { mode: "promote", to: target, promoted };
|
|
8011
|
+
},
|
|
8012
|
+
render: (result) => renderPromote(result)
|
|
8013
|
+
});
|
|
8014
|
+
function namedRecord(conceptId2) {
|
|
8015
|
+
const at2 = conceptId2.indexOf(".");
|
|
8016
|
+
const type = at2 === -1 ? conceptId2 : conceptId2.slice(0, at2);
|
|
8017
|
+
const slug = at2 === -1 ? "" : conceptId2.slice(at2 + 1);
|
|
8018
|
+
if (!KB_SLUG_PATTERN.test(type) || !KB_SLUG_PATTERN.test(slug)) {
|
|
8019
|
+
throw new KbInvalidConceptIdError(
|
|
8020
|
+
"concept id must be <type>.<slug>, both kebab-case",
|
|
8021
|
+
{ conceptId: conceptId2 }
|
|
8022
|
+
);
|
|
8023
|
+
}
|
|
8024
|
+
return { conceptId: conceptId2, type, slug };
|
|
8025
|
+
}
|
|
8026
|
+
function renderPromote(result) {
|
|
8027
|
+
if (result.mode === "list") {
|
|
8028
|
+
if (!result.candidates.length) return "No promotion candidates.";
|
|
8029
|
+
return result.candidates.flatMap((candidate) => [
|
|
8030
|
+
`${candidate.conceptId} [${candidate.type}]${candidate.title ? ` \u2014 ${candidate.title}` : ""}`,
|
|
8031
|
+
` ${candidate.why}`
|
|
8032
|
+
]).join("\n");
|
|
8033
|
+
}
|
|
8034
|
+
const lines = [
|
|
8035
|
+
`Promoted ${result.promoted.length} record${result.promoted.length === 1 ? "" : "s"} into ${result.to}.`
|
|
8036
|
+
];
|
|
8037
|
+
for (const entry of result.promoted) {
|
|
8038
|
+
lines.push(`- ${entry.conceptId}`);
|
|
8039
|
+
for (const link2 of entry.droppedLinks) {
|
|
8040
|
+
lines.push(
|
|
8041
|
+
` dropped ${link2.rel} \u2192 ${link2.target} (not promoted in this run)`
|
|
8042
|
+
);
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
return lines.join("\n");
|
|
8046
|
+
}
|
|
8047
|
+
|
|
6354
8048
|
// src/commands/query.ts
|
|
6355
|
-
var
|
|
8049
|
+
var import_zod29 = require("zod");
|
|
6356
8050
|
var queryCommand = define({
|
|
6357
8051
|
name: "query",
|
|
6358
8052
|
tool: "kb_query",
|
|
6359
|
-
usage: "query <text...> [--repo-root PATH]",
|
|
8053
|
+
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6360
8054
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
6361
|
-
input:
|
|
8055
|
+
input: import_zod29.z.object({
|
|
6362
8056
|
bundlePath,
|
|
6363
|
-
text:
|
|
6364
|
-
type:
|
|
6365
|
-
includeNonCurrent:
|
|
8057
|
+
text: import_zod29.z.string().optional(),
|
|
8058
|
+
type: import_zod29.z.enum(KB_RECORD_TYPES).optional(),
|
|
8059
|
+
includeNonCurrent: import_zod29.z.boolean().optional(),
|
|
8060
|
+
tags: TAGS,
|
|
6366
8061
|
repoRoot: REPO_ROOT
|
|
6367
8062
|
}),
|
|
6368
|
-
//
|
|
8063
|
+
// Both are flags, so neither's value may fall into the search text.
|
|
6369
8064
|
fromArgv: (argv, path) => {
|
|
6370
8065
|
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6371
|
-
const
|
|
6372
|
-
const
|
|
6373
|
-
if (flag !== -1) words.splice(flag, 2);
|
|
8066
|
+
const tags = argvFlags(argv, "--tag");
|
|
8067
|
+
const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
|
|
6374
8068
|
return {
|
|
6375
8069
|
bundlePath: path,
|
|
6376
8070
|
text: words.join(" ").trim(),
|
|
6377
8071
|
includeNonCurrent: true,
|
|
8072
|
+
...tags.length ? { tags } : {},
|
|
6378
8073
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
6379
8074
|
};
|
|
6380
8075
|
},
|
|
6381
|
-
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
8076
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
|
|
6382
8077
|
...type ? { type } : {},
|
|
6383
8078
|
includeNonCurrent: includeNonCurrent === true,
|
|
8079
|
+
...tags ? { tags } : {},
|
|
6384
8080
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
6385
8081
|
})).map((hit) => ({
|
|
6386
8082
|
conceptId: hit.record.conceptId,
|
|
@@ -6394,43 +8090,43 @@ var queryCommand = define({
|
|
|
6394
8090
|
});
|
|
6395
8091
|
|
|
6396
8092
|
// src/commands/read-index.ts
|
|
6397
|
-
var
|
|
8093
|
+
var import_zod30 = require("zod");
|
|
6398
8094
|
var readIndexCommand = define({
|
|
6399
8095
|
name: "index",
|
|
6400
8096
|
tool: "kb_index",
|
|
6401
8097
|
usage: "index",
|
|
6402
8098
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
6403
|
-
input:
|
|
8099
|
+
input: import_zod30.z.object({ bundlePath }),
|
|
6404
8100
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6405
8101
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6406
8102
|
});
|
|
6407
8103
|
|
|
6408
8104
|
// src/commands/schema.ts
|
|
6409
|
-
var
|
|
8105
|
+
var import_zod31 = require("zod");
|
|
6410
8106
|
var schemaCommand = define({
|
|
6411
8107
|
name: "schema",
|
|
6412
8108
|
tool: "kb_schema",
|
|
6413
8109
|
usage: "schema",
|
|
6414
8110
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6415
|
-
input:
|
|
8111
|
+
input: import_zod31.z.object({}),
|
|
6416
8112
|
fromArgv: () => ({}),
|
|
6417
8113
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6418
8114
|
});
|
|
6419
8115
|
|
|
6420
8116
|
// src/commands/stamp.ts
|
|
6421
|
-
var
|
|
6422
|
-
var
|
|
8117
|
+
var import_promises12 = require("fs/promises");
|
|
8118
|
+
var import_zod32 = require("zod");
|
|
6423
8119
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6424
8120
|
var stampCommand = define({
|
|
6425
8121
|
name: "stamp",
|
|
6426
8122
|
tool: "kb_stamp",
|
|
6427
8123
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6428
8124
|
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
6429
|
-
input:
|
|
6430
|
-
bundlePath:
|
|
8125
|
+
input: import_zod32.z.object({
|
|
8126
|
+
bundlePath: import_zod32.z.string().min(1).optional().describe(
|
|
6431
8127
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6432
8128
|
),
|
|
6433
|
-
since:
|
|
8129
|
+
since: import_zod32.z.string().min(1).optional().describe(
|
|
6434
8130
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6435
8131
|
)
|
|
6436
8132
|
}),
|
|
@@ -6492,7 +8188,7 @@ async function readBaseline(since) {
|
|
|
6492
8188
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
6493
8189
|
let parsed;
|
|
6494
8190
|
try {
|
|
6495
|
-
parsed = JSON.parse(await (0,
|
|
8191
|
+
parsed = JSON.parse(await (0, import_promises12.readFile)(since, "utf8"));
|
|
6496
8192
|
} catch {
|
|
6497
8193
|
throw new KbStampBaselineError(since);
|
|
6498
8194
|
}
|
|
@@ -6516,16 +8212,16 @@ async function readBaseline(since) {
|
|
|
6516
8212
|
}
|
|
6517
8213
|
|
|
6518
8214
|
// src/commands/status.ts
|
|
6519
|
-
var
|
|
8215
|
+
var import_zod33 = require("zod");
|
|
6520
8216
|
var statusCommand = define({
|
|
6521
8217
|
name: "status",
|
|
6522
8218
|
tool: "kb_status",
|
|
6523
8219
|
usage: "status <concept-id> <status>",
|
|
6524
8220
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6525
|
-
input:
|
|
8221
|
+
input: import_zod33.z.object({
|
|
6526
8222
|
bundlePath,
|
|
6527
8223
|
conceptId,
|
|
6528
|
-
status:
|
|
8224
|
+
status: import_zod33.z.enum(KB_RECORD_STATUSES)
|
|
6529
8225
|
}),
|
|
6530
8226
|
fromArgv: (argv, path) => ({
|
|
6531
8227
|
bundlePath: path,
|
|
@@ -6540,13 +8236,13 @@ var statusCommand = define({
|
|
|
6540
8236
|
});
|
|
6541
8237
|
|
|
6542
8238
|
// src/commands/supersede.ts
|
|
6543
|
-
var
|
|
8239
|
+
var import_zod34 = require("zod");
|
|
6544
8240
|
var supersedeCommand = define({
|
|
6545
8241
|
name: "supersede",
|
|
6546
8242
|
tool: "kb_supersede",
|
|
6547
8243
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6548
8244
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6549
|
-
input:
|
|
8245
|
+
input: import_zod34.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6550
8246
|
fromArgv: (argv, path) => ({
|
|
6551
8247
|
bundlePath: path,
|
|
6552
8248
|
conceptId: argv[1],
|
|
@@ -6559,17 +8255,153 @@ var supersedeCommand = define({
|
|
|
6559
8255
|
}
|
|
6560
8256
|
});
|
|
6561
8257
|
|
|
8258
|
+
// src/commands/sweep.ts
|
|
8259
|
+
var import_zod35 = require("zod");
|
|
8260
|
+
var TERMINAL = [
|
|
8261
|
+
"resolved",
|
|
8262
|
+
"rejected",
|
|
8263
|
+
"superseded"
|
|
8264
|
+
];
|
|
8265
|
+
var sweepCommand = define({
|
|
8266
|
+
name: "sweep",
|
|
8267
|
+
tool: "kb_sweep",
|
|
8268
|
+
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
8269
|
+
description: "Delete tagged records that are resolved, rejected or superseded. Refuses without --tag, keeps any record a surviving record still points at, and logs each deletion.",
|
|
8270
|
+
input: import_zod35.z.object({
|
|
8271
|
+
bundlePath,
|
|
8272
|
+
tag: import_zod35.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
|
|
8273
|
+
terminal: import_zod35.z.literal(true, {
|
|
8274
|
+
error: "sweep needs --terminal: it deletes only settled records"
|
|
8275
|
+
}).describe(
|
|
8276
|
+
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
8277
|
+
),
|
|
8278
|
+
dryRun: import_zod35.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
8279
|
+
}),
|
|
8280
|
+
fromArgv: (argv, path) => ({
|
|
8281
|
+
bundlePath: path,
|
|
8282
|
+
tag: argvFlag(argv, "--tag"),
|
|
8283
|
+
...argv.includes("--terminal") ? { terminal: true } : {},
|
|
8284
|
+
...argv.includes("--dry-run") ? { dryRun: true } : {}
|
|
8285
|
+
}),
|
|
8286
|
+
run: async ({ store, actor }, { bundlePath: path, tag, dryRun }) => {
|
|
8287
|
+
const bundle = await store.list(path);
|
|
8288
|
+
const held = holderIndex(bundle);
|
|
8289
|
+
const candidates = adjudicate(bundle, bundle).filter(
|
|
8290
|
+
(hit) => sweepable(hit, tag)
|
|
8291
|
+
);
|
|
8292
|
+
const doomed = new Set(candidates.map((hit) => hit.record.conceptId));
|
|
8293
|
+
let changed = true;
|
|
8294
|
+
while (changed) {
|
|
8295
|
+
changed = false;
|
|
8296
|
+
for (const conceptId2 of [...doomed]) {
|
|
8297
|
+
if (survivorsHolding(conceptId2, held, doomed).length === 0) continue;
|
|
8298
|
+
doomed.delete(conceptId2);
|
|
8299
|
+
changed = true;
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
const skipped = candidates.filter((hit) => !doomed.has(hit.record.conceptId)).map((hit) => ({
|
|
8303
|
+
conceptId: hit.record.conceptId,
|
|
8304
|
+
heldBy: survivorsHolding(hit.record.conceptId, held, doomed)
|
|
8305
|
+
}));
|
|
8306
|
+
const ordered = [...doomed].sort();
|
|
8307
|
+
if (dryRun) {
|
|
8308
|
+
return {
|
|
8309
|
+
tag,
|
|
8310
|
+
dryRun: true,
|
|
8311
|
+
deleted: [],
|
|
8312
|
+
candidates: ordered,
|
|
8313
|
+
skipped,
|
|
8314
|
+
failed: []
|
|
8315
|
+
};
|
|
8316
|
+
}
|
|
8317
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
8318
|
+
const deleted = [];
|
|
8319
|
+
const failed = [];
|
|
8320
|
+
try {
|
|
8321
|
+
for (const conceptId2 of ordered) {
|
|
8322
|
+
try {
|
|
8323
|
+
const outcome = await store.deleteRecord(
|
|
8324
|
+
path,
|
|
8325
|
+
conceptId2,
|
|
8326
|
+
{ tag, statuses: TERMINAL },
|
|
8327
|
+
actor
|
|
8328
|
+
);
|
|
8329
|
+
if (outcome === "deleted") deleted.push(conceptId2);
|
|
8330
|
+
else failed.push({ conceptId: conceptId2, reason: outcome });
|
|
8331
|
+
} catch (error) {
|
|
8332
|
+
failed.push({
|
|
8333
|
+
conceptId: conceptId2,
|
|
8334
|
+
reason: error instanceof Error ? error.message : "unknown"
|
|
8335
|
+
});
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
} finally {
|
|
8339
|
+
await store.readIndex(path);
|
|
8340
|
+
await store.dropSearchIndex(path);
|
|
8341
|
+
}
|
|
8342
|
+
return {
|
|
8343
|
+
tag,
|
|
8344
|
+
dryRun: false,
|
|
8345
|
+
deleted,
|
|
8346
|
+
candidates: ordered,
|
|
8347
|
+
skipped,
|
|
8348
|
+
failed
|
|
8349
|
+
};
|
|
8350
|
+
},
|
|
8351
|
+
render: (result) => renderSweep(result)
|
|
8352
|
+
});
|
|
8353
|
+
function sweepable(hit, tag) {
|
|
8354
|
+
const { tags, strauss_status } = hit.record.frontmatter;
|
|
8355
|
+
return (tags ?? []).includes(tag) && // Supersession is a standing, settled against the whole base; the other
|
|
8356
|
+
// two are the record's own word for itself.
|
|
8357
|
+
(hit.standing === "superseded" || strauss_status === "resolved" || strauss_status === "rejected");
|
|
8358
|
+
}
|
|
8359
|
+
function holderIndex(bundle) {
|
|
8360
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
8361
|
+
const hold = (target, from) => {
|
|
8362
|
+
if (target === from) return;
|
|
8363
|
+
const holders = byTarget.get(target) ?? /* @__PURE__ */ new Set();
|
|
8364
|
+
holders.add(from);
|
|
8365
|
+
byTarget.set(target, holders);
|
|
8366
|
+
};
|
|
8367
|
+
for (const [target, edges] of inboundIndex(bundle)) {
|
|
8368
|
+
for (const edge of edges) hold(target, edge.from);
|
|
8369
|
+
}
|
|
8370
|
+
for (const record of bundle) {
|
|
8371
|
+
const { strauss_supersedes, strauss_superseded_by } = record.frontmatter;
|
|
8372
|
+
for (const old of strauss_supersedes ?? []) hold(old, record.conceptId);
|
|
8373
|
+
if (strauss_superseded_by) hold(strauss_superseded_by, record.conceptId);
|
|
8374
|
+
}
|
|
8375
|
+
return byTarget;
|
|
8376
|
+
}
|
|
8377
|
+
function survivorsHolding(conceptId2, held, doomed) {
|
|
8378
|
+
return [...held.get(conceptId2) ?? []].filter((from) => !doomed.has(from)).sort();
|
|
8379
|
+
}
|
|
8380
|
+
function renderSweep(result) {
|
|
8381
|
+
const shown = result.dryRun ? result.candidates : result.deleted;
|
|
8382
|
+
const verb = result.dryRun ? "would delete" : "deleted";
|
|
8383
|
+
const lines = [`${verb} ${shown.length} (tag: ${result.tag})`];
|
|
8384
|
+
for (const conceptId2 of shown) lines.push(`- ${conceptId2}`);
|
|
8385
|
+
for (const skip of result.skipped) {
|
|
8386
|
+
lines.push(`kept ${skip.conceptId} \u2014 held by ${skip.heldBy.join(", ")}`);
|
|
8387
|
+
}
|
|
8388
|
+
for (const failure of result.failed) {
|
|
8389
|
+
lines.push(`failed ${failure.conceptId} \u2014 ${failure.reason}`);
|
|
8390
|
+
}
|
|
8391
|
+
return lines.join("\n");
|
|
8392
|
+
}
|
|
8393
|
+
|
|
6562
8394
|
// src/commands/sync-instructions.ts
|
|
6563
|
-
var
|
|
8395
|
+
var import_zod36 = require("zod");
|
|
6564
8396
|
var syncInstructionsCommand = define({
|
|
6565
8397
|
name: "sync-instructions",
|
|
6566
8398
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
6567
8399
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
6568
|
-
input:
|
|
6569
|
-
file:
|
|
6570
|
-
budgetTokens:
|
|
6571
|
-
fullUnderTokens:
|
|
6572
|
-
profile:
|
|
8400
|
+
input: import_zod36.z.object({
|
|
8401
|
+
file: import_zod36.z.string().min(1).describe("The instruction file to edit in place."),
|
|
8402
|
+
budgetTokens: import_zod36.z.number().int().positive().optional(),
|
|
8403
|
+
fullUnderTokens: import_zod36.z.number().int().positive().optional(),
|
|
8404
|
+
profile: import_zod36.z.string().optional()
|
|
6573
8405
|
}),
|
|
6574
8406
|
fromArgv: (argv) => {
|
|
6575
8407
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -6595,17 +8427,17 @@ var syncInstructionsCommand = define({
|
|
|
6595
8427
|
});
|
|
6596
8428
|
|
|
6597
8429
|
// src/commands/trace.ts
|
|
6598
|
-
var
|
|
8430
|
+
var import_zod37 = require("zod");
|
|
6599
8431
|
var traceCommand = define({
|
|
6600
8432
|
name: "trace",
|
|
6601
8433
|
tool: "kb_trace",
|
|
6602
8434
|
usage: "trace <concept-id> [edges...]",
|
|
6603
8435
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
6604
|
-
input:
|
|
8436
|
+
input: import_zod37.z.object({
|
|
6605
8437
|
bundlePath,
|
|
6606
8438
|
conceptId,
|
|
6607
|
-
edges:
|
|
6608
|
-
depth:
|
|
8439
|
+
edges: import_zod37.z.array(import_zod37.z.enum(TRACE_EDGES)).optional(),
|
|
8440
|
+
depth: import_zod37.z.number().int().positive().optional()
|
|
6609
8441
|
}),
|
|
6610
8442
|
fromArgv: (argv, path) => ({
|
|
6611
8443
|
bundlePath: path,
|
|
@@ -6627,37 +8459,37 @@ var traceCommand = define({
|
|
|
6627
8459
|
});
|
|
6628
8460
|
|
|
6629
8461
|
// src/commands/types.ts
|
|
6630
|
-
var
|
|
8462
|
+
var import_zod38 = require("zod");
|
|
6631
8463
|
var typesCommand = define({
|
|
6632
8464
|
name: "types",
|
|
6633
8465
|
tool: "kb_types",
|
|
6634
8466
|
usage: "types",
|
|
6635
8467
|
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.",
|
|
6636
|
-
input:
|
|
8468
|
+
input: import_zod38.z.object({}),
|
|
6637
8469
|
fromArgv: () => ({}),
|
|
6638
8470
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
6639
8471
|
});
|
|
6640
8472
|
|
|
6641
8473
|
// src/commands/unpin.ts
|
|
6642
|
-
var
|
|
8474
|
+
var import_zod39 = require("zod");
|
|
6643
8475
|
var unpinCommand = define({
|
|
6644
8476
|
name: "unpin",
|
|
6645
8477
|
tool: "kb_unpin",
|
|
6646
8478
|
usage: "unpin [bundle-path]",
|
|
6647
8479
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
6648
|
-
input:
|
|
8480
|
+
input: import_zod39.z.object({ bundlePath }),
|
|
6649
8481
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
6650
8482
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
6651
8483
|
});
|
|
6652
8484
|
|
|
6653
8485
|
// src/commands/validate.ts
|
|
6654
|
-
var
|
|
8486
|
+
var import_zod40 = require("zod");
|
|
6655
8487
|
var validateCommand = define({
|
|
6656
8488
|
name: "validate",
|
|
6657
8489
|
tool: "kb_validate",
|
|
6658
8490
|
usage: "validate",
|
|
6659
8491
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
6660
|
-
input:
|
|
8492
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
6661
8493
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6662
8494
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
6663
8495
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -6667,16 +8499,16 @@ var validateCommand = define({
|
|
|
6667
8499
|
});
|
|
6668
8500
|
|
|
6669
8501
|
// src/commands/verify.ts
|
|
6670
|
-
var
|
|
8502
|
+
var import_zod41 = require("zod");
|
|
6671
8503
|
var verifyCommand = define({
|
|
6672
8504
|
name: "verify",
|
|
6673
8505
|
tool: "kb_verify",
|
|
6674
8506
|
usage: "verify <concept-id> --note <text>",
|
|
6675
8507
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
6676
|
-
input:
|
|
8508
|
+
input: import_zod41.z.object({
|
|
6677
8509
|
bundlePath,
|
|
6678
8510
|
conceptId,
|
|
6679
|
-
note:
|
|
8511
|
+
note: import_zod41.z.string().refine((s) => s.trim().length > 0, {
|
|
6680
8512
|
message: "note must say what the check found"
|
|
6681
8513
|
})
|
|
6682
8514
|
}),
|
|
@@ -6696,15 +8528,15 @@ var verifyCommand = define({
|
|
|
6696
8528
|
});
|
|
6697
8529
|
|
|
6698
8530
|
// src/commands/write.ts
|
|
6699
|
-
var
|
|
8531
|
+
var import_zod42 = require("zod");
|
|
6700
8532
|
var writeCommand = define({
|
|
6701
8533
|
name: "write",
|
|
6702
8534
|
tool: "kb_write",
|
|
6703
8535
|
usage: "write <type> < record.json",
|
|
6704
8536
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
6705
|
-
input:
|
|
8537
|
+
input: import_zod42.z.object({
|
|
6706
8538
|
bundlePath,
|
|
6707
|
-
type:
|
|
8539
|
+
type: import_zod42.z.enum(KB_RECORD_TYPES),
|
|
6708
8540
|
input: composeInputSchema
|
|
6709
8541
|
}),
|
|
6710
8542
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -6728,13 +8560,13 @@ var writeCommand = define({
|
|
|
6728
8560
|
});
|
|
6729
8561
|
|
|
6730
8562
|
// src/commands/write-decision.ts
|
|
6731
|
-
var
|
|
8563
|
+
var import_zod43 = require("zod");
|
|
6732
8564
|
var writeDecisionCommand = define({
|
|
6733
8565
|
name: "write-decision",
|
|
6734
8566
|
tool: "kb_write_decision",
|
|
6735
8567
|
usage: "write-decision < decision.json",
|
|
6736
8568
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
6737
|
-
input:
|
|
8569
|
+
input: import_zod43.z.object({ bundlePath, input: decisionInputSchema }),
|
|
6738
8570
|
fromArgv: async (_argv, path, stdin) => ({
|
|
6739
8571
|
bundlePath: path,
|
|
6740
8572
|
input: JSON.parse(await stdin())
|
|
@@ -6765,19 +8597,24 @@ var KB_COMMANDS = [
|
|
|
6765
8597
|
verifyCommand,
|
|
6766
8598
|
anchorResolveCommand,
|
|
6767
8599
|
reassessCommand,
|
|
8600
|
+
promoteCommand,
|
|
6768
8601
|
loadCommand,
|
|
6769
8602
|
catalogCommand,
|
|
6770
8603
|
packCommand,
|
|
8604
|
+
exportCommand,
|
|
6771
8605
|
queryCommand,
|
|
6772
8606
|
traceCommand,
|
|
6773
8607
|
impactCommand,
|
|
6774
8608
|
backlinksCommand,
|
|
8609
|
+
matchCommand,
|
|
8610
|
+
classifyCommand,
|
|
6775
8611
|
listCommand,
|
|
6776
8612
|
readIndexCommand,
|
|
6777
8613
|
logCommand,
|
|
6778
8614
|
stampCommand,
|
|
6779
8615
|
validateCommand,
|
|
6780
8616
|
doctorCommand,
|
|
8617
|
+
sweepCommand,
|
|
6781
8618
|
schemaCommand,
|
|
6782
8619
|
pinCommand,
|
|
6783
8620
|
unpinCommand,
|
|
@@ -6795,7 +8632,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
6795
8632
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6796
8633
|
|
|
6797
8634
|
// src/version.ts
|
|
6798
|
-
var VERSION = true ? "0.1.
|
|
8635
|
+
var VERSION = true ? "0.1.20" : "0.0.0-dev";
|
|
6799
8636
|
|
|
6800
8637
|
// src/mcp.ts
|
|
6801
8638
|
function createKbMcpServer() {
|
|
@@ -6834,7 +8671,7 @@ async function runKbMcpServer() {
|
|
|
6834
8671
|
}
|
|
6835
8672
|
|
|
6836
8673
|
// src/cli.ts
|
|
6837
|
-
var
|
|
8674
|
+
var import_node_path15 = require("path");
|
|
6838
8675
|
async function runKbCli(argv) {
|
|
6839
8676
|
const { flags, literal } = takeLiteral(argv);
|
|
6840
8677
|
const {
|
|
@@ -6896,7 +8733,7 @@ function takeLiteral(argv) {
|
|
|
6896
8733
|
function takeBundle(argv) {
|
|
6897
8734
|
const at2 = argv.indexOf("--bundle");
|
|
6898
8735
|
if (at2 === -1) {
|
|
6899
|
-
return { bundle: (0,
|
|
8736
|
+
return { bundle: (0, import_node_path15.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
6900
8737
|
}
|
|
6901
8738
|
const bundle = argv[at2 + 1];
|
|
6902
8739
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -6907,11 +8744,11 @@ function takeBundle(argv) {
|
|
|
6907
8744
|
};
|
|
6908
8745
|
}
|
|
6909
8746
|
function readStdin() {
|
|
6910
|
-
return new Promise((
|
|
8747
|
+
return new Promise((resolve7, reject) => {
|
|
6911
8748
|
let text = "";
|
|
6912
8749
|
process.stdin.setEncoding("utf8");
|
|
6913
8750
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
6914
|
-
process.stdin.on("end", () =>
|
|
8751
|
+
process.stdin.on("end", () => resolve7(text));
|
|
6915
8752
|
process.stdin.on("error", reject);
|
|
6916
8753
|
});
|
|
6917
8754
|
}
|
|
@@ -6956,12 +8793,14 @@ function usage() {
|
|
|
6956
8793
|
DEFAULT_LOAD_BUDGET,
|
|
6957
8794
|
DEFAULT_PACK_HOPS,
|
|
6958
8795
|
DEFAULT_PACK_MAX_NODES,
|
|
8796
|
+
DEFAULT_THRESHOLDS,
|
|
6959
8797
|
DEFAULT_TYPED_LINK_RELS,
|
|
6960
8798
|
DEFAULT_UNVERIFIED_DAYS,
|
|
6961
8799
|
ErrorTypes,
|
|
6962
8800
|
Fault,
|
|
6963
8801
|
INDEX_FILE,
|
|
6964
8802
|
KB_CAUSAL_LINK_RELS,
|
|
8803
|
+
KB_CLASSES,
|
|
6965
8804
|
KB_COMMANDS,
|
|
6966
8805
|
KB_COMMANDS_BY_NAME,
|
|
6967
8806
|
KB_CONCEPT_ID_PATTERN,
|
|
@@ -6975,10 +8814,15 @@ function usage() {
|
|
|
6975
8814
|
KB_RECORD_TYPES,
|
|
6976
8815
|
KB_SLUG_PATTERN,
|
|
6977
8816
|
KbBaseFrozenError,
|
|
8817
|
+
KbClassifyInputError,
|
|
6978
8818
|
KbInvalidConceptIdError,
|
|
6979
8819
|
KbMissingFlagValueError,
|
|
6980
8820
|
KbPackBudgetExceededError,
|
|
6981
8821
|
KbPinsMalformedError,
|
|
8822
|
+
KbPromoteCollisionError,
|
|
8823
|
+
KbPromoteSelfError,
|
|
8824
|
+
KbPromoteStandingError,
|
|
8825
|
+
KbPromoteStoppedError,
|
|
6982
8826
|
KbRecordAlreadyExistsError,
|
|
6983
8827
|
KbRecordNotFoundError,
|
|
6984
8828
|
KbSelfVerificationError,
|
|
@@ -6991,16 +8835,20 @@ function usage() {
|
|
|
6991
8835
|
PINS_FILE,
|
|
6992
8836
|
PINS_LOCAL_FILE,
|
|
6993
8837
|
PIN_LAYERS,
|
|
8838
|
+
PROMOTION_SOURCE_ID,
|
|
6994
8839
|
RECORD_TYPES,
|
|
6995
8840
|
SEARCH_INDEX_FILE,
|
|
6996
8841
|
TRACE_EDGES,
|
|
6997
8842
|
TreeSitterResolver,
|
|
6998
8843
|
adjudicate,
|
|
6999
8844
|
anchorFilePath,
|
|
8845
|
+
anchorOnHunk,
|
|
7000
8846
|
assertBaseNotFrozen,
|
|
7001
8847
|
backlinks,
|
|
7002
8848
|
buildContext,
|
|
8849
|
+
carry,
|
|
7003
8850
|
catalog,
|
|
8851
|
+
classifyDiff,
|
|
7004
8852
|
classifyDrift,
|
|
7005
8853
|
composeDecisionRecord,
|
|
7006
8854
|
composeInputSchema,
|
|
@@ -7026,8 +8874,11 @@ function usage() {
|
|
|
7026
8874
|
isKbLinkRel,
|
|
7027
8875
|
isKbRecordType,
|
|
7028
8876
|
isNoDecisionRecord,
|
|
8877
|
+
isReviewTag,
|
|
7029
8878
|
kbActorStampSchema,
|
|
7030
8879
|
kbAnchorSchema,
|
|
8880
|
+
kbAnchorSpanSchema,
|
|
8881
|
+
kbAnchorWriteSchema,
|
|
7031
8882
|
kbConceptIdSchema,
|
|
7032
8883
|
kbJsonSchemas,
|
|
7033
8884
|
kbLinkSchema,
|
|
@@ -7039,6 +8890,7 @@ function usage() {
|
|
|
7039
8890
|
listPins,
|
|
7040
8891
|
loadQmd,
|
|
7041
8892
|
matchToDiff,
|
|
8893
|
+
matchesTags,
|
|
7042
8894
|
mergedContextBudgets,
|
|
7043
8895
|
neighbours,
|
|
7044
8896
|
pack,
|
|
@@ -7046,6 +8898,8 @@ function usage() {
|
|
|
7046
8898
|
parseMarkdownWithFrontmatter,
|
|
7047
8899
|
pinBase,
|
|
7048
8900
|
prepareResolvers,
|
|
8901
|
+
promoteCandidates,
|
|
8902
|
+
promoteInputSchema,
|
|
7049
8903
|
readMergedPins,
|
|
7050
8904
|
readPinsLayer,
|
|
7051
8905
|
readRemoteAnchors,
|
|
@@ -7067,6 +8921,7 @@ function usage() {
|
|
|
7067
8921
|
selectDecisions,
|
|
7068
8922
|
splitMarkdownFrontmatter,
|
|
7069
8923
|
stringifyMarkdownWithFrontmatter,
|
|
8924
|
+
symbolRangeIndex,
|
|
7070
8925
|
syncInstructions,
|
|
7071
8926
|
toHookJson,
|
|
7072
8927
|
trace,
|