@saasontools/strauss-kb 0.1.19 → 0.1.21
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 +16 -11
- package/dist/{chunk-CQBLH7CE.js → chunk-VXWN62HT.js} +2 -2
- package/dist/{chunk-ZWSCLHG6.js → chunk-WVJULVGY.js} +2 -2
- package/dist/{chunk-MNQNHYWL.js → chunk-XBZ7G5R5.js} +3005 -1152
- package/dist/chunk-XBZ7G5R5.js.map +1 -0
- package/dist/cli-main.cjs +2798 -982
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +2205 -421
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +472 -96
- package/dist/index.d.ts +472 -96
- package/dist/index.js +38 -83
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +3024 -1208
- 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-CQBLH7CE.js.map → chunk-VXWN62HT.js.map} +0 -0
- /package/dist/{chunk-ZWSCLHG6.js.map → chunk-WVJULVGY.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,17 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
371
433
|
})(Fault || {});
|
|
372
434
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
373
435
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
436
|
+
ErrorTypes2["KbClassifyInput"] = "KbClassifyInput";
|
|
437
|
+
ErrorTypes2["KbFlagConflict"] = "KbFlagConflict";
|
|
438
|
+
ErrorTypes2["KbInvalidActor"] = "KbInvalidActor";
|
|
374
439
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
440
|
+
ErrorTypes2["KbMatchInput"] = "KbMatchInput";
|
|
375
441
|
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
376
442
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
443
|
+
ErrorTypes2["KbPromoteCollision"] = "KbPromoteCollision";
|
|
444
|
+
ErrorTypes2["KbPromoteSelf"] = "KbPromoteSelf";
|
|
445
|
+
ErrorTypes2["KbPromoteStanding"] = "KbPromoteStanding";
|
|
446
|
+
ErrorTypes2["KbPromoteStopped"] = "KbPromoteStopped";
|
|
377
447
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
378
448
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
379
449
|
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
@@ -466,6 +536,38 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
466
536
|
actor;
|
|
467
537
|
generatedBy;
|
|
468
538
|
};
|
|
539
|
+
var KbInvalidActorError = class extends BaseError {
|
|
540
|
+
constructor(actor, reason) {
|
|
541
|
+
super({
|
|
542
|
+
message: `kb: actor ${JSON.stringify(actor)} ${reason} \u2014 set STRAUSS_KB_ACTOR, e.g. human:alice or agent:reviewer`,
|
|
543
|
+
errorType: "KbInvalidActor" /* KbInvalidActor */,
|
|
544
|
+
code: 400,
|
|
545
|
+
fault: "User" /* User */,
|
|
546
|
+
retriable: false,
|
|
547
|
+
reportToUser: true,
|
|
548
|
+
details: { actor, reason, action: "refused" }
|
|
549
|
+
});
|
|
550
|
+
this.actor = actor;
|
|
551
|
+
this.reason = reason;
|
|
552
|
+
}
|
|
553
|
+
actor;
|
|
554
|
+
reason;
|
|
555
|
+
};
|
|
556
|
+
var KbFlagConflictError = class extends BaseError {
|
|
557
|
+
constructor(flags) {
|
|
558
|
+
super({
|
|
559
|
+
message: `kb: ${flags.join(" and ")} cannot be combined`,
|
|
560
|
+
errorType: "KbFlagConflict" /* KbFlagConflict */,
|
|
561
|
+
code: 400,
|
|
562
|
+
fault: "User" /* User */,
|
|
563
|
+
retriable: false,
|
|
564
|
+
reportToUser: true,
|
|
565
|
+
details: { flags }
|
|
566
|
+
});
|
|
567
|
+
this.flags = flags;
|
|
568
|
+
}
|
|
569
|
+
flags;
|
|
570
|
+
};
|
|
469
571
|
var KbPackBudgetExceededError = class extends BaseError {
|
|
470
572
|
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
471
573
|
super({
|
|
@@ -519,6 +621,89 @@ var KbMissingFlagValueError = class extends BaseError {
|
|
|
519
621
|
}
|
|
520
622
|
flag;
|
|
521
623
|
};
|
|
624
|
+
var KbClassifyInputError = class extends BaseError {
|
|
625
|
+
constructor(reason) {
|
|
626
|
+
super({
|
|
627
|
+
message: `classify: ${reason}`,
|
|
628
|
+
errorType: "KbClassifyInput" /* KbClassifyInput */,
|
|
629
|
+
code: 400,
|
|
630
|
+
fault: "User" /* User */,
|
|
631
|
+
retriable: false,
|
|
632
|
+
reportToUser: true,
|
|
633
|
+
details: { reason }
|
|
634
|
+
});
|
|
635
|
+
this.reason = reason;
|
|
636
|
+
}
|
|
637
|
+
reason;
|
|
638
|
+
};
|
|
639
|
+
var KbPromoteCollisionError = class extends BaseError {
|
|
640
|
+
constructor(conceptId2, to) {
|
|
641
|
+
super({
|
|
642
|
+
message: `kb: ${to} already holds ${conceptId2} \u2014 re-run with force to overwrite it`,
|
|
643
|
+
errorType: "KbPromoteCollision" /* KbPromoteCollision */,
|
|
644
|
+
code: 409,
|
|
645
|
+
fault: "User" /* User */,
|
|
646
|
+
retriable: false,
|
|
647
|
+
reportToUser: true,
|
|
648
|
+
details: { conceptId: conceptId2, to, action: "refused" }
|
|
649
|
+
});
|
|
650
|
+
this.conceptId = conceptId2;
|
|
651
|
+
this.to = to;
|
|
652
|
+
}
|
|
653
|
+
conceptId;
|
|
654
|
+
to;
|
|
655
|
+
};
|
|
656
|
+
var KbPromoteStandingError = class extends BaseError {
|
|
657
|
+
constructor(conceptId2, standing) {
|
|
658
|
+
super({
|
|
659
|
+
message: `kb: ${conceptId2} is ${standing} \u2014 only a record that still stands can be promoted`,
|
|
660
|
+
errorType: "KbPromoteStanding" /* KbPromoteStanding */,
|
|
661
|
+
code: 409,
|
|
662
|
+
fault: "User" /* User */,
|
|
663
|
+
retriable: false,
|
|
664
|
+
reportToUser: true,
|
|
665
|
+
details: { conceptId: conceptId2, standing, action: "refused" }
|
|
666
|
+
});
|
|
667
|
+
this.conceptId = conceptId2;
|
|
668
|
+
this.standing = standing;
|
|
669
|
+
}
|
|
670
|
+
conceptId;
|
|
671
|
+
standing;
|
|
672
|
+
};
|
|
673
|
+
var KbPromoteSelfError = class extends BaseError {
|
|
674
|
+
constructor(to) {
|
|
675
|
+
super({
|
|
676
|
+
message: `kb: ${to} is the base being promoted from \u2014 name a different target`,
|
|
677
|
+
errorType: "KbPromoteSelf" /* KbPromoteSelf */,
|
|
678
|
+
code: 400,
|
|
679
|
+
fault: "User" /* User */,
|
|
680
|
+
retriable: false,
|
|
681
|
+
reportToUser: true,
|
|
682
|
+
details: { to, action: "refused" }
|
|
683
|
+
});
|
|
684
|
+
this.to = to;
|
|
685
|
+
}
|
|
686
|
+
to;
|
|
687
|
+
};
|
|
688
|
+
var KbPromoteStoppedError = class extends BaseError {
|
|
689
|
+
constructor(conceptId2, landed, reason) {
|
|
690
|
+
super({
|
|
691
|
+
message: `kb: promotion stopped at ${conceptId2} (${reason}) \u2014 landed: ${landed.length ? landed.join(", ") : "nothing"}`,
|
|
692
|
+
errorType: "KbPromoteStopped" /* KbPromoteStopped */,
|
|
693
|
+
code: 500,
|
|
694
|
+
fault: "System" /* System */,
|
|
695
|
+
retriable: false,
|
|
696
|
+
reportToUser: true,
|
|
697
|
+
details: { conceptId: conceptId2, landed, reason, action: "stopped" }
|
|
698
|
+
});
|
|
699
|
+
this.conceptId = conceptId2;
|
|
700
|
+
this.landed = landed;
|
|
701
|
+
this.reason = reason;
|
|
702
|
+
}
|
|
703
|
+
conceptId;
|
|
704
|
+
landed;
|
|
705
|
+
reason;
|
|
706
|
+
};
|
|
522
707
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
523
708
|
constructor(message, details) {
|
|
524
709
|
super({
|
|
@@ -698,7 +883,9 @@ function revRef(rev) {
|
|
|
698
883
|
var UNCHECKED_REASONS = [
|
|
699
884
|
"remote-unreachable",
|
|
700
885
|
"repo-unauthorized",
|
|
701
|
-
"default-branch-unknown"
|
|
886
|
+
"default-branch-unknown",
|
|
887
|
+
/** Local, but the same finding: a shallow clone has no rev to read. */
|
|
888
|
+
"ref-unavailable"
|
|
702
889
|
];
|
|
703
890
|
function isUncheckedReason(reason) {
|
|
704
891
|
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
@@ -766,6 +953,11 @@ function refShapeIsSafe(ref) {
|
|
|
766
953
|
if (ref.includes("..")) return false;
|
|
767
954
|
return REF_SHAPE.test(ref);
|
|
768
955
|
}
|
|
956
|
+
function localRevShapeIsSafe(rev) {
|
|
957
|
+
if (!rev || rev.length > MAX_REF_LENGTH) return false;
|
|
958
|
+
if (rev.includes("..")) return false;
|
|
959
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/^~-]*$/.test(rev);
|
|
960
|
+
}
|
|
769
961
|
async function refIsWellFormed(ref) {
|
|
770
962
|
if (!refShapeIsSafe(ref)) return false;
|
|
771
963
|
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
@@ -1164,6 +1356,142 @@ function bundleDigest(records, superseded) {
|
|
|
1164
1356
|
return bundleStamp(records, superseded).digest;
|
|
1165
1357
|
}
|
|
1166
1358
|
|
|
1359
|
+
// src/drift/git.ts
|
|
1360
|
+
var import_node_child_process3 = require("child_process");
|
|
1361
|
+
var import_node_util3 = require("util");
|
|
1362
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
1363
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
1364
|
+
var MAX_RANGE_DIFF_BYTES = 8 * 1048576;
|
|
1365
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
1366
|
+
var RANGE_DIFF_TIMEOUT_MS = 2e4;
|
|
1367
|
+
async function git2(cwd, args, limits = {}) {
|
|
1368
|
+
const env = { ...process.env };
|
|
1369
|
+
delete env["GIT_DIR"];
|
|
1370
|
+
delete env["GIT_WORK_TREE"];
|
|
1371
|
+
delete env["GIT_INDEX_FILE"];
|
|
1372
|
+
try {
|
|
1373
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
1374
|
+
timeout: limits.timeoutMs ?? GIT_TIMEOUT_MS,
|
|
1375
|
+
maxBuffer: limits.maxBytes ?? MAX_GIT_OUTPUT_BYTES,
|
|
1376
|
+
env
|
|
1377
|
+
});
|
|
1378
|
+
return { ok: true, stdout };
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
return { ok: false, reason: failureOf(error) };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function failureOf(error) {
|
|
1384
|
+
const { code, killed } = error;
|
|
1385
|
+
if (code === "ENOENT") return "git-missing";
|
|
1386
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "too-large";
|
|
1387
|
+
if (killed === true) return "timeout";
|
|
1388
|
+
return "failed";
|
|
1389
|
+
}
|
|
1390
|
+
async function readFileAtRef(repoRoot, anchor) {
|
|
1391
|
+
if (!filePathIsSafe(anchor.file))
|
|
1392
|
+
return { ok: false, reason: "outside-repo" };
|
|
1393
|
+
if (!anchor.ref || !refShapeIsSafe(anchor.ref)) {
|
|
1394
|
+
return { ok: false, reason: "ref-unreadable" };
|
|
1395
|
+
}
|
|
1396
|
+
const blob = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
1397
|
+
if (blob !== null) return { ok: true, source: blob };
|
|
1398
|
+
return {
|
|
1399
|
+
ok: false,
|
|
1400
|
+
reason: await hasCommit(repoRoot, anchor.ref) ? "ref-unreadable" : "ref-unavailable"
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
async function hasCommit(repoRoot, ref) {
|
|
1404
|
+
const found = await git2(repoRoot, [
|
|
1405
|
+
"cat-file",
|
|
1406
|
+
"-e",
|
|
1407
|
+
"--end-of-options",
|
|
1408
|
+
`${ref}^{commit}`
|
|
1409
|
+
]);
|
|
1410
|
+
return found.ok;
|
|
1411
|
+
}
|
|
1412
|
+
async function listRepoFiles(repoRoot) {
|
|
1413
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
1414
|
+
if (!result.ok) return [];
|
|
1415
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
1416
|
+
}
|
|
1417
|
+
var DIFF_RANGE = /^(.+?)(\.{2,3})(.+)$/;
|
|
1418
|
+
async function readRangeDiff(repoRoot, range, maxBytes = MAX_RANGE_DIFF_BYTES) {
|
|
1419
|
+
const parts = DIFF_RANGE.exec(range);
|
|
1420
|
+
if (!parts) return { ok: false, reason: "bad-range" };
|
|
1421
|
+
const [, base2 = "", dots = "", head = ""] = parts;
|
|
1422
|
+
if (!localRevShapeIsSafe(base2) || !localRevShapeIsSafe(head)) {
|
|
1423
|
+
return { ok: false, reason: "bad-range" };
|
|
1424
|
+
}
|
|
1425
|
+
const result = await git2(
|
|
1426
|
+
repoRoot,
|
|
1427
|
+
[
|
|
1428
|
+
"-c",
|
|
1429
|
+
"core.quotePath=false",
|
|
1430
|
+
"diff",
|
|
1431
|
+
"--unified=0",
|
|
1432
|
+
"--no-color",
|
|
1433
|
+
"--no-ext-diff",
|
|
1434
|
+
"--no-textconv",
|
|
1435
|
+
"--find-renames",
|
|
1436
|
+
"--src-prefix=a/",
|
|
1437
|
+
"--dst-prefix=b/",
|
|
1438
|
+
"--end-of-options",
|
|
1439
|
+
`${base2}${dots}${head}`,
|
|
1440
|
+
"--"
|
|
1441
|
+
],
|
|
1442
|
+
{ maxBytes, timeoutMs: RANGE_DIFF_TIMEOUT_MS }
|
|
1443
|
+
);
|
|
1444
|
+
if (result.ok) return { ok: true, text: result.stdout };
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
reason: result.reason === "failed" ? "bad-range" : result.reason
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
async function readOldSource(repoRoot, anchor) {
|
|
1451
|
+
if (!filePathIsSafe(anchor.file))
|
|
1452
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1453
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
1454
|
+
const shown2 = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
1455
|
+
if (shown2 !== null) {
|
|
1456
|
+
return {
|
|
1457
|
+
ok: true,
|
|
1458
|
+
source: shown2,
|
|
1459
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const at2 = anchor.resolved_at;
|
|
1464
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
1465
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1466
|
+
}
|
|
1467
|
+
const found = await git2(repoRoot, [
|
|
1468
|
+
"log",
|
|
1469
|
+
"-1",
|
|
1470
|
+
"--format=%H",
|
|
1471
|
+
`--before=${at2}`,
|
|
1472
|
+
"--end-of-options",
|
|
1473
|
+
"HEAD",
|
|
1474
|
+
"--",
|
|
1475
|
+
anchor.file
|
|
1476
|
+
]);
|
|
1477
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
1478
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
1479
|
+
return { ok: false, reason: "unrecoverable" };
|
|
1480
|
+
const shown = await catBlob(repoRoot, sha, anchor.file);
|
|
1481
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
1482
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
1483
|
+
}
|
|
1484
|
+
async function catBlob(repoRoot, ref, file) {
|
|
1485
|
+
const path = file.replace(/^\.\//, "");
|
|
1486
|
+
const result = await git2(repoRoot, [
|
|
1487
|
+
"cat-file",
|
|
1488
|
+
"blob",
|
|
1489
|
+
"--end-of-options",
|
|
1490
|
+
`${ref}:${path}`
|
|
1491
|
+
]);
|
|
1492
|
+
return result.ok ? result.stdout : null;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1167
1495
|
// src/anchor-resolver/read.ts
|
|
1168
1496
|
var import_promises2 = require("fs/promises");
|
|
1169
1497
|
var import_node_path2 = require("path");
|
|
@@ -1229,6 +1557,7 @@ function looksLikeWrongRepoRoot(drift) {
|
|
|
1229
1557
|
for (const entries of drift.values()) {
|
|
1230
1558
|
for (const entry of entries) {
|
|
1231
1559
|
if (entry.repo !== void 0) continue;
|
|
1560
|
+
if (entry.side === "old") continue;
|
|
1232
1561
|
checked += 1;
|
|
1233
1562
|
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
1234
1563
|
return false;
|
|
@@ -1364,7 +1693,7 @@ function size(bytes) {
|
|
|
1364
1693
|
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1365
1694
|
}
|
|
1366
1695
|
function pause(ms) {
|
|
1367
|
-
return new Promise((
|
|
1696
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1368
1697
|
}
|
|
1369
1698
|
|
|
1370
1699
|
// src/grammars/manifest.ts
|
|
@@ -1936,8 +2265,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1936
2265
|
}
|
|
1937
2266
|
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1938
2267
|
function captureIndentedBlock(lines, matchLine) {
|
|
1939
|
-
const
|
|
1940
|
-
const indent =
|
|
2268
|
+
const header2 = lines[matchLine] ?? "";
|
|
2269
|
+
const indent = header2.length - header2.trimStart().length;
|
|
1941
2270
|
let headerEnd = -1;
|
|
1942
2271
|
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1943
2272
|
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
@@ -1958,42 +2287,55 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1958
2287
|
}
|
|
1959
2288
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1960
2289
|
}
|
|
1961
|
-
var
|
|
1962
|
-
(name
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2290
|
+
var declarationTier = (name) => new RegExp(
|
|
2291
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
2292
|
+
);
|
|
2293
|
+
var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
|
|
2294
|
+
var anchoredAssignmentTier = (name) => new RegExp(
|
|
2295
|
+
`^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
|
|
2296
|
+
);
|
|
2297
|
+
var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
|
|
2298
|
+
var MENTION_TIERS = [
|
|
1966
2299
|
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1967
2300
|
(name) => new RegExp(`\\b${name}\\b`)
|
|
1968
2301
|
];
|
|
2302
|
+
var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
|
|
2303
|
+
function resolveWith(tiers, source, symbol) {
|
|
2304
|
+
const segments = symbol.split(".");
|
|
2305
|
+
const name = segments[segments.length - 1];
|
|
2306
|
+
if (!name) return null;
|
|
2307
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
2308
|
+
const escaped = escapeRegExp(name);
|
|
2309
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
2310
|
+
const lines = source.split("\n");
|
|
2311
|
+
for (const tier of tiers) {
|
|
2312
|
+
const pattern = tier(escaped);
|
|
2313
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
2314
|
+
if (!candidates.length) continue;
|
|
2315
|
+
if (parentPattern && candidates.length > 1) {
|
|
2316
|
+
const distances = candidates.map(
|
|
2317
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
2318
|
+
);
|
|
2319
|
+
const nearest = Math.min(...distances);
|
|
2320
|
+
if (Number.isFinite(nearest)) {
|
|
2321
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (candidates.length !== 1) return null;
|
|
2325
|
+
const matchLine = candidates[0];
|
|
2326
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
1969
2330
|
var regexResolver = {
|
|
1970
2331
|
name: "regex",
|
|
1971
2332
|
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;
|
|
2333
|
+
return resolveWith(TIERS, source, symbol);
|
|
2334
|
+
},
|
|
2335
|
+
attempt(source, symbol, _file, options) {
|
|
2336
|
+
const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
|
|
2337
|
+
const span2 = resolveWith(tiers, source, symbol);
|
|
2338
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1997
2339
|
}
|
|
1998
2340
|
};
|
|
1999
2341
|
function escapeRegExp(value) {
|
|
@@ -2015,6 +2357,7 @@ function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
|
2015
2357
|
}
|
|
2016
2358
|
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
2017
2359
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
2360
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
2018
2361
|
if (!anchor.symbol) {
|
|
2019
2362
|
const lines = normalized.split("\n");
|
|
2020
2363
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
@@ -2027,11 +2370,17 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2027
2370
|
}
|
|
2028
2371
|
};
|
|
2029
2372
|
}
|
|
2373
|
+
let afterParsedMiss = false;
|
|
2030
2374
|
for (const resolver of resolvers) {
|
|
2031
|
-
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file
|
|
2375
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
|
|
2376
|
+
afterParsedMiss
|
|
2377
|
+
}) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
2032
2378
|
if (attempt.kind === "abstain") continue;
|
|
2033
2379
|
if (attempt.kind === "unresolved") {
|
|
2034
|
-
if (attempt.reason === "symbol-not-found")
|
|
2380
|
+
if (attempt.reason === "symbol-not-found") {
|
|
2381
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2035
2384
|
return { ok: false, reason: attempt.reason };
|
|
2036
2385
|
}
|
|
2037
2386
|
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
@@ -2044,12 +2393,28 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
2044
2393
|
}
|
|
2045
2394
|
return { ok: false, reason: "symbol-not-found" };
|
|
2046
2395
|
}
|
|
2396
|
+
function sliceSpan(source, range) {
|
|
2397
|
+
const lines = source.split("\n");
|
|
2398
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
2399
|
+
if (range.end > lines.length) {
|
|
2400
|
+
return { ok: false, reason: "span-out-of-range" };
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
ok: true,
|
|
2404
|
+
span: {
|
|
2405
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
2406
|
+
startLine: range.start,
|
|
2407
|
+
endLine: range.end
|
|
2408
|
+
},
|
|
2409
|
+
resolver: "span"
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2047
2412
|
function fromResolve(resolver, source, symbol, file) {
|
|
2048
2413
|
const span2 = resolver.resolve(source, symbol, file);
|
|
2049
2414
|
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2050
2415
|
}
|
|
2051
2416
|
function isResolverName(name) {
|
|
2052
|
-
return name === "tree-sitter" || name === "regex";
|
|
2417
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2053
2418
|
}
|
|
2054
2419
|
async function prepareResolvers(resolvers, files) {
|
|
2055
2420
|
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
@@ -2068,6 +2433,9 @@ function resolverChanged(source, anchor, produced) {
|
|
|
2068
2433
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
2069
2434
|
}
|
|
2070
2435
|
function anchorHashOf(anchor, outcome) {
|
|
2436
|
+
if (outcome.resolver === "span") {
|
|
2437
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2438
|
+
}
|
|
2071
2439
|
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2072
2440
|
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2073
2441
|
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
@@ -2101,37 +2469,60 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
2101
2469
|
}
|
|
2102
2470
|
}
|
|
2103
2471
|
const files = [];
|
|
2472
|
+
const committedWants = [];
|
|
2104
2473
|
const wants = [];
|
|
2105
2474
|
for (const entries of planned.values()) {
|
|
2106
2475
|
for (const { anchor, foreign } of entries) {
|
|
2107
|
-
if (
|
|
2108
|
-
else
|
|
2476
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
2477
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2478
|
+
else files.push(anchor.file);
|
|
2109
2479
|
}
|
|
2110
2480
|
}
|
|
2111
|
-
const [reads, remote] = await Promise.all([
|
|
2481
|
+
const [reads, committed, remote] = await Promise.all([
|
|
2112
2482
|
readAnchorFiles(
|
|
2113
2483
|
files,
|
|
2114
2484
|
options.reader ?? anchorFileReader(repoRoot),
|
|
2115
2485
|
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
2116
2486
|
),
|
|
2487
|
+
readCommitted(repoRoot, committedWants, options),
|
|
2117
2488
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
2118
2489
|
]);
|
|
2119
2490
|
await prepareResolvers(resolvers, [
|
|
2120
2491
|
...files,
|
|
2492
|
+
...committedWants.map((anchor) => anchor.file),
|
|
2121
2493
|
...wants.map((want) => want.file)
|
|
2122
2494
|
]);
|
|
2123
2495
|
const drift = /* @__PURE__ */ new Map();
|
|
2124
2496
|
for (const record of records) {
|
|
2125
2497
|
const entries = [];
|
|
2126
2498
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2499
|
+
if (foreign) {
|
|
2500
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2504
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
2130
2505
|
}
|
|
2131
2506
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
2132
2507
|
}
|
|
2133
2508
|
return drift;
|
|
2134
2509
|
}
|
|
2510
|
+
function atRefKey(anchor) {
|
|
2511
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
2512
|
+
}
|
|
2513
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
2514
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
2515
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
2516
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2517
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
2518
|
+
const keys = [...byKey.keys()];
|
|
2519
|
+
const results = await mapLimit(
|
|
2520
|
+
keys,
|
|
2521
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2522
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
2523
|
+
);
|
|
2524
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2525
|
+
}
|
|
2135
2526
|
function remoteWants(anchor) {
|
|
2136
2527
|
const repo = anchor.repo;
|
|
2137
2528
|
const wants = [{ repo, file: anchor.file }];
|
|
@@ -2142,6 +2533,7 @@ function base(anchor) {
|
|
|
2142
2533
|
return {
|
|
2143
2534
|
file: anchor.file,
|
|
2144
2535
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2536
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
2145
2537
|
storedHash: anchor.hash
|
|
2146
2538
|
};
|
|
2147
2539
|
}
|
|
@@ -2155,9 +2547,15 @@ function unresolved(anchor, reason, repo) {
|
|
|
2155
2547
|
...classOf(reason)
|
|
2156
2548
|
};
|
|
2157
2549
|
}
|
|
2550
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2551
|
+
"file-missing",
|
|
2552
|
+
"symbol-not-found",
|
|
2553
|
+
"span-out-of-range",
|
|
2554
|
+
"ref-unreadable"
|
|
2555
|
+
]);
|
|
2158
2556
|
function provisionalDriftClass(entry) {
|
|
2159
2557
|
if (entry.state === "unresolved") {
|
|
2160
|
-
return
|
|
2558
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
2161
2559
|
}
|
|
2162
2560
|
return entry.state === "drifted" ? "changed" : void 0;
|
|
2163
2561
|
}
|
|
@@ -2226,6 +2624,13 @@ function remoteEntry(anchor, remote, resolvers) {
|
|
|
2226
2624
|
remoteState: "drifted-from-ref"
|
|
2227
2625
|
});
|
|
2228
2626
|
}
|
|
2627
|
+
if (anchor.side === "old") {
|
|
2628
|
+
return compared(anchor, current, {
|
|
2629
|
+
repo,
|
|
2630
|
+
...extras,
|
|
2631
|
+
remoteState: "matches-ref"
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2229
2634
|
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2230
2635
|
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2231
2636
|
...compared(anchor, head.current, {
|
|
@@ -2261,19 +2666,28 @@ var kbLogEntrySchema = import_zod3.z.object({
|
|
|
2261
2666
|
by: import_zod3.z.string().min(1),
|
|
2262
2667
|
operation: import_zod3.z.string().min(1),
|
|
2263
2668
|
conceptId: import_zod3.z.string().min(1),
|
|
2264
|
-
/**
|
|
2669
|
+
/**
|
|
2670
|
+
* The operation's other end, where it has one: a second concept id for
|
|
2671
|
+
* supersession, the other base's path for promotion.
|
|
2672
|
+
*/
|
|
2265
2673
|
target: import_zod3.z.string().min(1).optional()
|
|
2266
2674
|
}).strict();
|
|
2267
2675
|
function renderLogEntry(entry) {
|
|
2268
2676
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
2269
2677
|
`;
|
|
2270
2678
|
}
|
|
2679
|
+
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
2271
2680
|
function parseLog(raw) {
|
|
2272
2681
|
const entries = [];
|
|
2273
2682
|
const malformed = [];
|
|
2274
2683
|
const seen = /* @__PURE__ */ new Set();
|
|
2684
|
+
let conflicted = false;
|
|
2275
2685
|
raw.split("\n").forEach((text, index2) => {
|
|
2276
2686
|
if (!text.trim()) return;
|
|
2687
|
+
if (CONFLICT_MARKER.test(text)) {
|
|
2688
|
+
conflicted = true;
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2277
2691
|
let value;
|
|
2278
2692
|
try {
|
|
2279
2693
|
value = JSON.parse(text);
|
|
@@ -2294,7 +2708,7 @@ function parseLog(raw) {
|
|
|
2294
2708
|
entries.sort(
|
|
2295
2709
|
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
|
|
2296
2710
|
);
|
|
2297
|
-
return { entries, malformed };
|
|
2711
|
+
return { entries, malformed, conflicted };
|
|
2298
2712
|
}
|
|
2299
2713
|
|
|
2300
2714
|
// src/search-index.ts
|
|
@@ -2343,8 +2757,8 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2343
2757
|
async function isStale(bundlePath2) {
|
|
2344
2758
|
const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2345
2759
|
if (!indexAt) return true;
|
|
2346
|
-
const { readdir:
|
|
2347
|
-
const names = (await
|
|
2760
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
2761
|
+
const names = (await readdir3(bundlePath2).catch(() => [])).filter(
|
|
2348
2762
|
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
2349
2763
|
);
|
|
2350
2764
|
let stale = false;
|
|
@@ -2759,14 +3173,14 @@ function catalog(bundle, options = {}) {
|
|
|
2759
3173
|
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2760
3174
|
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
2761
3175
|
})).sort(byTypeThenTitle);
|
|
2762
|
-
const
|
|
2763
|
-
for (const entry of entries)
|
|
3176
|
+
const standings2 = { ...EMPTY_STANDINGS };
|
|
3177
|
+
for (const entry of entries) standings2[entry.standing] += 1;
|
|
2764
3178
|
return {
|
|
2765
3179
|
entries,
|
|
2766
3180
|
recordCount: entries.length,
|
|
2767
|
-
standings,
|
|
2768
|
-
currentCount:
|
|
2769
|
-
supersededCount:
|
|
3181
|
+
standings: standings2,
|
|
3182
|
+
currentCount: standings2.current,
|
|
3183
|
+
supersededCount: standings2.superseded,
|
|
2770
3184
|
staleCount: entries.filter((entry) => entry.stale).length
|
|
2771
3185
|
};
|
|
2772
3186
|
}
|
|
@@ -2922,9 +3336,13 @@ function hasEdge(edges, edge) {
|
|
|
2922
3336
|
);
|
|
2923
3337
|
}
|
|
2924
3338
|
|
|
3339
|
+
// src/kb-files.ts
|
|
3340
|
+
var STORE_OWNED_FILES = [INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE];
|
|
3341
|
+
|
|
2925
3342
|
// src/kb-gitattributes.ts
|
|
2926
3343
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
2927
|
-
var
|
|
3344
|
+
var GENERATED = "linguist-generated";
|
|
3345
|
+
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union ${GENERATED}=true`;
|
|
2928
3346
|
function parseLine(line) {
|
|
2929
3347
|
const trimmed = line.trim();
|
|
2930
3348
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -2932,23 +3350,39 @@ function parseLine(line) {
|
|
|
2932
3350
|
return pattern === void 0 ? null : { pattern, attrs };
|
|
2933
3351
|
}
|
|
2934
3352
|
function hasMergeDeclaration(contents) {
|
|
3353
|
+
return declares(contents, LOG_FILE, "merge");
|
|
3354
|
+
}
|
|
3355
|
+
function declares(contents, pattern, attribute) {
|
|
2935
3356
|
return contents.split("\n").some((line) => {
|
|
2936
3357
|
const parsed = parseLine(line);
|
|
2937
|
-
if (!parsed || parsed.pattern !==
|
|
3358
|
+
if (!parsed || parsed.pattern !== pattern) return false;
|
|
2938
3359
|
return parsed.attrs.some(
|
|
2939
|
-
(attr) => attr ===
|
|
3360
|
+
(attr) => attr === attribute || attr === `-${attribute}` || attr.startsWith(`${attribute}=`)
|
|
2940
3361
|
);
|
|
2941
3362
|
});
|
|
2942
3363
|
}
|
|
2943
|
-
function
|
|
3364
|
+
function missingGitattributesLines(contents) {
|
|
3365
|
+
const needsMerge = !hasMergeDeclaration(contents);
|
|
3366
|
+
const generated = STORE_OWNED_FILES.filter(
|
|
3367
|
+
// The union-merge line carries the log's `linguist-generated` too, so the
|
|
3368
|
+
// log needs its own line only where that line is already there without it.
|
|
3369
|
+
(file) => !(needsMerge && file === LOG_FILE)
|
|
3370
|
+
).filter((file) => !declares(contents, file, GENERATED)).map((file) => `${file} ${GENERATED}=true`);
|
|
3371
|
+
return needsMerge ? [UNION_MERGE_LINE, ...generated] : generated;
|
|
3372
|
+
}
|
|
3373
|
+
var GITATTRIBUTES_BLOCK = `${missingGitattributesLines("").join("\n")}
|
|
3374
|
+
`;
|
|
3375
|
+
function appendGitattributesLines(contents) {
|
|
3376
|
+
const lines = missingGitattributesLines(contents);
|
|
3377
|
+
if (lines.length === 0) return "";
|
|
2944
3378
|
const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
2945
|
-
return `${separator}${
|
|
3379
|
+
return `${separator}${lines.join("\n")}
|
|
2946
3380
|
`;
|
|
2947
3381
|
}
|
|
2948
3382
|
|
|
2949
3383
|
// src/kb-store.ts
|
|
2950
3384
|
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2951
|
-
var STORE_OWNED =
|
|
3385
|
+
var STORE_OWNED = new Set(STORE_OWNED_FILES);
|
|
2952
3386
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2953
3387
|
var KbStore = class {
|
|
2954
3388
|
constructor(logger = {}) {
|
|
@@ -2961,6 +3395,7 @@ var KbStore = class {
|
|
|
2961
3395
|
* its contents.
|
|
2962
3396
|
*/
|
|
2963
3397
|
async write(bundlePath2, input, actor = "unknown") {
|
|
3398
|
+
assertActor(actor);
|
|
2964
3399
|
if (!KB_SLUG_PATTERN.test(input.slug)) {
|
|
2965
3400
|
throw new KbInvalidConceptIdError("slug must be kebab-case", {
|
|
2966
3401
|
slug: input.slug
|
|
@@ -3062,6 +3497,7 @@ var KbStore = class {
|
|
|
3062
3497
|
* timeouts.
|
|
3063
3498
|
*/
|
|
3064
3499
|
async setStatus(bundlePath2, conceptId2, status, actor = "unknown") {
|
|
3500
|
+
assertActor(actor);
|
|
3065
3501
|
return this.mutate(
|
|
3066
3502
|
bundlePath2,
|
|
3067
3503
|
conceptId2,
|
|
@@ -3075,12 +3511,17 @@ var KbStore = class {
|
|
|
3075
3511
|
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3076
3512
|
* writing, so it holds the complete current set, and a merge would keep
|
|
3077
3513
|
* stale entries the resolution pass deliberately dropped.
|
|
3514
|
+
*
|
|
3515
|
+
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
3516
|
+
* the frontmatter must not be published back out under an actor stamp.
|
|
3078
3517
|
*/
|
|
3079
3518
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3519
|
+
assertActor(actor);
|
|
3520
|
+
const checked = anchors.map((anchor) => kbAnchorWriteSchema.parse(anchor));
|
|
3080
3521
|
return this.mutate(
|
|
3081
3522
|
bundlePath2,
|
|
3082
3523
|
conceptId2,
|
|
3083
|
-
(frontmatter) => ({ ...frontmatter, strauss_anchors:
|
|
3524
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: checked }),
|
|
3084
3525
|
{ operation: "anchor-resolve", by: actor }
|
|
3085
3526
|
);
|
|
3086
3527
|
}
|
|
@@ -3096,6 +3537,7 @@ var KbStore = class {
|
|
|
3096
3537
|
* logs what it publishes.
|
|
3097
3538
|
*/
|
|
3098
3539
|
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3540
|
+
assertActor(actor, { named: true });
|
|
3099
3541
|
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
3100
3542
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
3101
3543
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
@@ -3126,6 +3568,7 @@ var KbStore = class {
|
|
|
3126
3568
|
* in normal use, and validation drops to catching hand-edits.
|
|
3127
3569
|
*/
|
|
3128
3570
|
async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
|
|
3571
|
+
assertActor(actor);
|
|
3129
3572
|
const replacement = await this.read(bundlePath2, replacementId);
|
|
3130
3573
|
if (!replacement) throw new KbRecordNotFoundError(replacementId);
|
|
3131
3574
|
const superseded = await this.markSuperseded(
|
|
@@ -3149,6 +3592,7 @@ var KbStore = class {
|
|
|
3149
3592
|
}
|
|
3150
3593
|
/** Resolves an open question, stamping who answered and when. */
|
|
3151
3594
|
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3595
|
+
assertActor(actor);
|
|
3152
3596
|
return this.mutate(
|
|
3153
3597
|
bundlePath2,
|
|
3154
3598
|
conceptId2,
|
|
@@ -3166,6 +3610,36 @@ ${answer}
|
|
|
3166
3610
|
`
|
|
3167
3611
|
);
|
|
3168
3612
|
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Removes one record, logged as `sweep`. The only path in this store that
|
|
3615
|
+
* deletes — see the specification for the scope that makes it safe.
|
|
3616
|
+
*
|
|
3617
|
+
* `expected` is re-read and re-checked immediately before the unlink, the
|
|
3618
|
+
* compare-and-swap `mutate` makes: a record retagged or moved out of a
|
|
3619
|
+
* terminal status since the caller listed it is reported, not removed.
|
|
3620
|
+
*/
|
|
3621
|
+
async deleteRecord(bundlePath2, conceptId2, expected, actor = "unknown") {
|
|
3622
|
+
assertActor(actor);
|
|
3623
|
+
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3624
|
+
const witness = await this.read(bundlePath2, conceptId2);
|
|
3625
|
+
if (!witness) throw new KbRecordNotFoundError(conceptId2);
|
|
3626
|
+
const { tags, strauss_status } = witness.frontmatter;
|
|
3627
|
+
if (!(tags ?? []).includes(expected.tag) || !expected.statuses.includes(strauss_status)) {
|
|
3628
|
+
return "changed-since-listing";
|
|
3629
|
+
}
|
|
3630
|
+
try {
|
|
3631
|
+
await (0, import_promises6.unlink)(target);
|
|
3632
|
+
} catch (error) {
|
|
3633
|
+
if (error.code !== "ENOENT") throw error;
|
|
3634
|
+
throw new KbRecordNotFoundError(conceptId2);
|
|
3635
|
+
}
|
|
3636
|
+
await this.record(this.root(bundlePath2), {
|
|
3637
|
+
operation: "sweep",
|
|
3638
|
+
by: actor,
|
|
3639
|
+
conceptId: conceptId2
|
|
3640
|
+
});
|
|
3641
|
+
return "deleted";
|
|
3642
|
+
}
|
|
3169
3643
|
/**
|
|
3170
3644
|
* Records matching a text query, each carrying its standing.
|
|
3171
3645
|
*
|
|
@@ -3401,12 +3875,25 @@ ${answer}
|
|
|
3401
3875
|
}
|
|
3402
3876
|
return expected;
|
|
3403
3877
|
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Drops the derived search index, so the next search rebuilds it.
|
|
3880
|
+
*
|
|
3881
|
+
* `searchBase` re-indexes when a record is newer than the index, which no
|
|
3882
|
+
* deletion makes true — a swept record would stay findable until some other
|
|
3883
|
+
* record was written.
|
|
3884
|
+
*/
|
|
3885
|
+
async dropSearchIndex(bundlePath2) {
|
|
3886
|
+
await (0, import_promises6.unlink)((0, import_node_path7.join)(this.root(bundlePath2), SEARCH_INDEX_FILE)).catch(
|
|
3887
|
+
() => void 0
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3404
3890
|
/**
|
|
3405
3891
|
* The log, with unparseable lines reported rather than repaired.
|
|
3406
3892
|
*
|
|
3407
3893
|
* The log is the bundle's only artifact that cannot be reconstructed — the
|
|
3408
3894
|
* records rebuild the index, and the code outlives both, but nothing else
|
|
3409
3895
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
3896
|
+
* Conflict markers are read past rather than reported per line.
|
|
3410
3897
|
*/
|
|
3411
3898
|
async readLog(bundlePath2) {
|
|
3412
3899
|
const raw = await (0, import_promises6.readFile)(
|
|
@@ -3414,6 +3901,13 @@ ${answer}
|
|
|
3414
3901
|
"utf8"
|
|
3415
3902
|
).catch(() => "");
|
|
3416
3903
|
const result = parseLog(raw);
|
|
3904
|
+
if (result.conflicted) {
|
|
3905
|
+
this.logger.warn?.({
|
|
3906
|
+
operation: "kb.log.parse",
|
|
3907
|
+
bundlePath: this.root(bundlePath2),
|
|
3908
|
+
outcome: "conflicted"
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3417
3911
|
for (const bad of result.malformed) {
|
|
3418
3912
|
this.logger.warn?.({
|
|
3419
3913
|
operation: "kb.log.parse",
|
|
@@ -3423,6 +3917,15 @@ ${answer}
|
|
|
3423
3917
|
}
|
|
3424
3918
|
return result;
|
|
3425
3919
|
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Appends one log entry for a move the store cannot see from one base.
|
|
3922
|
+
* Promotion writes into a target base and has to be legible from the source
|
|
3923
|
+
* base too, where nothing was written.
|
|
3924
|
+
*/
|
|
3925
|
+
async note(bundlePath2, entry) {
|
|
3926
|
+
assertActor(entry.by);
|
|
3927
|
+
await this.record(this.root(bundlePath2), entry);
|
|
3928
|
+
}
|
|
3426
3929
|
/**
|
|
3427
3930
|
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
3428
3931
|
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
@@ -3513,7 +4016,8 @@ ${answer}
|
|
|
3513
4016
|
/**
|
|
3514
4017
|
* Declares union merge for the log, so two worktrees writing the same
|
|
3515
4018
|
* bundle interleave their `log.jsonl` lines on merge rather than one
|
|
3516
|
-
* side's appends silently losing to git's ordinary line-level merge
|
|
4019
|
+
* side's appends silently losing to git's ordinary line-level merge — and
|
|
4020
|
+
* marks every store-owned file generated, so GitHub collapses it in a diff.
|
|
3517
4021
|
*
|
|
3518
4022
|
* Called from `record` — every path that appends a log line, not just
|
|
3519
4023
|
* `write` — so a bundle only ever mutated through `setStatus`/`verify`/
|
|
@@ -3526,10 +4030,9 @@ ${answer}
|
|
|
3526
4030
|
* race and created the file between the `readFile` below and this call,
|
|
3527
4031
|
* `wx` fails instead of truncating what that writer just wrote, and the
|
|
3528
4032
|
* 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`).
|
|
4033
|
+
* miss. A file that exists gets only the lines it lacks appended, never a
|
|
4034
|
+
* wholesale rewrite; an attribute it already sets — this one's value or a
|
|
4035
|
+
* user's own — is left alone (see `missingGitattributesLines`).
|
|
3533
4036
|
*
|
|
3534
4037
|
* `readFile` failing is `existing === null` only for `ENOENT` — genuinely
|
|
3535
4038
|
* missing. Any other error (a permission problem, a transient `EMFILE`,
|
|
@@ -3540,12 +4043,12 @@ ${answer}
|
|
|
3540
4043
|
* therefore left untouched and reported as a failure like any other.
|
|
3541
4044
|
*
|
|
3542
4045
|
* 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
|
-
*
|
|
4046
|
+
* lines, both append them — is possible and left unguarded: `appendFile` is
|
|
4047
|
+
* `O_APPEND`, so the result is two copies of the same lines rather than a
|
|
4048
|
+
* torn write, and the next call sees a duplicate declaration as "already
|
|
4049
|
+
* declared". A cheap-to-detect, harmless-to-leave residue, not a reason to
|
|
4050
|
+
* add a cross-process lock (see `ARCHITECTURE.md`'s rejection of one for
|
|
4051
|
+
* the same trade on records).
|
|
3549
4052
|
*
|
|
3550
4053
|
* Best-effort, like the log append it precedes: failing to write this
|
|
3551
4054
|
* file must not fail the mutation it guards.
|
|
@@ -3562,7 +4065,7 @@ ${answer}
|
|
|
3562
4065
|
}
|
|
3563
4066
|
if (existing === null) {
|
|
3564
4067
|
try {
|
|
3565
|
-
await (0, import_promises6.writeFile)(target,
|
|
4068
|
+
await (0, import_promises6.writeFile)(target, appendGitattributesLines(""), {
|
|
3566
4069
|
encoding: "utf8",
|
|
3567
4070
|
flag: "wx"
|
|
3568
4071
|
});
|
|
@@ -3582,8 +4085,9 @@ ${answer}
|
|
|
3582
4085
|
});
|
|
3583
4086
|
return;
|
|
3584
4087
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
4088
|
+
const addition = appendGitattributesLines(existing);
|
|
4089
|
+
if (addition) {
|
|
4090
|
+
await (0, import_promises6.appendFile)(target, addition, "utf8");
|
|
3587
4091
|
this.logger.info?.({
|
|
3588
4092
|
operation: "kb.gitattributes.ensure",
|
|
3589
4093
|
bundlePath: root,
|
|
@@ -3677,6 +4181,15 @@ function normalizeActor(id) {
|
|
|
3677
4181
|
if (colon === -1) return id.toLowerCase();
|
|
3678
4182
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
3679
4183
|
}
|
|
4184
|
+
var KB_ACTOR_PATTERN = /^[A-Za-z][\w-]*(?::[\p{L}\p{M}\p{N}_.@+/-]+)?$/u;
|
|
4185
|
+
function assertActor(actor, { named = false } = {}) {
|
|
4186
|
+
if (!KB_ACTOR_PATTERN.test(actor)) {
|
|
4187
|
+
throw new KbInvalidActorError(actor, "is not kind or kind:name");
|
|
4188
|
+
}
|
|
4189
|
+
if (named && normalizeActor(actor) === "unknown") {
|
|
4190
|
+
throw new KbInvalidActorError(actor, "cannot verify: name who checked");
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
3680
4193
|
|
|
3681
4194
|
// src/compose.ts
|
|
3682
4195
|
var import_zod4 = require("zod");
|
|
@@ -3692,7 +4205,7 @@ var composeInputSchema = import_zod4.z.object({
|
|
|
3692
4205
|
why: import_zod4.z.string().min(1),
|
|
3693
4206
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
3694
4207
|
sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
|
|
3695
|
-
anchors: import_zod4.z.array(
|
|
4208
|
+
anchors: import_zod4.z.array(kbAnchorWriteSchema).optional(),
|
|
3696
4209
|
sources: import_zod4.z.array(kbSourceSchema).optional(),
|
|
3697
4210
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
3698
4211
|
assumption: import_zod4.z.boolean().optional(),
|
|
@@ -4352,7 +4865,7 @@ function kbJsonSchemas() {
|
|
|
4352
4865
|
|
|
4353
4866
|
// src/match-diff.ts
|
|
4354
4867
|
function matchToDiff(files, records, options = {}) {
|
|
4355
|
-
const ranges =
|
|
4868
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
4356
4869
|
const anchored = records.filter(
|
|
4357
4870
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
4358
4871
|
);
|
|
@@ -4370,8 +4883,8 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4370
4883
|
let precision = "symbol";
|
|
4371
4884
|
for (const { record, anchors } of candidates) {
|
|
4372
4885
|
const placement = place(anchors, file.filePath, hunk, ranges);
|
|
4373
|
-
if (placement === "miss") continue;
|
|
4374
|
-
if (placement === "file") precision = "file";
|
|
4886
|
+
if (placement.kind === "miss") continue;
|
|
4887
|
+
if (placement.kind === "file") precision = "file";
|
|
4375
4888
|
hits.push(record);
|
|
4376
4889
|
}
|
|
4377
4890
|
if (!hits.length) continue;
|
|
@@ -4385,19 +4898,51 @@ function matchToDiff(files, records, options = {}) {
|
|
|
4385
4898
|
}
|
|
4386
4899
|
return matches3;
|
|
4387
4900
|
}
|
|
4901
|
+
function placeOnHunk(record, filePath, hunk, symbolRanges = []) {
|
|
4902
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
4903
|
+
(anchor) => normalize(anchor.file) === normalize(filePath)
|
|
4904
|
+
);
|
|
4905
|
+
return place(anchors, filePath, hunk, asIndex(symbolRanges));
|
|
4906
|
+
}
|
|
4907
|
+
function asIndex(ranges) {
|
|
4908
|
+
return isIndex(ranges) ? ranges : symbolRangeIndex(ranges);
|
|
4909
|
+
}
|
|
4910
|
+
function isIndex(ranges) {
|
|
4911
|
+
return !Array.isArray(ranges);
|
|
4912
|
+
}
|
|
4913
|
+
function anchorOnHunk(record, filePath, hunk, symbolRanges = []) {
|
|
4914
|
+
return placeOnHunk(record, filePath, hunk, symbolRanges).anchor;
|
|
4915
|
+
}
|
|
4388
4916
|
function place(anchors, filePath, hunk, ranges) {
|
|
4389
|
-
let fallback = "miss";
|
|
4917
|
+
let fallback = { kind: "miss" };
|
|
4390
4918
|
for (const anchor of anchors) {
|
|
4391
|
-
if (
|
|
4392
|
-
|
|
4919
|
+
if (side(anchor.side) !== side(hunk.side)) continue;
|
|
4920
|
+
if (anchor.span) {
|
|
4921
|
+
if (overlaps(
|
|
4922
|
+
{ startLine: anchor.span.start, endLine: anchor.span.end },
|
|
4923
|
+
hunk
|
|
4924
|
+
)) {
|
|
4925
|
+
return { kind: "symbol", anchor };
|
|
4926
|
+
}
|
|
4927
|
+
continue;
|
|
4928
|
+
}
|
|
4929
|
+
if (!anchor.symbol) return { kind: "file", anchor };
|
|
4930
|
+
const resolved = ranges.get(
|
|
4931
|
+
key(filePath, anchor.symbol, side(anchor.side))
|
|
4932
|
+
);
|
|
4393
4933
|
if (!resolved?.length) {
|
|
4394
|
-
fallback = "file";
|
|
4934
|
+
if (fallback.kind === "miss") fallback = { kind: "file", anchor };
|
|
4395
4935
|
continue;
|
|
4396
4936
|
}
|
|
4397
|
-
if (resolved.some((range) => overlaps(range, hunk)))
|
|
4937
|
+
if (resolved.some((range) => overlaps(range, hunk))) {
|
|
4938
|
+
return { kind: "symbol", anchor };
|
|
4939
|
+
}
|
|
4398
4940
|
}
|
|
4399
4941
|
return fallback;
|
|
4400
4942
|
}
|
|
4943
|
+
function side(value) {
|
|
4944
|
+
return value ?? "new";
|
|
4945
|
+
}
|
|
4401
4946
|
function overlaps(range, hunk) {
|
|
4402
4947
|
return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;
|
|
4403
4948
|
}
|
|
@@ -4415,29 +4960,198 @@ function order(records) {
|
|
|
4415
4960
|
)
|
|
4416
4961
|
);
|
|
4417
4962
|
}
|
|
4418
|
-
function
|
|
4963
|
+
function symbolRangeIndex(ranges) {
|
|
4419
4964
|
const byKey = /* @__PURE__ */ new Map();
|
|
4420
4965
|
for (const range of ranges) {
|
|
4421
|
-
const id = key(range.file, range.symbol);
|
|
4966
|
+
const id = key(range.file, range.symbol, side(range.side));
|
|
4422
4967
|
byKey.set(id, [...byKey.get(id) ?? [], range]);
|
|
4423
4968
|
}
|
|
4424
4969
|
return byKey;
|
|
4425
4970
|
}
|
|
4426
|
-
function key(file, symbol) {
|
|
4427
|
-
return `${normalize(file)}#${symbol}`;
|
|
4971
|
+
function key(file, symbol, at2) {
|
|
4972
|
+
return `${normalize(file)}#${symbol}#${at2}`;
|
|
4428
4973
|
}
|
|
4429
4974
|
function normalize(path) {
|
|
4430
4975
|
return path.replace(/^\.\//, "");
|
|
4431
4976
|
}
|
|
4432
4977
|
|
|
4433
|
-
// src/
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4978
|
+
// src/classify/model.ts
|
|
4979
|
+
var KB_CLASSES = [
|
|
4980
|
+
"test",
|
|
4981
|
+
"config",
|
|
4982
|
+
"ci",
|
|
4983
|
+
"docs",
|
|
4984
|
+
"lockfile",
|
|
4985
|
+
"generated",
|
|
4986
|
+
"boilerplate",
|
|
4987
|
+
"rename",
|
|
4988
|
+
"source"
|
|
4989
|
+
];
|
|
4990
|
+
var DEFAULT_THRESHOLDS = {
|
|
4991
|
+
boilerplate: 0.8,
|
|
4992
|
+
rename: 90
|
|
4993
|
+
};
|
|
4994
|
+
|
|
4995
|
+
// src/classify/rules.ts
|
|
4996
|
+
var PATH_RULES = [
|
|
4997
|
+
{
|
|
4998
|
+
name: "test-path",
|
|
4999
|
+
class: "test",
|
|
5000
|
+
test: /(^|\/)(__tests__|__mocks__|tests?)\/|\.(spec|test)\.[^/]+$/
|
|
5001
|
+
},
|
|
5002
|
+
{
|
|
5003
|
+
name: "ci-path",
|
|
5004
|
+
class: "ci",
|
|
5005
|
+
test: /(^|\/)\.github\/|(^|\/)(\.circleci|\.buildkite|\.gitlab|ci)\/[^/]*\.ya?ml$|(^|\/)Dockerfile(\.[^/]*)?$|\.tf$/
|
|
5006
|
+
},
|
|
5007
|
+
{
|
|
5008
|
+
name: "docs-path",
|
|
5009
|
+
class: "docs",
|
|
5010
|
+
test: /\.md$|(^|\/)docs\/|(^|\/)LICENSE(\.(md|txt|rst))?$/
|
|
5011
|
+
},
|
|
5012
|
+
{
|
|
5013
|
+
name: "lockfile-path",
|
|
5014
|
+
class: "lockfile",
|
|
5015
|
+
test: /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|Cargo\.lock|go\.sum)$/
|
|
5016
|
+
},
|
|
5017
|
+
{
|
|
5018
|
+
name: "config-path",
|
|
5019
|
+
class: "config",
|
|
5020
|
+
// `.jsonl` rides with `.json`: an append-only log of JSON is configuration
|
|
5021
|
+
// data too, and calling it source would send a reviewer to read it. Every
|
|
5022
|
+
// arm is anchored at both ends: `src/tsconfig-loader.ts` and `report.env.ts`
|
|
5023
|
+
// are source, not config.
|
|
5024
|
+
test: /\.(jsonc?|jsonl|ya?ml|toml|ini)$|(^|\/)\.env(?![^/]*\.[cm]?[jt]sx?$)([.-][^/]*)?$|[^/]+\.env$|(^|\/)tsconfig[^/]*\.json$|\.config\.[^/]+$|(^|\/)\.(eslintrc|prettierrc)[^/]*$/
|
|
5025
|
+
}
|
|
5026
|
+
];
|
|
5027
|
+
var HEADER_LINES = 20;
|
|
5028
|
+
var GENERATED_MARKERS = [
|
|
5029
|
+
/@generated\b/i,
|
|
5030
|
+
/\bdo not edit\b/i,
|
|
5031
|
+
/\bcode generated by\b/i,
|
|
5032
|
+
/\bthis file was automatically generated\b/i
|
|
5033
|
+
];
|
|
5034
|
+
var BOILERPLATE_SHAPES = [
|
|
5035
|
+
{ name: "import", test: /^import\b|^\}\s*from\s+["']/ },
|
|
5036
|
+
{ name: "re-export", test: /^export\s+(\*|\{|type\s*[{*])/ },
|
|
5037
|
+
{
|
|
5038
|
+
name: "class-shell",
|
|
5039
|
+
test: /^(export\s+)?(default\s+)?(abstract\s+)?class\s+[\w$]+[^{]*\{\s*\}$/
|
|
5040
|
+
},
|
|
5041
|
+
{ name: "punctuation", test: /^[{}()[\],;]+$/ }
|
|
5042
|
+
];
|
|
5043
|
+
function generatedMarker(lines) {
|
|
5044
|
+
for (const line of lines.slice(0, HEADER_LINES)) {
|
|
5045
|
+
const hit = GENERATED_MARKERS.find((marker) => marker.test(line));
|
|
5046
|
+
if (hit) return hit.source.replaceAll("\\b", "");
|
|
5047
|
+
}
|
|
5048
|
+
return void 0;
|
|
5049
|
+
}
|
|
5050
|
+
function isBoilerplateLine(line) {
|
|
5051
|
+
return BOILERPLATE_SHAPES.some((shape) => shape.test.test(line));
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
// src/classify/classify.ts
|
|
5055
|
+
function classifyDiff(files, options = {}) {
|
|
5056
|
+
const overrides = currentOverrides(options.records ?? [], options.now);
|
|
5057
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
5058
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
|
|
5059
|
+
return files.map((file) => classifyFile(file, overrides, ranges, thresholds));
|
|
5060
|
+
}
|
|
5061
|
+
var OVERRIDE_CLASS = /* @__PURE__ */ new Map([
|
|
5062
|
+
["review:generated", "generated"],
|
|
5063
|
+
["review:boilerplate", "boilerplate"],
|
|
5064
|
+
["review:move", "rename"]
|
|
5065
|
+
]);
|
|
5066
|
+
var WHOLE_FILE = {
|
|
5067
|
+
startLine: 1,
|
|
5068
|
+
endLine: Number.MAX_SAFE_INTEGER
|
|
5069
|
+
};
|
|
5070
|
+
function classifyFile(file, overrides, ranges, thresholds) {
|
|
5071
|
+
const whole = overrides.find(
|
|
5072
|
+
({ record }) => placeOnHunk(record, file.filePath, WHOLE_FILE, ranges).kind === "file"
|
|
5073
|
+
);
|
|
5074
|
+
const verdict = whole ? verdictOf(whole) : heuristic(file, thresholds);
|
|
5075
|
+
const hunks = file.hunks.map((hunk) => {
|
|
5076
|
+
const hit = whole ?? overrides.find(
|
|
5077
|
+
({ record }) => placeOnHunk(record, file.filePath, hunk, ranges).kind !== "miss"
|
|
5078
|
+
);
|
|
5079
|
+
return {
|
|
5080
|
+
startLine: hunk.startLine,
|
|
5081
|
+
endLine: hunk.endLine,
|
|
5082
|
+
...hit ? verdictOf(hit) : verdict
|
|
5083
|
+
};
|
|
5084
|
+
});
|
|
5085
|
+
return {
|
|
5086
|
+
filePath: file.filePath,
|
|
5087
|
+
...verdict,
|
|
5088
|
+
...file.renamedFrom ? { renamedFrom: file.renamedFrom } : {},
|
|
5089
|
+
...hunks.some((hunk) => hunk.class !== verdict.class) ? { hunks } : {}
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
function verdictOf(override) {
|
|
5093
|
+
return {
|
|
5094
|
+
class: override.class,
|
|
5095
|
+
reason: `kb-override ${override.record.conceptId}`
|
|
5096
|
+
};
|
|
5097
|
+
}
|
|
5098
|
+
function heuristic(file, thresholds) {
|
|
5099
|
+
const marker = generatedMarker(file.header ?? headOfDiff(file));
|
|
5100
|
+
if (marker)
|
|
5101
|
+
return { class: "generated", reason: `generated-header ${marker}` };
|
|
5102
|
+
const rule = PATH_RULES.find((entry) => entry.test.test(file.filePath));
|
|
5103
|
+
if (rule) return { class: rule.class, reason: rule.name };
|
|
5104
|
+
if (file.renamedFrom && !file.hunks.length && (file.similarity ?? 100) >= thresholds.rename) {
|
|
5105
|
+
return { class: "rename", reason: `rename ${file.renamedFrom}` };
|
|
5106
|
+
}
|
|
5107
|
+
const share = boilerplateShare(file);
|
|
5108
|
+
if (share !== void 0 && share >= thresholds.boilerplate) {
|
|
5109
|
+
return {
|
|
5110
|
+
class: "boilerplate",
|
|
5111
|
+
reason: `boilerplate ${Math.round(share * 100)}%`
|
|
5112
|
+
};
|
|
5113
|
+
}
|
|
5114
|
+
return { class: "source", reason: "default" };
|
|
5115
|
+
}
|
|
5116
|
+
function headOfDiff(file) {
|
|
5117
|
+
return file.hunks.flatMap(
|
|
5118
|
+
(hunk) => (hunk.side ?? "new") === "new" && hunk.startLine <= HEADER_LINES ? (hunk.lines ?? []).slice(0, HEADER_LINES - hunk.startLine + 1) : []
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
function boilerplateShare(file) {
|
|
5122
|
+
const lines = file.hunks.flatMap((hunk) => hunk.lines ?? []).map((line) => line.trim()).filter(Boolean);
|
|
5123
|
+
if (!lines.length) return void 0;
|
|
5124
|
+
return lines.filter(isBoilerplateLine).length / lines.length;
|
|
5125
|
+
}
|
|
5126
|
+
function currentOverrides(records, now) {
|
|
5127
|
+
const tagged = records.flatMap((record) => {
|
|
5128
|
+
if (record.frontmatter.type !== "fact") return [];
|
|
5129
|
+
const tag = (record.frontmatter.tags ?? []).find(
|
|
5130
|
+
(entry) => OVERRIDE_CLASS.has(entry)
|
|
5131
|
+
);
|
|
5132
|
+
const asserted = tag && OVERRIDE_CLASS.get(tag);
|
|
5133
|
+
return asserted ? [{ record, class: asserted }] : [];
|
|
5134
|
+
});
|
|
5135
|
+
const current = new Set(
|
|
5136
|
+
adjudicate(
|
|
5137
|
+
tagged.map(({ record }) => record),
|
|
5138
|
+
records,
|
|
5139
|
+
now
|
|
5140
|
+
).filter((entry) => entry.standing === "current").map((entry) => entry.record.conceptId)
|
|
5141
|
+
);
|
|
5142
|
+
return tagged.filter(({ record }) => current.has(record.conceptId)).sort(
|
|
5143
|
+
(left, right) => left.record.conceptId.localeCompare(right.record.conceptId)
|
|
5144
|
+
);
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
// src/validate.ts
|
|
5148
|
+
function validateBundle(records) {
|
|
5149
|
+
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
5150
|
+
const problems = [];
|
|
5151
|
+
const report = (check, conceptId2, note, severity = "error") => problems.push({ check, conceptId: conceptId2, note, severity });
|
|
5152
|
+
for (const record of records) {
|
|
5153
|
+
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
5154
|
+
if (!isKbRecordType(fm.type)) {
|
|
4441
5155
|
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
4442
5156
|
}
|
|
4443
5157
|
if (fm.strauss_status === "superseded") {
|
|
@@ -4489,6 +5203,34 @@ function validateBundle(records) {
|
|
|
4489
5203
|
}
|
|
4490
5204
|
}
|
|
4491
5205
|
for (const anchor of fm.strauss_anchors ?? []) {
|
|
5206
|
+
if (anchor.span && anchor.symbol) {
|
|
5207
|
+
report(
|
|
5208
|
+
"anchor_span",
|
|
5209
|
+
conceptId2,
|
|
5210
|
+
`anchor ${anchor.file} names both a symbol and a span \u2014 one or the other`
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
5213
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
5214
|
+
report(
|
|
5215
|
+
"anchor_span",
|
|
5216
|
+
conceptId2,
|
|
5217
|
+
`anchor ${anchor.file} span ${anchor.span.start}-${anchor.span.end} ends before it starts`
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
5221
|
+
report(
|
|
5222
|
+
"anchor_span",
|
|
5223
|
+
conceptId2,
|
|
5224
|
+
`anchor ${anchor.file} is a span with hash_kind: "ast" \u2014 a span is hashed raw`
|
|
5225
|
+
);
|
|
5226
|
+
}
|
|
5227
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
5228
|
+
report(
|
|
5229
|
+
"anchor_side",
|
|
5230
|
+
conceptId2,
|
|
5231
|
+
`anchor ${anchor.file} is side: "old" with no ref`
|
|
5232
|
+
);
|
|
5233
|
+
}
|
|
4492
5234
|
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
4493
5235
|
report(
|
|
4494
5236
|
"anchor_repo",
|
|
@@ -4535,14 +5277,19 @@ var DAY_MS = 864e5;
|
|
|
4535
5277
|
function anchorResolverCounts(bundle) {
|
|
4536
5278
|
let treeSitter = 0;
|
|
4537
5279
|
let regex = 0;
|
|
5280
|
+
let span2 = 0;
|
|
5281
|
+
let oldSide = 0;
|
|
4538
5282
|
for (const record of bundle) {
|
|
4539
5283
|
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4540
|
-
if (!anchor.hash
|
|
4541
|
-
if (anchor.
|
|
5284
|
+
if (!anchor.hash) continue;
|
|
5285
|
+
if (anchor.side === "old") oldSide += 1;
|
|
5286
|
+
if (anchor.span) span2 += 1;
|
|
5287
|
+
else if (!anchor.symbol) continue;
|
|
5288
|
+
else if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
4542
5289
|
else regex += 1;
|
|
4543
5290
|
}
|
|
4544
5291
|
}
|
|
4545
|
-
return { total: treeSitter + regex, treeSitter, regex };
|
|
5292
|
+
return { total: treeSitter + regex + span2, treeSitter, regex, span: span2, oldSide };
|
|
4546
5293
|
}
|
|
4547
5294
|
function doctor(bundle, options = {}) {
|
|
4548
5295
|
const thresholds = {
|
|
@@ -4552,7 +5299,7 @@ function doctor(bundle, options = {}) {
|
|
|
4552
5299
|
};
|
|
4553
5300
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
4554
5301
|
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
4555
|
-
const
|
|
5302
|
+
const standings2 = new Map(
|
|
4556
5303
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
4557
5304
|
);
|
|
4558
5305
|
const inForce = adjudicated.filter(
|
|
@@ -4565,7 +5312,7 @@ function doctor(bundle, options = {}) {
|
|
|
4565
5312
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
4566
5313
|
group("orphaned", orphaned(bundle)),
|
|
4567
5314
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
4568
|
-
group("superseded-but-cited", supersededButCited(bundle,
|
|
5315
|
+
group("superseded-but-cited", supersededButCited(bundle, standings2)),
|
|
4569
5316
|
group("drifted", drifted(inForce)),
|
|
4570
5317
|
group("unchecked", unchecked(inForce))
|
|
4571
5318
|
];
|
|
@@ -4731,14 +5478,14 @@ function brokenSupersession(bundle, adjudicated) {
|
|
|
4731
5478
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
4732
5479
|
);
|
|
4733
5480
|
}
|
|
4734
|
-
function supersededButCited(bundle,
|
|
5481
|
+
function supersededButCited(bundle, standings2) {
|
|
4735
5482
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
4736
5483
|
const findings = [];
|
|
4737
5484
|
for (const record of bundle) {
|
|
4738
|
-
const standing =
|
|
5485
|
+
const standing = standings2.get(record.conceptId);
|
|
4739
5486
|
if (standing === "superseded" || standing === "rejected") continue;
|
|
4740
5487
|
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
4741
|
-
const targetStanding =
|
|
5488
|
+
const targetStanding = standings2.get(target.conceptId);
|
|
4742
5489
|
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
4743
5490
|
continue;
|
|
4744
5491
|
}
|
|
@@ -4943,19 +5690,11 @@ function argvPositional(argv, ...names) {
|
|
|
4943
5690
|
}
|
|
4944
5691
|
|
|
4945
5692
|
// src/commands/anchor-resolve.ts
|
|
4946
|
-
function resolverSummary(results) {
|
|
4947
|
-
const names = [
|
|
4948
|
-
...new Set(
|
|
4949
|
-
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
4950
|
-
)
|
|
4951
|
-
].sort();
|
|
4952
|
-
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
4953
|
-
}
|
|
4954
5693
|
var anchorResolveCommand = define({
|
|
4955
5694
|
name: "anchor-resolve",
|
|
4956
5695
|
tool: "kb_anchor_resolve",
|
|
4957
|
-
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
4958
|
-
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only.
|
|
5696
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]",
|
|
5697
|
+
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Exits non-zero on drift.",
|
|
4959
5698
|
input: import_zod9.z.object({
|
|
4960
5699
|
bundlePath,
|
|
4961
5700
|
conceptId,
|
|
@@ -4968,6 +5707,9 @@ var anchorResolveCommand = define({
|
|
|
4968
5707
|
),
|
|
4969
5708
|
restamp: import_zod9.z.boolean().optional().describe(
|
|
4970
5709
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
5710
|
+
),
|
|
5711
|
+
check: import_zod9.z.boolean().optional().describe(
|
|
5712
|
+
"Resolve and report only: no hash, no `resolved_at`, no log entry."
|
|
4971
5713
|
)
|
|
4972
5714
|
}),
|
|
4973
5715
|
fromArgv: (argv, path) => ({
|
|
@@ -4976,9 +5718,24 @@ var anchorResolveCommand = define({
|
|
|
4976
5718
|
repoRoot: argvFlag(argv, "--repo-root"),
|
|
4977
5719
|
offline: argv.includes("--offline"),
|
|
4978
5720
|
rebaseline: argv.includes("--rebaseline"),
|
|
4979
|
-
restamp: argv.includes("--restamp")
|
|
5721
|
+
restamp: argv.includes("--restamp"),
|
|
5722
|
+
check: argv.includes("--check")
|
|
4980
5723
|
}),
|
|
4981
|
-
run: async ({ store, actor, now }, {
|
|
5724
|
+
run: async ({ store, actor, now }, {
|
|
5725
|
+
bundlePath: path,
|
|
5726
|
+
conceptId: id,
|
|
5727
|
+
repoRoot,
|
|
5728
|
+
offline,
|
|
5729
|
+
rebaseline,
|
|
5730
|
+
restamp,
|
|
5731
|
+
check
|
|
5732
|
+
}) => {
|
|
5733
|
+
if (check && (rebaseline || restamp)) {
|
|
5734
|
+
throw new KbFlagConflictError([
|
|
5735
|
+
"check",
|
|
5736
|
+
rebaseline ? "rebaseline" : "restamp"
|
|
5737
|
+
]);
|
|
5738
|
+
}
|
|
4982
5739
|
const root = repoRoot ?? process.cwd();
|
|
4983
5740
|
const record = await store.read(path, id);
|
|
4984
5741
|
if (!record) throw new KbRecordNotFoundError(id);
|
|
@@ -4987,7 +5744,6 @@ var anchorResolveCommand = define({
|
|
|
4987
5744
|
return {
|
|
4988
5745
|
conceptId: id,
|
|
4989
5746
|
results: [],
|
|
4990
|
-
verified: false,
|
|
4991
5747
|
note: "record has no anchors"
|
|
4992
5748
|
};
|
|
4993
5749
|
}
|
|
@@ -5004,6 +5760,7 @@ var anchorResolveCommand = define({
|
|
|
5004
5760
|
const base2 = {
|
|
5005
5761
|
file: anchor.file,
|
|
5006
5762
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5763
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
5007
5764
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
5008
5765
|
// and now resolves to nothing is a broken anchor, and the exit code
|
|
5009
5766
|
// has to be able to tell it from one nobody ever stamped.
|
|
@@ -5044,7 +5801,7 @@ var anchorResolveCommand = define({
|
|
|
5044
5801
|
if (!anchor.hash) {
|
|
5045
5802
|
results.push({
|
|
5046
5803
|
...base2,
|
|
5047
|
-
state: "stamped",
|
|
5804
|
+
state: check ? "unstamped" : "stamped",
|
|
5048
5805
|
currentHash: stampedHash,
|
|
5049
5806
|
hashKind: stampedKind,
|
|
5050
5807
|
...producedBy ? { resolver: producedBy } : {}
|
|
@@ -5096,7 +5853,7 @@ var anchorResolveCommand = define({
|
|
|
5096
5853
|
if (refresh) dirty = true;
|
|
5097
5854
|
}
|
|
5098
5855
|
let frozen = false;
|
|
5099
|
-
if (dirty) {
|
|
5856
|
+
if (dirty && !check) {
|
|
5100
5857
|
try {
|
|
5101
5858
|
await assertBaseNotFrozen(process.cwd(), path);
|
|
5102
5859
|
} catch (error) {
|
|
@@ -5111,42 +5868,11 @@ var anchorResolveCommand = define({
|
|
|
5111
5868
|
const unreachable = results.filter(
|
|
5112
5869
|
(entry) => isUncheckedReason(entry.reason)
|
|
5113
5870
|
).length;
|
|
5114
|
-
const checked = results.length - unreachable;
|
|
5115
5871
|
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
5116
|
-
const note = `${matches3}/${
|
|
5117
|
-
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
5118
|
-
if (clean) {
|
|
5119
|
-
try {
|
|
5120
|
-
await store.verify(
|
|
5121
|
-
path,
|
|
5122
|
-
id,
|
|
5123
|
-
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
5124
|
-
actor,
|
|
5125
|
-
now()
|
|
5126
|
-
);
|
|
5127
|
-
} catch (error) {
|
|
5128
|
-
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
5129
|
-
return {
|
|
5130
|
-
conceptId: id,
|
|
5131
|
-
results,
|
|
5132
|
-
verified: false,
|
|
5133
|
-
verifyRefused: "self-verification",
|
|
5134
|
-
...frozenNote,
|
|
5135
|
-
...hintNote
|
|
5136
|
-
};
|
|
5137
|
-
}
|
|
5138
|
-
return {
|
|
5139
|
-
conceptId: id,
|
|
5140
|
-
results,
|
|
5141
|
-
verified: true,
|
|
5142
|
-
...frozenNote,
|
|
5143
|
-
...hintNote
|
|
5144
|
-
};
|
|
5145
|
-
}
|
|
5872
|
+
const note = `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable`;
|
|
5146
5873
|
return {
|
|
5147
5874
|
conceptId: id,
|
|
5148
5875
|
results,
|
|
5149
|
-
verified: false,
|
|
5150
5876
|
...unreachable ? { note } : {},
|
|
5151
5877
|
...frozenNote,
|
|
5152
5878
|
...hintNote
|
|
@@ -5180,12 +5906,18 @@ async function readSources(anchors, root, offline) {
|
|
|
5180
5906
|
const foreign = new Map(
|
|
5181
5907
|
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
5182
5908
|
);
|
|
5183
|
-
const local = anchors.filter(
|
|
5909
|
+
const local = anchors.filter(
|
|
5910
|
+
(anchor) => !foreign.get(anchor) && anchor.side !== "old"
|
|
5911
|
+
);
|
|
5912
|
+
const committed = anchors.filter(
|
|
5913
|
+
(anchor) => !foreign.get(anchor) && anchor.side === "old"
|
|
5914
|
+
);
|
|
5184
5915
|
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
5185
5916
|
const reads = await readAnchorFiles(
|
|
5186
5917
|
local.map((anchor) => anchor.file),
|
|
5187
5918
|
anchorFileReader(root)
|
|
5188
5919
|
);
|
|
5920
|
+
const atRef = await readCommitted(root, committed);
|
|
5189
5921
|
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
5190
5922
|
offline
|
|
5191
5923
|
});
|
|
@@ -5197,6 +5929,13 @@ async function readSources(anchors, root, offline) {
|
|
|
5197
5929
|
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
5198
5930
|
);
|
|
5199
5931
|
}
|
|
5932
|
+
for (const anchor of committed) {
|
|
5933
|
+
const read = atRef.get(atRefKey(anchor));
|
|
5934
|
+
sources.set(
|
|
5935
|
+
anchor,
|
|
5936
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
5937
|
+
);
|
|
5938
|
+
}
|
|
5200
5939
|
for (const anchor of remote) {
|
|
5201
5940
|
const repo = anchor.repo;
|
|
5202
5941
|
const key2 = normalizeRepoUrl(repo);
|
|
@@ -5329,141 +6068,12 @@ function count(value, noun) {
|
|
|
5329
6068
|
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
5330
6069
|
}
|
|
5331
6070
|
|
|
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
|
|
6071
|
+
// src/commands/classify.ts
|
|
6072
|
+
var import_node_buffer = require("buffer");
|
|
6073
|
+
var import_promises10 = require("fs/promises");
|
|
6074
|
+
var import_node_path12 = require("path");
|
|
5394
6075
|
var import_zod15 = require("zod");
|
|
5395
6076
|
|
|
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
6077
|
// src/drift/moved.ts
|
|
5468
6078
|
var import_promises9 = require("fs/promises");
|
|
5469
6079
|
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
@@ -5482,6 +6092,7 @@ function movedSearch(repoRoot, options = {}) {
|
|
|
5482
6092
|
async find(anchor) {
|
|
5483
6093
|
const stored = anchor.hash;
|
|
5484
6094
|
if (!stored) return void 0;
|
|
6095
|
+
if (anchor.span) return sameFileWindow(anchor, read, stored);
|
|
5485
6096
|
const language = languageForFile(anchor.file);
|
|
5486
6097
|
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5487
6098
|
const candidates = await filesForLanguage(language);
|
|
@@ -5579,6 +6190,11 @@ async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
|
5579
6190
|
);
|
|
5580
6191
|
const out = [];
|
|
5581
6192
|
for (const { anchor, entry } of wanted) {
|
|
6193
|
+
if (anchor.side === "old") {
|
|
6194
|
+
const settled2 = entry.class ?? "changed";
|
|
6195
|
+
out.push({ anchor, entry: { ...entry, class: settled2 }, class: settled2 });
|
|
6196
|
+
continue;
|
|
6197
|
+
}
|
|
5582
6198
|
const movedTo = await search.find(anchor);
|
|
5583
6199
|
if (movedTo) {
|
|
5584
6200
|
out.push({
|
|
@@ -5652,8 +6268,8 @@ function unifiedDiff(before, after, options = {}) {
|
|
|
5652
6268
|
}
|
|
5653
6269
|
const truncated = body.length > max;
|
|
5654
6270
|
const shown = truncated ? body.slice(0, max) : body;
|
|
5655
|
-
const
|
|
5656
|
-
const lines = [
|
|
6271
|
+
const header2 = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
6272
|
+
const lines = [header2, ...shown];
|
|
5657
6273
|
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5658
6274
|
return { text: lines.join("\n"), added, removed, truncated };
|
|
5659
6275
|
}
|
|
@@ -5713,12 +6329,12 @@ async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
|
5713
6329
|
...options.search ? { search: options.search } : {},
|
|
5714
6330
|
withHistory: options.withDiff !== false
|
|
5715
6331
|
});
|
|
5716
|
-
const
|
|
6332
|
+
const open2 = classified.filter(
|
|
5717
6333
|
(found) => found.class === "changed" || found.class === "gone"
|
|
5718
6334
|
);
|
|
5719
|
-
if (!
|
|
5720
|
-
const budget = diffBudget(
|
|
5721
|
-
const anchors =
|
|
6335
|
+
if (!open2.length) return { packet: null, classified };
|
|
6336
|
+
const budget = diffBudget(open2.length);
|
|
6337
|
+
const anchors = open2.map(
|
|
5722
6338
|
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5723
6339
|
);
|
|
5724
6340
|
const type = record.frontmatter.type;
|
|
@@ -5792,46 +6408,633 @@ function claimOf(record) {
|
|
|
5792
6408
|
return text ? { section, text } : null;
|
|
5793
6409
|
}
|
|
5794
6410
|
|
|
5795
|
-
// src/commands/
|
|
6411
|
+
// src/commands/match/command.ts
|
|
5796
6412
|
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
|
-
|
|
5834
|
-
|
|
6413
|
+
|
|
6414
|
+
// src/commands/match/errors.ts
|
|
6415
|
+
var KbMatchInputError = class extends BaseError {
|
|
6416
|
+
constructor(reason) {
|
|
6417
|
+
super({
|
|
6418
|
+
message: `match: ${reason}`,
|
|
6419
|
+
errorType: "KbMatchInput" /* KbMatchInput */,
|
|
6420
|
+
code: 400,
|
|
6421
|
+
fault: "User" /* User */,
|
|
6422
|
+
retriable: false,
|
|
6423
|
+
reportToUser: true,
|
|
6424
|
+
details: { reason }
|
|
6425
|
+
});
|
|
6426
|
+
this.reason = reason;
|
|
6427
|
+
}
|
|
6428
|
+
reason;
|
|
6429
|
+
};
|
|
6430
|
+
|
|
6431
|
+
// src/commands/match/model.ts
|
|
6432
|
+
var import_zod13 = require("zod");
|
|
6433
|
+
var diffHunkSchema = import_zod13.z.object({
|
|
6434
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
6435
|
+
endLine: import_zod13.z.number().int().positive(),
|
|
6436
|
+
side: import_zod13.z.enum(["old", "new"]).optional()
|
|
6437
|
+
}).passthrough();
|
|
6438
|
+
var diffFileSchema = import_zod13.z.object({
|
|
6439
|
+
filePath: import_zod13.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
6440
|
+
hunks: import_zod13.z.array(diffHunkSchema)
|
|
6441
|
+
});
|
|
6442
|
+
var symbolRangeSchema = import_zod13.z.object({
|
|
6443
|
+
file: import_zod13.z.string().min(1),
|
|
6444
|
+
symbol: import_zod13.z.string().min(1),
|
|
6445
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
6446
|
+
endLine: import_zod13.z.number().int().positive()
|
|
6447
|
+
});
|
|
6448
|
+
|
|
6449
|
+
// src/commands/match/parse-unified-diff.ts
|
|
6450
|
+
var FILE_HEADER = /^diff --git (.+)$/;
|
|
6451
|
+
var HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
6452
|
+
var SIMILARITY = /^similarity index (\d+)%$/;
|
|
6453
|
+
var KNOWN_PREFIX = /^[ab]\//;
|
|
6454
|
+
function parseUnifiedDiff(patch, options = {}) {
|
|
6455
|
+
const files = [];
|
|
6456
|
+
let current;
|
|
6457
|
+
let listed = false;
|
|
6458
|
+
let shared;
|
|
6459
|
+
let oldPath;
|
|
6460
|
+
let rename4 = {};
|
|
6461
|
+
let inHeader = false;
|
|
6462
|
+
let added;
|
|
6463
|
+
let removed;
|
|
6464
|
+
const list = () => {
|
|
6465
|
+
if (!current || listed) return;
|
|
6466
|
+
files.push(current);
|
|
6467
|
+
listed = true;
|
|
6468
|
+
};
|
|
6469
|
+
const open2 = (filePath) => {
|
|
6470
|
+
current = { filePath, hunks: [], ...rename4 };
|
|
6471
|
+
listed = false;
|
|
6472
|
+
};
|
|
6473
|
+
const amend = () => {
|
|
6474
|
+
if (current) Object.assign(current, rename4);
|
|
6475
|
+
};
|
|
6476
|
+
const close = () => {
|
|
6477
|
+
if (options.keepEmpty) list();
|
|
6478
|
+
current = void 0;
|
|
6479
|
+
listed = false;
|
|
6480
|
+
added = void 0;
|
|
6481
|
+
removed = void 0;
|
|
6482
|
+
};
|
|
6483
|
+
for (const raw of patch.split("\n")) {
|
|
6484
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
6485
|
+
const start = FILE_HEADER.exec(line);
|
|
6486
|
+
if (start) {
|
|
6487
|
+
close();
|
|
6488
|
+
oldPath = void 0;
|
|
6489
|
+
rename4 = {};
|
|
6490
|
+
shared = sharedHeaderPath(start[1]);
|
|
6491
|
+
inHeader = true;
|
|
6492
|
+
if (shared) open2(shared);
|
|
6493
|
+
continue;
|
|
6494
|
+
}
|
|
6495
|
+
if (inHeader) {
|
|
6496
|
+
const similarity = SIMILARITY.exec(line);
|
|
6497
|
+
if (similarity) {
|
|
6498
|
+
rename4.similarity = Number(similarity[1]);
|
|
6499
|
+
amend();
|
|
6500
|
+
continue;
|
|
6501
|
+
}
|
|
6502
|
+
if (line.startsWith("rename from ")) {
|
|
6503
|
+
rename4.renamedFrom = unquote(line.slice(12).trim());
|
|
6504
|
+
amend();
|
|
6505
|
+
continue;
|
|
6506
|
+
}
|
|
6507
|
+
if (line.startsWith("rename to ")) {
|
|
6508
|
+
open2(unquote(line.slice(10).trim()));
|
|
6509
|
+
continue;
|
|
6510
|
+
}
|
|
6511
|
+
if (line.startsWith("--- ")) {
|
|
6512
|
+
oldPath = sidePath(line.slice(4), shared);
|
|
6513
|
+
continue;
|
|
6514
|
+
}
|
|
6515
|
+
if (line.startsWith("+++ ")) {
|
|
6516
|
+
const path = sidePath(line.slice(4), shared) ?? oldPath;
|
|
6517
|
+
if (path) open2(path);
|
|
6518
|
+
else current = void 0;
|
|
6519
|
+
continue;
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
const hunk = HUNK.exec(line);
|
|
6523
|
+
if (!hunk) {
|
|
6524
|
+
if (!options.withLines || inHeader) continue;
|
|
6525
|
+
if (line.startsWith("+")) added?.lines?.push(line.slice(1));
|
|
6526
|
+
else if (line.startsWith("-")) removed?.lines?.push(line.slice(1));
|
|
6527
|
+
continue;
|
|
6528
|
+
}
|
|
6529
|
+
inHeader = false;
|
|
6530
|
+
added = void 0;
|
|
6531
|
+
removed = void 0;
|
|
6532
|
+
if (!current) continue;
|
|
6533
|
+
const [next, before] = hunksOf(hunk, options.withLines === true);
|
|
6534
|
+
added = next;
|
|
6535
|
+
removed = before;
|
|
6536
|
+
list();
|
|
6537
|
+
current.hunks.push(...before ? [next, before] : [next]);
|
|
6538
|
+
}
|
|
6539
|
+
close();
|
|
6540
|
+
return files;
|
|
6541
|
+
}
|
|
6542
|
+
function hunksOf(hunk, withLines) {
|
|
6543
|
+
const oldStart = Number(hunk[1]);
|
|
6544
|
+
const oldCount = hunk[2] === void 0 ? 1 : Number(hunk[2]);
|
|
6545
|
+
const newStart = Number(hunk[3]);
|
|
6546
|
+
const newCount = hunk[4] === void 0 ? 1 : Number(hunk[4]);
|
|
6547
|
+
const lines = withLines ? { lines: [] } : {};
|
|
6548
|
+
const added = newCount === 0 ? { ...point(newStart), ...lines } : { startLine: newStart, endLine: newStart + newCount - 1, ...lines };
|
|
6549
|
+
if (oldCount === 0) return [added];
|
|
6550
|
+
return [
|
|
6551
|
+
added,
|
|
6552
|
+
{
|
|
6553
|
+
startLine: oldStart,
|
|
6554
|
+
endLine: oldStart + oldCount - 1,
|
|
6555
|
+
side: "old",
|
|
6556
|
+
...withLines ? { lines: [] } : {}
|
|
6557
|
+
}
|
|
6558
|
+
];
|
|
6559
|
+
}
|
|
6560
|
+
function point(start) {
|
|
6561
|
+
const at2 = Math.max(1, start);
|
|
6562
|
+
return { startLine: at2, endLine: at2 };
|
|
6563
|
+
}
|
|
6564
|
+
function sidePath(raw, shared) {
|
|
6565
|
+
const text = unquote(raw.replace(/\t.*$/, "").trim());
|
|
6566
|
+
if (text === "/dev/null") return void 0;
|
|
6567
|
+
if (KNOWN_PREFIX.test(text)) return text.slice(2);
|
|
6568
|
+
return shared ?? text;
|
|
6569
|
+
}
|
|
6570
|
+
function sharedHeaderPath(rest) {
|
|
6571
|
+
const pair = splitHeaderPair(rest);
|
|
6572
|
+
if (!pair || pair[0] === pair[1]) return void 0;
|
|
6573
|
+
const from = unquote(pair[0]).split("/");
|
|
6574
|
+
const to = unquote(pair[1]).split("/");
|
|
6575
|
+
const shared = [];
|
|
6576
|
+
while (from.length > 1 && to.length > 1 && from.at(-1) === to.at(-1)) {
|
|
6577
|
+
shared.unshift(from.pop());
|
|
6578
|
+
to.pop();
|
|
6579
|
+
}
|
|
6580
|
+
return shared.length ? shared.join("/") : void 0;
|
|
6581
|
+
}
|
|
6582
|
+
function splitHeaderPair(rest) {
|
|
6583
|
+
if (rest.startsWith('"')) {
|
|
6584
|
+
const end = endOfQuoted(rest);
|
|
6585
|
+
if (end < 0 || rest[end + 1] !== " ") return void 0;
|
|
6586
|
+
return [rest.slice(0, end + 1), rest.slice(end + 2)];
|
|
6587
|
+
}
|
|
6588
|
+
const mid = (rest.length - 1) / 2;
|
|
6589
|
+
if (Number.isInteger(mid) && rest[mid] === " ") {
|
|
6590
|
+
return [rest.slice(0, mid), rest.slice(mid + 1)];
|
|
6591
|
+
}
|
|
6592
|
+
const at2 = rest.indexOf(" ");
|
|
6593
|
+
return at2 === -1 ? void 0 : [rest.slice(0, at2), rest.slice(at2 + 1)];
|
|
6594
|
+
}
|
|
6595
|
+
function endOfQuoted(text) {
|
|
6596
|
+
for (let at2 = 1; at2 < text.length; at2 += 1) {
|
|
6597
|
+
if (text[at2] === "\\") {
|
|
6598
|
+
at2 += 1;
|
|
6599
|
+
continue;
|
|
6600
|
+
}
|
|
6601
|
+
if (text[at2] === '"') return at2;
|
|
6602
|
+
}
|
|
6603
|
+
return -1;
|
|
6604
|
+
}
|
|
6605
|
+
var ESCAPES = {
|
|
6606
|
+
a: 7,
|
|
6607
|
+
b: 8,
|
|
6608
|
+
f: 12,
|
|
6609
|
+
n: 10,
|
|
6610
|
+
r: 13,
|
|
6611
|
+
t: 9,
|
|
6612
|
+
v: 11,
|
|
6613
|
+
'"': 34,
|
|
6614
|
+
"\\": 92
|
|
6615
|
+
};
|
|
6616
|
+
var OCTAL = /^[0-7]{3}/;
|
|
6617
|
+
var utf8 = new TextEncoder();
|
|
6618
|
+
function unquote(text) {
|
|
6619
|
+
if (text.length < 2 || !text.startsWith('"') || !text.endsWith('"')) {
|
|
6620
|
+
return text;
|
|
6621
|
+
}
|
|
6622
|
+
const body = text.slice(1, -1);
|
|
6623
|
+
const bytes = [];
|
|
6624
|
+
for (let at2 = 0; at2 < body.length; ) {
|
|
6625
|
+
const slash = body.indexOf("\\", at2);
|
|
6626
|
+
if (slash < 0) {
|
|
6627
|
+
bytes.push(...utf8.encode(body.slice(at2)));
|
|
6628
|
+
break;
|
|
6629
|
+
}
|
|
6630
|
+
if (slash > at2) bytes.push(...utf8.encode(body.slice(at2, slash)));
|
|
6631
|
+
const octal = OCTAL.exec(body.slice(slash + 1, slash + 4));
|
|
6632
|
+
if (octal) {
|
|
6633
|
+
bytes.push(Number.parseInt(octal[0], 8));
|
|
6634
|
+
at2 = slash + 4;
|
|
6635
|
+
continue;
|
|
6636
|
+
}
|
|
6637
|
+
const next = body[slash + 1];
|
|
6638
|
+
if (next === void 0) {
|
|
6639
|
+
bytes.push(ESCAPES["\\"]);
|
|
6640
|
+
break;
|
|
6641
|
+
}
|
|
6642
|
+
const mapped = ESCAPES[next];
|
|
6643
|
+
if (mapped === void 0) bytes.push(...utf8.encode(next));
|
|
6644
|
+
else bytes.push(mapped);
|
|
6645
|
+
at2 = slash + 2;
|
|
6646
|
+
}
|
|
6647
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
// src/commands/match/symbol-ranges.ts
|
|
6651
|
+
async function resolveSymbolRanges(repoRoot, files, records, offline = false) {
|
|
6652
|
+
const changed = new Set(files.map((file) => strip(file.filePath)));
|
|
6653
|
+
const wanted = [];
|
|
6654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6655
|
+
for (const record of records) {
|
|
6656
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
6657
|
+
if (!anchor.symbol || anchor.repo) continue;
|
|
6658
|
+
if (!changed.has(strip(anchor.file))) continue;
|
|
6659
|
+
const key2 = `${strip(anchor.file)}#${anchor.symbol}`;
|
|
6660
|
+
if (seen.has(key2)) continue;
|
|
6661
|
+
seen.add(key2);
|
|
6662
|
+
wanted.push(anchor);
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
if (!wanted.length) return [];
|
|
6666
|
+
const paths = [...new Set(wanted.map((anchor) => anchor.file))];
|
|
6667
|
+
const sources = await readAnchorFiles(paths, anchorFileReader(repoRoot));
|
|
6668
|
+
const resolvers = defaultAnchorResolvers({ offline });
|
|
6669
|
+
await prepareResolvers(resolvers, paths);
|
|
6670
|
+
const ranges = [];
|
|
6671
|
+
for (const anchor of wanted) {
|
|
6672
|
+
const read = sources.get(anchor.file);
|
|
6673
|
+
if (!read?.ok) continue;
|
|
6674
|
+
const outcome = resolveAnchorSpan(read.source, anchor, resolvers);
|
|
6675
|
+
if (!outcome.ok) continue;
|
|
6676
|
+
ranges.push({
|
|
6677
|
+
file: anchor.file,
|
|
6678
|
+
symbol: anchor.symbol,
|
|
6679
|
+
startLine: outcome.span.startLine,
|
|
6680
|
+
endLine: outcome.span.endLine
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
return ranges;
|
|
6684
|
+
}
|
|
6685
|
+
function strip(path) {
|
|
6686
|
+
return path.replace(/^\.\//, "");
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
// src/commands/match/command.ts
|
|
6690
|
+
var matchCommand = define({
|
|
6691
|
+
name: "match",
|
|
6692
|
+
tool: "kb_match",
|
|
6693
|
+
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
6694
|
+
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.",
|
|
6695
|
+
input: import_zod14.z.object({
|
|
6696
|
+
bundlePath,
|
|
6697
|
+
files: import_zod14.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
6698
|
+
symbolRanges: import_zod14.z.array(symbolRangeSchema).optional().describe(
|
|
6699
|
+
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
6700
|
+
),
|
|
6701
|
+
repoRoot: REPO_ROOT,
|
|
6702
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
6703
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
6704
|
+
),
|
|
6705
|
+
includeNonCurrent: import_zod14.z.boolean().optional().describe(
|
|
6706
|
+
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
6707
|
+
)
|
|
6708
|
+
}),
|
|
6709
|
+
fromArgv: async (argv, path, stdin) => {
|
|
6710
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6711
|
+
const range = argvFlag(argv, "--git");
|
|
6712
|
+
const base2 = {
|
|
6713
|
+
bundlePath: path,
|
|
6714
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
6715
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
6716
|
+
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
6717
|
+
};
|
|
6718
|
+
if (range !== void 0) {
|
|
6719
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
6720
|
+
if (!diff.ok) {
|
|
6721
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff.reason]}`);
|
|
6722
|
+
}
|
|
6723
|
+
return { ...base2, files: parseUnifiedDiff(diff.text) };
|
|
6724
|
+
}
|
|
6725
|
+
if (!argv.includes("--stdin")) {
|
|
6726
|
+
throw new KbMatchInputError(
|
|
6727
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
6728
|
+
);
|
|
6729
|
+
}
|
|
6730
|
+
return { ...base2, ...fromStdin(await stdin()) };
|
|
6731
|
+
},
|
|
6732
|
+
run: async ({ store }, {
|
|
6733
|
+
bundlePath: path,
|
|
6734
|
+
files,
|
|
6735
|
+
symbolRanges,
|
|
6736
|
+
repoRoot,
|
|
6737
|
+
offline,
|
|
6738
|
+
includeNonCurrent
|
|
6739
|
+
}) => {
|
|
6740
|
+
const records = await store.list(path);
|
|
6741
|
+
const ranges = symbolRanges ?? await resolveSymbolRanges(
|
|
6742
|
+
repoRoot ?? process.cwd(),
|
|
6743
|
+
files,
|
|
6744
|
+
records,
|
|
6745
|
+
offline === true
|
|
6746
|
+
);
|
|
6747
|
+
const index2 = symbolRangeIndex(ranges);
|
|
6748
|
+
return matchToDiff(files, records, { symbolRanges: ranges }).flatMap(
|
|
6749
|
+
(match) => project(match, index2, includeNonCurrent === true)
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6752
|
+
});
|
|
6753
|
+
var REFUSED = {
|
|
6754
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
6755
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
6756
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
6757
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
6758
|
+
};
|
|
6759
|
+
function fromStdin(text) {
|
|
6760
|
+
let payload;
|
|
6761
|
+
try {
|
|
6762
|
+
payload = JSON.parse(text);
|
|
6763
|
+
} catch {
|
|
6764
|
+
throw new KbMatchInputError("stdin is not JSON");
|
|
6765
|
+
}
|
|
6766
|
+
if (!Array.isArray(payload?.files)) {
|
|
6767
|
+
throw new KbMatchInputError("stdin needs a files array");
|
|
6768
|
+
}
|
|
6769
|
+
return {
|
|
6770
|
+
files: payload.files,
|
|
6771
|
+
...payload.symbolRanges !== void 0 ? { symbolRanges: payload.symbolRanges } : {}
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
function project(match, ranges, all) {
|
|
6775
|
+
const kept = all ? match.records : match.records.filter((hit) => hit.standing === "current");
|
|
6776
|
+
if (!kept.length) return [];
|
|
6777
|
+
const placed = kept.map((hit) => ({
|
|
6778
|
+
hit,
|
|
6779
|
+
at: placeOnHunk(hit.record, match.filePath, match.hunk, ranges)
|
|
6780
|
+
}));
|
|
6781
|
+
return [
|
|
6782
|
+
{
|
|
6783
|
+
filePath: match.filePath,
|
|
6784
|
+
hunk: match.hunk,
|
|
6785
|
+
// Over the records returned, not the ones matched: a hunk holding only
|
|
6786
|
+
// symbol-placed records is not `file` because a dropped one was.
|
|
6787
|
+
precision: placed.every(({ at: at2 }) => at2.kind === "symbol") ? "symbol" : "file",
|
|
6788
|
+
records: placed.map(({ hit, at: { anchor } }) => {
|
|
6789
|
+
const { frontmatter } = hit.record;
|
|
6790
|
+
return {
|
|
6791
|
+
conceptId: hit.record.conceptId,
|
|
6792
|
+
type: frontmatter.type,
|
|
6793
|
+
title: frontmatter.title ?? null,
|
|
6794
|
+
standing: hit.standing,
|
|
6795
|
+
status: frontmatter.strauss_status,
|
|
6796
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
6797
|
+
...frontmatter.strauss_materiality ? { materiality: frontmatter.strauss_materiality } : {},
|
|
6798
|
+
...frontmatter.strauss_confidence ? { confidence: frontmatter.strauss_confidence } : {},
|
|
6799
|
+
...frontmatter.tags?.length ? { tags: frontmatter.tags } : {},
|
|
6800
|
+
...anchor ? { anchor } : {}
|
|
6801
|
+
};
|
|
6802
|
+
})
|
|
6803
|
+
}
|
|
6804
|
+
];
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6807
|
+
// src/commands/classify.ts
|
|
6808
|
+
var classifyFileSchema = diffFileSchema.extend({
|
|
6809
|
+
hunks: import_zod15.z.array(
|
|
6810
|
+
diffHunkSchema.extend({ lines: import_zod15.z.array(import_zod15.z.string()).optional() })
|
|
6811
|
+
),
|
|
6812
|
+
renamedFrom: import_zod15.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
6813
|
+
similarity: import_zod15.z.number().min(0).max(100).optional()
|
|
6814
|
+
});
|
|
6815
|
+
var classifyCommand = define({
|
|
6816
|
+
name: "classify",
|
|
6817
|
+
tool: "kb_classify",
|
|
6818
|
+
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
6819
|
+
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.",
|
|
6820
|
+
input: import_zod15.z.object({
|
|
6821
|
+
bundlePath,
|
|
6822
|
+
files: import_zod15.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
6823
|
+
repoRoot: REPO_ROOT,
|
|
6824
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
6825
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
6826
|
+
)
|
|
6827
|
+
}),
|
|
6828
|
+
fromArgv: async (argv, path, stdin) => {
|
|
6829
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
6830
|
+
const range = argvFlag(argv, "--git");
|
|
6831
|
+
const base2 = {
|
|
6832
|
+
bundlePath: path,
|
|
6833
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
6834
|
+
...argv.includes("--offline") ? { offline: true } : {}
|
|
6835
|
+
};
|
|
6836
|
+
if (range !== void 0) {
|
|
6837
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
6838
|
+
if (!diff.ok) {
|
|
6839
|
+
throw new KbClassifyInputError(
|
|
6840
|
+
`--git ${range} ${REFUSED2[diff.reason]}`
|
|
6841
|
+
);
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
...base2,
|
|
6845
|
+
files: parseUnifiedDiff(diff.text, {
|
|
6846
|
+
keepEmpty: true,
|
|
6847
|
+
withLines: true
|
|
6848
|
+
})
|
|
6849
|
+
};
|
|
6850
|
+
}
|
|
6851
|
+
if (!argv.includes("--stdin")) {
|
|
6852
|
+
throw new KbClassifyInputError(
|
|
6853
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
6854
|
+
);
|
|
6855
|
+
}
|
|
6856
|
+
return { ...base2, files: fromStdin2(await stdin()) };
|
|
6857
|
+
},
|
|
6858
|
+
run: async ({ store }, { bundlePath: path, files, repoRoot, offline }) => {
|
|
6859
|
+
const records = await store.list(path);
|
|
6860
|
+
const root = repoRoot ?? process.cwd();
|
|
6861
|
+
const withHeaders = await mapLimit2(files, READERS, async (file) => ({
|
|
6862
|
+
...file,
|
|
6863
|
+
header: await header(root, file)
|
|
6864
|
+
}));
|
|
6865
|
+
const symbolRanges = await resolveSymbolRanges(
|
|
6866
|
+
root,
|
|
6867
|
+
files,
|
|
6868
|
+
records,
|
|
6869
|
+
offline === true
|
|
6870
|
+
);
|
|
6871
|
+
return { files: classifyDiff(withHeaders, { records, symbolRanges }) };
|
|
6872
|
+
},
|
|
6873
|
+
render: (result) => renderClassify(result)
|
|
6874
|
+
});
|
|
6875
|
+
var HEADER_BYTES = 65536;
|
|
6876
|
+
var READERS = 16;
|
|
6877
|
+
async function header(root, file) {
|
|
6878
|
+
if (!filePathIsSafe(file.filePath)) return void 0;
|
|
6879
|
+
let handle;
|
|
6880
|
+
try {
|
|
6881
|
+
handle = await (0, import_promises10.open)((0, import_node_path12.join)(root, file.filePath), "r");
|
|
6882
|
+
const buffer = import_node_buffer.Buffer.alloc(HEADER_BYTES);
|
|
6883
|
+
const { bytesRead } = await handle.read(buffer, 0, HEADER_BYTES, 0);
|
|
6884
|
+
return buffer.toString("utf8", 0, bytesRead).split("\n").slice(0, HEADER_LINES);
|
|
6885
|
+
} catch {
|
|
6886
|
+
return void 0;
|
|
6887
|
+
} finally {
|
|
6888
|
+
await handle?.close();
|
|
6889
|
+
}
|
|
6890
|
+
}
|
|
6891
|
+
async function mapLimit2(items, limit, run) {
|
|
6892
|
+
const out = Array.from({ length: items.length });
|
|
6893
|
+
let next = 0;
|
|
6894
|
+
const worker = async () => {
|
|
6895
|
+
while (next < items.length) {
|
|
6896
|
+
const at2 = next;
|
|
6897
|
+
next += 1;
|
|
6898
|
+
out[at2] = await run(items[at2]);
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6901
|
+
await Promise.all(
|
|
6902
|
+
Array.from({ length: Math.min(limit, items.length) }, () => worker())
|
|
6903
|
+
);
|
|
6904
|
+
return out;
|
|
6905
|
+
}
|
|
6906
|
+
var REFUSED2 = {
|
|
6907
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
6908
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
6909
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
6910
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
6911
|
+
};
|
|
6912
|
+
function fromStdin2(text) {
|
|
6913
|
+
let payload;
|
|
6914
|
+
try {
|
|
6915
|
+
payload = JSON.parse(text);
|
|
6916
|
+
} catch {
|
|
6917
|
+
throw new KbClassifyInputError("stdin is not JSON");
|
|
6918
|
+
}
|
|
6919
|
+
if (!Array.isArray(payload?.files)) {
|
|
6920
|
+
throw new KbClassifyInputError("stdin needs a files array");
|
|
6921
|
+
}
|
|
6922
|
+
return payload.files;
|
|
6923
|
+
}
|
|
6924
|
+
function renderClassify(result) {
|
|
6925
|
+
const width2 = Math.max(
|
|
6926
|
+
0,
|
|
6927
|
+
...result.files.map((file) => file.class.length)
|
|
6928
|
+
);
|
|
6929
|
+
return result.files.map(
|
|
6930
|
+
(file) => `${file.class.padEnd(width2)} ${file.filePath} (${file.reason})`
|
|
6931
|
+
).join("\n");
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
// src/commands/context.ts
|
|
6935
|
+
var import_zod16 = require("zod");
|
|
6936
|
+
var contextCommand = define({
|
|
6937
|
+
name: "context",
|
|
6938
|
+
tool: "kb_context",
|
|
6939
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
6940
|
+
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.",
|
|
6941
|
+
input: import_zod16.z.object({
|
|
6942
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
6943
|
+
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
6944
|
+
),
|
|
6945
|
+
fullUnderTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
6946
|
+
"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."
|
|
6947
|
+
),
|
|
6948
|
+
profile: import_zod16.z.string().optional().describe(
|
|
6949
|
+
"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."
|
|
6950
|
+
),
|
|
6951
|
+
excludeTags: import_zod16.z.array(import_zod16.z.string().min(1)).optional().describe(
|
|
6952
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
6953
|
+
),
|
|
6954
|
+
format: import_zod16.z.enum(["markdown", "json"]).optional().describe(
|
|
6955
|
+
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
6956
|
+
),
|
|
6957
|
+
event: import_zod16.z.string().optional().describe(
|
|
6958
|
+
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
6959
|
+
)
|
|
6960
|
+
}),
|
|
6961
|
+
fromArgv: (argv) => {
|
|
6962
|
+
const budget = argvFlag(argv, "--budget");
|
|
6963
|
+
const fullUnder = argvFlag(argv, "--full-under");
|
|
6964
|
+
const profile = argvFlag(argv, "--profile");
|
|
6965
|
+
const format = argvFlag(argv, "--format");
|
|
6966
|
+
const event = argvFlag(argv, "--event");
|
|
6967
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
6968
|
+
return {
|
|
6969
|
+
...budget ? { budgetTokens: Number(budget) } : {},
|
|
6970
|
+
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
6971
|
+
...profile ? { profile } : {},
|
|
6972
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
6973
|
+
...format ? { format } : {},
|
|
6974
|
+
...event ? { event } : {}
|
|
6975
|
+
};
|
|
6976
|
+
},
|
|
6977
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
6978
|
+
const result = await buildContext(store, process.cwd(), {
|
|
6979
|
+
...budgetTokens ? { budgetTokens } : {},
|
|
6980
|
+
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
6981
|
+
...profile ? { profile } : {},
|
|
6982
|
+
...excludeTags ? { excludeTags } : {},
|
|
6983
|
+
// Degradations — a full pin that could not fit, a refused block — go
|
|
6984
|
+
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
6985
|
+
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
6986
|
+
// see budget pressure without reading injected context.
|
|
6987
|
+
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
6988
|
+
`)
|
|
6989
|
+
});
|
|
6990
|
+
if (!result.block) return "";
|
|
6991
|
+
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
6992
|
+
}
|
|
6993
|
+
});
|
|
6994
|
+
|
|
6995
|
+
// src/commands/doctor.ts
|
|
6996
|
+
var import_zod18 = require("zod");
|
|
6997
|
+
|
|
6998
|
+
// src/commands/reassess.ts
|
|
6999
|
+
var import_zod17 = require("zod");
|
|
7000
|
+
var reassessCommand = define({
|
|
7001
|
+
name: "reassess",
|
|
7002
|
+
tool: "kb_reassess",
|
|
7003
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
7004
|
+
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.",
|
|
7005
|
+
input: import_zod17.z.object({
|
|
7006
|
+
bundlePath,
|
|
7007
|
+
conceptId,
|
|
7008
|
+
repoRoot: REPO_ROOT,
|
|
7009
|
+
withDiff: import_zod17.z.boolean().optional().describe(
|
|
7010
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
7011
|
+
)
|
|
7012
|
+
}),
|
|
7013
|
+
fromArgv: (argv, path) => {
|
|
7014
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
7015
|
+
return {
|
|
7016
|
+
bundlePath: path,
|
|
7017
|
+
conceptId: argv[1],
|
|
7018
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
7019
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
7020
|
+
};
|
|
7021
|
+
},
|
|
7022
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
7023
|
+
const root = repoRoot ?? process.cwd();
|
|
7024
|
+
const bundle = await store.list(path);
|
|
7025
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
7026
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
7027
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
7028
|
+
const entries = drift?.get(id) ?? [];
|
|
7029
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
7030
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
7031
|
+
}
|
|
7032
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
7033
|
+
(hit) => hit.record.conceptId === id
|
|
7034
|
+
)?.standing;
|
|
7035
|
+
const impact2 = await store.impact(path, id);
|
|
7036
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
7037
|
+
...withDiff ? { withDiff: true } : {},
|
|
5835
7038
|
impact: impact2,
|
|
5836
7039
|
...standing ? { standing } : {}
|
|
5837
7040
|
});
|
|
@@ -5846,7 +7049,10 @@ var reassessCommand = define({
|
|
|
5846
7049
|
relocated.set(found.anchor, {
|
|
5847
7050
|
...found.anchor,
|
|
5848
7051
|
file: to.file,
|
|
5849
|
-
...to.symbol ? { symbol: to.symbol } : {}
|
|
7052
|
+
...to.symbol ? { symbol: to.symbol } : {},
|
|
7053
|
+
// A span is the anchor's whole address, so relocating it means
|
|
7054
|
+
// moving the line range the same code now occupies.
|
|
7055
|
+
...found.anchor.span ? { span: { start: to.startLine, end: to.endLine } } : {}
|
|
5850
7056
|
});
|
|
5851
7057
|
rebaselined.push({
|
|
5852
7058
|
file: found.anchor.file,
|
|
@@ -5945,13 +7151,13 @@ function at(file, symbol) {
|
|
|
5945
7151
|
}
|
|
5946
7152
|
|
|
5947
7153
|
// src/commands/doctor.ts
|
|
5948
|
-
var days = (what, fallback) =>
|
|
7154
|
+
var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5949
7155
|
var doctorCommand = define({
|
|
5950
7156
|
name: "doctor",
|
|
5951
7157
|
tool: "kb_doctor",
|
|
5952
7158
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5953
7159
|
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:
|
|
7160
|
+
input: import_zod18.z.object({
|
|
5955
7161
|
bundlePath,
|
|
5956
7162
|
repoRoot: REPO_ROOT,
|
|
5957
7163
|
expiringDays: days(
|
|
@@ -5966,16 +7172,16 @@ var doctorCommand = define({
|
|
|
5966
7172
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5967
7173
|
DEFAULT_AGING_DAYS
|
|
5968
7174
|
),
|
|
5969
|
-
offline:
|
|
7175
|
+
offline: import_zod18.z.boolean().optional().describe(
|
|
5970
7176
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5971
7177
|
),
|
|
5972
|
-
strict:
|
|
7178
|
+
strict: import_zod18.z.boolean().optional().describe(
|
|
5973
7179
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5974
7180
|
),
|
|
5975
|
-
drifted:
|
|
7181
|
+
drifted: import_zod18.z.boolean().optional().describe(
|
|
5976
7182
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5977
7183
|
),
|
|
5978
|
-
withDiff:
|
|
7184
|
+
withDiff: import_zod18.z.boolean().optional().describe(
|
|
5979
7185
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5980
7186
|
)
|
|
5981
7187
|
}),
|
|
@@ -6031,7 +7237,7 @@ var doctorCommand = define({
|
|
|
6031
7237
|
...hints.length ? { hints } : {}
|
|
6032
7238
|
};
|
|
6033
7239
|
}
|
|
6034
|
-
const
|
|
7240
|
+
const standings2 = new Map(
|
|
6035
7241
|
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
6036
7242
|
hit.record.conceptId,
|
|
6037
7243
|
hit.standing
|
|
@@ -6045,7 +7251,7 @@ var doctorCommand = define({
|
|
|
6045
7251
|
(entry) => entry.conceptId === found.conceptId
|
|
6046
7252
|
);
|
|
6047
7253
|
if (!record) continue;
|
|
6048
|
-
const standing =
|
|
7254
|
+
const standing = standings2.get(record.conceptId);
|
|
6049
7255
|
const built = await reassessPacket(
|
|
6050
7256
|
repoRoot ?? process.cwd(),
|
|
6051
7257
|
record,
|
|
@@ -6082,15 +7288,16 @@ var doctorCommand = define({
|
|
|
6082
7288
|
});
|
|
6083
7289
|
function render2(result) {
|
|
6084
7290
|
if (result.packets) return renderPackets(result);
|
|
6085
|
-
const { thresholds } = result;
|
|
7291
|
+
const { thresholds, anchorResolvers: counts } = result;
|
|
6086
7292
|
const lines = [
|
|
6087
7293
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
6088
7294
|
`records: ${result.recordCount}`,
|
|
6089
7295
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
6090
7296
|
`checked: ${result.checkedAt}`,
|
|
6091
|
-
...
|
|
6092
|
-
|
|
6093
|
-
|
|
7297
|
+
...counts.total ? [anchorLine(counts)] : [],
|
|
7298
|
+
// Its own line: an old-side anchor may name a whole file, which no
|
|
7299
|
+
// resolver bucket and no `total` counts.
|
|
7300
|
+
...counts.oldSide ? [`old-side anchors: ${counts.oldSide}`] : [],
|
|
6094
7301
|
""
|
|
6095
7302
|
];
|
|
6096
7303
|
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
@@ -6115,6 +7322,14 @@ function render2(result) {
|
|
|
6115
7322
|
);
|
|
6116
7323
|
return lines.join("\n");
|
|
6117
7324
|
}
|
|
7325
|
+
function anchorLine(counts) {
|
|
7326
|
+
const parts = [
|
|
7327
|
+
`${counts.treeSitter} tree-sitter`,
|
|
7328
|
+
`${counts.regex} regex`,
|
|
7329
|
+
...counts.span ? [`${counts.span} span`] : []
|
|
7330
|
+
];
|
|
7331
|
+
return `anchors: ${counts.total} hashed \u2014 ${parts.join(", ")}`;
|
|
7332
|
+
}
|
|
6118
7333
|
function renderPackets(result) {
|
|
6119
7334
|
const packets = result.packets ?? [];
|
|
6120
7335
|
const lines = [
|
|
@@ -6140,20 +7355,155 @@ function renderPackets(result) {
|
|
|
6140
7355
|
return lines.join("\n");
|
|
6141
7356
|
}
|
|
6142
7357
|
|
|
7358
|
+
// src/commands/export.ts
|
|
7359
|
+
var import_promises11 = require("fs/promises");
|
|
7360
|
+
var import_node_path13 = require("path");
|
|
7361
|
+
var import_zod19 = require("zod");
|
|
7362
|
+
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
7363
|
+
var MARKER = "<!-- strauss-kb export: ";
|
|
7364
|
+
var exportCommand = define({
|
|
7365
|
+
name: "export",
|
|
7366
|
+
tool: "kb_export",
|
|
7367
|
+
usage: "export --format madr --to <dir>",
|
|
7368
|
+
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.",
|
|
7369
|
+
input: import_zod19.z.object({
|
|
7370
|
+
bundlePath,
|
|
7371
|
+
format: import_zod19.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
7372
|
+
to: import_zod19.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
7373
|
+
}),
|
|
7374
|
+
fromArgv: (argv, path) => ({
|
|
7375
|
+
bundlePath: path,
|
|
7376
|
+
format: argvFlag(argv, "--format"),
|
|
7377
|
+
to: argvFlag(argv, "--to")
|
|
7378
|
+
}),
|
|
7379
|
+
run: async ({ store }, { bundlePath: path, to }) => {
|
|
7380
|
+
const bundle = await store.list(path);
|
|
7381
|
+
const decisions = selectDecisions(bundle).sort(
|
|
7382
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
7383
|
+
);
|
|
7384
|
+
const adjudicated = new Map(
|
|
7385
|
+
adjudicate(decisions, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
7386
|
+
);
|
|
7387
|
+
await (0, import_promises11.mkdir)(to, { recursive: true });
|
|
7388
|
+
const taken = await existingFiles(to);
|
|
7389
|
+
let next = Math.max(0, ...[...taken.values()].map((row) => row.number)) + 1;
|
|
7390
|
+
const exported = [];
|
|
7391
|
+
const foreign = [];
|
|
7392
|
+
for (const record of decisions) {
|
|
7393
|
+
const slug = record.conceptId.slice(record.conceptId.indexOf(".") + 1);
|
|
7394
|
+
const held = taken.get(slug);
|
|
7395
|
+
if (held && !held.ours) {
|
|
7396
|
+
foreign.push({ conceptId: record.conceptId, file: held.file });
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
const number = held?.number ?? next++;
|
|
7400
|
+
const file = `${String(number).padStart(4, "0")}-${slug}.md`;
|
|
7401
|
+
const status = statusLine(adjudicated.get(record.conceptId));
|
|
7402
|
+
await publish((0, import_node_path13.join)(to, file), renderMadr(record, status));
|
|
7403
|
+
exported.push({ conceptId: record.conceptId, file, status });
|
|
7404
|
+
}
|
|
7405
|
+
return { to, format: "madr", exported, foreign };
|
|
7406
|
+
},
|
|
7407
|
+
render: (result) => {
|
|
7408
|
+
const { exported, foreign, to } = result;
|
|
7409
|
+
return [
|
|
7410
|
+
`Wrote ${exported.length} MADR file${exported.length === 1 ? "" : "s"} to ${to}.`,
|
|
7411
|
+
...exported.map(
|
|
7412
|
+
(entry) => `- ${entry.file} ${entry.conceptId} [${entry.status}]`
|
|
7413
|
+
),
|
|
7414
|
+
...foreign.map(
|
|
7415
|
+
(entry) => `- skipped ${entry.conceptId}: ${entry.file} was not written by export`
|
|
7416
|
+
)
|
|
7417
|
+
].join("\n");
|
|
7418
|
+
}
|
|
7419
|
+
});
|
|
7420
|
+
async function publish(target, contents) {
|
|
7421
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
7422
|
+
await (0, import_promises11.writeFile)(staging, contents, "utf8");
|
|
7423
|
+
try {
|
|
7424
|
+
await (0, import_promises11.rename)(staging, target);
|
|
7425
|
+
} catch (error) {
|
|
7426
|
+
await (0, import_promises11.unlink)(staging).catch(() => void 0);
|
|
7427
|
+
throw error;
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
async function existingFiles(to) {
|
|
7431
|
+
const names = await (0, import_promises11.readdir)(to).catch(() => []);
|
|
7432
|
+
const taken = /* @__PURE__ */ new Map();
|
|
7433
|
+
for (const name of names.sort()) {
|
|
7434
|
+
const [, number, slug] = NUMBERED.exec(name) ?? [];
|
|
7435
|
+
if (!number || !slug) continue;
|
|
7436
|
+
const text = await (0, import_promises11.readFile)((0, import_node_path13.join)(to, name), "utf8").catch(() => "");
|
|
7437
|
+
taken.set(slug, {
|
|
7438
|
+
file: name,
|
|
7439
|
+
number: Number(number),
|
|
7440
|
+
ours: text.includes(MARKER)
|
|
7441
|
+
});
|
|
7442
|
+
}
|
|
7443
|
+
return taken;
|
|
7444
|
+
}
|
|
7445
|
+
function statusLine(hit) {
|
|
7446
|
+
const status = hit?.record.frontmatter.strauss_status ?? "draft";
|
|
7447
|
+
if (status !== "superseded") return status;
|
|
7448
|
+
const by = (hit?.heads ?? []).map((head) => head.conceptId);
|
|
7449
|
+
return by.length ? `superseded by ${by.join(", ")}` : "superseded";
|
|
7450
|
+
}
|
|
7451
|
+
function renderMadr(record, status) {
|
|
7452
|
+
const sections = bodySections(record.body);
|
|
7453
|
+
const blocks = [
|
|
7454
|
+
`# ${record.frontmatter.title ?? record.conceptId}`,
|
|
7455
|
+
"## Status",
|
|
7456
|
+
status
|
|
7457
|
+
];
|
|
7458
|
+
push(blocks, "Context and Problem Statement", record.frontmatter.description);
|
|
7459
|
+
push(blocks, "Considered Options", sections.get("Rejected"));
|
|
7460
|
+
push(blocks, "Decision Outcome", sections.get("Decision"));
|
|
7461
|
+
push(blocks, "Consequences", sections.get("Impact"));
|
|
7462
|
+
blocks.push(`${MARKER}${record.conceptId} -->`);
|
|
7463
|
+
return `${blocks.join("\n\n")}
|
|
7464
|
+
`;
|
|
7465
|
+
}
|
|
7466
|
+
function push(blocks, heading, text) {
|
|
7467
|
+
if (text?.trim()) blocks.push(`## ${heading}`, text.trim());
|
|
7468
|
+
}
|
|
7469
|
+
function bodySections(body) {
|
|
7470
|
+
const generated = new RegExp(
|
|
7471
|
+
`^(?:(?:${Object.values(LINK_RELS).map((spec) => spec.phrase).join("|")}) \\[[^\\]]+\\]\\([^)]+\\.md\\)\\.|\\[\\^[^\\]]+\\]: .*)$`
|
|
7472
|
+
);
|
|
7473
|
+
const sections = /* @__PURE__ */ new Map();
|
|
7474
|
+
let heading = null;
|
|
7475
|
+
let lines = [];
|
|
7476
|
+
const flush = () => {
|
|
7477
|
+
if (heading) sections.set(heading, lines.join("\n").trim());
|
|
7478
|
+
};
|
|
7479
|
+
for (const line of body.split("\n")) {
|
|
7480
|
+
const match = /^## (.+?)\s*$/.exec(line);
|
|
7481
|
+
if (match) {
|
|
7482
|
+
flush();
|
|
7483
|
+
heading = match[1] ?? null;
|
|
7484
|
+
lines = [];
|
|
7485
|
+
} else if (heading && !generated.test(line)) {
|
|
7486
|
+
lines.push(line);
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
flush();
|
|
7490
|
+
return sections;
|
|
7491
|
+
}
|
|
7492
|
+
|
|
6143
7493
|
// src/commands/impact.ts
|
|
6144
|
-
var
|
|
7494
|
+
var import_zod20 = require("zod");
|
|
6145
7495
|
var impactCommand = define({
|
|
6146
7496
|
name: "impact",
|
|
6147
7497
|
tool: "kb_impact",
|
|
6148
7498
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
6149
7499
|
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:
|
|
7500
|
+
input: import_zod20.z.object({
|
|
6151
7501
|
bundlePath,
|
|
6152
7502
|
conceptId,
|
|
6153
|
-
depth:
|
|
7503
|
+
depth: import_zod20.z.number().int().positive().optional().describe(
|
|
6154
7504
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
6155
7505
|
),
|
|
6156
|
-
rels:
|
|
7506
|
+
rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
6157
7507
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
6158
7508
|
)
|
|
6159
7509
|
}),
|
|
@@ -6174,15 +7524,15 @@ var impactCommand = define({
|
|
|
6174
7524
|
});
|
|
6175
7525
|
|
|
6176
7526
|
// src/commands/list.ts
|
|
6177
|
-
var
|
|
7527
|
+
var import_zod21 = require("zod");
|
|
6178
7528
|
var listCommand = define({
|
|
6179
7529
|
name: "list",
|
|
6180
7530
|
tool: "kb_list",
|
|
6181
7531
|
usage: "list [type] [--tag T]...",
|
|
6182
7532
|
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
6183
|
-
input:
|
|
7533
|
+
input: import_zod21.z.object({
|
|
6184
7534
|
bundlePath,
|
|
6185
|
-
type:
|
|
7535
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
6186
7536
|
tags: TAGS
|
|
6187
7537
|
}),
|
|
6188
7538
|
fromArgv: (argv, path) => {
|
|
@@ -6206,17 +7556,17 @@ var listCommand = define({
|
|
|
6206
7556
|
});
|
|
6207
7557
|
|
|
6208
7558
|
// src/commands/load.ts
|
|
6209
|
-
var
|
|
7559
|
+
var import_zod22 = require("zod");
|
|
6210
7560
|
var loadCommand = define({
|
|
6211
7561
|
name: "load",
|
|
6212
7562
|
tool: "kb_load",
|
|
6213
7563
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6214
7564
|
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:
|
|
7565
|
+
input: import_zod22.z.object({
|
|
6216
7566
|
bundlePath,
|
|
6217
|
-
type:
|
|
6218
|
-
budgetTokens:
|
|
6219
|
-
all:
|
|
7567
|
+
type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
|
|
7568
|
+
budgetTokens: import_zod22.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
7569
|
+
all: import_zod22.z.boolean().optional().describe(
|
|
6220
7570
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6221
7571
|
),
|
|
6222
7572
|
repoRoot: REPO_ROOT
|
|
@@ -6258,25 +7608,25 @@ var loadCommand = define({
|
|
|
6258
7608
|
});
|
|
6259
7609
|
|
|
6260
7610
|
// src/commands/log.ts
|
|
6261
|
-
var
|
|
7611
|
+
var import_zod23 = require("zod");
|
|
6262
7612
|
var logCommand = define({
|
|
6263
7613
|
name: "log",
|
|
6264
7614
|
tool: "kb_log",
|
|
6265
7615
|
usage: "log",
|
|
6266
7616
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6267
|
-
input:
|
|
7617
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
6268
7618
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6269
7619
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6270
7620
|
});
|
|
6271
7621
|
|
|
6272
7622
|
// src/commands/no-decision.ts
|
|
6273
|
-
var
|
|
7623
|
+
var import_zod24 = require("zod");
|
|
6274
7624
|
var noDecisionCommand = define({
|
|
6275
7625
|
name: "no-decision",
|
|
6276
7626
|
tool: "kb_no_decision",
|
|
6277
7627
|
usage: "no-decision <reason...>",
|
|
6278
7628
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6279
|
-
input:
|
|
7629
|
+
input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
|
|
6280
7630
|
fromArgv: (argv, path) => ({
|
|
6281
7631
|
bundlePath: path,
|
|
6282
7632
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6293,20 +7643,20 @@ var noDecisionCommand = define({
|
|
|
6293
7643
|
});
|
|
6294
7644
|
|
|
6295
7645
|
// src/commands/pack.ts
|
|
6296
|
-
var
|
|
7646
|
+
var import_zod25 = require("zod");
|
|
6297
7647
|
var packCommand = define({
|
|
6298
7648
|
name: "pack",
|
|
6299
7649
|
tool: "kb_pack",
|
|
6300
7650
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6301
7651
|
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:
|
|
7652
|
+
input: import_zod25.z.object({
|
|
6303
7653
|
bundlePath,
|
|
6304
7654
|
conceptId,
|
|
6305
|
-
hops:
|
|
6306
|
-
maxNodes:
|
|
7655
|
+
hops: import_zod25.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
7656
|
+
maxNodes: import_zod25.z.number().int().positive().optional().describe(
|
|
6307
7657
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6308
7658
|
),
|
|
6309
|
-
budgetTokens:
|
|
7659
|
+
budgetTokens: import_zod25.z.number().int().positive().optional().describe(
|
|
6310
7660
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6311
7661
|
)
|
|
6312
7662
|
}),
|
|
@@ -6393,22 +7743,22 @@ function warningLabel(warning) {
|
|
|
6393
7743
|
}
|
|
6394
7744
|
|
|
6395
7745
|
// src/commands/pin.ts
|
|
6396
|
-
var
|
|
7746
|
+
var import_zod26 = require("zod");
|
|
6397
7747
|
var pinCommand = define({
|
|
6398
7748
|
name: "pin",
|
|
6399
7749
|
tool: "kb_pin",
|
|
6400
7750
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6401
7751
|
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:
|
|
7752
|
+
input: import_zod26.z.object({
|
|
6403
7753
|
bundlePath,
|
|
6404
|
-
mode:
|
|
7754
|
+
mode: import_zod26.z.enum(["full", "index"]).optional().describe(
|
|
6405
7755
|
"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
7756
|
),
|
|
6407
|
-
profiles:
|
|
6408
|
-
layer:
|
|
7757
|
+
profiles: import_zod26.z.array(import_zod26.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
7758
|
+
layer: import_zod26.z.enum(["project", "local", "user"]).optional().describe(
|
|
6409
7759
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6410
7760
|
),
|
|
6411
|
-
frozen:
|
|
7761
|
+
frozen: import_zod26.z.boolean().optional().describe(
|
|
6412
7762
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6413
7763
|
)
|
|
6414
7764
|
}),
|
|
@@ -6437,29 +7787,305 @@ var pinCommand = define({
|
|
|
6437
7787
|
});
|
|
6438
7788
|
|
|
6439
7789
|
// src/commands/pins.ts
|
|
6440
|
-
var
|
|
7790
|
+
var import_zod27 = require("zod");
|
|
6441
7791
|
var pinsCommand = define({
|
|
6442
7792
|
name: "pins",
|
|
6443
7793
|
tool: "kb_pins",
|
|
6444
7794
|
usage: "pins",
|
|
6445
7795
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6446
|
-
input:
|
|
7796
|
+
input: import_zod27.z.object({}),
|
|
6447
7797
|
fromArgv: () => ({}),
|
|
6448
7798
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6449
7799
|
});
|
|
6450
7800
|
|
|
7801
|
+
// src/commands/promote/command.ts
|
|
7802
|
+
var import_node_path14 = require("path");
|
|
7803
|
+
|
|
7804
|
+
// src/commands/promote/carry.ts
|
|
7805
|
+
var PROMOTION_SOURCE_ID = "promoted";
|
|
7806
|
+
var CARRIED_STATUS = {
|
|
7807
|
+
draft: "accepted",
|
|
7808
|
+
proposed: "accepted",
|
|
7809
|
+
accepted: "accepted",
|
|
7810
|
+
open: "open",
|
|
7811
|
+
resolved: "resolved",
|
|
7812
|
+
rejected: "rejected",
|
|
7813
|
+
superseded: "superseded"
|
|
7814
|
+
};
|
|
7815
|
+
function carry(record, promoted, source) {
|
|
7816
|
+
const {
|
|
7817
|
+
type: _type,
|
|
7818
|
+
// Both name records in the source base, and supersession in the target is
|
|
7819
|
+
// a separate question from whether this record belongs there at all.
|
|
7820
|
+
strauss_supersedes: _supersedes,
|
|
7821
|
+
strauss_superseded_by: _supersededBy,
|
|
7822
|
+
// A check run against the source repository, which the target never saw.
|
|
7823
|
+
verified: _verified,
|
|
7824
|
+
...rest
|
|
7825
|
+
} = record.frontmatter;
|
|
7826
|
+
const links = rest.strauss_links ?? [];
|
|
7827
|
+
const kept = links.filter((link2) => promoted.has(link2.target));
|
|
7828
|
+
const dropped = links.filter((link2) => !promoted.has(link2.target));
|
|
7829
|
+
const tags = (rest.tags ?? []).filter((tag) => !isReviewTag(tag));
|
|
7830
|
+
const frontmatter = {
|
|
7831
|
+
...rest,
|
|
7832
|
+
strauss_status: CARRIED_STATUS[rest.strauss_status]
|
|
7833
|
+
};
|
|
7834
|
+
setOrDrop(frontmatter, "tags", tags);
|
|
7835
|
+
setOrDrop(frontmatter, "strauss_links", kept);
|
|
7836
|
+
let body = withoutLinkSentences(record.body, dropped);
|
|
7837
|
+
if (source) {
|
|
7838
|
+
frontmatter.sources = [
|
|
7839
|
+
...(rest.sources ?? []).filter(
|
|
7840
|
+
(entry) => entry.id !== PROMOTION_SOURCE_ID
|
|
7841
|
+
),
|
|
7842
|
+
{ id: PROMOTION_SOURCE_ID, resource: source }
|
|
7843
|
+
];
|
|
7844
|
+
body = `${stripFootnote(body).trimEnd()}
|
|
7845
|
+
|
|
7846
|
+
[^${PROMOTION_SOURCE_ID}]: ${source}
|
|
7847
|
+
`;
|
|
7848
|
+
}
|
|
7849
|
+
return {
|
|
7850
|
+
frontmatter,
|
|
7851
|
+
body,
|
|
7852
|
+
droppedLinks: dropped.map(({ target, rel }) => ({ target, rel }))
|
|
7853
|
+
};
|
|
7854
|
+
}
|
|
7855
|
+
function isReviewTag(tag) {
|
|
7856
|
+
return tag === "review" || tag.startsWith("review:");
|
|
7857
|
+
}
|
|
7858
|
+
function withoutLinkSentences(body, dropped) {
|
|
7859
|
+
const sentences = new Set(
|
|
7860
|
+
dropped.filter((link2) => isKbLinkRel(link2.rel)).map(
|
|
7861
|
+
(link2) => `${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
7862
|
+
)
|
|
7863
|
+
);
|
|
7864
|
+
if (!sentences.size) return body;
|
|
7865
|
+
return body.split("\n\n").filter((block) => !sentences.has(block.trim())).join("\n\n");
|
|
7866
|
+
}
|
|
7867
|
+
function stripFootnote(body) {
|
|
7868
|
+
return body.split("\n").filter((line) => !line.startsWith(`[^${PROMOTION_SOURCE_ID}]: `)).join("\n");
|
|
7869
|
+
}
|
|
7870
|
+
function setOrDrop(frontmatter, key2, value) {
|
|
7871
|
+
if (value.length) frontmatter[key2] = value;
|
|
7872
|
+
else delete frontmatter[key2];
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7875
|
+
// src/commands/promote/standing.ts
|
|
7876
|
+
var WITHDRAWN = ["superseded", "rejected"];
|
|
7877
|
+
function standings(bundle) {
|
|
7878
|
+
return new Map(
|
|
7879
|
+
adjudicate(bundle, bundle).map((hit) => [
|
|
7880
|
+
hit.record.conceptId,
|
|
7881
|
+
hit.standing
|
|
7882
|
+
])
|
|
7883
|
+
);
|
|
7884
|
+
}
|
|
7885
|
+
function isWithdrawn(standing) {
|
|
7886
|
+
return standing !== void 0 && WITHDRAWN.includes(standing);
|
|
7887
|
+
}
|
|
7888
|
+
|
|
7889
|
+
// src/commands/promote/candidates.ts
|
|
7890
|
+
var REVIEW_TAG = "review";
|
|
7891
|
+
var SETTLED = ["resolved"];
|
|
7892
|
+
function promoteCandidates(bundle) {
|
|
7893
|
+
const inbound = inboundIndex(bundle);
|
|
7894
|
+
const standing = standings(bundle);
|
|
7895
|
+
const rows = [];
|
|
7896
|
+
for (const record of bundle) {
|
|
7897
|
+
if (isWithdrawn(standing.get(record.conceptId))) continue;
|
|
7898
|
+
const why2 = candidateReason(record, inbound.get(record.conceptId) ?? []);
|
|
7899
|
+
if (!why2) continue;
|
|
7900
|
+
rows.push({
|
|
7901
|
+
conceptId: record.conceptId,
|
|
7902
|
+
type: recordType(record.conceptId),
|
|
7903
|
+
title: record.frontmatter.title ?? null,
|
|
7904
|
+
why: why2
|
|
7905
|
+
});
|
|
7906
|
+
}
|
|
7907
|
+
return rows;
|
|
7908
|
+
}
|
|
7909
|
+
function candidateReason(record, inbound) {
|
|
7910
|
+
const { strauss_status: status, tags } = record.frontmatter;
|
|
7911
|
+
switch (recordType(record.conceptId)) {
|
|
7912
|
+
case "decision":
|
|
7913
|
+
if (isNoDecisionRecord(record)) return null;
|
|
7914
|
+
return tags?.includes(REVIEW_TAG) ? null : "decision no longer under review";
|
|
7915
|
+
case "constraint":
|
|
7916
|
+
return status === "proposed" ? "constraint still proposed \u2014 the target base is where it settles" : null;
|
|
7917
|
+
case "contract":
|
|
7918
|
+
return "contract \u2014 it outlives the change that introduced it";
|
|
7919
|
+
case "requirement":
|
|
7920
|
+
return inbound.some((edge) => edge.rel === "satisfies") ? "requirement something in the base satisfies" : null;
|
|
7921
|
+
case "risk":
|
|
7922
|
+
return record.frontmatter.strauss_materiality === "blocking" && !SETTLED.includes(status) ? "blocking risk still open" : null;
|
|
7923
|
+
default:
|
|
7924
|
+
return null;
|
|
7925
|
+
}
|
|
7926
|
+
}
|
|
7927
|
+
function recordType(conceptId2) {
|
|
7928
|
+
return conceptId2.slice(0, conceptId2.indexOf("."));
|
|
7929
|
+
}
|
|
7930
|
+
|
|
7931
|
+
// src/commands/promote/model.ts
|
|
7932
|
+
var import_zod28 = require("zod");
|
|
7933
|
+
var promoteInputSchema = import_zod28.z.object({
|
|
7934
|
+
bundlePath,
|
|
7935
|
+
conceptIds: import_zod28.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
7936
|
+
to: import_zod28.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
7937
|
+
source: import_zod28.z.string().min(1).optional().describe(
|
|
7938
|
+
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
7939
|
+
),
|
|
7940
|
+
force: import_zod28.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
7941
|
+
list: import_zod28.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
7942
|
+
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
7943
|
+
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
7944
|
+
path: ["to"]
|
|
7945
|
+
}).refine(
|
|
7946
|
+
(input) => input.list === true || (input.conceptIds?.length ?? 0) > 0,
|
|
7947
|
+
{
|
|
7948
|
+
message: "name at least one concept id to promote, or pass --list",
|
|
7949
|
+
path: ["conceptIds"]
|
|
7950
|
+
}
|
|
7951
|
+
);
|
|
7952
|
+
|
|
7953
|
+
// src/commands/promote/command.ts
|
|
7954
|
+
var promoteCommand = define({
|
|
7955
|
+
name: "promote",
|
|
7956
|
+
tool: "kb_promote",
|
|
7957
|
+
usage: "promote <concept-id...> --to <bundle> [--source <url>] [--force] | --list",
|
|
7958
|
+
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.",
|
|
7959
|
+
input: promoteInputSchema,
|
|
7960
|
+
fromArgv: (argv, path) => {
|
|
7961
|
+
const to = argvFlag(argv, "--to");
|
|
7962
|
+
const source = argvFlag(argv, "--source");
|
|
7963
|
+
const words = argv.slice(1);
|
|
7964
|
+
for (const flag of ["--to", "--source"]) {
|
|
7965
|
+
const at2 = words.indexOf(flag);
|
|
7966
|
+
if (at2 !== -1) words.splice(at2, 2);
|
|
7967
|
+
}
|
|
7968
|
+
const conceptIds = words.filter((word) => !word.startsWith("--"));
|
|
7969
|
+
return {
|
|
7970
|
+
bundlePath: path,
|
|
7971
|
+
...conceptIds.length ? { conceptIds } : {},
|
|
7972
|
+
...to !== void 0 ? { to } : {},
|
|
7973
|
+
...source !== void 0 ? { source } : {},
|
|
7974
|
+
...argv.includes("--force") ? { force: true } : {},
|
|
7975
|
+
...argv.includes("--list") ? { list: true } : {}
|
|
7976
|
+
};
|
|
7977
|
+
},
|
|
7978
|
+
run: async ({ store, actor }, { bundlePath: path, conceptIds, to, source, force, list }) => {
|
|
7979
|
+
const from = (0, import_node_path14.resolve)(path);
|
|
7980
|
+
const bundle = await store.list(from);
|
|
7981
|
+
if (list) {
|
|
7982
|
+
return { mode: "list", candidates: promoteCandidates(bundle) };
|
|
7983
|
+
}
|
|
7984
|
+
const target = (0, import_node_path14.resolve)(to);
|
|
7985
|
+
if (target === from) throw new KbPromoteSelfError(target);
|
|
7986
|
+
const named = (conceptIds ?? []).map(namedRecord);
|
|
7987
|
+
const wanted = named.map(({ conceptId: conceptId2, type, slug }) => {
|
|
7988
|
+
const record = bundle.find((entry) => entry.conceptId === conceptId2);
|
|
7989
|
+
if (!record) throw new KbRecordNotFoundError(conceptId2);
|
|
7990
|
+
return { record, type, slug };
|
|
7991
|
+
});
|
|
7992
|
+
await assertBaseNotFrozen(process.cwd(), from);
|
|
7993
|
+
await assertBaseNotFrozen(process.cwd(), target);
|
|
7994
|
+
const standing = standings(bundle);
|
|
7995
|
+
for (const { record } of wanted) {
|
|
7996
|
+
const where = standing.get(record.conceptId);
|
|
7997
|
+
if (isWithdrawn(where)) {
|
|
7998
|
+
throw new KbPromoteStandingError(record.conceptId, where);
|
|
7999
|
+
}
|
|
8000
|
+
if (!force && await store.read(target, record.conceptId)) {
|
|
8001
|
+
throw new KbPromoteCollisionError(record.conceptId, target);
|
|
8002
|
+
}
|
|
8003
|
+
}
|
|
8004
|
+
const promotedIds = new Set(wanted.map(({ record }) => record.conceptId));
|
|
8005
|
+
const promoted = [];
|
|
8006
|
+
for (const { record, type, slug } of wanted) {
|
|
8007
|
+
const { frontmatter, body, droppedLinks } = carry(
|
|
8008
|
+
record,
|
|
8009
|
+
promotedIds,
|
|
8010
|
+
source
|
|
8011
|
+
);
|
|
8012
|
+
try {
|
|
8013
|
+
await store.write(
|
|
8014
|
+
target,
|
|
8015
|
+
{ type, slug, frontmatter, body, overwrite: force === true },
|
|
8016
|
+
actor
|
|
8017
|
+
);
|
|
8018
|
+
} catch (error) {
|
|
8019
|
+
throw new KbPromoteStoppedError(
|
|
8020
|
+
record.conceptId,
|
|
8021
|
+
promoted.map((entry) => entry.conceptId),
|
|
8022
|
+
error instanceof Error ? error.message : "unknown"
|
|
8023
|
+
);
|
|
8024
|
+
}
|
|
8025
|
+
await store.note(target, {
|
|
8026
|
+
by: actor,
|
|
8027
|
+
operation: "promote-in",
|
|
8028
|
+
conceptId: record.conceptId,
|
|
8029
|
+
target: from
|
|
8030
|
+
});
|
|
8031
|
+
await store.note(from, {
|
|
8032
|
+
by: actor,
|
|
8033
|
+
operation: "promote-out",
|
|
8034
|
+
conceptId: record.conceptId,
|
|
8035
|
+
target
|
|
8036
|
+
});
|
|
8037
|
+
promoted.push({ conceptId: record.conceptId, droppedLinks });
|
|
8038
|
+
}
|
|
8039
|
+
return { mode: "promote", to: target, promoted };
|
|
8040
|
+
},
|
|
8041
|
+
render: (result) => renderPromote(result)
|
|
8042
|
+
});
|
|
8043
|
+
function namedRecord(conceptId2) {
|
|
8044
|
+
const at2 = conceptId2.indexOf(".");
|
|
8045
|
+
const type = at2 === -1 ? conceptId2 : conceptId2.slice(0, at2);
|
|
8046
|
+
const slug = at2 === -1 ? "" : conceptId2.slice(at2 + 1);
|
|
8047
|
+
if (!KB_SLUG_PATTERN.test(type) || !KB_SLUG_PATTERN.test(slug)) {
|
|
8048
|
+
throw new KbInvalidConceptIdError(
|
|
8049
|
+
"concept id must be <type>.<slug>, both kebab-case",
|
|
8050
|
+
{ conceptId: conceptId2 }
|
|
8051
|
+
);
|
|
8052
|
+
}
|
|
8053
|
+
return { conceptId: conceptId2, type, slug };
|
|
8054
|
+
}
|
|
8055
|
+
function renderPromote(result) {
|
|
8056
|
+
if (result.mode === "list") {
|
|
8057
|
+
if (!result.candidates.length) return "No promotion candidates.";
|
|
8058
|
+
return result.candidates.flatMap((candidate) => [
|
|
8059
|
+
`${candidate.conceptId} [${candidate.type}]${candidate.title ? ` \u2014 ${candidate.title}` : ""}`,
|
|
8060
|
+
` ${candidate.why}`
|
|
8061
|
+
]).join("\n");
|
|
8062
|
+
}
|
|
8063
|
+
const lines = [
|
|
8064
|
+
`Promoted ${result.promoted.length} record${result.promoted.length === 1 ? "" : "s"} into ${result.to}.`
|
|
8065
|
+
];
|
|
8066
|
+
for (const entry of result.promoted) {
|
|
8067
|
+
lines.push(`- ${entry.conceptId}`);
|
|
8068
|
+
for (const link2 of entry.droppedLinks) {
|
|
8069
|
+
lines.push(
|
|
8070
|
+
` dropped ${link2.rel} \u2192 ${link2.target} (not promoted in this run)`
|
|
8071
|
+
);
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
return lines.join("\n");
|
|
8075
|
+
}
|
|
8076
|
+
|
|
6451
8077
|
// src/commands/query.ts
|
|
6452
|
-
var
|
|
8078
|
+
var import_zod29 = require("zod");
|
|
6453
8079
|
var queryCommand = define({
|
|
6454
8080
|
name: "query",
|
|
6455
8081
|
tool: "kb_query",
|
|
6456
8082
|
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6457
8083
|
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:
|
|
8084
|
+
input: import_zod29.z.object({
|
|
6459
8085
|
bundlePath,
|
|
6460
|
-
text:
|
|
6461
|
-
type:
|
|
6462
|
-
includeNonCurrent:
|
|
8086
|
+
text: import_zod29.z.string().optional(),
|
|
8087
|
+
type: import_zod29.z.enum(KB_RECORD_TYPES).optional(),
|
|
8088
|
+
includeNonCurrent: import_zod29.z.boolean().optional(),
|
|
6463
8089
|
tags: TAGS,
|
|
6464
8090
|
repoRoot: REPO_ROOT
|
|
6465
8091
|
}),
|
|
@@ -6493,43 +8119,43 @@ var queryCommand = define({
|
|
|
6493
8119
|
});
|
|
6494
8120
|
|
|
6495
8121
|
// src/commands/read-index.ts
|
|
6496
|
-
var
|
|
8122
|
+
var import_zod30 = require("zod");
|
|
6497
8123
|
var readIndexCommand = define({
|
|
6498
8124
|
name: "index",
|
|
6499
8125
|
tool: "kb_index",
|
|
6500
8126
|
usage: "index",
|
|
6501
8127
|
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:
|
|
8128
|
+
input: import_zod30.z.object({ bundlePath }),
|
|
6503
8129
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6504
8130
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6505
8131
|
});
|
|
6506
8132
|
|
|
6507
8133
|
// src/commands/schema.ts
|
|
6508
|
-
var
|
|
8134
|
+
var import_zod31 = require("zod");
|
|
6509
8135
|
var schemaCommand = define({
|
|
6510
8136
|
name: "schema",
|
|
6511
8137
|
tool: "kb_schema",
|
|
6512
8138
|
usage: "schema",
|
|
6513
8139
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6514
|
-
input:
|
|
8140
|
+
input: import_zod31.z.object({}),
|
|
6515
8141
|
fromArgv: () => ({}),
|
|
6516
8142
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6517
8143
|
});
|
|
6518
8144
|
|
|
6519
8145
|
// src/commands/stamp.ts
|
|
6520
|
-
var
|
|
6521
|
-
var
|
|
8146
|
+
var import_promises12 = require("fs/promises");
|
|
8147
|
+
var import_zod32 = require("zod");
|
|
6522
8148
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6523
8149
|
var stampCommand = define({
|
|
6524
8150
|
name: "stamp",
|
|
6525
8151
|
tool: "kb_stamp",
|
|
6526
8152
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6527
8153
|
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:
|
|
8154
|
+
input: import_zod32.z.object({
|
|
8155
|
+
bundlePath: import_zod32.z.string().min(1).optional().describe(
|
|
6530
8156
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6531
8157
|
),
|
|
6532
|
-
since:
|
|
8158
|
+
since: import_zod32.z.string().min(1).optional().describe(
|
|
6533
8159
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6534
8160
|
)
|
|
6535
8161
|
}),
|
|
@@ -6591,7 +8217,7 @@ async function readBaseline(since) {
|
|
|
6591
8217
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
6592
8218
|
let parsed;
|
|
6593
8219
|
try {
|
|
6594
|
-
parsed = JSON.parse(await (0,
|
|
8220
|
+
parsed = JSON.parse(await (0, import_promises12.readFile)(since, "utf8"));
|
|
6595
8221
|
} catch {
|
|
6596
8222
|
throw new KbStampBaselineError(since);
|
|
6597
8223
|
}
|
|
@@ -6615,16 +8241,16 @@ async function readBaseline(since) {
|
|
|
6615
8241
|
}
|
|
6616
8242
|
|
|
6617
8243
|
// src/commands/status.ts
|
|
6618
|
-
var
|
|
8244
|
+
var import_zod33 = require("zod");
|
|
6619
8245
|
var statusCommand = define({
|
|
6620
8246
|
name: "status",
|
|
6621
8247
|
tool: "kb_status",
|
|
6622
8248
|
usage: "status <concept-id> <status>",
|
|
6623
8249
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6624
|
-
input:
|
|
8250
|
+
input: import_zod33.z.object({
|
|
6625
8251
|
bundlePath,
|
|
6626
8252
|
conceptId,
|
|
6627
|
-
status:
|
|
8253
|
+
status: import_zod33.z.enum(KB_RECORD_STATUSES)
|
|
6628
8254
|
}),
|
|
6629
8255
|
fromArgv: (argv, path) => ({
|
|
6630
8256
|
bundlePath: path,
|
|
@@ -6639,13 +8265,13 @@ var statusCommand = define({
|
|
|
6639
8265
|
});
|
|
6640
8266
|
|
|
6641
8267
|
// src/commands/supersede.ts
|
|
6642
|
-
var
|
|
8268
|
+
var import_zod34 = require("zod");
|
|
6643
8269
|
var supersedeCommand = define({
|
|
6644
8270
|
name: "supersede",
|
|
6645
8271
|
tool: "kb_supersede",
|
|
6646
8272
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6647
8273
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6648
|
-
input:
|
|
8274
|
+
input: import_zod34.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6649
8275
|
fromArgv: (argv, path) => ({
|
|
6650
8276
|
bundlePath: path,
|
|
6651
8277
|
conceptId: argv[1],
|
|
@@ -6658,17 +8284,153 @@ var supersedeCommand = define({
|
|
|
6658
8284
|
}
|
|
6659
8285
|
});
|
|
6660
8286
|
|
|
8287
|
+
// src/commands/sweep.ts
|
|
8288
|
+
var import_zod35 = require("zod");
|
|
8289
|
+
var TERMINAL = [
|
|
8290
|
+
"resolved",
|
|
8291
|
+
"rejected",
|
|
8292
|
+
"superseded"
|
|
8293
|
+
];
|
|
8294
|
+
var sweepCommand = define({
|
|
8295
|
+
name: "sweep",
|
|
8296
|
+
tool: "kb_sweep",
|
|
8297
|
+
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
8298
|
+
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.",
|
|
8299
|
+
input: import_zod35.z.object({
|
|
8300
|
+
bundlePath,
|
|
8301
|
+
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."),
|
|
8302
|
+
terminal: import_zod35.z.literal(true, {
|
|
8303
|
+
error: "sweep needs --terminal: it deletes only settled records"
|
|
8304
|
+
}).describe(
|
|
8305
|
+
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
8306
|
+
),
|
|
8307
|
+
dryRun: import_zod35.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
8308
|
+
}),
|
|
8309
|
+
fromArgv: (argv, path) => ({
|
|
8310
|
+
bundlePath: path,
|
|
8311
|
+
tag: argvFlag(argv, "--tag"),
|
|
8312
|
+
...argv.includes("--terminal") ? { terminal: true } : {},
|
|
8313
|
+
...argv.includes("--dry-run") ? { dryRun: true } : {}
|
|
8314
|
+
}),
|
|
8315
|
+
run: async ({ store, actor }, { bundlePath: path, tag, dryRun }) => {
|
|
8316
|
+
const bundle = await store.list(path);
|
|
8317
|
+
const held = holderIndex(bundle);
|
|
8318
|
+
const candidates = adjudicate(bundle, bundle).filter(
|
|
8319
|
+
(hit) => sweepable(hit, tag)
|
|
8320
|
+
);
|
|
8321
|
+
const doomed = new Set(candidates.map((hit) => hit.record.conceptId));
|
|
8322
|
+
let changed = true;
|
|
8323
|
+
while (changed) {
|
|
8324
|
+
changed = false;
|
|
8325
|
+
for (const conceptId2 of [...doomed]) {
|
|
8326
|
+
if (survivorsHolding(conceptId2, held, doomed).length === 0) continue;
|
|
8327
|
+
doomed.delete(conceptId2);
|
|
8328
|
+
changed = true;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
const skipped = candidates.filter((hit) => !doomed.has(hit.record.conceptId)).map((hit) => ({
|
|
8332
|
+
conceptId: hit.record.conceptId,
|
|
8333
|
+
heldBy: survivorsHolding(hit.record.conceptId, held, doomed)
|
|
8334
|
+
}));
|
|
8335
|
+
const ordered = [...doomed].sort();
|
|
8336
|
+
if (dryRun) {
|
|
8337
|
+
return {
|
|
8338
|
+
tag,
|
|
8339
|
+
dryRun: true,
|
|
8340
|
+
deleted: [],
|
|
8341
|
+
candidates: ordered,
|
|
8342
|
+
skipped,
|
|
8343
|
+
failed: []
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
8347
|
+
const deleted = [];
|
|
8348
|
+
const failed = [];
|
|
8349
|
+
try {
|
|
8350
|
+
for (const conceptId2 of ordered) {
|
|
8351
|
+
try {
|
|
8352
|
+
const outcome = await store.deleteRecord(
|
|
8353
|
+
path,
|
|
8354
|
+
conceptId2,
|
|
8355
|
+
{ tag, statuses: TERMINAL },
|
|
8356
|
+
actor
|
|
8357
|
+
);
|
|
8358
|
+
if (outcome === "deleted") deleted.push(conceptId2);
|
|
8359
|
+
else failed.push({ conceptId: conceptId2, reason: outcome });
|
|
8360
|
+
} catch (error) {
|
|
8361
|
+
failed.push({
|
|
8362
|
+
conceptId: conceptId2,
|
|
8363
|
+
reason: error instanceof Error ? error.message : "unknown"
|
|
8364
|
+
});
|
|
8365
|
+
}
|
|
8366
|
+
}
|
|
8367
|
+
} finally {
|
|
8368
|
+
await store.readIndex(path);
|
|
8369
|
+
await store.dropSearchIndex(path);
|
|
8370
|
+
}
|
|
8371
|
+
return {
|
|
8372
|
+
tag,
|
|
8373
|
+
dryRun: false,
|
|
8374
|
+
deleted,
|
|
8375
|
+
candidates: ordered,
|
|
8376
|
+
skipped,
|
|
8377
|
+
failed
|
|
8378
|
+
};
|
|
8379
|
+
},
|
|
8380
|
+
render: (result) => renderSweep(result)
|
|
8381
|
+
});
|
|
8382
|
+
function sweepable(hit, tag) {
|
|
8383
|
+
const { tags, strauss_status } = hit.record.frontmatter;
|
|
8384
|
+
return (tags ?? []).includes(tag) && // Supersession is a standing, settled against the whole base; the other
|
|
8385
|
+
// two are the record's own word for itself.
|
|
8386
|
+
(hit.standing === "superseded" || strauss_status === "resolved" || strauss_status === "rejected");
|
|
8387
|
+
}
|
|
8388
|
+
function holderIndex(bundle) {
|
|
8389
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
8390
|
+
const hold = (target, from) => {
|
|
8391
|
+
if (target === from) return;
|
|
8392
|
+
const holders = byTarget.get(target) ?? /* @__PURE__ */ new Set();
|
|
8393
|
+
holders.add(from);
|
|
8394
|
+
byTarget.set(target, holders);
|
|
8395
|
+
};
|
|
8396
|
+
for (const [target, edges] of inboundIndex(bundle)) {
|
|
8397
|
+
for (const edge of edges) hold(target, edge.from);
|
|
8398
|
+
}
|
|
8399
|
+
for (const record of bundle) {
|
|
8400
|
+
const { strauss_supersedes, strauss_superseded_by } = record.frontmatter;
|
|
8401
|
+
for (const old of strauss_supersedes ?? []) hold(old, record.conceptId);
|
|
8402
|
+
if (strauss_superseded_by) hold(strauss_superseded_by, record.conceptId);
|
|
8403
|
+
}
|
|
8404
|
+
return byTarget;
|
|
8405
|
+
}
|
|
8406
|
+
function survivorsHolding(conceptId2, held, doomed) {
|
|
8407
|
+
return [...held.get(conceptId2) ?? []].filter((from) => !doomed.has(from)).sort();
|
|
8408
|
+
}
|
|
8409
|
+
function renderSweep(result) {
|
|
8410
|
+
const shown = result.dryRun ? result.candidates : result.deleted;
|
|
8411
|
+
const verb = result.dryRun ? "would delete" : "deleted";
|
|
8412
|
+
const lines = [`${verb} ${shown.length} (tag: ${result.tag})`];
|
|
8413
|
+
for (const conceptId2 of shown) lines.push(`- ${conceptId2}`);
|
|
8414
|
+
for (const skip of result.skipped) {
|
|
8415
|
+
lines.push(`kept ${skip.conceptId} \u2014 held by ${skip.heldBy.join(", ")}`);
|
|
8416
|
+
}
|
|
8417
|
+
for (const failure of result.failed) {
|
|
8418
|
+
lines.push(`failed ${failure.conceptId} \u2014 ${failure.reason}`);
|
|
8419
|
+
}
|
|
8420
|
+
return lines.join("\n");
|
|
8421
|
+
}
|
|
8422
|
+
|
|
6661
8423
|
// src/commands/sync-instructions.ts
|
|
6662
|
-
var
|
|
8424
|
+
var import_zod36 = require("zod");
|
|
6663
8425
|
var syncInstructionsCommand = define({
|
|
6664
8426
|
name: "sync-instructions",
|
|
6665
8427
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
6666
8428
|
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:
|
|
8429
|
+
input: import_zod36.z.object({
|
|
8430
|
+
file: import_zod36.z.string().min(1).describe("The instruction file to edit in place."),
|
|
8431
|
+
budgetTokens: import_zod36.z.number().int().positive().optional(),
|
|
8432
|
+
fullUnderTokens: import_zod36.z.number().int().positive().optional(),
|
|
8433
|
+
profile: import_zod36.z.string().optional()
|
|
6672
8434
|
}),
|
|
6673
8435
|
fromArgv: (argv) => {
|
|
6674
8436
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -6694,17 +8456,17 @@ var syncInstructionsCommand = define({
|
|
|
6694
8456
|
});
|
|
6695
8457
|
|
|
6696
8458
|
// src/commands/trace.ts
|
|
6697
|
-
var
|
|
8459
|
+
var import_zod37 = require("zod");
|
|
6698
8460
|
var traceCommand = define({
|
|
6699
8461
|
name: "trace",
|
|
6700
8462
|
tool: "kb_trace",
|
|
6701
8463
|
usage: "trace <concept-id> [edges...]",
|
|
6702
8464
|
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:
|
|
8465
|
+
input: import_zod37.z.object({
|
|
6704
8466
|
bundlePath,
|
|
6705
8467
|
conceptId,
|
|
6706
|
-
edges:
|
|
6707
|
-
depth:
|
|
8468
|
+
edges: import_zod37.z.array(import_zod37.z.enum(TRACE_EDGES)).optional(),
|
|
8469
|
+
depth: import_zod37.z.number().int().positive().optional()
|
|
6708
8470
|
}),
|
|
6709
8471
|
fromArgv: (argv, path) => ({
|
|
6710
8472
|
bundlePath: path,
|
|
@@ -6726,37 +8488,37 @@ var traceCommand = define({
|
|
|
6726
8488
|
});
|
|
6727
8489
|
|
|
6728
8490
|
// src/commands/types.ts
|
|
6729
|
-
var
|
|
8491
|
+
var import_zod38 = require("zod");
|
|
6730
8492
|
var typesCommand = define({
|
|
6731
8493
|
name: "types",
|
|
6732
8494
|
tool: "kb_types",
|
|
6733
8495
|
usage: "types",
|
|
6734
8496
|
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:
|
|
8497
|
+
input: import_zod38.z.object({}),
|
|
6736
8498
|
fromArgv: () => ({}),
|
|
6737
8499
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
6738
8500
|
});
|
|
6739
8501
|
|
|
6740
8502
|
// src/commands/unpin.ts
|
|
6741
|
-
var
|
|
8503
|
+
var import_zod39 = require("zod");
|
|
6742
8504
|
var unpinCommand = define({
|
|
6743
8505
|
name: "unpin",
|
|
6744
8506
|
tool: "kb_unpin",
|
|
6745
8507
|
usage: "unpin [bundle-path]",
|
|
6746
8508
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
6747
|
-
input:
|
|
8509
|
+
input: import_zod39.z.object({ bundlePath }),
|
|
6748
8510
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
6749
8511
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
6750
8512
|
});
|
|
6751
8513
|
|
|
6752
8514
|
// src/commands/validate.ts
|
|
6753
|
-
var
|
|
8515
|
+
var import_zod40 = require("zod");
|
|
6754
8516
|
var validateCommand = define({
|
|
6755
8517
|
name: "validate",
|
|
6756
8518
|
tool: "kb_validate",
|
|
6757
8519
|
usage: "validate",
|
|
6758
8520
|
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:
|
|
8521
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
6760
8522
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6761
8523
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
6762
8524
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -6766,16 +8528,16 @@ var validateCommand = define({
|
|
|
6766
8528
|
});
|
|
6767
8529
|
|
|
6768
8530
|
// src/commands/verify.ts
|
|
6769
|
-
var
|
|
8531
|
+
var import_zod41 = require("zod");
|
|
6770
8532
|
var verifyCommand = define({
|
|
6771
8533
|
name: "verify",
|
|
6772
8534
|
tool: "kb_verify",
|
|
6773
8535
|
usage: "verify <concept-id> --note <text>",
|
|
6774
8536
|
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:
|
|
8537
|
+
input: import_zod41.z.object({
|
|
6776
8538
|
bundlePath,
|
|
6777
8539
|
conceptId,
|
|
6778
|
-
note:
|
|
8540
|
+
note: import_zod41.z.string().refine((s) => s.trim().length > 0, {
|
|
6779
8541
|
message: "note must say what the check found"
|
|
6780
8542
|
})
|
|
6781
8543
|
}),
|
|
@@ -6795,15 +8557,15 @@ var verifyCommand = define({
|
|
|
6795
8557
|
});
|
|
6796
8558
|
|
|
6797
8559
|
// src/commands/write.ts
|
|
6798
|
-
var
|
|
8560
|
+
var import_zod42 = require("zod");
|
|
6799
8561
|
var writeCommand = define({
|
|
6800
8562
|
name: "write",
|
|
6801
8563
|
tool: "kb_write",
|
|
6802
8564
|
usage: "write <type> < record.json",
|
|
6803
8565
|
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:
|
|
8566
|
+
input: import_zod42.z.object({
|
|
6805
8567
|
bundlePath,
|
|
6806
|
-
type:
|
|
8568
|
+
type: import_zod42.z.enum(KB_RECORD_TYPES),
|
|
6807
8569
|
input: composeInputSchema
|
|
6808
8570
|
}),
|
|
6809
8571
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -6827,13 +8589,13 @@ var writeCommand = define({
|
|
|
6827
8589
|
});
|
|
6828
8590
|
|
|
6829
8591
|
// src/commands/write-decision.ts
|
|
6830
|
-
var
|
|
8592
|
+
var import_zod43 = require("zod");
|
|
6831
8593
|
var writeDecisionCommand = define({
|
|
6832
8594
|
name: "write-decision",
|
|
6833
8595
|
tool: "kb_write_decision",
|
|
6834
8596
|
usage: "write-decision < decision.json",
|
|
6835
8597
|
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:
|
|
8598
|
+
input: import_zod43.z.object({ bundlePath, input: decisionInputSchema }),
|
|
6837
8599
|
fromArgv: async (_argv, path, stdin) => ({
|
|
6838
8600
|
bundlePath: path,
|
|
6839
8601
|
input: JSON.parse(await stdin())
|
|
@@ -6864,19 +8626,24 @@ var KB_COMMANDS = [
|
|
|
6864
8626
|
verifyCommand,
|
|
6865
8627
|
anchorResolveCommand,
|
|
6866
8628
|
reassessCommand,
|
|
8629
|
+
promoteCommand,
|
|
6867
8630
|
loadCommand,
|
|
6868
8631
|
catalogCommand,
|
|
6869
8632
|
packCommand,
|
|
8633
|
+
exportCommand,
|
|
6870
8634
|
queryCommand,
|
|
6871
8635
|
traceCommand,
|
|
6872
8636
|
impactCommand,
|
|
6873
8637
|
backlinksCommand,
|
|
8638
|
+
matchCommand,
|
|
8639
|
+
classifyCommand,
|
|
6874
8640
|
listCommand,
|
|
6875
8641
|
readIndexCommand,
|
|
6876
8642
|
logCommand,
|
|
6877
8643
|
stampCommand,
|
|
6878
8644
|
validateCommand,
|
|
6879
8645
|
doctorCommand,
|
|
8646
|
+
sweepCommand,
|
|
6880
8647
|
schemaCommand,
|
|
6881
8648
|
pinCommand,
|
|
6882
8649
|
unpinCommand,
|
|
@@ -6894,7 +8661,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
6894
8661
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6895
8662
|
|
|
6896
8663
|
// src/version.ts
|
|
6897
|
-
var VERSION = true ? "0.1.
|
|
8664
|
+
var VERSION = true ? "0.1.21" : "0.0.0-dev";
|
|
6898
8665
|
|
|
6899
8666
|
// src/mcp.ts
|
|
6900
8667
|
function createKbMcpServer() {
|
|
@@ -6933,7 +8700,7 @@ async function runKbMcpServer() {
|
|
|
6933
8700
|
}
|
|
6934
8701
|
|
|
6935
8702
|
// src/cli.ts
|
|
6936
|
-
var
|
|
8703
|
+
var import_node_path15 = require("path");
|
|
6937
8704
|
async function runKbCli(argv) {
|
|
6938
8705
|
const { flags, literal } = takeLiteral(argv);
|
|
6939
8706
|
const {
|
|
@@ -6995,7 +8762,7 @@ function takeLiteral(argv) {
|
|
|
6995
8762
|
function takeBundle(argv) {
|
|
6996
8763
|
const at2 = argv.indexOf("--bundle");
|
|
6997
8764
|
if (at2 === -1) {
|
|
6998
|
-
return { bundle: (0,
|
|
8765
|
+
return { bundle: (0, import_node_path15.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
6999
8766
|
}
|
|
7000
8767
|
const bundle = argv[at2 + 1];
|
|
7001
8768
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -7006,11 +8773,11 @@ function takeBundle(argv) {
|
|
|
7006
8773
|
};
|
|
7007
8774
|
}
|
|
7008
8775
|
function readStdin() {
|
|
7009
|
-
return new Promise((
|
|
8776
|
+
return new Promise((resolve7, reject) => {
|
|
7010
8777
|
let text = "";
|
|
7011
8778
|
process.stdin.setEncoding("utf8");
|
|
7012
8779
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
7013
|
-
process.stdin.on("end", () =>
|
|
8780
|
+
process.stdin.on("end", () => resolve7(text));
|
|
7014
8781
|
process.stdin.on("error", reject);
|
|
7015
8782
|
});
|
|
7016
8783
|
}
|
|
@@ -7055,12 +8822,14 @@ function usage() {
|
|
|
7055
8822
|
DEFAULT_LOAD_BUDGET,
|
|
7056
8823
|
DEFAULT_PACK_HOPS,
|
|
7057
8824
|
DEFAULT_PACK_MAX_NODES,
|
|
8825
|
+
DEFAULT_THRESHOLDS,
|
|
7058
8826
|
DEFAULT_TYPED_LINK_RELS,
|
|
7059
8827
|
DEFAULT_UNVERIFIED_DAYS,
|
|
7060
8828
|
ErrorTypes,
|
|
7061
8829
|
Fault,
|
|
7062
8830
|
INDEX_FILE,
|
|
7063
8831
|
KB_CAUSAL_LINK_RELS,
|
|
8832
|
+
KB_CLASSES,
|
|
7064
8833
|
KB_COMMANDS,
|
|
7065
8834
|
KB_COMMANDS_BY_NAME,
|
|
7066
8835
|
KB_CONCEPT_ID_PATTERN,
|
|
@@ -7074,10 +8843,15 @@ function usage() {
|
|
|
7074
8843
|
KB_RECORD_TYPES,
|
|
7075
8844
|
KB_SLUG_PATTERN,
|
|
7076
8845
|
KbBaseFrozenError,
|
|
8846
|
+
KbClassifyInputError,
|
|
7077
8847
|
KbInvalidConceptIdError,
|
|
7078
8848
|
KbMissingFlagValueError,
|
|
7079
8849
|
KbPackBudgetExceededError,
|
|
7080
8850
|
KbPinsMalformedError,
|
|
8851
|
+
KbPromoteCollisionError,
|
|
8852
|
+
KbPromoteSelfError,
|
|
8853
|
+
KbPromoteStandingError,
|
|
8854
|
+
KbPromoteStoppedError,
|
|
7081
8855
|
KbRecordAlreadyExistsError,
|
|
7082
8856
|
KbRecordNotFoundError,
|
|
7083
8857
|
KbSelfVerificationError,
|
|
@@ -7090,16 +8864,20 @@ function usage() {
|
|
|
7090
8864
|
PINS_FILE,
|
|
7091
8865
|
PINS_LOCAL_FILE,
|
|
7092
8866
|
PIN_LAYERS,
|
|
8867
|
+
PROMOTION_SOURCE_ID,
|
|
7093
8868
|
RECORD_TYPES,
|
|
7094
8869
|
SEARCH_INDEX_FILE,
|
|
7095
8870
|
TRACE_EDGES,
|
|
7096
8871
|
TreeSitterResolver,
|
|
7097
8872
|
adjudicate,
|
|
7098
8873
|
anchorFilePath,
|
|
8874
|
+
anchorOnHunk,
|
|
7099
8875
|
assertBaseNotFrozen,
|
|
7100
8876
|
backlinks,
|
|
7101
8877
|
buildContext,
|
|
8878
|
+
carry,
|
|
7102
8879
|
catalog,
|
|
8880
|
+
classifyDiff,
|
|
7103
8881
|
classifyDrift,
|
|
7104
8882
|
composeDecisionRecord,
|
|
7105
8883
|
composeInputSchema,
|
|
@@ -7125,8 +8903,11 @@ function usage() {
|
|
|
7125
8903
|
isKbLinkRel,
|
|
7126
8904
|
isKbRecordType,
|
|
7127
8905
|
isNoDecisionRecord,
|
|
8906
|
+
isReviewTag,
|
|
7128
8907
|
kbActorStampSchema,
|
|
7129
8908
|
kbAnchorSchema,
|
|
8909
|
+
kbAnchorSpanSchema,
|
|
8910
|
+
kbAnchorWriteSchema,
|
|
7130
8911
|
kbConceptIdSchema,
|
|
7131
8912
|
kbJsonSchemas,
|
|
7132
8913
|
kbLinkSchema,
|
|
@@ -7146,6 +8927,8 @@ function usage() {
|
|
|
7146
8927
|
parseMarkdownWithFrontmatter,
|
|
7147
8928
|
pinBase,
|
|
7148
8929
|
prepareResolvers,
|
|
8930
|
+
promoteCandidates,
|
|
8931
|
+
promoteInputSchema,
|
|
7149
8932
|
readMergedPins,
|
|
7150
8933
|
readPinsLayer,
|
|
7151
8934
|
readRemoteAnchors,
|
|
@@ -7167,6 +8950,7 @@ function usage() {
|
|
|
7167
8950
|
selectDecisions,
|
|
7168
8951
|
splitMarkdownFrontmatter,
|
|
7169
8952
|
stringifyMarkdownWithFrontmatter,
|
|
8953
|
+
symbolRangeIndex,
|
|
7170
8954
|
syncInstructions,
|
|
7171
8955
|
toHookJson,
|
|
7172
8956
|
trace,
|