Skip to content

feat(ce-review): validate the review run artifact against an executable schema - #830

Merged
marcusrbrown merged 7 commits into
mainfrom
fix/review-artifact-executable-contract
Aug 23, 2026
Merged

feat(ce-review): validate the review run artifact against an executable schema#830
marcusrbrown merged 7 commits into
mainfrom
fix/review-artifact-executable-contract

Conversation

@marcusrbrown

Copy link
Copy Markdown
Owner

ce:review writes two kinds of artifact. Per-persona records are validated by the parent before it persists them. The run-level review-summary.json — which carries the verdict, the input ledger, and the reconciliation arithmetic — is validated by nothing.

The difference is visible on disk. Across 26 run directories:

Path Files Distinct shapes Shapes per file
Per-persona, parent-validated 138 21 0.15
Run-level, unvalidated 7 7 1.00

111 of the 138 per-persona files share a single shape. No two run-level artifacts have ever shared one, and the seven are split across two different filenames — even the name drifted. The fields the contract documents most carefully, input_findings and disposition_counts and run_status, appear in none of them. verdict appears in all four review-summary.json files and was absent from the contract entirely.

These two paths are not a clean experiment: per-persona files are written many per run, the run-level shape was redesigned twice recently, and the per-persona schema only became strict lately. The measurement is a strong correlation between an unchecked write path and shape divergence, not proof of cause. It was enough to act on.

What this changes

The shape now has a Zod source of truth. The heterogeneous ledger is a discriminated union on record_type, so admitted rows and rejected-payload summaries are machine-separable rather than inferred from which keys happen to be present. Shared vocabulary is composed rather than copied. Every user-influenced string carries a length bound. Invariants JSON Schema cannot express are enforced directly — a rejection summary must report at least one rejected finding, and its severity list must match its count.

A committed JSON Schema is generated from that source behind a drift gate. It ships under skills/ for linking and inspection but is deliberately never inlined into a persona prompt, where it would add roughly 10 KB to every reviewer dispatch for content no reviewer reads.

Validation is exposed as systematic validate-review-artifact <path>, which ships in dist/ and so reaches every consumer. Four exit statuses distinguish outcomes that would otherwise collapse into "failed": valid, schema violation, operational problem, and an artifact predating the contract. That last one is what makes the historical corpus excludable by a machine rather than by a note.

The contract now has the parent stamp a version and run the validator after writing, with the failure path stated: repair and re-run, never report a verdict over an artifact that failed, and never delete the artifact to escape the check.

What it does not do

The contract says this plainly, and so does this description: an agent that never runs the command can still finalize an artifact. This is enforcement by visible failure, not by containment. The durable value is that the check is independently runnable — a human, a CI job, or a later audit can verify any artifact without the producing agent's cooperation. Claiming more would repeat a documented mistake in this repository, where a validator a sub-agent could decline to call was treated as enforcement.

On the original issue

#793 asked for a schema_version field, a compatibility path for in-flight reviewers, and a decision on the historical corpus.

The first was premature. There was no conforming producer to version — a version field describes a shape, and no stable shape existed. It lands here anyway, at the end rather than the beginning, because at creation time it costs nothing.

The second is not reachable the way the issue assumed. The parent inlines the schema into each persona's dispatch prompt and then validates returns against that same bundled file, so producer and consumer are one version by construction within a run. The reachable failure is a model ignoring the contract, which a version field cannot detect.

The third is resolved as an explicit exclusion. Seven synthesis artifacts, two filenames, no two shapes alike, and 19 of 26 runs with no synthesis artifact at all. A reader for that would need seven special cases and still could not answer the questions the corpus exists to answer.

Verification

1943 unit tests pass. Typecheck, lint, content-integrity, and all three drift gates clean. End to end through the built binary under Node: a legacy artifact exits 3, a conforming artifact exits 0, a missing argument exits 2, and a JSON array exits 1 rather than being excused as legacy.

Two review rounds ran against the plan before implementation and one against the code after. The code round found a real escape hatch — anything parsing as JSON but not an object was classified as merely old and excluded from scrutiny instead of failing.

Closes #793

ce:review writes two kinds of artifact. Per-persona records are validated by
the parent before persistence. The run-level review-summary.json is validated
by nothing, and the difference shows on disk: across 26 runs, 138 per-persona
files share 21 shapes with 111 of them identical, while 7 run-level artifacts
have 7 distinct shapes and were written under two different filenames.

The plan gives the run artifact a Zod source of truth, generates a committed
JSON Schema from it behind a drift gate, and exposes validation as a CLI
command that ships to consumers. Two review rounds reshaped it: the original
design put the validator in scripts/, which is not published, so no consumer
could ever run it.
review-summary.json carries the verdict, the input ledger, and the
reconciliation arithmetic for a review run, but nothing has ever checked its
shape. findings-schema.json covers only the per-persona record. The result is
that no two run artifacts on disk share a shape, and the fields the contract
documents most carefully — input_findings, disposition_counts, run_status —
appear in none of them.

The shape now has a Zod source of truth, with the ledger modeled as a
discriminated union on record_type so admitted rows and rejected-payload
summaries are machine-separable without inferring from which keys happen to be
present. Shared vocabulary is composed rather than duplicated, every
user-influenced string carries a length bound, and cross-field invariants that
JSON Schema cannot express are enforced directly: a rejection summary must
report at least one rejected finding, and its severity list must match its
count.

The committed JSON Schema is generated from that source and gated against
drift. It ships under skills/ for linking and inspection but is deliberately
not inlined into any persona prompt, where it would add roughly 10 KB to every
reviewer dispatch for content no reviewer uses.

The generator derives its options from one shared helper used by both the write
and check paths, and reads the committed file from disk rather than through a
static import. Both guard against failures this repository has already hit
once, recorded in docs/solutions/best-practices.

Fixtures capture the seven historical shapes, sanitized of review content, so
the measured non-conformance is pinned without depending on local state that
git ignores.
A schema nothing runs is documentation. This exposes validation as
`systematic validate-review-artifact <path>`, which ships in dist/ and so
reaches every consumer — the earlier design put it in scripts/, which the
published package does not include, so it would have worked only inside this
repository.

Four exit statuses let a caller tell outcomes apart that would otherwise
collapse into "failed": valid, schema violation, operational problem such as a
missing file or malformed JSON, and an artifact predating the contract with no
schema_version. That last one is what makes the historical corpus excludable
by a machine rather than by a note.

Error output names the JSON path and issue code and nothing else. Zod does not
give that property for free — its issues carry generated prose that can
interpolate values, plus fields like expected and maximum, and reportInput
would add the raw input outright. Since these artifacts quote source code, each
issue is projected to an explicit allowlist. The one exception is our own
refinement messages, which are authored constants with no artifact data in
them, and which would otherwise be reduced to a bare "custom" code that says
nothing.

The path argument is required and must resolve inside the artifact directory,
which keeps the command from doubling as a probe for whether arbitrary files
exist and parse.
…rites

The parent already treats every reviewer return as untrusted and validates it
before persisting. It applied none of that discipline to the artifact it writes
itself, which is why every run has been free to invent a shape.

The contract now has the parent stamp schema_version and run the validator
after writing, with the failure path stated: repair and re-run, never report a
verdict over an artifact that failed, and never delete the artifact to escape
the check, because a failing artifact is evidence.

It also says plainly what this does not do. An agent that never runs the
command can still finalize an artifact. The value is that the check is
independently runnable, so a human, a CI job, or a later audit can verify any
artifact without the producing agent's cooperation — not that compliance is
guaranteed. Claiming otherwise would repeat a documented mistake in this
repository, where a validator a sub-agent could decline to call was treated as
enforcement.

The historical corpus is excluded by an explicit decision rather than a
half-trusted read: 7 synthesis artifacts across 26 runs, two filenames, no two
shapes alike, and 19 runs with no synthesis artifact at all. A reader for that
would need seven special cases and still could not answer the questions the
corpus exists to answer.
Review found the legacy classifier too permissive. It answered yes for null and
for any non-object, so a JSON array or a bare string parsed cleanly and exited
with the legacy status — reported as predating the contract and excluded from
analysis rather than flagged as broken. Legacy now means an object without a
version field. Everything else fails.

The exception that lets authored refinement messages through the output
whitelist was safe by convention only. Both messages are now named constants
the schema and its tests share, so a future refinement that invents its own
message string fails a test rather than quietly widening what the validator is
allowed to print.

The finding type modeled its filtered case at runtime but not in the type: a
finding could be marked unvalidated with no reason and still typecheck, then
fail validation. It is a discriminated union now, so that state cannot be
constructed.

Two tests proved less than they claimed. The no-echo test asserted that a few
sample strings were absent, which a regression printing some other Zod field
would have passed; it now asserts the exact output shape. The historical
fixture test passed if a version issue appeared anywhere in the error list,
which would hold even when a fixture failed for an unrelated reason; it now
asserts each fixture's expected failure.

The command also documents its four exit statuses and points at the schema on
failure, because its caller is an agent expected to repair the artifact and
retry, and a bare issue code at a JSON path is not enough to act on. The drift
check no longer ignores trailing whitespace, which let a committed schema
missing its final newline report as current.

@fro-bot fro-bot left a comment

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.

Verdict: PASS

This PR gives review-summary.json — previously the only unvalidated artifact ce:review writes — a Zod source of truth, a generated JSON Schema behind a committed drift gate, and an independently runnable systematic validate-review-artifact <path> CLI command. I verified the claims rather than just reading the description:

  • bun run typecheck — clean.
  • bun run lint — clean for every file this PR touches (the 11 pre-existing complexity warnings are all in receipt-readback.ts, workflow-guard.ts, and unrelated test files, none of which are in this diff).
  • bun test tests/unit1943 pass, 0 fail, matching the PR description exactly.
  • bun run review-schema:drift and bun run registry:drift — both clean.
  • New file conventions: node: protocol imports, .js extensions on relative imports, zero any/@ts-ignore, functions only (no classes) — all followed.

Code quality highlights

  • src/lib/review-artifact-schema.ts: the heterogeneous ledger is a proper discriminated union (AdmittedInputFindingSchema / RejectedInputFindingSchema) rather than an inferred shape from optional fields, and the two invariants JSON Schema can't express (severity-count-matches-count, filtered-requires-reason) are enforced via superRefine with authored, tested messages (REVIEW_ARTIFACT_CUSTOM_MESSAGES).
  • src/cli.ts validate-review-artifact: path handling is genuinely defense-in-depth — rejects .. traversal in the raw argument, walks every path segment for symlinks before resolving, and independently confirms the realpath'd target is still inside .context/systematic/ce-review via isWithinDirectory. Error output never echoes artifact content (verified by the "does not echo artifact strings" test), which matters since this reads content from potentially adversarial persona/user-influenced fields.
  • Exit-status design (0/1/2/3) correctly distinguishes "legacy, pre-contract artifact" from "actually malformed" — a JSON array, bare string, number, and null are all correctly treated as validation failures (exit 1), not excused as legacy (exit 3). This is exactly the escape hatch the description says the code-round review caught, and it's now pinned down by four explicit tests.
  • The generator (scripts/generate-review-artifact-schema.ts) reads the committed file at check-time rather than importing it, avoiding ESM module-cache staleness — a subtle correctness detail that's easy to get wrong.

Non-blocking concerns

  • src/lib/AGENTS.md's module tables (Discovery/Loading/Config & Integration Layer) weren't updated to include the new review-artifact-schema.ts module, even though ARCHITECTURE.md was. Minor doc-completeness gap, not a functional issue.
  • pathContainsSymlink in src/cli.ts walks the path manually for symlinks even though the subsequent realpathSync + isWithinDirectory check would already reject an escaping symlink target. This is intentional stricter-than-necessary behavior (rejects symlinks unconditionally, not just escaping ones, which also closes a TOCTOU window) — worth a one-line comment explaining why both checks exist, but not a bug.
  • tests/unit/generate-review-artifact-schema.test.ts mutates the real committed schema file in place (restored via try/finally); consistent with this repo's existing drift-test pattern, so not a new risk, just noting it for anyone unfamiliar with the convention.

Missing tests

None identified. Coverage is unusually thorough: conforming fixture, 7 historical fixtures with exact expected issue signatures per fixture, symlink escape, parent-directory traversal, non-regular-file target, missing file, malformed JSON, missing argument, custom-message preservation, and secret-non-echo behavior are all directly tested.

Risk assessment: LOW

Purely additive — new schema module, new CLI subcommand, new CI gate, docs/skill updates. No existing code paths are modified in a way that changes behavior (the SKILL.md/contract doc changes add a validation requirement on the parent's own write path, not a breaking change to any consumer). The CLI command is scoped to a fixed subdirectory with fail-closed path resolution, so the security blast radius of a malicious <path> argument is well contained and tested. Blast radius of a schema defect would be limited to the validator rejecting/accepting artifacts incorrectly — verifiable and low-severity, not silently corrupting data.


Run Summary
Field Value
Event pull_request
Repository marcusrbrown/systematic
Run ID 32592191594
Cache hit
Session ses_fd52b557bffe0zkHfhtAUufqfh

Review noted the new module reached the architecture codemap but not the
module table in src/lib/AGENTS.md, which is where someone working in that
directory looks first.

Also records why the symlink rejection and the realpath containment check both
exist. They overlap, and the overlap is deliberate: containment rejects a
symlink whose target escapes the artifact root, while the segment walk rejects
every symlink in the path outright and narrows the window between checking and
reading. Without the note the next reader would reasonably delete one.

@fro-bot fro-bot left a comment

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.

Verdict: PASS

Re-review after the 5eb886e docs(lib): register the review artifact schema module push. This commit addresses both non-blocking notes from my prior review (run 32592191594):

  • src/lib/AGENTS.md now documents review-artifact-schema.ts in the Config & Integration Layer table, including its exports and a description of the discriminated-union ledger and superRefine invariants.
  • pathContainsSymlink in src/cli.ts now carries a comment explaining why it's deliberately redundant with the later realpathSync + isWithinDirectory containment check (narrows the symlink-swap TOCTOU window rather than being dead logic).

Re-ran verification after the push:

  • bun run typecheck — clean.
  • bun test tests/unit/review-artifact-schema.test.ts tests/unit/generate-review-artifact-schema.test.ts tests/unit/validate-review-artifact.test.ts — 38 pass, 0 fail.

No new concerns from this diff (src/cli.ts +4 comment lines, src/lib/AGENTS.md +1 table row — no behavior change). All findings from the prior full review still stand: discriminated-union schema design, defense-in-depth CLI path handling, correct legacy-vs-malformed exit-status separation, and thorough test coverage.

Blocking issues

None

Non-blocking concerns

None — both items from the prior review round were resolved in this commit.

Missing tests

None identified.

Risk assessment: LOW

This commit is documentation and a code comment only; no functional or behavioral change. The underlying feature remains purely additive (new schema module, new CLI subcommand, new CI gate) with no changes to existing consumer-facing behavior.


Run Summary
Field Value
Event pull_request
Repository marcusrbrown/systematic
Run ID 32594669442
Cache hit
Session ses_fd52b557bffe0zkHfhtAUufqfh

@marcusrbrown
marcusrbrown enabled auto-merge (squash) August 23, 2026 05:37

@fro-bot fro-bot left a comment

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.

Verdict: PASS

Re-review after d22a37e, a merge of main into this branch. This is a housekeeping merge only — it pulls in an unrelated main commit (cbbe54d, a fro-bot/agent version bump touching .github/workflows/fro-bot.yaml, bun.lock, package.json). It does not touch any file this PR authored or modifies the feature in any way.

Verified directly rather than assuming:

  • git show d22a37e --stat — confirms the merge only carries in main's unrelated dependency-bump diff (3 files, all outside this PR's scope).
  • bun run typecheck — clean.
  • bun test tests/unit/review-artifact-schema.test.ts tests/unit/generate-review-artifact-schema.test.ts tests/unit/validate-review-artifact.test.ts — 38 pass, 0 fail.
  • Confirmed the PR's own package.json additions (review-schema:generate, review-schema:drift) survived the merge intact, no conflict markers, no silent overwrite.

All findings from the two prior review rounds still stand and remain resolved: discriminated-union schema design, defense-in-depth CLI path handling with documented rationale, correct legacy-vs-malformed exit-status separation, thorough test coverage, and both previously-flagged non-blocking items (AGENTS.md registration, symlink-check comment) remain fixed.

Blocking issues

None

Non-blocking concerns

None

Missing tests

None identified.

Risk assessment: LOW

This change is a merge-only sync with main; it introduces zero new lines of feature logic. The underlying feature remains purely additive (new schema module, new CLI subcommand, new CI gate) with no changes to existing consumer-facing behavior.


Run Summary
Field Value
Event pull_request
Repository marcusrbrown/systematic
Run ID 32620794191
Cache hit
Session ses_fd52b557bffe0zkHfhtAUufqfh

@marcusrbrown
marcusrbrown merged commit 505ae58 into main Aug 23, 2026
12 checks passed
@marcusrbrown
marcusrbrown deleted the fix/review-artifact-executable-contract branch August 23, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ce:review findings schema has no version, breaking in-flight reviewers and the historical corpus

2 participants