Skip to content

fix: emitter refusal is a failed-gate outcome, not a crash#16

Merged
ryandmonk merged 2 commits into
mainfrom
fix/emitter-refusal-outcome
Jul 3, 2026
Merged

fix: emitter refusal is a failed-gate outcome, not a crash#16
ryandmonk merged 2 commits into
mainfrom
fix/emitter-refusal-outcome

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

Found live by #15's containment, mid-rerun: qwen generated a surface with card-header outside its compound parent — S2-clean (vocabulary gate, sub-components are in-vocabulary), S3-clean (no rule governs stray sub-components), but the a2ui profile cannot emit sub-components standalone, so emitSurface throws its typed EmitSurfaceError. runPipeline treated emission as infallible, so pre-#15 this was a second matrix-killing crash class; post-#15 it was contained as an error run — visible, but misclassified: it's a genuine model-behavior observation, not infrastructure.

The fix: runPipeline catches the typed refusal and finalizes failed-gate, exit 3 — the exit-code table's "target equivalent" of an A-gate failure — with the refusal message recorded in the report (emitted.refusal, additive within report v1; schema + AUDIT.md changelog + markdown rendering updated; validations empty, surfaceMessages absent in that case).

Classification rationale (findings-relevant): this is the ADR-D1 family's third face. A3-rejection (missing triggerLabel) = the emitter emitted something the instance gate refused; typed refusal (stray sub-component) = the emitter couldn't emit at all. Both are "S3 accepted what the target cannot project"; both now land in failed-gate / s3CleanGateFailures and attribute to the projection gap, not to infrastructure.

Rerun recovery: on merge, --resume over the rerun's output directory retries the contained .error.json records under the fixed pipeline — they become real outcomes; nothing else recomputes.

AC: 75 tests green (new: refusal path — outcome/exit/refusal field/schema-valid report).

🤖 Generated with Claude Code

Found live by #15's containment during the 2026-07-03 rerun: qwen produced
a surface with a sub-component outside its compound parent (card-header at
the root level) — in-vocabulary for S2, ungoverned by S3, but the a2ui
profile cannot emit sub-components standalone, so emitSurface throws its
typed EmitSurfaceError and runPipeline (which treated emission as
infallible) crashed the run.

The refusal is the emitter-gate failure class, not an infrastructure error:
runPipeline now catches EmitSurfaceError and finalizes failed-gate (exit 3
— the exit-code table's "target equivalent") with the refusal message in
the report (emitted.refusal, additive in report v1; schema + AUDIT.md
updated; markdown rendering shows it). ADR-D1 family evidence, same class
as an A3 rejection: S3 accepted what the emitter cannot project.

Also makes those rerun runs recoverable: with this merged, --resume retries
their contained .error.json records and records real outcomes.

AC: 75 tests (74 + refusal path: outcome/exit/refusal-field/schema-valid).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 3, 2026 10:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR reclassifies typed A2UI emitter refusals (when a lint-clean surface cannot be projected/emitted at all) from a crash-like condition into a first-class failed-gate outcome (exit code 3), with the refusal message preserved in the audit report for findings/analysis.

Changes:

  • Catch EmitSurfaceError in runPipeline and finalize the run as failed-gate (exit 3) while recording emitted.refusal.
  • Extend audit report v1 (type, JSON schema, markdown renderer) to support emitted.refusal and allow emitted.surfaceMessages to be absent in refusal cases.
  • Add a pipeline test covering the refusal path, including schema validation expectations.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/run/pipeline.test.ts Adds an end-to-end test asserting refusal → failed-gate (exit 3) and emitted.refusal is recorded.
src/run/orchestrator.ts Catches EmitSurfaceError and turns it into a failed-gate result with refusal recorded instead of throwing.
src/audit/report.ts Updates report type and markdown rendering to include the optional emitted.refusal field.
schemas/audit-report.v1.schema.json Adds emitted.refusal to the v1 schema.
docs/AUDIT.md Documents the additive emitted.refusal field and its semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/run/orchestrator.ts Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryandmonk ryandmonk merged commit 34609a4 into main Jul 3, 2026
2 checks passed
@ryandmonk ryandmonk deleted the fix/emitter-refusal-outcome branch July 5, 2026 02:34
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.

2 participants