@saasontools/strauss-kb 0.1.13 → 0.1.15
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 +59 -52
- package/README.md +163 -438
- package/dist/{chunk-XALWG3EZ.js → chunk-KNIUBCZY.js} +868 -346
- package/dist/chunk-KNIUBCZY.js.map +1 -0
- package/dist/{chunk-WZODZNR6.js → chunk-LACCRB2Y.js} +2 -2
- package/dist/{chunk-ZICKDZGY.js → chunk-MK7GU4DX.js} +2 -2
- package/dist/cli-main.cjs +973 -453
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +906 -377
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -67
- package/dist/index.d.ts +144 -67
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +895 -375
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-XALWG3EZ.js.map +0 -1
- /package/dist/{chunk-WZODZNR6.js.map → chunk-LACCRB2Y.js.map} +0 -0
- /package/dist/{chunk-ZICKDZGY.js.map → chunk-MK7GU4DX.js.map} +0 -0
package/dist/cli-main.cjs
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_path9 = require("path");
|
|
28
28
|
|
|
29
29
|
// src/decision-record.ts
|
|
30
30
|
var import_zod3 = require("zod");
|
|
@@ -58,16 +58,17 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
58
58
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
59
59
|
* own repository, which is what nearly every anchor means.
|
|
60
60
|
*
|
|
61
|
-
* Unvalidated beyond not-blank: one repository has many spellings
|
|
62
|
-
*
|
|
61
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
62
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
63
|
+
* warns on a short one; see ARCHITECTURE.
|
|
63
64
|
*/
|
|
64
65
|
repo: import_zod.z.string().trim().min(1).optional(),
|
|
65
66
|
/**
|
|
66
67
|
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
67
68
|
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
68
69
|
* came from wherever that branch happens to be now, which is not a
|
|
69
|
-
* baseline.
|
|
70
|
-
*
|
|
70
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
71
|
+
* the remote's default branch on top of it.
|
|
71
72
|
*/
|
|
72
73
|
ref: import_zod.z.string().trim().min(1).optional(),
|
|
73
74
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
@@ -442,13 +443,6 @@ function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
|
442
443
|
// src/commands/anchor-resolve.ts
|
|
443
444
|
var import_zod6 = require("zod");
|
|
444
445
|
|
|
445
|
-
// src/anchor-resolver.ts
|
|
446
|
-
var import_node_child_process = require("child_process");
|
|
447
|
-
var import_node_crypto = require("crypto");
|
|
448
|
-
var import_promises = require("fs/promises");
|
|
449
|
-
var import_node_path = require("path");
|
|
450
|
-
var import_node_util = require("util");
|
|
451
|
-
|
|
452
446
|
// src/concurrency.ts
|
|
453
447
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
454
448
|
async function mapLimit(items, limit, fn) {
|
|
@@ -478,196 +472,14 @@ async function mapLimit(items, limit, fn) {
|
|
|
478
472
|
return out;
|
|
479
473
|
}
|
|
480
474
|
|
|
481
|
-
// src/
|
|
475
|
+
// src/remote-repo/cache.ts
|
|
476
|
+
var import_node_os = require("os");
|
|
477
|
+
var import_node_path = require("path");
|
|
478
|
+
|
|
479
|
+
// src/anchor-resolver/repo-identity.ts
|
|
480
|
+
var import_node_child_process = require("child_process");
|
|
481
|
+
var import_node_util = require("util");
|
|
482
482
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
483
|
-
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
484
|
-
var PARENT_SCOPE_LINES = 50;
|
|
485
|
-
var CLEAN_STATE = { blockComment: false, template: false };
|
|
486
|
-
function stripLine(line, state) {
|
|
487
|
-
let out = "";
|
|
488
|
-
let index = 0;
|
|
489
|
-
let { blockComment, template } = state;
|
|
490
|
-
while (index < line.length) {
|
|
491
|
-
const char = line[index];
|
|
492
|
-
const next = line[index + 1];
|
|
493
|
-
if (blockComment) {
|
|
494
|
-
if (char === "*" && next === "/") {
|
|
495
|
-
blockComment = false;
|
|
496
|
-
index += 2;
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
index += 1;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
if (template) {
|
|
503
|
-
if (char === "\\") {
|
|
504
|
-
index += 2;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
if (char === "`") template = false;
|
|
508
|
-
index += 1;
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
if (char === "/" && next === "*") {
|
|
512
|
-
blockComment = true;
|
|
513
|
-
index += 2;
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
if (char === "/" && next === "/") break;
|
|
517
|
-
if (char === "`") {
|
|
518
|
-
template = true;
|
|
519
|
-
index += 1;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (char === "'" || char === '"') {
|
|
523
|
-
const quote = char;
|
|
524
|
-
index += 1;
|
|
525
|
-
while (index < line.length) {
|
|
526
|
-
if (line[index] === "\\") {
|
|
527
|
-
index += 2;
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
if (line[index] === quote) {
|
|
531
|
-
index += 1;
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
534
|
-
index += 1;
|
|
535
|
-
}
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
out += char;
|
|
539
|
-
index += 1;
|
|
540
|
-
}
|
|
541
|
-
return { code: out, state: { blockComment, template } };
|
|
542
|
-
}
|
|
543
|
-
function span(lines, from, to) {
|
|
544
|
-
return {
|
|
545
|
-
text: lines.slice(from, to + 1).join("\n"),
|
|
546
|
-
startLine: from + 1,
|
|
547
|
-
endLine: to + 1
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
function captureBraceBlock(lines, matchLine) {
|
|
551
|
-
let depth = 0;
|
|
552
|
-
let opened = false;
|
|
553
|
-
let state = CLEAN_STATE;
|
|
554
|
-
for (let index = matchLine; index < lines.length; index++) {
|
|
555
|
-
const stripped = stripLine(lines[index] ?? "", state);
|
|
556
|
-
state = stripped.state;
|
|
557
|
-
for (const char of stripped.code) {
|
|
558
|
-
if (char === "{") {
|
|
559
|
-
depth += 1;
|
|
560
|
-
opened = true;
|
|
561
|
-
} else if (char === "}") {
|
|
562
|
-
depth = Math.max(0, depth - 1);
|
|
563
|
-
} else if (char === ";" && !opened) {
|
|
564
|
-
return span(lines, matchLine, index);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
568
|
-
}
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
572
|
-
function captureIndentedBlock(lines, matchLine) {
|
|
573
|
-
const header = lines[matchLine] ?? "";
|
|
574
|
-
const indent = header.length - header.trimStart().length;
|
|
575
|
-
let headerEnd = -1;
|
|
576
|
-
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
577
|
-
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
578
|
-
if (code.endsWith(":")) {
|
|
579
|
-
headerEnd = index;
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
if (code.includes(":")) return span(lines, matchLine, index);
|
|
583
|
-
}
|
|
584
|
-
if (headerEnd === -1) return null;
|
|
585
|
-
let end = headerEnd;
|
|
586
|
-
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
587
|
-
const line = lines[index] ?? "";
|
|
588
|
-
if (line.trim() === "") continue;
|
|
589
|
-
const lineIndent = line.length - line.trimStart().length;
|
|
590
|
-
if (lineIndent <= indent) break;
|
|
591
|
-
end = index;
|
|
592
|
-
}
|
|
593
|
-
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
594
|
-
}
|
|
595
|
-
var TIERS = [
|
|
596
|
-
(name) => new RegExp(
|
|
597
|
-
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
598
|
-
),
|
|
599
|
-
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
600
|
-
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
601
|
-
(name) => new RegExp(`\\b${name}\\b`)
|
|
602
|
-
];
|
|
603
|
-
var regexResolver = {
|
|
604
|
-
name: "regex",
|
|
605
|
-
resolve(source, symbol) {
|
|
606
|
-
const segments = symbol.split(".");
|
|
607
|
-
const name = segments[segments.length - 1];
|
|
608
|
-
if (!name) return null;
|
|
609
|
-
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
610
|
-
const escaped = escapeRegExp(name);
|
|
611
|
-
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
612
|
-
const lines = source.split("\n");
|
|
613
|
-
for (const tier of TIERS) {
|
|
614
|
-
const pattern = tier(escaped);
|
|
615
|
-
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
616
|
-
if (!candidates.length) continue;
|
|
617
|
-
if (parentPattern && candidates.length > 1) {
|
|
618
|
-
const distances = candidates.map(
|
|
619
|
-
(index) => distanceToParent(lines, index, parentPattern)
|
|
620
|
-
);
|
|
621
|
-
const nearest = Math.min(...distances);
|
|
622
|
-
if (Number.isFinite(nearest)) {
|
|
623
|
-
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if (candidates.length !== 1) return null;
|
|
627
|
-
const matchLine = candidates[0];
|
|
628
|
-
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
629
|
-
}
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
function escapeRegExp(value) {
|
|
634
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
635
|
-
}
|
|
636
|
-
function distanceToParent(lines, index, parent) {
|
|
637
|
-
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
638
|
-
for (let at = index; at >= floor; at--) {
|
|
639
|
-
if (parent.test(lines[at] ?? "")) return index - at;
|
|
640
|
-
}
|
|
641
|
-
return Number.POSITIVE_INFINITY;
|
|
642
|
-
}
|
|
643
|
-
function hashAnchorText(text) {
|
|
644
|
-
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
645
|
-
}
|
|
646
|
-
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
647
|
-
const normalized = source.replace(/\r\n/g, "\n");
|
|
648
|
-
if (!anchor.symbol) {
|
|
649
|
-
const lines = normalized.split("\n");
|
|
650
|
-
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
651
|
-
return {
|
|
652
|
-
text: normalized,
|
|
653
|
-
startLine: 1,
|
|
654
|
-
endLine: Math.max(1, lines.length)
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
return resolver.resolve(normalized, anchor.symbol);
|
|
658
|
-
}
|
|
659
|
-
function anchorFilePath(repoRoot, file) {
|
|
660
|
-
const path = (0, import_node_path.resolve)(repoRoot, file.replace(/^\.\//, ""));
|
|
661
|
-
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(repoRoot), path);
|
|
662
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(rel)) {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
return path;
|
|
666
|
-
}
|
|
667
|
-
function contains(root, path) {
|
|
668
|
-
const rel = (0, import_node_path.relative)(root, path);
|
|
669
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`) && !(0, import_node_path.isAbsolute)(rel);
|
|
670
|
-
}
|
|
671
483
|
function normalizeRepoUrl(value) {
|
|
672
484
|
let url = value.trim().replace(/^git\+/, "");
|
|
673
485
|
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
@@ -682,6 +494,9 @@ function trimTrailingSlashes(value) {
|
|
|
682
494
|
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
683
495
|
return value.slice(0, end);
|
|
684
496
|
}
|
|
497
|
+
function isCanonicalRepoUrl(value) {
|
|
498
|
+
return /^[a-z0-9+.-]+:\/\//.test(normalizeRepoUrl(value));
|
|
499
|
+
}
|
|
685
500
|
function repoPath(normalized) {
|
|
686
501
|
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
687
502
|
const segments = withoutScheme.split("/").filter(Boolean);
|
|
@@ -733,13 +548,382 @@ var LazyOrigin = class {
|
|
|
733
548
|
return this.isForeign(anchor);
|
|
734
549
|
}
|
|
735
550
|
};
|
|
551
|
+
|
|
552
|
+
// src/remote-repo/cache.ts
|
|
553
|
+
function repoCacheDir(override) {
|
|
554
|
+
return override ?? process.env["STRAUSS_KB_REPO_CACHE"] ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".strauss", "repo-cache");
|
|
555
|
+
}
|
|
556
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
|
|
557
|
+
function fetchTimeoutMs(override) {
|
|
558
|
+
if (override !== void 0) return override;
|
|
559
|
+
const fromEnv = Number(process.env["STRAUSS_KB_FETCH_TIMEOUT_MS"]);
|
|
560
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_FETCH_TIMEOUT_MS;
|
|
561
|
+
}
|
|
562
|
+
function cachePathFor(repo, cacheDir) {
|
|
563
|
+
const normalized = normalizeRepoUrl(repo);
|
|
564
|
+
const scheme = /^[a-z0-9+.-]+:\/\//.exec(normalized);
|
|
565
|
+
if (!scheme) return null;
|
|
566
|
+
const segments = normalized.slice(scheme[0].length).split("/").filter(Boolean).map((segment) => safeSegment(segment));
|
|
567
|
+
if (segments.length < 2 || segments.some((segment) => segment === null)) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const path = segments;
|
|
571
|
+
return (0, import_node_path.join)(cacheDir, ...path.slice(0, -1), `${path[path.length - 1]}.git`);
|
|
572
|
+
}
|
|
573
|
+
function safeSegment(value) {
|
|
574
|
+
return value === "." || value === ".." || value.includes("\0") ? null : value.replace(/[/\\:]/g, "-");
|
|
575
|
+
}
|
|
576
|
+
function revRef(rev) {
|
|
577
|
+
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
578
|
+
let hash = 5381;
|
|
579
|
+
for (let at = 0; at < rev.length; at++) {
|
|
580
|
+
hash = (hash * 33 ^ rev.charCodeAt(at)) >>> 0;
|
|
581
|
+
}
|
|
582
|
+
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/remote-repo/model.ts
|
|
586
|
+
var UNCHECKED_REASONS = [
|
|
587
|
+
"remote-unreachable",
|
|
588
|
+
"repo-unauthorized",
|
|
589
|
+
"default-branch-unknown"
|
|
590
|
+
];
|
|
591
|
+
function isUncheckedReason(reason) {
|
|
592
|
+
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
593
|
+
}
|
|
594
|
+
function wantKey(repo, ref, file) {
|
|
595
|
+
return `${repo}\0${ref ?? ""}\0${file}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/remote-repo/read.ts
|
|
599
|
+
var import_promises = require("fs/promises");
|
|
600
|
+
|
|
601
|
+
// src/remote-repo/git.ts
|
|
602
|
+
var import_node_child_process2 = require("child_process");
|
|
603
|
+
var import_node_util2 = require("util");
|
|
604
|
+
|
|
605
|
+
// src/anchor-resolver/model.ts
|
|
606
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
607
|
+
|
|
608
|
+
// src/remote-repo/git.ts
|
|
609
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
610
|
+
function childEnv() {
|
|
611
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
612
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
613
|
+
delete env[name];
|
|
614
|
+
}
|
|
615
|
+
return env;
|
|
616
|
+
}
|
|
617
|
+
async function git(args, options = {}) {
|
|
618
|
+
try {
|
|
619
|
+
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
620
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
621
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
622
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
623
|
+
encoding: "utf8",
|
|
624
|
+
windowsHide: true,
|
|
625
|
+
env: childEnv()
|
|
626
|
+
});
|
|
627
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const failure = error;
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
stdout: failure.stdout ?? "",
|
|
633
|
+
stderr: failure.stderr ?? "",
|
|
634
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function transportReason(stderr) {
|
|
639
|
+
const text = stderr.toLowerCase();
|
|
640
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
641
|
+
return "repo-unauthorized";
|
|
642
|
+
}
|
|
643
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
644
|
+
return "ref-not-found";
|
|
645
|
+
}
|
|
646
|
+
return "remote-unreachable";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/remote-repo/validate.ts
|
|
650
|
+
var MAX_REF_LENGTH = 200;
|
|
651
|
+
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
652
|
+
function refShapeIsSafe(ref) {
|
|
653
|
+
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
654
|
+
if (ref.includes("..")) return false;
|
|
655
|
+
return REF_SHAPE.test(ref);
|
|
656
|
+
}
|
|
657
|
+
async function refIsWellFormed(ref) {
|
|
658
|
+
if (!refShapeIsSafe(ref)) return false;
|
|
659
|
+
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
660
|
+
return checked.ok;
|
|
661
|
+
}
|
|
662
|
+
function filePathIsSafe(file) {
|
|
663
|
+
const path = file.replace(/^\.\//, "");
|
|
664
|
+
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
665
|
+
return !path.split("/").includes("..");
|
|
666
|
+
}
|
|
667
|
+
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
668
|
+
function allowedProtocols() {
|
|
669
|
+
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
670
|
+
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
671
|
+
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
672
|
+
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
673
|
+
}
|
|
674
|
+
function isShortRepoName(repo) {
|
|
675
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
676
|
+
}
|
|
677
|
+
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
678
|
+
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
679
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
680
|
+
function repoUrlIsSafe(repo) {
|
|
681
|
+
const url = repo.trim();
|
|
682
|
+
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
683
|
+
const allowed = allowedProtocols();
|
|
684
|
+
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
685
|
+
const scheme = URL_SCHEME.exec(url);
|
|
686
|
+
if (!scheme?.[1]) return false;
|
|
687
|
+
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
688
|
+
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
689
|
+
const at = authority.lastIndexOf("@");
|
|
690
|
+
return at < 0 || !authority.slice(0, at).includes(":");
|
|
691
|
+
}
|
|
692
|
+
function protocolArgs() {
|
|
693
|
+
const allowed = allowedProtocols();
|
|
694
|
+
return [
|
|
695
|
+
"-c",
|
|
696
|
+
"protocol.ext.allow=never",
|
|
697
|
+
"-c",
|
|
698
|
+
`protocol.file.allow=${allowed.includes("file") ? "user" : "never"}`
|
|
699
|
+
];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/remote-repo/read.ts
|
|
703
|
+
var DEFAULT_REPO_CONCURRENCY = 4;
|
|
704
|
+
var IMMUTABLE_REV = /^[0-9a-f]{40}$/;
|
|
705
|
+
async function readRemoteAnchors(wants, options = {}) {
|
|
706
|
+
const out = /* @__PURE__ */ new Map();
|
|
707
|
+
if (!wants.length) return out;
|
|
708
|
+
const cacheDir = repoCacheDir(options.cacheDir);
|
|
709
|
+
const timeoutMs = fetchTimeoutMs(options.fetchTimeoutMs);
|
|
710
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
711
|
+
for (const want of wants) {
|
|
712
|
+
const key = normalizeRepoUrl(want.repo);
|
|
713
|
+
const group2 = byRepo.get(key) ?? { url: want.repo.trim(), wants: [] };
|
|
714
|
+
group2.wants.push(want);
|
|
715
|
+
byRepo.set(key, group2);
|
|
716
|
+
}
|
|
717
|
+
const groups = [...byRepo.entries()];
|
|
718
|
+
const results = await mapLimit(
|
|
719
|
+
groups,
|
|
720
|
+
Math.max(1, options.concurrency ?? DEFAULT_REPO_CONCURRENCY),
|
|
721
|
+
([repo, group2]) => readOneRepo(repo, group2.url, group2.wants, {
|
|
722
|
+
cacheDir,
|
|
723
|
+
timeoutMs,
|
|
724
|
+
offline: options.offline === true
|
|
725
|
+
})
|
|
726
|
+
);
|
|
727
|
+
for (const result of results) {
|
|
728
|
+
for (const [key, read] of result) out.set(key, read);
|
|
729
|
+
}
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
async function readOneRepo(repo, url, declared, context) {
|
|
733
|
+
let wants = declared;
|
|
734
|
+
const all = (read) => new Map(wants.map((want) => [wantKey(repo, want.ref, want.file), read]));
|
|
735
|
+
if (isShortRepoName(url)) {
|
|
736
|
+
return all({ ok: false, reason: "remote-unreachable" });
|
|
737
|
+
}
|
|
738
|
+
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
739
|
+
const cache = cachePathFor(repo, context.cacheDir);
|
|
740
|
+
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
741
|
+
const rejected = /* @__PURE__ */ new Map();
|
|
742
|
+
const usable = [];
|
|
743
|
+
for (const want of wants) {
|
|
744
|
+
const reason = wantReason(want);
|
|
745
|
+
if (reason)
|
|
746
|
+
rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
747
|
+
else usable.push(want);
|
|
748
|
+
}
|
|
749
|
+
if (!usable.length) return rejected;
|
|
750
|
+
wants = usable;
|
|
751
|
+
const opened = await openCache(cache, url, context);
|
|
752
|
+
if (opened) return new Map([...rejected, ...all(opened)]);
|
|
753
|
+
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
754
|
+
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
755
|
+
const revs = /* @__PURE__ */ new Map();
|
|
756
|
+
for (const rev of distinctRevs(wants, branch.name)) {
|
|
757
|
+
revs.set(
|
|
758
|
+
rev,
|
|
759
|
+
await refIsWellFormed(rev) ? await ensureRev(cache, rev, context) : { ok: false, reason: "ref-invalid" }
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const reads = await mapLimit(
|
|
763
|
+
wants,
|
|
764
|
+
DEFAULT_IO_CONCURRENCY,
|
|
765
|
+
async (want) => {
|
|
766
|
+
const rev = want.ref ?? branch.name;
|
|
767
|
+
if (rev === void 0) {
|
|
768
|
+
return {
|
|
769
|
+
ok: false,
|
|
770
|
+
reason: branch.reason ?? "default-branch-unknown"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const failed = revs.get(rev);
|
|
774
|
+
if (failed) return failed;
|
|
775
|
+
return readBlob(cache, rev, want.file, context);
|
|
776
|
+
}
|
|
777
|
+
);
|
|
778
|
+
return new Map([
|
|
779
|
+
...rejected,
|
|
780
|
+
...wants.map(
|
|
781
|
+
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
782
|
+
)
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
function wantReason(want) {
|
|
786
|
+
if (want.ref !== void 0 && !refShapeIsSafe(want.ref)) return "ref-invalid";
|
|
787
|
+
return filePathIsSafe(want.file) ? void 0 : "outside-repo";
|
|
788
|
+
}
|
|
789
|
+
function distinctRevs(wants, branch) {
|
|
790
|
+
const revs = /* @__PURE__ */ new Set();
|
|
791
|
+
for (const want of wants) {
|
|
792
|
+
if (want.ref !== void 0) revs.add(want.ref);
|
|
793
|
+
else if (branch) revs.add(branch);
|
|
794
|
+
}
|
|
795
|
+
return [...revs];
|
|
796
|
+
}
|
|
797
|
+
async function openCache(cache, url, context) {
|
|
798
|
+
try {
|
|
799
|
+
await (0, import_promises.mkdir)(cache, { recursive: true });
|
|
800
|
+
} catch {
|
|
801
|
+
return { ok: false, reason: "remote-unreachable" };
|
|
802
|
+
}
|
|
803
|
+
const init = await git(["init", "--bare", "--quiet", cache], {
|
|
804
|
+
timeoutMs: context.timeoutMs
|
|
805
|
+
});
|
|
806
|
+
if (!init.ok) return { ok: false, reason: "remote-unreachable" };
|
|
807
|
+
const remote = await git(["config", "remote.origin.url", url], {
|
|
808
|
+
cwd: cache,
|
|
809
|
+
timeoutMs: context.timeoutMs
|
|
810
|
+
});
|
|
811
|
+
return remote.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
812
|
+
}
|
|
813
|
+
async function defaultBranch(cache, context) {
|
|
814
|
+
if (!context.offline) {
|
|
815
|
+
const listed = await git(
|
|
816
|
+
[...protocolArgs(), "ls-remote", "--symref", "origin", "HEAD"],
|
|
817
|
+
{
|
|
818
|
+
cwd: cache,
|
|
819
|
+
timeoutMs: context.timeoutMs
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
const found = /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m.exec(listed.stdout);
|
|
823
|
+
if (listed.ok && found?.[1]) {
|
|
824
|
+
const name = found[1];
|
|
825
|
+
await git(["config", "strauss.defaultBranch", name], { cwd: cache });
|
|
826
|
+
return { name };
|
|
827
|
+
}
|
|
828
|
+
if (!listed.ok) {
|
|
829
|
+
const reason = transportReason(listed.stderr);
|
|
830
|
+
if (reason !== "ref-not-found") {
|
|
831
|
+
const cached2 = await cachedBranch(cache);
|
|
832
|
+
return cached2 ? { name: cached2 } : { reason };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const cached = await cachedBranch(cache);
|
|
837
|
+
if (cached) return { name: cached };
|
|
838
|
+
return {
|
|
839
|
+
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function cachedBranch(cache) {
|
|
843
|
+
const stored = await git(["config", "--get", "strauss.defaultBranch"], {
|
|
844
|
+
cwd: cache
|
|
845
|
+
});
|
|
846
|
+
if (stored.ok && stored.stdout.trim()) return stored.stdout.trim();
|
|
847
|
+
const head = await git(
|
|
848
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
849
|
+
{
|
|
850
|
+
cwd: cache
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
const name = head.stdout.trim().replace(/^origin\//, "");
|
|
854
|
+
return head.ok && name ? name : void 0;
|
|
855
|
+
}
|
|
856
|
+
async function ensureRev(cache, rev, context) {
|
|
857
|
+
const ref = revRef(rev);
|
|
858
|
+
const have = await git(
|
|
859
|
+
["rev-parse", "--verify", "--quiet", `${ref}^{commit}`],
|
|
860
|
+
{
|
|
861
|
+
cwd: cache
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
const cached = have.ok && have.stdout.trim().length > 0;
|
|
865
|
+
if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
866
|
+
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
867
|
+
const fetched = await git(
|
|
868
|
+
[
|
|
869
|
+
...protocolArgs(),
|
|
870
|
+
"fetch",
|
|
871
|
+
"--depth",
|
|
872
|
+
"1",
|
|
873
|
+
"origin",
|
|
874
|
+
"--end-of-options",
|
|
875
|
+
rev
|
|
876
|
+
],
|
|
877
|
+
{ cwd: cache, timeoutMs: context.timeoutMs }
|
|
878
|
+
);
|
|
879
|
+
if (!fetched.ok) {
|
|
880
|
+
const reason = transportReason(fetched.stderr);
|
|
881
|
+
if (cached && reason !== "ref-not-found") return void 0;
|
|
882
|
+
return { ok: false, reason };
|
|
883
|
+
}
|
|
884
|
+
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
885
|
+
const sha = head.stdout.trim();
|
|
886
|
+
if (!head.ok || !sha) return { ok: false, reason: "remote-unreachable" };
|
|
887
|
+
const updated = await git(["update-ref", ref, sha], { cwd: cache });
|
|
888
|
+
return updated.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
889
|
+
}
|
|
890
|
+
async function readBlob(cache, rev, file, context) {
|
|
891
|
+
const path = file.replace(/^\.\//, "");
|
|
892
|
+
const blob = await git(
|
|
893
|
+
["cat-file", "blob", "--end-of-options", `${revRef(rev)}:${path}`],
|
|
894
|
+
{
|
|
895
|
+
cwd: cache,
|
|
896
|
+
timeoutMs: context.timeoutMs
|
|
897
|
+
}
|
|
898
|
+
);
|
|
899
|
+
if (blob.ok) return { ok: true, source: blob.stdout };
|
|
900
|
+
if (blob.overflowed) return { ok: false, reason: "file-too-large" };
|
|
901
|
+
const text = blob.stderr.toLowerCase();
|
|
902
|
+
return text.includes("does not exist") || text.includes("not a valid object") ? { ok: false, reason: "file-missing" } : { ok: false, reason: "file-unreadable" };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/anchor-resolver/read.ts
|
|
906
|
+
var import_promises2 = require("fs/promises");
|
|
907
|
+
var import_node_path2 = require("path");
|
|
908
|
+
function anchorFilePath(repoRoot, file) {
|
|
909
|
+
const path = (0, import_node_path2.resolve)(repoRoot, file.replace(/^\.\//, ""));
|
|
910
|
+
const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(repoRoot), path);
|
|
911
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path2.sep}`) || (0, import_node_path2.isAbsolute)(rel)) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
return path;
|
|
915
|
+
}
|
|
916
|
+
function contains(root, path) {
|
|
917
|
+
const rel = (0, import_node_path2.relative)(root, path);
|
|
918
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path2.sep}`) && !(0, import_node_path2.isAbsolute)(rel);
|
|
919
|
+
}
|
|
736
920
|
function errorCode(error) {
|
|
737
921
|
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
738
922
|
}
|
|
739
923
|
function anchorFileReader(repoRoot) {
|
|
740
924
|
let rootOnce;
|
|
741
925
|
const realRoot = () => {
|
|
742
|
-
rootOnce ??= (0,
|
|
926
|
+
rootOnce ??= (0, import_promises2.realpath)((0, import_node_path2.resolve)(repoRoot)).catch((error) => {
|
|
743
927
|
rootOnce = void 0;
|
|
744
928
|
throw error;
|
|
745
929
|
});
|
|
@@ -754,59 +938,239 @@ async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
|
754
938
|
let path;
|
|
755
939
|
try {
|
|
756
940
|
root = await realRoot();
|
|
757
|
-
path = await (0,
|
|
941
|
+
path = await (0, import_promises2.realpath)(lexical);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
const code = errorCode(error);
|
|
944
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
945
|
+
return { ok: false, reason: "file-missing" };
|
|
946
|
+
}
|
|
947
|
+
return { ok: false, reason: "file-unreadable" };
|
|
948
|
+
}
|
|
949
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
950
|
+
try {
|
|
951
|
+
const stats = await (0, import_promises2.stat)(path);
|
|
952
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
953
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
954
|
+
return { ok: false, reason: "file-too-large" };
|
|
955
|
+
}
|
|
956
|
+
return { ok: true, source: await (0, import_promises2.readFile)(path, "utf8") };
|
|
758
957
|
} catch (error) {
|
|
759
958
|
const code = errorCode(error);
|
|
760
959
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
761
960
|
return { ok: false, reason: "file-missing" };
|
|
762
961
|
}
|
|
763
|
-
return { ok: false, reason: "file-unreadable" };
|
|
962
|
+
return { ok: false, reason: "file-unreadable" };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
966
|
+
let checked = 0;
|
|
967
|
+
for (const entries of drift.values()) {
|
|
968
|
+
for (const entry of entries) {
|
|
969
|
+
if (entry.repo !== void 0) continue;
|
|
970
|
+
checked += 1;
|
|
971
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return checked > 0;
|
|
977
|
+
}
|
|
978
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
979
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
980
|
+
throw new RangeError(
|
|
981
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
const wanted = [...new Set(files)];
|
|
985
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
986
|
+
try {
|
|
987
|
+
return await read(file);
|
|
988
|
+
} catch {
|
|
989
|
+
return { ok: false, reason: "file-unreadable" };
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/anchor-resolver/resolver.ts
|
|
996
|
+
var import_node_crypto = require("crypto");
|
|
997
|
+
var PARENT_SCOPE_LINES = 50;
|
|
998
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
999
|
+
function stripLine(line, state) {
|
|
1000
|
+
let out = "";
|
|
1001
|
+
let index = 0;
|
|
1002
|
+
let { blockComment, template } = state;
|
|
1003
|
+
while (index < line.length) {
|
|
1004
|
+
const char = line[index];
|
|
1005
|
+
const next = line[index + 1];
|
|
1006
|
+
if (blockComment) {
|
|
1007
|
+
if (char === "*" && next === "/") {
|
|
1008
|
+
blockComment = false;
|
|
1009
|
+
index += 2;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
index += 1;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (template) {
|
|
1016
|
+
if (char === "\\") {
|
|
1017
|
+
index += 2;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (char === "`") template = false;
|
|
1021
|
+
index += 1;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (char === "/" && next === "*") {
|
|
1025
|
+
blockComment = true;
|
|
1026
|
+
index += 2;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (char === "/" && next === "/") break;
|
|
1030
|
+
if (char === "`") {
|
|
1031
|
+
template = true;
|
|
1032
|
+
index += 1;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (char === "'" || char === '"') {
|
|
1036
|
+
const quote = char;
|
|
1037
|
+
index += 1;
|
|
1038
|
+
while (index < line.length) {
|
|
1039
|
+
if (line[index] === "\\") {
|
|
1040
|
+
index += 2;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (line[index] === quote) {
|
|
1044
|
+
index += 1;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
index += 1;
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
out += char;
|
|
1052
|
+
index += 1;
|
|
1053
|
+
}
|
|
1054
|
+
return { code: out, state: { blockComment, template } };
|
|
1055
|
+
}
|
|
1056
|
+
function span(lines, from, to) {
|
|
1057
|
+
return {
|
|
1058
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
1059
|
+
startLine: from + 1,
|
|
1060
|
+
endLine: to + 1
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function captureBraceBlock(lines, matchLine) {
|
|
1064
|
+
let depth = 0;
|
|
1065
|
+
let opened = false;
|
|
1066
|
+
let state = CLEAN_STATE;
|
|
1067
|
+
for (let index = matchLine; index < lines.length; index++) {
|
|
1068
|
+
const stripped = stripLine(lines[index] ?? "", state);
|
|
1069
|
+
state = stripped.state;
|
|
1070
|
+
for (const char of stripped.code) {
|
|
1071
|
+
if (char === "{") {
|
|
1072
|
+
depth += 1;
|
|
1073
|
+
opened = true;
|
|
1074
|
+
} else if (char === "}") {
|
|
1075
|
+
depth = Math.max(0, depth - 1);
|
|
1076
|
+
} else if (char === ";" && !opened) {
|
|
1077
|
+
return span(lines, matchLine, index);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
764
1081
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
1085
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
1086
|
+
const header = lines[matchLine] ?? "";
|
|
1087
|
+
const indent = header.length - header.trimStart().length;
|
|
1088
|
+
let headerEnd = -1;
|
|
1089
|
+
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
1090
|
+
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1091
|
+
if (code.endsWith(":")) {
|
|
1092
|
+
headerEnd = index;
|
|
1093
|
+
break;
|
|
777
1094
|
}
|
|
778
|
-
|
|
1095
|
+
if (code.includes(":")) return span(lines, matchLine, index);
|
|
1096
|
+
}
|
|
1097
|
+
if (headerEnd === -1) return null;
|
|
1098
|
+
let end = headerEnd;
|
|
1099
|
+
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
1100
|
+
const line = lines[index] ?? "";
|
|
1101
|
+
if (line.trim() === "") continue;
|
|
1102
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
1103
|
+
if (lineIndent <= indent) break;
|
|
1104
|
+
end = index;
|
|
779
1105
|
}
|
|
1106
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
780
1107
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1108
|
+
var TIERS = [
|
|
1109
|
+
(name) => new RegExp(
|
|
1110
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
1111
|
+
),
|
|
1112
|
+
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
1113
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
1114
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
1115
|
+
];
|
|
1116
|
+
var regexResolver = {
|
|
1117
|
+
name: "regex",
|
|
1118
|
+
resolve(source, symbol) {
|
|
1119
|
+
const segments = symbol.split(".");
|
|
1120
|
+
const name = segments[segments.length - 1];
|
|
1121
|
+
if (!name) return null;
|
|
1122
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
1123
|
+
const escaped = escapeRegExp(name);
|
|
1124
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
1125
|
+
const lines = source.split("\n");
|
|
1126
|
+
for (const tier of TIERS) {
|
|
1127
|
+
const pattern = tier(escaped);
|
|
1128
|
+
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1129
|
+
if (!candidates.length) continue;
|
|
1130
|
+
if (parentPattern && candidates.length > 1) {
|
|
1131
|
+
const distances = candidates.map(
|
|
1132
|
+
(index) => distanceToParent(lines, index, parentPattern)
|
|
1133
|
+
);
|
|
1134
|
+
const nearest = Math.min(...distances);
|
|
1135
|
+
if (Number.isFinite(nearest)) {
|
|
1136
|
+
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
1137
|
+
}
|
|
789
1138
|
}
|
|
1139
|
+
if (candidates.length !== 1) return null;
|
|
1140
|
+
const matchLine = candidates[0];
|
|
1141
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
790
1142
|
}
|
|
1143
|
+
return null;
|
|
791
1144
|
}
|
|
792
|
-
|
|
1145
|
+
};
|
|
1146
|
+
function escapeRegExp(value) {
|
|
1147
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
793
1148
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
);
|
|
1149
|
+
function distanceToParent(lines, index, parent) {
|
|
1150
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
1151
|
+
for (let at = index; at >= floor; at--) {
|
|
1152
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
799
1153
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1154
|
+
return Number.POSITIVE_INFINITY;
|
|
1155
|
+
}
|
|
1156
|
+
function hashAnchorText(text) {
|
|
1157
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1158
|
+
}
|
|
1159
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
1160
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
1161
|
+
if (!anchor.symbol) {
|
|
1162
|
+
const lines = normalized.split("\n");
|
|
1163
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1164
|
+
return {
|
|
1165
|
+
text: normalized,
|
|
1166
|
+
startLine: 1,
|
|
1167
|
+
endLine: Math.max(1, lines.length)
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
809
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
// src/anchor-resolver/drift.ts
|
|
810
1174
|
async function detectAnchorDrift(records, options = {}) {
|
|
811
1175
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
812
1176
|
const resolver = options.resolver ?? regexResolver;
|
|
@@ -832,67 +1196,97 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
832
1196
|
}
|
|
833
1197
|
}
|
|
834
1198
|
const files = [];
|
|
1199
|
+
const wants = [];
|
|
835
1200
|
for (const entries of planned.values()) {
|
|
836
|
-
for (const
|
|
837
|
-
if (!
|
|
1201
|
+
for (const { anchor, foreign } of entries) {
|
|
1202
|
+
if (!foreign) files.push(anchor.file);
|
|
1203
|
+
else wants.push(...remoteWants(anchor));
|
|
838
1204
|
}
|
|
839
1205
|
}
|
|
840
|
-
const reads = await
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1206
|
+
const [reads, remote] = await Promise.all([
|
|
1207
|
+
readAnchorFiles(
|
|
1208
|
+
files,
|
|
1209
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1210
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1211
|
+
),
|
|
1212
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1213
|
+
]);
|
|
845
1214
|
const drift = /* @__PURE__ */ new Map();
|
|
846
1215
|
for (const record of records) {
|
|
847
1216
|
const entries = [];
|
|
848
1217
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
storedHash: anchor.hash
|
|
853
|
-
};
|
|
854
|
-
if (foreign) {
|
|
855
|
-
entries.push({
|
|
856
|
-
...base,
|
|
857
|
-
state: "unresolved",
|
|
858
|
-
diffSize: null,
|
|
859
|
-
reason: "foreign-repo"
|
|
860
|
-
});
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
const read = reads.get(anchor.file);
|
|
864
|
-
if (!read.ok) {
|
|
865
|
-
entries.push({
|
|
866
|
-
...base,
|
|
867
|
-
state: "unresolved",
|
|
868
|
-
diffSize: null,
|
|
869
|
-
reason: read.reason
|
|
870
|
-
});
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
874
|
-
if (!resolved) {
|
|
875
|
-
entries.push({
|
|
876
|
-
...base,
|
|
877
|
-
state: "unresolved",
|
|
878
|
-
diffSize: null,
|
|
879
|
-
reason: "symbol-not-found"
|
|
880
|
-
});
|
|
881
|
-
continue;
|
|
882
|
-
}
|
|
883
|
-
const currentHash = hashAnchorText(resolved.text);
|
|
884
|
-
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
885
|
-
entries.push({
|
|
886
|
-
...base,
|
|
887
|
-
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
888
|
-
currentHash,
|
|
889
|
-
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
890
|
-
});
|
|
1218
|
+
entries.push(
|
|
1219
|
+
foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
|
|
1220
|
+
);
|
|
891
1221
|
}
|
|
892
1222
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
893
1223
|
}
|
|
894
1224
|
return drift;
|
|
895
1225
|
}
|
|
1226
|
+
function remoteWants(anchor) {
|
|
1227
|
+
const repo = anchor.repo;
|
|
1228
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1229
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1230
|
+
return wants;
|
|
1231
|
+
}
|
|
1232
|
+
function base(anchor) {
|
|
1233
|
+
return {
|
|
1234
|
+
file: anchor.file,
|
|
1235
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1236
|
+
storedHash: anchor.hash
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function unresolved(anchor, reason, repo) {
|
|
1240
|
+
return {
|
|
1241
|
+
...base(anchor),
|
|
1242
|
+
state: "unresolved",
|
|
1243
|
+
diffSize: null,
|
|
1244
|
+
...reason ? { reason } : {},
|
|
1245
|
+
...repo ? { repo } : {}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function hashIn(source, anchor, resolver) {
|
|
1249
|
+
const resolved = resolveAnchor(source, anchor, resolver);
|
|
1250
|
+
if (!resolved) return null;
|
|
1251
|
+
return {
|
|
1252
|
+
hash: hashAnchorText(resolved.text),
|
|
1253
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function compared(anchor, current, extra = {}) {
|
|
1257
|
+
return {
|
|
1258
|
+
...base(anchor),
|
|
1259
|
+
state: current.hash === anchor.hash ? "match" : "drifted",
|
|
1260
|
+
currentHash: current.hash,
|
|
1261
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1262
|
+
...extra
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function localEntry(anchor, read, resolver) {
|
|
1266
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1267
|
+
const current = hashIn(read.source, anchor, resolver);
|
|
1268
|
+
return current ? compared(anchor, current) : unresolved(anchor, "symbol-not-found");
|
|
1269
|
+
}
|
|
1270
|
+
function remoteEntry(anchor, remote, resolver) {
|
|
1271
|
+
const repo = anchor.repo;
|
|
1272
|
+
const key = normalizeRepoUrl(repo);
|
|
1273
|
+
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1274
|
+
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1275
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1276
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1277
|
+
const current = hashIn(primary.source, anchor, resolver);
|
|
1278
|
+
if (!current) return unresolved(anchor, "symbol-not-found", repo);
|
|
1279
|
+
if (!anchor.ref) return compared(anchor, current, { repo });
|
|
1280
|
+
if (current.hash !== anchor.hash) {
|
|
1281
|
+
return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
|
|
1282
|
+
}
|
|
1283
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
|
|
1284
|
+
return head && head.hash !== anchor.hash ? {
|
|
1285
|
+
...compared(anchor, head, { repo }),
|
|
1286
|
+
state: "drifted",
|
|
1287
|
+
remoteState: "drifted-on-default"
|
|
1288
|
+
} : compared(anchor, current, { repo, remoteState: "matches-ref" });
|
|
1289
|
+
}
|
|
896
1290
|
|
|
897
1291
|
// src/errors.ts
|
|
898
1292
|
var BaseError = class extends Error {
|
|
@@ -1095,18 +1489,18 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1095
1489
|
};
|
|
1096
1490
|
|
|
1097
1491
|
// src/kb-pins/frozen.ts
|
|
1098
|
-
var
|
|
1492
|
+
var import_node_path5 = require("path");
|
|
1099
1493
|
|
|
1100
1494
|
// src/kb-pins/layers.ts
|
|
1101
|
-
var
|
|
1102
|
-
var
|
|
1103
|
-
var
|
|
1495
|
+
var import_promises3 = require("fs/promises");
|
|
1496
|
+
var import_node_os2 = require("os");
|
|
1497
|
+
var import_node_path4 = require("path");
|
|
1104
1498
|
|
|
1105
1499
|
// src/kb-pins/model.ts
|
|
1106
|
-
var
|
|
1500
|
+
var import_node_path3 = require("path");
|
|
1107
1501
|
var import_zod4 = require("zod");
|
|
1108
|
-
var PINS_FILE = (0,
|
|
1109
|
-
var PINS_LOCAL_FILE = (0,
|
|
1502
|
+
var PINS_FILE = (0, import_node_path3.join)(".strauss", "kb-pins.json");
|
|
1503
|
+
var PINS_LOCAL_FILE = (0, import_node_path3.join)(".strauss", "kb-pins.local.json");
|
|
1110
1504
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1111
1505
|
var pinSchema = import_zod4.z.object({
|
|
1112
1506
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -1153,13 +1547,13 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
1153
1547
|
|
|
1154
1548
|
// src/kb-pins/layers.ts
|
|
1155
1549
|
function userRoot() {
|
|
1156
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
1550
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os2.homedir)();
|
|
1157
1551
|
}
|
|
1158
1552
|
function layerRoot(workspaceDir, layer) {
|
|
1159
|
-
return layer === "user" ? userRoot() : (0,
|
|
1553
|
+
return layer === "user" ? userRoot() : (0, import_node_path4.resolve)(workspaceDir);
|
|
1160
1554
|
}
|
|
1161
1555
|
function layerFile(workspaceDir, layer) {
|
|
1162
|
-
return (0,
|
|
1556
|
+
return (0, import_node_path4.join)(
|
|
1163
1557
|
layerRoot(workspaceDir, layer),
|
|
1164
1558
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1165
1559
|
);
|
|
@@ -1168,7 +1562,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1168
1562
|
const file = layerFile(workspaceDir, layer);
|
|
1169
1563
|
let raw;
|
|
1170
1564
|
try {
|
|
1171
|
-
raw = await (0,
|
|
1565
|
+
raw = await (0, import_promises3.readFile)(file, "utf8");
|
|
1172
1566
|
} catch {
|
|
1173
1567
|
return { pins: [] };
|
|
1174
1568
|
}
|
|
@@ -1192,16 +1586,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1192
1586
|
}
|
|
1193
1587
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1194
1588
|
const file = layerFile(workspaceDir, layer);
|
|
1195
|
-
await (0,
|
|
1196
|
-
await (0,
|
|
1589
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(file), { recursive: true });
|
|
1590
|
+
await (0, import_promises3.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
1197
1591
|
`, "utf8");
|
|
1198
1592
|
}
|
|
1199
1593
|
function resolvePinPath(rootDir, path) {
|
|
1200
|
-
return (0,
|
|
1594
|
+
return (0, import_node_path4.isAbsolute)(path) ? (0, import_node_path4.resolve)(path) : (0, import_node_path4.resolve)(rootDir, path.split("/").join(import_node_path4.sep));
|
|
1201
1595
|
}
|
|
1202
1596
|
function storablePath(rootDir, bundlePath2) {
|
|
1203
|
-
const rel = (0,
|
|
1204
|
-
return (rel === "" ? "." : rel).split(
|
|
1597
|
+
const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(bundlePath2));
|
|
1598
|
+
return (rel === "" ? "." : rel).split(import_node_path4.sep).join("/");
|
|
1205
1599
|
}
|
|
1206
1600
|
async function readMergedPins(workspaceDir) {
|
|
1207
1601
|
const manifests = {};
|
|
@@ -1229,7 +1623,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
1229
1623
|
// src/kb-pins/frozen.ts
|
|
1230
1624
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
1231
1625
|
const merged = await readMergedPins(workspaceDir);
|
|
1232
|
-
const absolute = (0,
|
|
1626
|
+
const absolute = (0, import_node_path5.resolve)(bundlePath2);
|
|
1233
1627
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
1234
1628
|
if (pin?.frozen === true) {
|
|
1235
1629
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -1314,7 +1708,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1314
1708
|
}
|
|
1315
1709
|
|
|
1316
1710
|
// src/kb-pins/unpin.ts
|
|
1317
|
-
var
|
|
1711
|
+
var import_node_path6 = require("path");
|
|
1318
1712
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1319
1713
|
const layers = [];
|
|
1320
1714
|
for (const layer of PIN_LAYERS) {
|
|
@@ -1335,7 +1729,7 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
1335
1729
|
}
|
|
1336
1730
|
}
|
|
1337
1731
|
return {
|
|
1338
|
-
path: storablePath((0,
|
|
1732
|
+
path: storablePath((0, import_node_path6.resolve)(workspaceDir), bundlePath2),
|
|
1339
1733
|
removed: layers.length > 0,
|
|
1340
1734
|
layers
|
|
1341
1735
|
};
|
|
@@ -1371,12 +1765,15 @@ function argvFlag(argv, name) {
|
|
|
1371
1765
|
var anchorResolveCommand = define({
|
|
1372
1766
|
name: "anchor-resolve",
|
|
1373
1767
|
tool: "kb_anchor_resolve",
|
|
1374
|
-
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
1375
|
-
description: "Resolve a record's anchors
|
|
1768
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
1769
|
+
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
|
|
1376
1770
|
input: import_zod6.z.object({
|
|
1377
1771
|
bundlePath,
|
|
1378
1772
|
conceptId,
|
|
1379
1773
|
repoRoot: import_zod6.z.string().min(1).optional(),
|
|
1774
|
+
offline: import_zod6.z.boolean().optional().describe(
|
|
1775
|
+
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
1776
|
+
),
|
|
1380
1777
|
rebaseline: import_zod6.z.boolean().optional().describe(
|
|
1381
1778
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
1382
1779
|
),
|
|
@@ -1388,10 +1785,11 @@ var anchorResolveCommand = define({
|
|
|
1388
1785
|
bundlePath: path,
|
|
1389
1786
|
conceptId: argv[1],
|
|
1390
1787
|
repoRoot: argvFlag(argv, "--repo-root"),
|
|
1788
|
+
offline: argv.includes("--offline"),
|
|
1391
1789
|
rebaseline: argv.includes("--rebaseline"),
|
|
1392
1790
|
restamp: argv.includes("--restamp")
|
|
1393
1791
|
}),
|
|
1394
|
-
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
1792
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, offline, rebaseline, restamp }) => {
|
|
1395
1793
|
const root = repoRoot ?? process.cwd();
|
|
1396
1794
|
const record = await store.read(path, id);
|
|
1397
1795
|
if (!record) throw new KbRecordNotFoundError(id);
|
|
@@ -1406,18 +1804,10 @@ var anchorResolveCommand = define({
|
|
|
1406
1804
|
}
|
|
1407
1805
|
const results = [];
|
|
1408
1806
|
const updated = [];
|
|
1409
|
-
const origin = new LazyOrigin(root);
|
|
1410
1807
|
let dirty = false;
|
|
1411
|
-
|
|
1412
|
-
const foreign = new Map(
|
|
1413
|
-
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
1414
|
-
);
|
|
1415
|
-
const reads = await readAnchorFiles(
|
|
1416
|
-
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
1417
|
-
anchorFileReader(root)
|
|
1418
|
-
);
|
|
1808
|
+
const sources = await readSources(anchors, root, offline === true);
|
|
1419
1809
|
for (const anchor of anchors) {
|
|
1420
|
-
const
|
|
1810
|
+
const base2 = {
|
|
1421
1811
|
file: anchor.file,
|
|
1422
1812
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1423
1813
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
@@ -1425,21 +1815,17 @@ var anchorResolveCommand = define({
|
|
|
1425
1815
|
// has to be able to tell it from one nobody ever stamped.
|
|
1426
1816
|
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
1427
1817
|
};
|
|
1428
|
-
|
|
1429
|
-
|
|
1818
|
+
const source = sources.get(anchor);
|
|
1819
|
+
if (source.repo) base2.repo = source.repo;
|
|
1820
|
+
if (!source.ok) {
|
|
1821
|
+
results.push({ ...base2, state: "unresolved", reason: source.reason });
|
|
1430
1822
|
updated.push(anchor);
|
|
1431
1823
|
continue;
|
|
1432
1824
|
}
|
|
1433
|
-
const
|
|
1434
|
-
if (!fileRead.ok) {
|
|
1435
|
-
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
1436
|
-
updated.push(anchor);
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
1825
|
+
const resolved = resolveAnchor(source.source, anchor);
|
|
1440
1826
|
if (!resolved) {
|
|
1441
1827
|
results.push({
|
|
1442
|
-
...
|
|
1828
|
+
...base2,
|
|
1443
1829
|
state: "unresolved",
|
|
1444
1830
|
reason: "symbol-not-found"
|
|
1445
1831
|
});
|
|
@@ -1454,30 +1840,47 @@ var anchorResolveCommand = define({
|
|
|
1454
1840
|
lines: currentLines,
|
|
1455
1841
|
resolved_at: now()
|
|
1456
1842
|
};
|
|
1843
|
+
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
1457
1844
|
if (!anchor.hash) {
|
|
1458
|
-
results.push({ ...
|
|
1845
|
+
results.push({ ...base2, state: "stamped", currentHash });
|
|
1459
1846
|
updated.push(stamped);
|
|
1460
1847
|
dirty = true;
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
state: "match",
|
|
1465
|
-
currentHash
|
|
1466
|
-
});
|
|
1467
|
-
const refresh = restamp || anchor.resolved_at === void 0;
|
|
1468
|
-
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
1469
|
-
if (refresh) dirty = true;
|
|
1470
|
-
} else {
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (anchor.hash !== currentHash) {
|
|
1471
1851
|
results.push({
|
|
1472
|
-
...
|
|
1852
|
+
...base2,
|
|
1473
1853
|
state: "drifted",
|
|
1474
1854
|
currentHash,
|
|
1475
|
-
diffSize: anchor
|
|
1855
|
+
diffSize: lineDelta(anchor, currentLines),
|
|
1856
|
+
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
1476
1857
|
...rebaseline ? { rebaselined: true } : {}
|
|
1477
1858
|
});
|
|
1478
1859
|
updated.push(rebaseline ? stamped : anchor);
|
|
1479
1860
|
if (rebaseline) dirty = true;
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
1864
|
+
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
1865
|
+
results.push({
|
|
1866
|
+
...base2,
|
|
1867
|
+
state: "drifted",
|
|
1868
|
+
currentHash: onDefault.hash,
|
|
1869
|
+
diffSize: lineDelta(anchor, onDefault.lines),
|
|
1870
|
+
remoteState: "drifted-on-default"
|
|
1871
|
+
});
|
|
1872
|
+
updated.push(anchor);
|
|
1873
|
+
continue;
|
|
1480
1874
|
}
|
|
1875
|
+
results.push({
|
|
1876
|
+
...base2,
|
|
1877
|
+
state: "match",
|
|
1878
|
+
currentHash,
|
|
1879
|
+
...pinned ? { remoteState: "matches-ref" } : {}
|
|
1880
|
+
});
|
|
1881
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
1882
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
1883
|
+
if (refresh) dirty = true;
|
|
1481
1884
|
}
|
|
1482
1885
|
let frozen = false;
|
|
1483
1886
|
if (dirty) {
|
|
@@ -1490,16 +1893,19 @@ var anchorResolveCommand = define({
|
|
|
1490
1893
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
1491
1894
|
}
|
|
1492
1895
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1896
|
+
const unreachable = results.filter(
|
|
1897
|
+
(entry) => isUncheckedReason(entry.reason)
|
|
1898
|
+
).length;
|
|
1899
|
+
const checked = results.length - unreachable;
|
|
1900
|
+
const matches2 = results.filter((entry) => entry.state === "match").length;
|
|
1901
|
+
const note = `${matches2}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
1902
|
+
const clean = checked > 0 && matches2 === checked && unreachable === 0;
|
|
1497
1903
|
if (clean) {
|
|
1498
1904
|
try {
|
|
1499
1905
|
await store.verify(
|
|
1500
1906
|
path,
|
|
1501
1907
|
id,
|
|
1502
|
-
`anchor-resolve: ${
|
|
1908
|
+
`anchor-resolve: ${note} (regex resolver)`,
|
|
1503
1909
|
actor,
|
|
1504
1910
|
now()
|
|
1505
1911
|
);
|
|
@@ -1515,18 +1921,81 @@ var anchorResolveCommand = define({
|
|
|
1515
1921
|
}
|
|
1516
1922
|
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
1517
1923
|
}
|
|
1518
|
-
return {
|
|
1924
|
+
return {
|
|
1925
|
+
conceptId: id,
|
|
1926
|
+
results,
|
|
1927
|
+
verified: false,
|
|
1928
|
+
...unreachable ? { note } : {},
|
|
1929
|
+
...frozenNote
|
|
1930
|
+
};
|
|
1519
1931
|
},
|
|
1520
1932
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
1521
1933
|
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
1522
1934
|
// let the one edit that destroys an anchor pass the gate that exists to
|
|
1523
1935
|
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1936
|
+
// whose remote nothing could reach was never checked — failing CI on either
|
|
1937
|
+
// would gate on work this command did not do.
|
|
1526
1938
|
failsWhen: (result) => result.results.some(
|
|
1527
|
-
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason
|
|
1939
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
|
|
1528
1940
|
)
|
|
1529
1941
|
});
|
|
1942
|
+
function lineDelta(anchor, current) {
|
|
1943
|
+
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
1944
|
+
}
|
|
1945
|
+
function headHash(source, anchor) {
|
|
1946
|
+
if (source.head === void 0) return void 0;
|
|
1947
|
+
const resolved = resolveAnchor(source.head, anchor);
|
|
1948
|
+
if (!resolved) return void 0;
|
|
1949
|
+
return {
|
|
1950
|
+
hash: hashAnchorText(resolved.text),
|
|
1951
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
async function readSources(anchors, root, offline) {
|
|
1955
|
+
const origin = new LazyOrigin(root);
|
|
1956
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
1957
|
+
const foreign = new Map(
|
|
1958
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
1959
|
+
);
|
|
1960
|
+
const local = anchors.filter((anchor) => !foreign.get(anchor));
|
|
1961
|
+
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
1962
|
+
const reads = await readAnchorFiles(
|
|
1963
|
+
local.map((anchor) => anchor.file),
|
|
1964
|
+
anchorFileReader(root)
|
|
1965
|
+
);
|
|
1966
|
+
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
1967
|
+
offline
|
|
1968
|
+
});
|
|
1969
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1970
|
+
for (const anchor of local) {
|
|
1971
|
+
const read = reads.get(anchor.file);
|
|
1972
|
+
sources.set(
|
|
1973
|
+
anchor,
|
|
1974
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
for (const anchor of remote) {
|
|
1978
|
+
const repo = anchor.repo;
|
|
1979
|
+
const key = normalizeRepoUrl(repo);
|
|
1980
|
+
const atDefault = blobs.get(wantKey(key, void 0, anchor.file));
|
|
1981
|
+
const primary = anchor.ref ? blobs.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1982
|
+
if (!primary?.ok) {
|
|
1983
|
+
sources.set(anchor, {
|
|
1984
|
+
ok: false,
|
|
1985
|
+
reason: primary?.ok === false ? primary.reason : "remote-unreachable",
|
|
1986
|
+
repo
|
|
1987
|
+
});
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
sources.set(anchor, {
|
|
1991
|
+
ok: true,
|
|
1992
|
+
source: primary.source,
|
|
1993
|
+
repo,
|
|
1994
|
+
...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
return sources;
|
|
1998
|
+
}
|
|
1530
1999
|
|
|
1531
2000
|
// src/commands/answer.ts
|
|
1532
2001
|
var import_zod7 = require("zod");
|
|
@@ -1534,7 +2003,7 @@ var answerCommand = define({
|
|
|
1534
2003
|
name: "answer",
|
|
1535
2004
|
tool: "kb_answer",
|
|
1536
2005
|
usage: "answer <concept-id> <answer...>",
|
|
1537
|
-
description: "Resolve an open question:
|
|
2006
|
+
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
1538
2007
|
input: import_zod7.z.object({ bundlePath, conceptId, answer: import_zod7.z.string().min(1) }),
|
|
1539
2008
|
fromArgv: (argv, path) => ({
|
|
1540
2009
|
bundlePath: path,
|
|
@@ -1603,23 +2072,34 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift)
|
|
|
1603
2072
|
if (!record.frontmatter.verified?.length) {
|
|
1604
2073
|
warnings.push({ kind: "unverified" });
|
|
1605
2074
|
}
|
|
1606
|
-
const
|
|
1607
|
-
(entry) => entry.state !== "match"
|
|
2075
|
+
const found = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
2076
|
+
(entry) => entry.state !== "match"
|
|
1608
2077
|
);
|
|
2078
|
+
const unchecked2 = found.filter((entry) => isUncheckedReason(entry.reason));
|
|
2079
|
+
const moved = found.filter((entry) => !isUncheckedReason(entry.reason));
|
|
1609
2080
|
if (moved.length) {
|
|
2081
|
+
warnings.push({ kind: "drifted", anchors: moved.map(warningAnchor) });
|
|
2082
|
+
}
|
|
2083
|
+
if (unchecked2.length) {
|
|
1610
2084
|
warnings.push({
|
|
1611
|
-
kind: "
|
|
1612
|
-
anchors:
|
|
1613
|
-
file,
|
|
1614
|
-
...symbol !== void 0 ? { symbol } : {},
|
|
1615
|
-
diffSize,
|
|
1616
|
-
...reason !== void 0 ? { reason } : {}
|
|
1617
|
-
}))
|
|
2085
|
+
kind: "unchecked",
|
|
2086
|
+
anchors: unchecked2.map(warningAnchor)
|
|
1618
2087
|
});
|
|
1619
2088
|
}
|
|
1620
2089
|
return { record, standing: STANDING[status], heads, warnings };
|
|
1621
2090
|
});
|
|
1622
2091
|
}
|
|
2092
|
+
function warningAnchor(entry) {
|
|
2093
|
+
const { file, symbol, diffSize, reason, repo, remoteState } = entry;
|
|
2094
|
+
return {
|
|
2095
|
+
file,
|
|
2096
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
2097
|
+
diffSize,
|
|
2098
|
+
...reason !== void 0 ? { reason } : {},
|
|
2099
|
+
...repo !== void 0 ? { repo } : {},
|
|
2100
|
+
...remoteState !== void 0 ? { remoteState } : {}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1623
2103
|
function resolveHeads(from, byId) {
|
|
1624
2104
|
const warnings = [];
|
|
1625
2105
|
const heads = /* @__PURE__ */ new Map();
|
|
@@ -1781,7 +2261,7 @@ function count(value, noun) {
|
|
|
1781
2261
|
var import_zod10 = require("zod");
|
|
1782
2262
|
|
|
1783
2263
|
// src/kb-context.ts
|
|
1784
|
-
var
|
|
2264
|
+
var import_promises4 = require("fs/promises");
|
|
1785
2265
|
|
|
1786
2266
|
// src/kb-index.ts
|
|
1787
2267
|
var INDEX_FILE = "INDEX.md";
|
|
@@ -1963,7 +2443,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
1963
2443
|
operation: "kb.context.refused",
|
|
1964
2444
|
approxTokens: total,
|
|
1965
2445
|
budgetTokens,
|
|
1966
|
-
bases: bases.map((
|
|
2446
|
+
bases: bases.map((base2) => base2.path)
|
|
1967
2447
|
});
|
|
1968
2448
|
const refusal = [
|
|
1969
2449
|
HEADING2,
|
|
@@ -1973,7 +2453,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
1973
2453
|
"from a complete one. The pinned bases:",
|
|
1974
2454
|
"",
|
|
1975
2455
|
...bases.map(
|
|
1976
|
-
(
|
|
2456
|
+
(base2) => `- ${base2.path} \u2014 ~${base2.approxTokens} tokens (bundlePath: \`${base2.absolutePath}\`)`
|
|
1977
2457
|
),
|
|
1978
2458
|
"",
|
|
1979
2459
|
"For the question at hand, read what you need now \u2014 `kb_load` a base",
|
|
@@ -2008,13 +2488,13 @@ function toHookJson(block, event) {
|
|
|
2008
2488
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2009
2489
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2010
2490
|
async function syncInstructions(file, block) {
|
|
2011
|
-
const existing = await (0,
|
|
2491
|
+
const existing = await (0, import_promises4.readFile)(file, "utf8").catch(() => null);
|
|
2012
2492
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2013
2493
|
${block.trim()}
|
|
2014
2494
|
${CONTEXT_END}` : null;
|
|
2015
2495
|
if (existing === null) {
|
|
2016
2496
|
if (!region) return { file, action: "unchanged" };
|
|
2017
|
-
await (0,
|
|
2497
|
+
await (0, import_promises4.writeFile)(file, `${region}
|
|
2018
2498
|
`, "utf8");
|
|
2019
2499
|
return { file, action: "created" };
|
|
2020
2500
|
}
|
|
@@ -2025,11 +2505,11 @@ ${CONTEXT_END}` : null;
|
|
|
2025
2505
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2026
2506
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2027
2507
|
if (next === existing) return { file, action: "unchanged" };
|
|
2028
|
-
await (0,
|
|
2508
|
+
await (0, import_promises4.writeFile)(file, next, "utf8");
|
|
2029
2509
|
return { file, action: region ? "replaced" : "removed" };
|
|
2030
2510
|
}
|
|
2031
2511
|
if (!region) return { file, action: "unchanged" };
|
|
2032
|
-
await (0,
|
|
2512
|
+
await (0, import_promises4.writeFile)(
|
|
2033
2513
|
file,
|
|
2034
2514
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2035
2515
|
`,
|
|
@@ -2043,7 +2523,7 @@ var contextCommand = define({
|
|
|
2043
2523
|
name: "context",
|
|
2044
2524
|
tool: "kb_context",
|
|
2045
2525
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2046
|
-
description: "
|
|
2526
|
+
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.",
|
|
2047
2527
|
input: import_zod10.z.object({
|
|
2048
2528
|
budgetTokens: import_zod10.z.number().int().positive().optional().describe(
|
|
2049
2529
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
@@ -2246,6 +2726,16 @@ function validateBundle(records) {
|
|
|
2246
2726
|
);
|
|
2247
2727
|
}
|
|
2248
2728
|
}
|
|
2729
|
+
for (const anchor of fm.strauss_anchors ?? []) {
|
|
2730
|
+
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
2731
|
+
report(
|
|
2732
|
+
"anchor_repo",
|
|
2733
|
+
conceptId2,
|
|
2734
|
+
`anchor repo "${anchor.repo}" is not a full remote URL, so it cannot be resolved against a remote`,
|
|
2735
|
+
"warning"
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2249
2739
|
if (fm.strauss_assumption && fm.sources?.length) {
|
|
2250
2740
|
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
2251
2741
|
}
|
|
@@ -2265,7 +2755,8 @@ var CHECK_HEADLINES = {
|
|
|
2265
2755
|
orphaned: "no other record links to it",
|
|
2266
2756
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
2267
2757
|
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
2268
|
-
drifted: "the code an anchor points at moved out from under its hash"
|
|
2758
|
+
drifted: "the code an anchor points at moved out from under its hash",
|
|
2759
|
+
unchecked: "an anchor in another repository nothing could reach"
|
|
2269
2760
|
};
|
|
2270
2761
|
var DAY_MS = 864e5;
|
|
2271
2762
|
function doctor(bundle, options = {}) {
|
|
@@ -2290,7 +2781,8 @@ function doctor(bundle, options = {}) {
|
|
|
2290
2781
|
group("orphaned", orphaned(bundle)),
|
|
2291
2782
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
2292
2783
|
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
2293
|
-
group("drifted", drifted(inForce))
|
|
2784
|
+
group("drifted", drifted(inForce)),
|
|
2785
|
+
group("unchecked", unchecked(inForce))
|
|
2294
2786
|
];
|
|
2295
2787
|
const counts = Object.fromEntries(
|
|
2296
2788
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -2477,21 +2969,38 @@ function supersededButCited(bundle, standings) {
|
|
|
2477
2969
|
return findings;
|
|
2478
2970
|
}
|
|
2479
2971
|
function drifted(hits) {
|
|
2972
|
+
return anchorFindings(
|
|
2973
|
+
hits,
|
|
2974
|
+
"drifted",
|
|
2975
|
+
(count2) => count2 === 1 ? "anchor no longer matches" : "anchors no longer match"
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
function unchecked(hits) {
|
|
2979
|
+
return anchorFindings(
|
|
2980
|
+
hits,
|
|
2981
|
+
"unchecked",
|
|
2982
|
+
(count2) => count2 === 1 ? "anchor was not checked" : "anchors were not checked"
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
function anchorFindings(hits, kind, headline) {
|
|
2480
2986
|
const findings = [];
|
|
2481
2987
|
for (const hit of hits) {
|
|
2482
|
-
const warning = hit.warnings.find(
|
|
2988
|
+
const warning = hit.warnings.find(
|
|
2989
|
+
(entry) => entry.kind === kind
|
|
2990
|
+
);
|
|
2483
2991
|
if (!warning) continue;
|
|
2992
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
2993
|
+
for (const anchor of warning.anchors) {
|
|
2994
|
+
const repo = anchor.repo ?? "";
|
|
2995
|
+
byRepo.set(repo, [...byRepo.get(repo) ?? [], describeAnchor(anchor)]);
|
|
2996
|
+
}
|
|
2997
|
+
const detail = [...byRepo.entries()].map(
|
|
2998
|
+
([repo, entries]) => repo ? `${repo}: ${entries.join(", ")}` : entries.join(", ")
|
|
2999
|
+
);
|
|
2484
3000
|
findings.push(
|
|
2485
3001
|
finding(
|
|
2486
3002
|
hit.record,
|
|
2487
|
-
`${warning.anchors.length} ${warning.anchors.length
|
|
2488
|
-
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
2489
|
-
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
2490
|
-
if (anchor.diffSize === null) {
|
|
2491
|
-
return `${at} (changed, size unrecorded)`;
|
|
2492
|
-
}
|
|
2493
|
-
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
2494
|
-
}).join(", ")}`
|
|
3003
|
+
`${warning.anchors.length} ${headline(warning.anchors.length)}: ${detail.join("; ")}`
|
|
2495
3004
|
)
|
|
2496
3005
|
);
|
|
2497
3006
|
}
|
|
@@ -2499,6 +3008,15 @@ function drifted(hits) {
|
|
|
2499
3008
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
2500
3009
|
);
|
|
2501
3010
|
}
|
|
3011
|
+
function describeAnchor(anchor) {
|
|
3012
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3013
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
3014
|
+
if (anchor.remoteState === "drifted-on-default") {
|
|
3015
|
+
return `${at} (matches ref, moved on the default branch)`;
|
|
3016
|
+
}
|
|
3017
|
+
if (anchor.diffSize === null) return `${at} (changed, size unrecorded)`;
|
|
3018
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3019
|
+
}
|
|
2502
3020
|
function replaces(later, earlier) {
|
|
2503
3021
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
2504
3022
|
}
|
|
@@ -2526,8 +3044,8 @@ var days = (what, fallback) => import_zod11.z.number().int().positive().optional
|
|
|
2526
3044
|
var doctorCommand = define({
|
|
2527
3045
|
name: "doctor",
|
|
2528
3046
|
tool: "kb_doctor",
|
|
2529
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2530
|
-
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
3047
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3048
|
+
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. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
2531
3049
|
input: import_zod11.z.object({
|
|
2532
3050
|
bundlePath,
|
|
2533
3051
|
repoRoot: REPO_ROOT,
|
|
@@ -2543,6 +3061,9 @@ var doctorCommand = define({
|
|
|
2543
3061
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2544
3062
|
DEFAULT_AGING_DAYS
|
|
2545
3063
|
),
|
|
3064
|
+
offline: import_zod11.z.boolean().optional().describe(
|
|
3065
|
+
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3066
|
+
),
|
|
2546
3067
|
strict: import_zod11.z.boolean().optional().describe(
|
|
2547
3068
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2548
3069
|
)
|
|
@@ -2562,13 +3083,23 @@ var doctorCommand = define({
|
|
|
2562
3083
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
2563
3084
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
2564
3085
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3086
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
2565
3087
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
2566
3088
|
};
|
|
2567
3089
|
},
|
|
2568
|
-
run: async ({ store, now }, {
|
|
3090
|
+
run: async ({ store, now }, {
|
|
3091
|
+
bundlePath: path,
|
|
3092
|
+
expiringDays,
|
|
3093
|
+
unverifiedDays,
|
|
3094
|
+
agingDays,
|
|
3095
|
+
repoRoot,
|
|
3096
|
+
offline
|
|
3097
|
+
}) => {
|
|
2569
3098
|
const checkedAt = now();
|
|
2570
3099
|
const records = await store.list(path);
|
|
2571
|
-
const anchorDrift = await store.detectDrift(records, repoRoot
|
|
3100
|
+
const anchorDrift = await store.detectDrift(records, repoRoot, {
|
|
3101
|
+
offline: offline === true
|
|
3102
|
+
});
|
|
2572
3103
|
const report = doctor(records, {
|
|
2573
3104
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
2574
3105
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
@@ -2657,7 +3188,7 @@ var listCommand = define({
|
|
|
2657
3188
|
name: "list",
|
|
2658
3189
|
tool: "kb_list",
|
|
2659
3190
|
usage: "list [type]",
|
|
2660
|
-
description: "Every record, optionally
|
|
3191
|
+
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
2661
3192
|
input: import_zod13.z.object({ bundlePath, type: import_zod13.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2662
3193
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2663
3194
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
@@ -2675,7 +3206,7 @@ var loadCommand = define({
|
|
|
2675
3206
|
name: "load",
|
|
2676
3207
|
tool: "kb_load",
|
|
2677
3208
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2678
|
-
description: "
|
|
3209
|
+
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.",
|
|
2679
3210
|
input: import_zod14.z.object({
|
|
2680
3211
|
bundlePath,
|
|
2681
3212
|
type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
|
|
@@ -2727,7 +3258,7 @@ var logCommand = define({
|
|
|
2727
3258
|
name: "log",
|
|
2728
3259
|
tool: "kb_log",
|
|
2729
3260
|
usage: "log",
|
|
2730
|
-
description: "
|
|
3261
|
+
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
2731
3262
|
input: import_zod15.z.object({ bundlePath }),
|
|
2732
3263
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2733
3264
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
@@ -2739,7 +3270,7 @@ var noDecisionCommand = define({
|
|
|
2739
3270
|
name: "no-decision",
|
|
2740
3271
|
tool: "kb_no_decision",
|
|
2741
3272
|
usage: "no-decision <reason...>",
|
|
2742
|
-
description:
|
|
3273
|
+
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
2743
3274
|
input: import_zod16.z.object({ bundlePath, reason: import_zod16.z.string().min(1) }),
|
|
2744
3275
|
fromArgv: (argv, path) => ({
|
|
2745
3276
|
bundlePath: path,
|
|
@@ -2762,7 +3293,7 @@ var packCommand = define({
|
|
|
2762
3293
|
name: "pack",
|
|
2763
3294
|
tool: "kb_pack",
|
|
2764
3295
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2765
|
-
description: "
|
|
3296
|
+
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.",
|
|
2766
3297
|
input: import_zod17.z.object({
|
|
2767
3298
|
bundlePath,
|
|
2768
3299
|
conceptId,
|
|
@@ -2862,7 +3393,7 @@ var pinCommand = define({
|
|
|
2862
3393
|
name: "pin",
|
|
2863
3394
|
tool: "kb_pin",
|
|
2864
3395
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2865
|
-
description: "Pin a base into a workspace
|
|
3396
|
+
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.",
|
|
2866
3397
|
input: import_zod18.z.object({
|
|
2867
3398
|
bundlePath,
|
|
2868
3399
|
mode: import_zod18.z.enum(["full", "index"]).optional().describe(
|
|
@@ -2906,7 +3437,7 @@ var pinsCommand = define({
|
|
|
2906
3437
|
name: "pins",
|
|
2907
3438
|
tool: "kb_pins",
|
|
2908
3439
|
usage: "pins",
|
|
2909
|
-
description: "Every pinned base across the manifest layers,
|
|
3440
|
+
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
2910
3441
|
input: import_zod19.z.object({}),
|
|
2911
3442
|
fromArgv: () => ({}),
|
|
2912
3443
|
run: ({ store }) => listPins(store, process.cwd())
|
|
@@ -2960,7 +3491,7 @@ var readIndexCommand = define({
|
|
|
2960
3491
|
name: "index",
|
|
2961
3492
|
tool: "kb_index",
|
|
2962
3493
|
usage: "index",
|
|
2963
|
-
description: "The index
|
|
3494
|
+
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.",
|
|
2964
3495
|
input: import_zod21.z.object({ bundlePath }),
|
|
2965
3496
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2966
3497
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
@@ -3040,7 +3571,7 @@ var schemaCommand = define({
|
|
|
3040
3571
|
name: "schema",
|
|
3041
3572
|
tool: "kb_schema",
|
|
3042
3573
|
usage: "schema",
|
|
3043
|
-
description: "JSON Schema for
|
|
3574
|
+
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3044
3575
|
input: import_zod24.z.object({}),
|
|
3045
3576
|
fromArgv: () => ({}),
|
|
3046
3577
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
@@ -3052,7 +3583,7 @@ var statusCommand = define({
|
|
|
3052
3583
|
name: "status",
|
|
3053
3584
|
tool: "kb_status",
|
|
3054
3585
|
usage: "status <concept-id> <status>",
|
|
3055
|
-
description: "Move a record's status
|
|
3586
|
+
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3056
3587
|
input: import_zod25.z.object({
|
|
3057
3588
|
bundlePath,
|
|
3058
3589
|
conceptId,
|
|
@@ -3076,7 +3607,7 @@ var supersedeCommand = define({
|
|
|
3076
3607
|
name: "supersede",
|
|
3077
3608
|
tool: "kb_supersede",
|
|
3078
3609
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3079
|
-
description: "Mark a record superseded by another,
|
|
3610
|
+
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3080
3611
|
input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3081
3612
|
fromArgv: (argv, path) => ({
|
|
3082
3613
|
bundlePath: path,
|
|
@@ -3095,7 +3626,7 @@ var import_zod27 = require("zod");
|
|
|
3095
3626
|
var syncInstructionsCommand = define({
|
|
3096
3627
|
name: "sync-instructions",
|
|
3097
3628
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3098
|
-
description: "
|
|
3629
|
+
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3099
3630
|
input: import_zod27.z.object({
|
|
3100
3631
|
file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
|
|
3101
3632
|
budgetTokens: import_zod27.z.number().int().positive().optional(),
|
|
@@ -3181,7 +3712,7 @@ var traceCommand = define({
|
|
|
3181
3712
|
name: "trace",
|
|
3182
3713
|
tool: "kb_trace",
|
|
3183
3714
|
usage: "trace <concept-id> [edges...]",
|
|
3184
|
-
description: '
|
|
3715
|
+
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".',
|
|
3185
3716
|
input: import_zod28.z.object({
|
|
3186
3717
|
bundlePath,
|
|
3187
3718
|
conceptId,
|
|
@@ -3225,7 +3756,7 @@ var unpinCommand = define({
|
|
|
3225
3756
|
name: "unpin",
|
|
3226
3757
|
tool: "kb_unpin",
|
|
3227
3758
|
usage: "unpin [bundle-path]",
|
|
3228
|
-
description: "Remove a base from every
|
|
3759
|
+
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3229
3760
|
input: import_zod30.z.object({ bundlePath }),
|
|
3230
3761
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3231
3762
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
@@ -3253,7 +3784,7 @@ var verifyCommand = define({
|
|
|
3253
3784
|
name: "verify",
|
|
3254
3785
|
tool: "kb_verify",
|
|
3255
3786
|
usage: "verify <concept-id> --note <text>",
|
|
3256
|
-
description: "Append
|
|
3787
|
+
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.",
|
|
3257
3788
|
input: import_zod32.z.object({
|
|
3258
3789
|
bundlePath,
|
|
3259
3790
|
conceptId,
|
|
@@ -3282,15 +3813,7 @@ var writeCommand = define({
|
|
|
3282
3813
|
name: "write",
|
|
3283
3814
|
tool: "kb_write",
|
|
3284
3815
|
usage: "write <type> < record.json",
|
|
3285
|
-
description:
|
|
3286
|
-
"Write one record. Search first \u2014 the same knowledge filed twice under different slugs is how a base rots, and a duplicate concept id is rejected rather than overwritten. Call kb_types for the sections each type accepts.",
|
|
3287
|
-
"",
|
|
3288
|
-
"Judgment the tool cannot enforce for you:",
|
|
3289
|
-
"- An unsourced claim is an `assumption` record with assumption: true, never a `fact` with a vague source. The distinction is what lets a later reader separate what was established from what was guessed.",
|
|
3290
|
-
"- When two records conflict, say so in a `risk`, an `open-question`, or a superseding `decision`. Quietly picking a winner destroys the disagreement, which is usually the useful part.",
|
|
3291
|
-
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3292
|
-
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3293
|
-
].join("\n"),
|
|
3816
|
+
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.",
|
|
3294
3817
|
input: import_zod33.z.object({
|
|
3295
3818
|
bundlePath,
|
|
3296
3819
|
type: import_zod33.z.enum(KB_RECORD_TYPES),
|
|
@@ -3322,14 +3845,7 @@ var writeDecisionCommand = define({
|
|
|
3322
3845
|
name: "write-decision",
|
|
3323
3846
|
tool: "kb_write_decision",
|
|
3324
3847
|
usage: "write-decision < decision.json",
|
|
3325
|
-
description:
|
|
3326
|
-
"Write a decision. Takes `alternative` and `impact` as fields rather than free sections, because what was rejected is the part a later reader cannot reconstruct from the code \u2014 a heading is too easy to leave empty.",
|
|
3327
|
-
"",
|
|
3328
|
-
"What belongs in one:",
|
|
3329
|
-
'- Record a decision when a later reader would otherwise "simplify" the constraint away. If the diff already answers the question, there is nothing here to write.',
|
|
3330
|
-
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3331
|
-
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3332
|
-
].join("\n"),
|
|
3848
|
+
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.",
|
|
3333
3849
|
input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3334
3850
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3335
3851
|
bundlePath: path,
|
|
@@ -3386,8 +3902,8 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
3386
3902
|
|
|
3387
3903
|
// src/kb-store.ts
|
|
3388
3904
|
var import_node_crypto2 = require("crypto");
|
|
3389
|
-
var
|
|
3390
|
-
var
|
|
3905
|
+
var import_promises6 = require("fs/promises");
|
|
3906
|
+
var import_node_path8 = require("path");
|
|
3391
3907
|
|
|
3392
3908
|
// src/markdown.ts
|
|
3393
3909
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -3415,8 +3931,8 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
3415
3931
|
}
|
|
3416
3932
|
|
|
3417
3933
|
// src/search-index.ts
|
|
3418
|
-
var
|
|
3419
|
-
var
|
|
3934
|
+
var import_promises5 = require("fs/promises");
|
|
3935
|
+
var import_node_path7 = require("path");
|
|
3420
3936
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
3421
3937
|
var COLLECTION = "kb";
|
|
3422
3938
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -3425,7 +3941,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3425
3941
|
let store = null;
|
|
3426
3942
|
try {
|
|
3427
3943
|
store = await qmd.createStore({
|
|
3428
|
-
dbPath: (0,
|
|
3944
|
+
dbPath: (0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
3429
3945
|
config: {
|
|
3430
3946
|
collections: {
|
|
3431
3947
|
[COLLECTION]: {
|
|
@@ -3460,7 +3976,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3460
3976
|
}
|
|
3461
3977
|
}
|
|
3462
3978
|
async function isStale(bundlePath2) {
|
|
3463
|
-
const indexAt = await (0,
|
|
3979
|
+
const indexAt = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3464
3980
|
if (!indexAt) return true;
|
|
3465
3981
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3466
3982
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -3469,7 +3985,7 @@ async function isStale(bundlePath2) {
|
|
|
3469
3985
|
let stale = false;
|
|
3470
3986
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3471
3987
|
if (stale) return;
|
|
3472
|
-
const at = await (0,
|
|
3988
|
+
const at = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3473
3989
|
if (at > indexAt) stale = true;
|
|
3474
3990
|
});
|
|
3475
3991
|
return stale;
|
|
@@ -3747,7 +4263,7 @@ function appendUnionMergeLine(contents) {
|
|
|
3747
4263
|
}
|
|
3748
4264
|
|
|
3749
4265
|
// src/kb-store.ts
|
|
3750
|
-
var KB_DIR = (0,
|
|
4266
|
+
var KB_DIR = (0, import_node_path8.join)(".strauss", "kb");
|
|
3751
4267
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
3752
4268
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
3753
4269
|
var KbStore = class {
|
|
@@ -3778,7 +4294,7 @@ var KbStore = class {
|
|
|
3778
4294
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
3779
4295
|
const root = this.root(bundlePath2);
|
|
3780
4296
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3781
|
-
await (0,
|
|
4297
|
+
await (0, import_promises6.mkdir)(root, { recursive: true });
|
|
3782
4298
|
await this.publish(
|
|
3783
4299
|
target,
|
|
3784
4300
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -3817,7 +4333,7 @@ var KbStore = class {
|
|
|
3817
4333
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3818
4334
|
let raw;
|
|
3819
4335
|
try {
|
|
3820
|
-
raw = await (0,
|
|
4336
|
+
raw = await (0, import_promises6.readFile)(target, "utf8");
|
|
3821
4337
|
} catch {
|
|
3822
4338
|
return null;
|
|
3823
4339
|
}
|
|
@@ -3834,7 +4350,7 @@ var KbStore = class {
|
|
|
3834
4350
|
const root = this.root(bundlePath2);
|
|
3835
4351
|
let names;
|
|
3836
4352
|
try {
|
|
3837
|
-
names = await (0,
|
|
4353
|
+
names = await (0, import_promises6.readdir)(root);
|
|
3838
4354
|
} catch {
|
|
3839
4355
|
return [];
|
|
3840
4356
|
}
|
|
@@ -3842,7 +4358,7 @@ var KbStore = class {
|
|
|
3842
4358
|
const records = await mapLimit(
|
|
3843
4359
|
wanted,
|
|
3844
4360
|
DEFAULT_IO_CONCURRENCY,
|
|
3845
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
4361
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path8.join)(root, name), "utf8"))
|
|
3846
4362
|
);
|
|
3847
4363
|
return records.filter((record) => record !== null);
|
|
3848
4364
|
}
|
|
@@ -4010,7 +4526,8 @@ ${answer}
|
|
|
4010
4526
|
* at the repo root, and the MCP server's cwd is the workspace.
|
|
4011
4527
|
*
|
|
4012
4528
|
* Public because `doctor` needs the same map with the same degradation: a
|
|
4013
|
-
* sweep that failed to read the tree should report no drift, not fail
|
|
4529
|
+
* sweep that failed to read the tree should report no drift, not fail — and
|
|
4530
|
+
* `offline: false` there, because a sweep is worth a fetch.
|
|
4014
4531
|
*
|
|
4015
4532
|
* When no root was given and not one anchored file was found, the finding is
|
|
4016
4533
|
* discarded. A base read from somewhere other than the tree it describes
|
|
@@ -4022,10 +4539,13 @@ ${answer}
|
|
|
4022
4539
|
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
4023
4540
|
* taken at its word either way.
|
|
4024
4541
|
*/
|
|
4025
|
-
async detectDrift(records, repoRoot) {
|
|
4542
|
+
async detectDrift(records, repoRoot, options = {}) {
|
|
4026
4543
|
try {
|
|
4027
4544
|
const drift = await detectAnchorDrift(records, {
|
|
4028
|
-
repoRoot: repoRoot ?? process.cwd()
|
|
4545
|
+
repoRoot: repoRoot ?? process.cwd(),
|
|
4546
|
+
// Offline by default: a read path must never spend a network fetch per
|
|
4547
|
+
// call. `doctor` and `anchor-resolve` are the verbs that go get it.
|
|
4548
|
+
remote: { offline: options.offline !== false }
|
|
4029
4549
|
});
|
|
4030
4550
|
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
4031
4551
|
this.logger.warn?.({
|
|
@@ -4143,11 +4663,11 @@ ${answer}
|
|
|
4143
4663
|
async readIndex(bundlePath2) {
|
|
4144
4664
|
const root = this.root(bundlePath2);
|
|
4145
4665
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4146
|
-
const stored = await (0,
|
|
4666
|
+
const stored = await (0, import_promises6.readFile)((0, import_node_path8.join)(root, INDEX_FILE), "utf8").catch(
|
|
4147
4667
|
() => null
|
|
4148
4668
|
);
|
|
4149
4669
|
if (indexIsStale(stored, expected)) {
|
|
4150
|
-
await this.publish((0,
|
|
4670
|
+
await this.publish((0, import_node_path8.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4151
4671
|
this.logger.info?.({
|
|
4152
4672
|
operation: "kb.index.repair",
|
|
4153
4673
|
bundlePath: root,
|
|
@@ -4164,8 +4684,8 @@ ${answer}
|
|
|
4164
4684
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4165
4685
|
*/
|
|
4166
4686
|
async readLog(bundlePath2) {
|
|
4167
|
-
const raw = await (0,
|
|
4168
|
-
(0,
|
|
4687
|
+
const raw = await (0, import_promises6.readFile)(
|
|
4688
|
+
(0, import_node_path8.join)(this.root(bundlePath2), LOG_FILE),
|
|
4169
4689
|
"utf8"
|
|
4170
4690
|
).catch(() => "");
|
|
4171
4691
|
const result = parseLog(raw);
|
|
@@ -4216,14 +4736,14 @@ ${answer}
|
|
|
4216
4736
|
}
|
|
4217
4737
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4218
4738
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4219
|
-
const before = await (0,
|
|
4739
|
+
const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
4220
4740
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4221
4741
|
const parsed = this.parse(conceptId2, before);
|
|
4222
4742
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4223
4743
|
const frontmatter = change(parsed.frontmatter);
|
|
4224
4744
|
const body = changeBody(parsed.body);
|
|
4225
4745
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4226
|
-
const witness = await (0,
|
|
4746
|
+
const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
4227
4747
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
4228
4748
|
throw new KbWriteConflictError(conceptId2);
|
|
4229
4749
|
}
|
|
@@ -4249,20 +4769,20 @@ ${answer}
|
|
|
4249
4769
|
*/
|
|
4250
4770
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4251
4771
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4252
|
-
await (0,
|
|
4772
|
+
await (0, import_promises6.writeFile)(staging, contents, "utf8");
|
|
4253
4773
|
try {
|
|
4254
4774
|
if (overwrite) {
|
|
4255
|
-
await (0,
|
|
4775
|
+
await (0, import_promises6.rename)(staging, target);
|
|
4256
4776
|
return;
|
|
4257
4777
|
}
|
|
4258
|
-
await (0,
|
|
4778
|
+
await (0, import_promises6.link)(staging, target);
|
|
4259
4779
|
} catch (error) {
|
|
4260
4780
|
if (error.code === "EEXIST") {
|
|
4261
4781
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
4262
4782
|
}
|
|
4263
4783
|
throw error;
|
|
4264
4784
|
} finally {
|
|
4265
|
-
await (0,
|
|
4785
|
+
await (0, import_promises6.unlink)(staging).catch(() => void 0);
|
|
4266
4786
|
}
|
|
4267
4787
|
}
|
|
4268
4788
|
/**
|
|
@@ -4306,18 +4826,18 @@ ${answer}
|
|
|
4306
4826
|
* file must not fail the mutation it guards.
|
|
4307
4827
|
*/
|
|
4308
4828
|
async ensureGitattributes(root) {
|
|
4309
|
-
const target = (0,
|
|
4829
|
+
const target = (0, import_node_path8.join)(root, GITATTRIBUTES_FILE);
|
|
4310
4830
|
try {
|
|
4311
4831
|
let existing;
|
|
4312
4832
|
try {
|
|
4313
|
-
existing = await (0,
|
|
4833
|
+
existing = await (0, import_promises6.readFile)(target, "utf8");
|
|
4314
4834
|
} catch (error) {
|
|
4315
4835
|
if (error.code !== "ENOENT") throw error;
|
|
4316
4836
|
existing = null;
|
|
4317
4837
|
}
|
|
4318
4838
|
if (existing === null) {
|
|
4319
4839
|
try {
|
|
4320
|
-
await (0,
|
|
4840
|
+
await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
|
|
4321
4841
|
encoding: "utf8",
|
|
4322
4842
|
flag: "wx"
|
|
4323
4843
|
});
|
|
@@ -4338,7 +4858,7 @@ ${answer}
|
|
|
4338
4858
|
return;
|
|
4339
4859
|
}
|
|
4340
4860
|
if (!hasMergeDeclaration(existing)) {
|
|
4341
|
-
await (0,
|
|
4861
|
+
await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
4342
4862
|
this.logger.info?.({
|
|
4343
4863
|
operation: "kb.gitattributes.ensure",
|
|
4344
4864
|
bundlePath: root,
|
|
@@ -4357,7 +4877,7 @@ ${answer}
|
|
|
4357
4877
|
async record(root, entry) {
|
|
4358
4878
|
await this.ensureGitattributes(root);
|
|
4359
4879
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4360
|
-
await (0,
|
|
4880
|
+
await (0, import_promises6.appendFile)((0, import_node_path8.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
4361
4881
|
this.logger.warn?.({
|
|
4362
4882
|
operation: "kb.log.append",
|
|
4363
4883
|
outcome: "failed",
|
|
@@ -4383,18 +4903,18 @@ ${answer}
|
|
|
4383
4903
|
};
|
|
4384
4904
|
}
|
|
4385
4905
|
root(bundlePath2) {
|
|
4386
|
-
return (0,
|
|
4906
|
+
return (0, import_node_path8.resolve)(bundlePath2);
|
|
4387
4907
|
}
|
|
4388
4908
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
4389
4909
|
// bundle root; anything carrying a separator would escape it.
|
|
4390
4910
|
recordPath(bundlePath2, conceptId2) {
|
|
4391
|
-
if (conceptId2.includes(
|
|
4911
|
+
if (conceptId2.includes(import_node_path8.sep) || conceptId2.includes("/")) {
|
|
4392
4912
|
throw new KbInvalidConceptIdError(
|
|
4393
4913
|
"concept id must not contain a path separator",
|
|
4394
4914
|
{ conceptId: conceptId2 }
|
|
4395
4915
|
);
|
|
4396
4916
|
}
|
|
4397
|
-
return (0,
|
|
4917
|
+
return (0, import_node_path8.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
4398
4918
|
}
|
|
4399
4919
|
};
|
|
4400
4920
|
function estimateTokens(record) {
|
|
@@ -4453,7 +4973,7 @@ function bundleDigest(records, superseded) {
|
|
|
4453
4973
|
}
|
|
4454
4974
|
|
|
4455
4975
|
// src/version.ts
|
|
4456
|
-
var VERSION = true ? "0.1.
|
|
4976
|
+
var VERSION = true ? "0.1.15" : "0.0.0-dev";
|
|
4457
4977
|
|
|
4458
4978
|
// src/cli.ts
|
|
4459
4979
|
async function runKbCli(argv) {
|
|
@@ -4512,7 +5032,7 @@ function takeLiteral(argv) {
|
|
|
4512
5032
|
function takeBundle(argv) {
|
|
4513
5033
|
const at = argv.indexOf("--bundle");
|
|
4514
5034
|
if (at === -1) {
|
|
4515
|
-
return { bundle: (0,
|
|
5035
|
+
return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), rest: argv };
|
|
4516
5036
|
}
|
|
4517
5037
|
const bundle = argv[at + 1];
|
|
4518
5038
|
if (!bundle) die("--bundle requires a path");
|