Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/src/commands/review/agent-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,13 @@ describe('agent-prompt (command boundary)', () => {
// The verdict branch: Exclusion Criteria yes, finding format no.
expect(briefText).toContain('What is NOT a finding');
expect(briefText).not.toContain('**Anchor:**');
// The witness rule: a confirmed Critical returns its executed evidence
// or the one-line reason, and the sweep is a named witness form. These
// demands are what the orchestrator's low-confidence demotion sorts on,
// so a brief that drops them silently demotes every trace-only Critical.
expect(briefText).toContain('A confirmed Critical returns its witness.');
expect(briefText).toContain('witness: not run —');
expect(briefText).toContain('sweep the real population');
} finally {
rmSync(dir, { recursive: true, force: true });
}
Expand Down
98 changes: 98 additions & 0 deletions packages/cli/src/commands/review/findings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type Finding,
type FindingsReport,
holdCriticalsFailingOnBase,
holdUnwitnessedCriticals,
sharedFailingFilesOf,
} from './findings.js';

Expand Down Expand Up @@ -511,6 +512,30 @@ describe('findings (command boundary)', () => {
return out;
}

it('demotes an unwitnessed Critical through the whole handler, and says so on stderr', () => {
// The unit tests pin holdUnwitnessedCriticals in isolation; this pins the
// WIRING — the call sits in the handler before buildReport, so removing
// it, or moving it after the report is built, fails here, not silently.
const input = join(dir, 'in.json');
const out = join(dir, 'findings.json');
writeFileSync(
input,
JSON.stringify([
{ ...base, id: 'w1' },
{ ...base, id: 'w2', witness: 'probe flipped: 2 calls → 1' },
]),
);
const stderr = runCapturingStderr({ input, out, print: false });
const report = JSON.parse(readFileSync(out, 'utf8')) as FindingsReport;
const byId = new Map(report.findings.map((f) => [f.id, f]));
expect(byId.get('w1')?.confidence).toBe('low');
expect(byId.get('w1')?.failureScenario).toContain('witness rule');
expect(byId.get('w2')?.confidence).toBe('high');
expect(stderr).toContain('w1 filed at low confidence');
expect(stderr).not.toContain('w2 filed at low confidence');
expect(report.counts.byConfidence['low']).toBe(1);
});

it('announces every hold, naming the finding and the measured file', () => {
// A severity this command lowered is a change to what the review says. Left
// unannounced it reads as the reviewer's own judgement, which is the one
Expand Down Expand Up @@ -790,6 +815,68 @@ describe('findings (command boundary)', () => {
});
});

describe('holdUnwitnessedCriticals — the witness rule has a machine half', () => {
const critical = {
id: 'w1',
severity: 'Critical' as const,
confidence: 'high' as const,
source: 'review' as const,
summary: 'double-executes the shell command',
shortSummary: 'double execute',
failureScenario: 'run !git push → sendShellCommand fires twice',
locations: [{ file: 'src/pay.ts', line: 42 }],
};

it('files an unwitnessed high-confidence review Critical at low confidence, and says why', () => {
// The demotion the SKILL promises as mechanical: without this, the sort
// exists only as Step 4 prose, and an omitted `confidence` even defaults
// to `high` — the fail-open direction (dogfood review of the witness PR).
const { findings, unwitnessed } = holdUnwitnessedCriticals([critical]);
expect(findings[0].confidence).toBe('low');
expect(findings[0].severity).toBe('Critical');
expect(findings[0].failureScenario).toContain('witness rule');
// The original evidence survives — the rule is appended, not substituted.
expect(findings[0].failureScenario).toContain('fires twice');
expect(unwitnessed).toEqual(['w1']);
});

it('leaves a witnessed Critical alone — either form of the field counts', () => {
for (const witness of [
'BASE: 2 calls / PR: 1 call — probe flipped',
'not run — needs a live OAuth endpoint this harness lacks',
]) {
const { findings, unwitnessed } = holdUnwitnessedCriticals([
{ ...critical, witness },
]);
expect(findings[0].confidence).toBe('high');
expect(unwitnessed).toEqual([]);
}
});

it('exempts deterministic sources — their witness is constitutive', () => {
// A [build]/[test]/[probe] finding IS a run's output; demanding a second
// witness would demote findings the pipeline treats as pre-confirmed.
for (const source of ['build', 'test', 'probe', 'lint'] as const) {
const { unwitnessed } = holdUnwitnessedCriticals([
{ ...critical, source },
]);
expect(unwitnessed).toEqual([]);
}
});

it('is idempotent — a demoted finding re-fed is not touched again', () => {
const once = holdUnwitnessedCriticals([critical]).findings[0];
const twice = holdUnwitnessedCriticals([once]).findings[0];
expect(twice).toEqual(once);
// Suggestions are never judged: the rule targets the severity that posts
// as a blocker.
expect(
holdUnwitnessedCriticals([{ ...critical, severity: 'Suggestion' }])
.unwitnessed,
).toEqual([]);
});
});

describe('holdCriticalsFailingOnBase', () => {
// The shape test-delta writes: workspace-relative paths, while a finding
// names the repo-relative one.
Expand Down Expand Up @@ -1214,4 +1301,15 @@ describe('validateFindings — the canonical artifact round-trips', () => {
expect(f.outcome).toBeUndefined();
expect(f.outcomeNote).toBeUndefined();
});

it('keeps witness, so the executed evidence survives being fed back', () => {
// The Step 4 witness rule attaches the evidence once; the report and the
// comment bodies read it back out of the artifact. Dropped here, every
// downstream quote becomes a fresh transcription.
const [f] = validateFindings([
{ ...base, witness: 'BASE: 2 calls / PR: 1 call — probe flipped' },
]);
expect(f.witness).toBe('BASE: 2 calls / PR: 1 call — probe flipped');
expect(validateFindings([{ ...base }])[0].witness).toBeUndefined();
});
});
62 changes: 62 additions & 0 deletions packages/cli/src/commands/review/findings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ export interface Finding {
shortSummary: string;
/** The concrete trigger and wrong outcome — the finding's evidence. */
failureScenario: string;
/**
* The executed evidence that settled the verdict (a probe's two sides, an
* A/B's quoted pair, a sweep count) — or the verifier's
* `not run — <reason>` line. Carried as data so the report and the comment
* bodies quote one recorded string instead of transcribing it twice more.
*/
witness?: string;
suggestedFix?: string;
/** Free-form kebab-case tag (`correctness`, `security`, `test-coverage`, …). */
category?: string;
Expand Down Expand Up @@ -351,6 +358,11 @@ export function validateFindings(raw: unknown): Finding[] {
const shortSummary =
asString(o, 'shortSummary') ?? asString(o, 'short_summary');

// `witness` round-trips for the same reason `outcomeNote` does: the Step 4
// witness rule attaches it once, and the report and the comment bodies read
// it back out of the artifact instead of transcribing the evidence again.
const witness = asString(o, 'witness');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-1: The witness demotion is promised as mechanical — SKILL.md: "deliberately mechanical, the same shape as the — [unverified] tag"; this file's verify brief: "This is mechanical downstream"; DESIGN.md: "the enforcement shape is borrowed from the — [unverified] tag" — but no code anywhere reads witness: the sort exists only as Step 4 prose for the orchestrating model. The precedent being borrowed HAS a machine half (compose-review scans the findings file for surviving — [unverified] tags, caps the verdict, and posts a disclosure count) that this change does not carry over; validateFindings defaults an omitted confidence to high — the fail-open direction for this rule — and this same command already mechanically demotes Criticals via the test-delta holdback, so the pattern is local. — Failure scenario: a verifier confirms a Critical and argues in prose instead of returning a witness: / not run — line; the orchestrator, sorting findings against a ~1300-line SKILL.md, misses the demotion (or omits confidence, silently defaulted to high); nothing between Step 4 and Step 7 inspects witness, so the unwitnessed Critical posts as a blocker without executed evidence — the exact failure mode this PR exists to prevent — with no count or telemetry showing whether the sort ran. Fix: enforce at the posting boundary — compose-review (beside findings-unverified-at-compose) or the findings command counts/demotes witness-less high-confidence Criticals — or soften the "mechanical" / "borrowed enforcement shape" wording to name the orchestrator sort as the only enforcer.

中文说明

[建议] witness 降级被承诺为机械执行——SKILL.md:"deliberately mechanical, the same shape as the — [unverified] tag";verify brief:"This is mechanical downstream";DESIGN.md:"the enforcement shape is borrowed from the — [unverified] tag"——但没有任何代码读取 witness:这个分检只存在于编排模型执行的 Step 4 文字里。被借用的先例机器的一半(compose-review 会扫描 findings 文件中残留的 — [unverified] 标记、封顶裁决并发布披露计数),本改动没有带上这一半;validateFindings 把缺省的 confidence 默认为 high——对此规则是 fail-open 的方向——而同一个命令已经通过 test-delta 抑制机械地降级过 Critical,模式就在本地。失败场景:verifier 确认了一个 Critical 却用文字论证、没有返回 witness: / not run — 行;编排器在约 1300 行的 SKILL.md 背景下做分检时漏掉了降级(或没写 confidence,被静默默认为 high);Step 4 到 Step 7 之间没有任何东西检查 witness,于是没有实测证据的 Critical 以 blocker 身份发布——正是本 PR 要防止的失效模式——且没有任何计数或遥测显示分检是否执行过。修复:在发布边界强制——compose-review(在 findings-unverified-at-compose 旁边)或 findings 命令统计并降级缺少 witness 的高置信 Critical;或者把 "mechanical" / "borrowed enforcement shape" 的措辞弱化为"编排器分检是唯一执行者"。

— qwen3.8-max via Qwen Code /review (v0.21.11)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4855a8a, at the boundary you named as local precedent: qwen review findings now runs holdUnwitnessedCriticals beside the test-delta holdback — a high-confidence [review]-source Critical with no witness is demoted to low confidence at canonicalization, each named on stderr, and the appended sentence tells the reader which rule moved it and the way back. Deterministic sources are exempt (their witness is constitutive), and the hold is idempotent on re-feed. SKILL.md and DESIGN.md now describe the machine half instead of only promising one; a compose-review-level cap stays follow-up.(已修:findings 规范化处代码强制降级,逐条 stderr 披露;确定性来源豁免;重复喂入幂等。)


return {
id,
severity,
Expand All @@ -361,6 +373,7 @@ export function validateFindings(raw: unknown): Finding[] {
? compressSummary(shortSummary)
: compressSummary(summary),
failureScenario,
...(witness ? { witness } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-3: The Web Shell artifact renderer — the documented second consumer of this artifact shape (CodeReviewArtifactDetail.tsx) — parses every other optional evidence field (suggestedFix, category, outcomeNote, heldByMeasurement) but never reads witness, silently dropping the executed evidence from display. Witness (probe, flipped): with the unmodified PR code, a finding carrying witness: 'BASE: 2 calls / PR: 1 call — probe flipped' parses to keys ["confidence","failureScenario","id","locations","severity","shortSummary","source","summary"] — witness silently dropped, parse succeeds; adding the field to parseFinding round-trips it byte-for-byte. — Failure scenario: a review run that does exactly what this PR intends — attaches executed evidence to every confirmed Critical — renders in the Web Shell UI with the evidence absent: the author sees the Critical claim and failure scenario but not the probe flip / A/B pair / sweep count the field exists to hand them, and the gap is invisible because parsing succeeds. The PR's "the Web Shell renderer is unaffected" claim answers "does it break" (no) but not "does it show the field this PR exists to deliver" (no). Fix: add witness to parseFinding in packages/web-shell/client/components/artifacts/CodeReviewArtifactDetail.tsx (an optionalString like outcomeNote) and render it with the finding's expandable fields — or document it as deliberately deferred.

中文说明

[建议] R1-3:Web Shell 工件渲染器——该工件格式的、有文档记载的第二个消费者(CodeReviewArtifactDetail.tsx)——解析了其他每一个可选证据字段(suggestedFixcategoryoutcomeNoteheldByMeasurement),却从不读取 witness,实测证据被静默丢弃、不显示。Witness(probe,已翻转):在未改动的 PR 代码上,携带 witness: 'BASE: 2 calls / PR: 1 call — probe flipped' 的 finding 解析出的键为 ["confidence","failureScenario","id","locations","severity","shortSummary","source","summary"]——witness 被静默丢弃且解析成功;把该字段加入 parseFinding 后可逐字节往返。失败场景:一次完全按本 PR 意图执行的审查——给每个已确认 Critical 附上实测证据——在 Web Shell UI 中渲染时证据缺席:作者看到 Critical 主张和失败场景,却看不到该字段本要交付的 probe 翻转 / A/B 引文对 / sweep 计数,而且因为解析成功,这个缺口不可见。PR 中 "Web Shell 渲染器不受影响" 的说法回答的是"会不会坏"(不会),而不是"会不会显示本 PR 存在的意义所交付的字段"(不会)。修复:在 packages/web-shell/client/components/artifacts/CodeReviewArtifactDetail.tsxparseFinding 中加入 witness(像 outcomeNote 一样的 optionalString)并随 finding 的可展开字段渲染——或明确记录为刻意推迟。

— qwen3.8-max via Qwen Code /review (v0.21.11)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4855a8a: parseFinding carries witness (an optionalString, exactly the outcomeNote shape your probe suggested) and the finding card renders it between Failure scenario and Suggested fix, with en/zh labels (Witness / 实测证据). Pinned by the renderer test — the fixture's Critical now carries a witness and the assertions require it displayed. Thanks for running the probe; 'parses successfully while dropping the one field the PR exists to deliver' is exactly the class of gap that hides behind a green parse.(已修:解析+渲染+中英文标签,渲染测试钉住显示。)

...(asString(o, 'suggestedFix') || asString(o, 'suggested_fix')
? {
suggestedFix: (asString(o, 'suggestedFix') ??
Expand Down Expand Up @@ -507,6 +520,45 @@ export function holdCriticalsFailingOnBase(
return { findings: out, held, readjudicated };
}

/**
* The witness rule's machine half. Step 4 demands that a confirmed Critical
* carry its executed evidence — the `witness` field, holding either the
* observed output or the verifier's `not run — <reason>` line — and promises
* the demotion is mechanical. This is the mechanism, in the same place the
* test-delta holdback lives: a high-confidence Critical from the one
* non-deterministic source that arrives with no witness is filed at low
* confidence — terminal-only, never posted. Only `source: 'review'` is
* judged: a `[build]`/`[test]`/`[lint]`/`[probe]` finding IS a run's output,
* so its witness is constitutive, not an attachment. Nothing is deleted and
* nothing is raised; the appended sentence names the rule that moved it and
* the way back (attach the witness, or say why none could run). Idempotent by
* construction — a demoted finding re-fed through `--input` is already low
* confidence and is not touched again.
*/
export function holdUnwitnessedCriticals(findings: readonly Finding[]): {
findings: Finding[];
unwitnessed: string[];
} {
const unwitnessed: string[] = [];
const out = findings.map((f) => {
if (
f.severity !== 'Critical' ||
f.confidence !== 'high' ||
f.source !== 'review' ||
f.witness !== undefined
) {
return f;
}
unwitnessed.push(f.id);
return {
...f,
confidence: 'low' as Confidence,
failureScenario: `${f.failureScenario}\n\nFiled at low confidence by the witness rule: this confirmed Critical arrived with neither a witness (the executed evidence that settled the verdict) nor a \`not run — <reason>\` line. Attach either and it stands at high confidence again.`,
};
});
return { findings: out, unwitnessed };
}

const WORKSPACE_IN_COMMAND_RE = /--workspace="([^"]+)"/;

/**
Expand Down Expand Up @@ -888,6 +940,8 @@ export const findingsCommand: CommandModule = {
shared,
));
}
const witnessHold = holdUnwitnessedCriticals(findings);
findings = witnessHold.findings;
Comment on lines +943 to +944

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Missing handler-level integration test for the witness rule — the holdUnwitnessedCriticals call and its stderr disclosure are exercised by unit tests, but there is no handler-level test that feeds an unwitnessed Critical through the findingsCommand pipeline and asserts the confidence is demoted to low and the stderr contains the filed at low confidence message. The existing runCapturingStderr helper and the test-delta stderr test serve as the pattern.

Failure scenario: If the holdUnwitnessedCriticals call is accidentally removed from the handler or moved past buildReport, the witness rule stops working silently — no handler test catches the regression. The existing handler tests all use a base fixture (no source/confidence fields) that defaults to review/high, so every Critical is silently demoted, but no test asserts on the resulting confidence or stderr output.

中文说明

建议 缺少 witness 规则的处理程序级别集成测试——holdUnwitnessedCriticals 调用及其 stderr 披露由单元测试覆盖,但没有任何处理程序级别测试将一个无 witness 的 Critical 通过 findingsCommand 管道输入并断言置信度被降级为 low 且 stderr 包含 filed at low confidence 消息。现有的 runCapturingStderr 辅助函数和 test-delta stderr 测试可作为模板。

失败场景:如果 holdUnwitnessedCriticals 调用被意外从处理程序中移除或被移到 buildReport 之后,witness 规则会静默停止工作——没有任何处理程序测试能捕获此回归。

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in b6e8f34: the handler-boundary test feeds an unwitnessed Critical and a witnessed sibling through findingsCommand.handler via the existing runCapturingStderr pattern, and asserts the demoted confidence, the appended rule sentence, the 'w1 filed at low confidence' stderr line (and its absence for the witnessed one), and the low-confidence count in the report — so unwiring the call, or moving it past buildReport, fails here rather than silently.(已补 handler 级集成测试,钉住接线与 stderr 披露。)

const report = buildReport(findings);

const target = resolve(out);
Expand All @@ -909,6 +963,14 @@ export const findingsCommand: CommandModule = {
`findings: ${h.id} held back from Critical — test-delta measured ${h.file} as failing on the merge base too`,
);
}
// The witness rule's demotions get the same disclosure: a confidence this
// command lowered must name the finding and the rule, or the demotion
// reads as the reviewer's own judgement.
for (const id of witnessHold.unwitnessed) {
writeStderrLine(
`findings: ${id} filed at low confidence — a confirmed Critical carried neither a witness nor a 'not run' reason (Step 4's witness rule)`,
);
}
// A hold that was weighed and reversed is a decision, and a decision this
// command declined to overrule is exactly as reportable as one it made.
for (const r of readjudicated) {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/review/lib/agent-briefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,8 @@ For each finding you were given:

**When the fix IS a threshold, measure the threshold.** A guard built on a ratio or length cutoff makes the fix's coverage an empirical number, not a reading: hold every other variable fixed, vary the guarded quantity, and binary-search the boundary where behaviour flips. Then put that number next to what the linked issue actually reports — a live verification of a prose-ratio guard measured the minimum recovering payload at ~473 chars with the issue's own preamble held fixed, which proved the fix covered the issue's \`edit\`/\`write_file\` half and silently declined its \`run_shell_command\` half. "Fix is narrower than its claim, here is the boundary, here is the half it misses" is a finding no amount of code-reading produces.

**When the defect is mechanically enumerable, sweep the real population — the count is the verdict.** For a claim about a pattern, a predicate, or a parser ("this misclassifies X", "this mishandles shape Y"), do not stop at the one reported instance: run the check over every real instance this repo holds (every workflow step body, every call site, every input the code will actually see) and report the count. "195 of 434 real \`run:\` bodies reach this path" confirms the finding, sizes its severity, and hands the author a number they can re-run rather than argue with — and a count of **zero** is the quoted contradiction that rejects it. Two rules keep a sweep evidence rather than theatre: its oracle must be an **external authority** — the real parser, the real tool, \`bash -n\` — never your own reimplementation of the logic under test, because a mirror shares the blind spots of what it mirrors and mirrored sweeps have manufactured false findings out of their own bugs; and spot-check one hit by reading it before you quote a nonzero count.

**A suggested fix you did not run is a hypothesis; say which one you are giving.** When a finding's fix is cheap to apply, patch it in, re-run the same probe/harness to show it works, then revert — and state that every other number in your report comes from the unmodified PR (the contamination line is what lets a reader trust the rest). A fix too costly to verify is still worth proposing, labeled untested.

**A probabilistic failure gets a RATE, not an anecdote.** For a timing/race claim, run N repetitions per arm and report the rates as the verdict; amplify with full CPU load to force the window open (a live case went from 4/11 idle to 5/5 loaded). And attribute honestly: a lower idle rate with no structural change is luck, not a fix. Fake-timer tests hardcode one ordering by construction — they cannot discriminate a race, so a green fake-timer suite is non-evidence here.
Expand Down Expand Up @@ -696,6 +698,8 @@ Return, for each finding, one verdict:
- **confirmed (low confidence)** — the mechanism is real but the trigger is uncertain (timing, environment, configuration). Say what would confirm it. Carry the severity.
- **rejected** — the code does not do what the finding claims (**quote the contradicting code**), or it matches an Exclusion Criterion (one-line reason).

**A confirmed Critical returns its witness.** Alongside the verdict, include a \`witness:\` line quoting the observed output that settled it — the probe's two sides, the A/B's \`BASE:\`/\`PR:\` pair, the extracted step's run, the sweep count — trimmed to the deciding lines. When every run-capability above is genuinely inapplicable and the confirmation rests on the trace alone, write the one line \`witness: not run — <why no run could settle this claim>\` instead; writing that line is also the moment you notice when the claim was runnable after all. This is mechanical downstream — enforced in code at the findings canonicalization, not merely by the orchestrator's read of its rules: a confirmed Critical returning neither the witness nor the reason line is filed at **low confidence** — terminal-only, never posted — whatever your prose argued, because the evidence a run produced is the one part of a Critical its author can act on without re-deriving the bug.

**Rejecting a Critical carries a higher bar than anything else, and it is one-way.** A rejected Critical is gone — no later stage revisits it, it vanishes from both the pull request and the terminal. To reject one you must **quote the specific code that contradicts the claim**. A passing test, a plausible-looking guard, or "I could not reproduce the reasoning" is not enough — when you cannot quote the contradiction, the floor is \`confirmed (low confidence)\`, never rejection. Downgrading is reversible; a human still sees a low-confidence finding under "Needs Human Review". Rejection is not.

**For anything non-Critical, when uncertain, downgrade to low confidence rather than rejecting.** Reserve outright rejection for a finding that clearly does not match the code (it describes behaviour the code does not have) or matches an Exclusion Criterion. Low confidence is for "likely real, needs human judgement", not for "I have no idea" — a vague suspicion with no concrete evidence in the code can still be rejected.
Expand Down
Loading
Loading