@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 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.106",
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 stat16, writeFile as writeFile50 } from "fs/promises";
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 stat16(ledger, { bigint: true });
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 realpath8, stat as stat17 } from "fs/promises";
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 realpath8(ownerRoot).catch(() => null);
77010
+ const ownerReal = await realpath9(ownerRoot).catch(() => null);
76686
77011
  if (!ownerReal)
76687
77012
  return [];
76688
- const canonical = await realpath8(root).catch(() => null);
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 stat17(full).catch(() => null);
76703
- if (!info || !isPathInside(ownerReal, await realpath8(full).catch(() => "")))
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 stat17(resolved.absolute).catch(() => null);
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 stat18 } from "fs/promises";
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 stat18(absolutePath)).mtimeMs;
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.106",
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:line` on the component that renders the box.
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, line (nullable only for repo-wide findings), problem, impact, suggested_fix, evidence, confidence, reviewer.
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, line, problem)`.
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": { "type": ["integer", "null"], "minimum": 1 },
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" },