fix: emitter refusal is a failed-gate outcome, not a crash#16
Merged
Conversation
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>
There was a problem hiding this comment.
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
EmitSurfaceErrorinrunPipelineand finalize the run asfailed-gate(exit 3) while recordingemitted.refusal. - Extend audit report v1 (type, JSON schema, markdown renderer) to support
emitted.refusaland allowemitted.surfaceMessagesto 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.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found live by #15's containment, mid-rerun: qwen generated a surface with
card-headeroutside 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, soemitSurfacethrows its typedEmitSurfaceError.runPipelinetreated emission as infallible, so pre-#15 this was a second matrix-killing crash class; post-#15 it was contained as anerrorrun — visible, but misclassified: it's a genuine model-behavior observation, not infrastructure.The fix:
runPipelinecatches the typed refusal and finalizesfailed-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;validationsempty,surfaceMessagesabsent 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 infailed-gate/s3CleanGateFailuresand attribute to the projection gap, not to infrastructure.Rerun recovery: on merge,
--resumeover the rerun's output directory retries the contained.error.jsonrecords under the fixed pipeline — they become real outcomes; nothing else recomputes.AC: 75 tests green (new: refusal path — outcome/exit/
refusalfield/schema-valid report).🤖 Generated with Claude Code