@mrciphersmith/keryx 0.2.106 → 0.2.107
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/README.md +6 -1
- package/dist/cli.js +341 -16
- package/package.json +1 -1
- package/src/gdskills/bundled/skills/review/review-layout/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +2 -2
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +8 -0
- package/src/gdskills/contracts/review-finding.schema.json +34 -1
package/README.md
CHANGED
|
@@ -325,7 +325,12 @@ Grouped by what you are trying to do, not by internal module layout.
|
|
|
325
325
|
trends.
|
|
326
326
|
- **review** — managed review packages, standalone under `.metaproject/reviews/`
|
|
327
327
|
or inside the flow package when attached to a flow, so review findings become
|
|
328
|
-
durable project artifacts.
|
|
328
|
+
durable project artifacts. A finding points at the code it **quotes**: the
|
|
329
|
+
line is derived by locating that quote at the commit the round records, not
|
|
330
|
+
taken on the reviewer's word, and a quote that cannot be found is marked
|
|
331
|
+
unlocatable rather than carrying a number nobody checked. Each round also
|
|
332
|
+
states its price — estimated before dispatch, recorded after, and divided by
|
|
333
|
+
the findings that survived it.
|
|
329
334
|
|
|
330
335
|
**Operate agents**
|
|
331
336
|
|
package/dist/cli.js
CHANGED
|
@@ -48579,7 +48579,7 @@ import path47 from "path";
|
|
|
48579
48579
|
// package.json
|
|
48580
48580
|
var package_default = {
|
|
48581
48581
|
name: "@mrciphersmith/keryx",
|
|
48582
|
-
version: "0.2.
|
|
48582
|
+
version: "0.2.107",
|
|
48583
48583
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
48584
48584
|
private: false,
|
|
48585
48585
|
publishConfig: {
|
|
@@ -62741,7 +62741,7 @@ ${learningSourceLessons(selection).map((lesson) => `- ${lesson}
|
|
|
62741
62741
|
|
|
62742
62742
|
// src/review/managed.ts
|
|
62743
62743
|
init_validator();
|
|
62744
|
-
import { mkdir as mkdir53, readFile as readFile86, readdir as readdir28, rm as rm11 } from "fs/promises";
|
|
62744
|
+
import { mkdir as mkdir53, readFile as readFile86, readdir as readdir28, realpath as realpath8, rm as rm11, stat as stat16 } from "fs/promises";
|
|
62745
62745
|
import path153 from "path";
|
|
62746
62746
|
init_fs();
|
|
62747
62747
|
init_store2();
|
|
@@ -64692,6 +64692,163 @@ function renderFilterStatsLine(stats) {
|
|
|
64692
64692
|
return `filter_stats: total=${show(stats.total)} prefilter=${show(stats.dropped_prefilter)} low_confidence=${show(stats.dropped_low_confidence)} refuted=${show(stats.dropped_refuted)} scope_b=${show(stats.dropped_scope_b)} findings_cap=${show(stats.dropped_findings_cap)} dismissed_by_round=${show(stats.dismissed_by_round)} retained=${show(stats.retained)}`;
|
|
64693
64693
|
}
|
|
64694
64694
|
|
|
64695
|
+
// src/review/locate.ts
|
|
64696
|
+
var MAX_QUOTE_LINES = 200;
|
|
64697
|
+
var MAX_LOCATE_FILE_LINES = 50000;
|
|
64698
|
+
var MAX_LOCATE_FILE_BYTES = 4000000;
|
|
64699
|
+
function quoteLines(quote) {
|
|
64700
|
+
const lines = quote.replace(/\r\n/g, `
|
|
64701
|
+
`).split(`
|
|
64702
|
+
`);
|
|
64703
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
64704
|
+
lines.shift();
|
|
64705
|
+
}
|
|
64706
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
64707
|
+
lines.pop();
|
|
64708
|
+
}
|
|
64709
|
+
if (lines.length === 0) {
|
|
64710
|
+
return [];
|
|
64711
|
+
}
|
|
64712
|
+
const indents = lines.filter((line) => line.trim() !== "").map((line) => (line.match(/^[ \t]*/)?.[0] ?? "").length);
|
|
64713
|
+
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
64714
|
+
return lines.map((line) => line.slice(common));
|
|
64715
|
+
}
|
|
64716
|
+
function normalise(line) {
|
|
64717
|
+
return line.trim().replace(/\s+/g, " ");
|
|
64718
|
+
}
|
|
64719
|
+
function deindent(lines) {
|
|
64720
|
+
const indents = lines.filter((line) => line.trim() !== "").map((line) => (line.match(/^[ \t]*/)?.[0] ?? "").length);
|
|
64721
|
+
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
64722
|
+
return lines.map((line) => line.slice(common));
|
|
64723
|
+
}
|
|
64724
|
+
var MAX_HITS = 2;
|
|
64725
|
+
function findRuns(haystack, needle, shape) {
|
|
64726
|
+
const hits = [];
|
|
64727
|
+
if (needle.length === 0 || needle.length > haystack.length) {
|
|
64728
|
+
return hits;
|
|
64729
|
+
}
|
|
64730
|
+
for (let start = 0;start + needle.length <= haystack.length; start += 1) {
|
|
64731
|
+
const window = shape(haystack.slice(start, start + needle.length));
|
|
64732
|
+
if (window.every((line, offset) => line === needle[offset])) {
|
|
64733
|
+
hits.push(start + 1);
|
|
64734
|
+
if (hits.length >= MAX_HITS) {
|
|
64735
|
+
return hits;
|
|
64736
|
+
}
|
|
64737
|
+
}
|
|
64738
|
+
}
|
|
64739
|
+
return hits;
|
|
64740
|
+
}
|
|
64741
|
+
function ambiguity(hits, pass) {
|
|
64742
|
+
return {
|
|
64743
|
+
state: "unlocatable",
|
|
64744
|
+
reason: `the quote matches at least ${hits.length} places${pass}(lines ${hits.join(", ")}, and the scan stopped there); an anchor chosen among them would be a guess`
|
|
64745
|
+
};
|
|
64746
|
+
}
|
|
64747
|
+
function locateQuote(fileText, quote) {
|
|
64748
|
+
const needle = quoteLines(quote);
|
|
64749
|
+
if (needle.length === 0) {
|
|
64750
|
+
return { state: "unlocatable", reason: "the quote is empty" };
|
|
64751
|
+
}
|
|
64752
|
+
if (needle.length > MAX_QUOTE_LINES) {
|
|
64753
|
+
return {
|
|
64754
|
+
state: "unlocatable",
|
|
64755
|
+
reason: `the quote is ${needle.length} lines, past the ${MAX_QUOTE_LINES}-line bound; quote the few lines the finding is about`
|
|
64756
|
+
};
|
|
64757
|
+
}
|
|
64758
|
+
const haystack = fileText.replace(/\r\n/g, `
|
|
64759
|
+
`).split(`
|
|
64760
|
+
`);
|
|
64761
|
+
if (haystack.length > MAX_LOCATE_FILE_LINES) {
|
|
64762
|
+
return {
|
|
64763
|
+
state: "unlocatable",
|
|
64764
|
+
reason: `the file is ${haystack.length} lines, past the ${MAX_LOCATE_FILE_LINES}-line bound for locating`
|
|
64765
|
+
};
|
|
64766
|
+
}
|
|
64767
|
+
const exact = findRuns(haystack, deindent(needle).map((line) => line.trimEnd()), (window) => deindent(window).map((line) => line.trimEnd()));
|
|
64768
|
+
if (exact.length === 1) {
|
|
64769
|
+
return { state: "derived", method: "exact", line: exact[0] };
|
|
64770
|
+
}
|
|
64771
|
+
if (exact.length > 1) {
|
|
64772
|
+
return ambiguity(exact, " in the file ");
|
|
64773
|
+
}
|
|
64774
|
+
const loose = findRuns(haystack, needle.map(normalise), (window) => window.map(normalise));
|
|
64775
|
+
if (loose.length === 1) {
|
|
64776
|
+
return { state: "derived", method: "whitespace-normalised", line: loose[0] };
|
|
64777
|
+
}
|
|
64778
|
+
if (loose.length > 1) {
|
|
64779
|
+
return ambiguity(loose, " once whitespace is normalised ");
|
|
64780
|
+
}
|
|
64781
|
+
return { state: "unlocatable", reason: "the quote does not appear in the file" };
|
|
64782
|
+
}
|
|
64783
|
+
async function locateFinding(finding, readFile) {
|
|
64784
|
+
const quote = typeof finding.quote === "string" ? finding.quote : "";
|
|
64785
|
+
if (quote.trim() === "") {
|
|
64786
|
+
return;
|
|
64787
|
+
}
|
|
64788
|
+
const reported = typeof finding.line === "number" ? finding.line : null;
|
|
64789
|
+
const file = typeof finding.file === "string" ? finding.file : "";
|
|
64790
|
+
if (file === "") {
|
|
64791
|
+
return { state: "unlocatable", reason: "the finding quotes code but names no file", reported_line: reported };
|
|
64792
|
+
}
|
|
64793
|
+
const text = await readFile(file);
|
|
64794
|
+
if (text === null) {
|
|
64795
|
+
return { state: "unlocatable", reason: `no such file at this round's head: ${file}`, reported_line: reported };
|
|
64796
|
+
}
|
|
64797
|
+
const outcome = locateQuote(text, quote);
|
|
64798
|
+
return outcome.state === "derived" ? { state: "derived", method: outcome.method, line: outcome.line, reported_line: reported } : { state: "unlocatable", reason: outcome.reason, reported_line: reported };
|
|
64799
|
+
}
|
|
64800
|
+
|
|
64801
|
+
// src/review/repair.ts
|
|
64802
|
+
var REPAIRABLE = new Set(["id", "problem"]);
|
|
64803
|
+
function isBlank(value) {
|
|
64804
|
+
return typeof value !== "string" || value.trim() === "";
|
|
64805
|
+
}
|
|
64806
|
+
function mintId(index, taken) {
|
|
64807
|
+
let n = index + 1;
|
|
64808
|
+
let candidate = `F-${String(n).padStart(3, "0")}`;
|
|
64809
|
+
while (taken.has(candidate)) {
|
|
64810
|
+
n += 1;
|
|
64811
|
+
candidate = `F-${String(n).padStart(3, "0")}`;
|
|
64812
|
+
}
|
|
64813
|
+
return candidate;
|
|
64814
|
+
}
|
|
64815
|
+
function acceptRepair(before, after) {
|
|
64816
|
+
if (before.length !== after.length) {
|
|
64817
|
+
throw new Error(`Refusing a repair that changed the number of findings: ${before.length} in, ${after.length} out. A repair may fill fields in; it may never add or drop a finding.`);
|
|
64818
|
+
}
|
|
64819
|
+
for (const [index, finding] of after.entries()) {
|
|
64820
|
+
const had = before[index];
|
|
64821
|
+
const keys = new Set(Object.keys(finding));
|
|
64822
|
+
const illegal = [...keys].filter((key) => !had.has(key) && !REPAIRABLE.has(key));
|
|
64823
|
+
if (illegal.length > 0) {
|
|
64824
|
+
throw new Error(`Refusing a repair that wrote ${illegal.join(", ")} onto finding ${index + 1}: only ${[...REPAIRABLE].join(" and ")} may be filled in. Everything else is a claim the reviewer has to make.`);
|
|
64825
|
+
}
|
|
64826
|
+
const removed = [...had].filter((key) => !keys.has(key));
|
|
64827
|
+
if (removed.length > 0) {
|
|
64828
|
+
throw new Error(`Refusing a repair that removed ${removed.join(", ")} from finding ${index + 1}: a repair may fill a field in; it may never clear one.`);
|
|
64829
|
+
}
|
|
64830
|
+
}
|
|
64831
|
+
}
|
|
64832
|
+
function repairMechanicalOmissions(findings) {
|
|
64833
|
+
const before = findings.map((finding) => new Set(Object.keys(finding)));
|
|
64834
|
+
const taken = new Set(findings.map((finding) => finding.id).filter((id) => !isBlank(id)));
|
|
64835
|
+
const repairs = [];
|
|
64836
|
+
for (const [index, finding] of findings.entries()) {
|
|
64837
|
+
if (isBlank(finding.id)) {
|
|
64838
|
+
const minted = mintId(index, taken);
|
|
64839
|
+
taken.add(minted);
|
|
64840
|
+
finding.id = minted;
|
|
64841
|
+
repairs.push({ finding: minted, field: "id", source: `position ${index + 1} in the report` });
|
|
64842
|
+
}
|
|
64843
|
+
if (isBlank(finding.problem) && !isBlank(finding["title"])) {
|
|
64844
|
+
finding.problem = String(finding["title"]).trim();
|
|
64845
|
+
repairs.push({ finding: finding.id, field: "problem", source: "the finding's own title" });
|
|
64846
|
+
}
|
|
64847
|
+
}
|
|
64848
|
+
acceptRepair(before, findings);
|
|
64849
|
+
return repairs;
|
|
64850
|
+
}
|
|
64851
|
+
|
|
64695
64852
|
// src/review/managed.ts
|
|
64696
64853
|
init_review_notes();
|
|
64697
64854
|
init_types3();
|
|
@@ -64761,6 +64918,9 @@ async function createManagedReviewPackage(input) {
|
|
|
64761
64918
|
source: input.findings,
|
|
64762
64919
|
reviewers
|
|
64763
64920
|
});
|
|
64921
|
+
const repairs = repairMechanicalOmissions(reported);
|
|
64922
|
+
const head = await resolveTargetHead(input);
|
|
64923
|
+
await anchorFindings(reported, input, head);
|
|
64764
64924
|
assignGlobalIds(reported, reviewId);
|
|
64765
64925
|
const merged = mergeVerifications(reported, input.verifications ?? [], {
|
|
64766
64926
|
mode: input.verificationMode ?? DEFAULT_VERIFICATION_MODE
|
|
@@ -64796,7 +64956,6 @@ async function createManagedReviewPackage(input) {
|
|
|
64796
64956
|
externalRetained: scoped.external.length,
|
|
64797
64957
|
...input.refuted === undefined ? {} : { roundDismissed: roundDismissed.length }
|
|
64798
64958
|
});
|
|
64799
|
-
const head = await resolveTargetHead(input);
|
|
64800
64959
|
const manifest = buildManifest2({
|
|
64801
64960
|
input,
|
|
64802
64961
|
target: targetWithHead(input.target, head),
|
|
@@ -64805,7 +64964,8 @@ async function createManagedReviewPackage(input) {
|
|
|
64805
64964
|
flowMatch,
|
|
64806
64965
|
coverage,
|
|
64807
64966
|
at,
|
|
64808
|
-
filterStats
|
|
64967
|
+
filterStats,
|
|
64968
|
+
repairs
|
|
64809
64969
|
});
|
|
64810
64970
|
const validation = await validateManagedReviewManifest(input.cwd, manifest);
|
|
64811
64971
|
if (!validation.valid) {
|
|
@@ -65067,6 +65227,8 @@ function buildManifest2(args) {
|
|
|
65067
65227
|
},
|
|
65068
65228
|
coverage: args.coverage,
|
|
65069
65229
|
filter_stats: args.filterStats,
|
|
65230
|
+
...args.repairs.length === 0 ? {} : { repairs: args.repairs },
|
|
65231
|
+
...args.input.cost === undefined ? {} : { cost: args.input.cost },
|
|
65070
65232
|
...crossFamily === undefined ? {} : { cross_family_review: crossFamily },
|
|
65071
65233
|
createdAt: args.at,
|
|
65072
65234
|
updatedAt: args.at
|
|
@@ -65132,6 +65294,80 @@ function triage(findings, args) {
|
|
|
65132
65294
|
return triaged;
|
|
65133
65295
|
});
|
|
65134
65296
|
}
|
|
65297
|
+
async function anchorFindings(findings, input, head) {
|
|
65298
|
+
const read = input.readTreeFile ?? (head === null ? readFromWorkingTree(input) : async (relative) => {
|
|
65299
|
+
const contained = await containedRelativePath(input.cwd, relative);
|
|
65300
|
+
return contained === null ? null : readFileAtCommit(input.cwd, head, contained);
|
|
65301
|
+
});
|
|
65302
|
+
for (const finding of findings) {
|
|
65303
|
+
const locator = await locateFinding(finding, read);
|
|
65304
|
+
if (locator === undefined) {
|
|
65305
|
+
delete finding.locator;
|
|
65306
|
+
continue;
|
|
65307
|
+
}
|
|
65308
|
+
if (locator.state === "derived") {
|
|
65309
|
+
finding.line = locator.line;
|
|
65310
|
+
finding.locator = {
|
|
65311
|
+
state: "derived",
|
|
65312
|
+
method: locator.method,
|
|
65313
|
+
...locator.reported_line === undefined ? {} : { reported_line: locator.reported_line }
|
|
65314
|
+
};
|
|
65315
|
+
continue;
|
|
65316
|
+
}
|
|
65317
|
+
finding.line = null;
|
|
65318
|
+
finding.locator = {
|
|
65319
|
+
state: "unlocatable",
|
|
65320
|
+
reason: locator.reason,
|
|
65321
|
+
...locator.reported_line === undefined ? {} : { reported_line: locator.reported_line }
|
|
65322
|
+
};
|
|
65323
|
+
}
|
|
65324
|
+
}
|
|
65325
|
+
async function containedRelativePath(cwd, relative) {
|
|
65326
|
+
const root = await realpath8(cwd).catch(() => path153.resolve(cwd));
|
|
65327
|
+
const resolved = path153.resolve(root, relative);
|
|
65328
|
+
const real = await realpath8(resolved).catch(() => null);
|
|
65329
|
+
if (real === null || real !== root && !real.startsWith(`${root}${path153.sep}`)) {
|
|
65330
|
+
return null;
|
|
65331
|
+
}
|
|
65332
|
+
return real;
|
|
65333
|
+
}
|
|
65334
|
+
async function readFileAtCommit(cwd, commit, absolute) {
|
|
65335
|
+
const relative = path153.relative(await realpath8(cwd).catch(() => path153.resolve(cwd)), absolute);
|
|
65336
|
+
try {
|
|
65337
|
+
const proc = Bun.spawn(["git", "show", `${commit}:${relative}`], {
|
|
65338
|
+
cwd,
|
|
65339
|
+
stdout: "pipe",
|
|
65340
|
+
stderr: "ignore",
|
|
65341
|
+
stdin: "ignore"
|
|
65342
|
+
});
|
|
65343
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
65344
|
+
if (exitCode !== 0 || stdout.length > MAX_LOCATE_FILE_BYTES) {
|
|
65345
|
+
return null;
|
|
65346
|
+
}
|
|
65347
|
+
return stdout;
|
|
65348
|
+
} catch {
|
|
65349
|
+
return null;
|
|
65350
|
+
}
|
|
65351
|
+
}
|
|
65352
|
+
function readFromWorkingTree(input) {
|
|
65353
|
+
return async (relative) => {
|
|
65354
|
+
const root = await realpath8(input.cwd).catch(() => path153.resolve(input.cwd));
|
|
65355
|
+
const resolved = path153.resolve(root, relative);
|
|
65356
|
+
const real = await realpath8(resolved).catch(() => null);
|
|
65357
|
+
if (real === null || real !== root && !real.startsWith(`${root}${path153.sep}`)) {
|
|
65358
|
+
return null;
|
|
65359
|
+
}
|
|
65360
|
+
try {
|
|
65361
|
+
const info = await stat16(real);
|
|
65362
|
+
if (!info.isFile() || info.size > MAX_LOCATE_FILE_BYTES) {
|
|
65363
|
+
return null;
|
|
65364
|
+
}
|
|
65365
|
+
return await readFile86(real, "utf8");
|
|
65366
|
+
} catch {
|
|
65367
|
+
return null;
|
|
65368
|
+
}
|
|
65369
|
+
};
|
|
65370
|
+
}
|
|
65135
65371
|
function toContractFinding(finding) {
|
|
65136
65372
|
const record = {
|
|
65137
65373
|
id: finding.id,
|
|
@@ -65149,6 +65385,12 @@ function toContractFinding(finding) {
|
|
|
65149
65385
|
if (finding.line !== undefined) {
|
|
65150
65386
|
record.line = finding.line;
|
|
65151
65387
|
}
|
|
65388
|
+
if (finding.quote !== undefined) {
|
|
65389
|
+
record.quote = finding.quote;
|
|
65390
|
+
}
|
|
65391
|
+
if (finding.locator !== undefined) {
|
|
65392
|
+
record.locator = finding.locator;
|
|
65393
|
+
}
|
|
65152
65394
|
if (finding.symbol !== undefined) {
|
|
65153
65395
|
record.symbol = finding.symbol;
|
|
65154
65396
|
}
|
|
@@ -65801,6 +66043,73 @@ async function loadDocpackSchema(cwd) {
|
|
|
65801
66043
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
65802
66044
|
}
|
|
65803
66045
|
|
|
66046
|
+
// src/review/cost.ts
|
|
66047
|
+
function estimateTokens2(text) {
|
|
66048
|
+
return Math.ceil(text.length / 4);
|
|
66049
|
+
}
|
|
66050
|
+
function grouped(value) {
|
|
66051
|
+
return value.toLocaleString("en-US");
|
|
66052
|
+
}
|
|
66053
|
+
function renderScopeEstimate(scopedDiff, reviewers) {
|
|
66054
|
+
const per = estimateTokens2(scopedDiff);
|
|
66055
|
+
const lines = [
|
|
66056
|
+
`### Estimated cost`,
|
|
66057
|
+
"",
|
|
66058
|
+
`- \u2248 ${grouped(per)} prompt tokens of scoped diff per reviewer (characters \xF7 4 \u2014 an estimate, not a measurement)`
|
|
66059
|
+
];
|
|
66060
|
+
if (reviewers > 0) {
|
|
66061
|
+
lines.push(`- \u2248 ${grouped(per * reviewers)} across ${reviewers} reviewer${reviewers === 1 ? "" : "s"}, before their own prompts, tools and any verifier wave`);
|
|
66062
|
+
} else {
|
|
66063
|
+
lines.push("- reviewer count not supplied, so the fan-out is not multiplied in \u2014 pass `--reviewers a,b` to see the round's total");
|
|
66064
|
+
}
|
|
66065
|
+
return lines.join(`
|
|
66066
|
+
`);
|
|
66067
|
+
}
|
|
66068
|
+
function costFrom(input) {
|
|
66069
|
+
const cost = {};
|
|
66070
|
+
if (typeof input.inputTokens === "number") {
|
|
66071
|
+
cost.input_tokens = input.inputTokens;
|
|
66072
|
+
}
|
|
66073
|
+
if (typeof input.outputTokens === "number") {
|
|
66074
|
+
cost.output_tokens = input.outputTokens;
|
|
66075
|
+
}
|
|
66076
|
+
if (typeof input.spentUsd === "number") {
|
|
66077
|
+
cost.spent_usd = input.spentUsd;
|
|
66078
|
+
}
|
|
66079
|
+
return Object.keys(cost).length === 0 ? undefined : cost;
|
|
66080
|
+
}
|
|
66081
|
+
function renderCostPerFinding(cost, retained) {
|
|
66082
|
+
if (cost === undefined) {
|
|
66083
|
+
return [
|
|
66084
|
+
"cost: not recorded",
|
|
66085
|
+
" Nothing reported what this round used. That is NOT zero: pass --tokens-in/--tokens-out",
|
|
66086
|
+
" or --spent at ingest so the price of a round is a number somebody can weigh."
|
|
66087
|
+
];
|
|
66088
|
+
}
|
|
66089
|
+
const tokens = cost.input_tokens === undefined && cost.output_tokens === undefined ? undefined : (cost.input_tokens ?? 0) + (cost.output_tokens ?? 0);
|
|
66090
|
+
const lines = [];
|
|
66091
|
+
if (tokens !== undefined) {
|
|
66092
|
+
lines.push(`tokens: ${grouped(tokens)}`);
|
|
66093
|
+
}
|
|
66094
|
+
if (cost.spent_usd !== undefined) {
|
|
66095
|
+
lines.push(`spent: ${cost.spent_usd} USD`);
|
|
66096
|
+
}
|
|
66097
|
+
lines.push(`retained findings: ${retained}`);
|
|
66098
|
+
if (retained === 0) {
|
|
66099
|
+
lines.push(" Nothing was retained, so there is no per-finding figure \u2014 only the bill.");
|
|
66100
|
+
return lines;
|
|
66101
|
+
}
|
|
66102
|
+
if (tokens !== undefined) {
|
|
66103
|
+
const per = tokens / retained;
|
|
66104
|
+
lines.push(per > 0 && Math.round(per) === 0 ? "per retained finding: < 1 token" : `per retained finding: ${grouped(Math.round(per))} tokens`);
|
|
66105
|
+
}
|
|
66106
|
+
if (cost.spent_usd !== undefined) {
|
|
66107
|
+
const per = cost.spent_usd / retained;
|
|
66108
|
+
lines.push(per > 0 && Number(per.toFixed(4)) === 0 ? "per retained finding: < 0.0001 USD" : `per retained finding: ${per.toFixed(4)} USD`);
|
|
66109
|
+
}
|
|
66110
|
+
return lines;
|
|
66111
|
+
}
|
|
66112
|
+
|
|
65804
66113
|
// src/review/reviewers.ts
|
|
65805
66114
|
init_project_skills();
|
|
65806
66115
|
import { readFile as readFile87, readdir as readdir29 } from "fs/promises";
|
|
@@ -66977,6 +67286,8 @@ var CREATE_FLAGS = [
|
|
|
66977
67286
|
"--max-findings",
|
|
66978
67287
|
"--spent",
|
|
66979
67288
|
"--spend-ceiling",
|
|
67289
|
+
"--tokens-in",
|
|
67290
|
+
"--tokens-out",
|
|
66980
67291
|
"--parallel",
|
|
66981
67292
|
"--outstanding"
|
|
66982
67293
|
];
|
|
@@ -67017,8 +67328,12 @@ var SCOPE_FLAGS = [
|
|
|
67017
67328
|
"--base",
|
|
67018
67329
|
"--json",
|
|
67019
67330
|
"--scoped-diff",
|
|
67020
|
-
"--append"
|
|
67331
|
+
"--append",
|
|
67332
|
+
"--reviewers"
|
|
67021
67333
|
];
|
|
67334
|
+
function reviewerList(args) {
|
|
67335
|
+
return (optionValue(args, "--reviewers") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
67336
|
+
}
|
|
67022
67337
|
var FLOOR_FLAGS = ["--ref", "--base", "--diff", "--context", "--json", "--report-only"];
|
|
67023
67338
|
var FLOOR_VALUE_FLAGS = ["--ref", "--base", "--diff", "--context"];
|
|
67024
67339
|
function rejectValuelessOptions(args, valueFlags, usage) {
|
|
@@ -67196,7 +67511,12 @@ async function runCreate(mode, args) {
|
|
|
67196
67511
|
maxFindingsPerReviewer: parseNonNegativeInteger(optionValue(args, "--max-findings"), "--max-findings"),
|
|
67197
67512
|
spend: parseMoney(optionValue(args, "--spent"), "--spent"),
|
|
67198
67513
|
spendCeiling: parseMoney(optionValue(args, "--spend-ceiling"), "--spend-ceiling"),
|
|
67199
|
-
concurrency: parseConcurrency(args)
|
|
67514
|
+
concurrency: parseConcurrency(args),
|
|
67515
|
+
cost: costFrom({
|
|
67516
|
+
inputTokens: parseNonNegativeInteger(optionValue(args, "--tokens-in"), "--tokens-in"),
|
|
67517
|
+
outputTokens: parseNonNegativeInteger(optionValue(args, "--tokens-out"), "--tokens-out"),
|
|
67518
|
+
spentUsd: parseMoney(optionValue(args, "--spent"), "--spent")
|
|
67519
|
+
})
|
|
67200
67520
|
};
|
|
67201
67521
|
const result = await createManagedReviewPackage(input);
|
|
67202
67522
|
console.log(`# managed review: ${result.reviewId}`);
|
|
@@ -67861,6 +68181,8 @@ async function runScope(args) {
|
|
|
67861
68181
|
console.log(renderScopedDiff(scope));
|
|
67862
68182
|
} else {
|
|
67863
68183
|
console.log(renderReviewScopeMarkdown(scope));
|
|
68184
|
+
console.log("");
|
|
68185
|
+
console.log(renderScopeEstimate(renderScopedDiff(scope), reviewerList(args).length));
|
|
67864
68186
|
}
|
|
67865
68187
|
const append = optionValue(args, "--append");
|
|
67866
68188
|
if (append !== undefined) {
|
|
@@ -68116,6 +68438,9 @@ async function runComplete3(args) {
|
|
|
68116
68438
|
console.log(`# managed review complete: ${manifest.reviewId}`);
|
|
68117
68439
|
console.log(`status: ${manifest.status}`);
|
|
68118
68440
|
console.log(`dispositions recorded: ${dispositions.length}`);
|
|
68441
|
+
for (const line of renderCostPerFinding(manifest.cost, manifest.filter_stats?.retained ?? 0)) {
|
|
68442
|
+
console.log(line);
|
|
68443
|
+
}
|
|
68119
68444
|
for (const note of reviewNotes.written) {
|
|
68120
68445
|
console.log(`review-note: ${note.finding} -> ${note.path} (attested by ${note.attestation})`);
|
|
68121
68446
|
}
|
|
@@ -75053,7 +75378,7 @@ init_workspace_service();
|
|
|
75053
75378
|
init_store2();
|
|
75054
75379
|
init_fs();
|
|
75055
75380
|
import { createHash as createHash35, randomUUID as randomUUID17 } from "crypto";
|
|
75056
|
-
import { appendFile as appendFile7, mkdir as mkdir58, open as open2, readFile as readFile94, rename as rename7, rm as rm12, stat as
|
|
75381
|
+
import { appendFile as appendFile7, mkdir as mkdir58, open as open2, readFile as readFile94, rename as rename7, rm as rm12, stat as stat17, writeFile as writeFile50 } from "fs/promises";
|
|
75057
75382
|
import path172 from "path";
|
|
75058
75383
|
|
|
75059
75384
|
// src/sac/policy-experiment.ts
|
|
@@ -75230,7 +75555,7 @@ var ledgerDigestChunkBytes = 64 * 1024;
|
|
|
75230
75555
|
var isMissing = (error) => error instanceof Error && error.code === "ENOENT";
|
|
75231
75556
|
var checkpointHash = (body) => createHash35("sha256").update(JSON.stringify(body), "utf8").digest("hex");
|
|
75232
75557
|
async function ledgerByteLength(ledger) {
|
|
75233
|
-
const value = await
|
|
75558
|
+
const value = await stat17(ledger, { bigint: true });
|
|
75234
75559
|
if (value.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
75235
75560
|
throw new Error("invalid access receipt ledger: ledger-too-large");
|
|
75236
75561
|
return Number(value.size);
|
|
@@ -76654,7 +76979,7 @@ function buildToolRegistry() {
|
|
|
76654
76979
|
init_fs();
|
|
76655
76980
|
init_contained_read();
|
|
76656
76981
|
import path175 from "path";
|
|
76657
|
-
import { lstat as lstat5, readdir as readdir34, realpath as
|
|
76982
|
+
import { lstat as lstat5, readdir as readdir34, realpath as realpath9, stat as stat18 } from "fs/promises";
|
|
76658
76983
|
var URI_PREFIX = "metaproject://";
|
|
76659
76984
|
function mimeForPath(filePath) {
|
|
76660
76985
|
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl")) {
|
|
@@ -76682,10 +77007,10 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
|
|
|
76682
77007
|
const ownerInfo = await lstat5(ownerRoot).catch(() => null);
|
|
76683
77008
|
if (!ownerInfo || !ownerInfo.isDirectory() || ownerInfo.isSymbolicLink())
|
|
76684
77009
|
return [];
|
|
76685
|
-
const ownerReal = await
|
|
77010
|
+
const ownerReal = await realpath9(ownerRoot).catch(() => null);
|
|
76686
77011
|
if (!ownerReal)
|
|
76687
77012
|
return [];
|
|
76688
|
-
const canonical = await
|
|
77013
|
+
const canonical = await realpath9(root).catch(() => null);
|
|
76689
77014
|
if (!canonical || !isPathInside(ownerReal, canonical) || ancestors.has(canonical))
|
|
76690
77015
|
return [];
|
|
76691
77016
|
const nextAncestors = new Set(ancestors);
|
|
@@ -76699,8 +77024,8 @@ async function walkFiles(root, ownerRoot = root, ancestors = new Set) {
|
|
|
76699
77024
|
}
|
|
76700
77025
|
for (const entry of entries) {
|
|
76701
77026
|
const full = path175.join(root, entry.name);
|
|
76702
|
-
const info = await
|
|
76703
|
-
if (!info || !isPathInside(ownerReal, await
|
|
77027
|
+
const info = await stat18(full).catch(() => null);
|
|
77028
|
+
if (!info || !isPathInside(ownerReal, await realpath9(full).catch(() => "")))
|
|
76704
77029
|
continue;
|
|
76705
77030
|
if (info.isDirectory()) {
|
|
76706
77031
|
out.push(...await walkFiles(full, ownerRoot, nextAncestors));
|
|
@@ -76812,7 +77137,7 @@ async function readResource(cwd, roots, uri) {
|
|
|
76812
77137
|
if (!resolved) {
|
|
76813
77138
|
throw new Error(`Resource path is outside its root (rejected): ${uri}`);
|
|
76814
77139
|
}
|
|
76815
|
-
const info = await
|
|
77140
|
+
const info = await stat18(resolved.absolute).catch(() => null);
|
|
76816
77141
|
if (!info || !info.isFile()) {
|
|
76817
77142
|
throw new Error(`Resource not found: ${uri}`);
|
|
76818
77143
|
}
|
|
@@ -86652,7 +86977,7 @@ init_store3();
|
|
|
86652
86977
|
init_proposal_lifecycle();
|
|
86653
86978
|
init_workspace_service();
|
|
86654
86979
|
import { randomUUID as randomUUID27 } from "crypto";
|
|
86655
|
-
import { readdir as readdir36, stat as
|
|
86980
|
+
import { readdir as readdir36, stat as stat19 } from "fs/promises";
|
|
86656
86981
|
import path190 from "path";
|
|
86657
86982
|
|
|
86658
86983
|
// src/sac/lifecycle-flag.ts
|
|
@@ -86912,7 +87237,7 @@ async function collectReceiptTargets(cwd, owner) {
|
|
|
86912
87237
|
async function attributeToSession(absolutePath, sessionsNewestFirst) {
|
|
86913
87238
|
let mtimeMs;
|
|
86914
87239
|
try {
|
|
86915
|
-
mtimeMs = (await
|
|
87240
|
+
mtimeMs = (await stat19(absolutePath)).mtimeMs;
|
|
86916
87241
|
} catch {
|
|
86917
87242
|
return;
|
|
86918
87243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.107",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -227,7 +227,7 @@ Report done only once all of these hold:
|
|
|
227
227
|
- The measurement was taken at the worst real case — the longest translation in
|
|
228
228
|
the catalog, the narrowest supported host, the state where the sibling is
|
|
229
229
|
absent — not at the convenient one.
|
|
230
|
-
- Every finding names `file
|
|
230
|
+
- Every finding names the `file` of the component that renders the box and `quote`s the code it is about; `line` is derived at ingest from that quote, never counted.
|
|
231
231
|
- Every `blocker` and `major` carries `class_scope` with `sites` and an
|
|
232
232
|
`enumeration_method` naming the search that derived the set of elements sharing
|
|
233
233
|
the sizing shape.
|
|
@@ -1350,10 +1350,10 @@ Each reviewer must return a `REVIEW_RESULT` object matching `.metaproject/skills
|
|
|
1350
1350
|
|
|
1351
1351
|
Before consolidation, validate every reviewer result:
|
|
1352
1352
|
- Required status: `DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, or `BLOCKED`.
|
|
1353
|
-
- Required finding fields: id, severity, file,
|
|
1353
|
+
- Required finding fields: id, severity, file, `quote`, problem, impact, suggested_fix, evidence, confidence, reviewer. **`quote`, not `line`:** copy the code the finding is about verbatim out of `file` and do not count lines — `keryx review ingest` locates the quote at the round's head and derives `line` from where it matched, so a number reported here is overwritten. A model is poor at counting lines and good at repeating text it has just read; this asks only for the second. Quote enough to be unambiguous: a snippet appearing twice in the file is recorded `unlocatable` rather than anchored to the first hit. A finding about the round rather than a site carries neither.
|
|
1354
1354
|
- Every blocker must include evidence and a concrete suggested fix.
|
|
1355
1355
|
- Findings without evidence are downgraded to `info` or returned to the reviewer for clarification.
|
|
1356
|
-
- Duplicate findings are merged by `dedupe_key` or by `(file,
|
|
1356
|
+
- Duplicate findings are merged by `dedupe_key` or by `(file, quote, problem)`.
|
|
1357
1357
|
- `NEEDS_CONTEXT` triggers one targeted context refill. If still unresolved, keep it as an explicit open question, not as a blocker.
|
|
1358
1358
|
- If a reviewer exceeds `max_findings`, keep blockers/majors first and summarize lower severity findings.
|
|
1359
1359
|
|
|
@@ -74,12 +74,20 @@
|
|
|
74
74
|
]
|
|
75
75
|
},
|
|
76
76
|
"line": {
|
|
77
|
+
"description": "Do not count lines. Quote the code in `quote` instead and leave this null: `keryx review ingest` finds the quote in `file` and writes the line the match started on, overwriting anything reported here. A model is poor at counting lines and good at repeating text it has just read, and this asks it only for the second. Kept in the contract because a finding with nothing to quote — one about the round rather than a site — still has nowhere else to point.",
|
|
77
78
|
"type": [
|
|
78
79
|
"integer",
|
|
79
80
|
"null"
|
|
80
81
|
],
|
|
81
82
|
"minimum": 1
|
|
82
83
|
},
|
|
84
|
+
"quote": {
|
|
85
|
+
"description": "The code this finding is about, copied verbatim out of `file`. A few lines is enough — it is matched against the file at the round's head, exactly first and then with whitespace collapsed. A quote that appears in more than one place is recorded as `unlocatable` rather than anchored to the first hit, so quote enough to be unambiguous.",
|
|
86
|
+
"type": [
|
|
87
|
+
"string",
|
|
88
|
+
"null"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
83
91
|
"problem": {
|
|
84
92
|
"type": "string"
|
|
85
93
|
},
|
|
@@ -100,7 +100,40 @@
|
|
|
100
100
|
"description": "Which repository `file` and `line` are in. Null, the default, means the repository under review. A cross-repo claim names the producer here and pins the ref it was read at in `evidence`; without this field a finding about another service's behaviour could not say where its evidence lived, so the rule that such a claim is `info` unless it has a `file:line` at a pinned ref could not be checked by anything but the reviewer's word. Declared in BOTH this schema and skills/review/review-orchestrator/reviewer-finding.schema.json: this one is additionalProperties:false, so a finding carrying `repo` is rejected here if only the reviewer-side schema knows about it."
|
|
101
101
|
},
|
|
102
102
|
"file": { "type": ["string", "null"] },
|
|
103
|
-
"line": {
|
|
103
|
+
"line": {
|
|
104
|
+
"title": "Where the finding points — DERIVED when the finding quotes code",
|
|
105
|
+
"description": "Reported by the reviewer only when there is nothing to quote. When `quote` is present this field is OUTPUT: `review ingest` finds the quote in `file` at the round's head and writes the line the match started on, overwriting whatever the reviewer said and recording both in `locator`. The reason is the failure it replaces — nothing compared a reported line to anything, so a number that had drifted travelled into findings.json, into the report, and into the disposition recorded against it. On a fix round the file has moved under the finding by construction, which is when the reported number is least trustworthy and most acted upon.",
|
|
106
|
+
"type": ["integer", "null"],
|
|
107
|
+
"minimum": 1
|
|
108
|
+
},
|
|
109
|
+
"quote": {
|
|
110
|
+
"title": "The code this finding is about, copied verbatim out of `file`",
|
|
111
|
+
"description": "What replaces asking the model for a line number: a model is poor at counting lines and good at repeating text it has just read. The quote is matched against `file` at the round's head — exactly first, then with whitespace collapsed, then not at all. A quote that matches in more than one place is `unlocatable` rather than anchored to the first hit, because choosing among them would be a guess wearing a line number. Absent for a finding that is about the round rather than a site.",
|
|
112
|
+
"type": ["string", "null"]
|
|
113
|
+
},
|
|
114
|
+
"locator": {
|
|
115
|
+
"title": "How `line` was arrived at",
|
|
116
|
+
"description": "Present exactly when the finding carries a quote. `derived` means the quote was found and `line` is where; `unlocatable` means it was not, and then `line` is null rather than a number nobody checked. `reported_line` keeps what the reviewer said, so a round can measure how far the claims drifted from the code — which is the only way to know whether deriving them repaired anything or is insurance.",
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["state"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"state": { "type": "string", "enum": ["derived", "unlocatable"] },
|
|
122
|
+
"method": { "type": "string", "enum": ["exact", "whitespace-normalised"] },
|
|
123
|
+
"reason": { "type": ["string", "null"] },
|
|
124
|
+
"reported_line": { "type": ["integer", "null"], "minimum": 1 }
|
|
125
|
+
},
|
|
126
|
+
"allOf": [
|
|
127
|
+
{
|
|
128
|
+
"if": { "properties": { "state": { "const": "derived" } }, "required": ["state"] },
|
|
129
|
+
"then": { "required": ["method"] }
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"if": { "properties": { "state": { "const": "unlocatable" } }, "required": ["state"] },
|
|
133
|
+
"then": { "required": ["reason"] }
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
104
137
|
"symbol": { "type": ["string", "null"] },
|
|
105
138
|
"problem": { "type": "string" },
|
|
106
139
|
"impact": { "type": "string" },
|