If AI Writes the Code and AI Reviews the Code, What Exactly Is the Developer Verifying?
When AI generates the feature, the tests, and the review, the developer's job shifts from authoring to adversarial verification. This article dissects the new verification surface — spec fidelity, test oracle integrity, and supply-chain provenance — with a production-grade CI gate that enforces it.
The Problem & Industry Shift
For two decades, the developer's core loop was: read the spec, write the code, write the tests, open the PR, respond to review. Each step was a human act of translation. The bottleneck was never typing speed — it was the cost of being wrong and discovering it late.
Agentic coding tools collapsed the authoring loop. A single prompt now produces a feature branch, a test suite, a changelog entry, and a pull request description. GitHub Copilot's coding agent, for example, can be assigned an issue and will open a draft PR with implementation and tests [1]. Review bots then comment on the same PR. The obvious question follows: if generation and review are both automated, what is the human actually approving?
The naive answer — "the human verifies correctness" — is not actionable. Correctness is not a property you can eyeball in a 600-line diff. The real shift is that the developer's unit of work moves from the statement to the specification, and from the implementation to the oracle. You are no longer verifying that the code does what the author intended; you are verifying that the intent itself is encoded, testable, and provably met.
Three failure modes make this concrete:
- Spec drift. The model optimizes for a plausible interpretation of an ambiguous ticket. It will confidently implement the wrong feature with clean, idiomatic code.
- Oracle collapse. If the same model writes the tests and the implementation, the tests encode the model's assumptions, not the requirement. A green suite proves self-consistency, not correctness.
- Provenance blindness. Generated code pulls in packages, APIs, and patterns that may not exist, may be deprecated, or may be hallucinated.
npm installsucceeds on a typosquatted name.
The industry shift is that verification is now the primary engineering activity, and it must be mechanized. Human review of AI-generated diffs at volume does not scale; you need gates that fail closed.
Architecture & Core Mechanics
The verification surface decomposes into four independent layers. A defect must pass through all four to reach production, so each layer is a distinct control point.
┌─────────────────────────────────────────────┐
Issue/Spec ──▶│ L1: SPEC FIDELITY │
│ - acceptance criteria are executable │
│ - ambiguity flagged, not guessed │
└───────────────┬─────────────────────────────┘
│ generated diff + tests
▼
┌─────────────────────────────────────────────┐
│ L2: ORACLE INTEGRITY │
│ - tests authored from spec, not from impl │
│ - mutation testing kills weak assertions │
└───────────────┬─────────────────────────────┘
│ verified behavior
▼
┌─────────────────────────────────────────────┐
│ L3: PROVENANCE & SUPPLY CHAIN │
│ - SBOM diff, lockfile pinning, signatures │
└───────────────┬─────────────────────────────┘
│ buildable artifact
▼
┌─────────────────────────────────────────────┐
│ L4: HUMAN ADVERSARIAL REVIEW │
│ - review the *spec* and the *gates*, │
│ not the diff line-by-line │
└─────────────────────────────────────────────┘
L1 — Spec fidelity. The ticket must contain machine-checkable acceptance criteria. "Users can reset their password" is not verifiable. "Given a valid email, POST /reset returns 202 and enqueues exactly one job within 500ms" is. The gate rejects PRs whose linked issue lacks structured criteria. This forces the human to do the one thing the model cannot: decide what "done" means.
L2 — Oracle integrity. The critical rule: tests must be derived from the spec, not from the implementation. In practice this means the test-generation prompt receives the acceptance criteria and the public interface, never the generated function body. Then mutation testing validates the oracle: if you mutate the implementation and the suite stays green, the tests are decorative. Mutation score becomes a merge gate, not a vanity metric [2].
L3 — Provenance. Every generated dependency must resolve to a pinned, signed artifact. The gate diffs the SBOM against the base branch and requires human approval for any new direct dependency. This is the layer that catches hallucinated packages and typosquats before they reach CI runners with credentials.
L4 — Human adversarial review. The human no longer reads every line. They read the spec, the acceptance criteria, the mutation report, and the SBOM delta. Their job is to find the case the model did not consider — the adversarial input, the race condition, the regulatory constraint. This is judgment work, and it is the only layer that cannot be automated away.
Production Code Example
The following is a realistic CI gate that enforces L1–L3. It is written in TypeScript and runs as a GitHub Action step. It fails closed: any missing signal blocks the merge.
// verify-pr.ts — runs on every AI-generated PR before human review.
// Design decisions are annotated; this is a gate, not a linter.
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
interface AcceptanceCriterion {
id: string;
given: string;
when: string;
then: string;
// A criterion is only verifiable if it names a test that asserts it.
testRef: string;
}
interface GateResult {
layer: "spec" | "oracle" | "provenance";
passed: boolean;
detail: string;
}
// L1: Parse structured criteria from the linked issue body.
// We require a fenced ```acceptance block so parsing is deterministic.
function checkSpecFidelity(issueBody: string): GateResult {
const match = issueBody.match(/```acceptance\n([\s\S]*?)```/);
if (!match) {
return { layer: "spec", passed: false, detail: "No ```acceptance block in issue." };
}
const criteria: AcceptanceCriterion[] = JSON.parse(match[1]);
const unbound = criteria.filter((c) => !c.testRef);
if (unbound.length > 0) {
// Unbound criteria are the #1 source of spec drift.
return {
layer: "spec",
passed: false,
detail: `Criteria without testRef: ${unbound.map((c) => c.id).join(", ")}`,
};
}
return { layer: "spec", passed: true, detail: `${criteria.length} criteria bound.` };
}
// L2: Mutation testing proves the oracle can detect faults.
// Threshold is intentionally high: weak suites are worse than none.
function checkOracleIntegrity(): GateResult {
const report = JSON.parse(readFileSync("reports/mutation.json", "utf8"));
const score: number = report.mutationScore; // 0..100
const MIN_SCORE = 80;
if (score < MIN_SCORE) {
return {
layer: "oracle",
passed: false,
detail: `Mutation score ${score} < ${MIN_SCORE}. Tests do not constrain behavior.`,
};
}
return { layer: "oracle", passed: true, detail: `Mutation score ${score}.` };
}
// L3: Diff the SBOM; any new direct dependency needs human sign-off.
function checkProvenance(baseRef: string): GateResult {
const baseSbom = JSON.parse(
execSync(`git show ${baseRef}:sbom.json`).toString()
);
const headSbom = JSON.parse(readFileSync("sbom.json", "utf8"));
const baseNames = new Set(baseSbom.dependencies.map((d: any) => d.name));
const added = headSbom.dependencies.filter((d: any) => !baseNames.has(d.name));
if (added.length > 0) {
return {
layer: "provenance",
passed: false,
detail: `New dependencies require approval: ${added.map((d: any) => d.name).join(", ")}`,
};
}
return { layer: "provenance", passed: true, detail: "No new dependencies." };
}
function main(): void {
const issueBody = process.env.ISSUE_BODY ?? "";
const baseRef = process.env.BASE_REF ?? "origin/main";
const results: GateResult[] = [
checkSpecFidelity(issueBody),
checkOracleIntegrity(),
checkProvenance(baseRef),
];
for (const r of results) {
console.log(`[${r.passed ? "PASS" : "FAIL"}] ${r.layer}: ${r.detail}`);
}
if (results.some((r) => !r.passed)) {
// Fail closed. A blocked PR is cheap; a bad merge is not.
process.exit(1);
}
}
main();
Two decisions matter most here. First, the gate reads the issue body, not the PR description — the model can rewrite a PR description to be persuasive, but it cannot rewrite the ticket the human approved. Second, mutation score is a hard gate. A suite that passes while the implementation is mutated is not a test suite; it is a rubber stamp.
Performance, Cost & Trade-offs
Every gate adds latency to the merge path. The engineering question is where to spend it.
| Layer | Typical added latency | Cost driver | What it catches |
|---|---|---|---|
| L1 Spec fidelity | < 1s | Parsing only | Ambiguous tickets, unbound criteria |
| L2 Mutation testing | 3–15 min | CPU-bound; scales with test count | Weak or self-referential oracles |
| L3 Provenance | 5–30s | SBOM diff + registry lookups | Hallucinated/typosquatted packages |
| L4 Human review | 10–30 min | Senior engineer attention | Adversarial cases, domain constraints |
Mutation testing is the expensive gate. On a mid-sized service with 2,000 tests, a full mutation run can take 15 minutes and consume significant CPU. The standard mitigation is incremental mutation: only mutate lines changed in the diff, using a tool like Stryker with --since [2]. This drops the run to under two minutes for typical PRs while preserving the signal on the code that actually changed.
The deeper trade-off is recall versus velocity. Tightening L2 to a 90% mutation score will block more PRs, including some that are genuinely fine. That is the correct bias when the author is a model: false negatives (bad code merged) are far more expensive than false positives (a human re-examines a good PR). But it is a policy decision, and it should be explicit in the repo's CONTRIBUTING.md, not buried in a CI config.
Security considerations are non-negotiable at L3. AI-generated code has been observed to suggest packages that do not exist, which attackers register as typosquats — a documented supply-chain attack class tracked by CISA and NIST guidance on software supply chain security [3]. Pinning dependencies and verifying signatures via Sigstore or SLSA provenance is the only durable defense [4].
Finally, cost: running generation, review, and mutation testing on every PR is not free. A reasonable budget is to run L1 and L3 on every PR (cheap, fast) and L2 only on PRs that touch production code paths, gated by path filters. This keeps the median PR under five minutes while still failing closed on the risky ones.
Actionable Checklist / Summary
When adopting AI-generated code and AI review in production, do the following:
- Make acceptance criteria executable. Require a structured
acceptanceblock in every issue; reject PRs whose criteria lack atestRef. - Separate the oracle from the implementation. Generate tests from the spec and public interface only — never feed the model its own implementation when writing tests.
- Gate on mutation score, not coverage. Coverage measures execution; mutation score measures detection. Enforce a minimum (80–90%) on changed lines.
- Diff the SBOM on every PR. Any new direct dependency requires explicit human approval. Pin and verify signatures.
- Review the spec and the gates, not the diff. The human's job is adversarial: find the case the model missed, the constraint it ignored, the input it did not consider.
- Fail closed. A blocked PR costs minutes; a bad merge costs days. Default to blocking when a signal is missing.
- Document the policy. Put the verification contract in
CONTRIBUTING.mdso it is a team decision, not a CI accident.
The developer is not verifying code anymore. They are verifying that the system of verification is sound — that the spec is unambiguous, the oracle is adversarial, and the supply chain is pinned. That is a harder, more valuable job than reading diffs.
References
- [1] GitHub Docs — About Copilot coding agent: https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent
- [2] Stryker Mutator — Incremental mutation testing documentation: https://stryker-mutator.io/docs/stryker-js/incremental/
- [3] NIST SP 800-218 — Secure Software Development Framework (SSDF): https://csrc.nist.gov/pubs/sp/800/218/final
- [4] SLSA — Supply-chain Levels for Software Artifacts specification: https://slsa.dev/spec/v1.0/levels
- [5] OWASP — Software Supply Chain Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Software_Supply_Chain_Security_Cheat_Sheet.html