Repository navigation
S2.3.6 — The hard-failure block emitter #316
Description
Parent: #47
Build target 7 of docs/design/stories/47.md.
Anchors: CLAIM-47.6. Cases: 8.
CORRECTED 2026-09-23, before work started. Two statements in the original body were false against the tree, and one Done-when bullet proposed work a currently-green test forbids. The original text is preserved in the Corrections section at the bottom; the body above it is the restated version. Gate rulings G-a, G-b and G-c are recorded in #47 (comment).
The hard-failure block has five copies and a rule too loose to police them
docs/design/02-roles.md:276-282 specifies the house shape:
HARD FAILURE in Phase {N} ({skill}):
- Story: #{story}
- Expected: {what should exist}
- Found: {what was actually found}
- Action: Pipeline cannot continue. Fix and re-run.
Five copies exist, not seven. Four are prose — skills/goal-create/SKILL.md:36-42, story-create:45-51, story-design:43-49, story-test-plan:40-46. The fifth is TypeScript: packages/core/src/guards/risk-mandate.ts:48-56 has a private hardFailure(story, rung) that emits a canonical block, reachable only through checkRiskMandate, which no production code calls.
Five more copies exist that are OUT OF SCOPE and must not be touched: four illustrative examples in design documents (02-roles.md:287's worked example, 03-workflow.md:499, 05-domain-trading.md:217, 06-domain-health.md:290) and one immutable copy at docs/evidence/19-*.md:55.
A lint rule already requires the block — test/skill-lint.test.ts:1369 requires every real skill body to match HARD_FAILURE_BLOCK_RE (:1311), and :1442-1459 asserts - Expected: .+ and - Found: none inside the matched block. The gap is that the rule is too loose, not that it is absent:
/^HARD FAILURE in Phase \d+ \([^)]+\):\n(?:- .+\n?)+/m
It checks neither field order, nor the Action: line, nor the invariant prefix. That is why the subject-line divergence below has gone unpoliced.
What actually varies across the five copies, measured
The action line is more invariant than it looks. All five preserve the literal prefix - Action: Pipeline cannot continue. and vary only the trailing imperative — "Supply the goals source", "Declare the domain", "Label the Story", "Supply the missing input", "Fix". The variable part is one trailing slot.
The subject line genuinely diverges, and ruling G-a resolves it: goal-create emits - Goal: <id>, story-create emits - Milestone: <milestone>, the other two emit - Story: #<n>. goal-create runs before any Story exists, so the template's fixed - Story: #{story} is unfillable there. G-a rules the template wrong: the subject is an open slot whose key comes from an ordered vocabulary, Milestone → Story → Goal.
Sequenced second, before any body
All three of task-create, task-do and task-verify need it, and CLAIM-47.6's refusal is expressed through it. Building it after the bodies means writing the copies first and retiring them second.
Case 8 is partially dischargeable here, and that is ruled
Build Target 4 — the claim-to-artifact resolver — does not exist, and this task does not own it. Under G-c it is now known to be unbuildable as specified (#341). Under G-b, case 8 is recorded as partially discharged: the refusal needs only the sound direction (token absent from every artifact ⇒ certainly uncovered), and the positive direction is carried to BT4.
The sound-direction scan must not ship as a resolver. The refusal site takes an already-determined resolution as a supplied parameter and reads nothing from disk — the seam risk-mandate.ts:58-60 already uses.
Done when
- One shared spec, consumed in two directions: a TypeScript renderer produces conformant blocks, and the lint rule validates prose against the same spec
-
risk-mandate.ts's privatehardFailureis deleted and rewired to the renderer, withpackages/core/test/risk-mandate.test.ts:30,40staying green untouched — it pins the exact string and is the regression guard that the refactor is behaviour-preserving - The lint rule is tightened to require field order, the subject key from G-a's vocabulary, and the
Action:invariant prefix - The four prose bodies keep restating the block, per the original bullet's second branch; G-a is the recorded reason. Reconciling them to cite it would turn
skill-lint.test.ts:1369red for all four at once, and those assertions encode CLAIM-41.5 and case 12 of41-plan.md— a closed Story with committed evidence -
CLAIM-47.6's refusal emits the block, as a pure function over a supplied resolution - Case 8 passes as far as G-b allows, with the
docs/evidence/denominator read at run time and asserted non-zero first, and an explicit carry row naming BT4 - A negative fixture proves the emitter fires, per the S1.3.5 — Purity enforcement for the gh directory #253 / S1.4.5 — Purity enforcement for the evidence directory #261 / S1.5.4 — Purity enforcement and the domain-pack import ban #272 posture — malformed in a way the current loose regex accepts, so one fixture proves the new rule fires and the old rule was insufficient
-
docs/design/02-roles.md's template restated per G-a
Corrections
The original body said:
All four shipped skills reproduce it by hand — … No module emits it, no lint rule requires it, and nothing parses it.
S2.3 adds three more bodies. That would make seven hand-copies of one contract, which is the
duplicate-contractshape the repository already polices in the other direction.
and its first Done-when bullet read:
- One renderer produces the block; the four existing bodies are reconciled to cite it rather than restate it, or the decision to leave them is recorded with a reason
Three errors:
- "No module emits it" is false.
risk-mandate.ts:48-56does. There is a fifth copy to absorb, in a package the original body did not contemplate — more work than stated. - "no lint rule requires it" is false.
test/skill-lint.test.ts:1369requires it. The deliverable is tightening an existing rule, not writing a new one. This is the critical correction: left standing, an implementer starts down the reconciliation path and hits a wall. - "seven hand-copies" is wrong in both directions — it undercounts the corpus (ten copies exist) and overcounts the scope (five are in scope). Stated as written, someone edits a design document or an immutable artifact.
Activity
dev-pmallapp commentedon Sep 23, 2026
iai-evidence
Verdict: PASS. Artifact:
docs/evidence/316-20260923T131945Z.md, pinned ate6f38c7690d238769613ff5ae43390465f7f5cd8. PR #342.Build target 7. Anchors
CLAIM-47.6. Case 8 PARTIALLY discharged per gate ruling G-b.The issue body was wrong, and one of its bullets was unbuildable
#316 said "No module emits it, no lint rule requires it, and nothing parses it." Two of three were false.
risk-mandate.ts:48-56already emitted a block;test/skill-lint.test.ts:1369already required one in every skill body.So its first Done-when bullet — reconcile the four prose bodies to cite the block — proposed work a green test forbids, over assertions encoding CLAIM-41.5 and case 12 of
41-plan.md, a closed Story with committed evidence. The second branch was taken and the body corrected before work started.What shipped
risk-mandate.ts's private copy deleted and rewired, its test untouchedMilestone→Story→Goal- Story: #was unsatisfiable forgoal-create, which runs before any Story existscheckEvidenceMandateresolves nothing, asserted staticallyThe mutation round found a fixture passing for the wrong reason
12 run, 12 killed — 11 on the first pass. M5 deleted the Action line's invariant-prefix check outright and the suite stayed green: the fixture written to pin that check was shorter than the 36-character prefix, so the surviving
.slice(PREFIX.length)yielded""and the blank-remainder branch rejected it regardless.The replacement miscases one word and stays long enough that the slice is non-blank either way, so only a real comparison can reject it — and the test now asserts that, so the accident cannot return silently. Same class as
docs/evidence/33-*.md, re-hit in #315.Chain at
b0616b9963 tests / 949 pass / 0 fail / 14 skip / 3326
expect()· lint 0 across 98 files · typecheck clean · skill-lint 0 errors, 18 rules · claim-lint 116 files, 0 errors, 1 warning (expected) · purity 9, binding sweep 5 · both verifiers PASS at six · CI 7/7.Not closed here
Case 8's positive direction (carried to build target 4, #341) ·
CLAIM-47.6has no production caller yet —task-verify(#50) is the first, and must supply the resolution, not compute it · #330 still unpaid.dev-pmallapp commentedon Sep 23, 2026
PR #342 was merged into
story/47-s2-3-execution-skillsatb0a7fcfd11ccacd05ba9e5b2546f06bb55a59eb2(2026-09-23T13:33:58Z).Closing as completed. Evidence:
docs/evidence/316-20260923T131945Z.md.A task PR targets the story branch, so closing keywords never fire (
CONTRIBUTING.md, Pull requests) — hence this explicit close citing the merge commit.Build target 7 is done. Case 8 remains partially discharged per gate ruling G-b on #47: the refusal direction is proven, and the positive direction is carried to build target 4, blocked on #341.
CLAIM-47.6is anchored but has no production caller until #50task-verify, which must supply the resolution rather than compute it.