@saasontools/strauss-kb 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +5 -5
- package/README.md +11 -4
- package/dist/{chunk-ROGVYSMV.js → chunk-EXZQZJU2.js} +2 -2
- package/dist/{chunk-GSOTMWZZ.js → chunk-QQLPJO4R.js} +3196 -1273
- package/dist/chunk-QQLPJO4R.js.map +1 -0
- package/dist/{chunk-IOIUS26S.js → chunk-XDH6J6CH.js} +2 -2
- package/dist/cli-main.cjs +2841 -954
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +2272 -417
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +510 -101
- package/dist/index.d.ts +510 -101
- package/dist/index.js +40 -83
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +2837 -950
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-GSOTMWZZ.js.map +0 -1
- /package/dist/{chunk-ROGVYSMV.js.map → chunk-EXZQZJU2.js.map} +0 -0
- /package/dist/{chunk-IOIUS26S.js.map → chunk-XDH6J6CH.js.map} +0 -0
package/dist/mcp-main.cjs
CHANGED
|
@@ -8,9 +8,9 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
9
|
var __copyProps = (to, from, except, desc) => {
|
|
10
10
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let
|
|
12
|
-
if (!__hasOwnProp.call(to,
|
|
13
|
-
__defProp(to,
|
|
11
|
+
for (let key2 of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
13
|
+
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
|
|
14
14
|
}
|
|
15
15
|
return to;
|
|
16
16
|
};
|
|
@@ -55,9 +55,24 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
55
55
|
message: "note must say what the check found"
|
|
56
56
|
})
|
|
57
57
|
});
|
|
58
|
+
var kbAnchorSpanSchema = import_zod.z.object({
|
|
59
|
+
start: import_zod.z.number().int().positive(),
|
|
60
|
+
end: import_zod.z.number().int().positive()
|
|
61
|
+
}).strict();
|
|
58
62
|
var kbAnchorSchema = import_zod.z.object({
|
|
59
63
|
file: import_zod.z.string().min(1),
|
|
60
64
|
symbol: import_zod.z.string().min(1).optional(),
|
|
65
|
+
/**
|
|
66
|
+
* The lines the concept names, when no symbol covers them — deleted code,
|
|
67
|
+
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
68
|
+
*/
|
|
69
|
+
span: kbAnchorSpanSchema.optional(),
|
|
70
|
+
/**
|
|
71
|
+
* Which side of the change the anchor describes. `old` is code as it was
|
|
72
|
+
* committed at `ref`, which is the only way to anchor something deleted;
|
|
73
|
+
* absent means the working tree.
|
|
74
|
+
*/
|
|
75
|
+
side: import_zod.z.enum(["old", "new"]).optional(),
|
|
61
76
|
/**
|
|
62
77
|
* Which repository the file lives in — a remote URL
|
|
63
78
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
@@ -96,8 +111,38 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
96
111
|
* before resolvers were named, which is read as `regex` — the only one
|
|
97
112
|
* there was. A hash from a different resolver is drift, not a match.
|
|
98
113
|
*/
|
|
99
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
114
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
100
115
|
}).strict();
|
|
116
|
+
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
117
|
+
if (anchor.span && anchor.symbol) {
|
|
118
|
+
ctx.addIssue({
|
|
119
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
120
|
+
path: ["span"],
|
|
121
|
+
message: "an anchor names a symbol or a span, not both"
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
127
|
+
path: ["span", "end"],
|
|
128
|
+
message: "span end must not precede start"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
132
|
+
ctx.addIssue({
|
|
133
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
134
|
+
path: ["hash_kind"],
|
|
135
|
+
message: "a span is hashed raw, never ast"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
139
|
+
ctx.addIssue({
|
|
140
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
141
|
+
path: ["ref"],
|
|
142
|
+
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
101
146
|
var kbLinkSchema = import_zod.z.object({
|
|
102
147
|
target: import_zod.z.string().min(1),
|
|
103
148
|
rel: import_zod.z.string().min(1)
|
|
@@ -313,7 +358,7 @@ var composeInputSchema = import_zod2.z.object({
|
|
|
313
358
|
why: import_zod2.z.string().min(1),
|
|
314
359
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
315
360
|
sections: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string().min(1)).optional(),
|
|
316
|
-
anchors: import_zod2.z.array(
|
|
361
|
+
anchors: import_zod2.z.array(kbAnchorWriteSchema).optional(),
|
|
317
362
|
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
318
363
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
319
364
|
assumption: import_zod2.z.boolean().optional(),
|
|
@@ -458,6 +503,14 @@ function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
|
458
503
|
writtenAt
|
|
459
504
|
);
|
|
460
505
|
}
|
|
506
|
+
function isNoDecisionRecord(record) {
|
|
507
|
+
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
508
|
+
}
|
|
509
|
+
function selectDecisions(records) {
|
|
510
|
+
return records.filter(
|
|
511
|
+
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
461
514
|
|
|
462
515
|
// src/commands/anchor-resolve.ts
|
|
463
516
|
var import_zod7 = require("zod");
|
|
@@ -491,14 +544,258 @@ async function mapLimit(items, limit, fn) {
|
|
|
491
544
|
return out;
|
|
492
545
|
}
|
|
493
546
|
|
|
547
|
+
// src/drift/git.ts
|
|
548
|
+
var import_node_child_process2 = require("child_process");
|
|
549
|
+
var import_node_util2 = require("util");
|
|
550
|
+
|
|
551
|
+
// src/remote-repo/git.ts
|
|
552
|
+
var import_node_child_process = require("child_process");
|
|
553
|
+
var import_node_util = require("util");
|
|
554
|
+
|
|
555
|
+
// src/anchor-resolver/model.ts
|
|
556
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
557
|
+
|
|
558
|
+
// src/remote-repo/git.ts
|
|
559
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
560
|
+
function childEnv() {
|
|
561
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
562
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
563
|
+
delete env[name];
|
|
564
|
+
}
|
|
565
|
+
return env;
|
|
566
|
+
}
|
|
567
|
+
async function git(args, options = {}) {
|
|
568
|
+
try {
|
|
569
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
570
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
571
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
572
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
573
|
+
encoding: "utf8",
|
|
574
|
+
windowsHide: true,
|
|
575
|
+
env: childEnv()
|
|
576
|
+
});
|
|
577
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const failure = error;
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
stdout: failure.stdout ?? "",
|
|
583
|
+
stderr: failure.stderr ?? "",
|
|
584
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function transportReason(stderr) {
|
|
589
|
+
const text = stderr.toLowerCase();
|
|
590
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
591
|
+
return "repo-unauthorized";
|
|
592
|
+
}
|
|
593
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
594
|
+
return "ref-not-found";
|
|
595
|
+
}
|
|
596
|
+
return "remote-unreachable";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/remote-repo/validate.ts
|
|
600
|
+
var MAX_REF_LENGTH = 200;
|
|
601
|
+
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
602
|
+
function refShapeIsSafe(ref) {
|
|
603
|
+
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
604
|
+
if (ref.includes("..")) return false;
|
|
605
|
+
return REF_SHAPE.test(ref);
|
|
606
|
+
}
|
|
607
|
+
function localRevShapeIsSafe(rev) {
|
|
608
|
+
if (!rev || rev.length > MAX_REF_LENGTH) return false;
|
|
609
|
+
if (rev.includes("..")) return false;
|
|
610
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/^~-]*$/.test(rev);
|
|
611
|
+
}
|
|
612
|
+
async function refIsWellFormed(ref) {
|
|
613
|
+
if (!refShapeIsSafe(ref)) return false;
|
|
614
|
+
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
615
|
+
return checked.ok;
|
|
616
|
+
}
|
|
617
|
+
function filePathIsSafe(file) {
|
|
618
|
+
const path = file.replace(/^\.\//, "");
|
|
619
|
+
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
620
|
+
return !path.split("/").includes("..");
|
|
621
|
+
}
|
|
622
|
+
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
623
|
+
function allowedProtocols() {
|
|
624
|
+
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
625
|
+
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
626
|
+
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
627
|
+
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
628
|
+
}
|
|
629
|
+
function isShortRepoName(repo) {
|
|
630
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
631
|
+
}
|
|
632
|
+
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
633
|
+
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
634
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
635
|
+
function repoUrlIsSafe(repo) {
|
|
636
|
+
const url = repo.trim();
|
|
637
|
+
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
638
|
+
const allowed = allowedProtocols();
|
|
639
|
+
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
640
|
+
const scheme = URL_SCHEME.exec(url);
|
|
641
|
+
if (!scheme?.[1]) return false;
|
|
642
|
+
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
643
|
+
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
644
|
+
const at2 = authority.lastIndexOf("@");
|
|
645
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
646
|
+
}
|
|
647
|
+
function protocolArgs() {
|
|
648
|
+
const allowed = allowedProtocols();
|
|
649
|
+
return [
|
|
650
|
+
"-c",
|
|
651
|
+
"protocol.ext.allow=never",
|
|
652
|
+
"-c",
|
|
653
|
+
`protocol.file.allow=${allowed.includes("file") ? "user" : "never"}`
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/drift/git.ts
|
|
658
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
659
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
660
|
+
var MAX_RANGE_DIFF_BYTES = 8 * 1048576;
|
|
661
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
662
|
+
var RANGE_DIFF_TIMEOUT_MS = 2e4;
|
|
663
|
+
async function git2(cwd, args, limits = {}) {
|
|
664
|
+
const env = { ...process.env };
|
|
665
|
+
delete env["GIT_DIR"];
|
|
666
|
+
delete env["GIT_WORK_TREE"];
|
|
667
|
+
delete env["GIT_INDEX_FILE"];
|
|
668
|
+
try {
|
|
669
|
+
const { stdout } = await execFileAsync2("git", ["-C", cwd, ...args], {
|
|
670
|
+
timeout: limits.timeoutMs ?? GIT_TIMEOUT_MS,
|
|
671
|
+
maxBuffer: limits.maxBytes ?? MAX_GIT_OUTPUT_BYTES,
|
|
672
|
+
env
|
|
673
|
+
});
|
|
674
|
+
return { ok: true, stdout };
|
|
675
|
+
} catch (error) {
|
|
676
|
+
return { ok: false, reason: failureOf(error) };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function failureOf(error) {
|
|
680
|
+
const { code, killed } = error;
|
|
681
|
+
if (code === "ENOENT") return "git-missing";
|
|
682
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "too-large";
|
|
683
|
+
if (killed === true) return "timeout";
|
|
684
|
+
return "failed";
|
|
685
|
+
}
|
|
686
|
+
async function readFileAtRef(repoRoot, anchor) {
|
|
687
|
+
if (!filePathIsSafe(anchor.file))
|
|
688
|
+
return { ok: false, reason: "outside-repo" };
|
|
689
|
+
if (!anchor.ref || !refShapeIsSafe(anchor.ref)) {
|
|
690
|
+
return { ok: false, reason: "ref-unreadable" };
|
|
691
|
+
}
|
|
692
|
+
const blob = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
693
|
+
if (blob !== null) return { ok: true, source: blob };
|
|
694
|
+
return {
|
|
695
|
+
ok: false,
|
|
696
|
+
reason: await hasCommit(repoRoot, anchor.ref) ? "ref-unreadable" : "ref-unavailable"
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function hasCommit(repoRoot, ref) {
|
|
700
|
+
const found = await git2(repoRoot, [
|
|
701
|
+
"cat-file",
|
|
702
|
+
"-e",
|
|
703
|
+
"--end-of-options",
|
|
704
|
+
`${ref}^{commit}`
|
|
705
|
+
]);
|
|
706
|
+
return found.ok;
|
|
707
|
+
}
|
|
708
|
+
async function listRepoFiles(repoRoot) {
|
|
709
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
710
|
+
if (!result.ok) return [];
|
|
711
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
712
|
+
}
|
|
713
|
+
var DIFF_RANGE = /^(.+?)(\.{2,3})(.+)$/;
|
|
714
|
+
async function readRangeDiff(repoRoot, range, maxBytes = MAX_RANGE_DIFF_BYTES) {
|
|
715
|
+
const parts = DIFF_RANGE.exec(range);
|
|
716
|
+
if (!parts) return { ok: false, reason: "bad-range" };
|
|
717
|
+
const [, base2 = "", dots = "", head = ""] = parts;
|
|
718
|
+
if (!localRevShapeIsSafe(base2) || !localRevShapeIsSafe(head)) {
|
|
719
|
+
return { ok: false, reason: "bad-range" };
|
|
720
|
+
}
|
|
721
|
+
const result = await git2(
|
|
722
|
+
repoRoot,
|
|
723
|
+
[
|
|
724
|
+
"-c",
|
|
725
|
+
"core.quotePath=false",
|
|
726
|
+
"diff",
|
|
727
|
+
"--unified=0",
|
|
728
|
+
"--no-color",
|
|
729
|
+
"--no-ext-diff",
|
|
730
|
+
"--no-textconv",
|
|
731
|
+
"--find-renames",
|
|
732
|
+
"--src-prefix=a/",
|
|
733
|
+
"--dst-prefix=b/",
|
|
734
|
+
"--end-of-options",
|
|
735
|
+
`${base2}${dots}${head}`,
|
|
736
|
+
"--"
|
|
737
|
+
],
|
|
738
|
+
{ maxBytes, timeoutMs: RANGE_DIFF_TIMEOUT_MS }
|
|
739
|
+
);
|
|
740
|
+
if (result.ok) return { ok: true, text: result.stdout };
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
reason: result.reason === "failed" ? "bad-range" : result.reason
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function readOldSource(repoRoot, anchor) {
|
|
747
|
+
if (!filePathIsSafe(anchor.file))
|
|
748
|
+
return { ok: false, reason: "unrecoverable" };
|
|
749
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
750
|
+
const shown2 = await catBlob(repoRoot, anchor.ref, anchor.file);
|
|
751
|
+
if (shown2 !== null) {
|
|
752
|
+
return {
|
|
753
|
+
ok: true,
|
|
754
|
+
source: shown2,
|
|
755
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const at2 = anchor.resolved_at;
|
|
760
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
761
|
+
return { ok: false, reason: "unrecoverable" };
|
|
762
|
+
}
|
|
763
|
+
const found = await git2(repoRoot, [
|
|
764
|
+
"log",
|
|
765
|
+
"-1",
|
|
766
|
+
"--format=%H",
|
|
767
|
+
`--before=${at2}`,
|
|
768
|
+
"--end-of-options",
|
|
769
|
+
"HEAD",
|
|
770
|
+
"--",
|
|
771
|
+
anchor.file
|
|
772
|
+
]);
|
|
773
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
774
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
775
|
+
return { ok: false, reason: "unrecoverable" };
|
|
776
|
+
const shown = await catBlob(repoRoot, sha, anchor.file);
|
|
777
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
778
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
779
|
+
}
|
|
780
|
+
async function catBlob(repoRoot, ref, file) {
|
|
781
|
+
const path = file.replace(/^\.\//, "");
|
|
782
|
+
const result = await git2(repoRoot, [
|
|
783
|
+
"cat-file",
|
|
784
|
+
"blob",
|
|
785
|
+
"--end-of-options",
|
|
786
|
+
`${ref}:${path}`
|
|
787
|
+
]);
|
|
788
|
+
return result.ok ? result.stdout : null;
|
|
789
|
+
}
|
|
790
|
+
|
|
494
791
|
// src/remote-repo/cache.ts
|
|
495
792
|
var import_node_os = require("os");
|
|
496
793
|
var import_node_path = require("path");
|
|
497
794
|
|
|
498
795
|
// src/anchor-resolver/repo-identity.ts
|
|
499
|
-
var
|
|
500
|
-
var
|
|
501
|
-
var
|
|
796
|
+
var import_node_child_process3 = require("child_process");
|
|
797
|
+
var import_node_util3 = require("util");
|
|
798
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
502
799
|
function normalizeRepoUrl(value) {
|
|
503
800
|
let url = value.trim().replace(/^git\+/, "");
|
|
504
801
|
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
@@ -533,7 +830,7 @@ function repoIdentifies(declared, originUrl) {
|
|
|
533
830
|
}
|
|
534
831
|
async function repoOriginUrl(repoRoot) {
|
|
535
832
|
try {
|
|
536
|
-
const { stdout } = await
|
|
833
|
+
const { stdout } = await execFileAsync3(
|
|
537
834
|
"git",
|
|
538
835
|
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
539
836
|
{ timeout: 5e3 }
|
|
@@ -605,7 +902,9 @@ function revRef(rev) {
|
|
|
605
902
|
var UNCHECKED_REASONS = [
|
|
606
903
|
"remote-unreachable",
|
|
607
904
|
"repo-unauthorized",
|
|
608
|
-
"default-branch-unknown"
|
|
905
|
+
"default-branch-unknown",
|
|
906
|
+
/** Local, but the same finding: a shallow clone has no rev to read. */
|
|
907
|
+
"ref-unavailable"
|
|
609
908
|
];
|
|
610
909
|
function isUncheckedReason(reason) {
|
|
611
910
|
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
@@ -616,137 +915,34 @@ function wantKey(repo, ref, file) {
|
|
|
616
915
|
|
|
617
916
|
// src/remote-repo/read.ts
|
|
618
917
|
var import_promises = require("fs/promises");
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
delete env[name];
|
|
918
|
+
var DEFAULT_REPO_CONCURRENCY = 4;
|
|
919
|
+
var IMMUTABLE_REV = /^[0-9a-f]{40}$/;
|
|
920
|
+
async function readRemoteAnchors(wants, options = {}) {
|
|
921
|
+
const out = /* @__PURE__ */ new Map();
|
|
922
|
+
if (!wants.length) return out;
|
|
923
|
+
const cacheDir = repoCacheDir(options.cacheDir);
|
|
924
|
+
const timeoutMs = fetchTimeoutMs(options.fetchTimeoutMs);
|
|
925
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const want of wants) {
|
|
927
|
+
const key2 = normalizeRepoUrl(want.repo);
|
|
928
|
+
const group2 = byRepo.get(key2) ?? { url: want.repo.trim(), wants: [] };
|
|
929
|
+
group2.wants.push(want);
|
|
930
|
+
byRepo.set(key2, group2);
|
|
633
931
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const failure = error;
|
|
649
|
-
return {
|
|
650
|
-
ok: false,
|
|
651
|
-
stdout: failure.stdout ?? "",
|
|
652
|
-
stderr: failure.stderr ?? "",
|
|
653
|
-
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
function transportReason(stderr) {
|
|
658
|
-
const text = stderr.toLowerCase();
|
|
659
|
-
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
660
|
-
return "repo-unauthorized";
|
|
661
|
-
}
|
|
662
|
-
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
663
|
-
return "ref-not-found";
|
|
664
|
-
}
|
|
665
|
-
return "remote-unreachable";
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// src/remote-repo/validate.ts
|
|
669
|
-
var MAX_REF_LENGTH = 200;
|
|
670
|
-
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
671
|
-
function refShapeIsSafe(ref) {
|
|
672
|
-
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
673
|
-
if (ref.includes("..")) return false;
|
|
674
|
-
return REF_SHAPE.test(ref);
|
|
675
|
-
}
|
|
676
|
-
async function refIsWellFormed(ref) {
|
|
677
|
-
if (!refShapeIsSafe(ref)) return false;
|
|
678
|
-
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
679
|
-
return checked.ok;
|
|
680
|
-
}
|
|
681
|
-
function filePathIsSafe(file) {
|
|
682
|
-
const path = file.replace(/^\.\//, "");
|
|
683
|
-
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
684
|
-
return !path.split("/").includes("..");
|
|
685
|
-
}
|
|
686
|
-
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
687
|
-
function allowedProtocols() {
|
|
688
|
-
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
689
|
-
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
690
|
-
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
691
|
-
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
692
|
-
}
|
|
693
|
-
function isShortRepoName(repo) {
|
|
694
|
-
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
695
|
-
}
|
|
696
|
-
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
697
|
-
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
698
|
-
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
699
|
-
function repoUrlIsSafe(repo) {
|
|
700
|
-
const url = repo.trim();
|
|
701
|
-
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
702
|
-
const allowed = allowedProtocols();
|
|
703
|
-
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
704
|
-
const scheme = URL_SCHEME.exec(url);
|
|
705
|
-
if (!scheme?.[1]) return false;
|
|
706
|
-
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
707
|
-
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
708
|
-
const at2 = authority.lastIndexOf("@");
|
|
709
|
-
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
710
|
-
}
|
|
711
|
-
function protocolArgs() {
|
|
712
|
-
const allowed = allowedProtocols();
|
|
713
|
-
return [
|
|
714
|
-
"-c",
|
|
715
|
-
"protocol.ext.allow=never",
|
|
716
|
-
"-c",
|
|
717
|
-
`protocol.file.allow=${allowed.includes("file") ? "user" : "never"}`
|
|
718
|
-
];
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// src/remote-repo/read.ts
|
|
722
|
-
var DEFAULT_REPO_CONCURRENCY = 4;
|
|
723
|
-
var IMMUTABLE_REV = /^[0-9a-f]{40}$/;
|
|
724
|
-
async function readRemoteAnchors(wants, options = {}) {
|
|
725
|
-
const out = /* @__PURE__ */ new Map();
|
|
726
|
-
if (!wants.length) return out;
|
|
727
|
-
const cacheDir = repoCacheDir(options.cacheDir);
|
|
728
|
-
const timeoutMs = fetchTimeoutMs(options.fetchTimeoutMs);
|
|
729
|
-
const byRepo = /* @__PURE__ */ new Map();
|
|
730
|
-
for (const want of wants) {
|
|
731
|
-
const key = normalizeRepoUrl(want.repo);
|
|
732
|
-
const group2 = byRepo.get(key) ?? { url: want.repo.trim(), wants: [] };
|
|
733
|
-
group2.wants.push(want);
|
|
734
|
-
byRepo.set(key, group2);
|
|
735
|
-
}
|
|
736
|
-
const groups = [...byRepo.entries()];
|
|
737
|
-
const results = await mapLimit(
|
|
738
|
-
groups,
|
|
739
|
-
Math.max(1, options.concurrency ?? DEFAULT_REPO_CONCURRENCY),
|
|
740
|
-
([repo, group2]) => readOneRepo(repo, group2.url, group2.wants, {
|
|
741
|
-
cacheDir,
|
|
742
|
-
timeoutMs,
|
|
743
|
-
offline: options.offline === true
|
|
744
|
-
})
|
|
745
|
-
);
|
|
746
|
-
for (const result of results) {
|
|
747
|
-
for (const [key, read] of result) out.set(key, read);
|
|
748
|
-
}
|
|
749
|
-
return out;
|
|
932
|
+
const groups = [...byRepo.entries()];
|
|
933
|
+
const results = await mapLimit(
|
|
934
|
+
groups,
|
|
935
|
+
Math.max(1, options.concurrency ?? DEFAULT_REPO_CONCURRENCY),
|
|
936
|
+
([repo, group2]) => readOneRepo(repo, group2.url, group2.wants, {
|
|
937
|
+
cacheDir,
|
|
938
|
+
timeoutMs,
|
|
939
|
+
offline: options.offline === true
|
|
940
|
+
})
|
|
941
|
+
);
|
|
942
|
+
for (const result of results) {
|
|
943
|
+
for (const [key2, read] of result) out.set(key2, read);
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
750
946
|
}
|
|
751
947
|
async function readOneRepo(repo, url, declared, context) {
|
|
752
948
|
let wants = declared;
|
|
@@ -986,6 +1182,7 @@ function looksLikeWrongRepoRoot(drift) {
|
|
|
986
1182
|
for (const entries of drift.values()) {
|
|
987
1183
|
for (const entry of entries) {
|
|
988
1184
|
if (entry.repo !== void 0) continue;
|
|
1185
|
+
if (entry.side === "old") continue;
|
|
989
1186
|
checked += 1;
|
|
990
1187
|
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
991
1188
|
return false;
|
|
@@ -1121,7 +1318,7 @@ function size(bytes) {
|
|
|
1121
1318
|
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1122
1319
|
}
|
|
1123
1320
|
function pause(ms) {
|
|
1124
|
-
return new Promise((
|
|
1321
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1125
1322
|
}
|
|
1126
1323
|
|
|
1127
1324
|
// src/grammars/manifest.ts
|
|
@@ -1184,8 +1381,8 @@ async function ensureGrammar(language, options = {}) {
|
|
|
1184
1381
|
if (!pack2) return null;
|
|
1185
1382
|
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1186
1383
|
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1187
|
-
const
|
|
1188
|
-
const existing = inFlight.get(
|
|
1384
|
+
const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1385
|
+
const existing = inFlight.get(key2);
|
|
1189
1386
|
if (existing) return existing;
|
|
1190
1387
|
const pending = (async () => {
|
|
1191
1388
|
const grammar = await ensurePart(
|
|
@@ -1209,9 +1406,9 @@ ${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
|
|
|
1209
1406
|
missing.delete(language);
|
|
1210
1407
|
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1211
1408
|
})();
|
|
1212
|
-
inFlight.set(
|
|
1409
|
+
inFlight.set(key2, pending);
|
|
1213
1410
|
const result = await pending;
|
|
1214
|
-
if (result === null) inFlight.delete(
|
|
1411
|
+
if (result === null) inFlight.delete(key2);
|
|
1215
1412
|
return result;
|
|
1216
1413
|
}
|
|
1217
1414
|
async function ensurePart(path, name, entry, options) {
|
|
@@ -1505,8 +1702,8 @@ var TreeSitterResolver = class {
|
|
|
1505
1702
|
}
|
|
1506
1703
|
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1507
1704
|
parse(language, loaded, source) {
|
|
1508
|
-
const
|
|
1509
|
-
const cached2 = this.trees.get(
|
|
1705
|
+
const key2 = `${language}:${(0, import_node_crypto2.createHash)("sha256").update(source).digest("hex")}`;
|
|
1706
|
+
const cached2 = this.trees.get(key2);
|
|
1510
1707
|
if (cached2) {
|
|
1511
1708
|
this.stats.cacheHits += 1;
|
|
1512
1709
|
return cached2;
|
|
@@ -1530,7 +1727,7 @@ var TreeSitterResolver = class {
|
|
|
1530
1727
|
this.trees.delete(oldest.value);
|
|
1531
1728
|
}
|
|
1532
1729
|
}
|
|
1533
|
-
this.trees.set(
|
|
1730
|
+
this.trees.set(key2, parsed);
|
|
1534
1731
|
return parsed;
|
|
1535
1732
|
}
|
|
1536
1733
|
/**
|
|
@@ -1690,8 +1887,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1690
1887
|
}
|
|
1691
1888
|
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1692
1889
|
function captureIndentedBlock(lines, matchLine) {
|
|
1693
|
-
const
|
|
1694
|
-
const indent =
|
|
1890
|
+
const header2 = lines[matchLine] ?? "";
|
|
1891
|
+
const indent = header2.length - header2.trimStart().length;
|
|
1695
1892
|
let headerEnd = -1;
|
|
1696
1893
|
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1697
1894
|
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
@@ -1712,42 +1909,55 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1712
1909
|
}
|
|
1713
1910
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1714
1911
|
}
|
|
1715
|
-
var
|
|
1716
|
-
(name
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1912
|
+
var declarationTier = (name) => new RegExp(
|
|
1913
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
1914
|
+
);
|
|
1915
|
+
var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
|
|
1916
|
+
var anchoredAssignmentTier = (name) => new RegExp(
|
|
1917
|
+
`^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
|
|
1918
|
+
);
|
|
1919
|
+
var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
|
|
1920
|
+
var MENTION_TIERS = [
|
|
1720
1921
|
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1721
1922
|
(name) => new RegExp(`\\b${name}\\b`)
|
|
1722
1923
|
];
|
|
1924
|
+
var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
|
|
1925
|
+
function resolveWith(tiers, source, symbol) {
|
|
1926
|
+
const segments = symbol.split(".");
|
|
1927
|
+
const name = segments[segments.length - 1];
|
|
1928
|
+
if (!name) return null;
|
|
1929
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
1930
|
+
const escaped = escapeRegExp(name);
|
|
1931
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
1932
|
+
const lines = source.split("\n");
|
|
1933
|
+
for (const tier of tiers) {
|
|
1934
|
+
const pattern = tier(escaped);
|
|
1935
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1936
|
+
if (!candidates.length) continue;
|
|
1937
|
+
if (parentPattern && candidates.length > 1) {
|
|
1938
|
+
const distances = candidates.map(
|
|
1939
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1940
|
+
);
|
|
1941
|
+
const nearest = Math.min(...distances);
|
|
1942
|
+
if (Number.isFinite(nearest)) {
|
|
1943
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (candidates.length !== 1) return null;
|
|
1947
|
+
const matchLine = candidates[0];
|
|
1948
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
1949
|
+
}
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1723
1952
|
var regexResolver = {
|
|
1724
1953
|
name: "regex",
|
|
1725
1954
|
resolve(source, symbol) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1732
|
-
const lines = source.split("\n");
|
|
1733
|
-
for (const tier of TIERS) {
|
|
1734
|
-
const pattern = tier(escaped);
|
|
1735
|
-
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1736
|
-
if (!candidates.length) continue;
|
|
1737
|
-
if (parentPattern && candidates.length > 1) {
|
|
1738
|
-
const distances = candidates.map(
|
|
1739
|
-
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1740
|
-
);
|
|
1741
|
-
const nearest = Math.min(...distances);
|
|
1742
|
-
if (Number.isFinite(nearest)) {
|
|
1743
|
-
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
if (candidates.length !== 1) return null;
|
|
1747
|
-
const matchLine = candidates[0];
|
|
1748
|
-
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
1749
|
-
}
|
|
1750
|
-
return null;
|
|
1955
|
+
return resolveWith(TIERS, source, symbol);
|
|
1956
|
+
},
|
|
1957
|
+
attempt(source, symbol, _file, options) {
|
|
1958
|
+
const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
|
|
1959
|
+
const span2 = resolveWith(tiers, source, symbol);
|
|
1960
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1751
1961
|
}
|
|
1752
1962
|
};
|
|
1753
1963
|
function escapeRegExp(value) {
|
|
@@ -1765,6 +1975,7 @@ function hashAnchorText(text) {
|
|
|
1765
1975
|
}
|
|
1766
1976
|
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1767
1977
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1978
|
+
if (anchor.span) return sliceSpan(normalized, anchor.span);
|
|
1768
1979
|
if (!anchor.symbol) {
|
|
1769
1980
|
const lines = normalized.split("\n");
|
|
1770
1981
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
@@ -1777,11 +1988,17 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1777
1988
|
}
|
|
1778
1989
|
};
|
|
1779
1990
|
}
|
|
1991
|
+
let afterParsedMiss = false;
|
|
1780
1992
|
for (const resolver of resolvers) {
|
|
1781
|
-
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file
|
|
1993
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
|
|
1994
|
+
afterParsedMiss
|
|
1995
|
+
}) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1782
1996
|
if (attempt.kind === "abstain") continue;
|
|
1783
1997
|
if (attempt.kind === "unresolved") {
|
|
1784
|
-
if (attempt.reason === "symbol-not-found")
|
|
1998
|
+
if (attempt.reason === "symbol-not-found") {
|
|
1999
|
+
if (resolver.attempt) afterParsedMiss = true;
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
1785
2002
|
return { ok: false, reason: attempt.reason };
|
|
1786
2003
|
}
|
|
1787
2004
|
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
@@ -1794,12 +2011,28 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
|
1794
2011
|
}
|
|
1795
2012
|
return { ok: false, reason: "symbol-not-found" };
|
|
1796
2013
|
}
|
|
2014
|
+
function sliceSpan(source, range) {
|
|
2015
|
+
const lines = source.split("\n");
|
|
2016
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
2017
|
+
if (range.end > lines.length) {
|
|
2018
|
+
return { ok: false, reason: "span-out-of-range" };
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
span: {
|
|
2023
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
2024
|
+
startLine: range.start,
|
|
2025
|
+
endLine: range.end
|
|
2026
|
+
},
|
|
2027
|
+
resolver: "span"
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
1797
2030
|
function fromResolve(resolver, source, symbol, file) {
|
|
1798
2031
|
const span2 = resolver.resolve(source, symbol, file);
|
|
1799
2032
|
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1800
2033
|
}
|
|
1801
2034
|
function isResolverName(name) {
|
|
1802
|
-
return name === "tree-sitter" || name === "regex";
|
|
2035
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
1803
2036
|
}
|
|
1804
2037
|
async function prepareResolvers(resolvers, files) {
|
|
1805
2038
|
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
@@ -1818,6 +2051,9 @@ function resolverChanged(source, anchor, produced) {
|
|
|
1818
2051
|
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1819
2052
|
}
|
|
1820
2053
|
function anchorHashOf(anchor, outcome) {
|
|
2054
|
+
if (outcome.resolver === "span") {
|
|
2055
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
2056
|
+
}
|
|
1821
2057
|
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1822
2058
|
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1823
2059
|
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
@@ -1851,37 +2087,60 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1851
2087
|
}
|
|
1852
2088
|
}
|
|
1853
2089
|
const files = [];
|
|
2090
|
+
const committedWants = [];
|
|
1854
2091
|
const wants = [];
|
|
1855
2092
|
for (const entries of planned.values()) {
|
|
1856
2093
|
for (const { anchor, foreign } of entries) {
|
|
1857
|
-
if (
|
|
1858
|
-
else
|
|
2094
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
2095
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
2096
|
+
else files.push(anchor.file);
|
|
1859
2097
|
}
|
|
1860
2098
|
}
|
|
1861
|
-
const [reads, remote] = await Promise.all([
|
|
2099
|
+
const [reads, committed, remote] = await Promise.all([
|
|
1862
2100
|
readAnchorFiles(
|
|
1863
2101
|
files,
|
|
1864
2102
|
options.reader ?? anchorFileReader(repoRoot),
|
|
1865
2103
|
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1866
2104
|
),
|
|
2105
|
+
readCommitted(repoRoot, committedWants, options),
|
|
1867
2106
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1868
2107
|
]);
|
|
1869
2108
|
await prepareResolvers(resolvers, [
|
|
1870
2109
|
...files,
|
|
2110
|
+
...committedWants.map((anchor) => anchor.file),
|
|
1871
2111
|
...wants.map((want) => want.file)
|
|
1872
2112
|
]);
|
|
1873
2113
|
const drift = /* @__PURE__ */ new Map();
|
|
1874
2114
|
for (const record of records) {
|
|
1875
2115
|
const entries = [];
|
|
1876
2116
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2117
|
+
if (foreign) {
|
|
2118
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2122
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
1880
2123
|
}
|
|
1881
2124
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
1882
2125
|
}
|
|
1883
2126
|
return drift;
|
|
1884
2127
|
}
|
|
2128
|
+
function atRefKey(anchor) {
|
|
2129
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
2130
|
+
}
|
|
2131
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
2132
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
2133
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
2134
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2135
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
2136
|
+
const keys = [...byKey.keys()];
|
|
2137
|
+
const results = await mapLimit(
|
|
2138
|
+
keys,
|
|
2139
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2140
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
2141
|
+
);
|
|
2142
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2143
|
+
}
|
|
1885
2144
|
function remoteWants(anchor) {
|
|
1886
2145
|
const repo = anchor.repo;
|
|
1887
2146
|
const wants = [{ repo, file: anchor.file }];
|
|
@@ -1892,6 +2151,7 @@ function base(anchor) {
|
|
|
1892
2151
|
return {
|
|
1893
2152
|
file: anchor.file,
|
|
1894
2153
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2154
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
1895
2155
|
storedHash: anchor.hash
|
|
1896
2156
|
};
|
|
1897
2157
|
}
|
|
@@ -1905,9 +2165,15 @@ function unresolved(anchor, reason, repo) {
|
|
|
1905
2165
|
...classOf(reason)
|
|
1906
2166
|
};
|
|
1907
2167
|
}
|
|
2168
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
2169
|
+
"file-missing",
|
|
2170
|
+
"symbol-not-found",
|
|
2171
|
+
"span-out-of-range",
|
|
2172
|
+
"ref-unreadable"
|
|
2173
|
+
]);
|
|
1908
2174
|
function provisionalDriftClass(entry) {
|
|
1909
2175
|
if (entry.state === "unresolved") {
|
|
1910
|
-
return
|
|
2176
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
1911
2177
|
}
|
|
1912
2178
|
return entry.state === "drifted" ? "changed" : void 0;
|
|
1913
2179
|
}
|
|
@@ -1959,9 +2225,9 @@ function localEntry(anchor, read, resolvers) {
|
|
|
1959
2225
|
}
|
|
1960
2226
|
function remoteEntry(anchor, remote, resolvers) {
|
|
1961
2227
|
const repo = anchor.repo;
|
|
1962
|
-
const
|
|
1963
|
-
const atDefault = remote.get(wantKey(
|
|
1964
|
-
const primary = anchor.ref ? remote.get(wantKey(
|
|
2228
|
+
const key2 = normalizeRepoUrl(repo);
|
|
2229
|
+
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
2230
|
+
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1965
2231
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1966
2232
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1967
2233
|
const found = hashIn(primary.source, anchor, resolvers);
|
|
@@ -1976,6 +2242,13 @@ function remoteEntry(anchor, remote, resolvers) {
|
|
|
1976
2242
|
remoteState: "drifted-from-ref"
|
|
1977
2243
|
});
|
|
1978
2244
|
}
|
|
2245
|
+
if (anchor.side === "old") {
|
|
2246
|
+
return compared(anchor, current, {
|
|
2247
|
+
repo,
|
|
2248
|
+
...extras,
|
|
2249
|
+
remoteState: "matches-ref"
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
1979
2252
|
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1980
2253
|
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1981
2254
|
...compared(anchor, head.current, {
|
|
@@ -2129,6 +2402,89 @@ var KbMissingFlagValueError = class extends BaseError {
|
|
|
2129
2402
|
}
|
|
2130
2403
|
flag;
|
|
2131
2404
|
};
|
|
2405
|
+
var KbClassifyInputError = class extends BaseError {
|
|
2406
|
+
constructor(reason) {
|
|
2407
|
+
super({
|
|
2408
|
+
message: `classify: ${reason}`,
|
|
2409
|
+
errorType: "KbClassifyInput" /* KbClassifyInput */,
|
|
2410
|
+
code: 400,
|
|
2411
|
+
fault: "User" /* User */,
|
|
2412
|
+
retriable: false,
|
|
2413
|
+
reportToUser: true,
|
|
2414
|
+
details: { reason }
|
|
2415
|
+
});
|
|
2416
|
+
this.reason = reason;
|
|
2417
|
+
}
|
|
2418
|
+
reason;
|
|
2419
|
+
};
|
|
2420
|
+
var KbPromoteCollisionError = class extends BaseError {
|
|
2421
|
+
constructor(conceptId2, to) {
|
|
2422
|
+
super({
|
|
2423
|
+
message: `kb: ${to} already holds ${conceptId2} \u2014 re-run with force to overwrite it`,
|
|
2424
|
+
errorType: "KbPromoteCollision" /* KbPromoteCollision */,
|
|
2425
|
+
code: 409,
|
|
2426
|
+
fault: "User" /* User */,
|
|
2427
|
+
retriable: false,
|
|
2428
|
+
reportToUser: true,
|
|
2429
|
+
details: { conceptId: conceptId2, to, action: "refused" }
|
|
2430
|
+
});
|
|
2431
|
+
this.conceptId = conceptId2;
|
|
2432
|
+
this.to = to;
|
|
2433
|
+
}
|
|
2434
|
+
conceptId;
|
|
2435
|
+
to;
|
|
2436
|
+
};
|
|
2437
|
+
var KbPromoteStandingError = class extends BaseError {
|
|
2438
|
+
constructor(conceptId2, standing) {
|
|
2439
|
+
super({
|
|
2440
|
+
message: `kb: ${conceptId2} is ${standing} \u2014 only a record that still stands can be promoted`,
|
|
2441
|
+
errorType: "KbPromoteStanding" /* KbPromoteStanding */,
|
|
2442
|
+
code: 409,
|
|
2443
|
+
fault: "User" /* User */,
|
|
2444
|
+
retriable: false,
|
|
2445
|
+
reportToUser: true,
|
|
2446
|
+
details: { conceptId: conceptId2, standing, action: "refused" }
|
|
2447
|
+
});
|
|
2448
|
+
this.conceptId = conceptId2;
|
|
2449
|
+
this.standing = standing;
|
|
2450
|
+
}
|
|
2451
|
+
conceptId;
|
|
2452
|
+
standing;
|
|
2453
|
+
};
|
|
2454
|
+
var KbPromoteSelfError = class extends BaseError {
|
|
2455
|
+
constructor(to) {
|
|
2456
|
+
super({
|
|
2457
|
+
message: `kb: ${to} is the base being promoted from \u2014 name a different target`,
|
|
2458
|
+
errorType: "KbPromoteSelf" /* KbPromoteSelf */,
|
|
2459
|
+
code: 400,
|
|
2460
|
+
fault: "User" /* User */,
|
|
2461
|
+
retriable: false,
|
|
2462
|
+
reportToUser: true,
|
|
2463
|
+
details: { to, action: "refused" }
|
|
2464
|
+
});
|
|
2465
|
+
this.to = to;
|
|
2466
|
+
}
|
|
2467
|
+
to;
|
|
2468
|
+
};
|
|
2469
|
+
var KbPromoteStoppedError = class extends BaseError {
|
|
2470
|
+
constructor(conceptId2, landed, reason) {
|
|
2471
|
+
super({
|
|
2472
|
+
message: `kb: promotion stopped at ${conceptId2} (${reason}) \u2014 landed: ${landed.length ? landed.join(", ") : "nothing"}`,
|
|
2473
|
+
errorType: "KbPromoteStopped" /* KbPromoteStopped */,
|
|
2474
|
+
code: 500,
|
|
2475
|
+
fault: "System" /* System */,
|
|
2476
|
+
retriable: false,
|
|
2477
|
+
reportToUser: true,
|
|
2478
|
+
details: { conceptId: conceptId2, landed, reason, action: "stopped" }
|
|
2479
|
+
});
|
|
2480
|
+
this.conceptId = conceptId2;
|
|
2481
|
+
this.landed = landed;
|
|
2482
|
+
this.reason = reason;
|
|
2483
|
+
}
|
|
2484
|
+
conceptId;
|
|
2485
|
+
landed;
|
|
2486
|
+
reason;
|
|
2487
|
+
};
|
|
2132
2488
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
2133
2489
|
constructor(message, details) {
|
|
2134
2490
|
super({
|
|
@@ -2177,15 +2533,18 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
2177
2533
|
function asBudgets(value) {
|
|
2178
2534
|
if (value === null || typeof value !== "object") return {};
|
|
2179
2535
|
const table2 = value;
|
|
2180
|
-
const pick = (
|
|
2181
|
-
const
|
|
2182
|
-
return typeof
|
|
2536
|
+
const pick = (key2, min) => {
|
|
2537
|
+
const raw2 = table2[key2];
|
|
2538
|
+
return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
|
|
2183
2539
|
};
|
|
2184
2540
|
const budgetTokens = pick("budgetTokens", 1);
|
|
2185
2541
|
const fullUnderTokens = pick("fullUnderTokens", 0);
|
|
2542
|
+
const raw = table2["excludeTags"];
|
|
2543
|
+
const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
2186
2544
|
return {
|
|
2187
2545
|
...budgetTokens ? { budgetTokens } : {},
|
|
2188
|
-
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
|
|
2546
|
+
...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
|
|
2547
|
+
...excludeTags ? { excludeTags } : {}
|
|
2189
2548
|
};
|
|
2190
2549
|
}
|
|
2191
2550
|
function contextProfileBudgets(manifest, profile) {
|
|
@@ -2472,6 +2831,9 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
2472
2831
|
var import_zod6 = require("zod");
|
|
2473
2832
|
var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2474
2833
|
var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2834
|
+
var TAGS = import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
|
|
2835
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
2836
|
+
);
|
|
2475
2837
|
var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
|
|
2476
2838
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2477
2839
|
);
|
|
@@ -2493,6 +2855,41 @@ function argvFlag(argv, name) {
|
|
|
2493
2855
|
}
|
|
2494
2856
|
return value;
|
|
2495
2857
|
}
|
|
2858
|
+
function argvFlags(argv, name) {
|
|
2859
|
+
const values = [];
|
|
2860
|
+
for (const [at2, arg] of argv.entries()) {
|
|
2861
|
+
if (arg.startsWith(`${name}=`)) {
|
|
2862
|
+
const value = arg.slice(name.length + 1);
|
|
2863
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
2864
|
+
values.push(value);
|
|
2865
|
+
} else if (arg === name) {
|
|
2866
|
+
const value = argv[at2 + 1];
|
|
2867
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2868
|
+
throw new KbMissingFlagValueError(name);
|
|
2869
|
+
}
|
|
2870
|
+
values.push(value);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return values;
|
|
2874
|
+
}
|
|
2875
|
+
function argvWithout(argv, ...names) {
|
|
2876
|
+
const kept = [];
|
|
2877
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
2878
|
+
const arg = argv[at2];
|
|
2879
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
2880
|
+
if (names.includes(arg)) {
|
|
2881
|
+
at2 += 1;
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
kept.push(arg);
|
|
2885
|
+
}
|
|
2886
|
+
return kept;
|
|
2887
|
+
}
|
|
2888
|
+
function argvPositional(argv, ...names) {
|
|
2889
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
2890
|
+
(arg) => !arg.startsWith("--")
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2496
2893
|
|
|
2497
2894
|
// src/commands/anchor-resolve.ts
|
|
2498
2895
|
function resolverSummary(results) {
|
|
@@ -2556,6 +2953,7 @@ var anchorResolveCommand = define({
|
|
|
2556
2953
|
const base2 = {
|
|
2557
2954
|
file: anchor.file,
|
|
2558
2955
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2956
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
2559
2957
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
2560
2958
|
// and now resolves to nothing is a broken anchor, and the exit code
|
|
2561
2959
|
// has to be able to tell it from one nobody ever stamped.
|
|
@@ -2732,12 +3130,18 @@ async function readSources(anchors, root, offline) {
|
|
|
2732
3130
|
const foreign = new Map(
|
|
2733
3131
|
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
2734
3132
|
);
|
|
2735
|
-
const local = anchors.filter(
|
|
3133
|
+
const local = anchors.filter(
|
|
3134
|
+
(anchor) => !foreign.get(anchor) && anchor.side !== "old"
|
|
3135
|
+
);
|
|
3136
|
+
const committed = anchors.filter(
|
|
3137
|
+
(anchor) => !foreign.get(anchor) && anchor.side === "old"
|
|
3138
|
+
);
|
|
2736
3139
|
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
2737
3140
|
const reads = await readAnchorFiles(
|
|
2738
3141
|
local.map((anchor) => anchor.file),
|
|
2739
3142
|
anchorFileReader(root)
|
|
2740
3143
|
);
|
|
3144
|
+
const atRef = await readCommitted(root, committed);
|
|
2741
3145
|
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
2742
3146
|
offline
|
|
2743
3147
|
});
|
|
@@ -2749,11 +3153,18 @@ async function readSources(anchors, root, offline) {
|
|
|
2749
3153
|
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
2750
3154
|
);
|
|
2751
3155
|
}
|
|
3156
|
+
for (const anchor of committed) {
|
|
3157
|
+
const read = atRef.get(atRefKey(anchor));
|
|
3158
|
+
sources.set(
|
|
3159
|
+
anchor,
|
|
3160
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
2752
3163
|
for (const anchor of remote) {
|
|
2753
3164
|
const repo = anchor.repo;
|
|
2754
|
-
const
|
|
2755
|
-
const atDefault = blobs.get(wantKey(
|
|
2756
|
-
const primary = anchor.ref ? blobs.get(wantKey(
|
|
3165
|
+
const key2 = normalizeRepoUrl(repo);
|
|
3166
|
+
const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
|
|
3167
|
+
const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
2757
3168
|
if (!primary?.ok) {
|
|
2758
3169
|
sources.set(anchor, {
|
|
2759
3170
|
ok: false,
|
|
@@ -2925,6 +3336,13 @@ function successors(record, byId) {
|
|
|
2925
3336
|
return { records, missing: missing2 };
|
|
2926
3337
|
}
|
|
2927
3338
|
|
|
3339
|
+
// src/kb-tags.ts
|
|
3340
|
+
function matchesTags(record, filter) {
|
|
3341
|
+
if (!filter.tags?.length && !filter.excludeTags?.length) return true;
|
|
3342
|
+
const carried = new Set(record.frontmatter.tags ?? []);
|
|
3343
|
+
return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
|
|
3344
|
+
}
|
|
3345
|
+
|
|
2928
3346
|
// src/catalog.ts
|
|
2929
3347
|
var EMPTY_STANDINGS = {
|
|
2930
3348
|
current: 0,
|
|
@@ -2935,7 +3353,7 @@ var EMPTY_STANDINGS = {
|
|
|
2935
3353
|
};
|
|
2936
3354
|
function catalog(bundle, options = {}) {
|
|
2937
3355
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2938
|
-
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
3356
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
|
|
2939
3357
|
conceptId: hit.record.conceptId,
|
|
2940
3358
|
type: hit.record.frontmatter.type,
|
|
2941
3359
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2943,14 +3361,14 @@ function catalog(bundle, options = {}) {
|
|
|
2943
3361
|
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2944
3362
|
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
2945
3363
|
})).sort(byTypeThenTitle);
|
|
2946
|
-
const
|
|
2947
|
-
for (const entry of entries)
|
|
3364
|
+
const standings2 = { ...EMPTY_STANDINGS };
|
|
3365
|
+
for (const entry of entries) standings2[entry.standing] += 1;
|
|
2948
3366
|
return {
|
|
2949
3367
|
entries,
|
|
2950
3368
|
recordCount: entries.length,
|
|
2951
|
-
standings,
|
|
2952
|
-
currentCount:
|
|
2953
|
-
supersededCount:
|
|
3369
|
+
standings: standings2,
|
|
3370
|
+
currentCount: standings2.current,
|
|
3371
|
+
supersededCount: standings2.superseded,
|
|
2954
3372
|
staleCount: entries.filter((entry) => entry.stale).length
|
|
2955
3373
|
};
|
|
2956
3374
|
}
|
|
@@ -2975,25 +3393,39 @@ function renderCatalogLine(entry) {
|
|
|
2975
3393
|
var catalogCommand = define({
|
|
2976
3394
|
name: "catalog",
|
|
2977
3395
|
tool: "kb_catalog",
|
|
2978
|
-
usage: "catalog [type]",
|
|
3396
|
+
usage: "catalog [type] [--tag T]...",
|
|
2979
3397
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2980
3398
|
input: import_zod10.z.object({
|
|
2981
3399
|
bundlePath,
|
|
2982
|
-
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
2983
|
-
|
|
2984
|
-
fromArgv: (argv, path) => ({
|
|
2985
|
-
bundlePath: path,
|
|
2986
|
-
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
3400
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
|
|
3401
|
+
tags: TAGS
|
|
2987
3402
|
}),
|
|
2988
|
-
|
|
2989
|
-
|
|
3403
|
+
fromArgv: (argv, path) => {
|
|
3404
|
+
const tags = argvFlags(argv, "--tag");
|
|
3405
|
+
const type = argvPositional(argv, "--tag");
|
|
3406
|
+
return {
|
|
3407
|
+
bundlePath: path,
|
|
3408
|
+
...type ? { type } : {},
|
|
3409
|
+
...tags.length ? { tags } : {}
|
|
3410
|
+
};
|
|
3411
|
+
},
|
|
3412
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => render(
|
|
3413
|
+
await store.catalog(path, {
|
|
3414
|
+
...type ? { type } : {},
|
|
3415
|
+
...tags ? { tags } : {}
|
|
3416
|
+
}),
|
|
2990
3417
|
path,
|
|
2991
|
-
type
|
|
3418
|
+
type,
|
|
3419
|
+
tags
|
|
2992
3420
|
)
|
|
2993
3421
|
});
|
|
2994
|
-
function render(result, bundle, type) {
|
|
3422
|
+
function render(result, bundle, type, tags) {
|
|
3423
|
+
const narrowed = [
|
|
3424
|
+
...type ? [type] : [],
|
|
3425
|
+
...tags?.length ? [`tags: ${tags.join(", ")}`] : []
|
|
3426
|
+
].join(" \xB7 ");
|
|
2995
3427
|
const lines = [
|
|
2996
|
-
`# KB Catalog${
|
|
3428
|
+
`# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
|
|
2997
3429
|
`bundle: ${bundle}`,
|
|
2998
3430
|
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
2999
3431
|
];
|
|
@@ -3005,7 +3437,7 @@ function render(result, bundle, type) {
|
|
|
3005
3437
|
lines.push("");
|
|
3006
3438
|
if (!result.entries.length) {
|
|
3007
3439
|
lines.push(
|
|
3008
|
-
|
|
3440
|
+
narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
|
|
3009
3441
|
);
|
|
3010
3442
|
} else {
|
|
3011
3443
|
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
@@ -3033,397 +3465,281 @@ function count(value, noun) {
|
|
|
3033
3465
|
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
3034
3466
|
}
|
|
3035
3467
|
|
|
3036
|
-
// src/commands/
|
|
3037
|
-
var
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
var
|
|
3041
|
-
|
|
3042
|
-
// src/kb-index.ts
|
|
3043
|
-
var INDEX_FILE = "INDEX.md";
|
|
3044
|
-
var HEADING = "# KB Index";
|
|
3045
|
-
function renderIndex(records) {
|
|
3046
|
-
const lines = [...records].sort((left, right) => left.conceptId.localeCompare(right.conceptId)).map(renderIndexLine);
|
|
3047
|
-
return `${HEADING}
|
|
3468
|
+
// src/commands/classify.ts
|
|
3469
|
+
var import_node_buffer = require("buffer");
|
|
3470
|
+
var import_promises7 = require("fs/promises");
|
|
3471
|
+
var import_node_path10 = require("path");
|
|
3472
|
+
var import_zod13 = require("zod");
|
|
3048
3473
|
|
|
3049
|
-
|
|
3050
|
-
|
|
3474
|
+
// src/match-diff.ts
|
|
3475
|
+
function matchToDiff(files, records, options = {}) {
|
|
3476
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
3477
|
+
const anchored = records.filter(
|
|
3478
|
+
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
3479
|
+
);
|
|
3480
|
+
const matches3 = [];
|
|
3481
|
+
for (const file of files) {
|
|
3482
|
+
const candidates = anchored.map((record) => ({
|
|
3483
|
+
record,
|
|
3484
|
+
anchors: (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3485
|
+
(anchor) => normalize(anchor.file) === normalize(file.filePath)
|
|
3486
|
+
)
|
|
3487
|
+
})).filter(({ anchors }) => anchors.length > 0);
|
|
3488
|
+
if (!candidates.length) continue;
|
|
3489
|
+
for (const hunk of file.hunks) {
|
|
3490
|
+
const hits = [];
|
|
3491
|
+
let precision = "symbol";
|
|
3492
|
+
for (const { record, anchors } of candidates) {
|
|
3493
|
+
const placement = place(anchors, file.filePath, hunk, ranges);
|
|
3494
|
+
if (placement.kind === "miss") continue;
|
|
3495
|
+
if (placement.kind === "file") precision = "file";
|
|
3496
|
+
hits.push(record);
|
|
3497
|
+
}
|
|
3498
|
+
if (!hits.length) continue;
|
|
3499
|
+
matches3.push({
|
|
3500
|
+
filePath: file.filePath,
|
|
3501
|
+
hunk,
|
|
3502
|
+
records: order(adjudicate(hits, records, options.now)),
|
|
3503
|
+
precision
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
return matches3;
|
|
3051
3508
|
}
|
|
3052
|
-
function
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3509
|
+
function placeOnHunk(record, filePath, hunk, symbolRanges = []) {
|
|
3510
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3511
|
+
(anchor) => normalize(anchor.file) === normalize(filePath)
|
|
3512
|
+
);
|
|
3513
|
+
return place(anchors, filePath, hunk, asIndex(symbolRanges));
|
|
3514
|
+
}
|
|
3515
|
+
function asIndex(ranges) {
|
|
3516
|
+
return isIndex(ranges) ? ranges : symbolRangeIndex(ranges);
|
|
3517
|
+
}
|
|
3518
|
+
function isIndex(ranges) {
|
|
3519
|
+
return !Array.isArray(ranges);
|
|
3520
|
+
}
|
|
3521
|
+
function place(anchors, filePath, hunk, ranges) {
|
|
3522
|
+
let fallback = { kind: "miss" };
|
|
3523
|
+
for (const anchor of anchors) {
|
|
3524
|
+
if (side(anchor.side) !== side(hunk.side)) continue;
|
|
3525
|
+
if (anchor.span) {
|
|
3526
|
+
if (overlaps(
|
|
3527
|
+
{ startLine: anchor.span.start, endLine: anchor.span.end },
|
|
3528
|
+
hunk
|
|
3529
|
+
)) {
|
|
3530
|
+
return { kind: "symbol", anchor };
|
|
3531
|
+
}
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
if (!anchor.symbol) return { kind: "file", anchor };
|
|
3535
|
+
const resolved = ranges.get(
|
|
3536
|
+
key(filePath, anchor.symbol, side(anchor.side))
|
|
3537
|
+
);
|
|
3538
|
+
if (!resolved?.length) {
|
|
3539
|
+
if (fallback.kind === "miss") fallback = { kind: "file", anchor };
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
if (resolved.some((range) => overlaps(range, hunk))) {
|
|
3543
|
+
return { kind: "symbol", anchor };
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return fallback;
|
|
3058
3547
|
}
|
|
3059
|
-
function
|
|
3060
|
-
return
|
|
3548
|
+
function side(value) {
|
|
3549
|
+
return value ?? "new";
|
|
3550
|
+
}
|
|
3551
|
+
function overlaps(range, hunk) {
|
|
3552
|
+
return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;
|
|
3553
|
+
}
|
|
3554
|
+
function order(records) {
|
|
3555
|
+
const rank = {
|
|
3556
|
+
current: 0,
|
|
3557
|
+
unsettled: 1,
|
|
3558
|
+
open: 2,
|
|
3559
|
+
superseded: 3,
|
|
3560
|
+
rejected: 4
|
|
3561
|
+
};
|
|
3562
|
+
return [...records].sort(
|
|
3563
|
+
(left, right) => (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) || (left.record.frontmatter.generated?.at ?? "").localeCompare(
|
|
3564
|
+
right.record.frontmatter.generated?.at ?? ""
|
|
3565
|
+
)
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
function symbolRangeIndex(ranges) {
|
|
3569
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3570
|
+
for (const range of ranges) {
|
|
3571
|
+
const id = key(range.file, range.symbol, side(range.side));
|
|
3572
|
+
byKey.set(id, [...byKey.get(id) ?? [], range]);
|
|
3573
|
+
}
|
|
3574
|
+
return byKey;
|
|
3575
|
+
}
|
|
3576
|
+
function key(file, symbol, at2) {
|
|
3577
|
+
return `${normalize(file)}#${symbol}#${at2}`;
|
|
3578
|
+
}
|
|
3579
|
+
function normalize(path) {
|
|
3580
|
+
return path.replace(/^\.\//, "");
|
|
3061
3581
|
}
|
|
3062
3582
|
|
|
3063
|
-
// src/
|
|
3064
|
-
var
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
"session-start": { fullUnderTokens: 1500 },
|
|
3068
|
-
compact: { budgetTokens: 2500 },
|
|
3069
|
-
turn: { budgetTokens: 2500 }
|
|
3583
|
+
// src/classify/model.ts
|
|
3584
|
+
var DEFAULT_THRESHOLDS = {
|
|
3585
|
+
boilerplate: 0.8,
|
|
3586
|
+
rename: 90
|
|
3070
3587
|
};
|
|
3071
|
-
|
|
3072
|
-
|
|
3588
|
+
|
|
3589
|
+
// src/classify/rules.ts
|
|
3590
|
+
var PATH_RULES = [
|
|
3591
|
+
{
|
|
3592
|
+
name: "test-path",
|
|
3593
|
+
class: "test",
|
|
3594
|
+
test: /(^|\/)(__tests__|__mocks__|tests?)\/|\.(spec|test)\.[^/]+$/
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
name: "ci-path",
|
|
3598
|
+
class: "ci",
|
|
3599
|
+
test: /(^|\/)\.github\/|(^|\/)(\.circleci|\.buildkite|\.gitlab|ci)\/[^/]*\.ya?ml$|(^|\/)Dockerfile(\.[^/]*)?$|\.tf$/
|
|
3600
|
+
},
|
|
3601
|
+
{
|
|
3602
|
+
name: "docs-path",
|
|
3603
|
+
class: "docs",
|
|
3604
|
+
test: /\.md$|(^|\/)docs\/|(^|\/)LICENSE(\.(md|txt|rst))?$/
|
|
3605
|
+
},
|
|
3606
|
+
{
|
|
3607
|
+
name: "lockfile-path",
|
|
3608
|
+
class: "lockfile",
|
|
3609
|
+
test: /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|Cargo\.lock|go\.sum)$/
|
|
3610
|
+
},
|
|
3611
|
+
{
|
|
3612
|
+
name: "config-path",
|
|
3613
|
+
class: "config",
|
|
3614
|
+
// `.jsonl` rides with `.json`: an append-only log of JSON is configuration
|
|
3615
|
+
// data too, and calling it source would send a reviewer to read it. Every
|
|
3616
|
+
// arm is anchored at both ends: `src/tsconfig-loader.ts` and `report.env.ts`
|
|
3617
|
+
// are source, not config.
|
|
3618
|
+
test: /\.(jsonc?|jsonl|ya?ml|toml|ini)$|(^|\/)\.env(?![^/]*\.[cm]?[jt]sx?$)([.-][^/]*)?$|[^/]+\.env$|(^|\/)tsconfig[^/]*\.json$|\.config\.[^/]+$|(^|\/)\.(eslintrc|prettierrc)[^/]*$/
|
|
3619
|
+
}
|
|
3620
|
+
];
|
|
3621
|
+
var HEADER_LINES = 20;
|
|
3622
|
+
var GENERATED_MARKERS = [
|
|
3623
|
+
/@generated\b/i,
|
|
3624
|
+
/\bdo not edit\b/i,
|
|
3625
|
+
/\bcode generated by\b/i,
|
|
3626
|
+
/\bthis file was automatically generated\b/i
|
|
3627
|
+
];
|
|
3628
|
+
var BOILERPLATE_SHAPES = [
|
|
3629
|
+
{ name: "import", test: /^import\b|^\}\s*from\s+["']/ },
|
|
3630
|
+
{ name: "re-export", test: /^export\s+(\*|\{|type\s*[{*])/ },
|
|
3631
|
+
{
|
|
3632
|
+
name: "class-shell",
|
|
3633
|
+
test: /^(export\s+)?(default\s+)?(abstract\s+)?class\s+[\w$]+[^{]*\{\s*\}$/
|
|
3634
|
+
},
|
|
3635
|
+
{ name: "punctuation", test: /^[{}()[\],;]+$/ }
|
|
3636
|
+
];
|
|
3637
|
+
function generatedMarker(lines) {
|
|
3638
|
+
for (const line of lines.slice(0, HEADER_LINES)) {
|
|
3639
|
+
const hit = GENERATED_MARKERS.find((marker) => marker.test(line));
|
|
3640
|
+
if (hit) return hit.source.replaceAll("\\b", "");
|
|
3641
|
+
}
|
|
3642
|
+
return void 0;
|
|
3073
3643
|
}
|
|
3074
|
-
function
|
|
3075
|
-
return
|
|
3076
|
-
HEADING2,
|
|
3077
|
-
"",
|
|
3078
|
-
"What follows is an index of this workspace's pinned knowledge bases \u2014",
|
|
3079
|
-
"concept ids, titles and standing only. The record bodies are NOT in this",
|
|
3080
|
-
"context.",
|
|
3081
|
-
"",
|
|
3082
|
-
"Consult records only through the strauss-kb MCP tools: `kb_load` (the",
|
|
3083
|
-
"preferred first call), `kb_query`, and `kb_trace`, passing the",
|
|
3084
|
-
"`bundlePath` listed with each base. Do not read record files directly:",
|
|
3085
|
-
"a raw file read bypasses supersession resolution, and a superseded or",
|
|
3086
|
-
"rejected record file reads exactly like a current one \u2014 only the store",
|
|
3087
|
-
"resolves chains and standing.",
|
|
3088
|
-
"",
|
|
3089
|
-
"KB content loaded earlier in a long session may have been compacted",
|
|
3090
|
-
"away. Before answering a question one of these bases governs, load it",
|
|
3091
|
-
"again at the point of use \u2014 reloading a small base costs a few thousand",
|
|
3092
|
-
"tokens."
|
|
3093
|
-
].join("\n");
|
|
3644
|
+
function isBoilerplateLine(line) {
|
|
3645
|
+
return BOILERPLATE_SHAPES.some((shape) => shape.test.test(line));
|
|
3094
3646
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3647
|
+
|
|
3648
|
+
// src/classify/classify.ts
|
|
3649
|
+
function classifyDiff(files, options = {}) {
|
|
3650
|
+
const overrides = currentOverrides(options.records ?? [], options.now);
|
|
3651
|
+
const ranges = symbolRangeIndex(options.symbolRanges ?? []);
|
|
3652
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
|
|
3653
|
+
return files.map((file) => classifyFile(file, overrides, ranges, thresholds));
|
|
3654
|
+
}
|
|
3655
|
+
var OVERRIDE_CLASS = /* @__PURE__ */ new Map([
|
|
3656
|
+
["review:generated", "generated"],
|
|
3657
|
+
["review:boilerplate", "boilerplate"],
|
|
3658
|
+
["review:move", "rename"]
|
|
3659
|
+
]);
|
|
3660
|
+
var WHOLE_FILE = {
|
|
3661
|
+
startLine: 1,
|
|
3662
|
+
endLine: Number.MAX_SAFE_INTEGER
|
|
3663
|
+
};
|
|
3664
|
+
function classifyFile(file, overrides, ranges, thresholds) {
|
|
3665
|
+
const whole = overrides.find(
|
|
3666
|
+
({ record }) => placeOnHunk(record, file.filePath, WHOLE_FILE, ranges).kind === "file"
|
|
3667
|
+
);
|
|
3668
|
+
const verdict = whole ? verdictOf(whole) : heuristic(file, thresholds);
|
|
3669
|
+
const hunks = file.hunks.map((hunk) => {
|
|
3670
|
+
const hit = whole ?? overrides.find(
|
|
3671
|
+
({ record }) => placeOnHunk(record, file.filePath, hunk, ranges).kind !== "miss"
|
|
3672
|
+
);
|
|
3098
3673
|
return {
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
body: "No readable records yet \u2014 pinned ahead of being populated."
|
|
3674
|
+
startLine: hunk.startLine,
|
|
3675
|
+
endLine: hunk.endLine,
|
|
3676
|
+
...hit ? verdictOf(hit) : verdict
|
|
3103
3677
|
};
|
|
3104
|
-
}
|
|
3105
|
-
const fullCap = pinMode === "full" ? budgetTokens : pinMode === "index" ? 0 : fullUnderTokens;
|
|
3106
|
-
let degradedFrom;
|
|
3107
|
-
if (fullCap > 0) {
|
|
3108
|
-
const full = await store.load(absolutePath, {
|
|
3109
|
-
budgetTokens: fullCap
|
|
3110
|
-
});
|
|
3111
|
-
if (!full.loaded && pinMode === "full") {
|
|
3112
|
-
degradedFrom = { approxTokens: full.approxTokens };
|
|
3113
|
-
}
|
|
3114
|
-
if (full.loaded) {
|
|
3115
|
-
const records = full.records.map(
|
|
3116
|
-
(hit) => [
|
|
3117
|
-
`#### ${hit.record.conceptId} \u2014 ${hit.record.frontmatter.title ?? "(untitled)"} (${hit.standing})`,
|
|
3118
|
-
"",
|
|
3119
|
-
hit.record.body.trim()
|
|
3120
|
-
].join("\n")
|
|
3121
|
-
);
|
|
3122
|
-
const superseded2 = full.superseded.map(
|
|
3123
|
-
(entry) => `- \`${entry.conceptId}\` \u2192 superseded by ${entry.supersededBy.map((id) => `\`${id}\``).join(", ") || "(missing replacement)"}`
|
|
3124
|
-
);
|
|
3125
|
-
return {
|
|
3126
|
-
path,
|
|
3127
|
-
absolutePath,
|
|
3128
|
-
mode: "full",
|
|
3129
|
-
body: [
|
|
3130
|
-
...records,
|
|
3131
|
-
...superseded2.length ? [
|
|
3132
|
-
"#### Superseded (bodies withheld \u2014 kb_trace reaches them)",
|
|
3133
|
-
...superseded2
|
|
3134
|
-
] : []
|
|
3135
|
-
].join("\n\n")
|
|
3136
|
-
};
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
const adjudicated = adjudicate(bundle, bundle);
|
|
3140
|
-
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
3141
|
-
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
3142
|
-
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
3143
|
-
);
|
|
3678
|
+
});
|
|
3144
3679
|
return {
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
...degradedFrom ? { degradedFrom } : {}
|
|
3680
|
+
filePath: file.filePath,
|
|
3681
|
+
...verdict,
|
|
3682
|
+
...file.renamedFrom ? { renamedFrom: file.renamedFrom } : {},
|
|
3683
|
+
...hunks.some((hunk) => hunk.class !== verdict.class) ? { hunks } : {}
|
|
3150
3684
|
};
|
|
3151
3685
|
}
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
const merged = await readMergedPins(workspaceDir);
|
|
3157
|
-
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
3158
|
-
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
3159
|
-
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
3160
|
-
const pins = merged.pins.filter(
|
|
3161
|
-
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
3162
|
-
);
|
|
3163
|
-
if (pins.length === 0) {
|
|
3164
|
-
return {
|
|
3165
|
-
block: "",
|
|
3166
|
-
refused: false,
|
|
3167
|
-
approxTokens: 0,
|
|
3168
|
-
budgetTokens,
|
|
3169
|
-
bases: []
|
|
3170
|
-
};
|
|
3171
|
-
}
|
|
3172
|
-
const sections = await Promise.all(
|
|
3173
|
-
pins.map(async (pin) => ({
|
|
3174
|
-
section: await renderBase(
|
|
3175
|
-
store,
|
|
3176
|
-
pin.path,
|
|
3177
|
-
pin.absolutePath,
|
|
3178
|
-
fullUnderTokens,
|
|
3179
|
-
pin.mode,
|
|
3180
|
-
budgetTokens
|
|
3181
|
-
),
|
|
3182
|
-
frozen: pin.frozen === true
|
|
3183
|
-
}))
|
|
3184
|
-
);
|
|
3185
|
-
const modeLabel = {
|
|
3186
|
-
index: "index only \u2014 record bodies are not here",
|
|
3187
|
-
full: "full records \u2014 this base arrives whole",
|
|
3188
|
-
empty: "empty"
|
|
3686
|
+
function verdictOf(override) {
|
|
3687
|
+
return {
|
|
3688
|
+
class: override.class,
|
|
3689
|
+
reason: `kb-override ${override.record.conceptId}`
|
|
3189
3690
|
};
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
const
|
|
3201
|
-
|
|
3202
|
-
return [
|
|
3203
|
-
`### ${section.path} (${label}${frozen ? " \xB7 frozen, read-only" : ""})`,
|
|
3204
|
-
"",
|
|
3205
|
-
`bundlePath: \`${section.absolutePath}\``,
|
|
3206
|
-
"",
|
|
3207
|
-
section.body
|
|
3208
|
-
].join("\n");
|
|
3209
|
-
});
|
|
3210
|
-
const block = [preamble(), "", rendered.join("\n\n"), ""].join("\n");
|
|
3211
|
-
const bases = sections.map(({ section }) => ({
|
|
3212
|
-
path: section.path,
|
|
3213
|
-
absolutePath: section.absolutePath,
|
|
3214
|
-
approxTokens: approxTokens(section.body)
|
|
3215
|
-
}));
|
|
3216
|
-
const total = approxTokens(block);
|
|
3217
|
-
if (total > budgetTokens) {
|
|
3218
|
-
options.warn?.({
|
|
3219
|
-
operation: "kb.context.refused",
|
|
3220
|
-
approxTokens: total,
|
|
3221
|
-
budgetTokens,
|
|
3222
|
-
bases: bases.map((base2) => base2.path)
|
|
3223
|
-
});
|
|
3224
|
-
const refusal = [
|
|
3225
|
-
HEADING2,
|
|
3226
|
-
"",
|
|
3227
|
-
`The pinned index runs to ~${total} tokens, past the ${budgetTokens}-token`,
|
|
3228
|
-
"budget, and was not emitted \u2014 a truncated index is indistinguishable",
|
|
3229
|
-
"from a complete one. The pinned bases:",
|
|
3230
|
-
"",
|
|
3231
|
-
...bases.map(
|
|
3232
|
-
(base2) => `- ${base2.path} \u2014 ~${base2.approxTokens} tokens (bundlePath: \`${base2.absolutePath}\`)`
|
|
3233
|
-
),
|
|
3234
|
-
"",
|
|
3235
|
-
"For the question at hand, read what you need now \u2014 `kb_load` a base",
|
|
3236
|
-
"(its own budget is separate), or `kb_index` for one base's shape.",
|
|
3237
|
-
"",
|
|
3238
|
-
"To bring this block back under budget, in order of preference:",
|
|
3239
|
-
"- supersede or resolve stale records \u2014 the base shrinks, the knowledge keeps",
|
|
3240
|
-
"- force a large base to index lines: `strauss-kb pin <path> --mode index`",
|
|
3241
|
-
"- scope a pin to the profiles that need it: `strauss-kb pin <path> --profiles session-start`",
|
|
3242
|
-
"- raise this profile's budget under `context` in .strauss/kb-pins.json",
|
|
3243
|
-
"- unpin what no session actually needs",
|
|
3244
|
-
""
|
|
3245
|
-
].join("\n");
|
|
3691
|
+
}
|
|
3692
|
+
function heuristic(file, thresholds) {
|
|
3693
|
+
const marker = generatedMarker(file.header ?? headOfDiff(file));
|
|
3694
|
+
if (marker)
|
|
3695
|
+
return { class: "generated", reason: `generated-header ${marker}` };
|
|
3696
|
+
const rule = PATH_RULES.find((entry) => entry.test.test(file.filePath));
|
|
3697
|
+
if (rule) return { class: rule.class, reason: rule.name };
|
|
3698
|
+
if (file.renamedFrom && !file.hunks.length && (file.similarity ?? 100) >= thresholds.rename) {
|
|
3699
|
+
return { class: "rename", reason: `rename ${file.renamedFrom}` };
|
|
3700
|
+
}
|
|
3701
|
+
const share = boilerplateShare(file);
|
|
3702
|
+
if (share !== void 0 && share >= thresholds.boilerplate) {
|
|
3246
3703
|
return {
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
approxTokens: total,
|
|
3250
|
-
budgetTokens,
|
|
3251
|
-
bases
|
|
3704
|
+
class: "boilerplate",
|
|
3705
|
+
reason: `boilerplate ${Math.round(share * 100)}%`
|
|
3252
3706
|
};
|
|
3253
3707
|
}
|
|
3254
|
-
return {
|
|
3708
|
+
return { class: "source", reason: "default" };
|
|
3255
3709
|
}
|
|
3256
|
-
function
|
|
3257
|
-
return
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
additionalContext: block
|
|
3261
|
-
}
|
|
3262
|
-
});
|
|
3710
|
+
function headOfDiff(file) {
|
|
3711
|
+
return file.hunks.flatMap(
|
|
3712
|
+
(hunk) => (hunk.side ?? "new") === "new" && hunk.startLine <= HEADER_LINES ? (hunk.lines ?? []).slice(0, HEADER_LINES - hunk.startLine + 1) : []
|
|
3713
|
+
);
|
|
3263
3714
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
const
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
if (!region) return { file, action: "unchanged" };
|
|
3288
|
-
await (0, import_promises6.writeFile)(
|
|
3289
|
-
file,
|
|
3290
|
-
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
3291
|
-
`,
|
|
3292
|
-
"utf8"
|
|
3715
|
+
function boilerplateShare(file) {
|
|
3716
|
+
const lines = file.hunks.flatMap((hunk) => hunk.lines ?? []).map((line) => line.trim()).filter(Boolean);
|
|
3717
|
+
if (!lines.length) return void 0;
|
|
3718
|
+
return lines.filter(isBoilerplateLine).length / lines.length;
|
|
3719
|
+
}
|
|
3720
|
+
function currentOverrides(records, now) {
|
|
3721
|
+
const tagged = records.flatMap((record) => {
|
|
3722
|
+
if (record.frontmatter.type !== "fact") return [];
|
|
3723
|
+
const tag = (record.frontmatter.tags ?? []).find(
|
|
3724
|
+
(entry) => OVERRIDE_CLASS.has(entry)
|
|
3725
|
+
);
|
|
3726
|
+
const asserted = tag && OVERRIDE_CLASS.get(tag);
|
|
3727
|
+
return asserted ? [{ record, class: asserted }] : [];
|
|
3728
|
+
});
|
|
3729
|
+
const current = new Set(
|
|
3730
|
+
adjudicate(
|
|
3731
|
+
tagged.map(({ record }) => record),
|
|
3732
|
+
records,
|
|
3733
|
+
now
|
|
3734
|
+
).filter((entry) => entry.standing === "current").map((entry) => entry.record.conceptId)
|
|
3735
|
+
);
|
|
3736
|
+
return tagged.filter(({ record }) => current.has(record.conceptId)).sort(
|
|
3737
|
+
(left, right) => left.record.conceptId.localeCompare(right.record.conceptId)
|
|
3293
3738
|
);
|
|
3294
|
-
return { file, action: "appended" };
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
// src/commands/context.ts
|
|
3298
|
-
var contextCommand = define({
|
|
3299
|
-
name: "context",
|
|
3300
|
-
tool: "kb_context",
|
|
3301
|
-
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
3302
|
-
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.",
|
|
3303
|
-
input: import_zod11.z.object({
|
|
3304
|
-
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
3305
|
-
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
3306
|
-
),
|
|
3307
|
-
fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
3308
|
-
"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."
|
|
3309
|
-
),
|
|
3310
|
-
profile: import_zod11.z.string().optional().describe(
|
|
3311
|
-
"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."
|
|
3312
|
-
),
|
|
3313
|
-
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
3314
|
-
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3315
|
-
),
|
|
3316
|
-
event: import_zod11.z.string().optional().describe(
|
|
3317
|
-
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
3318
|
-
)
|
|
3319
|
-
}),
|
|
3320
|
-
fromArgv: (argv) => {
|
|
3321
|
-
const budget = argvFlag(argv, "--budget");
|
|
3322
|
-
const fullUnder = argvFlag(argv, "--full-under");
|
|
3323
|
-
const profile = argvFlag(argv, "--profile");
|
|
3324
|
-
const format = argvFlag(argv, "--format");
|
|
3325
|
-
const event = argvFlag(argv, "--event");
|
|
3326
|
-
return {
|
|
3327
|
-
...budget ? { budgetTokens: Number(budget) } : {},
|
|
3328
|
-
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
3329
|
-
...profile ? { profile } : {},
|
|
3330
|
-
...format ? { format } : {},
|
|
3331
|
-
...event ? { event } : {}
|
|
3332
|
-
};
|
|
3333
|
-
},
|
|
3334
|
-
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
|
|
3335
|
-
const result = await buildContext(store, process.cwd(), {
|
|
3336
|
-
...budgetTokens ? { budgetTokens } : {},
|
|
3337
|
-
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
3338
|
-
...profile ? { profile } : {},
|
|
3339
|
-
// Degradations — a full pin that could not fit, a refused block — go
|
|
3340
|
-
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
3341
|
-
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
3342
|
-
// see budget pressure without reading injected context.
|
|
3343
|
-
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
3344
|
-
`)
|
|
3345
|
-
});
|
|
3346
|
-
if (!result.block) return "";
|
|
3347
|
-
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
3348
|
-
}
|
|
3349
|
-
});
|
|
3350
|
-
|
|
3351
|
-
// src/commands/doctor.ts
|
|
3352
|
-
var import_zod13 = require("zod");
|
|
3353
|
-
|
|
3354
|
-
// src/drift/git.ts
|
|
3355
|
-
var import_node_child_process3 = require("child_process");
|
|
3356
|
-
var import_node_util3 = require("util");
|
|
3357
|
-
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
3358
|
-
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
3359
|
-
var GIT_TIMEOUT_MS = 5e3;
|
|
3360
|
-
async function git2(cwd, args) {
|
|
3361
|
-
const env = { ...process.env };
|
|
3362
|
-
delete env["GIT_DIR"];
|
|
3363
|
-
delete env["GIT_WORK_TREE"];
|
|
3364
|
-
delete env["GIT_INDEX_FILE"];
|
|
3365
|
-
try {
|
|
3366
|
-
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
3367
|
-
timeout: GIT_TIMEOUT_MS,
|
|
3368
|
-
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
3369
|
-
env
|
|
3370
|
-
});
|
|
3371
|
-
return { ok: true, stdout };
|
|
3372
|
-
} catch {
|
|
3373
|
-
return { ok: false };
|
|
3374
|
-
}
|
|
3375
|
-
}
|
|
3376
|
-
async function listRepoFiles(repoRoot) {
|
|
3377
|
-
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
3378
|
-
if (!result.ok) return [];
|
|
3379
|
-
return result.stdout.split("\0").filter(Boolean);
|
|
3380
|
-
}
|
|
3381
|
-
async function readOldSource(repoRoot, anchor) {
|
|
3382
|
-
if (!filePathIsSafe(anchor.file))
|
|
3383
|
-
return { ok: false, reason: "unrecoverable" };
|
|
3384
|
-
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
3385
|
-
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
3386
|
-
if (shown2 !== null) {
|
|
3387
|
-
return {
|
|
3388
|
-
ok: true,
|
|
3389
|
-
source: shown2,
|
|
3390
|
-
origin: { kind: "ref", ref: anchor.ref }
|
|
3391
|
-
};
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
const at2 = anchor.resolved_at;
|
|
3395
|
-
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
3396
|
-
return { ok: false, reason: "unrecoverable" };
|
|
3397
|
-
}
|
|
3398
|
-
const found = await git2(repoRoot, [
|
|
3399
|
-
"log",
|
|
3400
|
-
"-1",
|
|
3401
|
-
"--format=%H",
|
|
3402
|
-
`--before=${at2}`,
|
|
3403
|
-
"--end-of-options",
|
|
3404
|
-
"HEAD",
|
|
3405
|
-
"--",
|
|
3406
|
-
anchor.file
|
|
3407
|
-
]);
|
|
3408
|
-
const sha = found.ok ? found.stdout.trim() : "";
|
|
3409
|
-
if (!sha || !refShapeIsSafe(sha))
|
|
3410
|
-
return { ok: false, reason: "unrecoverable" };
|
|
3411
|
-
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
3412
|
-
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
3413
|
-
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
3414
|
-
}
|
|
3415
|
-
async function showFile(repoRoot, ref, file) {
|
|
3416
|
-
const path = file.replace(/^\.\//, "");
|
|
3417
|
-
const result = await git2(repoRoot, [
|
|
3418
|
-
"show",
|
|
3419
|
-
"--end-of-options",
|
|
3420
|
-
`${ref}:${path}`
|
|
3421
|
-
]);
|
|
3422
|
-
return result.ok ? result.stdout : null;
|
|
3423
3739
|
}
|
|
3424
3740
|
|
|
3425
3741
|
// src/drift/moved.ts
|
|
3426
|
-
var
|
|
3742
|
+
var import_promises6 = require("fs/promises");
|
|
3427
3743
|
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
3428
3744
|
var SEARCH_BATCH = 64;
|
|
3429
3745
|
function movedSearch(repoRoot, options = {}) {
|
|
@@ -3440,6 +3756,7 @@ function movedSearch(repoRoot, options = {}) {
|
|
|
3440
3756
|
async find(anchor) {
|
|
3441
3757
|
const stored = anchor.hash;
|
|
3442
3758
|
if (!stored) return void 0;
|
|
3759
|
+
if (anchor.span) return sameFileWindow(anchor, read, stored);
|
|
3443
3760
|
const language = languageForFile(anchor.file);
|
|
3444
3761
|
if (!language) return sameFileWindow(anchor, read, stored);
|
|
3445
3762
|
const candidates = await filesForLanguage(language);
|
|
@@ -3488,7 +3805,7 @@ function diskSize(repoRoot) {
|
|
|
3488
3805
|
const path = anchorFilePath(repoRoot, file);
|
|
3489
3806
|
if (path === null) return null;
|
|
3490
3807
|
try {
|
|
3491
|
-
return (await (0,
|
|
3808
|
+
return (await (0, import_promises6.stat)(path)).size;
|
|
3492
3809
|
} catch {
|
|
3493
3810
|
return null;
|
|
3494
3811
|
}
|
|
@@ -3537,6 +3854,11 @@ async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
|
3537
3854
|
);
|
|
3538
3855
|
const out = [];
|
|
3539
3856
|
for (const { anchor, entry } of wanted) {
|
|
3857
|
+
if (anchor.side === "old") {
|
|
3858
|
+
const settled2 = entry.class ?? "changed";
|
|
3859
|
+
out.push({ anchor, entry: { ...entry, class: settled2 }, class: settled2 });
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3540
3862
|
const movedTo = await search.find(anchor);
|
|
3541
3863
|
if (movedTo) {
|
|
3542
3864
|
out.push({
|
|
@@ -3610,8 +3932,8 @@ function unifiedDiff(before, after, options = {}) {
|
|
|
3610
3932
|
}
|
|
3611
3933
|
const truncated = body.length > max;
|
|
3612
3934
|
const shown = truncated ? body.slice(0, max) : body;
|
|
3613
|
-
const
|
|
3614
|
-
const lines = [
|
|
3935
|
+
const header2 = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3936
|
+
const lines = [header2, ...shown];
|
|
3615
3937
|
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3616
3938
|
return { text: lines.join("\n"), added, removed, truncated };
|
|
3617
3939
|
}
|
|
@@ -3671,12 +3993,12 @@ async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
|
3671
3993
|
...options.search ? { search: options.search } : {},
|
|
3672
3994
|
withHistory: options.withDiff !== false
|
|
3673
3995
|
});
|
|
3674
|
-
const
|
|
3996
|
+
const open2 = classified.filter(
|
|
3675
3997
|
(found) => found.class === "changed" || found.class === "gone"
|
|
3676
3998
|
);
|
|
3677
|
-
if (!
|
|
3678
|
-
const budget = diffBudget(
|
|
3679
|
-
const anchors =
|
|
3999
|
+
if (!open2.length) return { packet: null, classified };
|
|
4000
|
+
const budget = diffBudget(open2.length);
|
|
4001
|
+
const anchors = open2.map(
|
|
3680
4002
|
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3681
4003
|
);
|
|
3682
4004
|
const type = record.frontmatter.type;
|
|
@@ -3715,41 +4037,893 @@ function anchorPacket(found, withDiff, maxLines) {
|
|
|
3715
4037
|
diffSize: entry.diffSize,
|
|
3716
4038
|
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3717
4039
|
};
|
|
3718
|
-
if (!withDiff) return base2;
|
|
3719
|
-
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3720
|
-
return { ...base2, diff: { status: "unrecoverable" } };
|
|
4040
|
+
if (!withDiff) return base2;
|
|
4041
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
4042
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
4043
|
+
}
|
|
4044
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
4045
|
+
maxLines
|
|
4046
|
+
});
|
|
4047
|
+
return {
|
|
4048
|
+
...base2,
|
|
4049
|
+
diff: {
|
|
4050
|
+
status: "ok",
|
|
4051
|
+
source: found.oldOrigin.kind,
|
|
4052
|
+
ref: found.oldOrigin.ref,
|
|
4053
|
+
unified: rendered.text,
|
|
4054
|
+
added: rendered.added,
|
|
4055
|
+
removed: rendered.removed,
|
|
4056
|
+
truncated: rendered.truncated
|
|
4057
|
+
}
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
function claimOf(record) {
|
|
4061
|
+
const type = record.frontmatter.type;
|
|
4062
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
4063
|
+
if (!section) return null;
|
|
4064
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
4065
|
+
const start = lines.findIndex(
|
|
4066
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
4067
|
+
);
|
|
4068
|
+
if (start < 0) return null;
|
|
4069
|
+
const rest = lines.slice(start + 1);
|
|
4070
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
4071
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
4072
|
+
return text ? { section, text } : null;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// src/commands/match/command.ts
|
|
4076
|
+
var import_zod12 = require("zod");
|
|
4077
|
+
|
|
4078
|
+
// src/commands/match/errors.ts
|
|
4079
|
+
var KbMatchInputError = class extends BaseError {
|
|
4080
|
+
constructor(reason) {
|
|
4081
|
+
super({
|
|
4082
|
+
message: `match: ${reason}`,
|
|
4083
|
+
errorType: "KbMatchInput" /* KbMatchInput */,
|
|
4084
|
+
code: 400,
|
|
4085
|
+
fault: "User" /* User */,
|
|
4086
|
+
retriable: false,
|
|
4087
|
+
reportToUser: true,
|
|
4088
|
+
details: { reason }
|
|
4089
|
+
});
|
|
4090
|
+
this.reason = reason;
|
|
4091
|
+
}
|
|
4092
|
+
reason;
|
|
4093
|
+
};
|
|
4094
|
+
|
|
4095
|
+
// src/commands/match/model.ts
|
|
4096
|
+
var import_zod11 = require("zod");
|
|
4097
|
+
var diffHunkSchema = import_zod11.z.object({
|
|
4098
|
+
startLine: import_zod11.z.number().int().positive(),
|
|
4099
|
+
endLine: import_zod11.z.number().int().positive(),
|
|
4100
|
+
side: import_zod11.z.enum(["old", "new"]).optional()
|
|
4101
|
+
}).passthrough();
|
|
4102
|
+
var diffFileSchema = import_zod11.z.object({
|
|
4103
|
+
filePath: import_zod11.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
4104
|
+
hunks: import_zod11.z.array(diffHunkSchema)
|
|
4105
|
+
});
|
|
4106
|
+
var symbolRangeSchema = import_zod11.z.object({
|
|
4107
|
+
file: import_zod11.z.string().min(1),
|
|
4108
|
+
symbol: import_zod11.z.string().min(1),
|
|
4109
|
+
startLine: import_zod11.z.number().int().positive(),
|
|
4110
|
+
endLine: import_zod11.z.number().int().positive()
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
// src/commands/match/parse-unified-diff.ts
|
|
4114
|
+
var FILE_HEADER = /^diff --git (.+)$/;
|
|
4115
|
+
var HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
4116
|
+
var SIMILARITY = /^similarity index (\d+)%$/;
|
|
4117
|
+
var KNOWN_PREFIX = /^[ab]\//;
|
|
4118
|
+
function parseUnifiedDiff(patch, options = {}) {
|
|
4119
|
+
const files = [];
|
|
4120
|
+
let current;
|
|
4121
|
+
let listed = false;
|
|
4122
|
+
let shared;
|
|
4123
|
+
let oldPath;
|
|
4124
|
+
let rename4 = {};
|
|
4125
|
+
let inHeader = false;
|
|
4126
|
+
let added;
|
|
4127
|
+
let removed;
|
|
4128
|
+
const list = () => {
|
|
4129
|
+
if (!current || listed) return;
|
|
4130
|
+
files.push(current);
|
|
4131
|
+
listed = true;
|
|
4132
|
+
};
|
|
4133
|
+
const open2 = (filePath) => {
|
|
4134
|
+
current = { filePath, hunks: [], ...rename4 };
|
|
4135
|
+
listed = false;
|
|
4136
|
+
};
|
|
4137
|
+
const amend = () => {
|
|
4138
|
+
if (current) Object.assign(current, rename4);
|
|
4139
|
+
};
|
|
4140
|
+
const close = () => {
|
|
4141
|
+
if (options.keepEmpty) list();
|
|
4142
|
+
current = void 0;
|
|
4143
|
+
listed = false;
|
|
4144
|
+
added = void 0;
|
|
4145
|
+
removed = void 0;
|
|
4146
|
+
};
|
|
4147
|
+
for (const raw of patch.split("\n")) {
|
|
4148
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
4149
|
+
const start = FILE_HEADER.exec(line);
|
|
4150
|
+
if (start) {
|
|
4151
|
+
close();
|
|
4152
|
+
oldPath = void 0;
|
|
4153
|
+
rename4 = {};
|
|
4154
|
+
shared = sharedHeaderPath(start[1]);
|
|
4155
|
+
inHeader = true;
|
|
4156
|
+
if (shared) open2(shared);
|
|
4157
|
+
continue;
|
|
4158
|
+
}
|
|
4159
|
+
if (inHeader) {
|
|
4160
|
+
const similarity = SIMILARITY.exec(line);
|
|
4161
|
+
if (similarity) {
|
|
4162
|
+
rename4.similarity = Number(similarity[1]);
|
|
4163
|
+
amend();
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
if (line.startsWith("rename from ")) {
|
|
4167
|
+
rename4.renamedFrom = unquote(line.slice(12).trim());
|
|
4168
|
+
amend();
|
|
4169
|
+
continue;
|
|
4170
|
+
}
|
|
4171
|
+
if (line.startsWith("rename to ")) {
|
|
4172
|
+
open2(unquote(line.slice(10).trim()));
|
|
4173
|
+
continue;
|
|
4174
|
+
}
|
|
4175
|
+
if (line.startsWith("--- ")) {
|
|
4176
|
+
oldPath = sidePath(line.slice(4), shared);
|
|
4177
|
+
continue;
|
|
4178
|
+
}
|
|
4179
|
+
if (line.startsWith("+++ ")) {
|
|
4180
|
+
const path = sidePath(line.slice(4), shared) ?? oldPath;
|
|
4181
|
+
if (path) open2(path);
|
|
4182
|
+
else current = void 0;
|
|
4183
|
+
continue;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
const hunk = HUNK.exec(line);
|
|
4187
|
+
if (!hunk) {
|
|
4188
|
+
if (!options.withLines || inHeader) continue;
|
|
4189
|
+
if (line.startsWith("+")) added?.lines?.push(line.slice(1));
|
|
4190
|
+
else if (line.startsWith("-")) removed?.lines?.push(line.slice(1));
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
inHeader = false;
|
|
4194
|
+
added = void 0;
|
|
4195
|
+
removed = void 0;
|
|
4196
|
+
if (!current) continue;
|
|
4197
|
+
const [next, before] = hunksOf(hunk, options.withLines === true);
|
|
4198
|
+
added = next;
|
|
4199
|
+
removed = before;
|
|
4200
|
+
list();
|
|
4201
|
+
current.hunks.push(...before ? [next, before] : [next]);
|
|
4202
|
+
}
|
|
4203
|
+
close();
|
|
4204
|
+
return files;
|
|
4205
|
+
}
|
|
4206
|
+
function hunksOf(hunk, withLines) {
|
|
4207
|
+
const oldStart = Number(hunk[1]);
|
|
4208
|
+
const oldCount = hunk[2] === void 0 ? 1 : Number(hunk[2]);
|
|
4209
|
+
const newStart = Number(hunk[3]);
|
|
4210
|
+
const newCount = hunk[4] === void 0 ? 1 : Number(hunk[4]);
|
|
4211
|
+
const lines = withLines ? { lines: [] } : {};
|
|
4212
|
+
const added = newCount === 0 ? { ...point(newStart), ...lines } : { startLine: newStart, endLine: newStart + newCount - 1, ...lines };
|
|
4213
|
+
if (oldCount === 0) return [added];
|
|
4214
|
+
return [
|
|
4215
|
+
added,
|
|
4216
|
+
{
|
|
4217
|
+
startLine: oldStart,
|
|
4218
|
+
endLine: oldStart + oldCount - 1,
|
|
4219
|
+
side: "old",
|
|
4220
|
+
...withLines ? { lines: [] } : {}
|
|
4221
|
+
}
|
|
4222
|
+
];
|
|
4223
|
+
}
|
|
4224
|
+
function point(start) {
|
|
4225
|
+
const at2 = Math.max(1, start);
|
|
4226
|
+
return { startLine: at2, endLine: at2 };
|
|
4227
|
+
}
|
|
4228
|
+
function sidePath(raw, shared) {
|
|
4229
|
+
const text = unquote(raw.replace(/\t.*$/, "").trim());
|
|
4230
|
+
if (text === "/dev/null") return void 0;
|
|
4231
|
+
if (KNOWN_PREFIX.test(text)) return text.slice(2);
|
|
4232
|
+
return shared ?? text;
|
|
4233
|
+
}
|
|
4234
|
+
function sharedHeaderPath(rest) {
|
|
4235
|
+
const pair = splitHeaderPair(rest);
|
|
4236
|
+
if (!pair || pair[0] === pair[1]) return void 0;
|
|
4237
|
+
const from = unquote(pair[0]).split("/");
|
|
4238
|
+
const to = unquote(pair[1]).split("/");
|
|
4239
|
+
const shared = [];
|
|
4240
|
+
while (from.length > 1 && to.length > 1 && from.at(-1) === to.at(-1)) {
|
|
4241
|
+
shared.unshift(from.pop());
|
|
4242
|
+
to.pop();
|
|
4243
|
+
}
|
|
4244
|
+
return shared.length ? shared.join("/") : void 0;
|
|
4245
|
+
}
|
|
4246
|
+
function splitHeaderPair(rest) {
|
|
4247
|
+
if (rest.startsWith('"')) {
|
|
4248
|
+
const end = endOfQuoted(rest);
|
|
4249
|
+
if (end < 0 || rest[end + 1] !== " ") return void 0;
|
|
4250
|
+
return [rest.slice(0, end + 1), rest.slice(end + 2)];
|
|
4251
|
+
}
|
|
4252
|
+
const mid = (rest.length - 1) / 2;
|
|
4253
|
+
if (Number.isInteger(mid) && rest[mid] === " ") {
|
|
4254
|
+
return [rest.slice(0, mid), rest.slice(mid + 1)];
|
|
4255
|
+
}
|
|
4256
|
+
const at2 = rest.indexOf(" ");
|
|
4257
|
+
return at2 === -1 ? void 0 : [rest.slice(0, at2), rest.slice(at2 + 1)];
|
|
4258
|
+
}
|
|
4259
|
+
function endOfQuoted(text) {
|
|
4260
|
+
for (let at2 = 1; at2 < text.length; at2 += 1) {
|
|
4261
|
+
if (text[at2] === "\\") {
|
|
4262
|
+
at2 += 1;
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
if (text[at2] === '"') return at2;
|
|
4266
|
+
}
|
|
4267
|
+
return -1;
|
|
4268
|
+
}
|
|
4269
|
+
var ESCAPES = {
|
|
4270
|
+
a: 7,
|
|
4271
|
+
b: 8,
|
|
4272
|
+
f: 12,
|
|
4273
|
+
n: 10,
|
|
4274
|
+
r: 13,
|
|
4275
|
+
t: 9,
|
|
4276
|
+
v: 11,
|
|
4277
|
+
'"': 34,
|
|
4278
|
+
"\\": 92
|
|
4279
|
+
};
|
|
4280
|
+
var OCTAL = /^[0-7]{3}/;
|
|
4281
|
+
var utf8 = new TextEncoder();
|
|
4282
|
+
function unquote(text) {
|
|
4283
|
+
if (text.length < 2 || !text.startsWith('"') || !text.endsWith('"')) {
|
|
4284
|
+
return text;
|
|
4285
|
+
}
|
|
4286
|
+
const body = text.slice(1, -1);
|
|
4287
|
+
const bytes = [];
|
|
4288
|
+
for (let at2 = 0; at2 < body.length; ) {
|
|
4289
|
+
const slash = body.indexOf("\\", at2);
|
|
4290
|
+
if (slash < 0) {
|
|
4291
|
+
bytes.push(...utf8.encode(body.slice(at2)));
|
|
4292
|
+
break;
|
|
4293
|
+
}
|
|
4294
|
+
if (slash > at2) bytes.push(...utf8.encode(body.slice(at2, slash)));
|
|
4295
|
+
const octal = OCTAL.exec(body.slice(slash + 1, slash + 4));
|
|
4296
|
+
if (octal) {
|
|
4297
|
+
bytes.push(Number.parseInt(octal[0], 8));
|
|
4298
|
+
at2 = slash + 4;
|
|
4299
|
+
continue;
|
|
4300
|
+
}
|
|
4301
|
+
const next = body[slash + 1];
|
|
4302
|
+
if (next === void 0) {
|
|
4303
|
+
bytes.push(ESCAPES["\\"]);
|
|
4304
|
+
break;
|
|
4305
|
+
}
|
|
4306
|
+
const mapped = ESCAPES[next];
|
|
4307
|
+
if (mapped === void 0) bytes.push(...utf8.encode(next));
|
|
4308
|
+
else bytes.push(mapped);
|
|
4309
|
+
at2 = slash + 2;
|
|
4310
|
+
}
|
|
4311
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// src/commands/match/symbol-ranges.ts
|
|
4315
|
+
async function resolveSymbolRanges(repoRoot, files, records, offline = false) {
|
|
4316
|
+
const changed = new Set(files.map((file) => strip(file.filePath)));
|
|
4317
|
+
const wanted = [];
|
|
4318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4319
|
+
for (const record of records) {
|
|
4320
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4321
|
+
if (!anchor.symbol || anchor.repo) continue;
|
|
4322
|
+
if (!changed.has(strip(anchor.file))) continue;
|
|
4323
|
+
const key2 = `${strip(anchor.file)}#${anchor.symbol}`;
|
|
4324
|
+
if (seen.has(key2)) continue;
|
|
4325
|
+
seen.add(key2);
|
|
4326
|
+
wanted.push(anchor);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
if (!wanted.length) return [];
|
|
4330
|
+
const paths = [...new Set(wanted.map((anchor) => anchor.file))];
|
|
4331
|
+
const sources = await readAnchorFiles(paths, anchorFileReader(repoRoot));
|
|
4332
|
+
const resolvers = defaultAnchorResolvers({ offline });
|
|
4333
|
+
await prepareResolvers(resolvers, paths);
|
|
4334
|
+
const ranges = [];
|
|
4335
|
+
for (const anchor of wanted) {
|
|
4336
|
+
const read = sources.get(anchor.file);
|
|
4337
|
+
if (!read?.ok) continue;
|
|
4338
|
+
const outcome = resolveAnchorSpan(read.source, anchor, resolvers);
|
|
4339
|
+
if (!outcome.ok) continue;
|
|
4340
|
+
ranges.push({
|
|
4341
|
+
file: anchor.file,
|
|
4342
|
+
symbol: anchor.symbol,
|
|
4343
|
+
startLine: outcome.span.startLine,
|
|
4344
|
+
endLine: outcome.span.endLine
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
return ranges;
|
|
4348
|
+
}
|
|
4349
|
+
function strip(path) {
|
|
4350
|
+
return path.replace(/^\.\//, "");
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
// src/commands/match/command.ts
|
|
4354
|
+
var matchCommand = define({
|
|
4355
|
+
name: "match",
|
|
4356
|
+
tool: "kb_match",
|
|
4357
|
+
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
4358
|
+
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.",
|
|
4359
|
+
input: import_zod12.z.object({
|
|
4360
|
+
bundlePath,
|
|
4361
|
+
files: import_zod12.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
4362
|
+
symbolRanges: import_zod12.z.array(symbolRangeSchema).optional().describe(
|
|
4363
|
+
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
4364
|
+
),
|
|
4365
|
+
repoRoot: REPO_ROOT,
|
|
4366
|
+
offline: import_zod12.z.boolean().optional().describe(
|
|
4367
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4368
|
+
),
|
|
4369
|
+
includeNonCurrent: import_zod12.z.boolean().optional().describe(
|
|
4370
|
+
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
4371
|
+
)
|
|
4372
|
+
}),
|
|
4373
|
+
fromArgv: async (argv, path, stdin) => {
|
|
4374
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4375
|
+
const range = argvFlag(argv, "--git");
|
|
4376
|
+
const base2 = {
|
|
4377
|
+
bundlePath: path,
|
|
4378
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4379
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
4380
|
+
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
4381
|
+
};
|
|
4382
|
+
if (range !== void 0) {
|
|
4383
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4384
|
+
if (!diff.ok) {
|
|
4385
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff.reason]}`);
|
|
4386
|
+
}
|
|
4387
|
+
return { ...base2, files: parseUnifiedDiff(diff.text) };
|
|
4388
|
+
}
|
|
4389
|
+
if (!argv.includes("--stdin")) {
|
|
4390
|
+
throw new KbMatchInputError(
|
|
4391
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
4392
|
+
);
|
|
4393
|
+
}
|
|
4394
|
+
return { ...base2, ...fromStdin(await stdin()) };
|
|
4395
|
+
},
|
|
4396
|
+
run: async ({ store }, {
|
|
4397
|
+
bundlePath: path,
|
|
4398
|
+
files,
|
|
4399
|
+
symbolRanges,
|
|
4400
|
+
repoRoot,
|
|
4401
|
+
offline,
|
|
4402
|
+
includeNonCurrent
|
|
4403
|
+
}) => {
|
|
4404
|
+
const records = await store.list(path);
|
|
4405
|
+
const ranges = symbolRanges ?? await resolveSymbolRanges(
|
|
4406
|
+
repoRoot ?? process.cwd(),
|
|
4407
|
+
files,
|
|
4408
|
+
records,
|
|
4409
|
+
offline === true
|
|
4410
|
+
);
|
|
4411
|
+
const index2 = symbolRangeIndex(ranges);
|
|
4412
|
+
return matchToDiff(files, records, { symbolRanges: ranges }).flatMap(
|
|
4413
|
+
(match) => project(match, index2, includeNonCurrent === true)
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
});
|
|
4417
|
+
var REFUSED = {
|
|
4418
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
4419
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
4420
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
4421
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
4422
|
+
};
|
|
4423
|
+
function fromStdin(text) {
|
|
4424
|
+
let payload;
|
|
4425
|
+
try {
|
|
4426
|
+
payload = JSON.parse(text);
|
|
4427
|
+
} catch {
|
|
4428
|
+
throw new KbMatchInputError("stdin is not JSON");
|
|
4429
|
+
}
|
|
4430
|
+
if (!Array.isArray(payload?.files)) {
|
|
4431
|
+
throw new KbMatchInputError("stdin needs a files array");
|
|
4432
|
+
}
|
|
4433
|
+
return {
|
|
4434
|
+
files: payload.files,
|
|
4435
|
+
...payload.symbolRanges !== void 0 ? { symbolRanges: payload.symbolRanges } : {}
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
function project(match, ranges, all) {
|
|
4439
|
+
const kept = all ? match.records : match.records.filter((hit) => hit.standing === "current");
|
|
4440
|
+
if (!kept.length) return [];
|
|
4441
|
+
const placed = kept.map((hit) => ({
|
|
4442
|
+
hit,
|
|
4443
|
+
at: placeOnHunk(hit.record, match.filePath, match.hunk, ranges)
|
|
4444
|
+
}));
|
|
4445
|
+
return [
|
|
4446
|
+
{
|
|
4447
|
+
filePath: match.filePath,
|
|
4448
|
+
hunk: match.hunk,
|
|
4449
|
+
// Over the records returned, not the ones matched: a hunk holding only
|
|
4450
|
+
// symbol-placed records is not `file` because a dropped one was.
|
|
4451
|
+
precision: placed.every(({ at: at2 }) => at2.kind === "symbol") ? "symbol" : "file",
|
|
4452
|
+
records: placed.map(({ hit, at: { anchor } }) => {
|
|
4453
|
+
const { frontmatter } = hit.record;
|
|
4454
|
+
return {
|
|
4455
|
+
conceptId: hit.record.conceptId,
|
|
4456
|
+
type: frontmatter.type,
|
|
4457
|
+
title: frontmatter.title ?? null,
|
|
4458
|
+
standing: hit.standing,
|
|
4459
|
+
status: frontmatter.strauss_status,
|
|
4460
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
4461
|
+
...frontmatter.strauss_materiality ? { materiality: frontmatter.strauss_materiality } : {},
|
|
4462
|
+
...frontmatter.strauss_confidence ? { confidence: frontmatter.strauss_confidence } : {},
|
|
4463
|
+
...frontmatter.tags?.length ? { tags: frontmatter.tags } : {},
|
|
4464
|
+
...anchor ? { anchor } : {}
|
|
4465
|
+
};
|
|
4466
|
+
})
|
|
4467
|
+
}
|
|
4468
|
+
];
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
// src/commands/classify.ts
|
|
4472
|
+
var classifyFileSchema = diffFileSchema.extend({
|
|
4473
|
+
hunks: import_zod13.z.array(
|
|
4474
|
+
diffHunkSchema.extend({ lines: import_zod13.z.array(import_zod13.z.string()).optional() })
|
|
4475
|
+
),
|
|
4476
|
+
renamedFrom: import_zod13.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
4477
|
+
similarity: import_zod13.z.number().min(0).max(100).optional()
|
|
4478
|
+
});
|
|
4479
|
+
var classifyCommand = define({
|
|
4480
|
+
name: "classify",
|
|
4481
|
+
tool: "kb_classify",
|
|
4482
|
+
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
4483
|
+
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.",
|
|
4484
|
+
input: import_zod13.z.object({
|
|
4485
|
+
bundlePath,
|
|
4486
|
+
files: import_zod13.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
4487
|
+
repoRoot: REPO_ROOT,
|
|
4488
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
4489
|
+
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4490
|
+
)
|
|
4491
|
+
}),
|
|
4492
|
+
fromArgv: async (argv, path, stdin) => {
|
|
4493
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4494
|
+
const range = argvFlag(argv, "--git");
|
|
4495
|
+
const base2 = {
|
|
4496
|
+
bundlePath: path,
|
|
4497
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4498
|
+
...argv.includes("--offline") ? { offline: true } : {}
|
|
4499
|
+
};
|
|
4500
|
+
if (range !== void 0) {
|
|
4501
|
+
const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4502
|
+
if (!diff.ok) {
|
|
4503
|
+
throw new KbClassifyInputError(
|
|
4504
|
+
`--git ${range} ${REFUSED2[diff.reason]}`
|
|
4505
|
+
);
|
|
4506
|
+
}
|
|
4507
|
+
return {
|
|
4508
|
+
...base2,
|
|
4509
|
+
files: parseUnifiedDiff(diff.text, {
|
|
4510
|
+
keepEmpty: true,
|
|
4511
|
+
withLines: true
|
|
4512
|
+
})
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
if (!argv.includes("--stdin")) {
|
|
4516
|
+
throw new KbClassifyInputError(
|
|
4517
|
+
"pass --git <base>..<head>, or --stdin with { files } as JSON"
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
4520
|
+
return { ...base2, files: fromStdin2(await stdin()) };
|
|
4521
|
+
},
|
|
4522
|
+
run: async ({ store }, { bundlePath: path, files, repoRoot, offline }) => {
|
|
4523
|
+
const records = await store.list(path);
|
|
4524
|
+
const root = repoRoot ?? process.cwd();
|
|
4525
|
+
const withHeaders = await mapLimit2(files, READERS, async (file) => ({
|
|
4526
|
+
...file,
|
|
4527
|
+
header: await header(root, file)
|
|
4528
|
+
}));
|
|
4529
|
+
const symbolRanges = await resolveSymbolRanges(
|
|
4530
|
+
root,
|
|
4531
|
+
files,
|
|
4532
|
+
records,
|
|
4533
|
+
offline === true
|
|
4534
|
+
);
|
|
4535
|
+
return { files: classifyDiff(withHeaders, { records, symbolRanges }) };
|
|
4536
|
+
},
|
|
4537
|
+
render: (result) => renderClassify(result)
|
|
4538
|
+
});
|
|
4539
|
+
var HEADER_BYTES = 65536;
|
|
4540
|
+
var READERS = 16;
|
|
4541
|
+
async function header(root, file) {
|
|
4542
|
+
if (!filePathIsSafe(file.filePath)) return void 0;
|
|
4543
|
+
let handle;
|
|
4544
|
+
try {
|
|
4545
|
+
handle = await (0, import_promises7.open)((0, import_node_path10.join)(root, file.filePath), "r");
|
|
4546
|
+
const buffer = import_node_buffer.Buffer.alloc(HEADER_BYTES);
|
|
4547
|
+
const { bytesRead } = await handle.read(buffer, 0, HEADER_BYTES, 0);
|
|
4548
|
+
return buffer.toString("utf8", 0, bytesRead).split("\n").slice(0, HEADER_LINES);
|
|
4549
|
+
} catch {
|
|
4550
|
+
return void 0;
|
|
4551
|
+
} finally {
|
|
4552
|
+
await handle?.close();
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
async function mapLimit2(items, limit, run) {
|
|
4556
|
+
const out = Array.from({ length: items.length });
|
|
4557
|
+
let next = 0;
|
|
4558
|
+
const worker = async () => {
|
|
4559
|
+
while (next < items.length) {
|
|
4560
|
+
const at2 = next;
|
|
4561
|
+
next += 1;
|
|
4562
|
+
out[at2] = await run(items[at2]);
|
|
4563
|
+
}
|
|
4564
|
+
};
|
|
4565
|
+
await Promise.all(
|
|
4566
|
+
Array.from({ length: Math.min(limit, items.length) }, () => worker())
|
|
4567
|
+
);
|
|
4568
|
+
return out;
|
|
4569
|
+
}
|
|
4570
|
+
var REFUSED2 = {
|
|
4571
|
+
"bad-range": "is not a range git could read here \u2014 both halves of <base>..<head> are required",
|
|
4572
|
+
"too-large": "diffs to a patch past the output cap \u2014 narrow the range",
|
|
4573
|
+
timeout: "took longer to diff than the runner allows \u2014 narrow the range",
|
|
4574
|
+
"git-missing": "needs git on PATH, and there is none"
|
|
4575
|
+
};
|
|
4576
|
+
function fromStdin2(text) {
|
|
4577
|
+
let payload;
|
|
4578
|
+
try {
|
|
4579
|
+
payload = JSON.parse(text);
|
|
4580
|
+
} catch {
|
|
4581
|
+
throw new KbClassifyInputError("stdin is not JSON");
|
|
4582
|
+
}
|
|
4583
|
+
if (!Array.isArray(payload?.files)) {
|
|
4584
|
+
throw new KbClassifyInputError("stdin needs a files array");
|
|
4585
|
+
}
|
|
4586
|
+
return payload.files;
|
|
4587
|
+
}
|
|
4588
|
+
function renderClassify(result) {
|
|
4589
|
+
const width2 = Math.max(
|
|
4590
|
+
0,
|
|
4591
|
+
...result.files.map((file) => file.class.length)
|
|
4592
|
+
);
|
|
4593
|
+
return result.files.map(
|
|
4594
|
+
(file) => `${file.class.padEnd(width2)} ${file.filePath} (${file.reason})`
|
|
4595
|
+
).join("\n");
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
// src/commands/context.ts
|
|
4599
|
+
var import_zod14 = require("zod");
|
|
4600
|
+
|
|
4601
|
+
// src/kb-context.ts
|
|
4602
|
+
var import_promises8 = require("fs/promises");
|
|
4603
|
+
|
|
4604
|
+
// src/kb-index.ts
|
|
4605
|
+
var INDEX_FILE = "INDEX.md";
|
|
4606
|
+
var HEADING = "# KB Index";
|
|
4607
|
+
function renderIndex(records) {
|
|
4608
|
+
const lines = [...records].sort((left, right) => left.conceptId.localeCompare(right.conceptId)).map(renderIndexLine);
|
|
4609
|
+
return `${HEADING}
|
|
4610
|
+
|
|
4611
|
+
${lines.join("\n")}
|
|
4612
|
+
`;
|
|
4613
|
+
}
|
|
4614
|
+
function renderIndexLine(record) {
|
|
4615
|
+
const { frontmatter: fm } = record;
|
|
4616
|
+
const parts = [fm.type, fm.strauss_status];
|
|
4617
|
+
if (fm.tags?.length) parts.push(`tags: ${fm.tags.join(", ")}`);
|
|
4618
|
+
if (fm.description) parts.push(fm.description);
|
|
4619
|
+
return `- [${fm.title ?? record.conceptId}](${record.conceptId}.md) \u2014 ${parts.join(" \xB7 ")}`;
|
|
4620
|
+
}
|
|
4621
|
+
function indexIsStale(stored, expected) {
|
|
4622
|
+
return stored !== expected;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
// src/kb-context.ts
|
|
4626
|
+
var HEADING2 = "## Knowledge bases (pinned)";
|
|
4627
|
+
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
4628
|
+
var CONTEXT_PROFILES = {
|
|
4629
|
+
"session-start": { fullUnderTokens: 1500 },
|
|
4630
|
+
compact: { budgetTokens: 2500 },
|
|
4631
|
+
turn: { budgetTokens: 2500 }
|
|
4632
|
+
};
|
|
4633
|
+
function approxTokens(text) {
|
|
4634
|
+
return Math.ceil(text.length / 4);
|
|
4635
|
+
}
|
|
4636
|
+
function preamble() {
|
|
4637
|
+
return [
|
|
4638
|
+
HEADING2,
|
|
4639
|
+
"",
|
|
4640
|
+
"What follows is an index of this workspace's pinned knowledge bases \u2014",
|
|
4641
|
+
"concept ids, titles and standing only. The record bodies are NOT in this",
|
|
4642
|
+
"context.",
|
|
4643
|
+
"",
|
|
4644
|
+
"Consult records only through the strauss-kb MCP tools: `kb_load` (the",
|
|
4645
|
+
"preferred first call), `kb_query`, and `kb_trace`, passing the",
|
|
4646
|
+
"`bundlePath` listed with each base. Do not read record files directly:",
|
|
4647
|
+
"a raw file read bypasses supersession resolution, and a superseded or",
|
|
4648
|
+
"rejected record file reads exactly like a current one \u2014 only the store",
|
|
4649
|
+
"resolves chains and standing.",
|
|
4650
|
+
"",
|
|
4651
|
+
"KB content loaded earlier in a long session may have been compacted",
|
|
4652
|
+
"away. Before answering a question one of these bases governs, load it",
|
|
4653
|
+
"again at the point of use \u2014 reloading a small base costs a few thousand",
|
|
4654
|
+
"tokens."
|
|
4655
|
+
].join("\n");
|
|
4656
|
+
}
|
|
4657
|
+
async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
|
|
4658
|
+
const bundle = await store.list(absolutePath);
|
|
4659
|
+
if (bundle.length === 0) {
|
|
4660
|
+
return {
|
|
4661
|
+
path,
|
|
4662
|
+
absolutePath,
|
|
4663
|
+
mode: "empty",
|
|
4664
|
+
body: "No readable records yet \u2014 pinned ahead of being populated."
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
4667
|
+
const fullCap = pinMode === "full" ? budgetTokens : pinMode === "index" ? 0 : fullUnderTokens;
|
|
4668
|
+
let degradedFrom;
|
|
4669
|
+
if (fullCap > 0) {
|
|
4670
|
+
const full = await store.load(absolutePath, {
|
|
4671
|
+
budgetTokens: fullCap,
|
|
4672
|
+
excludeTags
|
|
4673
|
+
});
|
|
4674
|
+
if (!full.loaded && pinMode === "full") {
|
|
4675
|
+
degradedFrom = { approxTokens: full.approxTokens };
|
|
4676
|
+
}
|
|
4677
|
+
if (full.loaded) {
|
|
4678
|
+
const records = full.records.map(
|
|
4679
|
+
(hit) => [
|
|
4680
|
+
`#### ${hit.record.conceptId} \u2014 ${hit.record.frontmatter.title ?? "(untitled)"} (${hit.standing})`,
|
|
4681
|
+
"",
|
|
4682
|
+
hit.record.body.trim()
|
|
4683
|
+
].join("\n")
|
|
4684
|
+
);
|
|
4685
|
+
const superseded2 = full.superseded.map(
|
|
4686
|
+
(entry) => `- \`${entry.conceptId}\` \u2192 superseded by ${entry.supersededBy.map((id) => `\`${id}\``).join(", ") || "(missing replacement)"}`
|
|
4687
|
+
);
|
|
4688
|
+
return {
|
|
4689
|
+
path,
|
|
4690
|
+
absolutePath,
|
|
4691
|
+
mode: "full",
|
|
4692
|
+
body: [
|
|
4693
|
+
...records,
|
|
4694
|
+
...superseded2.length ? [
|
|
4695
|
+
"#### Superseded (bodies withheld \u2014 kb_trace reaches them)",
|
|
4696
|
+
...superseded2
|
|
4697
|
+
] : []
|
|
4698
|
+
].join("\n\n")
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
const adjudicated = adjudicate(bundle, bundle).filter(
|
|
4703
|
+
(hit) => matchesTags(hit.record, { excludeTags })
|
|
4704
|
+
);
|
|
4705
|
+
const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
|
|
4706
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
|
|
4707
|
+
(hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
|
|
4708
|
+
);
|
|
4709
|
+
return {
|
|
4710
|
+
path,
|
|
4711
|
+
absolutePath,
|
|
4712
|
+
mode: "index",
|
|
4713
|
+
body: [...lines, ...superseded].join("\n"),
|
|
4714
|
+
...degradedFrom ? { degradedFrom } : {}
|
|
4715
|
+
};
|
|
4716
|
+
}
|
|
4717
|
+
async function buildContext(store, workspaceDir, options = {}) {
|
|
4718
|
+
const builtin = options.profile ? CONTEXT_PROFILES[options.profile] ?? {} : {};
|
|
4719
|
+
let budgetTokens = options.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
4720
|
+
let fullUnderTokens = options.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
4721
|
+
const merged = await readMergedPins(workspaceDir);
|
|
4722
|
+
const fromManifest = mergedContextBudgets(merged, options.profile);
|
|
4723
|
+
budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
|
|
4724
|
+
fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
|
|
4725
|
+
const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
|
|
4726
|
+
const pins = merged.pins.filter(
|
|
4727
|
+
(pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
|
|
4728
|
+
);
|
|
4729
|
+
if (pins.length === 0) {
|
|
4730
|
+
return {
|
|
4731
|
+
block: "",
|
|
4732
|
+
refused: false,
|
|
4733
|
+
approxTokens: 0,
|
|
4734
|
+
budgetTokens,
|
|
4735
|
+
bases: []
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
4738
|
+
const sections = await Promise.all(
|
|
4739
|
+
pins.map(async (pin) => ({
|
|
4740
|
+
section: await renderBase(
|
|
4741
|
+
store,
|
|
4742
|
+
pin.path,
|
|
4743
|
+
pin.absolutePath,
|
|
4744
|
+
fullUnderTokens,
|
|
4745
|
+
pin.mode,
|
|
4746
|
+
budgetTokens,
|
|
4747
|
+
excludeTags
|
|
4748
|
+
),
|
|
4749
|
+
frozen: pin.frozen === true
|
|
4750
|
+
}))
|
|
4751
|
+
);
|
|
4752
|
+
const modeLabel = {
|
|
4753
|
+
index: "index only \u2014 record bodies are not here",
|
|
4754
|
+
full: "full records \u2014 this base arrives whole",
|
|
4755
|
+
empty: "empty"
|
|
4756
|
+
};
|
|
4757
|
+
for (const { section } of sections) {
|
|
4758
|
+
if (section.degradedFrom) {
|
|
4759
|
+
options.warn?.({
|
|
4760
|
+
operation: "kb.context.full-pin-degraded",
|
|
4761
|
+
path: section.path,
|
|
4762
|
+
approxTokens: section.degradedFrom.approxTokens,
|
|
4763
|
+
budgetTokens
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
3721
4766
|
}
|
|
3722
|
-
const rendered =
|
|
3723
|
-
|
|
4767
|
+
const rendered = sections.map(({ section, frozen }) => {
|
|
4768
|
+
const label = section.degradedFrom ? `index only \u2014 pinned \`mode: full\`, but its ~${section.degradedFrom.approxTokens} tokens exceed this block's ${budgetTokens}-token budget; kb_load it directly (load's budget is separate), or raise this profile's budget` : modeLabel[section.mode];
|
|
4769
|
+
return [
|
|
4770
|
+
`### ${section.path} (${label}${frozen ? " \xB7 frozen, read-only" : ""})`,
|
|
4771
|
+
"",
|
|
4772
|
+
`bundlePath: \`${section.absolutePath}\``,
|
|
4773
|
+
"",
|
|
4774
|
+
section.body
|
|
4775
|
+
].join("\n");
|
|
3724
4776
|
});
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
4777
|
+
const block = [preamble(), "", rendered.join("\n\n"), ""].join("\n");
|
|
4778
|
+
const bases = sections.map(({ section }) => ({
|
|
4779
|
+
path: section.path,
|
|
4780
|
+
absolutePath: section.absolutePath,
|
|
4781
|
+
approxTokens: approxTokens(section.body)
|
|
4782
|
+
}));
|
|
4783
|
+
const total = approxTokens(block);
|
|
4784
|
+
if (total > budgetTokens) {
|
|
4785
|
+
options.warn?.({
|
|
4786
|
+
operation: "kb.context.refused",
|
|
4787
|
+
approxTokens: total,
|
|
4788
|
+
budgetTokens,
|
|
4789
|
+
bases: bases.map((base2) => base2.path)
|
|
4790
|
+
});
|
|
4791
|
+
const refusal = [
|
|
4792
|
+
HEADING2,
|
|
4793
|
+
"",
|
|
4794
|
+
`The pinned index runs to ~${total} tokens, past the ${budgetTokens}-token`,
|
|
4795
|
+
"budget, and was not emitted \u2014 a truncated index is indistinguishable",
|
|
4796
|
+
"from a complete one. The pinned bases:",
|
|
4797
|
+
"",
|
|
4798
|
+
...bases.map(
|
|
4799
|
+
(base2) => `- ${base2.path} \u2014 ~${base2.approxTokens} tokens (bundlePath: \`${base2.absolutePath}\`)`
|
|
4800
|
+
),
|
|
4801
|
+
"",
|
|
4802
|
+
"For the question at hand, read what you need now \u2014 `kb_load` a base",
|
|
4803
|
+
"(its own budget is separate), or `kb_index` for one base's shape.",
|
|
4804
|
+
"",
|
|
4805
|
+
"To bring this block back under budget, in order of preference:",
|
|
4806
|
+
"- supersede or resolve stale records \u2014 the base shrinks, the knowledge keeps",
|
|
4807
|
+
"- force a large base to index lines: `strauss-kb pin <path> --mode index`",
|
|
4808
|
+
"- scope a pin to the profiles that need it: `strauss-kb pin <path> --profiles session-start`",
|
|
4809
|
+
"- raise this profile's budget under `context` in .strauss/kb-pins.json",
|
|
4810
|
+
"- unpin what no session actually needs",
|
|
4811
|
+
""
|
|
4812
|
+
].join("\n");
|
|
4813
|
+
return {
|
|
4814
|
+
block: refusal,
|
|
4815
|
+
refused: true,
|
|
4816
|
+
approxTokens: total,
|
|
4817
|
+
budgetTokens,
|
|
4818
|
+
bases
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
return { block, refused: false, approxTokens: total, budgetTokens, bases };
|
|
4822
|
+
}
|
|
4823
|
+
function toHookJson(block, event) {
|
|
4824
|
+
return JSON.stringify({
|
|
4825
|
+
hookSpecificOutput: {
|
|
4826
|
+
hookEventName: event,
|
|
4827
|
+
additionalContext: block
|
|
3735
4828
|
}
|
|
3736
|
-
};
|
|
4829
|
+
});
|
|
3737
4830
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
const
|
|
3743
|
-
|
|
3744
|
-
|
|
4831
|
+
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
4832
|
+
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
4833
|
+
async function syncInstructions(file, block) {
|
|
4834
|
+
const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
|
|
4835
|
+
const region = block ? `${CONTEXT_BEGIN}
|
|
4836
|
+
${block.trim()}
|
|
4837
|
+
${CONTEXT_END}` : null;
|
|
4838
|
+
if (existing === null) {
|
|
4839
|
+
if (!region) return { file, action: "unchanged" };
|
|
4840
|
+
await (0, import_promises8.writeFile)(file, `${region}
|
|
4841
|
+
`, "utf8");
|
|
4842
|
+
return { file, action: "created" };
|
|
4843
|
+
}
|
|
4844
|
+
const begin = existing.indexOf(CONTEXT_BEGIN);
|
|
4845
|
+
const end = existing.indexOf(CONTEXT_END);
|
|
4846
|
+
if (begin !== -1 && end !== -1 && end >= begin) {
|
|
4847
|
+
const before = existing.slice(0, begin);
|
|
4848
|
+
const after = existing.slice(end + CONTEXT_END.length);
|
|
4849
|
+
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
4850
|
+
if (next === existing) return { file, action: "unchanged" };
|
|
4851
|
+
await (0, import_promises8.writeFile)(file, next, "utf8");
|
|
4852
|
+
return { file, action: region ? "replaced" : "removed" };
|
|
4853
|
+
}
|
|
4854
|
+
if (!region) return { file, action: "unchanged" };
|
|
4855
|
+
await (0, import_promises8.writeFile)(
|
|
4856
|
+
file,
|
|
4857
|
+
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
4858
|
+
`,
|
|
4859
|
+
"utf8"
|
|
3745
4860
|
);
|
|
3746
|
-
|
|
3747
|
-
const rest = lines.slice(start + 1);
|
|
3748
|
-
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3749
|
-
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3750
|
-
return text ? { section, text } : null;
|
|
4861
|
+
return { file, action: "appended" };
|
|
3751
4862
|
}
|
|
3752
4863
|
|
|
4864
|
+
// src/commands/context.ts
|
|
4865
|
+
var contextCommand = define({
|
|
4866
|
+
name: "context",
|
|
4867
|
+
tool: "kb_context",
|
|
4868
|
+
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
4869
|
+
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.",
|
|
4870
|
+
input: import_zod14.z.object({
|
|
4871
|
+
budgetTokens: import_zod14.z.number().int().positive().optional().describe(
|
|
4872
|
+
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4873
|
+
),
|
|
4874
|
+
fullUnderTokens: import_zod14.z.number().int().positive().optional().describe(
|
|
4875
|
+
"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."
|
|
4876
|
+
),
|
|
4877
|
+
profile: import_zod14.z.string().optional().describe(
|
|
4878
|
+
"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."
|
|
4879
|
+
),
|
|
4880
|
+
excludeTags: import_zod14.z.array(import_zod14.z.string().min(1)).optional().describe(
|
|
4881
|
+
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
4882
|
+
),
|
|
4883
|
+
format: import_zod14.z.enum(["markdown", "json"]).optional().describe(
|
|
4884
|
+
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4885
|
+
),
|
|
4886
|
+
event: import_zod14.z.string().optional().describe(
|
|
4887
|
+
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4888
|
+
)
|
|
4889
|
+
}),
|
|
4890
|
+
fromArgv: (argv) => {
|
|
4891
|
+
const budget = argvFlag(argv, "--budget");
|
|
4892
|
+
const fullUnder = argvFlag(argv, "--full-under");
|
|
4893
|
+
const profile = argvFlag(argv, "--profile");
|
|
4894
|
+
const format = argvFlag(argv, "--format");
|
|
4895
|
+
const event = argvFlag(argv, "--event");
|
|
4896
|
+
const excludeTags = argvFlags(argv, "--exclude-tag");
|
|
4897
|
+
return {
|
|
4898
|
+
...budget ? { budgetTokens: Number(budget) } : {},
|
|
4899
|
+
...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
|
|
4900
|
+
...profile ? { profile } : {},
|
|
4901
|
+
...excludeTags.length ? { excludeTags } : {},
|
|
4902
|
+
...format ? { format } : {},
|
|
4903
|
+
...event ? { event } : {}
|
|
4904
|
+
};
|
|
4905
|
+
},
|
|
4906
|
+
run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
|
|
4907
|
+
const result = await buildContext(store, process.cwd(), {
|
|
4908
|
+
...budgetTokens ? { budgetTokens } : {},
|
|
4909
|
+
...fullUnderTokens ? { fullUnderTokens } : {},
|
|
4910
|
+
...profile ? { profile } : {},
|
|
4911
|
+
...excludeTags ? { excludeTags } : {},
|
|
4912
|
+
// Degradations — a full pin that could not fit, a refused block — go
|
|
4913
|
+
// to stderr as well as into the block itself: stderr is diagnostics on
|
|
4914
|
+
// both surfaces (hooks discard it, MCP logs it), so an operator can
|
|
4915
|
+
// see budget pressure without reading injected context.
|
|
4916
|
+
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
4917
|
+
`)
|
|
4918
|
+
});
|
|
4919
|
+
if (!result.block) return "";
|
|
4920
|
+
return format === "json" ? toHookJson(result.block, event ?? "SessionStart") : result.block;
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4923
|
+
|
|
4924
|
+
// src/commands/doctor.ts
|
|
4925
|
+
var import_zod16 = require("zod");
|
|
4926
|
+
|
|
3753
4927
|
// src/kb-edges.ts
|
|
3754
4928
|
var KB_EDGE_KINDS = [
|
|
3755
4929
|
"body-link",
|
|
@@ -3902,6 +5076,34 @@ function validateBundle(records) {
|
|
|
3902
5076
|
}
|
|
3903
5077
|
}
|
|
3904
5078
|
for (const anchor of fm.strauss_anchors ?? []) {
|
|
5079
|
+
if (anchor.span && anchor.symbol) {
|
|
5080
|
+
report(
|
|
5081
|
+
"anchor_span",
|
|
5082
|
+
conceptId2,
|
|
5083
|
+
`anchor ${anchor.file} names both a symbol and a span \u2014 one or the other`
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
5086
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
5087
|
+
report(
|
|
5088
|
+
"anchor_span",
|
|
5089
|
+
conceptId2,
|
|
5090
|
+
`anchor ${anchor.file} span ${anchor.span.start}-${anchor.span.end} ends before it starts`
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5093
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
5094
|
+
report(
|
|
5095
|
+
"anchor_span",
|
|
5096
|
+
conceptId2,
|
|
5097
|
+
`anchor ${anchor.file} is a span with hash_kind: "ast" \u2014 a span is hashed raw`
|
|
5098
|
+
);
|
|
5099
|
+
}
|
|
5100
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
5101
|
+
report(
|
|
5102
|
+
"anchor_side",
|
|
5103
|
+
conceptId2,
|
|
5104
|
+
`anchor ${anchor.file} is side: "old" with no ref`
|
|
5105
|
+
);
|
|
5106
|
+
}
|
|
3905
5107
|
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
3906
5108
|
report(
|
|
3907
5109
|
"anchor_repo",
|
|
@@ -3937,14 +5139,19 @@ var DAY_MS = 864e5;
|
|
|
3937
5139
|
function anchorResolverCounts(bundle) {
|
|
3938
5140
|
let treeSitter = 0;
|
|
3939
5141
|
let regex = 0;
|
|
5142
|
+
let span2 = 0;
|
|
5143
|
+
let oldSide = 0;
|
|
3940
5144
|
for (const record of bundle) {
|
|
3941
5145
|
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
3942
|
-
if (!anchor.hash
|
|
3943
|
-
if (anchor.
|
|
5146
|
+
if (!anchor.hash) continue;
|
|
5147
|
+
if (anchor.side === "old") oldSide += 1;
|
|
5148
|
+
if (anchor.span) span2 += 1;
|
|
5149
|
+
else if (!anchor.symbol) continue;
|
|
5150
|
+
else if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
3944
5151
|
else regex += 1;
|
|
3945
5152
|
}
|
|
3946
5153
|
}
|
|
3947
|
-
return { total: treeSitter + regex, treeSitter, regex };
|
|
5154
|
+
return { total: treeSitter + regex + span2, treeSitter, regex, span: span2, oldSide };
|
|
3948
5155
|
}
|
|
3949
5156
|
function doctor(bundle, options = {}) {
|
|
3950
5157
|
const thresholds = {
|
|
@@ -3954,7 +5161,7 @@ function doctor(bundle, options = {}) {
|
|
|
3954
5161
|
};
|
|
3955
5162
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3956
5163
|
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
3957
|
-
const
|
|
5164
|
+
const standings2 = new Map(
|
|
3958
5165
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
3959
5166
|
);
|
|
3960
5167
|
const inForce = adjudicated.filter(
|
|
@@ -3967,7 +5174,7 @@ function doctor(bundle, options = {}) {
|
|
|
3967
5174
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
3968
5175
|
group("orphaned", orphaned(bundle)),
|
|
3969
5176
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
3970
|
-
group("superseded-but-cited", supersededButCited(bundle,
|
|
5177
|
+
group("superseded-but-cited", supersededButCited(bundle, standings2)),
|
|
3971
5178
|
group("drifted", drifted(inForce)),
|
|
3972
5179
|
group("unchecked", unchecked(inForce))
|
|
3973
5180
|
];
|
|
@@ -4090,9 +5297,9 @@ function brokenSupersession(bundle, adjudicated) {
|
|
|
4090
5297
|
const findings = [];
|
|
4091
5298
|
const seen = /* @__PURE__ */ new Set();
|
|
4092
5299
|
const add = (record, note) => {
|
|
4093
|
-
const
|
|
4094
|
-
if (seen.has(
|
|
4095
|
-
seen.add(
|
|
5300
|
+
const key2 = `${record.conceptId}\0${note}`;
|
|
5301
|
+
if (seen.has(key2)) return;
|
|
5302
|
+
seen.add(key2);
|
|
4096
5303
|
findings.push(finding(record, note));
|
|
4097
5304
|
};
|
|
4098
5305
|
for (const problem of validateBundle(bundle)) {
|
|
@@ -4133,14 +5340,14 @@ function brokenSupersession(bundle, adjudicated) {
|
|
|
4133
5340
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
4134
5341
|
);
|
|
4135
5342
|
}
|
|
4136
|
-
function supersededButCited(bundle,
|
|
5343
|
+
function supersededButCited(bundle, standings2) {
|
|
4137
5344
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
4138
5345
|
const findings = [];
|
|
4139
5346
|
for (const record of bundle) {
|
|
4140
|
-
const standing =
|
|
5347
|
+
const standing = standings2.get(record.conceptId);
|
|
4141
5348
|
if (standing === "superseded" || standing === "rejected") continue;
|
|
4142
5349
|
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
4143
|
-
const targetStanding =
|
|
5350
|
+
const targetStanding = standings2.get(target.conceptId);
|
|
4144
5351
|
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
4145
5352
|
continue;
|
|
4146
5353
|
}
|
|
@@ -4231,17 +5438,17 @@ function ageInDays(record, now) {
|
|
|
4231
5438
|
}
|
|
4232
5439
|
|
|
4233
5440
|
// src/commands/reassess.ts
|
|
4234
|
-
var
|
|
5441
|
+
var import_zod15 = require("zod");
|
|
4235
5442
|
var reassessCommand = define({
|
|
4236
5443
|
name: "reassess",
|
|
4237
5444
|
tool: "kb_reassess",
|
|
4238
5445
|
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4239
5446
|
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.",
|
|
4240
|
-
input:
|
|
5447
|
+
input: import_zod15.z.object({
|
|
4241
5448
|
bundlePath,
|
|
4242
5449
|
conceptId,
|
|
4243
5450
|
repoRoot: REPO_ROOT,
|
|
4244
|
-
withDiff:
|
|
5451
|
+
withDiff: import_zod15.z.boolean().optional().describe(
|
|
4245
5452
|
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4246
5453
|
)
|
|
4247
5454
|
}),
|
|
@@ -4284,7 +5491,10 @@ var reassessCommand = define({
|
|
|
4284
5491
|
relocated.set(found.anchor, {
|
|
4285
5492
|
...found.anchor,
|
|
4286
5493
|
file: to.file,
|
|
4287
|
-
...to.symbol ? { symbol: to.symbol } : {}
|
|
5494
|
+
...to.symbol ? { symbol: to.symbol } : {},
|
|
5495
|
+
// A span is the anchor's whole address, so relocating it means
|
|
5496
|
+
// moving the line range the same code now occupies.
|
|
5497
|
+
...found.anchor.span ? { span: { start: to.startLine, end: to.endLine } } : {}
|
|
4288
5498
|
});
|
|
4289
5499
|
rebaselined.push({
|
|
4290
5500
|
file: found.anchor.file,
|
|
@@ -4383,13 +5593,13 @@ function at(file, symbol) {
|
|
|
4383
5593
|
}
|
|
4384
5594
|
|
|
4385
5595
|
// src/commands/doctor.ts
|
|
4386
|
-
var days = (what, fallback) =>
|
|
5596
|
+
var days = (what, fallback) => import_zod16.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
4387
5597
|
var doctorCommand = define({
|
|
4388
5598
|
name: "doctor",
|
|
4389
5599
|
tool: "kb_doctor",
|
|
4390
5600
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4391
5601
|
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.",
|
|
4392
|
-
input:
|
|
5602
|
+
input: import_zod16.z.object({
|
|
4393
5603
|
bundlePath,
|
|
4394
5604
|
repoRoot: REPO_ROOT,
|
|
4395
5605
|
expiringDays: days(
|
|
@@ -4404,16 +5614,16 @@ var doctorCommand = define({
|
|
|
4404
5614
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
4405
5615
|
DEFAULT_AGING_DAYS
|
|
4406
5616
|
),
|
|
4407
|
-
offline:
|
|
5617
|
+
offline: import_zod16.z.boolean().optional().describe(
|
|
4408
5618
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
4409
5619
|
),
|
|
4410
|
-
strict:
|
|
5620
|
+
strict: import_zod16.z.boolean().optional().describe(
|
|
4411
5621
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4412
5622
|
),
|
|
4413
|
-
drifted:
|
|
5623
|
+
drifted: import_zod16.z.boolean().optional().describe(
|
|
4414
5624
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4415
5625
|
),
|
|
4416
|
-
withDiff:
|
|
5626
|
+
withDiff: import_zod16.z.boolean().optional().describe(
|
|
4417
5627
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
4418
5628
|
)
|
|
4419
5629
|
}),
|
|
@@ -4469,7 +5679,7 @@ var doctorCommand = define({
|
|
|
4469
5679
|
...hints.length ? { hints } : {}
|
|
4470
5680
|
};
|
|
4471
5681
|
}
|
|
4472
|
-
const
|
|
5682
|
+
const standings2 = new Map(
|
|
4473
5683
|
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4474
5684
|
hit.record.conceptId,
|
|
4475
5685
|
hit.standing
|
|
@@ -4483,7 +5693,7 @@ var doctorCommand = define({
|
|
|
4483
5693
|
(entry) => entry.conceptId === found.conceptId
|
|
4484
5694
|
);
|
|
4485
5695
|
if (!record) continue;
|
|
4486
|
-
const standing =
|
|
5696
|
+
const standing = standings2.get(record.conceptId);
|
|
4487
5697
|
const built = await reassessPacket(
|
|
4488
5698
|
repoRoot ?? process.cwd(),
|
|
4489
5699
|
record,
|
|
@@ -4520,15 +5730,16 @@ var doctorCommand = define({
|
|
|
4520
5730
|
});
|
|
4521
5731
|
function render2(result) {
|
|
4522
5732
|
if (result.packets) return renderPackets(result);
|
|
4523
|
-
const { thresholds } = result;
|
|
5733
|
+
const { thresholds, anchorResolvers: counts } = result;
|
|
4524
5734
|
const lines = [
|
|
4525
5735
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
4526
5736
|
`records: ${result.recordCount}`,
|
|
4527
5737
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
4528
5738
|
`checked: ${result.checkedAt}`,
|
|
4529
|
-
...
|
|
4530
|
-
|
|
4531
|
-
|
|
5739
|
+
...counts.total ? [anchorLine(counts)] : [],
|
|
5740
|
+
// Its own line: an old-side anchor may name a whole file, which no
|
|
5741
|
+
// resolver bucket and no `total` counts.
|
|
5742
|
+
...counts.oldSide ? [`old-side anchors: ${counts.oldSide}`] : [],
|
|
4532
5743
|
""
|
|
4533
5744
|
];
|
|
4534
5745
|
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
@@ -4553,6 +5764,14 @@ function render2(result) {
|
|
|
4553
5764
|
);
|
|
4554
5765
|
return lines.join("\n");
|
|
4555
5766
|
}
|
|
5767
|
+
function anchorLine(counts) {
|
|
5768
|
+
const parts = [
|
|
5769
|
+
`${counts.treeSitter} tree-sitter`,
|
|
5770
|
+
`${counts.regex} regex`,
|
|
5771
|
+
...counts.span ? [`${counts.span} span`] : []
|
|
5772
|
+
];
|
|
5773
|
+
return `anchors: ${counts.total} hashed \u2014 ${parts.join(", ")}`;
|
|
5774
|
+
}
|
|
4556
5775
|
function renderPackets(result) {
|
|
4557
5776
|
const packets = result.packets ?? [];
|
|
4558
5777
|
const lines = [
|
|
@@ -4575,23 +5794,158 @@ function renderPackets(result) {
|
|
|
4575
5794
|
})
|
|
4576
5795
|
);
|
|
4577
5796
|
}
|
|
4578
|
-
return lines.join("\n");
|
|
5797
|
+
return lines.join("\n");
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
// src/commands/export.ts
|
|
5801
|
+
var import_promises9 = require("fs/promises");
|
|
5802
|
+
var import_node_path11 = require("path");
|
|
5803
|
+
var import_zod17 = require("zod");
|
|
5804
|
+
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
5805
|
+
var MARKER = "<!-- strauss-kb export: ";
|
|
5806
|
+
var exportCommand = define({
|
|
5807
|
+
name: "export",
|
|
5808
|
+
tool: "kb_export",
|
|
5809
|
+
usage: "export --format madr --to <dir>",
|
|
5810
|
+
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.",
|
|
5811
|
+
input: import_zod17.z.object({
|
|
5812
|
+
bundlePath,
|
|
5813
|
+
format: import_zod17.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
5814
|
+
to: import_zod17.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
5815
|
+
}),
|
|
5816
|
+
fromArgv: (argv, path) => ({
|
|
5817
|
+
bundlePath: path,
|
|
5818
|
+
format: argvFlag(argv, "--format"),
|
|
5819
|
+
to: argvFlag(argv, "--to")
|
|
5820
|
+
}),
|
|
5821
|
+
run: async ({ store }, { bundlePath: path, to }) => {
|
|
5822
|
+
const bundle = await store.list(path);
|
|
5823
|
+
const decisions = selectDecisions(bundle).sort(
|
|
5824
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
5825
|
+
);
|
|
5826
|
+
const adjudicated = new Map(
|
|
5827
|
+
adjudicate(decisions, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
5828
|
+
);
|
|
5829
|
+
await (0, import_promises9.mkdir)(to, { recursive: true });
|
|
5830
|
+
const taken = await existingFiles(to);
|
|
5831
|
+
let next = Math.max(0, ...[...taken.values()].map((row) => row.number)) + 1;
|
|
5832
|
+
const exported = [];
|
|
5833
|
+
const foreign = [];
|
|
5834
|
+
for (const record of decisions) {
|
|
5835
|
+
const slug = record.conceptId.slice(record.conceptId.indexOf(".") + 1);
|
|
5836
|
+
const held = taken.get(slug);
|
|
5837
|
+
if (held && !held.ours) {
|
|
5838
|
+
foreign.push({ conceptId: record.conceptId, file: held.file });
|
|
5839
|
+
continue;
|
|
5840
|
+
}
|
|
5841
|
+
const number = held?.number ?? next++;
|
|
5842
|
+
const file = `${String(number).padStart(4, "0")}-${slug}.md`;
|
|
5843
|
+
const status = statusLine(adjudicated.get(record.conceptId));
|
|
5844
|
+
await publish((0, import_node_path11.join)(to, file), renderMadr(record, status));
|
|
5845
|
+
exported.push({ conceptId: record.conceptId, file, status });
|
|
5846
|
+
}
|
|
5847
|
+
return { to, format: "madr", exported, foreign };
|
|
5848
|
+
},
|
|
5849
|
+
render: (result) => {
|
|
5850
|
+
const { exported, foreign, to } = result;
|
|
5851
|
+
return [
|
|
5852
|
+
`Wrote ${exported.length} MADR file${exported.length === 1 ? "" : "s"} to ${to}.`,
|
|
5853
|
+
...exported.map(
|
|
5854
|
+
(entry) => `- ${entry.file} ${entry.conceptId} [${entry.status}]`
|
|
5855
|
+
),
|
|
5856
|
+
...foreign.map(
|
|
5857
|
+
(entry) => `- skipped ${entry.conceptId}: ${entry.file} was not written by export`
|
|
5858
|
+
)
|
|
5859
|
+
].join("\n");
|
|
5860
|
+
}
|
|
5861
|
+
});
|
|
5862
|
+
async function publish(target, contents) {
|
|
5863
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
5864
|
+
await (0, import_promises9.writeFile)(staging, contents, "utf8");
|
|
5865
|
+
try {
|
|
5866
|
+
await (0, import_promises9.rename)(staging, target);
|
|
5867
|
+
} catch (error) {
|
|
5868
|
+
await (0, import_promises9.unlink)(staging).catch(() => void 0);
|
|
5869
|
+
throw error;
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
async function existingFiles(to) {
|
|
5873
|
+
const names = await (0, import_promises9.readdir)(to).catch(() => []);
|
|
5874
|
+
const taken = /* @__PURE__ */ new Map();
|
|
5875
|
+
for (const name of names.sort()) {
|
|
5876
|
+
const [, number, slug] = NUMBERED.exec(name) ?? [];
|
|
5877
|
+
if (!number || !slug) continue;
|
|
5878
|
+
const text = await (0, import_promises9.readFile)((0, import_node_path11.join)(to, name), "utf8").catch(() => "");
|
|
5879
|
+
taken.set(slug, {
|
|
5880
|
+
file: name,
|
|
5881
|
+
number: Number(number),
|
|
5882
|
+
ours: text.includes(MARKER)
|
|
5883
|
+
});
|
|
5884
|
+
}
|
|
5885
|
+
return taken;
|
|
5886
|
+
}
|
|
5887
|
+
function statusLine(hit) {
|
|
5888
|
+
const status = hit?.record.frontmatter.strauss_status ?? "draft";
|
|
5889
|
+
if (status !== "superseded") return status;
|
|
5890
|
+
const by = (hit?.heads ?? []).map((head) => head.conceptId);
|
|
5891
|
+
return by.length ? `superseded by ${by.join(", ")}` : "superseded";
|
|
5892
|
+
}
|
|
5893
|
+
function renderMadr(record, status) {
|
|
5894
|
+
const sections = bodySections(record.body);
|
|
5895
|
+
const blocks = [
|
|
5896
|
+
`# ${record.frontmatter.title ?? record.conceptId}`,
|
|
5897
|
+
"## Status",
|
|
5898
|
+
status
|
|
5899
|
+
];
|
|
5900
|
+
push(blocks, "Context and Problem Statement", record.frontmatter.description);
|
|
5901
|
+
push(blocks, "Considered Options", sections.get("Rejected"));
|
|
5902
|
+
push(blocks, "Decision Outcome", sections.get("Decision"));
|
|
5903
|
+
push(blocks, "Consequences", sections.get("Impact"));
|
|
5904
|
+
blocks.push(`${MARKER}${record.conceptId} -->`);
|
|
5905
|
+
return `${blocks.join("\n\n")}
|
|
5906
|
+
`;
|
|
5907
|
+
}
|
|
5908
|
+
function push(blocks, heading, text) {
|
|
5909
|
+
if (text?.trim()) blocks.push(`## ${heading}`, text.trim());
|
|
5910
|
+
}
|
|
5911
|
+
function bodySections(body) {
|
|
5912
|
+
const generated = new RegExp(
|
|
5913
|
+
`^(?:(?:${Object.values(LINK_RELS).map((spec) => spec.phrase).join("|")}) \\[[^\\]]+\\]\\([^)]+\\.md\\)\\.|\\[\\^[^\\]]+\\]: .*)$`
|
|
5914
|
+
);
|
|
5915
|
+
const sections = /* @__PURE__ */ new Map();
|
|
5916
|
+
let heading = null;
|
|
5917
|
+
let lines = [];
|
|
5918
|
+
const flush = () => {
|
|
5919
|
+
if (heading) sections.set(heading, lines.join("\n").trim());
|
|
5920
|
+
};
|
|
5921
|
+
for (const line of body.split("\n")) {
|
|
5922
|
+
const match = /^## (.+?)\s*$/.exec(line);
|
|
5923
|
+
if (match) {
|
|
5924
|
+
flush();
|
|
5925
|
+
heading = match[1] ?? null;
|
|
5926
|
+
lines = [];
|
|
5927
|
+
} else if (heading && !generated.test(line)) {
|
|
5928
|
+
lines.push(line);
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
flush();
|
|
5932
|
+
return sections;
|
|
4579
5933
|
}
|
|
4580
5934
|
|
|
4581
5935
|
// src/commands/impact.ts
|
|
4582
|
-
var
|
|
5936
|
+
var import_zod18 = require("zod");
|
|
4583
5937
|
var impactCommand = define({
|
|
4584
5938
|
name: "impact",
|
|
4585
5939
|
tool: "kb_impact",
|
|
4586
5940
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
4587
5941
|
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.",
|
|
4588
|
-
input:
|
|
5942
|
+
input: import_zod18.z.object({
|
|
4589
5943
|
bundlePath,
|
|
4590
5944
|
conceptId,
|
|
4591
|
-
depth:
|
|
5945
|
+
depth: import_zod18.z.number().int().positive().optional().describe(
|
|
4592
5946
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
4593
5947
|
),
|
|
4594
|
-
rels:
|
|
5948
|
+
rels: import_zod18.z.array(import_zod18.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
4595
5949
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
4596
5950
|
)
|
|
4597
5951
|
}),
|
|
@@ -4612,35 +5966,49 @@ var impactCommand = define({
|
|
|
4612
5966
|
});
|
|
4613
5967
|
|
|
4614
5968
|
// src/commands/list.ts
|
|
4615
|
-
var
|
|
5969
|
+
var import_zod19 = require("zod");
|
|
4616
5970
|
var listCommand = define({
|
|
4617
5971
|
name: "list",
|
|
4618
5972
|
tool: "kb_list",
|
|
4619
|
-
usage: "list [type]",
|
|
4620
|
-
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
4621
|
-
input:
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
5973
|
+
usage: "list [type] [--tag T]...",
|
|
5974
|
+
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
5975
|
+
input: import_zod19.z.object({
|
|
5976
|
+
bundlePath,
|
|
5977
|
+
type: import_zod19.z.enum(KB_RECORD_TYPES).optional(),
|
|
5978
|
+
tags: TAGS
|
|
5979
|
+
}),
|
|
5980
|
+
fromArgv: (argv, path) => {
|
|
5981
|
+
const tags = argvFlags(argv, "--tag");
|
|
5982
|
+
const type = argvPositional(argv, "--tag");
|
|
5983
|
+
return {
|
|
5984
|
+
bundlePath: path,
|
|
5985
|
+
...type ? { type } : {},
|
|
5986
|
+
...tags.length ? { tags } : {}
|
|
5987
|
+
};
|
|
5988
|
+
},
|
|
5989
|
+
run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
|
|
5990
|
+
(record) => ({
|
|
5991
|
+
conceptId: record.conceptId,
|
|
5992
|
+
title: record.frontmatter.title ?? null,
|
|
5993
|
+
description: record.frontmatter.description ?? null,
|
|
5994
|
+
status: record.frontmatter.strauss_status,
|
|
5995
|
+
anchors: record.frontmatter.strauss_anchors ?? []
|
|
5996
|
+
})
|
|
5997
|
+
)
|
|
4630
5998
|
});
|
|
4631
5999
|
|
|
4632
6000
|
// src/commands/load.ts
|
|
4633
|
-
var
|
|
6001
|
+
var import_zod20 = require("zod");
|
|
4634
6002
|
var loadCommand = define({
|
|
4635
6003
|
name: "load",
|
|
4636
6004
|
tool: "kb_load",
|
|
4637
6005
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
4638
6006
|
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.",
|
|
4639
|
-
input:
|
|
6007
|
+
input: import_zod20.z.object({
|
|
4640
6008
|
bundlePath,
|
|
4641
|
-
type:
|
|
4642
|
-
budgetTokens:
|
|
4643
|
-
all:
|
|
6009
|
+
type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
|
|
6010
|
+
budgetTokens: import_zod20.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6011
|
+
all: import_zod20.z.boolean().optional().describe(
|
|
4644
6012
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4645
6013
|
),
|
|
4646
6014
|
repoRoot: REPO_ROOT
|
|
@@ -4682,25 +6050,25 @@ var loadCommand = define({
|
|
|
4682
6050
|
});
|
|
4683
6051
|
|
|
4684
6052
|
// src/commands/log.ts
|
|
4685
|
-
var
|
|
6053
|
+
var import_zod21 = require("zod");
|
|
4686
6054
|
var logCommand = define({
|
|
4687
6055
|
name: "log",
|
|
4688
6056
|
tool: "kb_log",
|
|
4689
6057
|
usage: "log",
|
|
4690
6058
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4691
|
-
input:
|
|
6059
|
+
input: import_zod21.z.object({ bundlePath }),
|
|
4692
6060
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4693
6061
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4694
6062
|
});
|
|
4695
6063
|
|
|
4696
6064
|
// src/commands/no-decision.ts
|
|
4697
|
-
var
|
|
6065
|
+
var import_zod22 = require("zod");
|
|
4698
6066
|
var noDecisionCommand = define({
|
|
4699
6067
|
name: "no-decision",
|
|
4700
6068
|
tool: "kb_no_decision",
|
|
4701
6069
|
usage: "no-decision <reason...>",
|
|
4702
6070
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4703
|
-
input:
|
|
6071
|
+
input: import_zod22.z.object({ bundlePath, reason: import_zod22.z.string().min(1) }),
|
|
4704
6072
|
fromArgv: (argv, path) => ({
|
|
4705
6073
|
bundlePath: path,
|
|
4706
6074
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4717,20 +6085,20 @@ var noDecisionCommand = define({
|
|
|
4717
6085
|
});
|
|
4718
6086
|
|
|
4719
6087
|
// src/commands/pack.ts
|
|
4720
|
-
var
|
|
6088
|
+
var import_zod23 = require("zod");
|
|
4721
6089
|
var packCommand = define({
|
|
4722
6090
|
name: "pack",
|
|
4723
6091
|
tool: "kb_pack",
|
|
4724
6092
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4725
6093
|
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.",
|
|
4726
|
-
input:
|
|
6094
|
+
input: import_zod23.z.object({
|
|
4727
6095
|
bundlePath,
|
|
4728
6096
|
conceptId,
|
|
4729
|
-
hops:
|
|
4730
|
-
maxNodes:
|
|
6097
|
+
hops: import_zod23.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6098
|
+
maxNodes: import_zod23.z.number().int().positive().optional().describe(
|
|
4731
6099
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4732
6100
|
),
|
|
4733
|
-
budgetTokens:
|
|
6101
|
+
budgetTokens: import_zod23.z.number().int().positive().optional().describe(
|
|
4734
6102
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4735
6103
|
)
|
|
4736
6104
|
}),
|
|
@@ -4817,22 +6185,22 @@ function warningLabel(warning) {
|
|
|
4817
6185
|
}
|
|
4818
6186
|
|
|
4819
6187
|
// src/commands/pin.ts
|
|
4820
|
-
var
|
|
6188
|
+
var import_zod24 = require("zod");
|
|
4821
6189
|
var pinCommand = define({
|
|
4822
6190
|
name: "pin",
|
|
4823
6191
|
tool: "kb_pin",
|
|
4824
6192
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4825
6193
|
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.",
|
|
4826
|
-
input:
|
|
6194
|
+
input: import_zod24.z.object({
|
|
4827
6195
|
bundlePath,
|
|
4828
|
-
mode:
|
|
6196
|
+
mode: import_zod24.z.enum(["full", "index"]).optional().describe(
|
|
4829
6197
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
4830
6198
|
),
|
|
4831
|
-
profiles:
|
|
4832
|
-
layer:
|
|
6199
|
+
profiles: import_zod24.z.array(import_zod24.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6200
|
+
layer: import_zod24.z.enum(["project", "local", "user"]).optional().describe(
|
|
4833
6201
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4834
6202
|
),
|
|
4835
|
-
frozen:
|
|
6203
|
+
frozen: import_zod24.z.boolean().optional().describe(
|
|
4836
6204
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4837
6205
|
)
|
|
4838
6206
|
}),
|
|
@@ -4861,47 +6229,460 @@ var pinCommand = define({
|
|
|
4861
6229
|
});
|
|
4862
6230
|
|
|
4863
6231
|
// src/commands/pins.ts
|
|
4864
|
-
var
|
|
6232
|
+
var import_zod25 = require("zod");
|
|
4865
6233
|
var pinsCommand = define({
|
|
4866
6234
|
name: "pins",
|
|
4867
6235
|
tool: "kb_pins",
|
|
4868
6236
|
usage: "pins",
|
|
4869
6237
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4870
|
-
input:
|
|
6238
|
+
input: import_zod25.z.object({}),
|
|
4871
6239
|
fromArgv: () => ({}),
|
|
4872
6240
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4873
6241
|
});
|
|
4874
6242
|
|
|
6243
|
+
// src/commands/promote/command.ts
|
|
6244
|
+
var import_node_path12 = require("path");
|
|
6245
|
+
|
|
6246
|
+
// src/commands/promote/carry.ts
|
|
6247
|
+
var PROMOTION_SOURCE_ID = "promoted";
|
|
6248
|
+
var CARRIED_STATUS = {
|
|
6249
|
+
draft: "accepted",
|
|
6250
|
+
proposed: "accepted",
|
|
6251
|
+
accepted: "accepted",
|
|
6252
|
+
open: "open",
|
|
6253
|
+
resolved: "resolved",
|
|
6254
|
+
rejected: "rejected",
|
|
6255
|
+
superseded: "superseded"
|
|
6256
|
+
};
|
|
6257
|
+
function carry(record, promoted, source) {
|
|
6258
|
+
const {
|
|
6259
|
+
type: _type,
|
|
6260
|
+
// Both name records in the source base, and supersession in the target is
|
|
6261
|
+
// a separate question from whether this record belongs there at all.
|
|
6262
|
+
strauss_supersedes: _supersedes,
|
|
6263
|
+
strauss_superseded_by: _supersededBy,
|
|
6264
|
+
// A check run against the source repository, which the target never saw.
|
|
6265
|
+
verified: _verified,
|
|
6266
|
+
...rest
|
|
6267
|
+
} = record.frontmatter;
|
|
6268
|
+
const links = rest.strauss_links ?? [];
|
|
6269
|
+
const kept = links.filter((link2) => promoted.has(link2.target));
|
|
6270
|
+
const dropped = links.filter((link2) => !promoted.has(link2.target));
|
|
6271
|
+
const tags = (rest.tags ?? []).filter((tag) => !isReviewTag(tag));
|
|
6272
|
+
const frontmatter = {
|
|
6273
|
+
...rest,
|
|
6274
|
+
strauss_status: CARRIED_STATUS[rest.strauss_status]
|
|
6275
|
+
};
|
|
6276
|
+
setOrDrop(frontmatter, "tags", tags);
|
|
6277
|
+
setOrDrop(frontmatter, "strauss_links", kept);
|
|
6278
|
+
let body = withoutLinkSentences(record.body, dropped);
|
|
6279
|
+
if (source) {
|
|
6280
|
+
frontmatter.sources = [
|
|
6281
|
+
...(rest.sources ?? []).filter(
|
|
6282
|
+
(entry) => entry.id !== PROMOTION_SOURCE_ID
|
|
6283
|
+
),
|
|
6284
|
+
{ id: PROMOTION_SOURCE_ID, resource: source }
|
|
6285
|
+
];
|
|
6286
|
+
body = `${stripFootnote(body).trimEnd()}
|
|
6287
|
+
|
|
6288
|
+
[^${PROMOTION_SOURCE_ID}]: ${source}
|
|
6289
|
+
`;
|
|
6290
|
+
}
|
|
6291
|
+
return {
|
|
6292
|
+
frontmatter,
|
|
6293
|
+
body,
|
|
6294
|
+
droppedLinks: dropped.map(({ target, rel }) => ({ target, rel }))
|
|
6295
|
+
};
|
|
6296
|
+
}
|
|
6297
|
+
function isReviewTag(tag) {
|
|
6298
|
+
return tag === "review" || tag.startsWith("review:");
|
|
6299
|
+
}
|
|
6300
|
+
function withoutLinkSentences(body, dropped) {
|
|
6301
|
+
const sentences = new Set(
|
|
6302
|
+
dropped.filter((link2) => isKbLinkRel(link2.rel)).map(
|
|
6303
|
+
(link2) => `${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
6304
|
+
)
|
|
6305
|
+
);
|
|
6306
|
+
if (!sentences.size) return body;
|
|
6307
|
+
return body.split("\n\n").filter((block) => !sentences.has(block.trim())).join("\n\n");
|
|
6308
|
+
}
|
|
6309
|
+
function stripFootnote(body) {
|
|
6310
|
+
return body.split("\n").filter((line) => !line.startsWith(`[^${PROMOTION_SOURCE_ID}]: `)).join("\n");
|
|
6311
|
+
}
|
|
6312
|
+
function setOrDrop(frontmatter, key2, value) {
|
|
6313
|
+
if (value.length) frontmatter[key2] = value;
|
|
6314
|
+
else delete frontmatter[key2];
|
|
6315
|
+
}
|
|
6316
|
+
|
|
6317
|
+
// src/kb-links/inbound.ts
|
|
6318
|
+
function inboundIndex(bundle) {
|
|
6319
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
6320
|
+
for (const record of bundle) {
|
|
6321
|
+
for (const link2 of record.frontmatter.strauss_links ?? []) {
|
|
6322
|
+
if (link2.target === record.conceptId) continue;
|
|
6323
|
+
const edges = byTarget.get(link2.target) ?? [];
|
|
6324
|
+
if (edges.some(
|
|
6325
|
+
(edge) => edge.from === record.conceptId && edge.rel === link2.rel
|
|
6326
|
+
)) {
|
|
6327
|
+
continue;
|
|
6328
|
+
}
|
|
6329
|
+
edges.push({ from: record.conceptId, rel: link2.rel });
|
|
6330
|
+
byTarget.set(link2.target, edges);
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
return byTarget;
|
|
6334
|
+
}
|
|
6335
|
+
|
|
6336
|
+
// src/kb-links/backlinks.ts
|
|
6337
|
+
function backlinks(targetId, bundle) {
|
|
6338
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
6339
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
6340
|
+
const standingOf = new Map(
|
|
6341
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
6342
|
+
);
|
|
6343
|
+
const rows = [];
|
|
6344
|
+
for (const edge of inboundIndex(bundle).get(targetId) ?? []) {
|
|
6345
|
+
const record = byId.get(edge.from);
|
|
6346
|
+
if (!record) continue;
|
|
6347
|
+
const hit = standingOf.get(edge.from);
|
|
6348
|
+
rows.push({
|
|
6349
|
+
...edge,
|
|
6350
|
+
title: record.frontmatter.title ?? null,
|
|
6351
|
+
standing: hit?.standing ?? "unsettled",
|
|
6352
|
+
warnings: hit?.warnings ?? []
|
|
6353
|
+
});
|
|
6354
|
+
}
|
|
6355
|
+
return {
|
|
6356
|
+
target: targetId,
|
|
6357
|
+
backlinks: rows.sort(
|
|
6358
|
+
(left, right) => left.from.localeCompare(right.from) || left.rel.localeCompare(right.rel)
|
|
6359
|
+
)
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
|
|
6363
|
+
// src/kb-links/impact.ts
|
|
6364
|
+
function impact(targetId, bundle, options = {}) {
|
|
6365
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
6366
|
+
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
6367
|
+
const rels = resolveRels(options.rels);
|
|
6368
|
+
const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
|
|
6369
|
+
const inbound = inboundIndex(bundle);
|
|
6370
|
+
const standingOf = new Map(
|
|
6371
|
+
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
6372
|
+
);
|
|
6373
|
+
const reached = /* @__PURE__ */ new Map();
|
|
6374
|
+
const stopped = [];
|
|
6375
|
+
let frontier = [targetId];
|
|
6376
|
+
let depth = 0;
|
|
6377
|
+
while (frontier.length && depth < maxDepth) {
|
|
6378
|
+
depth += 1;
|
|
6379
|
+
const next = [];
|
|
6380
|
+
const consider = (dependantId, edge) => {
|
|
6381
|
+
if (dependantId === targetId) return;
|
|
6382
|
+
const existing = reached.get(dependantId);
|
|
6383
|
+
if (existing) {
|
|
6384
|
+
if (!hasEdge(existing.via, edge)) existing.via.push(edge);
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
6387
|
+
const record = byId.get(dependantId);
|
|
6388
|
+
if (!record) return;
|
|
6389
|
+
const hit = standingOf.get(dependantId);
|
|
6390
|
+
const entry = {
|
|
6391
|
+
conceptId: dependantId,
|
|
6392
|
+
title: record.frontmatter.title ?? null,
|
|
6393
|
+
standing: hit?.standing ?? "unsettled",
|
|
6394
|
+
warnings: hit?.warnings ?? [],
|
|
6395
|
+
depth,
|
|
6396
|
+
via: [edge]
|
|
6397
|
+
};
|
|
6398
|
+
reached.set(dependantId, entry);
|
|
6399
|
+
if (entry.standing === "superseded" || entry.standing === "rejected") {
|
|
6400
|
+
stopped.push(dependantId);
|
|
6401
|
+
return;
|
|
6402
|
+
}
|
|
6403
|
+
next.push(dependantId);
|
|
6404
|
+
};
|
|
6405
|
+
for (const id of frontier) {
|
|
6406
|
+
for (const edge of inbound.get(id) ?? []) {
|
|
6407
|
+
if (!rels.has(edge.rel)) continue;
|
|
6408
|
+
if (dependantEnd(edge.rel) !== "source") continue;
|
|
6409
|
+
consider(edge.from, { source: edge.from, target: id, rel: edge.rel });
|
|
6410
|
+
}
|
|
6411
|
+
for (const link2 of byId.get(id)?.frontmatter.strauss_links ?? []) {
|
|
6412
|
+
if (!rels.has(link2.rel)) continue;
|
|
6413
|
+
if (dependantEnd(link2.rel) !== "target") continue;
|
|
6414
|
+
if (link2.target === id) continue;
|
|
6415
|
+
consider(link2.target, {
|
|
6416
|
+
source: id,
|
|
6417
|
+
target: link2.target,
|
|
6418
|
+
rel: link2.rel
|
|
6419
|
+
});
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
frontier = next;
|
|
6423
|
+
}
|
|
6424
|
+
return {
|
|
6425
|
+
root: targetId,
|
|
6426
|
+
impacted: [...reached.values()].sort(
|
|
6427
|
+
(left, right) => left.depth - right.depth || left.conceptId.localeCompare(right.conceptId)
|
|
6428
|
+
),
|
|
6429
|
+
stopped: stopped.sort(),
|
|
6430
|
+
truncated: frontier.length > 0,
|
|
6431
|
+
unexpanded: [...frontier].sort()
|
|
6432
|
+
};
|
|
6433
|
+
}
|
|
6434
|
+
function resolveRels(rels) {
|
|
6435
|
+
if (!rels?.length) return new Set(KB_CAUSAL_LINK_RELS);
|
|
6436
|
+
for (const rel of rels) {
|
|
6437
|
+
if (!isKbLinkRel(rel) || LINK_RELS[rel].dependant === null) {
|
|
6438
|
+
throw new KbUnknownLinkRelError(rel, KB_CAUSAL_LINK_RELS);
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
return new Set(rels);
|
|
6442
|
+
}
|
|
6443
|
+
function dependantEnd(rel) {
|
|
6444
|
+
return isKbLinkRel(rel) ? LINK_RELS[rel].dependant : null;
|
|
6445
|
+
}
|
|
6446
|
+
function hasEdge(edges, edge) {
|
|
6447
|
+
return edges.some(
|
|
6448
|
+
(existing) => existing.source === edge.source && existing.target === edge.target && existing.rel === edge.rel
|
|
6449
|
+
);
|
|
6450
|
+
}
|
|
6451
|
+
|
|
6452
|
+
// src/commands/promote/standing.ts
|
|
6453
|
+
var WITHDRAWN = ["superseded", "rejected"];
|
|
6454
|
+
function standings(bundle) {
|
|
6455
|
+
return new Map(
|
|
6456
|
+
adjudicate(bundle, bundle).map((hit) => [
|
|
6457
|
+
hit.record.conceptId,
|
|
6458
|
+
hit.standing
|
|
6459
|
+
])
|
|
6460
|
+
);
|
|
6461
|
+
}
|
|
6462
|
+
function isWithdrawn(standing) {
|
|
6463
|
+
return standing !== void 0 && WITHDRAWN.includes(standing);
|
|
6464
|
+
}
|
|
6465
|
+
|
|
6466
|
+
// src/commands/promote/candidates.ts
|
|
6467
|
+
var REVIEW_TAG = "review";
|
|
6468
|
+
var SETTLED = ["resolved"];
|
|
6469
|
+
function promoteCandidates(bundle) {
|
|
6470
|
+
const inbound = inboundIndex(bundle);
|
|
6471
|
+
const standing = standings(bundle);
|
|
6472
|
+
const rows = [];
|
|
6473
|
+
for (const record of bundle) {
|
|
6474
|
+
if (isWithdrawn(standing.get(record.conceptId))) continue;
|
|
6475
|
+
const why2 = candidateReason(record, inbound.get(record.conceptId) ?? []);
|
|
6476
|
+
if (!why2) continue;
|
|
6477
|
+
rows.push({
|
|
6478
|
+
conceptId: record.conceptId,
|
|
6479
|
+
type: recordType(record.conceptId),
|
|
6480
|
+
title: record.frontmatter.title ?? null,
|
|
6481
|
+
why: why2
|
|
6482
|
+
});
|
|
6483
|
+
}
|
|
6484
|
+
return rows;
|
|
6485
|
+
}
|
|
6486
|
+
function candidateReason(record, inbound) {
|
|
6487
|
+
const { strauss_status: status, tags } = record.frontmatter;
|
|
6488
|
+
switch (recordType(record.conceptId)) {
|
|
6489
|
+
case "decision":
|
|
6490
|
+
if (isNoDecisionRecord(record)) return null;
|
|
6491
|
+
return tags?.includes(REVIEW_TAG) ? null : "decision no longer under review";
|
|
6492
|
+
case "constraint":
|
|
6493
|
+
return status === "proposed" ? "constraint still proposed \u2014 the target base is where it settles" : null;
|
|
6494
|
+
case "contract":
|
|
6495
|
+
return "contract \u2014 it outlives the change that introduced it";
|
|
6496
|
+
case "requirement":
|
|
6497
|
+
return inbound.some((edge) => edge.rel === "satisfies") ? "requirement something in the base satisfies" : null;
|
|
6498
|
+
case "risk":
|
|
6499
|
+
return record.frontmatter.strauss_materiality === "blocking" && !SETTLED.includes(status) ? "blocking risk still open" : null;
|
|
6500
|
+
default:
|
|
6501
|
+
return null;
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
function recordType(conceptId2) {
|
|
6505
|
+
return conceptId2.slice(0, conceptId2.indexOf("."));
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
// src/commands/promote/model.ts
|
|
6509
|
+
var import_zod26 = require("zod");
|
|
6510
|
+
var promoteInputSchema = import_zod26.z.object({
|
|
6511
|
+
bundlePath,
|
|
6512
|
+
conceptIds: import_zod26.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
6513
|
+
to: import_zod26.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
6514
|
+
source: import_zod26.z.string().min(1).optional().describe(
|
|
6515
|
+
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
6516
|
+
),
|
|
6517
|
+
force: import_zod26.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
6518
|
+
list: import_zod26.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
6519
|
+
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
6520
|
+
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
6521
|
+
path: ["to"]
|
|
6522
|
+
}).refine(
|
|
6523
|
+
(input) => input.list === true || (input.conceptIds?.length ?? 0) > 0,
|
|
6524
|
+
{
|
|
6525
|
+
message: "name at least one concept id to promote, or pass --list",
|
|
6526
|
+
path: ["conceptIds"]
|
|
6527
|
+
}
|
|
6528
|
+
);
|
|
6529
|
+
|
|
6530
|
+
// src/commands/promote/command.ts
|
|
6531
|
+
var promoteCommand = define({
|
|
6532
|
+
name: "promote",
|
|
6533
|
+
tool: "kb_promote",
|
|
6534
|
+
usage: "promote <concept-id...> --to <bundle> [--source <url>] [--force] | --list",
|
|
6535
|
+
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.",
|
|
6536
|
+
input: promoteInputSchema,
|
|
6537
|
+
fromArgv: (argv, path) => {
|
|
6538
|
+
const to = argvFlag(argv, "--to");
|
|
6539
|
+
const source = argvFlag(argv, "--source");
|
|
6540
|
+
const words = argv.slice(1);
|
|
6541
|
+
for (const flag of ["--to", "--source"]) {
|
|
6542
|
+
const at2 = words.indexOf(flag);
|
|
6543
|
+
if (at2 !== -1) words.splice(at2, 2);
|
|
6544
|
+
}
|
|
6545
|
+
const conceptIds = words.filter((word) => !word.startsWith("--"));
|
|
6546
|
+
return {
|
|
6547
|
+
bundlePath: path,
|
|
6548
|
+
...conceptIds.length ? { conceptIds } : {},
|
|
6549
|
+
...to !== void 0 ? { to } : {},
|
|
6550
|
+
...source !== void 0 ? { source } : {},
|
|
6551
|
+
...argv.includes("--force") ? { force: true } : {},
|
|
6552
|
+
...argv.includes("--list") ? { list: true } : {}
|
|
6553
|
+
};
|
|
6554
|
+
},
|
|
6555
|
+
run: async ({ store, actor }, { bundlePath: path, conceptIds, to, source, force, list }) => {
|
|
6556
|
+
const from = (0, import_node_path12.resolve)(path);
|
|
6557
|
+
const bundle = await store.list(from);
|
|
6558
|
+
if (list) {
|
|
6559
|
+
return { mode: "list", candidates: promoteCandidates(bundle) };
|
|
6560
|
+
}
|
|
6561
|
+
const target = (0, import_node_path12.resolve)(to);
|
|
6562
|
+
if (target === from) throw new KbPromoteSelfError(target);
|
|
6563
|
+
const named = (conceptIds ?? []).map(namedRecord);
|
|
6564
|
+
const wanted = named.map(({ conceptId: conceptId2, type, slug }) => {
|
|
6565
|
+
const record = bundle.find((entry) => entry.conceptId === conceptId2);
|
|
6566
|
+
if (!record) throw new KbRecordNotFoundError(conceptId2);
|
|
6567
|
+
return { record, type, slug };
|
|
6568
|
+
});
|
|
6569
|
+
await assertBaseNotFrozen(process.cwd(), from);
|
|
6570
|
+
await assertBaseNotFrozen(process.cwd(), target);
|
|
6571
|
+
const standing = standings(bundle);
|
|
6572
|
+
for (const { record } of wanted) {
|
|
6573
|
+
const where = standing.get(record.conceptId);
|
|
6574
|
+
if (isWithdrawn(where)) {
|
|
6575
|
+
throw new KbPromoteStandingError(record.conceptId, where);
|
|
6576
|
+
}
|
|
6577
|
+
if (!force && await store.read(target, record.conceptId)) {
|
|
6578
|
+
throw new KbPromoteCollisionError(record.conceptId, target);
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
const promotedIds = new Set(wanted.map(({ record }) => record.conceptId));
|
|
6582
|
+
const promoted = [];
|
|
6583
|
+
for (const { record, type, slug } of wanted) {
|
|
6584
|
+
const { frontmatter, body, droppedLinks } = carry(
|
|
6585
|
+
record,
|
|
6586
|
+
promotedIds,
|
|
6587
|
+
source
|
|
6588
|
+
);
|
|
6589
|
+
try {
|
|
6590
|
+
await store.write(
|
|
6591
|
+
target,
|
|
6592
|
+
{ type, slug, frontmatter, body, overwrite: force === true },
|
|
6593
|
+
actor
|
|
6594
|
+
);
|
|
6595
|
+
} catch (error) {
|
|
6596
|
+
throw new KbPromoteStoppedError(
|
|
6597
|
+
record.conceptId,
|
|
6598
|
+
promoted.map((entry) => entry.conceptId),
|
|
6599
|
+
error instanceof Error ? error.message : "unknown"
|
|
6600
|
+
);
|
|
6601
|
+
}
|
|
6602
|
+
await store.note(target, {
|
|
6603
|
+
by: actor,
|
|
6604
|
+
operation: "promote-in",
|
|
6605
|
+
conceptId: record.conceptId,
|
|
6606
|
+
target: from
|
|
6607
|
+
});
|
|
6608
|
+
await store.note(from, {
|
|
6609
|
+
by: actor,
|
|
6610
|
+
operation: "promote-out",
|
|
6611
|
+
conceptId: record.conceptId,
|
|
6612
|
+
target
|
|
6613
|
+
});
|
|
6614
|
+
promoted.push({ conceptId: record.conceptId, droppedLinks });
|
|
6615
|
+
}
|
|
6616
|
+
return { mode: "promote", to: target, promoted };
|
|
6617
|
+
},
|
|
6618
|
+
render: (result) => renderPromote(result)
|
|
6619
|
+
});
|
|
6620
|
+
function namedRecord(conceptId2) {
|
|
6621
|
+
const at2 = conceptId2.indexOf(".");
|
|
6622
|
+
const type = at2 === -1 ? conceptId2 : conceptId2.slice(0, at2);
|
|
6623
|
+
const slug = at2 === -1 ? "" : conceptId2.slice(at2 + 1);
|
|
6624
|
+
if (!KB_SLUG_PATTERN.test(type) || !KB_SLUG_PATTERN.test(slug)) {
|
|
6625
|
+
throw new KbInvalidConceptIdError(
|
|
6626
|
+
"concept id must be <type>.<slug>, both kebab-case",
|
|
6627
|
+
{ conceptId: conceptId2 }
|
|
6628
|
+
);
|
|
6629
|
+
}
|
|
6630
|
+
return { conceptId: conceptId2, type, slug };
|
|
6631
|
+
}
|
|
6632
|
+
function renderPromote(result) {
|
|
6633
|
+
if (result.mode === "list") {
|
|
6634
|
+
if (!result.candidates.length) return "No promotion candidates.";
|
|
6635
|
+
return result.candidates.flatMap((candidate) => [
|
|
6636
|
+
`${candidate.conceptId} [${candidate.type}]${candidate.title ? ` \u2014 ${candidate.title}` : ""}`,
|
|
6637
|
+
` ${candidate.why}`
|
|
6638
|
+
]).join("\n");
|
|
6639
|
+
}
|
|
6640
|
+
const lines = [
|
|
6641
|
+
`Promoted ${result.promoted.length} record${result.promoted.length === 1 ? "" : "s"} into ${result.to}.`
|
|
6642
|
+
];
|
|
6643
|
+
for (const entry of result.promoted) {
|
|
6644
|
+
lines.push(`- ${entry.conceptId}`);
|
|
6645
|
+
for (const link2 of entry.droppedLinks) {
|
|
6646
|
+
lines.push(
|
|
6647
|
+
` dropped ${link2.rel} \u2192 ${link2.target} (not promoted in this run)`
|
|
6648
|
+
);
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
return lines.join("\n");
|
|
6652
|
+
}
|
|
6653
|
+
|
|
4875
6654
|
// src/commands/query.ts
|
|
4876
|
-
var
|
|
6655
|
+
var import_zod27 = require("zod");
|
|
4877
6656
|
var queryCommand = define({
|
|
4878
6657
|
name: "query",
|
|
4879
6658
|
tool: "kb_query",
|
|
4880
|
-
usage: "query <text...> [--repo-root PATH]",
|
|
6659
|
+
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
4881
6660
|
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.",
|
|
4882
|
-
input:
|
|
6661
|
+
input: import_zod27.z.object({
|
|
4883
6662
|
bundlePath,
|
|
4884
|
-
text:
|
|
4885
|
-
type:
|
|
4886
|
-
includeNonCurrent:
|
|
6663
|
+
text: import_zod27.z.string().optional(),
|
|
6664
|
+
type: import_zod27.z.enum(KB_RECORD_TYPES).optional(),
|
|
6665
|
+
includeNonCurrent: import_zod27.z.boolean().optional(),
|
|
6666
|
+
tags: TAGS,
|
|
4887
6667
|
repoRoot: REPO_ROOT
|
|
4888
6668
|
}),
|
|
4889
|
-
//
|
|
6669
|
+
// Both are flags, so neither's value may fall into the search text.
|
|
4890
6670
|
fromArgv: (argv, path) => {
|
|
4891
6671
|
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4892
|
-
const
|
|
4893
|
-
const
|
|
4894
|
-
if (flag !== -1) words.splice(flag, 2);
|
|
6672
|
+
const tags = argvFlags(argv, "--tag");
|
|
6673
|
+
const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
|
|
4895
6674
|
return {
|
|
4896
6675
|
bundlePath: path,
|
|
4897
6676
|
text: words.join(" ").trim(),
|
|
4898
6677
|
includeNonCurrent: true,
|
|
6678
|
+
...tags.length ? { tags } : {},
|
|
4899
6679
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4900
6680
|
};
|
|
4901
6681
|
},
|
|
4902
|
-
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
6682
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
|
|
4903
6683
|
...type ? { type } : {},
|
|
4904
6684
|
includeNonCurrent: includeNonCurrent === true,
|
|
6685
|
+
...tags ? { tags } : {},
|
|
4905
6686
|
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
4906
6687
|
})).map((hit) => ({
|
|
4907
6688
|
conceptId: hit.record.conceptId,
|
|
@@ -4915,27 +6696,27 @@ var queryCommand = define({
|
|
|
4915
6696
|
});
|
|
4916
6697
|
|
|
4917
6698
|
// src/commands/read-index.ts
|
|
4918
|
-
var
|
|
6699
|
+
var import_zod28 = require("zod");
|
|
4919
6700
|
var readIndexCommand = define({
|
|
4920
6701
|
name: "index",
|
|
4921
6702
|
tool: "kb_index",
|
|
4922
6703
|
usage: "index",
|
|
4923
6704
|
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.",
|
|
4924
|
-
input:
|
|
6705
|
+
input: import_zod28.z.object({ bundlePath }),
|
|
4925
6706
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4926
6707
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4927
6708
|
});
|
|
4928
6709
|
|
|
4929
6710
|
// src/commands/schema.ts
|
|
4930
|
-
var
|
|
6711
|
+
var import_zod31 = require("zod");
|
|
4931
6712
|
|
|
4932
6713
|
// src/json-schema.ts
|
|
4933
|
-
var
|
|
6714
|
+
var import_zod30 = require("zod");
|
|
4934
6715
|
|
|
4935
6716
|
// src/kb-log.ts
|
|
4936
|
-
var
|
|
6717
|
+
var import_zod29 = require("zod");
|
|
4937
6718
|
var LOG_FILE = "log.jsonl";
|
|
4938
|
-
var kbLogEntrySchema =
|
|
6719
|
+
var kbLogEntrySchema = import_zod29.z.object({
|
|
4939
6720
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
4940
6721
|
// below), and a value that isn't actually chronological — a Unix
|
|
4941
6722
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -4944,23 +6725,32 @@ var kbLogEntrySchema = import_zod24.z.object({
|
|
|
4944
6725
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
4945
6726
|
// malformed `at` is reported the same way a malformed line already is,
|
|
4946
6727
|
// rather than silently sorting into the wrong place.
|
|
4947
|
-
at:
|
|
4948
|
-
by:
|
|
4949
|
-
operation:
|
|
4950
|
-
conceptId:
|
|
4951
|
-
/**
|
|
4952
|
-
|
|
6728
|
+
at: import_zod29.z.iso.datetime(),
|
|
6729
|
+
by: import_zod29.z.string().min(1),
|
|
6730
|
+
operation: import_zod29.z.string().min(1),
|
|
6731
|
+
conceptId: import_zod29.z.string().min(1),
|
|
6732
|
+
/**
|
|
6733
|
+
* The operation's other end, where it has one: a second concept id for
|
|
6734
|
+
* supersession, the other base's path for promotion.
|
|
6735
|
+
*/
|
|
6736
|
+
target: import_zod29.z.string().min(1).optional()
|
|
4953
6737
|
}).strict();
|
|
4954
6738
|
function renderLogEntry(entry) {
|
|
4955
6739
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
4956
6740
|
`;
|
|
4957
6741
|
}
|
|
6742
|
+
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
4958
6743
|
function parseLog(raw) {
|
|
4959
6744
|
const entries = [];
|
|
4960
6745
|
const malformed = [];
|
|
4961
6746
|
const seen = /* @__PURE__ */ new Set();
|
|
6747
|
+
let conflicted = false;
|
|
4962
6748
|
raw.split("\n").forEach((text, index2) => {
|
|
4963
6749
|
if (!text.trim()) return;
|
|
6750
|
+
if (CONFLICT_MARKER.test(text)) {
|
|
6751
|
+
conflicted = true;
|
|
6752
|
+
return;
|
|
6753
|
+
}
|
|
4964
6754
|
let value;
|
|
4965
6755
|
try {
|
|
4966
6756
|
value = JSON.parse(text);
|
|
@@ -4973,25 +6763,25 @@ function parseLog(raw) {
|
|
|
4973
6763
|
malformed.push({ line: index2 + 1, text });
|
|
4974
6764
|
return;
|
|
4975
6765
|
}
|
|
4976
|
-
const
|
|
4977
|
-
if (seen.has(
|
|
4978
|
-
seen.add(
|
|
6766
|
+
const key2 = JSON.stringify(parsed.data);
|
|
6767
|
+
if (seen.has(key2)) return;
|
|
6768
|
+
seen.add(key2);
|
|
4979
6769
|
entries.push(parsed.data);
|
|
4980
6770
|
});
|
|
4981
6771
|
entries.sort(
|
|
4982
6772
|
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
|
|
4983
6773
|
);
|
|
4984
|
-
return { entries, malformed };
|
|
6774
|
+
return { entries, malformed, conflicted };
|
|
4985
6775
|
}
|
|
4986
6776
|
|
|
4987
6777
|
// src/json-schema.ts
|
|
4988
6778
|
function kbJsonSchemas() {
|
|
4989
6779
|
return {
|
|
4990
|
-
recordFrontmatter:
|
|
6780
|
+
recordFrontmatter: import_zod30.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
4991
6781
|
io: "input"
|
|
4992
6782
|
}),
|
|
4993
|
-
composeInput:
|
|
4994
|
-
logEntry:
|
|
6783
|
+
composeInput: import_zod30.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
6784
|
+
logEntry: import_zod30.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
4995
6785
|
};
|
|
4996
6786
|
}
|
|
4997
6787
|
|
|
@@ -5001,25 +6791,25 @@ var schemaCommand = define({
|
|
|
5001
6791
|
tool: "kb_schema",
|
|
5002
6792
|
usage: "schema",
|
|
5003
6793
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
5004
|
-
input:
|
|
6794
|
+
input: import_zod31.z.object({}),
|
|
5005
6795
|
fromArgv: () => ({}),
|
|
5006
6796
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
5007
6797
|
});
|
|
5008
6798
|
|
|
5009
6799
|
// src/commands/stamp.ts
|
|
5010
|
-
var
|
|
5011
|
-
var
|
|
6800
|
+
var import_promises10 = require("fs/promises");
|
|
6801
|
+
var import_zod32 = require("zod");
|
|
5012
6802
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
5013
6803
|
var stampCommand = define({
|
|
5014
6804
|
name: "stamp",
|
|
5015
6805
|
tool: "kb_stamp",
|
|
5016
6806
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
5017
6807
|
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.",
|
|
5018
|
-
input:
|
|
5019
|
-
bundlePath:
|
|
6808
|
+
input: import_zod32.z.object({
|
|
6809
|
+
bundlePath: import_zod32.z.string().min(1).optional().describe(
|
|
5020
6810
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5021
6811
|
),
|
|
5022
|
-
since:
|
|
6812
|
+
since: import_zod32.z.string().min(1).optional().describe(
|
|
5023
6813
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5024
6814
|
)
|
|
5025
6815
|
}),
|
|
@@ -5081,7 +6871,7 @@ async function readBaseline(since) {
|
|
|
5081
6871
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5082
6872
|
let parsed;
|
|
5083
6873
|
try {
|
|
5084
|
-
parsed = JSON.parse(await (0,
|
|
6874
|
+
parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
|
|
5085
6875
|
} catch {
|
|
5086
6876
|
throw new KbStampBaselineError(since);
|
|
5087
6877
|
}
|
|
@@ -5105,16 +6895,16 @@ async function readBaseline(since) {
|
|
|
5105
6895
|
}
|
|
5106
6896
|
|
|
5107
6897
|
// src/commands/status.ts
|
|
5108
|
-
var
|
|
6898
|
+
var import_zod33 = require("zod");
|
|
5109
6899
|
var statusCommand = define({
|
|
5110
6900
|
name: "status",
|
|
5111
6901
|
tool: "kb_status",
|
|
5112
6902
|
usage: "status <concept-id> <status>",
|
|
5113
6903
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
5114
|
-
input:
|
|
6904
|
+
input: import_zod33.z.object({
|
|
5115
6905
|
bundlePath,
|
|
5116
6906
|
conceptId,
|
|
5117
|
-
status:
|
|
6907
|
+
status: import_zod33.z.enum(KB_RECORD_STATUSES)
|
|
5118
6908
|
}),
|
|
5119
6909
|
fromArgv: (argv, path) => ({
|
|
5120
6910
|
bundlePath: path,
|
|
@@ -5129,13 +6919,13 @@ var statusCommand = define({
|
|
|
5129
6919
|
});
|
|
5130
6920
|
|
|
5131
6921
|
// src/commands/supersede.ts
|
|
5132
|
-
var
|
|
6922
|
+
var import_zod34 = require("zod");
|
|
5133
6923
|
var supersedeCommand = define({
|
|
5134
6924
|
name: "supersede",
|
|
5135
6925
|
tool: "kb_supersede",
|
|
5136
6926
|
usage: "supersede <concept-id> <replacement-id>",
|
|
5137
6927
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
5138
|
-
input:
|
|
6928
|
+
input: import_zod34.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
5139
6929
|
fromArgv: (argv, path) => ({
|
|
5140
6930
|
bundlePath: path,
|
|
5141
6931
|
conceptId: argv[1],
|
|
@@ -5148,17 +6938,153 @@ var supersedeCommand = define({
|
|
|
5148
6938
|
}
|
|
5149
6939
|
});
|
|
5150
6940
|
|
|
6941
|
+
// src/commands/sweep.ts
|
|
6942
|
+
var import_zod35 = require("zod");
|
|
6943
|
+
var TERMINAL = [
|
|
6944
|
+
"resolved",
|
|
6945
|
+
"rejected",
|
|
6946
|
+
"superseded"
|
|
6947
|
+
];
|
|
6948
|
+
var sweepCommand = define({
|
|
6949
|
+
name: "sweep",
|
|
6950
|
+
tool: "kb_sweep",
|
|
6951
|
+
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
6952
|
+
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.",
|
|
6953
|
+
input: import_zod35.z.object({
|
|
6954
|
+
bundlePath,
|
|
6955
|
+
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."),
|
|
6956
|
+
terminal: import_zod35.z.literal(true, {
|
|
6957
|
+
error: "sweep needs --terminal: it deletes only settled records"
|
|
6958
|
+
}).describe(
|
|
6959
|
+
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
6960
|
+
),
|
|
6961
|
+
dryRun: import_zod35.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
6962
|
+
}),
|
|
6963
|
+
fromArgv: (argv, path) => ({
|
|
6964
|
+
bundlePath: path,
|
|
6965
|
+
tag: argvFlag(argv, "--tag"),
|
|
6966
|
+
...argv.includes("--terminal") ? { terminal: true } : {},
|
|
6967
|
+
...argv.includes("--dry-run") ? { dryRun: true } : {}
|
|
6968
|
+
}),
|
|
6969
|
+
run: async ({ store, actor }, { bundlePath: path, tag, dryRun }) => {
|
|
6970
|
+
const bundle = await store.list(path);
|
|
6971
|
+
const held = holderIndex(bundle);
|
|
6972
|
+
const candidates = adjudicate(bundle, bundle).filter(
|
|
6973
|
+
(hit) => sweepable(hit, tag)
|
|
6974
|
+
);
|
|
6975
|
+
const doomed = new Set(candidates.map((hit) => hit.record.conceptId));
|
|
6976
|
+
let changed = true;
|
|
6977
|
+
while (changed) {
|
|
6978
|
+
changed = false;
|
|
6979
|
+
for (const conceptId2 of [...doomed]) {
|
|
6980
|
+
if (survivorsHolding(conceptId2, held, doomed).length === 0) continue;
|
|
6981
|
+
doomed.delete(conceptId2);
|
|
6982
|
+
changed = true;
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
const skipped = candidates.filter((hit) => !doomed.has(hit.record.conceptId)).map((hit) => ({
|
|
6986
|
+
conceptId: hit.record.conceptId,
|
|
6987
|
+
heldBy: survivorsHolding(hit.record.conceptId, held, doomed)
|
|
6988
|
+
}));
|
|
6989
|
+
const ordered = [...doomed].sort();
|
|
6990
|
+
if (dryRun) {
|
|
6991
|
+
return {
|
|
6992
|
+
tag,
|
|
6993
|
+
dryRun: true,
|
|
6994
|
+
deleted: [],
|
|
6995
|
+
candidates: ordered,
|
|
6996
|
+
skipped,
|
|
6997
|
+
failed: []
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
7000
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
7001
|
+
const deleted = [];
|
|
7002
|
+
const failed = [];
|
|
7003
|
+
try {
|
|
7004
|
+
for (const conceptId2 of ordered) {
|
|
7005
|
+
try {
|
|
7006
|
+
const outcome = await store.deleteRecord(
|
|
7007
|
+
path,
|
|
7008
|
+
conceptId2,
|
|
7009
|
+
{ tag, statuses: TERMINAL },
|
|
7010
|
+
actor
|
|
7011
|
+
);
|
|
7012
|
+
if (outcome === "deleted") deleted.push(conceptId2);
|
|
7013
|
+
else failed.push({ conceptId: conceptId2, reason: outcome });
|
|
7014
|
+
} catch (error) {
|
|
7015
|
+
failed.push({
|
|
7016
|
+
conceptId: conceptId2,
|
|
7017
|
+
reason: error instanceof Error ? error.message : "unknown"
|
|
7018
|
+
});
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
7021
|
+
} finally {
|
|
7022
|
+
await store.readIndex(path);
|
|
7023
|
+
await store.dropSearchIndex(path);
|
|
7024
|
+
}
|
|
7025
|
+
return {
|
|
7026
|
+
tag,
|
|
7027
|
+
dryRun: false,
|
|
7028
|
+
deleted,
|
|
7029
|
+
candidates: ordered,
|
|
7030
|
+
skipped,
|
|
7031
|
+
failed
|
|
7032
|
+
};
|
|
7033
|
+
},
|
|
7034
|
+
render: (result) => renderSweep(result)
|
|
7035
|
+
});
|
|
7036
|
+
function sweepable(hit, tag) {
|
|
7037
|
+
const { tags, strauss_status } = hit.record.frontmatter;
|
|
7038
|
+
return (tags ?? []).includes(tag) && // Supersession is a standing, settled against the whole base; the other
|
|
7039
|
+
// two are the record's own word for itself.
|
|
7040
|
+
(hit.standing === "superseded" || strauss_status === "resolved" || strauss_status === "rejected");
|
|
7041
|
+
}
|
|
7042
|
+
function holderIndex(bundle) {
|
|
7043
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
7044
|
+
const hold = (target, from) => {
|
|
7045
|
+
if (target === from) return;
|
|
7046
|
+
const holders = byTarget.get(target) ?? /* @__PURE__ */ new Set();
|
|
7047
|
+
holders.add(from);
|
|
7048
|
+
byTarget.set(target, holders);
|
|
7049
|
+
};
|
|
7050
|
+
for (const [target, edges] of inboundIndex(bundle)) {
|
|
7051
|
+
for (const edge of edges) hold(target, edge.from);
|
|
7052
|
+
}
|
|
7053
|
+
for (const record of bundle) {
|
|
7054
|
+
const { strauss_supersedes, strauss_superseded_by } = record.frontmatter;
|
|
7055
|
+
for (const old of strauss_supersedes ?? []) hold(old, record.conceptId);
|
|
7056
|
+
if (strauss_superseded_by) hold(strauss_superseded_by, record.conceptId);
|
|
7057
|
+
}
|
|
7058
|
+
return byTarget;
|
|
7059
|
+
}
|
|
7060
|
+
function survivorsHolding(conceptId2, held, doomed) {
|
|
7061
|
+
return [...held.get(conceptId2) ?? []].filter((from) => !doomed.has(from)).sort();
|
|
7062
|
+
}
|
|
7063
|
+
function renderSweep(result) {
|
|
7064
|
+
const shown = result.dryRun ? result.candidates : result.deleted;
|
|
7065
|
+
const verb = result.dryRun ? "would delete" : "deleted";
|
|
7066
|
+
const lines = [`${verb} ${shown.length} (tag: ${result.tag})`];
|
|
7067
|
+
for (const conceptId2 of shown) lines.push(`- ${conceptId2}`);
|
|
7068
|
+
for (const skip of result.skipped) {
|
|
7069
|
+
lines.push(`kept ${skip.conceptId} \u2014 held by ${skip.heldBy.join(", ")}`);
|
|
7070
|
+
}
|
|
7071
|
+
for (const failure of result.failed) {
|
|
7072
|
+
lines.push(`failed ${failure.conceptId} \u2014 ${failure.reason}`);
|
|
7073
|
+
}
|
|
7074
|
+
return lines.join("\n");
|
|
7075
|
+
}
|
|
7076
|
+
|
|
5151
7077
|
// src/commands/sync-instructions.ts
|
|
5152
|
-
var
|
|
7078
|
+
var import_zod36 = require("zod");
|
|
5153
7079
|
var syncInstructionsCommand = define({
|
|
5154
7080
|
name: "sync-instructions",
|
|
5155
7081
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
5156
7082
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
5157
|
-
input:
|
|
5158
|
-
file:
|
|
5159
|
-
budgetTokens:
|
|
5160
|
-
fullUnderTokens:
|
|
5161
|
-
profile:
|
|
7083
|
+
input: import_zod36.z.object({
|
|
7084
|
+
file: import_zod36.z.string().min(1).describe("The instruction file to edit in place."),
|
|
7085
|
+
budgetTokens: import_zod36.z.number().int().positive().optional(),
|
|
7086
|
+
fullUnderTokens: import_zod36.z.number().int().positive().optional(),
|
|
7087
|
+
profile: import_zod36.z.string().optional()
|
|
5162
7088
|
}),
|
|
5163
7089
|
fromArgv: (argv) => {
|
|
5164
7090
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5184,7 +7110,7 @@ var syncInstructionsCommand = define({
|
|
|
5184
7110
|
});
|
|
5185
7111
|
|
|
5186
7112
|
// src/commands/trace.ts
|
|
5187
|
-
var
|
|
7113
|
+
var import_zod37 = require("zod");
|
|
5188
7114
|
|
|
5189
7115
|
// src/trace.ts
|
|
5190
7116
|
var TRACE_EDGES = [
|
|
@@ -5240,11 +7166,11 @@ var traceCommand = define({
|
|
|
5240
7166
|
tool: "kb_trace",
|
|
5241
7167
|
usage: "trace <concept-id> [edges...]",
|
|
5242
7168
|
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".',
|
|
5243
|
-
input:
|
|
7169
|
+
input: import_zod37.z.object({
|
|
5244
7170
|
bundlePath,
|
|
5245
7171
|
conceptId,
|
|
5246
|
-
edges:
|
|
5247
|
-
depth:
|
|
7172
|
+
edges: import_zod37.z.array(import_zod37.z.enum(TRACE_EDGES)).optional(),
|
|
7173
|
+
depth: import_zod37.z.number().int().positive().optional()
|
|
5248
7174
|
}),
|
|
5249
7175
|
fromArgv: (argv, path) => ({
|
|
5250
7176
|
bundlePath: path,
|
|
@@ -5266,37 +7192,37 @@ var traceCommand = define({
|
|
|
5266
7192
|
});
|
|
5267
7193
|
|
|
5268
7194
|
// src/commands/types.ts
|
|
5269
|
-
var
|
|
7195
|
+
var import_zod38 = require("zod");
|
|
5270
7196
|
var typesCommand = define({
|
|
5271
7197
|
name: "types",
|
|
5272
7198
|
tool: "kb_types",
|
|
5273
7199
|
usage: "types",
|
|
5274
7200
|
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.",
|
|
5275
|
-
input:
|
|
7201
|
+
input: import_zod38.z.object({}),
|
|
5276
7202
|
fromArgv: () => ({}),
|
|
5277
7203
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5278
7204
|
});
|
|
5279
7205
|
|
|
5280
7206
|
// src/commands/unpin.ts
|
|
5281
|
-
var
|
|
7207
|
+
var import_zod39 = require("zod");
|
|
5282
7208
|
var unpinCommand = define({
|
|
5283
7209
|
name: "unpin",
|
|
5284
7210
|
tool: "kb_unpin",
|
|
5285
7211
|
usage: "unpin [bundle-path]",
|
|
5286
7212
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5287
|
-
input:
|
|
7213
|
+
input: import_zod39.z.object({ bundlePath }),
|
|
5288
7214
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5289
7215
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5290
7216
|
});
|
|
5291
7217
|
|
|
5292
7218
|
// src/commands/validate.ts
|
|
5293
|
-
var
|
|
7219
|
+
var import_zod40 = require("zod");
|
|
5294
7220
|
var validateCommand = define({
|
|
5295
7221
|
name: "validate",
|
|
5296
7222
|
tool: "kb_validate",
|
|
5297
7223
|
usage: "validate",
|
|
5298
7224
|
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.",
|
|
5299
|
-
input:
|
|
7225
|
+
input: import_zod40.z.object({ bundlePath }),
|
|
5300
7226
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5301
7227
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5302
7228
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5306,16 +7232,16 @@ var validateCommand = define({
|
|
|
5306
7232
|
});
|
|
5307
7233
|
|
|
5308
7234
|
// src/commands/verify.ts
|
|
5309
|
-
var
|
|
7235
|
+
var import_zod41 = require("zod");
|
|
5310
7236
|
var verifyCommand = define({
|
|
5311
7237
|
name: "verify",
|
|
5312
7238
|
tool: "kb_verify",
|
|
5313
7239
|
usage: "verify <concept-id> --note <text>",
|
|
5314
7240
|
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.",
|
|
5315
|
-
input:
|
|
7241
|
+
input: import_zod41.z.object({
|
|
5316
7242
|
bundlePath,
|
|
5317
7243
|
conceptId,
|
|
5318
|
-
note:
|
|
7244
|
+
note: import_zod41.z.string().refine((s) => s.trim().length > 0, {
|
|
5319
7245
|
message: "note must say what the check found"
|
|
5320
7246
|
})
|
|
5321
7247
|
}),
|
|
@@ -5335,15 +7261,15 @@ var verifyCommand = define({
|
|
|
5335
7261
|
});
|
|
5336
7262
|
|
|
5337
7263
|
// src/commands/write.ts
|
|
5338
|
-
var
|
|
7264
|
+
var import_zod42 = require("zod");
|
|
5339
7265
|
var writeCommand = define({
|
|
5340
7266
|
name: "write",
|
|
5341
7267
|
tool: "kb_write",
|
|
5342
7268
|
usage: "write <type> < record.json",
|
|
5343
7269
|
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.",
|
|
5344
|
-
input:
|
|
7270
|
+
input: import_zod42.z.object({
|
|
5345
7271
|
bundlePath,
|
|
5346
|
-
type:
|
|
7272
|
+
type: import_zod42.z.enum(KB_RECORD_TYPES),
|
|
5347
7273
|
input: composeInputSchema
|
|
5348
7274
|
}),
|
|
5349
7275
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5367,13 +7293,13 @@ var writeCommand = define({
|
|
|
5367
7293
|
});
|
|
5368
7294
|
|
|
5369
7295
|
// src/commands/write-decision.ts
|
|
5370
|
-
var
|
|
7296
|
+
var import_zod43 = require("zod");
|
|
5371
7297
|
var writeDecisionCommand = define({
|
|
5372
7298
|
name: "write-decision",
|
|
5373
7299
|
tool: "kb_write_decision",
|
|
5374
7300
|
usage: "write-decision < decision.json",
|
|
5375
7301
|
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.",
|
|
5376
|
-
input:
|
|
7302
|
+
input: import_zod43.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5377
7303
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5378
7304
|
bundlePath: path,
|
|
5379
7305
|
input: JSON.parse(await stdin())
|
|
@@ -5404,19 +7330,24 @@ var KB_COMMANDS = [
|
|
|
5404
7330
|
verifyCommand,
|
|
5405
7331
|
anchorResolveCommand,
|
|
5406
7332
|
reassessCommand,
|
|
7333
|
+
promoteCommand,
|
|
5407
7334
|
loadCommand,
|
|
5408
7335
|
catalogCommand,
|
|
5409
7336
|
packCommand,
|
|
7337
|
+
exportCommand,
|
|
5410
7338
|
queryCommand,
|
|
5411
7339
|
traceCommand,
|
|
5412
7340
|
impactCommand,
|
|
5413
7341
|
backlinksCommand,
|
|
7342
|
+
matchCommand,
|
|
7343
|
+
classifyCommand,
|
|
5414
7344
|
listCommand,
|
|
5415
7345
|
readIndexCommand,
|
|
5416
7346
|
logCommand,
|
|
5417
7347
|
stampCommand,
|
|
5418
7348
|
validateCommand,
|
|
5419
7349
|
doctorCommand,
|
|
7350
|
+
sweepCommand,
|
|
5420
7351
|
schemaCommand,
|
|
5421
7352
|
pinCommand,
|
|
5422
7353
|
unpinCommand,
|
|
@@ -5430,8 +7361,8 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
5430
7361
|
);
|
|
5431
7362
|
|
|
5432
7363
|
// src/kb-store.ts
|
|
5433
|
-
var
|
|
5434
|
-
var
|
|
7364
|
+
var import_promises12 = require("fs/promises");
|
|
7365
|
+
var import_node_path14 = require("path");
|
|
5435
7366
|
|
|
5436
7367
|
// src/markdown.ts
|
|
5437
7368
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -5491,8 +7422,8 @@ function bundleDigest(records, superseded) {
|
|
|
5491
7422
|
}
|
|
5492
7423
|
|
|
5493
7424
|
// src/search-index.ts
|
|
5494
|
-
var
|
|
5495
|
-
var
|
|
7425
|
+
var import_promises11 = require("fs/promises");
|
|
7426
|
+
var import_node_path13 = require("path");
|
|
5496
7427
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
5497
7428
|
var COLLECTION = "kb";
|
|
5498
7429
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -5501,7 +7432,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
5501
7432
|
let store = null;
|
|
5502
7433
|
try {
|
|
5503
7434
|
store = await qmd.createStore({
|
|
5504
|
-
dbPath: (0,
|
|
7435
|
+
dbPath: (0, import_node_path13.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
5505
7436
|
config: {
|
|
5506
7437
|
collections: {
|
|
5507
7438
|
[COLLECTION]: {
|
|
@@ -5536,16 +7467,16 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
5536
7467
|
}
|
|
5537
7468
|
}
|
|
5538
7469
|
async function isStale(bundlePath2) {
|
|
5539
|
-
const indexAt = await (0,
|
|
7470
|
+
const indexAt = await (0, import_promises11.stat)((0, import_node_path13.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5540
7471
|
if (!indexAt) return true;
|
|
5541
|
-
const { readdir:
|
|
5542
|
-
const names = (await
|
|
7472
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
7473
|
+
const names = (await readdir3(bundlePath2).catch(() => [])).filter(
|
|
5543
7474
|
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
5544
7475
|
);
|
|
5545
7476
|
let stale = false;
|
|
5546
7477
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
5547
7478
|
if (stale) return;
|
|
5548
|
-
const at2 = await (0,
|
|
7479
|
+
const at2 = await (0, import_promises11.stat)((0, import_node_path13.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5549
7480
|
if (at2 > indexAt) stale = true;
|
|
5550
7481
|
});
|
|
5551
7482
|
return stale;
|
|
@@ -5663,144 +7594,13 @@ function typeRank(record) {
|
|
|
5663
7594
|
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
5664
7595
|
}
|
|
5665
7596
|
|
|
5666
|
-
// src/kb-
|
|
5667
|
-
|
|
5668
|
-
const byTarget = /* @__PURE__ */ new Map();
|
|
5669
|
-
for (const record of bundle) {
|
|
5670
|
-
for (const link2 of record.frontmatter.strauss_links ?? []) {
|
|
5671
|
-
if (link2.target === record.conceptId) continue;
|
|
5672
|
-
const edges = byTarget.get(link2.target) ?? [];
|
|
5673
|
-
if (edges.some(
|
|
5674
|
-
(edge) => edge.from === record.conceptId && edge.rel === link2.rel
|
|
5675
|
-
)) {
|
|
5676
|
-
continue;
|
|
5677
|
-
}
|
|
5678
|
-
edges.push({ from: record.conceptId, rel: link2.rel });
|
|
5679
|
-
byTarget.set(link2.target, edges);
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
return byTarget;
|
|
5683
|
-
}
|
|
5684
|
-
|
|
5685
|
-
// src/kb-links/backlinks.ts
|
|
5686
|
-
function backlinks(targetId, bundle) {
|
|
5687
|
-
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
5688
|
-
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
5689
|
-
const standingOf = new Map(
|
|
5690
|
-
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
5691
|
-
);
|
|
5692
|
-
const rows = [];
|
|
5693
|
-
for (const edge of inboundIndex(bundle).get(targetId) ?? []) {
|
|
5694
|
-
const record = byId.get(edge.from);
|
|
5695
|
-
if (!record) continue;
|
|
5696
|
-
const hit = standingOf.get(edge.from);
|
|
5697
|
-
rows.push({
|
|
5698
|
-
...edge,
|
|
5699
|
-
title: record.frontmatter.title ?? null,
|
|
5700
|
-
standing: hit?.standing ?? "unsettled",
|
|
5701
|
-
warnings: hit?.warnings ?? []
|
|
5702
|
-
});
|
|
5703
|
-
}
|
|
5704
|
-
return {
|
|
5705
|
-
target: targetId,
|
|
5706
|
-
backlinks: rows.sort(
|
|
5707
|
-
(left, right) => left.from.localeCompare(right.from) || left.rel.localeCompare(right.rel)
|
|
5708
|
-
)
|
|
5709
|
-
};
|
|
5710
|
-
}
|
|
5711
|
-
|
|
5712
|
-
// src/kb-links/impact.ts
|
|
5713
|
-
function impact(targetId, bundle, options = {}) {
|
|
5714
|
-
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
5715
|
-
if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
|
|
5716
|
-
const rels = resolveRels(options.rels);
|
|
5717
|
-
const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
|
|
5718
|
-
const inbound = inboundIndex(bundle);
|
|
5719
|
-
const standingOf = new Map(
|
|
5720
|
-
adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
|
|
5721
|
-
);
|
|
5722
|
-
const reached = /* @__PURE__ */ new Map();
|
|
5723
|
-
const stopped = [];
|
|
5724
|
-
let frontier = [targetId];
|
|
5725
|
-
let depth = 0;
|
|
5726
|
-
while (frontier.length && depth < maxDepth) {
|
|
5727
|
-
depth += 1;
|
|
5728
|
-
const next = [];
|
|
5729
|
-
const consider = (dependantId, edge) => {
|
|
5730
|
-
if (dependantId === targetId) return;
|
|
5731
|
-
const existing = reached.get(dependantId);
|
|
5732
|
-
if (existing) {
|
|
5733
|
-
if (!hasEdge(existing.via, edge)) existing.via.push(edge);
|
|
5734
|
-
return;
|
|
5735
|
-
}
|
|
5736
|
-
const record = byId.get(dependantId);
|
|
5737
|
-
if (!record) return;
|
|
5738
|
-
const hit = standingOf.get(dependantId);
|
|
5739
|
-
const entry = {
|
|
5740
|
-
conceptId: dependantId,
|
|
5741
|
-
title: record.frontmatter.title ?? null,
|
|
5742
|
-
standing: hit?.standing ?? "unsettled",
|
|
5743
|
-
warnings: hit?.warnings ?? [],
|
|
5744
|
-
depth,
|
|
5745
|
-
via: [edge]
|
|
5746
|
-
};
|
|
5747
|
-
reached.set(dependantId, entry);
|
|
5748
|
-
if (entry.standing === "superseded" || entry.standing === "rejected") {
|
|
5749
|
-
stopped.push(dependantId);
|
|
5750
|
-
return;
|
|
5751
|
-
}
|
|
5752
|
-
next.push(dependantId);
|
|
5753
|
-
};
|
|
5754
|
-
for (const id of frontier) {
|
|
5755
|
-
for (const edge of inbound.get(id) ?? []) {
|
|
5756
|
-
if (!rels.has(edge.rel)) continue;
|
|
5757
|
-
if (dependantEnd(edge.rel) !== "source") continue;
|
|
5758
|
-
consider(edge.from, { source: edge.from, target: id, rel: edge.rel });
|
|
5759
|
-
}
|
|
5760
|
-
for (const link2 of byId.get(id)?.frontmatter.strauss_links ?? []) {
|
|
5761
|
-
if (!rels.has(link2.rel)) continue;
|
|
5762
|
-
if (dependantEnd(link2.rel) !== "target") continue;
|
|
5763
|
-
if (link2.target === id) continue;
|
|
5764
|
-
consider(link2.target, {
|
|
5765
|
-
source: id,
|
|
5766
|
-
target: link2.target,
|
|
5767
|
-
rel: link2.rel
|
|
5768
|
-
});
|
|
5769
|
-
}
|
|
5770
|
-
}
|
|
5771
|
-
frontier = next;
|
|
5772
|
-
}
|
|
5773
|
-
return {
|
|
5774
|
-
root: targetId,
|
|
5775
|
-
impacted: [...reached.values()].sort(
|
|
5776
|
-
(left, right) => left.depth - right.depth || left.conceptId.localeCompare(right.conceptId)
|
|
5777
|
-
),
|
|
5778
|
-
stopped: stopped.sort(),
|
|
5779
|
-
truncated: frontier.length > 0,
|
|
5780
|
-
unexpanded: [...frontier].sort()
|
|
5781
|
-
};
|
|
5782
|
-
}
|
|
5783
|
-
function resolveRels(rels) {
|
|
5784
|
-
if (!rels?.length) return new Set(KB_CAUSAL_LINK_RELS);
|
|
5785
|
-
for (const rel of rels) {
|
|
5786
|
-
if (!isKbLinkRel(rel) || LINK_RELS[rel].dependant === null) {
|
|
5787
|
-
throw new KbUnknownLinkRelError(rel, KB_CAUSAL_LINK_RELS);
|
|
5788
|
-
}
|
|
5789
|
-
}
|
|
5790
|
-
return new Set(rels);
|
|
5791
|
-
}
|
|
5792
|
-
function dependantEnd(rel) {
|
|
5793
|
-
return isKbLinkRel(rel) ? LINK_RELS[rel].dependant : null;
|
|
5794
|
-
}
|
|
5795
|
-
function hasEdge(edges, edge) {
|
|
5796
|
-
return edges.some(
|
|
5797
|
-
(existing) => existing.source === edge.source && existing.target === edge.target && existing.rel === edge.rel
|
|
5798
|
-
);
|
|
5799
|
-
}
|
|
7597
|
+
// src/kb-files.ts
|
|
7598
|
+
var STORE_OWNED_FILES = [INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE];
|
|
5800
7599
|
|
|
5801
7600
|
// src/kb-gitattributes.ts
|
|
5802
7601
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
5803
|
-
var
|
|
7602
|
+
var GENERATED = "linguist-generated";
|
|
7603
|
+
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union ${GENERATED}=true`;
|
|
5804
7604
|
function parseLine(line) {
|
|
5805
7605
|
const trimmed = line.trim();
|
|
5806
7606
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
@@ -5808,23 +7608,39 @@ function parseLine(line) {
|
|
|
5808
7608
|
return pattern === void 0 ? null : { pattern, attrs };
|
|
5809
7609
|
}
|
|
5810
7610
|
function hasMergeDeclaration(contents) {
|
|
7611
|
+
return declares(contents, LOG_FILE, "merge");
|
|
7612
|
+
}
|
|
7613
|
+
function declares(contents, pattern, attribute) {
|
|
5811
7614
|
return contents.split("\n").some((line) => {
|
|
5812
7615
|
const parsed = parseLine(line);
|
|
5813
|
-
if (!parsed || parsed.pattern !==
|
|
7616
|
+
if (!parsed || parsed.pattern !== pattern) return false;
|
|
5814
7617
|
return parsed.attrs.some(
|
|
5815
|
-
(attr) => attr ===
|
|
7618
|
+
(attr) => attr === attribute || attr === `-${attribute}` || attr.startsWith(`${attribute}=`)
|
|
5816
7619
|
);
|
|
5817
7620
|
});
|
|
5818
7621
|
}
|
|
5819
|
-
function
|
|
7622
|
+
function missingGitattributesLines(contents) {
|
|
7623
|
+
const needsMerge = !hasMergeDeclaration(contents);
|
|
7624
|
+
const generated = STORE_OWNED_FILES.filter(
|
|
7625
|
+
// The union-merge line carries the log's `linguist-generated` too, so the
|
|
7626
|
+
// log needs its own line only where that line is already there without it.
|
|
7627
|
+
(file) => !(needsMerge && file === LOG_FILE)
|
|
7628
|
+
).filter((file) => !declares(contents, file, GENERATED)).map((file) => `${file} ${GENERATED}=true`);
|
|
7629
|
+
return needsMerge ? [UNION_MERGE_LINE, ...generated] : generated;
|
|
7630
|
+
}
|
|
7631
|
+
var GITATTRIBUTES_BLOCK = `${missingGitattributesLines("").join("\n")}
|
|
7632
|
+
`;
|
|
7633
|
+
function appendGitattributesLines(contents) {
|
|
7634
|
+
const lines = missingGitattributesLines(contents);
|
|
7635
|
+
if (lines.length === 0) return "";
|
|
5820
7636
|
const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
5821
|
-
return `${separator}${
|
|
7637
|
+
return `${separator}${lines.join("\n")}
|
|
5822
7638
|
`;
|
|
5823
7639
|
}
|
|
5824
7640
|
|
|
5825
7641
|
// src/kb-store.ts
|
|
5826
|
-
var KB_DIR = (0,
|
|
5827
|
-
var STORE_OWNED =
|
|
7642
|
+
var KB_DIR = (0, import_node_path14.join)(".strauss", "kb");
|
|
7643
|
+
var STORE_OWNED = new Set(STORE_OWNED_FILES);
|
|
5828
7644
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
5829
7645
|
var KbStore = class {
|
|
5830
7646
|
constructor(logger = {}) {
|
|
@@ -5854,7 +7670,7 @@ var KbStore = class {
|
|
|
5854
7670
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
5855
7671
|
const root = this.root(bundlePath2);
|
|
5856
7672
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
5857
|
-
await (0,
|
|
7673
|
+
await (0, import_promises12.mkdir)(root, { recursive: true });
|
|
5858
7674
|
await this.publish(
|
|
5859
7675
|
target,
|
|
5860
7676
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -5893,24 +7709,27 @@ var KbStore = class {
|
|
|
5893
7709
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
5894
7710
|
let raw;
|
|
5895
7711
|
try {
|
|
5896
|
-
raw = await (0,
|
|
7712
|
+
raw = await (0, import_promises12.readFile)(target, "utf8");
|
|
5897
7713
|
} catch {
|
|
5898
7714
|
return null;
|
|
5899
7715
|
}
|
|
5900
7716
|
return this.parse(conceptId2, raw);
|
|
5901
7717
|
}
|
|
5902
7718
|
/**
|
|
5903
|
-
* Every record in the bundle, optionally narrowed to one type
|
|
7719
|
+
* Every record in the bundle, optionally narrowed to one type and to the
|
|
7720
|
+
* records carrying every tag in `filter.tags`. Selection only — `excludeTags`
|
|
7721
|
+
* is not taken here, because `query`, `catalog` and `load` read through this
|
|
7722
|
+
* and must adjudicate over the whole base.
|
|
5904
7723
|
*
|
|
5905
7724
|
* A file that fails to parse is skipped and logged rather than thrown: one
|
|
5906
7725
|
* malformed record — hand-edited, or written by a producer we don't know —
|
|
5907
7726
|
* must not make the whole bundle unreadable.
|
|
5908
7727
|
*/
|
|
5909
|
-
async list(bundlePath2, type) {
|
|
7728
|
+
async list(bundlePath2, type, filter = {}) {
|
|
5910
7729
|
const root = this.root(bundlePath2);
|
|
5911
7730
|
let names;
|
|
5912
7731
|
try {
|
|
5913
|
-
names = await (0,
|
|
7732
|
+
names = await (0, import_promises12.readdir)(root);
|
|
5914
7733
|
} catch {
|
|
5915
7734
|
return [];
|
|
5916
7735
|
}
|
|
@@ -5918,9 +7737,11 @@ var KbStore = class {
|
|
|
5918
7737
|
const records = await mapLimit(
|
|
5919
7738
|
wanted,
|
|
5920
7739
|
DEFAULT_IO_CONCURRENCY,
|
|
5921
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
7740
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises12.readFile)((0, import_node_path14.join)(root, name), "utf8"))
|
|
7741
|
+
);
|
|
7742
|
+
return records.filter(
|
|
7743
|
+
(record) => record !== null && matchesTags(record, filter)
|
|
5922
7744
|
);
|
|
5923
|
-
return records.filter((record) => record !== null);
|
|
5924
7745
|
}
|
|
5925
7746
|
/**
|
|
5926
7747
|
* Moves a record's status, preserving everything else.
|
|
@@ -5946,12 +7767,16 @@ var KbStore = class {
|
|
|
5946
7767
|
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
5947
7768
|
* writing, so it holds the complete current set, and a merge would keep
|
|
5948
7769
|
* stale entries the resolution pass deliberately dropped.
|
|
7770
|
+
*
|
|
7771
|
+
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
7772
|
+
* the frontmatter must not be published back out under an actor stamp.
|
|
5949
7773
|
*/
|
|
5950
7774
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
7775
|
+
const checked = anchors.map((anchor) => kbAnchorWriteSchema.parse(anchor));
|
|
5951
7776
|
return this.mutate(
|
|
5952
7777
|
bundlePath2,
|
|
5953
7778
|
conceptId2,
|
|
5954
|
-
(frontmatter) => ({ ...frontmatter, strauss_anchors:
|
|
7779
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: checked }),
|
|
5955
7780
|
{ operation: "anchor-resolve", by: actor }
|
|
5956
7781
|
);
|
|
5957
7782
|
}
|
|
@@ -6037,6 +7862,35 @@ ${answer}
|
|
|
6037
7862
|
`
|
|
6038
7863
|
);
|
|
6039
7864
|
}
|
|
7865
|
+
/**
|
|
7866
|
+
* Removes one record, logged as `sweep`. The only path in this store that
|
|
7867
|
+
* deletes — see the specification for the scope that makes it safe.
|
|
7868
|
+
*
|
|
7869
|
+
* `expected` is re-read and re-checked immediately before the unlink, the
|
|
7870
|
+
* compare-and-swap `mutate` makes: a record retagged or moved out of a
|
|
7871
|
+
* terminal status since the caller listed it is reported, not removed.
|
|
7872
|
+
*/
|
|
7873
|
+
async deleteRecord(bundlePath2, conceptId2, expected, actor = "unknown") {
|
|
7874
|
+
const target = this.recordPath(bundlePath2, conceptId2);
|
|
7875
|
+
const witness = await this.read(bundlePath2, conceptId2);
|
|
7876
|
+
if (!witness) throw new KbRecordNotFoundError(conceptId2);
|
|
7877
|
+
const { tags, strauss_status } = witness.frontmatter;
|
|
7878
|
+
if (!(tags ?? []).includes(expected.tag) || !expected.statuses.includes(strauss_status)) {
|
|
7879
|
+
return "changed-since-listing";
|
|
7880
|
+
}
|
|
7881
|
+
try {
|
|
7882
|
+
await (0, import_promises12.unlink)(target);
|
|
7883
|
+
} catch (error) {
|
|
7884
|
+
if (error.code !== "ENOENT") throw error;
|
|
7885
|
+
throw new KbRecordNotFoundError(conceptId2);
|
|
7886
|
+
}
|
|
7887
|
+
await this.record(this.root(bundlePath2), {
|
|
7888
|
+
operation: "sweep",
|
|
7889
|
+
by: actor,
|
|
7890
|
+
conceptId: conceptId2
|
|
7891
|
+
});
|
|
7892
|
+
return "deleted";
|
|
7893
|
+
}
|
|
6040
7894
|
/**
|
|
6041
7895
|
* Records matching a text query, each carrying its standing.
|
|
6042
7896
|
*
|
|
@@ -6060,9 +7914,10 @@ ${answer}
|
|
|
6060
7914
|
/* @__PURE__ */ new Date(),
|
|
6061
7915
|
await this.detectDrift(narrowed, options.repoRoot)
|
|
6062
7916
|
);
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
7917
|
+
const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
|
|
7918
|
+
if (options.includeNonCurrent) return kept;
|
|
7919
|
+
const present = new Set(kept.map((hit) => hit.record.conceptId));
|
|
7920
|
+
return kept.filter(
|
|
6066
7921
|
(hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
|
|
6067
7922
|
);
|
|
6068
7923
|
}
|
|
@@ -6165,14 +8020,17 @@ ${answer}
|
|
|
6165
8020
|
/* @__PURE__ */ new Date(),
|
|
6166
8021
|
await this.detectDrift(wanted, options.repoRoot)
|
|
6167
8022
|
);
|
|
6168
|
-
const
|
|
6169
|
-
|
|
8023
|
+
const kept = adjudicated.filter(
|
|
8024
|
+
(hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
|
|
8025
|
+
);
|
|
8026
|
+
const records = kept.filter((hit) => hit.standing !== "superseded");
|
|
8027
|
+
const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
|
|
6170
8028
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
6171
8029
|
const bundleDigestValue = bundleDigest(records, superseded);
|
|
6172
8030
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
6173
8031
|
return {
|
|
6174
8032
|
loaded: false,
|
|
6175
|
-
recordCount:
|
|
8033
|
+
recordCount: kept.length,
|
|
6176
8034
|
approxTokens: approxTokens2,
|
|
6177
8035
|
budgetTokens,
|
|
6178
8036
|
message: refusalMessage({
|
|
@@ -6185,7 +8043,7 @@ ${answer}
|
|
|
6185
8043
|
}
|
|
6186
8044
|
return {
|
|
6187
8045
|
loaded: true,
|
|
6188
|
-
recordCount:
|
|
8046
|
+
recordCount: kept.length,
|
|
6189
8047
|
tokensLoaded: approxTokens2,
|
|
6190
8048
|
budgetTokens: options.all ? null : budgetTokens,
|
|
6191
8049
|
records,
|
|
@@ -6255,11 +8113,11 @@ ${answer}
|
|
|
6255
8113
|
async readIndex(bundlePath2) {
|
|
6256
8114
|
const root = this.root(bundlePath2);
|
|
6257
8115
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
6258
|
-
const stored = await (0,
|
|
8116
|
+
const stored = await (0, import_promises12.readFile)((0, import_node_path14.join)(root, INDEX_FILE), "utf8").catch(
|
|
6259
8117
|
() => null
|
|
6260
8118
|
);
|
|
6261
8119
|
if (indexIsStale(stored, expected)) {
|
|
6262
|
-
await this.publish((0,
|
|
8120
|
+
await this.publish((0, import_node_path14.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
6263
8121
|
this.logger.info?.({
|
|
6264
8122
|
operation: "kb.index.repair",
|
|
6265
8123
|
bundlePath: root,
|
|
@@ -6268,19 +8126,39 @@ ${answer}
|
|
|
6268
8126
|
}
|
|
6269
8127
|
return expected;
|
|
6270
8128
|
}
|
|
8129
|
+
/**
|
|
8130
|
+
* Drops the derived search index, so the next search rebuilds it.
|
|
8131
|
+
*
|
|
8132
|
+
* `searchBase` re-indexes when a record is newer than the index, which no
|
|
8133
|
+
* deletion makes true — a swept record would stay findable until some other
|
|
8134
|
+
* record was written.
|
|
8135
|
+
*/
|
|
8136
|
+
async dropSearchIndex(bundlePath2) {
|
|
8137
|
+
await (0, import_promises12.unlink)((0, import_node_path14.join)(this.root(bundlePath2), SEARCH_INDEX_FILE)).catch(
|
|
8138
|
+
() => void 0
|
|
8139
|
+
);
|
|
8140
|
+
}
|
|
6271
8141
|
/**
|
|
6272
8142
|
* The log, with unparseable lines reported rather than repaired.
|
|
6273
8143
|
*
|
|
6274
8144
|
* The log is the bundle's only artifact that cannot be reconstructed — the
|
|
6275
8145
|
* records rebuild the index, and the code outlives both, but nothing else
|
|
6276
8146
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
8147
|
+
* Conflict markers are read past rather than reported per line.
|
|
6277
8148
|
*/
|
|
6278
8149
|
async readLog(bundlePath2) {
|
|
6279
|
-
const raw = await (0,
|
|
6280
|
-
(0,
|
|
8150
|
+
const raw = await (0, import_promises12.readFile)(
|
|
8151
|
+
(0, import_node_path14.join)(this.root(bundlePath2), LOG_FILE),
|
|
6281
8152
|
"utf8"
|
|
6282
8153
|
).catch(() => "");
|
|
6283
8154
|
const result = parseLog(raw);
|
|
8155
|
+
if (result.conflicted) {
|
|
8156
|
+
this.logger.warn?.({
|
|
8157
|
+
operation: "kb.log.parse",
|
|
8158
|
+
bundlePath: this.root(bundlePath2),
|
|
8159
|
+
outcome: "conflicted"
|
|
8160
|
+
});
|
|
8161
|
+
}
|
|
6284
8162
|
for (const bad of result.malformed) {
|
|
6285
8163
|
this.logger.warn?.({
|
|
6286
8164
|
operation: "kb.log.parse",
|
|
@@ -6290,6 +8168,14 @@ ${answer}
|
|
|
6290
8168
|
}
|
|
6291
8169
|
return result;
|
|
6292
8170
|
}
|
|
8171
|
+
/**
|
|
8172
|
+
* Appends one log entry for a move the store cannot see from one base.
|
|
8173
|
+
* Promotion writes into a target base and has to be legible from the source
|
|
8174
|
+
* base too, where nothing was written.
|
|
8175
|
+
*/
|
|
8176
|
+
async note(bundlePath2, entry) {
|
|
8177
|
+
await this.record(this.root(bundlePath2), entry);
|
|
8178
|
+
}
|
|
6293
8179
|
/**
|
|
6294
8180
|
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
6295
8181
|
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
@@ -6328,14 +8214,14 @@ ${answer}
|
|
|
6328
8214
|
}
|
|
6329
8215
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
6330
8216
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
6331
|
-
const before = await (0,
|
|
8217
|
+
const before = await (0, import_promises12.readFile)(target, "utf8").catch(() => null);
|
|
6332
8218
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
6333
8219
|
const parsed = this.parse(conceptId2, before);
|
|
6334
8220
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
6335
8221
|
const frontmatter = change(parsed.frontmatter);
|
|
6336
8222
|
const body = changeBody(parsed.body);
|
|
6337
8223
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
6338
|
-
const witness = await (0,
|
|
8224
|
+
const witness = await (0, import_promises12.readFile)(target, "utf8").catch(() => null);
|
|
6339
8225
|
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
6340
8226
|
throw new KbWriteConflictError(conceptId2);
|
|
6341
8227
|
}
|
|
@@ -6361,26 +8247,27 @@ ${answer}
|
|
|
6361
8247
|
*/
|
|
6362
8248
|
async publish(target, contents, overwrite, conceptId2) {
|
|
6363
8249
|
const staging = `${target}.${process.pid}.tmp`;
|
|
6364
|
-
await (0,
|
|
8250
|
+
await (0, import_promises12.writeFile)(staging, contents, "utf8");
|
|
6365
8251
|
try {
|
|
6366
8252
|
if (overwrite) {
|
|
6367
|
-
await (0,
|
|
8253
|
+
await (0, import_promises12.rename)(staging, target);
|
|
6368
8254
|
return;
|
|
6369
8255
|
}
|
|
6370
|
-
await (0,
|
|
8256
|
+
await (0, import_promises12.link)(staging, target);
|
|
6371
8257
|
} catch (error) {
|
|
6372
8258
|
if (error.code === "EEXIST") {
|
|
6373
8259
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
6374
8260
|
}
|
|
6375
8261
|
throw error;
|
|
6376
8262
|
} finally {
|
|
6377
|
-
await (0,
|
|
8263
|
+
await (0, import_promises12.unlink)(staging).catch(() => void 0);
|
|
6378
8264
|
}
|
|
6379
8265
|
}
|
|
6380
8266
|
/**
|
|
6381
8267
|
* Declares union merge for the log, so two worktrees writing the same
|
|
6382
8268
|
* bundle interleave their `log.jsonl` lines on merge rather than one
|
|
6383
|
-
* side's appends silently losing to git's ordinary line-level merge
|
|
8269
|
+
* side's appends silently losing to git's ordinary line-level merge — and
|
|
8270
|
+
* marks every store-owned file generated, so GitHub collapses it in a diff.
|
|
6384
8271
|
*
|
|
6385
8272
|
* Called from `record` — every path that appends a log line, not just
|
|
6386
8273
|
* `write` — so a bundle only ever mutated through `setStatus`/`verify`/
|
|
@@ -6393,10 +8280,9 @@ ${answer}
|
|
|
6393
8280
|
* race and created the file between the `readFile` below and this call,
|
|
6394
8281
|
* `wx` fails instead of truncating what that writer just wrote, and the
|
|
6395
8282
|
* failure is swallowed by the catch below same as any other best-effort
|
|
6396
|
-
* miss. A file that exists
|
|
6397
|
-
*
|
|
6398
|
-
*
|
|
6399
|
-
* entirely (see `hasMergeDeclaration`).
|
|
8283
|
+
* miss. A file that exists gets only the lines it lacks appended, never a
|
|
8284
|
+
* wholesale rewrite; an attribute it already sets — this one's value or a
|
|
8285
|
+
* user's own — is left alone (see `missingGitattributesLines`).
|
|
6400
8286
|
*
|
|
6401
8287
|
* `readFile` failing is `existing === null` only for `ENOENT` — genuinely
|
|
6402
8288
|
* missing. Any other error (a permission problem, a transient `EMFILE`,
|
|
@@ -6407,29 +8293,29 @@ ${answer}
|
|
|
6407
8293
|
* therefore left untouched and reported as a failure like any other.
|
|
6408
8294
|
*
|
|
6409
8295
|
* Two processes racing the append branch — both read a file without the
|
|
6410
|
-
*
|
|
6411
|
-
* `O_APPEND`, so the result is two copies of the same
|
|
6412
|
-
* torn write, and
|
|
6413
|
-
*
|
|
6414
|
-
*
|
|
6415
|
-
*
|
|
8296
|
+
* lines, both append them — is possible and left unguarded: `appendFile` is
|
|
8297
|
+
* `O_APPEND`, so the result is two copies of the same lines rather than a
|
|
8298
|
+
* torn write, and the next call sees a duplicate declaration as "already
|
|
8299
|
+
* declared". A cheap-to-detect, harmless-to-leave residue, not a reason to
|
|
8300
|
+
* add a cross-process lock (see `ARCHITECTURE.md`'s rejection of one for
|
|
8301
|
+
* the same trade on records).
|
|
6416
8302
|
*
|
|
6417
8303
|
* Best-effort, like the log append it precedes: failing to write this
|
|
6418
8304
|
* file must not fail the mutation it guards.
|
|
6419
8305
|
*/
|
|
6420
8306
|
async ensureGitattributes(root) {
|
|
6421
|
-
const target = (0,
|
|
8307
|
+
const target = (0, import_node_path14.join)(root, GITATTRIBUTES_FILE);
|
|
6422
8308
|
try {
|
|
6423
8309
|
let existing;
|
|
6424
8310
|
try {
|
|
6425
|
-
existing = await (0,
|
|
8311
|
+
existing = await (0, import_promises12.readFile)(target, "utf8");
|
|
6426
8312
|
} catch (error) {
|
|
6427
8313
|
if (error.code !== "ENOENT") throw error;
|
|
6428
8314
|
existing = null;
|
|
6429
8315
|
}
|
|
6430
8316
|
if (existing === null) {
|
|
6431
8317
|
try {
|
|
6432
|
-
await (0,
|
|
8318
|
+
await (0, import_promises12.writeFile)(target, appendGitattributesLines(""), {
|
|
6433
8319
|
encoding: "utf8",
|
|
6434
8320
|
flag: "wx"
|
|
6435
8321
|
});
|
|
@@ -6449,8 +8335,9 @@ ${answer}
|
|
|
6449
8335
|
});
|
|
6450
8336
|
return;
|
|
6451
8337
|
}
|
|
6452
|
-
|
|
6453
|
-
|
|
8338
|
+
const addition = appendGitattributesLines(existing);
|
|
8339
|
+
if (addition) {
|
|
8340
|
+
await (0, import_promises12.appendFile)(target, addition, "utf8");
|
|
6454
8341
|
this.logger.info?.({
|
|
6455
8342
|
operation: "kb.gitattributes.ensure",
|
|
6456
8343
|
bundlePath: root,
|
|
@@ -6469,7 +8356,7 @@ ${answer}
|
|
|
6469
8356
|
async record(root, entry) {
|
|
6470
8357
|
await this.ensureGitattributes(root);
|
|
6471
8358
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
6472
|
-
await (0,
|
|
8359
|
+
await (0, import_promises12.appendFile)((0, import_node_path14.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
6473
8360
|
this.logger.warn?.({
|
|
6474
8361
|
operation: "kb.log.append",
|
|
6475
8362
|
outcome: "failed",
|
|
@@ -6495,18 +8382,18 @@ ${answer}
|
|
|
6495
8382
|
};
|
|
6496
8383
|
}
|
|
6497
8384
|
root(bundlePath2) {
|
|
6498
|
-
return (0,
|
|
8385
|
+
return (0, import_node_path14.resolve)(bundlePath2);
|
|
6499
8386
|
}
|
|
6500
8387
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
6501
8388
|
// bundle root; anything carrying a separator would escape it.
|
|
6502
8389
|
recordPath(bundlePath2, conceptId2) {
|
|
6503
|
-
if (conceptId2.includes(
|
|
8390
|
+
if (conceptId2.includes(import_node_path14.sep) || conceptId2.includes("/")) {
|
|
6504
8391
|
throw new KbInvalidConceptIdError(
|
|
6505
8392
|
"concept id must not contain a path separator",
|
|
6506
8393
|
{ conceptId: conceptId2 }
|
|
6507
8394
|
);
|
|
6508
8395
|
}
|
|
6509
|
-
return (0,
|
|
8396
|
+
return (0, import_node_path14.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
6510
8397
|
}
|
|
6511
8398
|
};
|
|
6512
8399
|
function estimateTokens(record) {
|
|
@@ -6546,7 +8433,7 @@ function normalizeActor(id) {
|
|
|
6546
8433
|
}
|
|
6547
8434
|
|
|
6548
8435
|
// src/version.ts
|
|
6549
|
-
var VERSION = true ? "0.1.
|
|
8436
|
+
var VERSION = true ? "0.1.20" : "0.0.0-dev";
|
|
6550
8437
|
|
|
6551
8438
|
// src/mcp.ts
|
|
6552
8439
|
function createKbMcpServer() {
|