Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


id: linter.failure_shape kind: intent checked_against_core: clear statement: “WHEN a spec repo’s tool files are linted, THE failure-shape checker SHALL reject a tool file whose failure terminal states emit nothing, whose error labels collide within a file, or whose failure guards neither cite their negated transition’s citation set nor appear on the recorded carve-out list.”

Linter: Failure Shape Check

The checker half of the error contract (errors.md): it turns the contract’s tier-2 rules — mute failure terminals, label collisions, and typed-negation guard totals — from graph-observable facts into lint findings. Everything it checks is decidable from the reference graph the graph tool already derives; nothing here reads prose, so the checker cannot over-reject a file whose prose happens to argue for failure classes the graph already separates (the D2a decision: classes ⟺ distinct guard citation sets, never a prose judgment).

v1 scope is failure terminals only. model_check.md’s timed_out and exploration_only are real terminal outcomes but are a stated non-goal of this version: typing them raises the exit-code mapping question (does an exhausted bound map to 1?) that errors.md’s exit_code_mapping row defers to a later Revision — the gap is stated here so it is visible rather than forgotten.

This file shipped spec-only (design D6): no Checker Ownership row lands until the checker exists — the table’s invariant is that every listed checker exists. The checker is implemented in spk lint (specodelic-ct5): the three constraint rules below are enforced per file on every lint pass, so they are claims about today’s linter, not norms for a future one. Resolved of record (2026-10-01, specodelic-hhp, decision a): the checker has its Checker Ownership row and rides the orchestrator’s lint stage, dependent on linter.model_shape — the failure-shape walk reads the Model’s states, transitions and emits edges, so it runs only once the model is proven well-formed (dependency_respecting_skip). The earlier note about AGENTS.md’s stale specodelic-6pi citation belongs to governance, not to this file.

Purpose

The checker half of the error contract (errors.md): it turns the contract’s tier-2 rules — mute failure terminals, label collisions, and typed-negation guard totals — from graph-observable facts into lint findings. Everything it checks is decidable from the reference graph the graph tool already derives; nothing here reads prose, so the checker cannot over-reject a file whose prose happens to argue for failure classes the graph already separates (the D2a decision: classes ⟺ distinct guard citation sets, never a prose judgment). v1 scope is failure terminals only. model_check.md’s timed_out and exploration_only are real terminal outcomes but are a stated non-goal of this version: typing them raises the exit-code mapping question (does an exhausted bound map to 1?) that errors.md’s exit_code_mapping row defers to a later Revision — the gap is stated here so it is visible rather than forgotten. This file shipped spec-only (design D6): no Checker Ownership row lands until the checker exists — the table’s invariant is that every listed checker exists. The checker is implemented in spk lint (specodelic-ct5): the three constraint rules below are enforced per file on every lint pass, so they are claims about today’s linter, not norms for a future one. Resolved of record (2026-10-01, specodelic-hhp, decision a): the checker has its Checker Ownership row and rides the orchestrator’s lint stage, dependent on linter.model_shape — the failure-shape walk reads the Model’s states, transitions and emits edges, so it runs only once the model is proven well-formed (dependency_respecting_skip). The earlier note about AGENTS.md’s stale specodelic-6pi citation belongs to governance, not to this file.

Constraints

idkindexprtraces_tosatisfies
terminal_states_emitinvariantevery failure terminal state in a tool file's Model (a state with inbound transitions, no outbound transitions, carrying an emits edge obligation per errors.md's failure_state_emits) emits exactly one effect Constraint owned by that file — a mute failure terminal is a finding (v1 scope: failure terminals only; timed_out / exploration_only are the stated non-goal above)[[linter.failure_shape]]
error_labels_uniqueinvariantwithin one file, no two error Constraints carry the same variant head — cross-file collisions are structurally impossible by errors.md's error_expr_shape (the label is file-id-namespaced), so this check is per-file and trivially green corpus-wide today; it exists so a future format change cannot silently drop the namespacing law[[linter.failure_shape]]
guard_negation_totalinvariantevery failure transition either cites exactly the union of its success siblings' citation sets (the negated disjunction, errors.md's guard_negation_typed), or is on the recorded carve-out list — which is exactly orchestrate.md's stage-fail transitions (lint_fail, compile_fail, model_check_fail, verify_fail), per that file's own Notes' don't-restate discipline; a zero-citation failure guard off the list is a finding, never a silent pass[[linter.failure_shape]]
check_failureeffectlinter.failure_shape.check_failure(detail)[[linter.failure_shape]][[errors.envelope_error_kind]] ∧ [[errors.exit_code_mapping]] ∧ [[errors.remediation_hint_present]]

Model

States

  • unchecked
  • checking
  • passed
  • failed (emits: [[linter.failure_shape.check_failure]])

Transitions

idfromtoguard
beginuncheckedcheckingthe repo declares a specs corpus to check
acceptcheckingpassed[[linter.failure_shape.terminal_states_emit]] ∧ [[linter.failure_shape.error_labels_unique]] ∧ [[linter.failure_shape.guard_negation_total]]
rejectcheckingfailed¬([[linter.failure_shape.terminal_states_emit]] ∧ [[linter.failure_shape.error_labels_unique]] ∧ [[linter.failure_shape.guard_negation_total]])

Properties

idkindderives_fromgeneratorpredicate
mute_terminal_rejectedunit[[linter.failure_shape.terminal_states_emit]]tool_file_with_failure_terminal_and_no_emits_edge()check(file) == failed — the corpus’s pre-add-error-contract shape, kept as the negative fixture
label_collision_rejectedunit[[linter.failure_shape.error_labels_unique]]file_with_two_error_constraints_sharing_a_variant_head()check(file) == failed
zero_citation_flaggedunit[[linter.failure_shape.guard_negation_total]]failure_transition_citing_nothing_off_the_carveout_list()check(file) == failed — the carve-out is a checked list, not an escape hatch
carved_out_guard_passesunit[[linter.failure_shape.guard_negation_total]]orchestrate_stage_fail_transition()check(file) == passed — the carve-out is checked, not assumed
negation_set_mismatch_flaggedunit[[linter.failure_shape.guard_negation_total]]failure_transition_whose_citations_differ_from_its_siblings()check(file) == failed — citation-set inequality is graph-decidable
clean_repo_passesunit[[linter.failure_shape.terminal_states_emit]]the_repo_itself()check(repo) == passed — the corpus dogfoods this checker’s contract
check_failure_label_assertedunit[[linter.failure_shape.check_failure]]failure_shape_violation_found()error_label == "linter.failure_shape.check_failure" — renaming the label touches the error Constraint, this property, and its note together (EDGE-002)

Notes

The class rule is the graph-decidable form. errors.md’s failure_class_is_state owns the corpus-side rule (a state with ≥2 inbound failure transitions whose guard citation sets differ is malformed); this checker never re-derives classes from prose — “classes ⟺ distinct citation sets” is the only reading it implements, so a single-class file (one failure state, one inbound failure transition, however many prose paragraphs argue about it) cannot be over-rejected.

Carve-out list, restated. The only recorded carve-out is orchestrate.md‘s stage-fail transitions. That file’s Notes decline to restate upstream files’ logic as guard-citable rows; retyping their guards would reintroduce exactly the duplication it argues against. The list is data to this checker (checked membership), not an assumption — a new zero-citation failure guard anywhere else is a finding.

Ownership was deliberately absent, then granted. This file originally shipped no Checker Ownership row — its findings rode spk lint only after the checker was implemented (specodelic-ct5), and orchestrator wiring waited for an ownership decision. That decision landed (specodelic-hhp): the file now owns terminal_states_emit, error_labels_unique, and guard_negation_total as a gating row in specodelic.md’s Checker Ownership table (depending on linter-model_shape.md), and its findings gate spk lint. The AGENTS.md staleness it once flagged is governance’s to refresh.

Requirements

Requirement: Property coverage mirror

Every property row SHALL be verified by exactly one dedicated scenario; ah sync SHALL derive one contract per VERIFIES link.

Scenario: mute_terminal_rejected

  • WHEN tool_file_with_failure_terminal_and_no_emits_edge()
  • THEN check(file) == failed — the corpus’s pre-add-error-contract shape, kept as the negative fixture
  • VERIFIES [[linter.failure_shape.mute_terminal_rejected]]

Scenario: label_collision_rejected

  • WHEN file_with_two_error_constraints_sharing_a_variant_head()
  • THEN check(file) == failed
  • VERIFIES [[linter.failure_shape.label_collision_rejected]]

Scenario: zero_citation_flagged

  • WHEN failure_transition_citing_nothing_off_the_carveout_list()
  • THEN check(file) == failed — the carve-out is a checked list, not an escape hatch
  • VERIFIES [[linter.failure_shape.zero_citation_flagged]]

Scenario: carved_out_guard_passes

  • WHEN orchestrate_stage_fail_transition()
  • THEN check(file) == passed — the carve-out is checked, not assumed
  • VERIFIES [[linter.failure_shape.carved_out_guard_passes]]

Scenario: negation_set_mismatch_flagged

  • WHEN failure_transition_whose_citations_differ_from_its_siblings()
  • THEN check(file) == failed — citation-set inequality is graph-decidable
  • VERIFIES [[linter.failure_shape.negation_set_mismatch_flagged]]

Scenario: clean_repo_passes

  • WHEN the_repo_itself()
  • THEN check(repo) == passed — the corpus dogfoods this checker’s contract
  • VERIFIES [[linter.failure_shape.clean_repo_passes]]

Scenario: check_failure_label_asserted

  • WHEN failure_shape_violation_found()
  • THEN error_label == "linter.failure_shape.check_failure" — renaming the label touches the error Constraint, this property, and its note together (EDGE-002)
  • VERIFIES [[linter.failure_shape.check_failure_label_asserted]]