Skip to content

S2.3.6 — The hard-failure block emitter #316

Closed

Description

@dev-pmallapp

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 private hardFailure is deleted and rewired to the renderer, with packages/core/test/risk-mandate.test.ts:30,40 staying 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:1369 red for all four at once, and those assertions encode CLAIM-41.5 and case 12 of 41-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-contract shape 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:

  1. "No module emits it" is false. risk-mandate.ts:48-56 does. There is a fifth copy to absorb, in a package the original body did not contemplate — more work than stated.
  2. "no lint rule requires it" is false. test/skill-lint.test.ts:1369 requires 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.
  3. "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.

Refs #47, #341

Activity

  1. added
    type:taskOne unit of work within a Story
    domain:devSoftware development pack
    iaiCreated or managed by iAI
    on Sep 7, 2026
  2. dev-pmallapp commented on Sep 23, 2026

    @dev-pmallapp
    OwnerAuthor

    iai-evidence

    Verdict: PASS. Artifact: docs/evidence/316-20260923T131945Z.md, pinned at e6f38c7690d238769613ff5ae43390465f7f5cd8. 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-56 already emitted a block; test/skill-lint.test.ts:1369 already 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

    Behaviour Why
    One spec, two enforcement directions — renderer and lint rule read the same exported constants A renderer and a validator agreeing by coincidence are two contracts
    risk-mandate.ts's private copy deleted and rewired, its test untouched It pins the output verbatim, so leaving it alone makes it a regression guard
    Subject line is an open slot, Milestone→Story→Goal - Story: # was unsatisfiable for goal-create, which runs before any Story exists
    checkEvidenceMandate resolves nothing, asserted statically Build target 4 is blocked (#341); only the refusal direction is sound
    The four prose bodies keep restating the block A body is a model's OUTPUT contract, not something a function can emit into

    The 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.

    A length accident was standing in for a content comparison, and every assertion in the test read as though the comparison were pinned.

    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 b0616b9

    963 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.6 has no production caller yet — task-verify (#50) is the first, and must supply the resolution, not compute it · #330 still unpaid.

  3. dev-pmallapp commented on Sep 23, 2026

    @dev-pmallapp
    OwnerAuthor

    PR #342 was merged into story/47-s2-3-execution-skills at b0a7fcfd11ccacd05ba9e5b2546f06bb55a59eb2 (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.6 is anchored but has no production caller until #50 task-verify, which must supply the resolution rather than compute it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devSoftware development packiaiCreated or managed by iAIstatus:resolvedWork complete, awaiting closuretype:taskOne unit of work within a Story

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions