Skip to content

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

Merged
dev-pmallapp merged 4 commits into
story/47-s2-3-execution-skillsfrom
task/316-hard-failure-block-emitter
Sep 23, 2026
Merged

dev-pmallapp merged 4 commits into
story/47-s2-3-execution-skillsfrom
task/316-hard-failure-block-emitter

Conversation

@dev-pmallapp

Copy link
Copy Markdown
Owner

Build target 7 of docs/design/stories/47.md. Anchors CLAIM-47.6.

Evidence: docs/evidence/316-20260923T131945Z.md.

The task began by finding its own issue body wrong

#316 said "No module emits it, no lint rule requires it, and nothing parses it." Two of the three were false, and the second one would have wasted the task:

  • packages/core/src/guards/risk-mandate.ts:48-56 already emitted a block — a private fifth copy, in TypeScript.
  • test/skill-lint.test.ts:1369 already required the block in every real skill body.

So #316's first Done-when bullet — reconcile the four prose bodies to cite the block rather than restate it — proposed work a green test forbids. skill-lint.test.ts:1362 loops the whole real corpus, and those assertions encode CLAIM-41.5 and case 12 of 41-plan.md, a closed Story with committed evidence. It would have retro-invalidated a discharged claim.

The bullet's second branch was taken instead, and the issue body was corrected before work started. Gate rulings G-a/G-b/G-c are recorded on #47.

What shipped

One spec, two enforcement directions. packages/core/src/guards/hard-failure.ts holds the contract once. renderHardFailure produces conformant blocks; skill-lint's new hard-failure-block rule (18th) validates prose against the same exported constants. A renderer and a validator that agree only by coincidence are two contracts — which is the shape this task existed to remove.

risk-mandate.ts's private copy is deleted and rewired, and its test is untouched and still green — it pins the output verbatim, so leaving it alone is what makes it a regression guard. Mutation M12 confirms it is load-bearing.

checkEvidenceMandate is the reader the doctrine at references/evidence-artifacts.md:140-144 never had.

The subject line is an open slot keyed Milestone → Story → Goal (G-a). The fixed - Story: #{story} was unsatisfiable for goal-create, which runs before any Story exists. All four shipped bodies pass unchanged. docs/design/02-roles.md's template is restated.

Case 8 is partially discharged, deliberately

Build target 4's resolver cannot be built as specified — 21 of 54 artifacts carry no | Claims | row, the other 33 use five incompatible notations, and docs/evidence/ is immutable, so the gaps can never be backfilled. Raised as #341.

Only one direction is sound: a claim id in zero artifacts is certainly uncovered. A refusal needs only that one, so the refusal ships as a pure function over a supplied resolution and exports no resolver. The seam is asserted statically, because a future edit making it read the corpus would return identical verdicts while adopting the unsound direction (#322's M11 precedent).

Verification

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.

12 mutations, 12 killed. M5 is the return on the round: it deleted the Action line's invariant check and the suite stayed green, because the fixture pinning that check was shorter than the prefix and was being rejected by a length accident rather than a content comparison. Fixed, and the accident is now asserted against.

There is no seam job on story/47, so this PR reports 7 checks, not 8.

Refs #316, #47, #341

The block at docs/design/02-roles.md:276-282 had five copies in the tree —
four prose bodies and a private hardFailure() in risk-mandate.ts — and a
rule too loose to police any of them: /(?:- .+\n?)+/ accepts any bullets in
any order, which is why the subject-line divergence went unnoticed.

packages/core/src/guards/hard-failure.ts holds the spec once. A TypeScript
renderer produces conformant blocks; skill-lint's new hard-failure-block
rule validates prose against the same exported constants. One contract,
two enforcement directions.

risk-mandate.ts's private copy is deleted and rewired. Its test is
UNTOUCHED and still green — it pins the output string verbatim, so it is
the regression guard that the swap is behaviour-preserving.

The subject line is an open slot keyed Milestone/Story/Goal per gate
ruling G-a on #47: the fixed '- Story: #{story}' was unsatisfiable for
goal-create, which runs before any Story exists. All four shipped bodies
pass the new rule unchanged.

Refs #316, #47
…rule fires

checkEvidenceMandate is the reader the doctrine at
references/evidence-artifacts.md:140-144 never had. It RESOLVES NOTHING: it
takes an already-determined resolution and decides what to do about it, and
a static assertion pins that its own source imports no fs and no path.

That seam is what makes the task shippable. Build target 4's resolver
cannot be built as specified (#341): 21 of 54 artifacts carry no Claims row
and the other 33 use five incompatible notations, over an immutable corpus.
Only one direction is sound — a claim id in ZERO artifacts is certainly
uncovered — and a refusal needs only that one. Case 8 is recorded PARTIALLY
discharged per gate ruling G-b, carrying the positive direction to BT4.

The negative fixtures each assert TWO things: the old loose regex ACCEPTED
the malformation, and the new rule rejects it. Without the first, they
would only show that some rule fires on some garbage; with it, each one
names a defect that would have shipped green — a missing Action-line
prefix, a subject key outside the vocabulary, transposed Expected/Found.

docs/design/02-roles.md's template restated per G-a: the subject is an open
slot keyed Milestone/Story/Goal, and {remedy} is the trailing imperative
only.

Refs #316, #47, #341
Mutation M5 deleted the Action line's invariant-prefix check entirely --
startsWith(HARD_FAILURE_ACTION_PREFIX) became startsWith("- Action:") --
and the suite stayed green. The negative fixture meant to pin that check
still failed the mutant, but by accident: its Action line is SHORTER than
the 36-character prefix, so the surviving .slice(PREFIX.length) ran off the
end and yielded "", which the blank-remainder branch rejected regardless.
A length accident was standing in for a content comparison.

The added fixture miscases one word and keeps the line long enough that the
slice yields a non-blank remainder either way, so the only thing that can
reject it is a real comparison against the invariant. It is asserted to be
longer than the prefix, and its remainder asserted non-blank, so the
accident cannot come back silently.

Round: 12 mutations, 12 killed -- 11 on the first pass, M5 after this fix.

Refs #316
Verdict PASS. 963 tests / 949 pass / 0 fail / 14 skip, measured at the
parent commit b0616b9; the +2 expect() and +1 claim-lint file this artifact
itself adds are stated rather than measured, per #299, and both predictions
were confirmed after it landed.

12 mutations, 12 killed. M5 is the round's return: it deleted the Action
line's invariant check and the suite stayed green, because the fixture
pinning that check was shorter than the prefix and was being rejected by a
length accident instead of a content comparison.

Case 8 is PARTIALLY discharged per gate ruling G-b, carrying the positive
direction to build target 4 and #341.

Refs #316
@dev-pmallapp
dev-pmallapp marked this pull request as ready for review September 23, 2026 13:23
@dev-pmallapp
dev-pmallapp merged commit b0a7fcf into story/47-s2-3-execution-skills Sep 23, 2026
7 checks passed
@dev-pmallapp
dev-pmallapp deleted the task/316-hard-failure-block-emitter branch September 23, 2026 13: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