@saasontools/strauss-kb 0.1.19 → 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-ZWSCLHG6.js → chunk-EXZQZJU2.js} +2 -2
- package/dist/{chunk-MNQNHYWL.js → chunk-QQLPJO4R.js} +2781 -957
- package/dist/chunk-QQLPJO4R.js.map +1 -0
- package/dist/{chunk-CQBLH7CE.js → chunk-XDH6J6CH.js} +2 -2
- package/dist/cli-main.cjs +2783 -994
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +2149 -394
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +470 -96
- package/dist/index.d.ts +470 -96
- package/dist/index.js +38 -83
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +2779 -990
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-MNQNHYWL.js.map +0 -1
- /package/dist/{chunk-ZWSCLHG6.js.map → chunk-EXZQZJU2.js.map} +0 -0
- /package/dist/{chunk-CQBLH7CE.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,
|
|
@@ -131,6 +145,8 @@ __export(index_exports, {
|
|
|
131
145
|
parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
|
|
132
146
|
pinBase: () => pinBase,
|
|
133
147
|
prepareResolvers: () => prepareResolvers,
|
|
148
|
+
promoteCandidates: () => promoteCandidates,
|
|
149
|
+
promoteInputSchema: () => promoteInputSchema,
|
|
134
150
|
readMergedPins: () => readMergedPins,
|
|
135
151
|
readPinsLayer: () => readPinsLayer,
|
|
136
152
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
@@ -152,6 +168,7 @@ __export(index_exports, {
|
|
|
152
168
|
selectDecisions: () => selectDecisions,
|
|
153
169
|
splitMarkdownFrontmatter: () => splitMarkdownFrontmatter,
|
|
154
170
|
stringifyMarkdownWithFrontmatter: () => stringifyMarkdownWithFrontmatter,
|
|
171
|
+
symbolRangeIndex: () => symbolRangeIndex,
|
|
155
172
|
syncInstructions: () => syncInstructions,
|
|
156
173
|
toHookJson: () => toHookJson,
|
|
157
174
|
trace: () => trace,
|
|
@@ -242,9 +259,24 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
242
259
|
message: "note must say what the check found"
|
|
243
260
|
})
|
|
244
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();
|
|
245
266
|
var kbAnchorSchema = import_zod.z.object({
|
|
246
267
|
file: import_zod.z.string().min(1),
|
|
247
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(),
|
|
248
280
|
/**
|
|
249
281
|
* Which repository the file lives in — a remote URL
|
|
250
282
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
@@ -283,8 +315,38 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
283
315
|
* before resolvers were named, which is read as `regex` — the only one
|
|
284
316
|
* there was. A hash from a different resolver is drift, not a match.
|
|
285
317
|
*/
|
|
286
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
318
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
287
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
|
+
});
|
|
288
350
|
var kbLinkSchema = import_zod.z.object({
|
|
289
351
|
target: import_zod.z.string().min(1),
|
|
290
352
|
rel: import_zod.z.string().min(1)
|
|
@@ -371,9 +433,15 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
371
433
|
})(Fault || {});
|
|
372
434
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
373
435
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
436
|
+
ErrorTypes2["KbClassifyInput"] = "KbClassifyInput";
|
|
374
437
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
438
|
+
ErrorTypes2["KbMatchInput"] = "KbMatchInput";
|
|
375
439
|
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
376
440
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
441
|
+
ErrorTypes2["KbPromoteCollision"] = "KbPromoteCollision";
|
|
442
|
+
ErrorTypes2["KbPromoteSelf"] = "KbPromoteSelf";
|
|
443
|
+
ErrorTypes2["KbPromoteStanding"] = "KbPromoteStanding";
|
|
444
|
+
ErrorTypes2["KbPromoteStopped"] = "KbPromoteStopped";
|
|
377
445
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
378
446
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
379
447
|
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
@@ -519,6 +587,89 @@ var KbMissingFlagValueError = class extends BaseError {
|
|
|
519
587
|
}
|
|
520
588
|
flag;
|
|
521
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
|
+
};
|
|
522
673
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
523
674
|
constructor(message, details) {
|
|
524
675
|
super({
|
|
@@ -698,7 +849,9 @@ function revRef(rev) {
|
|
|
698
849
|
var UNCHECKED_REASONS = [
|
|
699
850
|
"remote-unreachable",
|
|
700
851
|
"repo-unauthorized",
|
|
701
|
-
"default-branch-unknown"
|
|
852
|
+
"default-branch-unknown",
|
|
853
|
+
/** Local, but the same finding: a shallow clone has no rev to read. */
|
|
854
|
+
"ref-unavailable"
|
|
702
855
|
];
|
|
703
856
|
function isUncheckedReason(reason) {
|
|
704
857
|
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
@@ -766,6 +919,11 @@ function refShapeIsSafe(ref) {
|
|
|
766
919
|
if (ref.includes("..")) return false;
|
|
767
920
|
return REF_SHAPE.test(ref);
|
|
768
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
|
+
}
|
|
769
927
|
async function refIsWellFormed(ref) {
|
|
770
928
|
if (!refShapeIsSafe(ref)) return false;
|
|
771
929
|
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
@@ -1164,6 +1322,142 @@ function bundleDigest(records, superseded) {
|
|
|
1164
1322
|
return bundleStamp(records, superseded).digest;
|
|
1165
1323
|
}
|
|
1166
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
|
+
|
|
1167
1461
|
// src/anchor-resolver/read.ts
|
|
1168
1462
|
var import_promises2 = require("fs/promises");
|
|
1169
1463
|
var import_node_path2 = require("path");
|
|
@@ -1229,6 +1523,7 @@ function looksLikeWrongRepoRoot(drift) {
|
|
|
1229
1523
|
for (const entries of drift.values()) {
|
|
1230
1524
|
for (const entry of entries) {
|
|
1231
1525
|
if (entry.repo !== void 0) continue;
|
|
1526
|
+
if (entry.side === "old") continue;
|
|
1232
1527
|
checked += 1;
|
|
1233
1528
|
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
1234
1529
|
return false;
|
|
@@ -1364,7 +1659,7 @@ function size(bytes) {
|
|
|
1364
1659
|
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1365
1660
|
}
|
|
1366
1661
|
function pause(ms) {
|
|
1367
|
-
return new Promise((
|
|
1662
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1368
1663
|
}
|
|
1369
1664
|
|
|
1370
1665
|
// src/grammars/manifest.ts
|
|
@@ -1936,8 +2231,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1936
2231
|
}
|
|
1937
2232
|
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1938
2233
|
function captureIndentedBlock(lines, matchLine) {
|
|
1939
|
-
const
|
|
1940
|
-
const indent =
|
|
2234
|
+
const header2 = lines[matchLine] ?? "";
|
|
2235
|
+
const indent = header2.length - header2.trimStart().length;
|
|
1941
2236
|
let headerEnd = -1;
|
|
1942
2237
|
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1943
2238
|
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
@@ -1958,42 +2253,55 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1958
2253
|
}
|
|
1959
2254
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1960
2255
|
}
|
|
1961
|
-
var
|
|
1962
|
-
(name
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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 = [
|
|
1966
2265
|
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1967
2266
|
(name) => new RegExp(`\\b${name}\\b`)
|
|
1968
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
|
+
}
|
|
1969
2296
|
var regexResolver = {
|
|
1970
2297
|
name: "regex",
|
|
1971
2298
|
resolve(source, symbol) {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
const lines = source.split("\n");
|
|
1979
|
-
for (const tier of TIERS) {
|
|
1980
|
-
const pattern = tier(escaped);
|
|
1981
|
-
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1982
|
-
if (!candidates.length) continue;
|
|
1983
|
-
if (parentPattern && candidates.length > 1) {
|
|
1984
|
-
const distances = candidates.map(
|
|
1985
|
-
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1986
|
-
);
|
|
1987
|
-
const nearest = Math.min(...distances);
|
|
1988
|
-
if (Number.isFinite(nearest)) {
|
|
1989
|
-
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
if (candidates.length !== 1) return null;
|
|
1993
|
-
const matchLine = candidates[0];
|
|
1994
|
-
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
1995
|
-
}
|
|
1996
|
-
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" };
|
|
1997
2305
|
}
|
|
1998
2306
|
};
|
|
1999
2307
|
function escapeRegExp(value) {
|
|
@@ -2015,6 +2323,7 @@ function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
|
2015
2323
|
}
|
|
2016
2324
|
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
2017
2325
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
2326
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
2018
2327
|
if (!anchor.symbol) {
|
|
2019
2328
|
const lines = normalized.split("\n");
|
|
2020
2329
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
@@ -2027,11 +2336,17 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2027
2336
|
}
|
|
2028
2337
|
};
|
|
2029
2338
|
}
|
|
2339
|
+
let afterParsedMiss = false;
|
|
2030
2340
|
for (const resolver of resolvers) {
|
|
2031
|
-
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);
|
|
2032
2344
|
if (attempt.kind === "abstain") continue;
|
|
2033
2345
|
if (attempt.kind === "unresolved") {
|
|
2034
|
-
if (attempt.reason === "symbol-not-found")
|
|
2346
|
+
if (attempt.reason === "symbol-not-found") {
|
|
2347
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2035
2350
|
return { ok: false, reason: attempt.reason };
|
|
2036
2351
|
}
|
|
2037
2352
|
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
@@ -2044,12 +2359,28 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2044
2359
|
}
|
|
2045
2360
|
return { ok: false, reason: "symbol-not-found" };
|
|
2046
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
|
+
}
|
|
2047
2378
|
function fromResolve(resolver, source, symbol, file) {
|
|
2048
2379
|
const span2 = resolver.resolve(source, symbol, file);
|
|
2049
2380
|
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2050
2381
|
}
|
|
2051
2382
|
function isResolverName(name) {
|
|
2052
|
-
return name === "tree-sitter" || name === "regex";
|
|
2383
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2053
2384
|
}
|
|
2054
2385
|
async function prepareResolvers(resolvers, files) {
|
|
2055
2386
|
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
@@ -2068,6 +2399,9 @@ function resolverChanged(source, anchor, produced) {
|
|
|
2068
2399
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
2069
2400
|
}
|
|
2070
2401
|
function anchorHashOf(anchor, outcome) {
|
|
2402
|
+
if (outcome.resolver === "span") {
|
|
2403
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2404
|
+
}
|
|
2071
2405
|
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2072
2406
|
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2073
2407
|
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
@@ -2101,37 +2435,60 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
2101
2435
|
}
|
|
2102
2436
|
}
|
|
2103
2437
|
const files = [];
|
|
2438
|
+
const committedWants = [];
|
|
2104
2439
|
const wants = [];
|
|
2105
2440
|
for (const entries of planned.values()) {
|
|
2106
2441
|
for (const { anchor, foreign } of entries) {
|
|
2107
|
-
if (
|
|
2108
|
-
else
|
|
2442
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
2443
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2444
|
+
else files.push(anchor.file);
|
|
2109
2445
|
}
|
|
2110
2446
|
}
|
|
2111
|
-
const [reads, remote] = await Promise.all([
|
|
2447
|
+
const [reads, committed, remote] = await Promise.all([
|
|
2112
2448
|
readAnchorFiles(
|
|
2113
2449
|
files,
|
|
2114
2450
|
options.reader ?? anchorFileReader(repoRoot),
|
|
2115
2451
|
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
2116
2452
|
),
|
|
2453
|
+
readCommitted(repoRoot, committedWants, options),
|
|
2117
2454
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
2118
2455
|
]);
|
|
2119
2456
|
await prepareResolvers(resolvers, [
|
|
2120
2457
|
...files,
|
|
2458
|
+
...committedWants.map((anchor) => anchor.file),
|
|
2121
2459
|
...wants.map((want) => want.file)
|
|
2122
2460
|
]);
|
|
2123
2461
|
const drift = /* @__PURE__ */ new Map();
|
|
2124
2462
|
for (const record of records) {
|
|
2125
2463
|
const entries = [];
|
|
2126
2464
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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));
|
|
2130
2471
|
}
|
|
2131
2472
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
2132
2473
|
}
|
|
2133
2474
|
return drift;
|
|
2134
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
|
+
}
|
|
2135
2492
|
function remoteWants(anchor) {
|
|
2136
2493
|
const repo = anchor.repo;
|
|
2137
2494
|
const wants = [{ repo, file: anchor.file }];
|
|
@@ -2142,6 +2499,7 @@ function base(anchor) {
|
|
|
2142
2499
|
return {
|
|
2143
2500
|
file: anchor.file,
|
|
2144
2501
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2502
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
2145
2503
|
storedHash: anchor.hash
|
|
2146
2504
|
};
|
|
2147
2505
|
}
|
|
@@ -2155,9 +2513,15 @@ function unresolved(anchor, reason, repo) {
|
|
|
2155
2513
|
...classOf(reason)
|
|
2156
2514
|
};
|
|
2157
2515
|
}
|
|
2516
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2517
|
+
"file-missing",
|
|
2518
|
+
"symbol-not-found",
|
|
2519
|
+
"span-out-of-range",
|
|
2520
|
+
"ref-unreadable"
|
|
2521
|
+
]);
|
|
2158
2522
|
function provisionalDriftClass(entry) {
|
|
2159
2523
|
if (entry.state === "unresolved") {
|
|
2160
|
-
return
|
|
2524
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
2161
2525
|
}
|
|
2162
2526
|
return entry.state === "drifted" ? "changed" : void 0;
|
|
2163
2527
|
}
|
|
@@ -2226,6 +2590,13 @@ function remoteEntry(anchor, remote, resolvers) {
|
|
|
2226
2590
|
remoteState: "drifted-from-ref"
|
|
2227
2591
|
});
|
|
2228
2592
|
}
|
|
2593
|
+
if (anchor.side === "old") {
|
|
2594
|
+
return compared(anchor, current, {
|
|
2595
|
+
repo,
|
|
2596
|
+
...extras,
|
|
2597
|
+
remoteState: "matches-ref"
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2229
2600
|
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2230
2601
|
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2231
2602
|
...compared(anchor, head.current, {
|
|
@@ -2261,19 +2632,28 @@ var kbLogEntrySchema = import_zod3.z.object({
|
|
|
2261
2632
|
by: import_zod3.z.string().min(1),
|
|
2262
2633
|
operation: import_zod3.z.string().min(1),
|
|
2263
2634
|
conceptId: import_zod3.z.string().min(1),
|
|
2264
|
-
/**
|
|
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
|
+
*/
|
|
2265
2639
|
target: import_zod3.z.string().min(1).optional()
|
|
2266
2640
|
}).strict();
|
|
2267
2641
|
function renderLogEntry(entry) {
|
|
2268
2642
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
2269
2643
|
`;
|
|
2270
2644
|
}
|
|
2645
|
+
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
2271
2646
|
function parseLog(raw) {
|
|
2272
2647
|
const entries = [];
|
|
2273
2648
|
const malformed = [];
|
|
2274
2649
|
const seen = /* @__PURE__ */ new Set();
|
|
2650
|
+
let conflicted = false;
|
|
2275
2651
|
raw.split("\n").forEach((text, index2) => {
|
|
2276
2652
|
if (!text.trim()) return;
|
|
2653
|
+
if (CONFLICT_MARKER.test(text)) {
|
|
2654
|
+
conflicted = true;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2277
2657
|
let value;
|
|
2278
2658
|
try {
|
|
2279
2659
|
value = JSON.parse(text);
|
|
@@ -2294,7 +2674,7 @@ function parseLog(raw) {
|
|
|
2294
2674
|
entries.sort(
|
|
2295
2675
|
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
|
|
2296
2676
|
);
|
|
2297
|
-
return { entries, malformed };
|
|
2677
|
+
return { entries, malformed, conflicted };
|
|
2298
2678
|
}
|
|
2299
2679
|
|
|
2300
2680
|
// src/search-index.ts
|
|
@@ -2343,8 +2723,8 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2343
2723
|
async function isStale(bundlePath2) {
|
|
2344
2724
|
const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2345
2725
|
if (!indexAt) return true;
|
|
2346
|
-
const { readdir:
|
|
2347
|
-
const names = (await
|
|
2726
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
2727
|
+
const names = (await readdir3(bundlePath2).catch(() => [])).filter(
|
|
2348
2728
|
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
2349
2729
|
);
|
|
2350
2730
|
let stale = false;
|
|
@@ -2759,14 +3139,14 @@ function catalog(bundle, options = {}) {
|
|
|
2759
3139
|
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2760
3140
|
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
2761
3141
|
})).sort(byTypeThenTitle);
|
|
2762
|
-
const
|
|
2763
|
-
for (const entry of entries)
|
|
3142
|
+
const standings2 = { ...EMPTY_STANDINGS };
|
|
3143
|
+
for (const entry of entries) standings2[entry.standing] += 1;
|
|
2764
3144
|
return {
|
|
2765
3145
|
entries,
|
|
2766
3146
|
recordCount: entries.length,
|
|
2767
|
-
standings,
|
|
2768
|
-
currentCount:
|
|
2769
|
-
supersededCount:
|
|
3147
|
+
standings: standings2,
|
|
3148
|
+
currentCount: standings2.current,
|
|
3149
|
+
supersededCount: standings2.superseded,
|
|
2770
3150
|
staleCount: entries.filter((entry) => entry.stale).length
|
|
2771
3151
|
};
|
|
2772
3152
|
}
|
|
@@ -2922,9 +3302,13 @@ function hasEdge(edges, edge) {
|
|
|
2922
3302
|
);
|
|
2923
3303
|
}
|
|
2924
3304
|
|
|
3305
|
+
// src/kb-files.ts
|
|
3306
|
+
var STORE_OWNED_FILES = [INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE];
|
|
3307
|
+
|
|
2925
3308
|
// src/kb-gitattributes.ts
|
|
2926
3309
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
2927
|
-
var
|
|
3310
|
+
var GENERATED = "linguist-generated";
|
|
3311
|
+
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union ${GENERATED}=true`;
|
|
2928
3312
|
function parseLine(line) {
|
|
2929
3313
|
const trimmed = line.trim();
|
|
2930
3314
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -2932,23 +3316,39 @@ function parseLine(line) {
|
|
|
2932
3316
|
return pattern === void 0 ? null : { pattern, attrs };
|
|
2933
3317
|
}
|
|
2934
3318
|
function hasMergeDeclaration(contents) {
|
|
3319
|
+
return declares(contents, LOG_FILE, "merge");
|
|
3320
|
+
}
|
|
3321
|
+
function declares(contents, pattern, attribute) {
|
|
2935
3322
|
return contents.split("\n").some((line) => {
|
|
2936
3323
|
const parsed = parseLine(line);
|
|
2937
|
-
if (!parsed || parsed.pattern !==
|
|
3324
|
+
if (!parsed || parsed.pattern !== pattern) return false;
|
|
2938
3325
|
return parsed.attrs.some(
|
|
2939
|
-
(attr) => attr ===
|
|
3326
|
+
(attr) => attr === attribute || attr === `-${attribute}` || attr.startsWith(`${attribute}=`)
|
|
2940
3327
|
);
|
|
2941
3328
|
});
|
|
2942
3329
|
}
|
|
2943
|
-
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 "";
|
|
2944
3344
|
const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
2945
|
-
return `${separator}${
|
|
3345
|
+
return `${separator}${lines.join("\n")}
|
|
2946
3346
|
`;
|
|
2947
3347
|
}
|
|
2948
3348
|
|
|
2949
3349
|
// src/kb-store.ts
|
|
2950
3350
|
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2951
|
-
var STORE_OWNED =
|
|
3351
|
+
var STORE_OWNED = new Set(STORE_OWNED_FILES);
|
|
2952
3352
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2953
3353
|
var KbStore = class {
|
|
2954
3354
|
constructor(logger = {}) {
|
|
@@ -3075,12 +3475,16 @@ var KbStore = class {
|
|
|
3075
3475
|
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3076
3476
|
* writing, so it holds the complete current set, and a merge would keep
|
|
3077
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.
|
|
3078
3481
|
*/
|
|
3079
3482
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3483
|
+
const checked = anchors.map((anchor) => kbAnchorWriteSchema.parse(anchor));
|
|
3080
3484
|
return this.mutate(
|
|
3081
3485
|
bundlePath2,
|
|
3082
3486
|
conceptId2,
|
|
3083
|
-
(frontmatter) => ({ ...frontmatter, strauss_anchors:
|
|
3487
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: checked }),
|
|
3084
3488
|
{ operation: "anchor-resolve", by: actor }
|
|
3085
3489
|
);
|
|
3086
3490
|
}
|
|
@@ -3166,6 +3570,35 @@ ${answer}
|
|
|
3166
3570
|
`
|
|
3167
3571
|
);
|
|
3168
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
|
+
}
|
|
3169
3602
|
/**
|
|
3170
3603
|
* Records matching a text query, each carrying its standing.
|
|
3171
3604
|
*
|
|
@@ -3401,12 +3834,25 @@ ${answer}
|
|
|
3401
3834
|
}
|
|
3402
3835
|
return expected;
|
|
3403
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
|
+
}
|
|
3404
3849
|
/**
|
|
3405
3850
|
* The log, with unparseable lines reported rather than repaired.
|
|
3406
3851
|
*
|
|
3407
3852
|
* The log is the bundle's only artifact that cannot be reconstructed — the
|
|
3408
3853
|
* records rebuild the index, and the code outlives both, but nothing else
|
|
3409
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.
|
|
3410
3856
|
*/
|
|
3411
3857
|
async readLog(bundlePath2) {
|
|
3412
3858
|
const raw = await (0, import_promises6.readFile)(
|
|
@@ -3414,6 +3860,13 @@ ${answer}
|
|
|
3414
3860
|
"utf8"
|
|
3415
3861
|
).catch(() => "");
|
|
3416
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
|
+
}
|
|
3417
3870
|
for (const bad of result.malformed) {
|
|
3418
3871
|
this.logger.warn?.({
|
|
3419
3872
|
operation: "kb.log.parse",
|
|
@@ -3423,6 +3876,14 @@ ${answer}
|
|
|
3423
3876
|
}
|
|
3424
3877
|
return result;
|
|
3425
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
|
+
}
|
|
3426
3887
|
/**
|
|
3427
3888
|
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
3428
3889
|
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
@@ -3513,7 +3974,8 @@ ${answer}
|
|
|
3513
3974
|
/**
|
|
3514
3975
|
* Declares union merge for the log, so two worktrees writing the same
|
|
3515
3976
|
* bundle interleave their `log.jsonl` lines on merge rather than one
|
|
3516
|
-
* 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.
|
|
3517
3979
|
*
|
|
3518
3980
|
* Called from `record` — every path that appends a log line, not just
|
|
3519
3981
|
* `write` — so a bundle only ever mutated through `setStatus`/`verify`/
|
|
@@ -3526,10 +3988,9 @@ ${answer}
|
|
|
3526
3988
|
* race and created the file between the `readFile` below and this call,
|
|
3527
3989
|
* `wx` fails instead of truncating what that writer just wrote, and the
|
|
3528
3990
|
* failure is swallowed by the catch below same as any other best-effort
|
|
3529
|
-
* miss. A file that exists
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3532
|
-
* 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`).
|
|
3533
3994
|
*
|
|
3534
3995
|
* `readFile` failing is `existing === null` only for `ENOENT` — genuinely
|
|
3535
3996
|
* missing. Any other error (a permission problem, a transient `EMFILE`,
|
|
@@ -3540,12 +4001,12 @@ ${answer}
|
|
|
3540
4001
|
* therefore left untouched and reported as a failure like any other.
|
|
3541
4002
|
*
|
|
3542
4003
|
* Two processes racing the append branch — both read a file without the
|
|
3543
|
-
*
|
|
3544
|
-
* `O_APPEND`, so the result is two copies of the same
|
|
3545
|
-
* torn write, and
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
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).
|
|
3549
4010
|
*
|
|
3550
4011
|
* Best-effort, like the log append it precedes: failing to write this
|
|
3551
4012
|
* file must not fail the mutation it guards.
|
|
@@ -3562,7 +4023,7 @@ ${answer}
|
|
|
3562
4023
|
}
|
|
3563
4024
|
if (existing === null) {
|
|
3564
4025
|
try {
|
|
3565
|
-
await (0, import_promises6.writeFile)(target,
|
|
4026
|
+
await (0, import_promises6.writeFile)(target, appendGitattributesLines(""), {
|
|
3566
4027
|
encoding: "utf8",
|
|
3567
4028
|
flag: "wx"
|
|
3568
4029
|
});
|
|
@@ -3582,8 +4043,9 @@ ${answer}
|
|
|
3582
4043
|
});
|
|
3583
4044
|
return;
|
|
3584
4045
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
4046
|
+
const addition = appendGitattributesLines(existing);
|
|
4047
|
+
if (addition) {
|
|
4048
|
+
await (0, import_promises6.appendFile)(target, addition, "utf8");
|
|
3587
4049
|
this.logger.info?.({
|
|
3588
4050
|
operation: "kb.gitattributes.ensure",
|
|
3589
4051
|
bundlePath: root,
|
|
@@ -3692,7 +4154,7 @@ var composeInputSchema = import_zod4.z.object({
|
|
|
3692
4154
|
why: import_zod4.z.string().min(1),
|
|
3693
4155
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
3694
4156
|
sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
|
|
3695
|
-
anchors: import_zod4.z.array(
|
|
4157
|
+
anchors: import_zod4.z.array(kbAnchorWriteSchema).optional(),
|
|
3696
4158
|
sources: import_zod4.z.array(kbSourceSchema).optional(),
|
|
3697
4159
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
3698
4160
|
assumption: import_zod4.z.boolean().optional(),
|
|
@@ -4352,7 +4814,7 @@ function kbJsonSchemas() {
|
|
|
4352
4814
|
|
|
4353
4815
|
// src/match-diff.ts
|
|
4354
4816
|
function matchToDiff(files, records, options = {}) {
|
|
4355
|
-
const ranges =
|
|
4817
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
4356
4818
|
const anchored = records.filter(
|
|
4357
4819
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
4358
4820
|
);
|
|
@@ -4370,8 +4832,8 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4370
4832
|
let precision = "symbol";
|
|
4371
4833
|
for (const { record, anchors } of candidates) {
|
|
4372
4834
|
const placement = place(anchors, file.filePath, hunk, ranges);
|
|
4373
|
-
if (placement === "miss") continue;
|
|
4374
|
-
if (placement === "file") precision = "file";
|
|
4835
|
+
if (placement.kind === "miss") continue;
|
|
4836
|
+
if (placement.kind === "file") precision = "file";
|
|
4375
4837
|
hits.push(record);
|
|
4376
4838
|
}
|
|
4377
4839
|
if (!hits.length) continue;
|
|
@@ -4385,19 +4847,51 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4385
4847
|
}
|
|
4386
4848
|
return matches3;
|
|
4387
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
|
+
}
|
|
4388
4865
|
function place(anchors, filePath, hunk, ranges) {
|
|
4389
|
-
let fallback = "miss";
|
|
4866
|
+
let fallback = { kind: "miss" };
|
|
4390
4867
|
for (const anchor of anchors) {
|
|
4391
|
-
if (
|
|
4392
|
-
|
|
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
|
+
);
|
|
4393
4882
|
if (!resolved?.length) {
|
|
4394
|
-
fallback = "file";
|
|
4883
|
+
if (fallback.kind === "miss") fallback = { kind: "file", anchor };
|
|
4395
4884
|
continue;
|
|
4396
4885
|
}
|
|
4397
|
-
if (resolved.some((range) => overlaps(range, hunk)))
|
|
4886
|
+
if (resolved.some((range) => overlaps(range, hunk))) {
|
|
4887
|
+
return { kind: "symbol", anchor };
|
|
4888
|
+
}
|
|
4398
4889
|
}
|
|
4399
4890
|
return fallback;
|
|
4400
4891
|
}
|
|
4892
|
+
function side(value) {
|
|
4893
|
+
return value ?? "new";
|
|
4894
|
+
}
|
|
4401
4895
|
function overlaps(range, hunk) {
|
|
4402
4896
|
return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;
|
|
4403
4897
|
}
|
|
@@ -4415,50 +4909,219 @@ function order(records) {
|
|
|
4415
4909
|
)
|
|
4416
4910
|
);
|
|
4417
4911
|
}
|
|
4418
|
-
function
|
|
4912
|
+
function symbolRangeIndex(ranges) {
|
|
4419
4913
|
const byKey = /* @__PURE__ */ new Map();
|
|
4420
4914
|
for (const range of ranges) {
|
|
4421
|
-
const id = key(range.file, range.symbol);
|
|
4915
|
+
const id = key(range.file, range.symbol, side(range.side));
|
|
4422
4916
|
byKey.set(id, [...byKey.get(id) ?? [], range]);
|
|
4423
4917
|
}
|
|
4424
4918
|
return byKey;
|
|
4425
4919
|
}
|
|
4426
|
-
function key(file, symbol) {
|
|
4427
|
-
return `${normalize(file)}#${symbol}`;
|
|
4920
|
+
function key(file, symbol, at2) {
|
|
4921
|
+
return `${normalize(file)}#${symbol}#${at2}`;
|
|
4428
4922
|
}
|
|
4429
4923
|
function normalize(path) {
|
|
4430
4924
|
return path.replace(/^\.\//, "");
|
|
4431
4925
|
}
|
|
4432
4926
|
|
|
4433
|
-
// src/
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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 ?? []) {
|
|
5117
|
+
const previous = byId.get(old);
|
|
5118
|
+
if (!previous) {
|
|
5119
|
+
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
5120
|
+
} else if (previous.frontmatter.strauss_status !== "superseded") {
|
|
5121
|
+
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
for (const link2 of fm.strauss_links ?? []) {
|
|
4462
5125
|
if (!isKbLinkRel(link2.rel)) {
|
|
4463
5126
|
report(
|
|
4464
5127
|
"link_rel",
|
|
@@ -4489,6 +5152,34 @@ function validateBundle(records) {
|
|
|
4489
5152
|
}
|
|
4490
5153
|
}
|
|
4491
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
|
+
}
|
|
4492
5183
|
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
4493
5184
|
report(
|
|
4494
5185
|
"anchor_repo",
|
|
@@ -4535,14 +5226,19 @@ var DAY_MS = 864e5;
|
|
|
4535
5226
|
function anchorResolverCounts(bundle) {
|
|
4536
5227
|
let treeSitter = 0;
|
|
4537
5228
|
let regex = 0;
|
|
5229
|
+
let span2 = 0;
|
|
5230
|
+
let oldSide = 0;
|
|
4538
5231
|
for (const record of bundle) {
|
|
4539
5232
|
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4540
|
-
if (!anchor.hash
|
|
4541
|
-
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;
|
|
4542
5238
|
else regex += 1;
|
|
4543
5239
|
}
|
|
4544
5240
|
}
|
|
4545
|
-
return { total: treeSitter + regex, treeSitter, regex };
|
|
5241
|
+
return { total: treeSitter + regex + span2, treeSitter, regex, span: span2, oldSide };
|
|
4546
5242
|
}
|
|
4547
5243
|
function doctor(bundle, options = {}) {
|
|
4548
5244
|
const thresholds = {
|
|
@@ -4552,7 +5248,7 @@ function doctor(bundle, options = {}) {
|
|
|
4552
5248
|
};
|
|
4553
5249
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
4554
5250
|
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
4555
|
-
const
|
|
5251
|
+
const standings2 = new Map(
|
|
4556
5252
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
4557
5253
|
);
|
|
4558
5254
|
const inForce = adjudicated.filter(
|
|
@@ -4565,7 +5261,7 @@ function doctor(bundle, options = {}) {
|
|
|
4565
5261
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
4566
5262
|
group("orphaned", orphaned(bundle)),
|
|
4567
5263
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
4568
|
-
group("superseded-but-cited", supersededButCited(bundle,
|
|
5264
|
+
group("superseded-but-cited", supersededButCited(bundle, standings2)),
|
|
4569
5265
|
group("drifted", drifted(inForce)),
|
|
4570
5266
|
group("unchecked", unchecked(inForce))
|
|
4571
5267
|
];
|
|
@@ -4731,14 +5427,14 @@ function brokenSupersession(bundle, adjudicated) {
|
|
|
4731
5427
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
4732
5428
|
);
|
|
4733
5429
|
}
|
|
4734
|
-
function supersededButCited(bundle,
|
|
5430
|
+
function supersededButCited(bundle, standings2) {
|
|
4735
5431
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
4736
5432
|
const findings = [];
|
|
4737
5433
|
for (const record of bundle) {
|
|
4738
|
-
const standing =
|
|
5434
|
+
const standing = standings2.get(record.conceptId);
|
|
4739
5435
|
if (standing === "superseded" || standing === "rejected") continue;
|
|
4740
5436
|
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
4741
|
-
const targetStanding =
|
|
5437
|
+
const targetStanding = standings2.get(target.conceptId);
|
|
4742
5438
|
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
4743
5439
|
continue;
|
|
4744
5440
|
}
|
|
@@ -5004,6 +5700,7 @@ var anchorResolveCommand = define({
|
|
|
5004
5700
|
const base2 = {
|
|
5005
5701
|
file: anchor.file,
|
|
5006
5702
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5703
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
5007
5704
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
5008
5705
|
// and now resolves to nothing is a broken anchor, and the exit code
|
|
5009
5706
|
// has to be able to tell it from one nobody ever stamped.
|
|
@@ -5180,12 +5877,18 @@ async function readSources(anchors, root, offline) {
|
|
|
5180
5877
|
const foreign = new Map(
|
|
5181
5878
|
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
5182
5879
|
);
|
|
5183
|
-
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
|
+
);
|
|
5184
5886
|
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
5185
5887
|
const reads = await readAnchorFiles(
|
|
5186
5888
|
local.map((anchor) => anchor.file),
|
|
5187
5889
|
anchorFileReader(root)
|
|
5188
5890
|
);
|
|
5891
|
+
const atRef = await readCommitted(root, committed);
|
|
5189
5892
|
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
5190
5893
|
offline
|
|
5191
5894
|
});
|
|
@@ -5197,6 +5900,13 @@ async function readSources(anchors, root, offline) {
|
|
|
5197
5900
|
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
5198
5901
|
);
|
|
5199
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
|
+
}
|
|
5200
5910
|
for (const anchor of remote) {
|
|
5201
5911
|
const repo = anchor.repo;
|
|
5202
5912
|
const key2 = normalizeRepoUrl(repo);
|
|
@@ -5329,141 +6039,12 @@ function count(value, noun) {
|
|
|
5329
6039
|
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
5330
6040
|
}
|
|
5331
6041
|
|
|
5332
|
-
// src/commands/
|
|
5333
|
-
var
|
|
5334
|
-
var
|
|
5335
|
-
|
|
5336
|
-
tool: "kb_context",
|
|
5337
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
5338
|
-
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
5339
|
-
input: import_zod13.z.object({
|
|
5340
|
-
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
5341
|
-
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
5342
|
-
),
|
|
5343
|
-
fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
5344
|
-
"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."
|
|
5345
|
-
),
|
|
5346
|
-
profile: import_zod13.z.string().optional().describe(
|
|
5347
|
-
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
5348
|
-
),
|
|
5349
|
-
excludeTags: import_zod13.z.array(import_zod13.z.string().min(1)).optional().describe(
|
|
5350
|
-
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
5351
|
-
),
|
|
5352
|
-
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
5353
|
-
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
5354
|
-
),
|
|
5355
|
-
event: import_zod13.z.string().optional().describe(
|
|
5356
|
-
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
5357
|
-
)
|
|
5358
|
-
}),
|
|
5359
|
-
fromArgv: (argv) => {
|
|
5360
|
-
const budget = argvFlag(argv, "--budget");
|
|
5361
|
-
const fullUnder = argvFlag(argv, "--full-under");
|
|
5362
|
-
const profile = argvFlag(argv, "--profile");
|
|
5363
|
-
const format = argvFlag(argv, "--format");
|
|
5364
|
-
const event = argvFlag(argv, "--event");
|
|
5365
|
-
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
5366
|
-
return {
|
|
5367
|
-
...budget ? { budgetTokens: Number(budget) } : {},
|
|
5368
|
-
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
5369
|
-
...profile ? { profile } : {},
|
|
5370
|
-
...excludeTags.length ? { excludeTags } : {},
|
|
5371
|
-
...format ? { format } : {},
|
|
5372
|
-
...event ? { event } : {}
|
|
5373
|
-
};
|
|
5374
|
-
},
|
|
5375
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
5376
|
-
const result = await buildContext(store, process.cwd(), {
|
|
5377
|
-
...budgetTokens ? { budgetTokens } : {},
|
|
5378
|
-
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
5379
|
-
...profile ? { profile } : {},
|
|
5380
|
-
...excludeTags ? { excludeTags } : {},
|
|
5381
|
-
// Degradations — a full pin that could not fit, a refused block — go
|
|
5382
|
-
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
5383
|
-
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
5384
|
-
// see budget pressure without reading injected context.
|
|
5385
|
-
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
5386
|
-
`)
|
|
5387
|
-
});
|
|
5388
|
-
if (!result.block) return "";
|
|
5389
|
-
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
5390
|
-
}
|
|
5391
|
-
});
|
|
5392
|
-
|
|
5393
|
-
// 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");
|
|
5394
6046
|
var import_zod15 = require("zod");
|
|
5395
6047
|
|
|
5396
|
-
// src/drift/git.ts
|
|
5397
|
-
var import_node_child_process3 = require("child_process");
|
|
5398
|
-
var import_node_util3 = require("util");
|
|
5399
|
-
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
5400
|
-
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
5401
|
-
var GIT_TIMEOUT_MS = 5e3;
|
|
5402
|
-
async function git2(cwd, args) {
|
|
5403
|
-
const env = { ...process.env };
|
|
5404
|
-
delete env["GIT_DIR"];
|
|
5405
|
-
delete env["GIT_WORK_TREE"];
|
|
5406
|
-
delete env["GIT_INDEX_FILE"];
|
|
5407
|
-
try {
|
|
5408
|
-
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
5409
|
-
timeout: GIT_TIMEOUT_MS,
|
|
5410
|
-
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
5411
|
-
env
|
|
5412
|
-
});
|
|
5413
|
-
return { ok: true, stdout };
|
|
5414
|
-
} catch {
|
|
5415
|
-
return { ok: false };
|
|
5416
|
-
}
|
|
5417
|
-
}
|
|
5418
|
-
async function listRepoFiles(repoRoot) {
|
|
5419
|
-
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
5420
|
-
if (!result.ok) return [];
|
|
5421
|
-
return result.stdout.split("\0").filter(Boolean);
|
|
5422
|
-
}
|
|
5423
|
-
async function readOldSource(repoRoot, anchor) {
|
|
5424
|
-
if (!filePathIsSafe(anchor.file))
|
|
5425
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5426
|
-
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
5427
|
-
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
5428
|
-
if (shown2 !== null) {
|
|
5429
|
-
return {
|
|
5430
|
-
ok: true,
|
|
5431
|
-
source: shown2,
|
|
5432
|
-
origin: { kind: "ref", ref: anchor.ref }
|
|
5433
|
-
};
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5436
|
-
const at2 = anchor.resolved_at;
|
|
5437
|
-
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
5438
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5439
|
-
}
|
|
5440
|
-
const found = await git2(repoRoot, [
|
|
5441
|
-
"log",
|
|
5442
|
-
"-1",
|
|
5443
|
-
"--format=%H",
|
|
5444
|
-
`--before=${at2}`,
|
|
5445
|
-
"--end-of-options",
|
|
5446
|
-
"HEAD",
|
|
5447
|
-
"--",
|
|
5448
|
-
anchor.file
|
|
5449
|
-
]);
|
|
5450
|
-
const sha = found.ok ? found.stdout.trim() : "";
|
|
5451
|
-
if (!sha || !refShapeIsSafe(sha))
|
|
5452
|
-
return { ok: false, reason: "unrecoverable" };
|
|
5453
|
-
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
5454
|
-
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
5455
|
-
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
5456
|
-
}
|
|
5457
|
-
async function showFile(repoRoot, ref, file) {
|
|
5458
|
-
const path = file.replace(/^\.\//, "");
|
|
5459
|
-
const result = await git2(repoRoot, [
|
|
5460
|
-
"show",
|
|
5461
|
-
"--end-of-options",
|
|
5462
|
-
`${ref}:${path}`
|
|
5463
|
-
]);
|
|
5464
|
-
return result.ok ? result.stdout : null;
|
|
5465
|
-
}
|
|
5466
|
-
|
|
5467
6048
|
// src/drift/moved.ts
|
|
5468
6049
|
var import_promises9 = require("fs/promises");
|
|
5469
6050
|
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
@@ -5482,6 +6063,7 @@ function movedSearch(repoRoot, options = {}) {
|
|
|
5482
6063
|
async find(anchor) {
|
|
5483
6064
|
const stored = anchor.hash;
|
|
5484
6065
|
if (!stored) return void 0;
|
|
6066
|
+
if (anchor.span) return sameFileWindow(anchor, read, stored);
|
|
5485
6067
|
const language = languageForFile(anchor.file);
|
|
5486
6068
|
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5487
6069
|
const candidates = await filesForLanguage(language);
|
|
@@ -5579,6 +6161,11 @@ async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
|
5579
6161
|
);
|
|
5580
6162
|
const out = [];
|
|
5581
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
|
+
}
|
|
5582
6169
|
const movedTo = await search.find(anchor);
|
|
5583
6170
|
if (movedTo) {
|
|
5584
6171
|
out.push({
|
|
@@ -5652,8 +6239,8 @@ function unifiedDiff(before, after, options = {}) {
|
|
|
5652
6239
|
}
|
|
5653
6240
|
const truncated = body.length > max;
|
|
5654
6241
|
const shown = truncated ? body.slice(0, max) : body;
|
|
5655
|
-
const
|
|
5656
|
-
const lines = [
|
|
6242
|
+
const header2 = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
6243
|
+
const lines = [header2, ...shown];
|
|
5657
6244
|
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5658
6245
|
return { text: lines.join("\n"), added, removed, truncated };
|
|
5659
6246
|
}
|
|
@@ -5713,12 +6300,12 @@ async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
|
5713
6300
|
...options.search ? { search: options.search } : {},
|
|
5714
6301
|
withHistory: options.withDiff !== false
|
|
5715
6302
|
});
|
|
5716
|
-
const
|
|
6303
|
+
const open2 = classified.filter(
|
|
5717
6304
|
(found) => found.class === "changed" || found.class === "gone"
|
|
5718
6305
|
);
|
|
5719
|
-
if (!
|
|
5720
|
-
const budget = diffBudget(
|
|
5721
|
-
const anchors =
|
|
6306
|
+
if (!open2.length) return { packet: null, classified };
|
|
6307
|
+
const budget = diffBudget(open2.length);
|
|
6308
|
+
const anchors = open2.map(
|
|
5722
6309
|
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5723
6310
|
);
|
|
5724
6311
|
const type = record.frontmatter.type;
|
|
@@ -5792,45 +6379,632 @@ function claimOf(record) {
|
|
|
5792
6379
|
return text ? { section, text } : null;
|
|
5793
6380
|
}
|
|
5794
6381
|
|
|
5795
|
-
// src/commands/
|
|
6382
|
+
// src/commands/match/command.ts
|
|
5796
6383
|
var import_zod14 = require("zod");
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
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) ?? [];
|
|
7000
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
7001
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
7002
|
+
}
|
|
7003
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
7004
|
+
(hit) => hit.record.conceptId === id
|
|
7005
|
+
)?.standing;
|
|
7006
|
+
const impact2 = await store.impact(path, id);
|
|
7007
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
5834
7008
|
...withDiff ? { withDiff: true } : {},
|
|
5835
7009
|
impact: impact2,
|
|
5836
7010
|
...standing ? { standing } : {}
|
|
@@ -5846,7 +7020,10 @@ var reassessCommand = define({
|
|
|
5846
7020
|
relocated.set(found.anchor, {
|
|
5847
7021
|
...found.anchor,
|
|
5848
7022
|
file: to.file,
|
|
5849
|
-
...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 } } : {}
|
|
5850
7027
|
});
|
|
5851
7028
|
rebaselined.push({
|
|
5852
7029
|
file: found.anchor.file,
|
|
@@ -5945,13 +7122,13 @@ function at(file, symbol) {
|
|
|
5945
7122
|
}
|
|
5946
7123
|
|
|
5947
7124
|
// src/commands/doctor.ts
|
|
5948
|
-
var days = (what, fallback) =>
|
|
7125
|
+
var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5949
7126
|
var doctorCommand = define({
|
|
5950
7127
|
name: "doctor",
|
|
5951
7128
|
tool: "kb_doctor",
|
|
5952
7129
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5953
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.",
|
|
5954
|
-
input:
|
|
7131
|
+
input: import_zod18.z.object({
|
|
5955
7132
|
bundlePath,
|
|
5956
7133
|
repoRoot: REPO_ROOT,
|
|
5957
7134
|
expiringDays: days(
|
|
@@ -5966,16 +7143,16 @@ var doctorCommand = define({
|
|
|
5966
7143
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5967
7144
|
DEFAULT_AGING_DAYS
|
|
5968
7145
|
),
|
|
5969
|
-
offline:
|
|
7146
|
+
offline: import_zod18.z.boolean().optional().describe(
|
|
5970
7147
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5971
7148
|
),
|
|
5972
|
-
strict:
|
|
7149
|
+
strict: import_zod18.z.boolean().optional().describe(
|
|
5973
7150
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5974
7151
|
),
|
|
5975
|
-
drifted:
|
|
7152
|
+
drifted: import_zod18.z.boolean().optional().describe(
|
|
5976
7153
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5977
7154
|
),
|
|
5978
|
-
withDiff:
|
|
7155
|
+
withDiff: import_zod18.z.boolean().optional().describe(
|
|
5979
7156
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5980
7157
|
)
|
|
5981
7158
|
}),
|
|
@@ -6031,7 +7208,7 @@ var doctorCommand = define({
|
|
|
6031
7208
|
...hints.length ? { hints } : {}
|
|
6032
7209
|
};
|
|
6033
7210
|
}
|
|
6034
|
-
const
|
|
7211
|
+
const standings2 = new Map(
|
|
6035
7212
|
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
6036
7213
|
hit.record.conceptId,
|
|
6037
7214
|
hit.standing
|
|
@@ -6045,7 +7222,7 @@ var doctorCommand = define({
|
|
|
6045
7222
|
(entry) => entry.conceptId === found.conceptId
|
|
6046
7223
|
);
|
|
6047
7224
|
if (!record) continue;
|
|
6048
|
-
const standing =
|
|
7225
|
+
const standing = standings2.get(record.conceptId);
|
|
6049
7226
|
const built = await reassessPacket(
|
|
6050
7227
|
repoRoot ?? process.cwd(),
|
|
6051
7228
|
record,
|
|
@@ -6082,15 +7259,16 @@ var doctorCommand = define({
|
|
|
6082
7259
|
});
|
|
6083
7260
|
function render2(result) {
|
|
6084
7261
|
if (result.packets) return renderPackets(result);
|
|
6085
|
-
const { thresholds } = result;
|
|
7262
|
+
const { thresholds, anchorResolvers: counts } = result;
|
|
6086
7263
|
const lines = [
|
|
6087
7264
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
6088
7265
|
`records: ${result.recordCount}`,
|
|
6089
7266
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
6090
7267
|
`checked: ${result.checkedAt}`,
|
|
6091
|
-
...
|
|
6092
|
-
|
|
6093
|
-
|
|
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}`] : [],
|
|
6094
7272
|
""
|
|
6095
7273
|
];
|
|
6096
7274
|
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
@@ -6115,6 +7293,14 @@ function render2(result) {
|
|
|
6115
7293
|
);
|
|
6116
7294
|
return lines.join("\n");
|
|
6117
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
|
+
}
|
|
6118
7304
|
function renderPackets(result) {
|
|
6119
7305
|
const packets = result.packets ?? [];
|
|
6120
7306
|
const lines = [
|
|
@@ -6140,20 +7326,155 @@ function renderPackets(result) {
|
|
|
6140
7326
|
return lines.join("\n");
|
|
6141
7327
|
}
|
|
6142
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
|
+
|
|
6143
7464
|
// src/commands/impact.ts
|
|
6144
|
-
var
|
|
7465
|
+
var import_zod20 = require("zod");
|
|
6145
7466
|
var impactCommand = define({
|
|
6146
7467
|
name: "impact",
|
|
6147
7468
|
tool: "kb_impact",
|
|
6148
7469
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
6149
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.",
|
|
6150
|
-
input:
|
|
7471
|
+
input: import_zod20.z.object({
|
|
6151
7472
|
bundlePath,
|
|
6152
7473
|
conceptId,
|
|
6153
|
-
depth:
|
|
7474
|
+
depth: import_zod20.z.number().int().positive().optional().describe(
|
|
6154
7475
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
6155
7476
|
),
|
|
6156
|
-
rels:
|
|
7477
|
+
rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
6157
7478
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
6158
7479
|
)
|
|
6159
7480
|
}),
|
|
@@ -6174,15 +7495,15 @@ var impactCommand = define({
|
|
|
6174
7495
|
});
|
|
6175
7496
|
|
|
6176
7497
|
// src/commands/list.ts
|
|
6177
|
-
var
|
|
7498
|
+
var import_zod21 = require("zod");
|
|
6178
7499
|
var listCommand = define({
|
|
6179
7500
|
name: "list",
|
|
6180
7501
|
tool: "kb_list",
|
|
6181
7502
|
usage: "list [type] [--tag T]...",
|
|
6182
7503
|
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
6183
|
-
input:
|
|
7504
|
+
input: import_zod21.z.object({
|
|
6184
7505
|
bundlePath,
|
|
6185
|
-
type:
|
|
7506
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
6186
7507
|
tags: TAGS
|
|
6187
7508
|
}),
|
|
6188
7509
|
fromArgv: (argv, path) => {
|
|
@@ -6206,17 +7527,17 @@ var listCommand = define({
|
|
|
6206
7527
|
});
|
|
6207
7528
|
|
|
6208
7529
|
// src/commands/load.ts
|
|
6209
|
-
var
|
|
7530
|
+
var import_zod22 = require("zod");
|
|
6210
7531
|
var loadCommand = define({
|
|
6211
7532
|
name: "load",
|
|
6212
7533
|
tool: "kb_load",
|
|
6213
7534
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6214
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.",
|
|
6215
|
-
input:
|
|
7536
|
+
input: import_zod22.z.object({
|
|
6216
7537
|
bundlePath,
|
|
6217
|
-
type:
|
|
6218
|
-
budgetTokens:
|
|
6219
|
-
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(
|
|
6220
7541
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6221
7542
|
),
|
|
6222
7543
|
repoRoot: REPO_ROOT
|
|
@@ -6258,25 +7579,25 @@ var loadCommand = define({
|
|
|
6258
7579
|
});
|
|
6259
7580
|
|
|
6260
7581
|
// src/commands/log.ts
|
|
6261
|
-
var
|
|
7582
|
+
var import_zod23 = require("zod");
|
|
6262
7583
|
var logCommand = define({
|
|
6263
7584
|
name: "log",
|
|
6264
7585
|
tool: "kb_log",
|
|
6265
7586
|
usage: "log",
|
|
6266
7587
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6267
|
-
input:
|
|
7588
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
6268
7589
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6269
7590
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6270
7591
|
});
|
|
6271
7592
|
|
|
6272
7593
|
// src/commands/no-decision.ts
|
|
6273
|
-
var
|
|
7594
|
+
var import_zod24 = require("zod");
|
|
6274
7595
|
var noDecisionCommand = define({
|
|
6275
7596
|
name: "no-decision",
|
|
6276
7597
|
tool: "kb_no_decision",
|
|
6277
7598
|
usage: "no-decision <reason...>",
|
|
6278
7599
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6279
|
-
input:
|
|
7600
|
+
input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
|
|
6280
7601
|
fromArgv: (argv, path) => ({
|
|
6281
7602
|
bundlePath: path,
|
|
6282
7603
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6293,20 +7614,20 @@ var noDecisionCommand = define({
|
|
|
6293
7614
|
});
|
|
6294
7615
|
|
|
6295
7616
|
// src/commands/pack.ts
|
|
6296
|
-
var
|
|
7617
|
+
var import_zod25 = require("zod");
|
|
6297
7618
|
var packCommand = define({
|
|
6298
7619
|
name: "pack",
|
|
6299
7620
|
tool: "kb_pack",
|
|
6300
7621
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6301
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.",
|
|
6302
|
-
input:
|
|
7623
|
+
input: import_zod25.z.object({
|
|
6303
7624
|
bundlePath,
|
|
6304
7625
|
conceptId,
|
|
6305
|
-
hops:
|
|
6306
|
-
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(
|
|
6307
7628
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6308
7629
|
),
|
|
6309
|
-
budgetTokens:
|
|
7630
|
+
budgetTokens: import_zod25.z.number().int().positive().optional().describe(
|
|
6310
7631
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6311
7632
|
)
|
|
6312
7633
|
}),
|
|
@@ -6393,22 +7714,22 @@ function warningLabel(warning) {
|
|
|
6393
7714
|
}
|
|
6394
7715
|
|
|
6395
7716
|
// src/commands/pin.ts
|
|
6396
|
-
var
|
|
7717
|
+
var import_zod26 = require("zod");
|
|
6397
7718
|
var pinCommand = define({
|
|
6398
7719
|
name: "pin",
|
|
6399
7720
|
tool: "kb_pin",
|
|
6400
7721
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6401
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.",
|
|
6402
|
-
input:
|
|
7723
|
+
input: import_zod26.z.object({
|
|
6403
7724
|
bundlePath,
|
|
6404
|
-
mode:
|
|
7725
|
+
mode: import_zod26.z.enum(["full", "index"]).optional().describe(
|
|
6405
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."
|
|
6406
7727
|
),
|
|
6407
|
-
profiles:
|
|
6408
|
-
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(
|
|
6409
7730
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6410
7731
|
),
|
|
6411
|
-
frozen:
|
|
7732
|
+
frozen: import_zod26.z.boolean().optional().describe(
|
|
6412
7733
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6413
7734
|
)
|
|
6414
7735
|
}),
|
|
@@ -6437,29 +7758,305 @@ var pinCommand = define({
|
|
|
6437
7758
|
});
|
|
6438
7759
|
|
|
6439
7760
|
// src/commands/pins.ts
|
|
6440
|
-
var
|
|
7761
|
+
var import_zod27 = require("zod");
|
|
6441
7762
|
var pinsCommand = define({
|
|
6442
7763
|
name: "pins",
|
|
6443
7764
|
tool: "kb_pins",
|
|
6444
7765
|
usage: "pins",
|
|
6445
7766
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6446
|
-
input:
|
|
7767
|
+
input: import_zod27.z.object({}),
|
|
6447
7768
|
fromArgv: () => ({}),
|
|
6448
7769
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6449
7770
|
});
|
|
6450
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
|
+
|
|
6451
8048
|
// src/commands/query.ts
|
|
6452
|
-
var
|
|
8049
|
+
var import_zod29 = require("zod");
|
|
6453
8050
|
var queryCommand = define({
|
|
6454
8051
|
name: "query",
|
|
6455
8052
|
tool: "kb_query",
|
|
6456
8053
|
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6457
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.",
|
|
6458
|
-
input:
|
|
8055
|
+
input: import_zod29.z.object({
|
|
6459
8056
|
bundlePath,
|
|
6460
|
-
text:
|
|
6461
|
-
type:
|
|
6462
|
-
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(),
|
|
6463
8060
|
tags: TAGS,
|
|
6464
8061
|
repoRoot: REPO_ROOT
|
|
6465
8062
|
}),
|
|
@@ -6493,43 +8090,43 @@ var queryCommand = define({
|
|
|
6493
8090
|
});
|
|
6494
8091
|
|
|
6495
8092
|
// src/commands/read-index.ts
|
|
6496
|
-
var
|
|
8093
|
+
var import_zod30 = require("zod");
|
|
6497
8094
|
var readIndexCommand = define({
|
|
6498
8095
|
name: "index",
|
|
6499
8096
|
tool: "kb_index",
|
|
6500
8097
|
usage: "index",
|
|
6501
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.",
|
|
6502
|
-
input:
|
|
8099
|
+
input: import_zod30.z.object({ bundlePath }),
|
|
6503
8100
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6504
8101
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6505
8102
|
});
|
|
6506
8103
|
|
|
6507
8104
|
// src/commands/schema.ts
|
|
6508
|
-
var
|
|
8105
|
+
var import_zod31 = require("zod");
|
|
6509
8106
|
var schemaCommand = define({
|
|
6510
8107
|
name: "schema",
|
|
6511
8108
|
tool: "kb_schema",
|
|
6512
8109
|
usage: "schema",
|
|
6513
8110
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6514
|
-
input:
|
|
8111
|
+
input: import_zod31.z.object({}),
|
|
6515
8112
|
fromArgv: () => ({}),
|
|
6516
8113
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6517
8114
|
});
|
|
6518
8115
|
|
|
6519
8116
|
// src/commands/stamp.ts
|
|
6520
|
-
var
|
|
6521
|
-
var
|
|
8117
|
+
var import_promises12 = require("fs/promises");
|
|
8118
|
+
var import_zod32 = require("zod");
|
|
6522
8119
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6523
8120
|
var stampCommand = define({
|
|
6524
8121
|
name: "stamp",
|
|
6525
8122
|
tool: "kb_stamp",
|
|
6526
8123
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6527
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.",
|
|
6528
|
-
input:
|
|
6529
|
-
bundlePath:
|
|
8125
|
+
input: import_zod32.z.object({
|
|
8126
|
+
bundlePath: import_zod32.z.string().min(1).optional().describe(
|
|
6530
8127
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6531
8128
|
),
|
|
6532
|
-
since:
|
|
8129
|
+
since: import_zod32.z.string().min(1).optional().describe(
|
|
6533
8130
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6534
8131
|
)
|
|
6535
8132
|
}),
|
|
@@ -6591,7 +8188,7 @@ async function readBaseline(since) {
|
|
|
6591
8188
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
6592
8189
|
let parsed;
|
|
6593
8190
|
try {
|
|
6594
|
-
parsed = JSON.parse(await (0,
|
|
8191
|
+
parsed = JSON.parse(await (0, import_promises12.readFile)(since, "utf8"));
|
|
6595
8192
|
} catch {
|
|
6596
8193
|
throw new KbStampBaselineError(since);
|
|
6597
8194
|
}
|
|
@@ -6615,16 +8212,16 @@ async function readBaseline(since) {
|
|
|
6615
8212
|
}
|
|
6616
8213
|
|
|
6617
8214
|
// src/commands/status.ts
|
|
6618
|
-
var
|
|
8215
|
+
var import_zod33 = require("zod");
|
|
6619
8216
|
var statusCommand = define({
|
|
6620
8217
|
name: "status",
|
|
6621
8218
|
tool: "kb_status",
|
|
6622
8219
|
usage: "status <concept-id> <status>",
|
|
6623
8220
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6624
|
-
input:
|
|
8221
|
+
input: import_zod33.z.object({
|
|
6625
8222
|
bundlePath,
|
|
6626
8223
|
conceptId,
|
|
6627
|
-
status:
|
|
8224
|
+
status: import_zod33.z.enum(KB_RECORD_STATUSES)
|
|
6628
8225
|
}),
|
|
6629
8226
|
fromArgv: (argv, path) => ({
|
|
6630
8227
|
bundlePath: path,
|
|
@@ -6639,13 +8236,13 @@ var statusCommand = define({
|
|
|
6639
8236
|
});
|
|
6640
8237
|
|
|
6641
8238
|
// src/commands/supersede.ts
|
|
6642
|
-
var
|
|
8239
|
+
var import_zod34 = require("zod");
|
|
6643
8240
|
var supersedeCommand = define({
|
|
6644
8241
|
name: "supersede",
|
|
6645
8242
|
tool: "kb_supersede",
|
|
6646
8243
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6647
8244
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6648
|
-
input:
|
|
8245
|
+
input: import_zod34.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6649
8246
|
fromArgv: (argv, path) => ({
|
|
6650
8247
|
bundlePath: path,
|
|
6651
8248
|
conceptId: argv[1],
|
|
@@ -6658,17 +8255,153 @@ var supersedeCommand = define({
|
|
|
6658
8255
|
}
|
|
6659
8256
|
});
|
|
6660
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
|
+
|
|
6661
8394
|
// src/commands/sync-instructions.ts
|
|
6662
|
-
var
|
|
8395
|
+
var import_zod36 = require("zod");
|
|
6663
8396
|
var syncInstructionsCommand = define({
|
|
6664
8397
|
name: "sync-instructions",
|
|
6665
8398
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
6666
8399
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
6667
|
-
input:
|
|
6668
|
-
file:
|
|
6669
|
-
budgetTokens:
|
|
6670
|
-
fullUnderTokens:
|
|
6671
|
-
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()
|
|
6672
8405
|
}),
|
|
6673
8406
|
fromArgv: (argv) => {
|
|
6674
8407
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -6694,17 +8427,17 @@ var syncInstructionsCommand = define({
|
|
|
6694
8427
|
});
|
|
6695
8428
|
|
|
6696
8429
|
// src/commands/trace.ts
|
|
6697
|
-
var
|
|
8430
|
+
var import_zod37 = require("zod");
|
|
6698
8431
|
var traceCommand = define({
|
|
6699
8432
|
name: "trace",
|
|
6700
8433
|
tool: "kb_trace",
|
|
6701
8434
|
usage: "trace <concept-id> [edges...]",
|
|
6702
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".',
|
|
6703
|
-
input:
|
|
8436
|
+
input: import_zod37.z.object({
|
|
6704
8437
|
bundlePath,
|
|
6705
8438
|
conceptId,
|
|
6706
|
-
edges:
|
|
6707
|
-
depth:
|
|
8439
|
+
edges: import_zod37.z.array(import_zod37.z.enum(TRACE_EDGES)).optional(),
|
|
8440
|
+
depth: import_zod37.z.number().int().positive().optional()
|
|
6708
8441
|
}),
|
|
6709
8442
|
fromArgv: (argv, path) => ({
|
|
6710
8443
|
bundlePath: path,
|
|
@@ -6726,37 +8459,37 @@ var traceCommand = define({
|
|
|
6726
8459
|
});
|
|
6727
8460
|
|
|
6728
8461
|
// src/commands/types.ts
|
|
6729
|
-
var
|
|
8462
|
+
var import_zod38 = require("zod");
|
|
6730
8463
|
var typesCommand = define({
|
|
6731
8464
|
name: "types",
|
|
6732
8465
|
tool: "kb_types",
|
|
6733
8466
|
usage: "types",
|
|
6734
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.",
|
|
6735
|
-
input:
|
|
8468
|
+
input: import_zod38.z.object({}),
|
|
6736
8469
|
fromArgv: () => ({}),
|
|
6737
8470
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
6738
8471
|
});
|
|
6739
8472
|
|
|
6740
8473
|
// src/commands/unpin.ts
|
|
6741
|
-
var
|
|
8474
|
+
var import_zod39 = require("zod");
|
|
6742
8475
|
var unpinCommand = define({
|
|
6743
8476
|
name: "unpin",
|
|
6744
8477
|
tool: "kb_unpin",
|
|
6745
8478
|
usage: "unpin [bundle-path]",
|
|
6746
8479
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
6747
|
-
input:
|
|
8480
|
+
input: import_zod39.z.object({ bundlePath }),
|
|
6748
8481
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
6749
8482
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
6750
8483
|
});
|
|
6751
8484
|
|
|
6752
8485
|
// src/commands/validate.ts
|
|
6753
|
-
var
|
|
8486
|
+
var import_zod40 = require("zod");
|
|
6754
8487
|
var validateCommand = define({
|
|
6755
8488
|
name: "validate",
|
|
6756
8489
|
tool: "kb_validate",
|
|
6757
8490
|
usage: "validate",
|
|
6758
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.",
|
|
6759
|
-
input:
|
|
8492
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
6760
8493
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6761
8494
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
6762
8495
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -6766,16 +8499,16 @@ var validateCommand = define({
|
|
|
6766
8499
|
});
|
|
6767
8500
|
|
|
6768
8501
|
// src/commands/verify.ts
|
|
6769
|
-
var
|
|
8502
|
+
var import_zod41 = require("zod");
|
|
6770
8503
|
var verifyCommand = define({
|
|
6771
8504
|
name: "verify",
|
|
6772
8505
|
tool: "kb_verify",
|
|
6773
8506
|
usage: "verify <concept-id> --note <text>",
|
|
6774
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.",
|
|
6775
|
-
input:
|
|
8508
|
+
input: import_zod41.z.object({
|
|
6776
8509
|
bundlePath,
|
|
6777
8510
|
conceptId,
|
|
6778
|
-
note:
|
|
8511
|
+
note: import_zod41.z.string().refine((s) => s.trim().length > 0, {
|
|
6779
8512
|
message: "note must say what the check found"
|
|
6780
8513
|
})
|
|
6781
8514
|
}),
|
|
@@ -6795,15 +8528,15 @@ var verifyCommand = define({
|
|
|
6795
8528
|
});
|
|
6796
8529
|
|
|
6797
8530
|
// src/commands/write.ts
|
|
6798
|
-
var
|
|
8531
|
+
var import_zod42 = require("zod");
|
|
6799
8532
|
var writeCommand = define({
|
|
6800
8533
|
name: "write",
|
|
6801
8534
|
tool: "kb_write",
|
|
6802
8535
|
usage: "write <type> < record.json",
|
|
6803
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.",
|
|
6804
|
-
input:
|
|
8537
|
+
input: import_zod42.z.object({
|
|
6805
8538
|
bundlePath,
|
|
6806
|
-
type:
|
|
8539
|
+
type: import_zod42.z.enum(KB_RECORD_TYPES),
|
|
6807
8540
|
input: composeInputSchema
|
|
6808
8541
|
}),
|
|
6809
8542
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -6827,13 +8560,13 @@ var writeCommand = define({
|
|
|
6827
8560
|
});
|
|
6828
8561
|
|
|
6829
8562
|
// src/commands/write-decision.ts
|
|
6830
|
-
var
|
|
8563
|
+
var import_zod43 = require("zod");
|
|
6831
8564
|
var writeDecisionCommand = define({
|
|
6832
8565
|
name: "write-decision",
|
|
6833
8566
|
tool: "kb_write_decision",
|
|
6834
8567
|
usage: "write-decision < decision.json",
|
|
6835
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.",
|
|
6836
|
-
input:
|
|
8569
|
+
input: import_zod43.z.object({ bundlePath, input: decisionInputSchema }),
|
|
6837
8570
|
fromArgv: async (_argv, path, stdin) => ({
|
|
6838
8571
|
bundlePath: path,
|
|
6839
8572
|
input: JSON.parse(await stdin())
|
|
@@ -6864,19 +8597,24 @@ var KB_COMMANDS = [
|
|
|
6864
8597
|
verifyCommand,
|
|
6865
8598
|
anchorResolveCommand,
|
|
6866
8599
|
reassessCommand,
|
|
8600
|
+
promoteCommand,
|
|
6867
8601
|
loadCommand,
|
|
6868
8602
|
catalogCommand,
|
|
6869
8603
|
packCommand,
|
|
8604
|
+
exportCommand,
|
|
6870
8605
|
queryCommand,
|
|
6871
8606
|
traceCommand,
|
|
6872
8607
|
impactCommand,
|
|
6873
8608
|
backlinksCommand,
|
|
8609
|
+
matchCommand,
|
|
8610
|
+
classifyCommand,
|
|
6874
8611
|
listCommand,
|
|
6875
8612
|
readIndexCommand,
|
|
6876
8613
|
logCommand,
|
|
6877
8614
|
stampCommand,
|
|
6878
8615
|
validateCommand,
|
|
6879
8616
|
doctorCommand,
|
|
8617
|
+
sweepCommand,
|
|
6880
8618
|
schemaCommand,
|
|
6881
8619
|
pinCommand,
|
|
6882
8620
|
unpinCommand,
|
|
@@ -6894,7 +8632,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
6894
8632
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6895
8633
|
|
|
6896
8634
|
// src/version.ts
|
|
6897
|
-
var VERSION = true ? "0.1.
|
|
8635
|
+
var VERSION = true ? "0.1.20" : "0.0.0-dev";
|
|
6898
8636
|
|
|
6899
8637
|
// src/mcp.ts
|
|
6900
8638
|
function createKbMcpServer() {
|
|
@@ -6933,7 +8671,7 @@ async function runKbMcpServer() {
|
|
|
6933
8671
|
}
|
|
6934
8672
|
|
|
6935
8673
|
// src/cli.ts
|
|
6936
|
-
var
|
|
8674
|
+
var import_node_path15 = require("path");
|
|
6937
8675
|
async function runKbCli(argv) {
|
|
6938
8676
|
const { flags, literal } = takeLiteral(argv);
|
|
6939
8677
|
const {
|
|
@@ -6995,7 +8733,7 @@ function takeLiteral(argv) {
|
|
|
6995
8733
|
function takeBundle(argv) {
|
|
6996
8734
|
const at2 = argv.indexOf("--bundle");
|
|
6997
8735
|
if (at2 === -1) {
|
|
6998
|
-
return { bundle: (0,
|
|
8736
|
+
return { bundle: (0, import_node_path15.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
6999
8737
|
}
|
|
7000
8738
|
const bundle = argv[at2 + 1];
|
|
7001
8739
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -7006,11 +8744,11 @@ function takeBundle(argv) {
|
|
|
7006
8744
|
};
|
|
7007
8745
|
}
|
|
7008
8746
|
function readStdin() {
|
|
7009
|
-
return new Promise((
|
|
8747
|
+
return new Promise((resolve7, reject) => {
|
|
7010
8748
|
let text = "";
|
|
7011
8749
|
process.stdin.setEncoding("utf8");
|
|
7012
8750
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
7013
|
-
process.stdin.on("end", () =>
|
|
8751
|
+
process.stdin.on("end", () => resolve7(text));
|
|
7014
8752
|
process.stdin.on("error", reject);
|
|
7015
8753
|
});
|
|
7016
8754
|
}
|
|
@@ -7055,12 +8793,14 @@ function usage() {
|
|
|
7055
8793
|
DEFAULT_LOAD_BUDGET,
|
|
7056
8794
|
DEFAULT_PACK_HOPS,
|
|
7057
8795
|
DEFAULT_PACK_MAX_NODES,
|
|
8796
|
+
DEFAULT_THRESHOLDS,
|
|
7058
8797
|
DEFAULT_TYPED_LINK_RELS,
|
|
7059
8798
|
DEFAULT_UNVERIFIED_DAYS,
|
|
7060
8799
|
ErrorTypes,
|
|
7061
8800
|
Fault,
|
|
7062
8801
|
INDEX_FILE,
|
|
7063
8802
|
KB_CAUSAL_LINK_RELS,
|
|
8803
|
+
KB_CLASSES,
|
|
7064
8804
|
KB_COMMANDS,
|
|
7065
8805
|
KB_COMMANDS_BY_NAME,
|
|
7066
8806
|
KB_CONCEPT_ID_PATTERN,
|
|
@@ -7074,10 +8814,15 @@ function usage() {
|
|
|
7074
8814
|
KB_RECORD_TYPES,
|
|
7075
8815
|
KB_SLUG_PATTERN,
|
|
7076
8816
|
KbBaseFrozenError,
|
|
8817
|
+
KbClassifyInputError,
|
|
7077
8818
|
KbInvalidConceptIdError,
|
|
7078
8819
|
KbMissingFlagValueError,
|
|
7079
8820
|
KbPackBudgetExceededError,
|
|
7080
8821
|
KbPinsMalformedError,
|
|
8822
|
+
KbPromoteCollisionError,
|
|
8823
|
+
KbPromoteSelfError,
|
|
8824
|
+
KbPromoteStandingError,
|
|
8825
|
+
KbPromoteStoppedError,
|
|
7081
8826
|
KbRecordAlreadyExistsError,
|
|
7082
8827
|
KbRecordNotFoundError,
|
|
7083
8828
|
KbSelfVerificationError,
|
|
@@ -7090,16 +8835,20 @@ function usage() {
|
|
|
7090
8835
|
PINS_FILE,
|
|
7091
8836
|
PINS_LOCAL_FILE,
|
|
7092
8837
|
PIN_LAYERS,
|
|
8838
|
+
PROMOTION_SOURCE_ID,
|
|
7093
8839
|
RECORD_TYPES,
|
|
7094
8840
|
SEARCH_INDEX_FILE,
|
|
7095
8841
|
TRACE_EDGES,
|
|
7096
8842
|
TreeSitterResolver,
|
|
7097
8843
|
adjudicate,
|
|
7098
8844
|
anchorFilePath,
|
|
8845
|
+
anchorOnHunk,
|
|
7099
8846
|
assertBaseNotFrozen,
|
|
7100
8847
|
backlinks,
|
|
7101
8848
|
buildContext,
|
|
8849
|
+
carry,
|
|
7102
8850
|
catalog,
|
|
8851
|
+
classifyDiff,
|
|
7103
8852
|
classifyDrift,
|
|
7104
8853
|
composeDecisionRecord,
|
|
7105
8854
|
composeInputSchema,
|
|
@@ -7125,8 +8874,11 @@ function usage() {
|
|
|
7125
8874
|
isKbLinkRel,
|
|
7126
8875
|
isKbRecordType,
|
|
7127
8876
|
isNoDecisionRecord,
|
|
8877
|
+
isReviewTag,
|
|
7128
8878
|
kbActorStampSchema,
|
|
7129
8879
|
kbAnchorSchema,
|
|
8880
|
+
kbAnchorSpanSchema,
|
|
8881
|
+
kbAnchorWriteSchema,
|
|
7130
8882
|
kbConceptIdSchema,
|
|
7131
8883
|
kbJsonSchemas,
|
|
7132
8884
|
kbLinkSchema,
|
|
@@ -7146,6 +8898,8 @@ function usage() {
|
|
|
7146
8898
|
parseMarkdownWithFrontmatter,
|
|
7147
8899
|
pinBase,
|
|
7148
8900
|
prepareResolvers,
|
|
8901
|
+
promoteCandidates,
|
|
8902
|
+
promoteInputSchema,
|
|
7149
8903
|
readMergedPins,
|
|
7150
8904
|
readPinsLayer,
|
|
7151
8905
|
readRemoteAnchors,
|
|
@@ -7167,6 +8921,7 @@ function usage() {
|
|
|
7167
8921
|
selectDecisions,
|
|
7168
8922
|
splitMarkdownFrontmatter,
|
|
7169
8923
|
stringifyMarkdownWithFrontmatter,
|
|
8924
|
+
symbolRangeIndex,
|
|
7170
8925
|
syncInstructions,
|
|
7171
8926
|
toHookJson,
|
|
7172
8927
|
trace,
|