@nathapp/nax 0.67.8 → 0.67.10

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.
Files changed (2) hide show
  1. package/dist/nax.js +665 -441
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -17224,7 +17224,11 @@ var init_schemas_review = __esm(() => {
17224
17224
  timeoutMs: exports_external.number().int().positive().default(600000),
17225
17225
  excludePatterns: exports_external.array(exports_external.string()).optional(),
17226
17226
  parallel: exports_external.boolean().default(false),
17227
- maxConcurrentSessions: exports_external.number().int().min(1).max(4).default(2)
17227
+ maxConcurrentSessions: exports_external.number().int().min(1).max(4).default(2),
17228
+ substantiation: exports_external.object({
17229
+ requote: exports_external.boolean().default(true),
17230
+ maxRequotes: exports_external.number().int().min(0).default(5)
17231
+ }).optional()
17228
17232
  });
17229
17233
  ReviewConfigSchema = exports_external.object({
17230
17234
  enabled: exports_external.boolean(),
@@ -17450,6 +17454,18 @@ var init_schemas3 = __esm(() => {
17450
17454
  ":!.nax/",
17451
17455
  ":!.nax-pids"
17452
17456
  ]
17457
+ },
17458
+ adversarial: {
17459
+ model: "balanced",
17460
+ diffMode: "ref",
17461
+ rules: [],
17462
+ timeoutMs: 600000,
17463
+ parallel: false,
17464
+ maxConcurrentSessions: 2,
17465
+ substantiation: {
17466
+ requote: true,
17467
+ maxRequotes: 5
17468
+ }
17453
17469
  }
17454
17470
  }),
17455
17471
  plan: PlanConfigSchema.default({
@@ -21922,7 +21938,8 @@ function makeParseRetryStrategy(opts) {
21922
21938
  if (ctx.site === "complete") {
21923
21939
  getSafeLogger()?.warn(opts.reviewerKind, "makeParseRetryStrategy: lastOutput is not populated on complete-kind ops \u2014 retry will never fire", { storyId: ctx.storyId });
21924
21940
  }
21925
- return { retry: false };
21941
+ const fallback = opts.exhaustedFallback ? opts.exhaustedFallback("") : undefined;
21942
+ return { retry: false, ...fallback !== undefined ? { fallback } : {} };
21926
21943
  }
21927
21944
  let parsed;
21928
21945
  try {
@@ -30381,18 +30398,19 @@ Schema: {"passed": boolean, "findings": [{"severity": string, "category": string
30381
30398
  const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
30382
30399
  return `Your previous verifiedBy.observed value did not match the referenced file on disk.
30383
30400
 
30401
+ You MUST use your file-reading tool to open ${file3} and copy the actual bytes around line ${line}. Do NOT quote from memory or from the prior conversation \u2014 the previous quote was wrong precisely because it was not read from disk. If you reply without a file-read tool call, the quote will be rejected.
30402
+
30384
30403
  Return ONLY this JSON object:
30385
30404
  {"file":"${file3}","line":${line},"observed":"exact 1-3 line quote"}
30386
30405
 
30387
30406
  Finding issue: ${opts.finding.issue}
30388
30407
  Referenced file: ${file3}
30389
30408
  Referenced line: ${line}
30390
- Previous observed: ${opts.previousObserved}
30391
30409
 
30392
30410
  Rules:
30393
- - Copy observed verbatim from the file. Do not paraphrase.
30394
- - observed must be a 1-3 line excerpt that proves the claim.
30395
- - If you cannot quote proof exactly, set observed to "".
30411
+ - Read ${file3} with your file tool first. Then copy observed verbatim from the read result.
30412
+ - observed must be a 1-3 line excerpt that proves the claim, taken from at or near line ${line}.
30413
+ - If after reading the file you cannot find anything that proves the claim, set observed to "".
30396
30414
  - Do not return a full review. Do not include markdown fences or explanation.`;
30397
30415
  }
30398
30416
  }
@@ -30625,6 +30643,26 @@ ${config2.rules.map((r) => `- ${r}`).join(`
30625
30643
  diffBlock
30626
30644
  ].join("");
30627
30645
  }
30646
+ static requoteVerbatim(opts) {
30647
+ const file3 = opts.finding.verifiedBy?.file ?? opts.finding.file;
30648
+ const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
30649
+ return `Your previous verifiedBy.observed value did not match the referenced file on disk.
30650
+
30651
+ You MUST use your file-reading tool to open ${file3} and copy the actual bytes around line ${line}. Do NOT quote from memory or from the prior conversation \u2014 the previous quote was wrong precisely because it was not read from disk. If you reply without a file-read tool call, the quote will be rejected.
30652
+
30653
+ Return ONLY this JSON object:
30654
+ {"file":"${file3}","line":${line},"observed":"exact 1-3 line quote"}
30655
+
30656
+ Finding issue: ${opts.finding.issue}
30657
+ Referenced file: ${file3}
30658
+ Referenced line: ${line}
30659
+
30660
+ Rules:
30661
+ - Read ${file3} with your file tool first. Then copy observed verbatim from the read result.
30662
+ - observed must be a 1-3 line excerpt that proves the claim, taken from at or near line ${line}.
30663
+ - If after reading the file you cannot find anything that proves the claim, set observed to "".
30664
+ - Do not return a full review. Do not include markdown fences or explanation.`;
30665
+ }
30628
30666
  }
30629
30667
  var ADVERSARIAL_ROLE = `You are an adversarial code reviewer with full access to the repository.
30630
30668
 
@@ -31248,8 +31286,335 @@ var init_adversarial_helpers = __esm(() => {
31248
31286
  init_severity();
31249
31287
  });
31250
31288
 
31289
+ // src/review/semantic-helpers.ts
31290
+ function validateLLMShape(parsed) {
31291
+ if (typeof parsed !== "object" || parsed === null)
31292
+ return null;
31293
+ const obj = parsed;
31294
+ if (typeof obj.passed !== "boolean")
31295
+ return null;
31296
+ if (!Array.isArray(obj.findings))
31297
+ return null;
31298
+ return { passed: obj.passed, findings: obj.findings };
31299
+ }
31300
+ function parseLLMResponse(raw) {
31301
+ try {
31302
+ return validateLLMShape(tryParseLLMJson(raw));
31303
+ } catch {
31304
+ return null;
31305
+ }
31306
+ }
31307
+ function formatFindings2(findings) {
31308
+ return findings.map((f) => `[${f.severity}] ${f.file}:${f.line} \u2014 ${f.issue}
31309
+ Suggestion: ${f.suggestion}`).join(`
31310
+ `);
31311
+ }
31312
+ function normalizeSeverity2(sev) {
31313
+ if (sev === "warn")
31314
+ return "warning";
31315
+ if (sev === "critical" || sev === "error" || sev === "warning" || sev === "info" || sev === "low" || sev === "unverifiable")
31316
+ return sev;
31317
+ return "info";
31318
+ }
31319
+ function sanitizeRefModeFindings(findings, diffMode, blockingThreshold = "error") {
31320
+ if (diffMode !== "ref")
31321
+ return findings;
31322
+ return findings.map((finding) => needsDowngradeForMissingEvidence(finding, blockingThreshold) ? downgradeToUnverifiable(finding) : finding);
31323
+ }
31324
+ function needsDowngradeForMissingEvidence(finding, blockingThreshold) {
31325
+ if (!isBlockingSeverity(finding.severity, blockingThreshold))
31326
+ return false;
31327
+ return mentionsUnverifiedSource(finding) || !hasVerifiedEvidence(finding);
31328
+ }
31329
+ function mentionsUnverifiedSource(finding) {
31330
+ const text = `${finding.issue} ${finding.suggestion}`.toLowerCase();
31331
+ return UNVERIFIED_FINDING_PATTERNS.some((pattern) => text.includes(pattern));
31332
+ }
31333
+ function hasVerifiedEvidence(finding) {
31334
+ const evidence = finding.verifiedBy;
31335
+ return !!evidence?.file?.trim() && !!evidence.observed?.trim();
31336
+ }
31337
+ function downgradeToUnverifiable(finding) {
31338
+ return {
31339
+ ...finding,
31340
+ severity: "unverifiable"
31341
+ };
31342
+ }
31343
+ function llmFindingToFinding(f) {
31344
+ const metaExtras = {};
31345
+ if (f.verifiedBy)
31346
+ metaExtras.verifiedBy = f.verifiedBy;
31347
+ if (f.acQuote)
31348
+ metaExtras.acQuote = f.acQuote;
31349
+ if (f.acIndex != null)
31350
+ metaExtras.acIndex = f.acIndex;
31351
+ return {
31352
+ source: "semantic-review",
31353
+ severity: normalizeSeverity2(f.severity),
31354
+ category: "",
31355
+ file: f.file,
31356
+ line: f.line,
31357
+ message: f.issue,
31358
+ suggestion: f.suggestion ?? undefined,
31359
+ fixTarget: "source",
31360
+ meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
31361
+ };
31362
+ }
31363
+ function toReviewFindings(findings) {
31364
+ return findings.map(llmFindingToFinding);
31365
+ }
31366
+ var UNVERIFIED_FINDING_PATTERNS;
31367
+ var init_semantic_helpers = __esm(() => {
31368
+ init_severity();
31369
+ UNVERIFIED_FINDING_PATTERNS = [
31370
+ "cannot verify",
31371
+ "can't verify",
31372
+ "from diff alone",
31373
+ "missing from diff",
31374
+ "not found in diff",
31375
+ "not present in diff",
31376
+ "does not appear in diff"
31377
+ ];
31378
+ });
31379
+
31380
+ // src/review/semantic-evidence.ts
31381
+ import { isAbsolute as isAbsolute8 } from "path";
31382
+ async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error") {
31383
+ if (diffMode !== "ref")
31384
+ return findings;
31385
+ return Promise.all(findings.map(async (finding) => {
31386
+ if (!isBlockingSeverity(finding.severity, blockingThreshold))
31387
+ return finding;
31388
+ const evidence = await checkFindingEvidence({ finding, workdir });
31389
+ if (evidence.status !== "unmatched")
31390
+ return finding;
31391
+ return downgradeUnsubstantiatedFinding({ finding, storyId, ...evidence });
31392
+ }));
31393
+ }
31394
+ async function checkFindingEvidence(opts) {
31395
+ const observed = opts.finding.verifiedBy?.observed?.trim();
31396
+ const file3 = opts.finding.verifiedBy?.file?.trim() || opts.finding.file;
31397
+ const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
31398
+ if (!observed)
31399
+ return { status: "missing-observed", file: file3, line };
31400
+ const contents = await readSafeFile(opts.workdir, file3);
31401
+ if (contents === null)
31402
+ return { status: "unreadable", file: file3, line, observed };
31403
+ return matchesEvidence(contents, observed, line) ? { status: "matched", file: file3, line, observed } : { status: "unmatched", file: file3, line, observed };
31404
+ }
31405
+ function matchesEvidence(contents, observed, line) {
31406
+ if (!line || line <= 0) {
31407
+ return normalizedIncludes(contents, observed);
31408
+ }
31409
+ const lines = contents.split(`
31410
+ `);
31411
+ const cited = Math.min(Math.max(0, line - 1), lines.length - 1);
31412
+ const start = Math.max(0, cited - EVIDENCE_LINE_WINDOW);
31413
+ const end = Math.min(lines.length, cited + EVIDENCE_LINE_WINDOW + 1);
31414
+ const windowText = lines.slice(start, end).join(`
31415
+ `);
31416
+ return normalizedIncludes(windowText, observed);
31417
+ }
31418
+ function downgradeUnsubstantiatedFinding(opts) {
31419
+ _evidenceDeps.getLogger()?.warn("review", "Downgraded unsubstantiated review finding", {
31420
+ storyId: opts.storyId,
31421
+ event: opts.event ?? SEMANTIC_FINDING_DOWNGRADED_EVENT,
31422
+ file: opts.file ?? opts.finding.verifiedBy?.file ?? opts.finding.file,
31423
+ line: opts.line ?? opts.finding.verifiedBy?.line ?? opts.finding.line,
31424
+ issue: opts.finding.issue?.slice(0, ISSUE_PREVIEW_CHARS),
31425
+ observed: opts.observed?.slice(0, OBSERVED_PREVIEW_CHARS)
31426
+ });
31427
+ return { ...opts.finding, severity: "unverifiable" };
31428
+ }
31429
+ async function readSafeFile(workdir, file3) {
31430
+ const validated = validateModulePath(file3, [workdir]);
31431
+ if (validated.valid && validated.absolutePath) {
31432
+ try {
31433
+ return await Bun.file(validated.absolutePath).text();
31434
+ } catch {
31435
+ return null;
31436
+ }
31437
+ }
31438
+ if (isAbsolute8(file3)) {
31439
+ try {
31440
+ return await Bun.file(file3).text();
31441
+ } catch {
31442
+ return null;
31443
+ }
31444
+ }
31445
+ return null;
31446
+ }
31447
+ function normalizedIncludes(contents, observed) {
31448
+ const normalizedObserved = normalizeEvidenceText(observed);
31449
+ return normalizedObserved.length > 0 && normalizeEvidenceText(contents).includes(normalizedObserved);
31450
+ }
31451
+ function normalizeEvidenceText(text) {
31452
+ return stripWrappingQuotes(text).replace(/\s+/g, " ").trim();
31453
+ }
31454
+ function stripWrappingQuotes(text) {
31455
+ let trimmed = text.trim();
31456
+ while (trimmed.length >= 2 && isMatchingWrapper(trimmed[0], trimmed[trimmed.length - 1])) {
31457
+ trimmed = trimmed.slice(1, -1).trim();
31458
+ }
31459
+ return trimmed;
31460
+ }
31461
+ function isMatchingWrapper(first, last) {
31462
+ return first === "`" && last === "`" || first === `"` && last === `"` || first === "'" && last === "'";
31463
+ }
31464
+ var OBSERVED_PREVIEW_CHARS = 160, ISSUE_PREVIEW_CHARS = 200, EVIDENCE_LINE_WINDOW = 10, SEMANTIC_FINDING_DOWNGRADED_EVENT = "review.semantic.finding.downgraded", ADVERSARIAL_FINDING_DOWNGRADED_EVENT = "review.adversarial.finding.downgraded", _evidenceDeps;
31465
+ var init_semantic_evidence = __esm(() => {
31466
+ init_logger2();
31467
+ init_path_security2();
31468
+ init_semantic_helpers();
31469
+ _evidenceDeps = {
31470
+ getLogger: getSafeLogger
31471
+ };
31472
+ });
31473
+
31474
+ // src/review/finding-filters.ts
31475
+ async function substantiateAdversarialFindings(opts) {
31476
+ const { findings, workdir, storyId, blockingThreshold } = opts;
31477
+ return Promise.all(findings.map(async (finding) => {
31478
+ if (!isBlockingSeverity(finding.severity, blockingThreshold))
31479
+ return finding;
31480
+ const evidence = await checkFindingEvidence({ finding, workdir });
31481
+ if (evidence.status !== "unmatched" && evidence.status !== "missing-observed")
31482
+ return finding;
31483
+ return downgradeUnsubstantiatedFinding({
31484
+ finding,
31485
+ storyId,
31486
+ event: ADVERSARIAL_FINDING_DOWNGRADED_EVENT,
31487
+ file: evidence.file,
31488
+ line: evidence.line,
31489
+ observed: evidence.observed
31490
+ });
31491
+ }));
31492
+ }
31493
+ var init_finding_filters = __esm(() => {
31494
+ init_adversarial_helpers();
31495
+ init_semantic_evidence();
31496
+ init_semantic_helpers();
31497
+ init_semantic_evidence();
31498
+ init_ac_quote_validator();
31499
+ });
31500
+
31501
+ // src/review/requote-response.ts
31502
+ function parseRequoteResponse(output) {
31503
+ const parsed = tryParseLLMJson(output);
31504
+ if (!isRecord(parsed))
31505
+ return null;
31506
+ const canonical = extractCanonical(parsed);
31507
+ if (canonical)
31508
+ return canonical;
31509
+ const findings = parsed.findings;
31510
+ if (!Array.isArray(findings) || findings.length !== 1)
31511
+ return null;
31512
+ const finding = findings[0];
31513
+ if (!isRecord(finding))
31514
+ return null;
31515
+ return extractCanonical(finding.verifiedBy) ?? extractCanonical(finding);
31516
+ }
31517
+ function extractCanonical(value) {
31518
+ if (!isRecord(value))
31519
+ return null;
31520
+ if (typeof value.file !== "string" || typeof value.observed !== "string")
31521
+ return null;
31522
+ const file3 = value.file.trim();
31523
+ if (!file3)
31524
+ return null;
31525
+ const line = coerceLine(value.line);
31526
+ if (line === null)
31527
+ return null;
31528
+ return {
31529
+ file: file3,
31530
+ line: line === undefined ? undefined : line,
31531
+ observed: value.observed
31532
+ };
31533
+ }
31534
+ function coerceLine(value) {
31535
+ if (value == null)
31536
+ return;
31537
+ if (typeof value === "number")
31538
+ return value;
31539
+ if (typeof value === "string" && /^\d+$/.test(value))
31540
+ return Number.parseInt(value, 10);
31541
+ return null;
31542
+ }
31543
+ function isRecord(value) {
31544
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31545
+ }
31546
+ var init_requote_response = () => {};
31547
+
31251
31548
  // src/operations/adversarial-review.ts
31252
- var FAIL_OPEN, adversarialParseRetry = (input) => makeParseRetryStrategy({
31549
+ async function requoteBlockingAdversarialFindings(findings, ctx) {
31550
+ const threshold = ctx.input.blockingThreshold ?? "error";
31551
+ const maxRequotes = ctx.input.adversarialConfig.substantiation?.maxRequotes ?? DEFAULT_MAX_REQUOTES;
31552
+ const requoteEnabled = ctx.input.adversarialConfig.substantiation?.requote ?? true;
31553
+ if (ctx.input.mode !== "ref" || !requoteEnabled || maxRequotes <= 0) {
31554
+ return { findings, changed: false, extraCostUsd: 0 };
31555
+ }
31556
+ const next = [...findings];
31557
+ let changed = false;
31558
+ let extraCostUsd = 0;
31559
+ let used = 0;
31560
+ for (const [index, finding] of next.entries()) {
31561
+ if (!isBlockingSeverity(finding.severity, threshold))
31562
+ continue;
31563
+ const initialEvidence = await checkFindingEvidence({ finding, workdir: ctx.input.workdir });
31564
+ if (initialEvidence.status !== "unmatched")
31565
+ continue;
31566
+ if (used >= maxRequotes)
31567
+ break;
31568
+ used += 1;
31569
+ const retry = await ctx.send(AdversarialReviewPromptBuilder.requoteVerbatim({ finding }));
31570
+ extraCostUsd += retry.estimatedCostUsd ?? 0;
31571
+ const requote = parseRequoteResponse(retry.output);
31572
+ if (!requote) {
31573
+ next[index] = downgradeUnsubstantiatedFinding({
31574
+ finding,
31575
+ storyId: ctx.input.story.id,
31576
+ event: ADVERSARIAL_REQUOTE_FAILED_EVENT,
31577
+ ...initialEvidence
31578
+ });
31579
+ changed = true;
31580
+ continue;
31581
+ }
31582
+ const updatedFinding = {
31583
+ ...finding,
31584
+ verifiedBy: {
31585
+ file: requote.file,
31586
+ line: requote.line,
31587
+ observed: requote.observed
31588
+ }
31589
+ };
31590
+ const requotedEvidence = await checkFindingEvidence({
31591
+ finding: updatedFinding,
31592
+ workdir: ctx.input.workdir
31593
+ });
31594
+ if (requotedEvidence.status === "matched") {
31595
+ getSafeLogger()?.info("review", "Recovered adversarial finding via same-session requote", {
31596
+ storyId: ctx.input.story.id,
31597
+ event: ADVERSARIAL_REQUOTE_RECOVERED_EVENT,
31598
+ file: requotedEvidence.file,
31599
+ line: requotedEvidence.line
31600
+ });
31601
+ next[index] = updatedFinding;
31602
+ changed = true;
31603
+ continue;
31604
+ }
31605
+ next[index] = downgradeUnsubstantiatedFinding({
31606
+ finding: updatedFinding,
31607
+ storyId: ctx.input.story.id,
31608
+ event: ADVERSARIAL_REQUOTE_FAILED_EVENT,
31609
+ file: requotedEvidence.file,
31610
+ line: requotedEvidence.line,
31611
+ observed: requotedEvidence.observed
31612
+ });
31613
+ changed = true;
31614
+ }
31615
+ return { findings: next, changed, extraCostUsd };
31616
+ }
31617
+ var FAIL_OPEN, ADVERSARIAL_REQUOTE_RECOVERED_EVENT = "review.adversarial.finding.requote_recovered", ADVERSARIAL_REQUOTE_FAILED_EVENT = "review.adversarial.finding.requote_failed", DEFAULT_MAX_REQUOTES = 5, adversarialParseRetry = (input) => makeParseRetryStrategy({
31253
31618
  validate: (parsed) => validateAdversarialShape(parsed) !== null,
31254
31619
  reviewerKind: "adversarial",
31255
31620
  maxAttempts: 2,
@@ -31257,15 +31622,24 @@ var FAIL_OPEN, adversarialParseRetry = (input) => makeParseRetryStrategy({
31257
31622
  invalid: () => ReviewPromptBuilder.jsonRetry(),
31258
31623
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
31259
31624
  },
31260
- exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], looksLikeFail: true } : FAIL_OPEN,
31625
+ exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true } : FAIL_OPEN,
31261
31626
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
31262
31627
  }), adversarialReviewOp;
31263
31628
  var init_adversarial_review = __esm(() => {
31264
31629
  init_retry();
31265
31630
  init_config();
31631
+ init_logger2();
31266
31632
  init_prompts();
31267
31633
  init_adversarial_helpers();
31268
- FAIL_OPEN = { passed: true, findings: [], failOpen: true };
31634
+ init_finding_filters();
31635
+ init_requote_response();
31636
+ FAIL_OPEN = {
31637
+ passed: true,
31638
+ findings: [],
31639
+ normalizedFindings: [],
31640
+ acDropped: [],
31641
+ failOpen: true
31642
+ };
31269
31643
  adversarialReviewOp = {
31270
31644
  kind: "run",
31271
31645
  name: "adversarial-review",
@@ -31275,6 +31649,21 @@ var init_adversarial_review = __esm(() => {
31275
31649
  model: (input) => input.adversarialConfig.model,
31276
31650
  timeoutMs: (input) => input.adversarialConfig.timeoutMs,
31277
31651
  retry: (input) => adversarialParseRetry(input),
31652
+ async hopBody(initialPrompt, ctx) {
31653
+ const turn = await ctx.sendWithParseRetry(initialPrompt);
31654
+ const parsed = validateAdversarialShape(tryParseLLMJson(turn.output));
31655
+ if (!parsed)
31656
+ return turn;
31657
+ const requoted = await requoteBlockingAdversarialFindings(parsed.findings, ctx);
31658
+ if (!requoted.changed)
31659
+ return turn;
31660
+ const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
31661
+ return {
31662
+ ...turn,
31663
+ output: JSON.stringify({ passed, findings: requoted.findings }),
31664
+ estimatedCostUsd: (turn.estimatedCostUsd ?? 0) + requoted.extraCostUsd
31665
+ };
31666
+ },
31278
31667
  build(input, _ctx) {
31279
31668
  const base = new AdversarialReviewPromptBuilder().buildAdversarialReviewPrompt(input.story, input.adversarialConfig, {
31280
31669
  mode: input.mode,
@@ -31297,12 +31686,42 @@ var init_adversarial_review = __esm(() => {
31297
31686
  parse(output, _input, _ctx) {
31298
31687
  const raw = tryParseLLMJson(output);
31299
31688
  const parsed = validateAdversarialShape(raw);
31300
- if (parsed)
31301
- return { passed: parsed.passed, findings: parsed.findings };
31689
+ if (parsed) {
31690
+ return {
31691
+ passed: parsed.passed,
31692
+ findings: parsed.findings,
31693
+ normalizedFindings: [],
31694
+ acDropped: []
31695
+ };
31696
+ }
31302
31697
  if (/"passed"\s*:\s*false/.test(output) && !/"findings"\s*:\s*\[\s*\{/.test(output)) {
31303
- return { passed: false, findings: [], looksLikeFail: true };
31698
+ return { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true };
31304
31699
  }
31305
31700
  throw new ParseValidationError("[adversarial-review] parse failed: invalid JSON shape");
31701
+ },
31702
+ async verify(parsed, input, _verifyCtx) {
31703
+ if (parsed.failOpen || parsed.looksLikeFail)
31704
+ return parsed;
31705
+ if (parsed.findings.length === 0)
31706
+ return parsed;
31707
+ const threshold = input.blockingThreshold ?? "error";
31708
+ const findings = parsed.findings;
31709
+ const substantiated = await substantiateAdversarialFindings({
31710
+ findings,
31711
+ workdir: input.workdir,
31712
+ storyId: input.story.id,
31713
+ blockingThreshold: threshold
31714
+ });
31715
+ const { accepted, dropped } = filterByAcQuote(substantiated, input.story.acceptanceCriteria);
31716
+ const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
31717
+ const passed = parsed.passed && blocking.length === 0;
31718
+ return {
31719
+ ...parsed,
31720
+ passed,
31721
+ findings: accepted,
31722
+ normalizedFindings: toAdversarialReviewFindings(blocking),
31723
+ acDropped: dropped
31724
+ };
31306
31725
  }
31307
31726
  };
31308
31727
  });
@@ -31737,178 +32156,6 @@ var init_review_audit = __esm(() => {
31737
32156
  };
31738
32157
  });
31739
32158
 
31740
- // src/review/semantic-helpers.ts
31741
- function validateLLMShape(parsed) {
31742
- if (typeof parsed !== "object" || parsed === null)
31743
- return null;
31744
- const obj = parsed;
31745
- if (typeof obj.passed !== "boolean")
31746
- return null;
31747
- if (!Array.isArray(obj.findings))
31748
- return null;
31749
- return { passed: obj.passed, findings: obj.findings };
31750
- }
31751
- function parseLLMResponse(raw) {
31752
- try {
31753
- return validateLLMShape(tryParseLLMJson(raw));
31754
- } catch {
31755
- return null;
31756
- }
31757
- }
31758
- function formatFindings2(findings) {
31759
- return findings.map((f) => `[${f.severity}] ${f.file}:${f.line} \u2014 ${f.issue}
31760
- Suggestion: ${f.suggestion}`).join(`
31761
- `);
31762
- }
31763
- function normalizeSeverity2(sev) {
31764
- if (sev === "warn")
31765
- return "warning";
31766
- if (sev === "critical" || sev === "error" || sev === "warning" || sev === "info" || sev === "low" || sev === "unverifiable")
31767
- return sev;
31768
- return "info";
31769
- }
31770
- function sanitizeRefModeFindings(findings, diffMode, blockingThreshold = "error") {
31771
- if (diffMode !== "ref")
31772
- return findings;
31773
- return findings.map((finding) => needsDowngradeForMissingEvidence(finding, blockingThreshold) ? downgradeToUnverifiable(finding) : finding);
31774
- }
31775
- function needsDowngradeForMissingEvidence(finding, blockingThreshold) {
31776
- if (!isBlockingSeverity(finding.severity, blockingThreshold))
31777
- return false;
31778
- return mentionsUnverifiedSource(finding) || !hasVerifiedEvidence(finding);
31779
- }
31780
- function mentionsUnverifiedSource(finding) {
31781
- const text = `${finding.issue} ${finding.suggestion}`.toLowerCase();
31782
- return UNVERIFIED_FINDING_PATTERNS.some((pattern) => text.includes(pattern));
31783
- }
31784
- function hasVerifiedEvidence(finding) {
31785
- const evidence = finding.verifiedBy;
31786
- return !!evidence?.file?.trim() && !!evidence.observed?.trim();
31787
- }
31788
- function downgradeToUnverifiable(finding) {
31789
- return {
31790
- ...finding,
31791
- severity: "unverifiable"
31792
- };
31793
- }
31794
- function llmFindingToFinding(f) {
31795
- const metaExtras = {};
31796
- if (f.verifiedBy)
31797
- metaExtras.verifiedBy = f.verifiedBy;
31798
- if (f.acQuote)
31799
- metaExtras.acQuote = f.acQuote;
31800
- if (f.acIndex != null)
31801
- metaExtras.acIndex = f.acIndex;
31802
- return {
31803
- source: "semantic-review",
31804
- severity: normalizeSeverity2(f.severity),
31805
- category: "",
31806
- file: f.file,
31807
- line: f.line,
31808
- message: f.issue,
31809
- suggestion: f.suggestion ?? undefined,
31810
- fixTarget: "source",
31811
- meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
31812
- };
31813
- }
31814
- function toReviewFindings(findings) {
31815
- return findings.map(llmFindingToFinding);
31816
- }
31817
- var UNVERIFIED_FINDING_PATTERNS;
31818
- var init_semantic_helpers = __esm(() => {
31819
- init_severity();
31820
- UNVERIFIED_FINDING_PATTERNS = [
31821
- "cannot verify",
31822
- "can't verify",
31823
- "from diff alone",
31824
- "missing from diff",
31825
- "not found in diff",
31826
- "not present in diff",
31827
- "does not appear in diff"
31828
- ];
31829
- });
31830
-
31831
- // src/review/semantic-evidence.ts
31832
- import { isAbsolute as isAbsolute8 } from "path";
31833
- async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error") {
31834
- if (diffMode !== "ref")
31835
- return findings;
31836
- return Promise.all(findings.map(async (finding) => {
31837
- if (!isBlockingSeverity(finding.severity, blockingThreshold))
31838
- return finding;
31839
- const evidence = await checkFindingEvidence({ finding, workdir });
31840
- if (evidence.status !== "unmatched")
31841
- return finding;
31842
- return downgradeUnsubstantiatedFinding({ finding, storyId, ...evidence });
31843
- }));
31844
- }
31845
- async function checkFindingEvidence(opts) {
31846
- const observed = opts.finding.verifiedBy?.observed?.trim();
31847
- const file3 = opts.finding.verifiedBy?.file?.trim() || opts.finding.file;
31848
- const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
31849
- if (!observed)
31850
- return { status: "missing-observed", file: file3, line };
31851
- const contents = await readSafeFile(opts.workdir, file3);
31852
- if (contents === null)
31853
- return { status: "unreadable", file: file3, line, observed };
31854
- return normalizedIncludes(contents, observed) ? { status: "matched", file: file3, line, observed } : { status: "unmatched", file: file3, line, observed };
31855
- }
31856
- function downgradeUnsubstantiatedFinding(opts) {
31857
- _evidenceDeps.getLogger()?.warn("review", "Downgraded unsubstantiated review finding", {
31858
- storyId: opts.storyId,
31859
- event: opts.event ?? SEMANTIC_FINDING_DOWNGRADED_EVENT,
31860
- file: opts.file ?? opts.finding.verifiedBy?.file ?? opts.finding.file,
31861
- line: opts.line ?? opts.finding.verifiedBy?.line ?? opts.finding.line,
31862
- issue: opts.finding.issue?.slice(0, ISSUE_PREVIEW_CHARS),
31863
- observed: opts.observed?.slice(0, OBSERVED_PREVIEW_CHARS)
31864
- });
31865
- return { ...opts.finding, severity: "unverifiable" };
31866
- }
31867
- async function readSafeFile(workdir, file3) {
31868
- const validated = validateModulePath(file3, [workdir]);
31869
- if (validated.valid && validated.absolutePath) {
31870
- try {
31871
- return await Bun.file(validated.absolutePath).text();
31872
- } catch {
31873
- return null;
31874
- }
31875
- }
31876
- if (isAbsolute8(file3)) {
31877
- try {
31878
- return await Bun.file(file3).text();
31879
- } catch {
31880
- return null;
31881
- }
31882
- }
31883
- return null;
31884
- }
31885
- function normalizedIncludes(contents, observed) {
31886
- const normalizedObserved = normalizeEvidenceText(observed);
31887
- return normalizedObserved.length > 0 && normalizeEvidenceText(contents).includes(normalizedObserved);
31888
- }
31889
- function normalizeEvidenceText(text) {
31890
- return stripWrappingQuotes(text).replace(/\s+/g, " ").trim();
31891
- }
31892
- function stripWrappingQuotes(text) {
31893
- let trimmed = text.trim();
31894
- while (trimmed.length >= 2 && isMatchingWrapper(trimmed[0], trimmed[trimmed.length - 1])) {
31895
- trimmed = trimmed.slice(1, -1).trim();
31896
- }
31897
- return trimmed;
31898
- }
31899
- function isMatchingWrapper(first, last) {
31900
- return first === "`" && last === "`" || first === `"` && last === `"` || first === "'" && last === "'";
31901
- }
31902
- var OBSERVED_PREVIEW_CHARS = 160, ISSUE_PREVIEW_CHARS = 200, SEMANTIC_FINDING_DOWNGRADED_EVENT = "review.semantic.finding.downgraded", ADVERSARIAL_FINDING_DOWNGRADED_EVENT = "review.adversarial.finding.downgraded", _evidenceDeps;
31903
- var init_semantic_evidence = __esm(() => {
31904
- init_logger2();
31905
- init_path_security2();
31906
- init_semantic_helpers();
31907
- _evidenceDeps = {
31908
- getLogger: getSafeLogger
31909
- };
31910
- });
31911
-
31912
32159
  // src/review/adversarial.ts
31913
32160
  import { relative as relative7, sep } from "path";
31914
32161
  function recordAdversarialAudit(opts) {
@@ -31941,7 +32188,6 @@ async function runAdversarialReview(opts) {
31941
32188
  agentManager,
31942
32189
  config: naxConfig,
31943
32190
  featureName,
31944
- priorFailures,
31945
32191
  blockingThreshold,
31946
32192
  featureContextMarkdown,
31947
32193
  contextBundle,
@@ -32062,13 +32308,13 @@ async function runAdversarialReview(opts) {
32062
32308
  let opResult;
32063
32309
  try {
32064
32310
  opResult = await _adversarialDeps.callOp(callCtx, adversarialReviewOp, {
32311
+ workdir,
32065
32312
  story,
32066
32313
  adversarialConfig,
32067
32314
  mode: diffMode,
32068
32315
  diff,
32069
32316
  storyGitRef: effectiveRef,
32070
32317
  stat,
32071
- priorFailures,
32072
32318
  testInventory,
32073
32319
  excludePatterns: adversarialConfig.excludePatterns,
32074
32320
  testGlobs: resolvedTestPatterns.globs,
@@ -32153,27 +32399,11 @@ async function runAdversarialReview(opts) {
32153
32399
  durationMs: Date.now() - startTime
32154
32400
  };
32155
32401
  }
32156
- const rawParsedRaw = {
32157
- passed: opResult.passed,
32158
- findings: opResult.findings
32159
- };
32160
- const blockingThresholdEffective = blockingThreshold ?? "error";
32161
- const substantiatedFindings = await Promise.all(rawParsedRaw.findings.map(async (finding) => {
32162
- if (!isBlockingSeverity(finding.severity, blockingThresholdEffective))
32163
- return finding;
32164
- const evidence = await checkFindingEvidence({ finding, workdir });
32165
- if (evidence.status !== "unmatched" && evidence.status !== "missing-observed")
32166
- return finding;
32167
- return downgradeUnsubstantiatedFinding({
32168
- finding,
32169
- storyId: story.id,
32170
- event: ADVERSARIAL_FINDING_DOWNGRADED_EVENT,
32171
- file: evidence.file,
32172
- line: evidence.line,
32173
- observed: evidence.observed
32174
- });
32175
- }));
32176
- const rawParsed = { ...rawParsedRaw, findings: substantiatedFindings };
32402
+ const threshold = blockingThreshold ?? "error";
32403
+ const allFindings = opResult.findings;
32404
+ const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
32405
+ const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
32406
+ const acDropped = opResult.acDropped ?? [];
32177
32407
  let diffFiles;
32178
32408
  let diffAvailable;
32179
32409
  if (diff && diff.length > 0) {
@@ -32189,13 +32419,6 @@ async function runAdversarialReview(opts) {
32189
32419
  diffAvailable = true;
32190
32420
  }
32191
32421
  }
32192
- const { accepted: acGroundedFindings, dropped: acDropped } = filterByAcQuote(rawParsed.findings, story.acceptanceCriteria);
32193
- if (acDropped.length > 0) {
32194
- logger?.warn("review", "Adversarial findings dropped: acQuote validation failed", {
32195
- storyId: story.id,
32196
- dropped: acDropped.map((d) => ({ file: d.finding.file, issue: d.finding.issue, code: d.code }))
32197
- });
32198
- }
32199
32422
  const adversarialDropAnalysis = acDropped.map((d) => ({
32200
32423
  finding: {
32201
32424
  file: d.finding.file ?? "<unknown>",
@@ -32209,10 +32432,6 @@ async function runAdversarialReview(opts) {
32209
32432
  rawCategory: d.finding.category ?? "",
32210
32433
  counterfactual: analyzeStructuralCounterfactual({ acIndex: d.finding.acIndex, category: d.finding.category, file: d.finding.file }, story.acceptanceCriteria, diffFiles)
32211
32434
  }));
32212
- const parsed = { ...rawParsed, findings: acGroundedFindings };
32213
- const threshold = blockingThresholdEffective;
32214
- const blockingFindings = parsed.findings.filter((f) => isBlockingSeverity(f.severity, threshold));
32215
- const advisoryFindings = parsed.findings.filter((f) => !isBlockingSeverity(f.severity, threshold));
32216
32435
  const adversarialAcceptAnalysis = blockingFindings.map((f) => ({
32217
32436
  finding: {
32218
32437
  file: f.file,
@@ -32235,11 +32454,11 @@ async function runAdversarialReview(opts) {
32235
32454
  }))
32236
32455
  });
32237
32456
  }
32457
+ const durationMs = Date.now() - startTime;
32238
32458
  if (blockingFindings.length > 0) {
32239
- const durationMs2 = Date.now() - startTime;
32240
32459
  logger?.warn("review", `Adversarial review failed: ${blockingFindings.length} blocking findings`, {
32241
32460
  storyId: story.id,
32242
- durationMs: durationMs2,
32461
+ durationMs,
32243
32462
  findings: blockingFindings.map((f) => ({
32244
32463
  severity: f.severity,
32245
32464
  category: f.category,
@@ -32260,72 +32479,37 @@ async function runAdversarialReview(opts) {
32260
32479
  blockingThreshold: threshold,
32261
32480
  result: {
32262
32481
  passed: false,
32263
- findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
32482
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
32264
32483
  },
32265
32484
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined,
32266
32485
  diffAvailable,
32267
32486
  adversarialDropAnalysis,
32268
32487
  adversarialAcceptAnalysis
32269
32488
  });
32489
+ const output = blockingFindings.length > 0 ? `Adversarial review failed:
32490
+
32491
+ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)";
32270
32492
  return {
32271
32493
  check: "adversarial",
32272
32494
  success: false,
32273
32495
  command: "",
32274
32496
  exitCode: 1,
32275
- output: `Adversarial review failed:
32276
-
32277
- ${formatFindings(blockingFindings)}`,
32278
- durationMs: durationMs2,
32279
- findings: toAdversarialReviewFindings(blockingFindings),
32497
+ output,
32498
+ durationMs,
32499
+ findings: blockingFindings.length > 0 ? toAdversarialReviewFindings(blockingFindings) : undefined,
32280
32500
  advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
32281
32501
  cost: llmCost
32282
32502
  };
32283
32503
  }
32284
- if (!parsed.passed && blockingFindings.length === 0) {
32285
- if (acDropped.length > 0) {
32286
- const durationMs3 = Date.now() - startTime;
32287
- logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
32288
- storyId: story.id,
32289
- durationMs: durationMs3,
32290
- droppedCount: acDropped.length,
32291
- dropCodes: acDropped.map((d) => d.code)
32292
- });
32293
- const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
32294
- `);
32295
- recordAdversarialAudit({
32296
- runtime,
32297
- workdir,
32298
- projectDir,
32299
- storyId: story.id,
32300
- featureName,
32301
- parsed: true,
32302
- failOpen: false,
32303
- passed: false,
32304
- blockingThreshold: threshold,
32305
- result: { passed: false, findings: [] },
32306
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined,
32307
- diffAvailable,
32308
- adversarialDropAnalysis,
32309
- adversarialAcceptAnalysis: []
32310
- });
32311
- return {
32312
- check: "adversarial",
32313
- success: false,
32314
- command: "",
32315
- exitCode: 1,
32316
- output: `Adversarial review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Either re-classify these as "info" upstream or extend the ACs. Drops:
32317
-
32318
- ${dropSummary}`,
32319
- durationMs: durationMs3,
32320
- advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
32321
- cost: llmCost
32322
- };
32323
- }
32324
- const durationMs2 = Date.now() - startTime;
32325
- logger?.info("review", "Adversarial review passed (all findings below blocking threshold)", {
32504
+ if (!opResult.passed && acDropped.length > 0) {
32505
+ logger?.warn("review", "Adversarial review fail-closed: blocking findings dropped as ungrounded", {
32326
32506
  storyId: story.id,
32327
- durationMs: durationMs2
32507
+ durationMs,
32508
+ droppedCount: acDropped.length,
32509
+ dropCodes: acDropped.map((d) => d.code)
32328
32510
  });
32511
+ const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
32512
+ `);
32329
32513
  recordAdversarialAudit({
32330
32514
  runtime,
32331
32515
  workdir,
@@ -32334,12 +32518,9 @@ ${dropSummary}`,
32334
32518
  featureName,
32335
32519
  parsed: true,
32336
32520
  failOpen: false,
32337
- passed: true,
32521
+ passed: false,
32338
32522
  blockingThreshold: threshold,
32339
- result: {
32340
- passed: true,
32341
- findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
32342
- },
32523
+ result: { passed: false, findings: [] },
32343
32524
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined,
32344
32525
  diffAvailable,
32345
32526
  adversarialDropAnalysis,
@@ -32347,19 +32528,18 @@ ${dropSummary}`,
32347
32528
  });
32348
32529
  return {
32349
32530
  check: "adversarial",
32350
- success: true,
32531
+ success: false,
32351
32532
  command: "",
32352
- exitCode: 0,
32353
- output: "Adversarial review passed (all findings were advisory \u2014 below blocking threshold)",
32354
- durationMs: durationMs2,
32533
+ exitCode: 1,
32534
+ output: `Adversarial review failed: ${acDropped.length} blocking finding(s) dropped as ungrounded \u2014 the model emitted "passed: false" with concerns it could not ground in any acceptance criterion. Drops:
32535
+
32536
+ ${dropSummary}`,
32537
+ durationMs,
32355
32538
  advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
32356
32539
  cost: llmCost
32357
32540
  };
32358
32541
  }
32359
- const durationMs = Date.now() - startTime;
32360
- if (parsed.passed) {
32361
- logger?.info("review", "Adversarial review passed", { storyId: story.id, durationMs });
32362
- }
32542
+ logger?.info("review", "Adversarial review passed", { storyId: story.id, durationMs });
32363
32543
  recordAdversarialAudit({
32364
32544
  runtime,
32365
32545
  workdir,
@@ -32368,23 +32548,23 @@ ${dropSummary}`,
32368
32548
  featureName,
32369
32549
  parsed: true,
32370
32550
  failOpen: false,
32371
- passed: parsed.passed,
32551
+ passed: true,
32372
32552
  blockingThreshold: threshold,
32373
32553
  result: {
32374
- passed: parsed.passed,
32375
- findings: llmFindingsToReviewFindings(parsed.findings, { source: "adversarial-review" })
32554
+ passed: true,
32555
+ findings: llmFindingsToReviewFindings(allFindings, { source: "adversarial-review" })
32376
32556
  },
32377
32557
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "adversarial-review" }) : undefined,
32378
32558
  diffAvailable,
32379
32559
  adversarialDropAnalysis,
32380
- adversarialAcceptAnalysis
32560
+ adversarialAcceptAnalysis: []
32381
32561
  });
32382
32562
  return {
32383
32563
  check: "adversarial",
32384
- success: parsed.passed,
32564
+ success: true,
32385
32565
  command: "",
32386
- exitCode: parsed.passed ? 0 : 1,
32387
- output: parsed.passed ? "Adversarial review passed" : "Adversarial review failed (no findings)",
32566
+ exitCode: 0,
32567
+ output: allFindings.length === 0 ? "Adversarial review passed" : "Adversarial review passed (all findings were advisory \u2014 below blocking threshold)",
32388
32568
  durationMs,
32389
32569
  advisoryFindings: advisoryFindings.length > 0 ? toAdversarialReviewFindings(advisoryFindings) : undefined,
32390
32570
  cost: llmCost
@@ -32399,13 +32579,11 @@ var init_adversarial = __esm(() => {
32399
32579
  init_adversarial_review();
32400
32580
  init_call();
32401
32581
  init_test_runners();
32402
- init_ac_quote_validator();
32403
32582
  init_ac_structural_counterfactual();
32404
32583
  init_adversarial_helpers();
32405
32584
  init_diff_utils();
32406
32585
  init_finding_projection();
32407
32586
  init_review_audit();
32408
- init_semantic_evidence();
32409
32587
  _adversarialDeps = {
32410
32588
  writeReviewAudit,
32411
32589
  callOp
@@ -34989,57 +35167,10 @@ var init_acceptance_fix = __esm(() => {
34989
35167
  };
34990
35168
  });
34991
35169
 
34992
- // src/review/requote-response.ts
34993
- function parseRequoteResponse(output) {
34994
- const parsed = tryParseLLMJson(output);
34995
- if (!isRecord(parsed))
34996
- return null;
34997
- const canonical = extractCanonical(parsed);
34998
- if (canonical)
34999
- return canonical;
35000
- const findings = parsed.findings;
35001
- if (!Array.isArray(findings) || findings.length !== 1)
35002
- return null;
35003
- const finding = findings[0];
35004
- if (!isRecord(finding))
35005
- return null;
35006
- return extractCanonical(finding.verifiedBy) ?? extractCanonical(finding);
35007
- }
35008
- function extractCanonical(value) {
35009
- if (!isRecord(value))
35010
- return null;
35011
- if (typeof value.file !== "string" || typeof value.observed !== "string")
35012
- return null;
35013
- const file3 = value.file.trim();
35014
- if (!file3)
35015
- return null;
35016
- const line = coerceLine(value.line);
35017
- if (line === null)
35018
- return null;
35019
- return {
35020
- file: file3,
35021
- line: line === undefined ? undefined : line,
35022
- observed: value.observed
35023
- };
35024
- }
35025
- function coerceLine(value) {
35026
- if (value == null)
35027
- return;
35028
- if (typeof value === "number")
35029
- return value;
35030
- if (typeof value === "string" && /^\d+$/.test(value))
35031
- return Number.parseInt(value, 10);
35032
- return null;
35033
- }
35034
- function isRecord(value) {
35035
- return typeof value === "object" && value !== null && !Array.isArray(value);
35036
- }
35037
- var init_requote_response = () => {};
35038
-
35039
35170
  // src/operations/semantic-review.ts
35040
35171
  async function requoteBlockingFindings(findings, ctx) {
35041
35172
  const threshold = ctx.input.blockingThreshold ?? "error";
35042
- const maxRequotes = ctx.input.semanticConfig.substantiation?.maxRequotes ?? DEFAULT_MAX_REQUOTES;
35173
+ const maxRequotes = ctx.input.semanticConfig.substantiation?.maxRequotes ?? DEFAULT_MAX_REQUOTES2;
35043
35174
  const requoteEnabled = ctx.input.semanticConfig.substantiation?.requote ?? true;
35044
35175
  if (ctx.input.mode !== "ref" || !requoteEnabled || maxRequotes <= 0) {
35045
35176
  return { findings, changed: false, extraCostUsd: 0 };
@@ -35057,7 +35188,7 @@ async function requoteBlockingFindings(findings, ctx) {
35057
35188
  if (used >= maxRequotes)
35058
35189
  break;
35059
35190
  used += 1;
35060
- const retry = await ctx.send(ReviewPromptBuilder.requoteVerbatim({ finding, previousObserved: initialEvidence.observed ?? "" }));
35191
+ const retry = await ctx.send(ReviewPromptBuilder.requoteVerbatim({ finding }));
35061
35192
  extraCostUsd += retry.estimatedCostUsd ?? 0;
35062
35193
  const requote = parseRequoteResponse(retry.output);
35063
35194
  if (!requote) {
@@ -35106,7 +35237,7 @@ async function requoteBlockingFindings(findings, ctx) {
35106
35237
  }
35107
35238
  return { findings: next, changed, extraCostUsd };
35108
35239
  }
35109
- var FAIL_OPEN2, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requote_recovered", SEMANTIC_REQUOTE_FAILED_EVENT = "review.semantic.finding.requote_failed", DEFAULT_MAX_REQUOTES = 5, semanticReviewHopBody = async (initialPrompt, ctx) => {
35240
+ var FAIL_OPEN2, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requote_recovered", SEMANTIC_REQUOTE_FAILED_EVENT = "review.semantic.finding.requote_failed", DEFAULT_MAX_REQUOTES2 = 5, semanticReviewHopBody = async (initialPrompt, ctx) => {
35110
35241
  const turn = await ctx.sendWithParseRetry(initialPrompt);
35111
35242
  const parsed = validateLLMShape(tryParseLLMJson(turn.output));
35112
35243
  if (!parsed)
@@ -35126,10 +35257,9 @@ var init_semantic_review = __esm(() => {
35126
35257
  init_config();
35127
35258
  init_logger2();
35128
35259
  init_prompts();
35260
+ init_finding_filters();
35129
35261
  init_requote_response();
35130
- init_semantic_evidence();
35131
- init_semantic_helpers();
35132
- FAIL_OPEN2 = { passed: true, findings: [], failOpen: true };
35262
+ FAIL_OPEN2 = { passed: true, findings: [], normalizedFindings: [], failOpen: true };
35133
35263
  semanticReviewOp = {
35134
35264
  kind: "run",
35135
35265
  name: "semantic-review",
@@ -35146,6 +35276,7 @@ var init_semantic_review = __esm(() => {
35146
35276
  invalid: () => ReviewPromptBuilder.jsonRetry(),
35147
35277
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
35148
35278
  },
35279
+ exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], looksLikeFail: true } : FAIL_OPEN2,
35149
35280
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
35150
35281
  }),
35151
35282
  hopBody: semanticReviewHopBody,
@@ -35167,11 +35298,36 @@ var init_semantic_review = __esm(() => {
35167
35298
  parse(output, _input, _ctx) {
35168
35299
  const raw = tryParseLLMJson(output);
35169
35300
  const parsed = validateLLMShape(raw);
35170
- if (parsed)
35171
- return { passed: parsed.passed, findings: parsed.findings };
35172
- if (/"passed"\s*:\s*false/.test(output))
35173
- return { passed: false, findings: [], looksLikeFail: true };
35301
+ if (parsed) {
35302
+ return {
35303
+ passed: parsed.passed,
35304
+ findings: parsed.findings,
35305
+ normalizedFindings: []
35306
+ };
35307
+ }
35308
+ if (/"passed"\s*:\s*false/.test(output)) {
35309
+ return { passed: false, findings: [], normalizedFindings: [], looksLikeFail: true };
35310
+ }
35174
35311
  return FAIL_OPEN2;
35312
+ },
35313
+ async verify(parsed, input, _verifyCtx) {
35314
+ if (parsed.failOpen || parsed.looksLikeFail)
35315
+ return parsed;
35316
+ if (parsed.findings.length === 0)
35317
+ return parsed;
35318
+ const threshold = input.blockingThreshold ?? "error";
35319
+ const findings = parsed.findings;
35320
+ const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
35321
+ const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold);
35322
+ const { accepted } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
35323
+ const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
35324
+ const passed = parsed.passed && blocking.length === 0;
35325
+ return {
35326
+ ...parsed,
35327
+ passed,
35328
+ findings: accepted,
35329
+ normalizedFindings: toReviewFindings(blocking)
35330
+ };
35175
35331
  }
35176
35332
  };
35177
35333
  });
@@ -37334,14 +37490,45 @@ var init_full_suite_rectify = __esm(() => {
37334
37490
  init_implement();
37335
37491
  });
37336
37492
 
37493
+ // src/operations/_finding-to-check.ts
37494
+ function findingsToFailedChecks(findings) {
37495
+ const grouped = new Map;
37496
+ for (const finding of findings) {
37497
+ const check2 = SOURCE_TO_CHECK[finding.source];
37498
+ if (!check2)
37499
+ continue;
37500
+ const bucket = grouped.get(check2) ?? [];
37501
+ bucket.push(finding);
37502
+ grouped.set(check2, bucket);
37503
+ }
37504
+ return [...grouped.entries()].map(([check2, grpFindings]) => ({
37505
+ check: check2,
37506
+ success: false,
37507
+ command: "",
37508
+ exitCode: 1,
37509
+ output: "",
37510
+ durationMs: 0,
37511
+ findings: grpFindings
37512
+ }));
37513
+ }
37514
+ var SOURCE_TO_CHECK;
37515
+ var init__finding_to_check = __esm(() => {
37516
+ SOURCE_TO_CHECK = {
37517
+ "semantic-review": "semantic",
37518
+ "adversarial-review": "adversarial",
37519
+ lint: "lint",
37520
+ typecheck: "typecheck"
37521
+ };
37522
+ });
37523
+
37337
37524
  // src/operations/autofix-implementer-strategy.ts
37338
37525
  function makeAutofixImplementerStrategy(story) {
37339
37526
  return {
37340
37527
  name: "autofix-implementer",
37341
37528
  appliesTo: (f) => f.fixTarget === "source" && IMPLEMENTER_SOURCES.has(f.source),
37342
37529
  fixOp: implementerRectifyOp,
37343
- buildInput: (_findings, _prior, _cycleCtx) => ({
37344
- failedChecks: [],
37530
+ buildInput: (findings, _prior, _cycleCtx) => ({
37531
+ failedChecks: findingsToFailedChecks(findings),
37345
37532
  story
37346
37533
  }),
37347
37534
  extractApplied: (output) => ({
@@ -37354,6 +37541,7 @@ function makeAutofixImplementerStrategy(story) {
37354
37541
  }
37355
37542
  var IMPLEMENTER_SOURCES;
37356
37543
  var init_autofix_implementer_strategy = __esm(() => {
37544
+ init__finding_to_check();
37357
37545
  init_autofix_implementer();
37358
37546
  IMPLEMENTER_SOURCES = new Set(["lint", "typecheck", "semantic-review"]);
37359
37547
  });
@@ -37364,8 +37552,8 @@ function makeAutofixTestWriterStrategy(story, config2) {
37364
37552
  name: "autofix-test-writer",
37365
37553
  appliesTo: (f) => f.fixTarget === "test" || f.source === "adversarial-review",
37366
37554
  fixOp: testWriterRectifyOp,
37367
- buildInput: (_findings, _prior, _cycleCtx) => ({
37368
- failedChecks: [],
37555
+ buildInput: (findings, _prior, _cycleCtx) => ({
37556
+ failedChecks: findingsToFailedChecks(findings),
37369
37557
  story,
37370
37558
  blockingThreshold: config2.review?.blockingThreshold
37371
37559
  }),
@@ -37374,6 +37562,7 @@ function makeAutofixTestWriterStrategy(story, config2) {
37374
37562
  };
37375
37563
  }
37376
37564
  var init_autofix_test_writer_strategy = __esm(() => {
37565
+ init__finding_to_check();
37377
37566
  init_autofix_test_writer();
37378
37567
  });
37379
37568
 
@@ -37857,6 +38046,7 @@ var init_operations = __esm(() => {
37857
38046
  init_full_suite_rectify();
37858
38047
  init_autofix_implementer_strategy();
37859
38048
  init_autofix_test_writer_strategy();
38049
+ init__finding_to_check();
37860
38050
  init_mechanical_lintfix_strategy();
37861
38051
  init_mechanical_formatfix_strategy();
37862
38052
  init_lint_check();
@@ -38062,7 +38252,10 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
38062
38252
  if (allExhausted) {
38063
38253
  let liteFindingsAfter;
38064
38254
  try {
38065
- liteFindingsAfter = await cycle.validate(ctx, { mode: "lite" });
38255
+ liteFindingsAfter = await cycle.validate(ctx, {
38256
+ mode: "lite",
38257
+ strategiesRun: group.map((s) => s.name)
38258
+ });
38066
38259
  } catch (err) {
38067
38260
  const finishedAt3 = now();
38068
38261
  cycle.iterations.push({
@@ -38134,7 +38327,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
38134
38327
  let validatorAttempt = 0;
38135
38328
  for (;; ) {
38136
38329
  try {
38137
- findingsAfter = await cycle.validate(ctx, { mode: "full" });
38330
+ findingsAfter = await cycle.validate(ctx, { mode: "full", strategiesRun: group.map((s) => s.name) });
38138
38331
  break;
38139
38332
  } catch (err) {
38140
38333
  if (validatorAttempt >= cycle.config.validatorRetries) {
@@ -39309,30 +39502,21 @@ async function runSemanticReview(opts) {
39309
39502
  durationMs: Date.now() - startTime
39310
39503
  };
39311
39504
  }
39312
- const parsed = { passed: opResult.passed, findings: opResult.findings };
39313
- const sanitizedFindings = await substantiateSemanticEvidence(sanitizeRefModeFindings(parsed.findings, diffMode, blockingThreshold ?? "error"), diffMode, workdir, story.id, blockingThreshold ?? "error");
39314
- const { accepted: acGroundedFindings, dropped: acDropped } = filterByAcGroundingMinimal(sanitizedFindings, story.acceptanceCriteria);
39315
- if (acDropped.length > 0) {
39316
- logger?.warn("review", "Semantic findings dropped: acIndex missing or out of range", {
39317
- storyId: story.id,
39318
- dropped: acDropped.map((d) => ({ file: d.finding.file, issue: d.finding.issue, code: d.code }))
39319
- });
39320
- }
39321
- const sanitizedParsed = { ...parsed, findings: acGroundedFindings };
39322
39505
  const threshold = blockingThreshold ?? "error";
39323
- const blockingFindings = sanitizedParsed.findings.filter((f) => isBlockingSeverity(f.severity, threshold));
39324
- const advisoryFindings = sanitizedParsed.findings.filter((f) => !isBlockingSeverity(f.severity, threshold));
39506
+ const allFindings = opResult.findings;
39507
+ const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
39508
+ const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
39325
39509
  if (advisoryFindings.length > 0) {
39326
39510
  logger?.debug("review", `Semantic review: ${advisoryFindings.length} advisory findings (below threshold '${threshold}')`, {
39327
39511
  storyId: story.id,
39328
39512
  findings: advisoryFindings.map((f) => ({ severity: f.severity, file: f.file, issue: f.issue }))
39329
39513
  });
39330
39514
  }
39331
- if (!sanitizedParsed.passed && blockingFindings.length > 0) {
39332
- const durationMs2 = Date.now() - startTime;
39515
+ const durationMs = Date.now() - startTime;
39516
+ if (blockingFindings.length > 0) {
39333
39517
  logger?.warn("review", `Semantic review failed: ${blockingFindings.length} blocking findings`, {
39334
39518
  storyId: story.id,
39335
- durationMs: durationMs2
39519
+ durationMs
39336
39520
  });
39337
39521
  logger?.debug("review", "Semantic review findings", {
39338
39522
  storyId: story.id,
@@ -39359,7 +39543,7 @@ ${formatFindings2(blockingFindings)}`;
39359
39543
  blockingThreshold: threshold,
39360
39544
  result: {
39361
39545
  passed: false,
39362
- findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
39546
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
39363
39547
  },
39364
39548
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
39365
39549
  });
@@ -39369,53 +39553,16 @@ ${formatFindings2(blockingFindings)}`;
39369
39553
  command: "",
39370
39554
  exitCode: 1,
39371
39555
  output,
39372
- durationMs: durationMs2,
39556
+ durationMs,
39373
39557
  findings: toReviewFindings(blockingFindings),
39374
39558
  advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
39375
39559
  cost: llmCost
39376
39560
  };
39377
39561
  }
39378
- if (!sanitizedParsed.passed && blockingFindings.length === 0) {
39379
- if (acDropped.length > 0) {
39380
- const durationMs3 = Date.now() - startTime;
39381
- logger?.warn("review", "Semantic review fail-closed: blocking findings dropped (acIndex invalid)", {
39382
- storyId: story.id,
39383
- durationMs: durationMs3,
39384
- droppedCount: acDropped.length,
39385
- dropCodes: acDropped.map((d) => d.code)
39386
- });
39387
- const dropSummary = acDropped.map((d, i) => `${i + 1}. [${d.code}] ${d.finding.file ?? "<unknown>"}: ${d.finding.issue}`).join(`
39388
- `);
39389
- recordSemanticAudit({
39390
- runtime,
39391
- workdir,
39392
- projectDir,
39393
- storyId: story.id,
39394
- featureName,
39395
- parsed: true,
39396
- failOpen: false,
39397
- passed: false,
39398
- blockingThreshold: threshold,
39399
- result: { passed: false, findings: [] },
39400
- advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
39401
- });
39402
- return {
39403
- check: "semantic",
39404
- success: false,
39405
- command: "",
39406
- exitCode: 1,
39407
- output: `Semantic review failed: ${acDropped.length} blocking finding(s) dropped \u2014 acIndex was missing or out of range. The model emitted "passed: false" without valid AC attribution. Either re-classify these as "info" or ensure each error finding includes a valid acIndex. Drops:
39408
-
39409
- ${dropSummary}`,
39410
- durationMs: durationMs3,
39411
- advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
39412
- cost: llmCost
39413
- };
39414
- }
39415
- const durationMs2 = Date.now() - startTime;
39416
- logger?.info("review", "Semantic review passed (all findings below blocking threshold)", {
39562
+ if (!opResult.passed && allFindings.length === 0) {
39563
+ logger?.warn("review", "Semantic review fail-closed: blocking findings dropped (acIndex invalid)", {
39417
39564
  storyId: story.id,
39418
- durationMs: durationMs2
39565
+ durationMs
39419
39566
  });
39420
39567
  recordSemanticAudit({
39421
39568
  runtime,
@@ -39425,29 +39572,23 @@ ${dropSummary}`,
39425
39572
  featureName,
39426
39573
  parsed: true,
39427
39574
  failOpen: false,
39428
- passed: true,
39575
+ passed: false,
39429
39576
  blockingThreshold: threshold,
39430
- result: {
39431
- passed: true,
39432
- findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
39433
- },
39577
+ result: { passed: false, findings: [] },
39434
39578
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
39435
39579
  });
39436
39580
  return {
39437
39581
  check: "semantic",
39438
- success: true,
39582
+ success: false,
39439
39583
  command: "",
39440
- exitCode: 0,
39441
- output: "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
39442
- durationMs: durationMs2,
39584
+ exitCode: 1,
39585
+ output: 'Semantic review failed: blocking finding(s) were dropped \u2014 acIndex was missing or out of range. The model emitted "passed: false" without valid AC attribution.',
39586
+ durationMs,
39443
39587
  advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
39444
39588
  cost: llmCost
39445
39589
  };
39446
39590
  }
39447
- const durationMs = Date.now() - startTime;
39448
- if (sanitizedParsed.passed) {
39449
- logger?.info("review", "Semantic review passed", { storyId: story.id, durationMs });
39450
- }
39591
+ logger?.info("review", "Semantic review passed", { storyId: story.id, durationMs });
39451
39592
  recordSemanticAudit({
39452
39593
  runtime,
39453
39594
  workdir,
@@ -39456,20 +39597,20 @@ ${dropSummary}`,
39456
39597
  featureName,
39457
39598
  parsed: true,
39458
39599
  failOpen: false,
39459
- passed: sanitizedParsed.passed,
39600
+ passed: true,
39460
39601
  blockingThreshold: threshold,
39461
39602
  result: {
39462
- passed: sanitizedParsed.passed,
39463
- findings: llmFindingsToReviewFindings(sanitizedParsed.findings, { source: "semantic-review" })
39603
+ passed: true,
39604
+ findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review" })
39464
39605
  },
39465
39606
  advisoryFindings: advisoryFindings.length > 0 ? llmFindingsToReviewFindings(advisoryFindings, { source: "semantic-review" }) : undefined
39466
39607
  });
39467
39608
  return {
39468
39609
  check: "semantic",
39469
- success: sanitizedParsed.passed,
39610
+ success: true,
39470
39611
  command: "",
39471
- exitCode: sanitizedParsed.passed ? 0 : 1,
39472
- output: sanitizedParsed.passed ? "Semantic review passed" : "Semantic review failed (no findings)",
39612
+ exitCode: 0,
39613
+ output: allFindings.length === 0 ? "Semantic review passed" : "Semantic review passed (all findings were advisory \u2014 below blocking threshold)",
39473
39614
  durationMs,
39474
39615
  advisoryFindings: advisoryFindings.length > 0 ? toReviewFindings(advisoryFindings) : undefined,
39475
39616
  cost: llmCost
@@ -39486,12 +39627,10 @@ var init_semantic = __esm(() => {
39486
39627
  init_semantic_review();
39487
39628
  init_prompts();
39488
39629
  init_test_runners();
39489
- init_ac_quote_validator();
39490
39630
  init_diff_utils();
39491
39631
  init_finding_projection();
39492
39632
  init_review_audit();
39493
39633
  init_semantic_debate();
39494
- init_semantic_evidence();
39495
39634
  init_semantic_helpers();
39496
39635
  _semanticDeps = {
39497
39636
  createDebateRunner: (opts) => new DebateRunner(opts),
@@ -41818,6 +41957,38 @@ async function callOp(ctx, op, input) {
41818
41957
  const rawOutput = outcome.result.output;
41819
41958
  const totalCost = outcome.result.estimatedCostUsd ?? 0;
41820
41959
  if (!rawOutput) {
41960
+ if (maxRetriesExceeded) {
41961
+ getSafeLogger()?.error("callop", "Op retry budget exhausted (empty output)", {
41962
+ storyId: ctx.storyId,
41963
+ opName: op.name,
41964
+ site: "run",
41965
+ totalAttempts: MAX_COMPLETE_RETRY_ATTEMPTS + 1
41966
+ });
41967
+ throw new NaxError(`callOp[${op.name}]: CALL_OP_MAX_RETRIES \u2014 exceeded MAX_COMPLETE_RETRY_ATTEMPTS (${MAX_COMPLETE_RETRY_ATTEMPTS})`, "CALL_OP_MAX_RETRIES", { stage: op.stage, storyId: ctx.storyId });
41968
+ }
41969
+ if (retryFallback !== undefined) {
41970
+ if (typeof retryFallback !== "object" || retryFallback === null) {
41971
+ throw new NaxError(`callOp[${op.name}]: exhaustedFallback returned a non-object (${typeof retryFallback}); fallback must be a plain object`, "CALL_OP_INVALID_FALLBACK", { stage: op.stage, storyId: ctx.storyId });
41972
+ }
41973
+ getSafeLogger()?.warn("callop", "Returning exhaustedFallback on empty output", {
41974
+ storyId: ctx.storyId,
41975
+ opName: op.name,
41976
+ agentName: dispatchAgent
41977
+ });
41978
+ return { ...retryFallback, estimatedCostUsd: totalCost };
41979
+ }
41980
+ if (op.recover) {
41981
+ const verifyCtx = makeVerifyCtx(buildCtx);
41982
+ const recovered = await op.recover(input, verifyCtx);
41983
+ if (recovered !== null) {
41984
+ getSafeLogger()?.warn("callop", "Recovered from empty output via op.recover", {
41985
+ storyId: ctx.storyId,
41986
+ opName: op.name,
41987
+ agentName: dispatchAgent
41988
+ });
41989
+ return recovered;
41990
+ }
41991
+ }
41821
41992
  throw new NaxError(`callOp[${op.name}]: agent returned no output`, "CALL_OP_NO_OUTPUT", {
41822
41993
  stage: op.stage,
41823
41994
  storyId: ctx.storyId,
@@ -51966,18 +52137,32 @@ function phasePassed(opName, output) {
51966
52137
  });
51967
52138
  return true;
51968
52139
  }
52140
+ function isFinding(value) {
52141
+ return typeof value === "object" && value !== null && typeof value.source === "string" && value.source.length > 0;
52142
+ }
51969
52143
  function extractPhaseFindings(output) {
51970
52144
  if (output === null || output === undefined || typeof output !== "object") {
51971
52145
  return [];
51972
52146
  }
51973
52147
  const record2 = output;
51974
- const findings = Array.isArray(record2.findings) ? record2.findings : [];
52148
+ const rawArray = Array.isArray(record2.normalizedFindings) ? record2.normalizedFindings : Array.isArray(record2.findings) ? record2.findings : [];
52149
+ const findings = rawArray.filter(isFinding);
51975
52150
  const success2 = "success" in record2 ? record2.success === true : ("passed" in record2) ? record2.passed === true : findings.length === 0;
51976
52151
  return success2 ? [] : findings;
51977
52152
  }
51978
- function gatherRectificationFindings(phaseOutputs, phases) {
52153
+ function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
52154
+ if (phase.kind !== "full-suite-gate")
52155
+ return false;
52156
+ const verifierName = state.verifier?.slot.op.name;
52157
+ if (!verifierName)
52158
+ return false;
52159
+ return phaseExplicitlyPassed(phaseOutputs[verifierName]);
52160
+ }
52161
+ function gatherRectificationFindings(phaseOutputs, phases, state) {
51979
52162
  const findings = [];
51980
52163
  for (const phase of phases) {
52164
+ if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
52165
+ continue;
51981
52166
  findings.push(...extractPhaseFindings(phaseOutputs[phase.slot.op.name]));
51982
52167
  }
51983
52168
  return findings;
@@ -51993,6 +52178,21 @@ function collectRectificationPhases(state) {
51993
52178
  state.adversarialReview
51994
52179
  ].filter((phase) => phase !== undefined);
51995
52180
  }
52181
+ function phasesToRevalidate(strategiesRun, allPhases) {
52182
+ const sourceFiltered = allPhases.filter((p) => p.kind !== "verifier");
52183
+ if (!strategiesRun || strategiesRun.length === 0)
52184
+ return sourceFiltered;
52185
+ const unknown2 = strategiesRun.some((name) => STRATEGY_TO_REVALIDATION_PHASES[name] === undefined);
52186
+ if (unknown2)
52187
+ return sourceFiltered;
52188
+ const needed = new Set;
52189
+ for (const name of strategiesRun) {
52190
+ for (const kind of STRATEGY_TO_REVALIDATION_PHASES[name] ?? []) {
52191
+ needed.add(kind);
52192
+ }
52193
+ }
52194
+ return sourceFiltered.filter((p) => needed.has(p.kind));
52195
+ }
51996
52196
  function toReviewDecisionPayload(opName, output) {
51997
52197
  if (output === null || output === undefined || typeof output !== "object")
51998
52198
  return null;
@@ -52148,7 +52348,7 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs) {
52148
52348
  if (ctx.runtime.signal?.aborted) {
52149
52349
  return {};
52150
52350
  }
52151
- const initialFindings = gatherRectificationFindings(phaseOutputs, validationPhases);
52351
+ const initialFindings = gatherRectificationFindings(phaseOutputs, validationPhases, state);
52152
52352
  if (initialFindings.length === 0) {
52153
52353
  return {};
52154
52354
  }
@@ -52168,13 +52368,22 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs) {
52168
52368
  validate: async (_validateCtx, opts) => {
52169
52369
  if (ctx.runtime.signal?.aborted)
52170
52370
  return [];
52171
- const lite = opts?.mode === "lite";
52371
+ const lite = (opts?.mode ?? "full") === "lite";
52372
+ const phases = phasesToRevalidate(opts?.strategiesRun, validationPhases);
52373
+ getSafeLogger()?.debug("story-orchestrator", "rectification validate scope", {
52374
+ storyId: ctx.storyId,
52375
+ mode: opts?.mode ?? "full",
52376
+ strategiesRun: opts?.strategiesRun,
52377
+ phasesSelected: phases.map((p) => p.kind)
52378
+ });
52172
52379
  const findings = [];
52173
- for (const phase of validationPhases) {
52380
+ for (const phase of phases) {
52174
52381
  if (lite && phase.kind === "full-suite-gate") {
52175
52382
  continue;
52176
52383
  }
52177
52384
  await runPhase(ctx, phase.slot, phaseCosts, phaseOutputs);
52385
+ if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
52386
+ continue;
52178
52387
  findings.push(...extractPhaseFindings(phaseOutputs[phase.slot.op.name]));
52179
52388
  }
52180
52389
  return findings;
@@ -52321,7 +52530,7 @@ class StoryOrchestratorBuilder {
52321
52530
  return new ExecutionPlan(ctx, { ...this.state }, opts.isThreeSession ?? false);
52322
52531
  }
52323
52532
  }
52324
- var _storyOrchestratorDeps, TDD_OP_NAMES, CANONICAL_ORDER, PHASE_KIND_TO_STATE_KEY;
52533
+ var _storyOrchestratorDeps, TDD_OP_NAMES, CANONICAL_ORDER, PHASE_KIND_TO_STATE_KEY, STRATEGY_TO_REVALIDATION_PHASES;
52325
52534
  var init_story_orchestrator = __esm(() => {
52326
52535
  init_errors();
52327
52536
  init_findings();
@@ -52359,6 +52568,20 @@ var init_story_orchestrator = __esm(() => {
52359
52568
  "semantic-review": "semanticReview",
52360
52569
  "adversarial-review": "adversarialReview"
52361
52570
  };
52571
+ STRATEGY_TO_REVALIDATION_PHASES = {
52572
+ "mechanical-lintfix": ["lint-check"],
52573
+ "mechanical-formatfix": ["lint-check"],
52574
+ "autofix-implementer": [
52575
+ "lint-check",
52576
+ "typecheck-check",
52577
+ "full-suite-gate",
52578
+ "verify-scoped",
52579
+ "semantic-review",
52580
+ "adversarial-review"
52581
+ ],
52582
+ "autofix-test-writer": ["lint-check", "typecheck-check", "full-suite-gate", "verify-scoped", "adversarial-review"],
52583
+ "full-suite-rectify": ["lint-check", "typecheck-check", "full-suite-gate", "verify-scoped", "semantic-review"]
52584
+ };
52362
52585
  });
52363
52586
 
52364
52587
  // src/execution/build-plan-for-strategy.ts
@@ -52551,6 +52774,7 @@ async function assemblePlanInputsFromCtx(ctx) {
52551
52774
  blockingThreshold: ctx.config.review.blockingThreshold
52552
52775
  } : undefined;
52553
52776
  const adversarialReviewInput = ctx.config.review?.enabled === true && ctx.config.review.checks?.includes("adversarial") && ctx.config.review.adversarial ? {
52777
+ workdir: ctx.workdir,
52554
52778
  story,
52555
52779
  adversarialConfig: ctx.config.review.adversarial,
52556
52780
  mode: ctx.config.review.adversarial.diffMode,
@@ -56844,7 +57068,7 @@ var package_default;
56844
57068
  var init_package = __esm(() => {
56845
57069
  package_default = {
56846
57070
  name: "@nathapp/nax",
56847
- version: "0.67.8",
57071
+ version: "0.67.10",
56848
57072
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
56849
57073
  type: "module",
56850
57074
  bin: {
@@ -56939,8 +57163,8 @@ var init_version = __esm(() => {
56939
57163
  NAX_VERSION = package_default.version;
56940
57164
  NAX_COMMIT = (() => {
56941
57165
  try {
56942
- if (/^[0-9a-f]{6,10}$/.test("1e88267c"))
56943
- return "1e88267c";
57166
+ if (/^[0-9a-f]{6,10}$/.test("1d0ef5ac"))
57167
+ return "1d0ef5ac";
56944
57168
  } catch {}
56945
57169
  try {
56946
57170
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {