Trigger: A cn:-prefixed ID used in a spec artifact is not registered in any loaded canon directory (toolkit core canon or project canon). Commonly fires when --spec-root / --git-root are omitted and the project-level canon is not discovered, or when the ID is genuinely absent from all canon files.
Resolution: Run specdev guide E110 for the full playbook. Quick summary:
- Re-run the check with the correct flags so the project canon is loaded:
specdev spec-check spec --repo-root ./devspec_toolkit --spec-root ./spec --git-root . - If the ID is new, declare it in
canonical_proposalsin the artifact, then promote withspecdev canon-accept.
See also: E120, E211.
Trigger: A canonical reference ({id, kind, version}) resolves to a registered canon entry whose kind differs from the kind declared on the reference — e.g. a spec artifact references cn:project:foo with kind: "api" but the canon registry has that ID registered under kind: "capability". Raised by CanonRegistry.validate_ref in tools/specdev_tools/canonical/registry.py.
Resolution: Fix the kind field on the reference to match the canon entry's registered kind, or — if the reference's kind is actually correct — correct the canon entry's kind at its source of truth and re-run specdev canon-accept to re-promote it.
See also: E110.
Trigger: A validator could not resolve a required input and fails closed rather than crashing. Three message families share this code:
UNRESOLVED_INPUT— a required directory, file, or spec input was missing or unreadable (e.g.missing_spec_dir,missing_canon_dir,invalid_json), or a CLI input precondition was not met.schema_not_found— the schema registry could not load the schema for a$schemaURI. As of 1.1.0,specdev validate/spec-checkfail closed on all registry I/O errors (previously onlyFileNotFoundErrorwas caught;PermissionErrorand otherOSErrors now route here instead of escaping uncaught). The step-11 validator likewise emits a structuredE520for malformedthreats[]/target_ids[]entries that previously raised an unhandledAttributeError.checklist-proof/docs_impactcontent validation — the step-16 checklist validator (step_16.py) emitsE520when a proof-required checklist item (a non-deferred, non-wont_doitem whosetypeis not in the docs/metadata/logging/config exemption set) has nonfr_refswhile its FR has NFRs traced in07_nfrs.json, or has nofixture_ref; and separately when a milestone plan has code-change actions (non-docfile_create/file_edittargets) butplan.docs_impactis missing, itsstatusis not"required",docs_touchedis empty, or adocs_touchedentry does not match adoc_pathspattern fromseed_manifest.json.changelog_parser.pystructural validation —validate_changelog()intools/specdev_tools/core/changelog_parser.pyemitsE520for unloadable/malformed YAML (missing file, parse error, empty or non-mapping content), plus four raw-dict structural checks run before the changelog is parsed into a dataclass: Check 1 —breakingis present but not abool; Check 2 — a top-level key is not in the format'srequired_fields/optional_fields; Check 3 —versionis not valid semver or does not match the filename's version, except that aversion: "unreleased"value inunreleased.yamlis exempted via an explicit unreleased-sentinel bypass; Check 4 — a declaredsource_of_truth/render_targetpath is not a non-empty string, does not exist on disk, exists but points to a non-file path (e.g. a directory), or exists but is empty. It also emitsE520for invalidchange.type/change.migration.actionvalues found after parsing.
Resolution: Supply the missing input (pass the correct --spec-root / --git-root, create the expected spec_dir, or fix the malformed JSON). For schema_not_found, confirm the $schema URI is registered in tools/schema_registry.json and that the target schema file exists and is readable. For checklist-proof failures, add the missing nfr_refs / fixture_ref to the checklist item (or mark it deferred/wont_do if genuinely out of scope). For docs_impact failures, set plan.docs_impact.status to "required" and list the touched doc paths in docs_touched, matching a pattern in seed_manifest.json's doc_paths. For changelog_parser.py failures, fix the offending YAML field per the check that fired (correct breaking's type, remove/rename the unknown key, fix version to match the filename or use the unreleased sentinel only in unreleased.yaml, or fix the source_of_truth/render_target path).
Trigger: A value in a spec artifact does not match any allowed enum member, canonical ID, or registered entry. The most common variant is a leading verb in a command field that is not in the toolkit or project allowlist.
Resolution: Run specdev guide E530-INVENTED_ENUM_OR_ID for the full playbook. Quick summary: extend spec/canon/command_prefixes.json with the allowed verb, or register the verb as a canonical command entry and attach a sibling command_ref.
See also: E530-LINKED_TEST_FILE_NOT_FOUND, E110.
Trigger: A linked_test_expectation field contains a path-shaped token (containing a slash or a recognised extension: .py, .ts, .js, .go, .java, .rb, .sh, .json) that does not exist on disk. bash -c "..." / sh -c '...' wrappers are unwrapped before the check. Compound commands (&&, ||, ;) are skipped.
Resolution: Run specdev guide E530-LINKED_TEST_FILE_NOT_FOUND for the full playbook. Quick summary: correct the path in the artifact with specdev json patch, or create the missing test file at the referenced path.
See also: E530-INVENTED_ENUM_OR_ID.
Trigger: A canonical alias with a past sunset_date is used in a spec artifact.
Resolution: Replace the alias with the canonical term specified in the replaced_by field of the alias lifecycle block.
Trigger: The same term maps to different canonical IDs across different spec artifacts (N-1 updated, 1 stale).
Resolution: Update the stale artifact(s) to use the current canonical ID. The error message includes per-artifact paths showing which file uses which canonical ID.
Removed prior to 1.0.0. E511 (PLACEHOLDER_SCAN_MISMATCH) was redundant with E510 independent placeholder scan. The associated sub-field has been removed.
Trigger: A functional requirement appears in both apis.out_of_scope[] in step 05 (declaring it has no HTTP API surface) and in an API's trace[] in the same step 05 file (declaring it is covered by an API endpoint). The two claims are mutually exclusive.
Resolution: Remove the FR from out_of_scope[] if an API does cover it, or remove the trace reference from the API if the FR genuinely has no API surface.
Note: out_of_scope[] entries suppress W564 (UNCOVERED_FR_API). Without this check, a contradicted out_of_scope[] entry would silently mask a real API trace from coverage reporting.
Trigger: A functional requirement appears in both fixtures.out_of_scope[] in step 08 (declaring it needs no fixture) and in a fixture's targets[] in the same step 08 file (declaring it is covered by a fixture). The two claims are mutually exclusive. Independent of E535 — Step 05's "no API surface" and Step 08's "no fixture" are separate scoping decisions (see step08_oos_fr_ids in traceability_closure.py).
Resolution: Remove the FR from out_of_scope[] if a fixture does cover it, or remove the target reference from the fixture if the FR genuinely needs no fixture.
Note: out_of_scope[] entries suppress W565 (UNCOVERED_FR_FIXTURE). Without this check, a contradicted out_of_scope[] entry would silently mask a real fixture target from coverage reporting.
Trigger: A free-text field (e.g. a description, rationale, or intent) mentions a canonical term registered in canon/ without a sibling binding reference (a *_ref slot) that pins the term to its canonical ID. The mention reads as an unbound, hallucination-prone reference.
Resolution: Add the appropriate *_ref (e.g. fr_ref, capability_ref, term_ref) alongside the free-text field to bind the canonical term to its ID, or reword the field so it no longer names the canonical term.
Note: E541 is suppressed through four independent mechanisms; a free-text field is checked only if none of them apply:
-
Structural suppression (as of 1.1.0) — fields whose enclosing object is structurally unbindable: the object's schema sets
additionalProperties: falseand declares no*_ref/*_refsslot, so it physically cannot carry a binding reference. -
Namespace-aware slot-kind suppression (as of 1.1.0, round 4) — an object whose schema sets
additionalProperties: falseand declares only purpose-specific ref slots (e.g.fr_refs,capability_refs,metric_ref) that cannot accept aterm/acronymcanonical ID is also suppressed. The check extracts the kind labels from each*_ref/*_refsslot name (e.g.fr_refs→ kindfr;capability_refs→ kindcapability) and suppresses E541 for a given term when the term's canonical kind (e.g.term) is absent from the slot-kind set. Example:14_roadmapmilestones havefr_refs/capability_refs(slot kinds{"fr", "capability"}); aterm-kind canonical ID has no namespace overlap → E541 is suppressed. Objects that DO have a matching slot (e.g.03_glossary.jsonterms[]items withterm_ref) are NOT suppressed by this mechanism — they fall through to mechanism 3 (runtime binding check). -
Term-specific runtime suppression (the
bound_refspath) — when a sibling*_ref/*_refskey is present at the same object level, it suppresses E541 only for the specific term(s) it binds: suppression applies when one of the ref's values is one of the mentioned term's canonical IDs. An unrelated reference (binding a different ID) does not suppress, so a genuinely unbound term in the same object still fires. (Prior to this release the check was object-level — any ref present suppressed every free-text field — which over-suppressed legitimate unbound mentions.) -
Key-name skip fallback (the
_E541_SKIP_KEYS/_E541_SKIP_KEYS_BY_FILEpath) — certain keys are always exempt regardless of schema, because their subtrees are vocabulary definitions or free-form runner output rather than spec content that should bind refs. The complete set of exempted keys, sourced fromhallucination_lint.pylines 774–838:Global skips (
_E541_SKIP_KEYS):Category A — canonical vocabulary subtrees:
canonical_proposals,canonical_refs_used,canonical_conflicts— canonical metadata; not spec content that should bind refstech_stack,user_segments,seeds— structured enumerations whose values are not canonical term-binding sites
Category B — Step 16 implementation/narrative subtrees (belt-and-suspenders for
impl_context/artifacts with variable filenames where$schemalookup is unreliable):actions,coding_examples— Step 16a/16b implementation subtreesemergent_ambiguities,ambiguities,docs_impact— Step 16 narrative subtreesreview_requirements— Step 16 operational verification instructions (test commands, NFR measurement methods); these are not canonical term-binding sites. Note: structural suppression cannot reliably cover these becausetest_commands[]usesoneOfandnfr_measurement_methodsusespatternProperties, both of which can causeslot_kindsto resolve asNoneunder certain schema-navigation paths.
Category C — free-form runner output:
execution—execution.final_status.test_resultsitems haveadditionalPropertiesunset (intentionally free-form test-runner output). The structural rule requiresadditionalProperties: falseand will NOT suppress items with unsetadditionalProperties, so a key-name skip is necessary. Example subtree that triggers this: a16_impl_context.jsonartifact'sexecution.final_status.test_results[]items describing test-runner output such as{"name": "test_send_newsletter", "status": "passed", "duration": 1.2}.
File-scoped skips (
_E541_SKIP_KEYS_BY_FILE):edge_casesin11_redteam.json— unbindable narrative subtree; the red-team schema setsadditionalProperties: falsewith no*_reffield onedge_casesitems. Scoped to that file to avoid silently exempting a futureedge_caseskey in another artifact.definitionin03_glossary.json— glossary definitions are vocabulary prose; requiringterm_refbindings for every cross-term mention in the definition text is semantically incorrect. This skip applies only when the file's basename is03_glossary.json; adefinitionfield in any other file is still checked normally. Note: the glossaryterms[]object DOES haveterm_ref/acronym_refslots (used to pin the entry's own canonical ID), so the namespace-aware structural rule does NOT suppress it — this explicit skip is required.
For artifacts with no resolvable
$schema(where structural suppression and namespace-aware slot-kind suppression are inactive), two mechanisms remain active: the key-name skip (mechanism 4) and the term-specific runtimebound_ref_idssuppression (mechanism 3). Mechanism 3 — thebound_ref_idscollection athallucination_lint.pylines 898–904 — is unconditional and has no guard onschema_node; the checkif cids & bound_ref_ids: continue(line 959) fires regardless of whether$schemaresolved. A sibling*_refbinding the mentioned term therefore suppresses E541 for that specific term even in a schema-free artifact.
Trigger: Fired by the traceability-closure check (check_traceability_closure()) for two Charter → Capabilities gaps: (1) charter_goal_without_capability — a Step 00 charter goals[].goal_id is not referenced by any Step 01 capability's trace[] (a trace entry of the charter-goal type); or (2) charter_success_metric_without_capability — a Step 00 charter success_metrics[].metric_id is not referenced by any capability's success_metric_refs[]. Only fires when both 00_charter.json and 01_capabilities.json are present. Note: E560 previously covered a capability_without_fr check (a capability with no tracing FR); that check was superseded by W568/E568 UNCOVERED_CAPABILITY, and E560 now covers only the two charter-level gaps described above.
Resolution: For charter_goal_without_capability, add a capability in 01_capabilities.json with a trace[] entry referencing the uncovered goal_id. For charter_success_metric_without_capability, add the metric_id to the appropriate capability's success_metric_refs[].
Promotable: PROMOTABLE_PAIRS registers W560 → E560, but the emission direction is inverted in practice: check_traceability_closure() only ever emits E560 directly (never W560). The forward-replay check (forward_replay_check.py) re-runs traceability closure during PR-scoped analysis and downgrades any E560 finding it sees to W560 (code substitution), so a Charter→Capability gap is advisory during incremental replay. Outside forward-replay, E560 fires as a full error.
See also: E568 (capability → FR coverage — the check E560 formerly performed).
Trigger: A functional requirement defined in step 04 is not assigned to any milestone in step 14 (fr_refs).
Resolution: Add the FR ID to the appropriate milestone's fr_refs array in 14_roadmap.json. If the FR has no implementation surface (e.g. an infra or ops constraint), add it to out_of_scope[] in 05_interface_contracts.json with a non-empty rationale — that exempts it from W561, W564, and W566.
Trigger: A milestone in step 14 is not referenced by any checklist item in step 16a.
Resolution: Either add FR references to the milestone or add checklist items that reference the milestone's tasks.
Trigger: A roadmap task exists in step 14 but has no corresponding checklist item in step 16a.
Resolution: Create a checklist item with spec_ref.id matching the roadmap task_id, or mark the task as deferred.
Trigger: A functional requirement defined in step 04 has no API trace reference — no API in step 05 has a trace link with type "fr" pointing to this FR.
Resolution: Add a trace link of type "fr" in the appropriate API contract in 05_interface_contracts.json. If the FR genuinely has no HTTP API surface (infra, ops, external constraint), add it to out_of_scope[] in 05_interface_contracts.json with a non-empty rationale — that suppresses W564, W561, and W566 for that FR. See also E535 for the contradiction check.
Promotable: W564 → E564.
Trigger: A functional requirement defined in step 04 has no fixture coverage — no fixture in step 08 has a target with type "fr" pointing to this FR.
Resolution: Add a fixture in 08_fixtures.json with a target referencing the FR, or add it to out_of_scope[] in 08_fixtures.json with a non-empty rationale — that suppresses W565 for that FR. This is a separate exemption list from 05_interface_contracts.json's out_of_scope[]: an FR with no API surface can still need a fixture (e.g. a background job), so the two are not interchangeable — populate whichever step's out_of_scope[] actually applies to the FR.
Promotable: W565 → E565.
Trigger: A functional requirement defined in step 04 is not referenced in any step 14 milestone fr_refs.
Resolution: Add the FR ID to the appropriate milestone's fr_refs array in 14_roadmap.json. If the FR has no implementation surface, use out_of_scope[] in 05_interface_contracts.json with a non-empty rationale — that exempts it from W566, W564, and W561.
Promotable: W566 → E566.
Trigger: A milestone in step 14 has no tasks, or the milestone's fr_refs are not covered by any task's fr_refs.
Resolution: Add tasks to the milestone, or ensure task fr_refs collectively cover the milestone's fr_refs.
Promotable: W567 → E567.
Trigger: A capability defined in step 01 has no functional requirement tracing to it. This replaces the former E560 capability_without_fr check.
Resolution: Add an FR in 04_fr_list.json that traces to the uncovered capability, or remove the capability if it is no longer relevant.
Promotable: W568 → E568.
Trigger: Fired by the traceability-closure check when a Step 10 governance pr_rules[] entry (a rule-name string) is not a substring of any CI job step's command field across all jobs in 12_ci_gates.json. Only fires when both 10_governance.json and 12_ci_gates.json are present.
Resolution: Add or update a CI job step in 12_ci_gates.json whose command references the uncovered rule name, so the governance rule is demonstrably enforced in CI.
Promotable: W569 → E569.
Trigger: Fired by the traceability-closure check for Step 09 impl-plan deliverable → Step 14 roadmap pairwise completeness. For each Step 09 milestone deliverables[].id, the ID must be referenced by at least one Step 14 task's fr_refs[] or appear in any Step 14 milestone's deliverables[].id. Only fires when both 09_impl_plan.json and 14_roadmap.json are present.
Resolution: Reference the Step 09 deliverable ID from a Step 14 task's fr_refs[], or add a matching deliverables[] entry to the covering Step 14 milestone.
Promotable: W575 → E575.
Trigger: Fired by the traceability-closure check for Step 14 task → Step 16b execution pairwise completeness. For each Step 14 task with status not in (done, deferred, wont_do), verify the task_id appears in the executed-task set derived from execution.critical_evidence.satisfied_checklist_ids on each Trinity Anchor milestone plan, resolved through that plan's checklist spec_ref.id. A task is also exempt if every checklist item covering it already has checklist_status deferred or wont_do (a transitional-pause allowance mirroring E304). Only fires when execution data was loaded from at least one milestone plan and 14_roadmap.json is present.
Resolution: Record execution evidence for the task — add its checklist item's id to execution.critical_evidence.satisfied_checklist_ids in the covering milestone plan, or mark the task deferred/wont_do in 14_roadmap.json if it is legitimately paused or cancelled.
Promotable: W576 → E576.
Trigger (E582): Fired in step 16 under two conditions: (1) a checklist item's milestone_ref names a milestone that does not exist in the step 14 roadmap; or (2) a non-deferred checklist item's milestone_ref does not match the milestone that owns its spec_ref.id task in the step 14 roadmap.
Trigger (W582): Fired in step 16c when a review artifact with verdict: verified has FRs declared in the corresponding step 14 roadmap milestone(s) that are not present in semantic_review.fr_coverage. If no milestone is scoped, the check runs against all milestones in the roadmap.
Resolution: For E582 — correct the checklist item's milestone_ref to match an existing step 14 milestone that owns the referenced task. For W582 — add the missing FR IDs to semantic_review.fr_coverage in the step 16c artifact, or verify that the FR is intentionally excluded.
Promotable: W582 → E582.
Trigger: Fired in step 16a when a review section is present on the same Trinity-loop artifact (post-split model: 16a plan / 16b code / 16c review share one spec/impl_context/{milestone_id}.json file) and a review.findings[].remediation_task.checklist_ids entry does not resolve to any id in the artifact's plan.spec_alignment.checklist[]. Indicates the planner accepted a reviewer's remediation task without representing it as a plan checklist item.
Resolution: Add a checklist item to plan.spec_alignment.checklist[] whose id matches each referenced checklist_ids entry, or correct the remediation_task.checklist_ids value to reference an existing checklist item.
Trigger: Fired in two distinct contexts:
- Canon manifest (
canonical/lint.py): a canonical registry manifest contains a duplicate entryid, a duplicate(kind, normalized)alias pair, or a single alias that resolves to more than one targetid. - Seed manifest (
seed_lint.py):spec/common/seed_manifest.jsondeclares two or moreseeds[]entries sharing the sameseed_idvalue.
Resolution: For canon-manifest collisions, remove or rename the duplicate entry or alias in the relevant canon/ manifest file. For seed-manifest collisions, ensure each entry in seed_manifest.json seeds[] has a unique seed_id; deduplicate or rename the conflicting entries.
Trigger: A step listed in seed_manifest.json's step_requirements has no corresponding seed documents available.
Resolution: Ensure the step's required seeds (as declared in spec/common/seed_manifest.json → step_requirements) are present and accessible.
Promotable: W150 → E150.
Trigger: A spec artifact for a seed-consuming step shares fewer than 3 content tokens with the seed documents required by seed_manifest.json for that step. Applies to any pipeline step (00–16c) for which step_requirements declares seeds.
Resolution: Either incorporate content from the required seed documents into the artifact or review whether the step truly depends on those seeds.
Trigger: An on-disk seed file (under a directory declared in the manifest's seeds[].path entries) is not declared in the seeds[] array of spec/common/seed_manifest.json.
Resolution: Either add the file to the seeds[] array in seed_manifest.json with an appropriate seed_id and path, or remove the undeclared file from the seed directory.
Trigger: A key in seed_manifest.json step_requirements is not a recognized pipeline step in step_order.json (e.g. a typo such as "9" instead of "09", a non-existent step "02b", or an out-of-range value "17"). Real pipeline steps including "16", "16a", "16b", and "16c" do not trigger this warning.
Resolution: Fix the typo in the step_requirements key to match a real step ID, or remove the phantom entry.
Note: W553 was previously named SEED_STEP_OUT_OF_RANGE (fired for any step outside 00–02a). It was repurposed in DEVSPEC-43 to fire only on genuinely unknown steps, enabling seed routing to all pipeline steps.
Trigger: A prompt file contains a literal seed_*.md filename (matched by the pattern seed_\w+\.md). spec/common/seed_manifest.json is the authoritative source for seed names; hardcoding seed filenames in prompts duplicates that authority and drifts silently when seed names change.
Resolution: Remove the literal seed filename from the prompt. Reference seed documents through the manifest's step_requirements routing, not by name in prompt prose. Run specdev hardcoded-seed-check to identify all occurrences.
Warning-only and non-promotable: W554 has no E-counterpart (E554 is CANON_ENUM_DRIFT) and is excluded from PROMOTABLE_PAIRS, so neither SPECDEV_WARNINGS_AS_ERRORS nor SPECDEV_PROMOTE_CODES can escalate it to an error.
Trigger: Seeds routed to step "00" in seed_manifest.json step_requirements supply fewer than 3 substantive out-of-scope items in aggregate. The Step 00 charter schema requires out_of_scope minItems:3, so thin seeds cause authors to hit a gate failure or to hallucinate content at authoring time. "Substantive" means a markdown bullet item under an out-of-scope / non-goals heading that is NOT a bracket-only placeholder (e.g. [Non-goal 1]), NOT a template scaffold label (e.g. - **Expectation**:), and NOT content inside a fenced code block (delimited by ``` or ~~~). Fence tracking matches the opening delimiter, so a ~~~ line inside a ```-opened fence (or vice-versa) is treated as fenced content, not a closing delimiter. Items are counted across ALL seeds routed to step 00 combined; W555 fires at most once per lint_seeds() invocation.
Resolution: Add at least 3 real, project-specific out-of-scope items to the seed(s) routed to step 00. The heading must match one of the accepted variants (case-insensitive): Out-of-Scope, Non-Goals, or Non-Goal. A good out-of-scope item describes a concrete capability that is deliberately excluded from scope — for example "Multi-tenant support is deferred to Phase 2" or "Offline mode is not in scope for MVP".
Warning-only and non-promotable: E555 already exists as SEMANTIC_COVERAGE_REGRESSION (a structurally different code with different semantics) and W555 is intentionally excluded from PROMOTABLE_PAIRS, so neither SPECDEV_WARNINGS_AS_ERRORS nor SPECDEV_PROMOTE_CODES can escalate W555 to an error.
Trigger: A non-deferred checklist item in step 16 lacks a milestone_ref field binding it to a step 14 milestone.
Resolution: Add a milestone_ref field to the checklist item with the milestone_id from step 14 that owns the referenced task.
Promotable: W581 → E581.
Trigger: Fired by the Step 11 (red-team) validator when Step 05 interfaces are present and a public API ID is not named by the target_ids (entries with type: api) of any threat in the artifact. Each public API should be targeted by at least one threat. The check only runs when Step 05 is present (api_ids is not None).
Exemption: An API is skipped if it has at least one FR trace and every FR it traces to carries priority: "wont-have" in step 04 — such an API will never be built, so demanding threat coverage for it is moot. An API with zero FR traces (e.g. only a capability trace) is not exempted. This mirrors the equivalent wont-have exclusion in matrix.py/traceability_closure.py (DEVSPEC-122 follow-up).
Resolution: Add a threat in Step 11 whose target_ids includes the uncovered API ({"type": "api", "id": "<api-id>"}), or confirm the API is intentionally outside threat scope.
Warning-only and non-promotable: W583 has no E-counterpart and is excluded from PROMOTABLE_PAIRS, so neither SPECDEV_WARNINGS_AS_ERRORS nor SPECDEV_PROMOTE_CODES can escalate it to an error.
Trigger (W604): Fired by the Step 14 validator when extras/trace_matrix.json is missing, or when its modification time is older than 14_roadmap.json — an mtime-based freshness heuristic indicating the trace matrix may not reflect the current roadmap. Only W604 is emitted; E604 is registered as the nominal error form but is not currently produced by any code path.
Resolution: Regenerate extras/trace_matrix.json so it post-dates 14_roadmap.json, or confirm the staleness is benign (e.g. a checkout reordered file modification times).
Warning-only and non-promotable: W604 is excluded from PROMOTABLE_PAIRS because matrix staleness is advisory rather than a hard correctness failure; SPECDEV_WARNINGS_AS_ERRORS and SPECDEV_PROMOTE_CODES therefore do not escalate it. E604 exists in the registry as the same-named error form but is not wired to promotion.
Trigger: Fired by the canonical-integrity check when an artifact's canonical_refs_used[] array drifts from the canonical references actually present in the document body. Three subcodes: canonical_refs_used_missing (a canonical ID is referenced in the body but absent from canonical_refs_used[]), canonical_refs_used_extra (an ID is listed in canonical_refs_used[] but not used in the body), and unresolved_canonical_semantic (a semantic field carries a value with no resolved canonical ref).
Resolution: For the canonical_refs_used_* subcodes, run ./tools/run_specdev.sh canonical-autofix <file> --write to sync canonical_refs_used[] with the body. For unresolved_canonical_semantic, supply the missing canonical reference (declare it in canonical_proposals and run canon-accept, or correct the semantic value).
Promotable: No — error-only; there is no W210 counterpart.
Trigger: A JSON Schema enum contains values that are not present in the paired canonical kind.
Resolution: Remove the extra values from the schema enum, or register them in the canonical kind via canon/manifest.json.
Promotable: No (error only).
Trigger: A schema file referenced in the canon/schema pairing configuration does not exist on disk.
Resolution: Verify the schema path in the pairing config and ensure the schema file exists at the expected location.
Promotable: No (error only).
Trigger: A JSON path referenced in the canon/schema pairing configuration does not exist in the target schema.
Resolution: Fix the JSON path in the pairing config to match the actual schema structure, or add the missing enum field to the schema.
Promotable: No (error only).
Trigger: An unregistered schema enum has high overlap (>=80%) with a canonical kind, suggesting a pairing should be declared.
Resolution: Add an explicit pairing entry in the canon/schema alignment configuration, or document why the overlap is coincidental.
Promotable: No (warning only — advisory for canon maintenance).
Trigger: A JSON Schema enum does not include all values defined in the canonical registry for that kind.
Resolution: Add the missing canonical values to the schema enum, or register the missing values in canon/manifest.json.
Promotable: No (error only).
Trigger: IDs present in the old version of an upstream artifact are absent from the new version, indicating dropped coverage.
Resolution: Restore the dropped IDs or update all downstream artifacts that reference them.
Promotable: No (error only).
Trigger (E590): A spec artifact references an ID (FR, API, INV, NFR, capability, governance label) that does not exist in the upstream artifact.
Trigger (W590): The upstream artifact file is missing entirely; cross-step validation is skipped for that upstream.
Resolution (E590): Fix the broken ID reference to match an ID that exists in the upstream artifact.
Resolution (W590): Generate the missing upstream artifact first, then re-validate.
Promotable: No. W590 (CROSS_STEP_UPSTREAM_MISSING — upstream file absent) and E590 (CROSS_STEP_ID_NOT_FOUND — ID absent from a present file) describe different defects with different resolutions. Promoting W590 to E590 would mislabel a missing-file condition as a broken ID reference, so W590 stays a warning; a fatal "upstream missing" would require a dedicated E-code.
Trigger: A prompt has a ### Extraction Intent section header but no parseable artifact entries beneath it.
Resolution: Add extraction intent entries listing each upstream artifact and what to extract from it.
Promotable: W591 → E591.
Trigger: The traceability matrix FR coverage percentage falls below the configured threshold in tools/step_order.json → coverage_thresholds.fr_coverage (default: 80%).
Resolution: Increase FR coverage by adding API contracts, fixtures, or NFR bindings for uncovered FRs.
Promotable: W592 → E592. Mode controlled by coverage_thresholds.mode in step_order.json ("warn" or "error").
Trigger: A free-text field (description, statement, rationale, justification, notes, narrative, postconditions, preconditions, risks, spikes, migration_plan, definition) contains vague quantifiers (few, some, many, several, various, appropriate, adequate, sufficient, reasonable, significant, typical, generally, usually, fast, reliable, easy, hard, quick).
Resolution: Replace vague language with specific, measurable terms.
Promotable: W593 → E593. Note: W571 is emitted for vague language in assumptions fields specifically.
Trigger: A downstream artifact's free-text content shares fewer than the configured threshold (default: 5) of distinct tokens with its declared upstream artifacts.
Resolution: Ensure downstream content is derived from upstream artifacts. Review extraction intent and incorporate upstream terminology.
Promotable: W594 → E594. W594 is advisory by default — content derivation is heuristic with inherent false-positive risk.
Trigger: An upstream artifact has been modified with new content tokens, but none of those new tokens appear in downstream artifacts that consume it.
Resolution: Review the upstream changes and update downstream artifacts to reflect the new content.
Promotable: W595 → E595.
Sub-reasons (emitted by specdev context freshness):
CONTENT_STALENESS— a seed's on-disk hash differs from its hash inseed_requirements.json(seed edited since last index). Re-index with/specdev-step.SEED_UNTRACKED— a seed listed inseed_manifest.json(with a path) is absent fromseed_requirements.json, so edits to it trip no drift signal.build_seed_indexhashes every manifest seed, so an untracked seed means the index is stale-by-omission — re-index with/specdev-stepto bring it under drift detection.
Trigger: A non-terminal step has zero entries in downstream_consumers in step_order.json.
Resolution: Add the consuming steps to the step's downstream_consumers array in tools/step_order.json.
Promotable: No. E596 is error-only (emitted by dag-lint).
Trigger: A prompt's extraction intent references an artifact from a step that is not in the computed allowed upstream steps (all steps preceding the current step in tools/step_order.json).
Resolution: Either adjust the step ordering in steps so the referenced step precedes the current step, or remove the reference from the prompt's extraction intent.
Promotable: No. W596 has different semantics from E596 (undeclared ref vs dead-end producer).
Trigger (E597): A step has an allowed upstream step (computed at runtime from the steps order in step_order.json), but the prompt's extraction intent has no entry for that upstream artifact.
Trigger (W597): An extraction intent entry has vague text (fewer than 10 words or contains "relevant", "as needed", "as appropriate", "various", "TBD", "TODO").
Resolution (E597): Add an extraction intent entry for the missing upstream artifact.
Resolution (W597): Expand the vague intent text with specific field names and extraction purposes.
Promotable: No. W597 (EXTRACTION_INTENT_VAGUE) and E597 (EXTRACTION_INTENT_UPSTREAM_GAP) have different semantics — vague text in a present entry vs. a missing entry for a required upstream artifact. W597 has no fatal counterpart and is not promoted; promoting it would mislabel a vague entry as an upstream gap.
Trigger: An extraction intent entry references a step that does not exist in step_order.json's steps list.
Resolution: Fix the artifact reference to point to a valid step number.
Promotable: No (error only).
Trigger: A step is listed in another step's downstream_consumers, but the consumer step does not appear after the producer in the steps ordering (i.e., the consumer precedes or equals the producer in the steps list).
Resolution: Ensure downstream_consumers entries are consistent with the positional ordering of steps in tools/step_order.json. Every declared consumer must appear after its producer in the steps list.
Promotable: No (error only).
Trigger: The computed upstream dependency graph (derived from steps positional order) contains a cycle.
Resolution: Remove the circular dependency from the steps ordering in tools/step_order.json.
Promotable: No (error only).
Trigger: An assumption in the assumptions array contains vague quantifiers.
Resolution: Replace vague language with specific, measurable terms.
Promotable: W571 → E571.
Trigger: The number of assumptions exceeds the configured threshold.
Resolution: Review assumptions and remove or consolidate redundant entries.
Promotable: W572 → E572.
Trigger: An assumption references an ID that cannot be resolved to any known artifact.
Resolution: Bind the assumption to a valid spec artifact ID or remove the reference.
Promotable: W573 → E573.
Trigger: A validator cannot resolve an optional upstream input needed for a cross-reference check, so it skips that check rather than emitting a false unresolved-reference error. W570 is a general graceful-skip signal emitted from several validators (hallucination_lint, traceability_closure, seed_lint, canonical/autofix, and the step-16 validator) whenever a dependency they would cross-check is absent. The representative case is the NFR-ref check in hallucination_lint: when spec/07_nfrs.json is absent, nfr_refs in downstream artifacts (e.g. 13_impl.json) cannot be validated, so the linter records W570 GRACEFUL_SKIP nfr_refs 07_nfrs.json_absent and proceeds without flagging any nfr-* reference as unresolved. Each emitter's message payload names the specific check and the missing input.
Resolution: None required — the skip is intentional and informational. To make the skipped check actually run, provide the missing upstream artifact named in the message (e.g. spec/07_nfrs.json via the step-07 NFR prompt) and re-run the validator.
Promotable: No. W570 has no E570 counterpart — a graceful skip is the absence of a checkable condition, not a defect, so there is nothing to promote to an error.
Trigger: A substep reference in a checklist does not match the expected step structure.
Resolution: Update the substep reference to match the current step definition.
Promotable: W580 → E580.
Trigger: Fires in any of three cases: (1) spec/specdev_version is absent or is present but malformed / missing the toolkit_version key; (2) the active toolkit version cannot be read from tools/pyproject.toml (file missing or unparseable); (3) the toolkit_version recorded in spec/specdev_version differs from the active toolkit version.
Resolution: Run specdev update <spec_dir> to sync your project to the current toolkit version. When there are no schema changes, update re-stamps spec/specdev_version instantly. When schema changes are required, it directs you through the specdev align flow (apply --auto, optionally prompts, then validate), finalizing with specdev align validate <spec_dir> — which runs full post-migration validation and stamps the new version with a migration-history entry.
Promotable: No (error only).
Trigger: A step-06 invariant has a non-empty risk_category_ref field (marking it as security-relevant) but no step-11 threat has a mitigation of type inv referencing its inv_id. This indicates that a security-relevant invariant is not exercised by any threat in the red-team assessment, creating a gap between invariant definitions and threat coverage.
Only fires when step 06 is present (absent step-06 file → check skipped silently). Invariants without a risk_category_ref are not flagged — only those explicitly marked as belonging to a risk category are expected to have a corresponding threat.
Exemption: An invariant is skipped if it has at least one FR trace and every FR it traces to carries priority: "wont-have" in step 04 — such an invariant guards behavior that will never be built, so demanding threat coverage for it is moot. An invariant with zero FR traces (e.g. only a capability trace) is not exempted. This mirrors the equivalent wont-have exclusion in matrix.py/traceability_closure.py (DEVSPEC-122 follow-up).
Resolution: Either add a threat in 11_redteam.json with a mitigation of type inv referencing the flagged inv_id, or remove risk_category_ref from the invariant if it is not genuinely security-relevant.
Promotable: W615 → E615 (via SPECDEV_WARNINGS_AS_ERRORS=1 or SPECDEV_PROMOTE_CODES=W615).
Trigger: A Step 16 checklist item has checklist_status == "deferred" or "wont_do" while its implementation.status still says "verified". This combination is a stale contradiction: work was verified, then the item was marked paused or permanently cancelled, without ever clearing or annotating the implementation record. (This check does not itself validate evidence quality — that's E301/W600/W601's job — it only compares these two status fields.)
Resolution: Reconcile which state is accurate. If the item is genuinely paused/cancelled, reset implementation.status to something other than verified (e.g. deferred) or note in deferred_reason/wont_do_reason why verified work is being shelved. If the work is actually done, set checklist_status back to active.
Promotable: No. This is a human-reconcile nudge, not a correctness failure with a single unambiguous fix direction — there is no E616 counterpart.
Trigger: Fired by step_16.py in any of three shapes: (1) a checklist item's implementation.status is "verified" but it has no actions; (2) a "verified" item has actions but none of them carry an evidence field (EVIDENCE_CONTENT_INVALID/no-evidence variant); or (3) an action's evidence.content is present but lacks a recognized success-marker keyword (e.g. PASS, OK, passed, success, 0 failures) and the evidence has no stdout/stderr field to fall back on. A fourth, plan-level shape (MISSING_PROOF_CLOSURE) fires when plan.status == "active" and a review_requirements.test_commands entry is not found among execution.execution_results with status in (passed, blocked) — an unacknowledged test command on an active plan.
Resolution: Add actions documenting the work for verified items, ensure at least one action carries an evidence field, and make evidence content include a success marker (or structured stdout/stderr) so proof can be machine-verified. For the plan-level shape, run the declared test command and record its result in execution.execution_results, or mark it blocked with a reason.
Promotable: No (error only; no W301 counterpart).
Trigger: review.verdict == "verified" but the proof chain backing that verdict is incomplete, in any of four ways: (1) no execution section exists at all; (2) execution.execution_results is empty; (3) one or more review_requirements.test_commands entries have no matching status == "passed" entry in execution_results; or (4) execution.critical_evidence.passed_test_commands does not list every declared test command. Unlike E301's acknowledged set (which also accepts blocked), E302 requires an explicit passed status — a blocked acknowledgment satisfies E301 but not E302.
Resolution: Do not set review.verdict to "verified" until every review_requirements.test_commands entry has a corresponding status: "passed" entry in execution.execution_results, and until execution.critical_evidence.passed_test_commands lists each of them.
Promotable: No (error only; no W302 counterpart).
Trigger: review.verdict == "verified" but review.fixture_status.ci_status is absent or is not exactly "green" (Task 7-04 / AUDIT-032 strengthened this to reject any non-"green" value, not just an explicit failure status).
Resolution: Set review.fixture_status.ci_status to "green" once CI has genuinely passed, or revert review.verdict until it has. (E303 is also the code used for governance commit-message mismatches — see validation/governance.py — a distinct emitter sharing the same code for a different artifact class.)
Promotable: No (error only; no W303 counterpart).
Trigger: Fired on 16a milestone plans (not on the Trinity Anchor) when a 14_roadmap.json task — scoped to the milestones referenced by the plan's checklist milestone_refs, or to all non-done/non-deferred milestones when no scoping signal exists — has no checklist item whose spec_ref.id matches the task's task_id. A task itself marked deferred/wont_do in the roadmap is exempt (already an authored acknowledgment). Also covers two structural sub-conditions under the same code: ROADMAP_PARSE_ERROR (14_roadmap.json fails to load) and ROADMAP_STRUCTURE_ERROR (unexpected roadmap shape).
Resolution: Add a checklist item with spec_ref.id matching the uncovered roadmap task_id, or mark the roadmap task deferred/wont_do with a status_reason if it is genuinely paused or cancelled. For the structural variants, fix the malformed or misshapen 14_roadmap.json.
Promotable: No (error only; no W304 counterpart).
Trigger: execution.final_status.ci_status == "green" but a checklist item that is active (checklist_status not in the paused/cancelled set) is not present in execution.critical_evidence.satisfied_checklist_ids. A green final CI status implies every planned item should have been executed and recorded as satisfied.
Resolution: Add the item's id to execution.critical_evidence.satisfied_checklist_ids once its work is genuinely done, or set checklist_status to deferred/wont_do if it was intentionally skipped for this milestone pass.
Promotable: No (error only; no W305 counterpart).
Trigger: review.semantic_review.fr_coverage[].fr_id references an fr_id that does not exist in the corresponding 04_fr_list.json (resolved one directory up when the artifact lives under impl_context/). Skipped silently if 04_fr_list.json is missing or unreadable.
Resolution: Correct the fr_id to reference an FR that actually exists in 04_fr_list.json, or remove the stale fr_coverage entry.
Promotable: No (error only; no W306 counterpart).
Trigger: For every behavioral spec_ref (types other than doc, code, task — i.e. fr, api, inv, nfr) referenced by at least one checklist item, the full set of checklist items sharing that spec_ref.id must include at least one item of type: "behavior" and one of type: "validation". Deferred items still count toward pairing (a paused validation item still satisfies the pairing requirement); only its own proof-of-work fields are exempted elsewhere.
Resolution: Add the missing behavior and/or validation checklist item(s) for the flagged spec_ref.id.
Promotable: No (error only; no W307 counterpart).
Trigger: Fired by step_16_anchor.py (the Trinity Anchor's cross-milestone drift check) in two shapes: (1) [ownership] — the same FR/API ref_id appears in fr_refs of two non-done milestone_index entries, i.e. two active milestones both claim the same requirement; (2) [scope] — a milestone's plan.summary.scope_in item (case-insensitive) appears in the anchor's own plan.summary.scope_out, or a milestone's scope_out item appears in the anchor's scope_in — a bidirectional scope contradiction. The [scope] shape only runs when spec_path is provided and at least one milestone context file survives the W588/W589 filtering (see W611); the [ownership] shape runs purely in-memory off milestone_index regardless of spec_path.
Resolution: For [ownership] conflicts, ensure at most one non-done milestone claims a given FR/API in its fr_refs (mark the stale milestone done or remove the duplicate ref). For [scope] conflicts, reconcile the milestone's scope_in/scope_out against the anchor's plan.summary.scope_in/scope_out so the same category is not declared in-scope by one and out-of-scope by the other.
Promotable: No (error only; no W308 counterpart).
Trigger: A verified action's evidence.content is a string shorter than 50 characters — too short to plausibly contain a meaningful proof of work.
Resolution: Expand the evidence content to include the actual command output, or reference stdout/stderr fields instead of a short freeform string.
Promotable: No (warning only; no E599 counterpart — E599 is DAG_CONSUMER_INCONSISTENCY, an unrelated Step-04→Step-05 DAG check).
Trigger: Two shapes share this code: (1) VERIFIED_ACTION_NO_EVIDENCE — a verified action has no evidence field at all; (2) EVIDENCE_NO_CONTENT — the action's evidence is a dict but has none of content, stdout, or stderr.
Resolution: Add an evidence field to the action (a content string or structured stdout/stderr fields) documenting what was actually verified.
Promotable: No (warning only; no E600 counterpart).
Trigger: A verified action's evidence (content, stdout, and/or stderr combined) does not contain any recognizable spec artifact ID pattern (fr-*, api-*, nfr-*, inv-*) — the evidence cannot be tied back to the spec artifact it is meant to prove.
Resolution: Include the relevant artifact ID (e.g. the fr-* or api-* being verified) in the evidence content, stdout, or stderr text.
Promotable: No (warning only; no E601 counterpart).
Trigger: A file listed in execution.files_touched (Task 7-09 / AUDIT-087) is not declared in any checklist item's implementation.files_touched, nor in plan.summary.milestone_supporting_files — the file was touched during execution but isn't tracked as belonging to any specific task's scope.
Resolution: Add the file to the owning checklist item's implementation.files_touched, or to plan.summary.milestone_supporting_files if it is milestone-wide supporting infrastructure rather than task-specific.
Promotable: No (warning only; no E603 counterpart).
Trigger: Fired by step_16_anchor.py (the Trinity Anchor validator) when plan.milestone_index has one or more entries but plan.drift.checks is empty — the anchor is tracking milestones without recording any drift-check evidence for a Trinity cycle. Fires purely in-memory off the anchor's own plan data, regardless of whether spec_path is provided or impl_context/ exists on disk.
Resolution: Record at least one entry in plan.drift.checks per Trinity cycle documenting a drift observation (even a "no drift found" entry satisfies the check).
Promotable: No. W587 has no E587 counterpart — stale drift-check bookkeeping is advisory, not a hard defect.
Trigger: A milestone-plan JSON file the Trinity Anchor either declares (via plan.milestone_index[].context_path in 16_impl_context.json) or that is discovered by globbing impl_context/*.json exists on disk but cannot be parsed as JSON or cannot be read. Two independent emitters share this code: traceability_closure.py (registry-driven discovery, feeding the coverage checks above) and step_16_anchor.py (directory-glob discovery, feeding E308/E309 cross-milestone drift detection) — the same underlying condition surfaces through whichever path finds it first. A declared context_path that is simply missing from disk (rather than present-but-unreadable) is a separate condition owned by W607 ANCHOR_CONTEXT_PATH_MISSING, not W588.
Resolution: Fix the malformed JSON in the named milestone-plan file, or restore its file-read permissions. Until fixed, the file is excluded from traceability coverage checks and from E308/E309 drift detection — see W611 below if this causes every milestone plan to be filtered out.
Promotable: No. W588 has no E588 counterpart — an unreadable file is a data-integrity problem to fix directly, not a severity to escalate.
Trigger: The Trinity Anchor's impl_context/ directory contains one or more JSON files, but none of them declare $schema='vc:16-impl-context', so every per-milestone plan was filtered out (typically because each tripped W588/W589). With zero surviving milestone contexts, the cross-milestone E308 (scope drift) and E309 (checklist drift) checks are completely suppressed — the anchor looks clean while contributing nothing to drift detection. Does not fire when impl_context/ is genuinely empty (files_seen == 0), which is a valid state for a fresh anchor.
Resolution: Fix the W588/W589 warnings reported above the W611 line so the per-milestone plans are recognized (correct each plan's $schema to vc:16-impl-context). Once at least one plan survives filtering, E308/E309 drift detection is restored.
Promotable: No. W611 flags a suppressed-coverage condition, not a concrete drift defect; it has no E611 counterpart.
Trigger: A milestone_index entry in the Trinity Anchor names a milestone_id that does not match any milestone_id in 14_roadmap.json. Phantom milestones bypass ownership, prefix, and scope-drift detection, so the mismatch is surfaced as a warning. Only checked when 14_roadmap.json is present and readable.
Resolution: Verify the milestone_id is not a typo. Either correct it to reference an existing roadmap milestone, or add the milestone to 14_roadmap.json if it is legitimately new (replaying the step-14 roadmap step).
Promotable: No. W612 has no E612 counterpart — a phantom-milestone reference is advisory because the roadmap may legitimately lag a freshly-scaffolded anchor.
Trigger: A canonical reference ({id, kind, version}) names a version that does not match the registered entry's version, via CanonicalRegistry.validate_ref in tools/specdev_tools/canonical/registry.py.
Resolution: Update the reference's version field to match the canon entry's current version, or bump the canon entry if the reference is intentionally targeting a newer version.
See also: E110, E120, W130.
Trigger: A canonical reference supplies an alias_used value that, for the given kind, resolves to more than one candidate canonical ID (CanonicalRegistry.validate_ref).
Resolution: Replace the ambiguous alias with the specific canonical id it should resolve to, or deduplicate the conflicting alias registrations in the canon manifest.
Trigger: A canonical reference resolves to an entry whose status is "deprecated" (CanonicalRegistry.validate_ref).
Resolution: Migrate the reference to the entry's replacement canonical ID (see the entry's lifecycle replaced_by), or confirm continued use of the deprecated entry is intentional.
Promotable: No. W110 has no E110-shared promotion path — E110 is UNKNOWN_CANONICAL_ID, a different condition (missing vs. deprecated-but-present).
Trigger: A canonical reference's alias_used resolves via an alias whose lifecycle marks it deprecated (but not yet sunset — see E125 for the sunset-expired case), via CanonicalRegistry.validate_ref.
Resolution: Replace the deprecated alias with the replaced_by alias or the canonical id directly.
See also: E125 ALIAS_SUNSET_EXPIRED (the fatal counterpart once the alias's sunset_date has passed).
Trigger: A canonical reference omits the version field entirely (CanonicalRegistry.validate_ref).
Resolution: Add an explicit version field to the reference pinning it to the canon entry's current version.
Promotable: No dedicated pairing in PROMOTABLE_PAIRS under this name; E130 (CANONICAL_VERSION_MISMATCH) is a structurally different condition (version present but wrong, not omitted).
Trigger: Fired by lint_canon_dir() in tools/specdev_tools/canonical/lint.py for two distinct shapes: (1) a canon manifest entry (_validate_lifecycle) whose lifecycle block is missing introduced_at (always required), or is missing deprecated_since when status is "deprecated"/"sunset", or is missing sunset_after when status is "sunset", or is missing retired_at when status is "retired"; (2) a manifest alias with status: "deprecated" that has no deprecated_since on the alias itself or on its nested lifecycle block.
Resolution: Add the missing lifecycle field(s) to the canon entry (or alias) matching its current status — introduced_at is always required, and deprecated_since / sunset_after / retired_at become required once the entry transitions to the corresponding status value. See the canon manifest schema for the expected lifecycle shape.
Promotable: No (error only; no W420 counterpart).
Trigger: During two-tier canon loading (CanonicalRegistry.load), a project-canon entry id collides with a core-canon entry id. The project entry silently overrides the core entry; W421 surfaces that override so it isn't invisible.
Resolution: Rename the project-canon entry's id if the collision is accidental, or leave it if the override is intentional (the warning is informational once acknowledged).
Promotable: No (warning only).
Trigger: specdev canon-accept is invoked with a --namespace under cn:core: while writing to project canon (canonical/accept.py). Project canon may not declare entries in the cn:core: namespace — that namespace is toolkit-owned.
Resolution: Use a cn:project:-prefixed (or other non-core) namespace for canon-accept, or promote the entry through the toolkit's own core-canon process if it genuinely belongs in cn:core:.
Promotable: No (error only).
Trigger: Two independent emitters share this code: (1) step_12.py — a CI job's requires[] graph in 12_ci_gates.json contains a cycle; (2) step_14.py — a step 14 roadmap task's dependency graph (task-to-task, via dfs) contains a cycle involving a named milestone.
Resolution: Break the cycle by removing or reordering the offending requires/task-dependency edge so the graph is acyclic.
Promotable: No (error only).
Trigger: Fired by step_14.py when a tech_stack entry used in 14_roadmap.json is not present in the corresponding Step 09 (09_impl_plan.json) tech_stack.
Resolution: Add the technology to Step 09's tech_stack, or remove it from the roadmap's tech_stack if it is not actually part of the approved stack.
See also: W602 / W605 (the equivalent Step 02 ↔ Step 14 tech-stack consistency checks).
Trigger: Fired by prompt_schema_sync.py when a prompt_NN_*.md file's documented output contract drifts from its paired JSON Schema — missing/extra required fields, a missing schema-reference section, a missing or malformed schema URI/file reference, a missing property, or a property-level mismatch (type/description drift) between the prompt's documented fields and the schema.
Resolution: Update the prompt's output-contract section to match the paired schema (or vice versa, if the schema is stale) — align required fields, the $schema URI reference, and per-property descriptions.
Promotable: No (error only).
Trigger: Fired by step_05.py when an API entry in 05_interface_contracts.json declares enum values (in request/response fields) but has no enum_provenance field recording where those enum values originated, for reproducibility tracking.
Resolution: Add an enum_provenance field to the API entry documenting the source of the enum values (e.g. a canonical registry entry or an upstream spec artifact).
Promotable: No (error only).
Trigger: Fired by step_13.py for a malformed extension entry in 13_impl.json: either a required_schema_sections[] item that is not a valid identifier (must start alphanumeric; only alphanumeric/underscore/hyphen allowed), or a missing/empty justification field on the extension.
Resolution: Fix the malformed required_schema_sections[] entry to a valid identifier, or supply a non-empty justification for the extension.
Promotable: No (error only).
Trigger: Fired by dependency_order_lint.py when a prompt file references (via a spec/NN_* path mention) either itself (self-dependency) or a step that comes later in the waterfall (forward dependency) — both violate the forward-only ordering.
Resolution: Remove the self-reference, or move the referenced content to a step that precedes the current one in tools/step_order.json's steps ordering.
Promotable: No (error only).
Trigger: Fired by dependency_order_lint.py (M4 check) in three shapes, all comparing tools/step_order.json's step_metadata[].required_spec_inputs against the downstream_consumers-derived inverse graph: (1) a step_metadata key references a step not declared in the top-level steps array (phantom key); (2) a step's required_spec_inputs is missing an entry implied by another step's downstream_consumers; (3) a step's required_spec_inputs has an extra entry not implied by any downstream_consumers.
Resolution: Remove the phantom step_metadata key, or reconcile required_spec_inputs with the corresponding downstream_consumers entries so the two stay consistent.
Promotable: No (error only).
Trigger: Fired by spec_quality_lint.py's independent placeholder scan when any string value in a spec artifact matches the placeholder pattern (e.g. bracket-wrapped tokens like [TBD], [FIXME]).
Resolution: Replace the placeholder text with real, project-specific content.
See also: E511 (removed — was redundant with this independent scan).
Trigger: Fired by spec_quality_lint.py when a value inside an assumptions[] entry matches the placeholder pattern.
Resolution: Replace the placeholder text in the assumption with a concrete, project-specific statement.
See also: E571/W571 ASSUMPTION_VAGUE_QUANTIFIER, W573 ASSUMPTION_UNBOUND_ID (the other assumption-content checks).
Trigger: A validator's own execution fails unexpectedly and is caught rather than propagating an unhandled exception — emitted from several sites (canonical/lint.py schema-validation runtime errors, validation/validate.py deep-validation critical errors, glossary_drift_lint.py runtime errors). Also used as the fallback code in ensure_spec_errors() when a raw string error cannot be parsed into a structured SpecError.
Resolution: Inspect the embedded exception type/message in the error text to diagnose the underlying failure (malformed input, missing file, bad schema); fix the root cause and re-run.
Promotable: No (error only).
Trigger: Fired by spec_quality_lint.py when 09_impl_plan.json's tech_stack and 14_roadmap.json's tech_stack are both present but not identical.
Resolution: Reconcile the two tech_stack objects so Step 09 and Step 14 declare the same technology choices.
Promotable: No (warning only).
Trigger (W602): step_14.py (AUDIT-034) fires when a tech_stack entry used in 14_roadmap.json is not found in Step 02's (02_system_sketch.json) tech_stack.
Trigger (W605): Fires when a technology declared in Step 02's tech_stack is absent from 14_roadmap.json's tech_stack — the roadmap dropped a technology the system sketch declared.
Resolution: Add the missing technology to whichever artifact is missing it (Step 02 system sketch or Step 14 roadmap) so the two tech_stack sets agree.
Promotable: No (warning only). This is a separate check from E142 (which compares Step 09 vs. Step 14 tech stacks).
Trigger (E550): Fired by forward_replay_check.py in three shapes: (1) SPECDEV_REPLAY_DIFF_ERROR_MODE is set to an invalid value (must be error or ignore); (2) the diff against the base ref could not be computed at all; (3) a changed step's downstream consumers (per tools/step_order.json) were not also replayed.
Trigger (W550): Fired when semantic coverage checking could not read the base-ref version of an artifact (e.g. the file didn't exist at the base ref), so the coverage-regression check for that artifact is skipped rather than failing closed.
Resolution (E550): Fix the SPECDEV_REPLAY_DIFF_ERROR_MODE value, ensure the base ref is reachable and diffable, or replay the missing downstream artifacts.
Resolution (W550): Confirm the base ref genuinely predates the artifact (expected for newly-added files); no action needed if so.
Promotable: PROMOTABLE_PAIRS registers W550 → E550, though the two are emitted by structurally different conditions (skip vs. hard failure) — promotion escalates the skip to a hard failure under SPECDEV_WARNINGS_AS_ERRORS/SPECDEV_PROMOTE_CODES=W550.
Trigger: Fired by forward_replay_check.py alongside an E555 SEMANTIC_COVERAGE_REGRESSION finding — for each individual ID that was present in the base-ref version of an artifact and is now absent, W598 fires once per removed ID (in addition to the aggregate E555), to surface likely ID renames early.
Resolution: If the ID was renamed, update all downstream references to the new ID. If it was genuinely removed, confirm no downstream artifact still depends on it.
Promotable: No (warning only; co-fires with E555 rather than promoting to it).
Trigger: Fired by glossary_drift_lint.py when a 03_glossary.json terms[] entry's definition differs from the definition on its matching canonical_proposals entry (matched by term-{temp_id}, kind == "term" proposals only — acronym proposals are excluded since acronyms have no terms[] counterpart).
Resolution: Reconcile the term's definition in terms[] with the corresponding canonical_proposals entry so both describe the same definition.
Trigger: Fired by glossary_drift_lint.py when a 03_glossary.json term's definition differs from the definition of the already-accepted cn:project: canon entry it's bound to via term_ref.
Resolution: Update the glossary term's definition to match the canon entry (the source of truth once accepted), or re-run canon-accept if the glossary's definition is the intended update.
Trigger: Fired by glossary_drift_lint.py when a cn:project: canon entry whose lifecycle records accepted_from: 03_glossary.json is neither referenced in canonical_refs_used[] nor still present as a canonical_proposals entry — it was accepted from the glossary but the glossary no longer names it anywhere.
Resolution: Either re-add a reference to the canon ID in the glossary (if it's still a valid term), or leave the canon entry in place as an intentionally-retained but currently-unused registration.
Promotable: No (warning only).
Trigger: Fired by step_16_anchor.py in two shapes: (1) [prefix] — two milestone_index entries declare the same checklist_id_prefix, so two milestones would allocate checklist IDs from the same namespace; (2) [mapping] — the same checklist item id appears in two different milestone plan files but maps to a different spec_ref.id in each, i.e. the same ID is bound to two different requirements.
Resolution: For [prefix], give each milestone a unique checklist_id_prefix. For [mapping], rename the colliding checklist id in one of the milestone plans so each ID maps to exactly one spec_ref.id.
Promotable: No (error only; no W309 counterpart).
Trigger: Fired by step_16_anchor.py when spec_path is not provided to validate_step_16_anchor, so all filesystem-dependent drift checks (W607, the [scope] shape of E308, the [mapping] shape of E309, W610) are skipped — only the in-memory milestone_index checks still run.
Resolution: Pass spec_path when invoking the anchor validator so the filesystem-dependent checks can run.
Promotable: No (warning only).
Trigger: validate_step_16_anchor is called on an artifact that is neither field-marked (artifact_role == "anchor") nor path-marked (spec/16_impl_context.json outside impl_context/) — indicating a routing bug in validate.py's dispatch, or a mis-authored artifact that should not have reached the anchor validator.
Resolution: Fix the routing logic in validate.py, or correct the artifact's artifact_role/file location so it is unambiguously an anchor or a milestone plan.
Promotable: No (warning only).
Trigger: A JSON file under impl_context/ parses successfully but declares a $schema other than vc:16-impl-context — it isn't recognized as a milestone plan, so it's excluded from every cross-milestone drift check (E308/E309).
Resolution: Correct the file's $schema to vc:16-impl-context, or move the file out of impl_context/ if it isn't meant to be a milestone plan.
See also: W611 ANCHOR_DRIFT_SUPPRESSED (fires when W589/W588 filter out every milestone plan, suppressing drift detection entirely).
Trigger: A milestone_index[] entry declares a context_path pointing to a milestone plan file, but no file exists at the resolved path — drift detection silently skips that milestone until the plan is authored.
Resolution: Author the missing milestone plan file at the declared context_path, or correct the context_path to point to the existing plan file.
See also: W588 ANCHOR_MILESTONE_UNREADABLE (the file exists but can't be parsed — a distinct condition from W607's file-absent case).
Trigger: The artifact at the anchor path (spec/16_impl_context.json) declares $schema='vc:16-impl-context' — the pre-Trinity-split, per-milestone schema — instead of the anchor's own vc:16-anchor schema. Schema validation against vc:16-impl-context still passes, but the file is contributing nothing to cross-milestone drift detection.
Resolution: Migrate: move per-milestone checklist content to spec/impl_context/<milestone>_plan.json, rewrite the anchor file against vc:16-anchor (with plan.summary, plan.ambiguities, plan.drift, plan.milestone_index), and set artifact_role='anchor'. See prompts/prompt_16_impl_context.md.
Promotable: No (warning only).
Trigger: A file with artifact_role='anchor' lives inside spec/impl_context/ instead of at spec/16_impl_context.json. The routing logic demotes the dispatch, so every drift check resolves impl_context_dir to a nonexistent nested path and silently no-ops.
Resolution: Move the file to spec/16_impl_context.json (one level up from impl_context/) so cross-milestone drift checks can resolve the impl_context/ sibling directory correctly.
Trigger: A checklist item id in a milestone plan does not start with the checklist_id_prefix declared for that milestone in the anchor's milestone_index.
Resolution: Rename the checklist item id to start with the declared prefix ({prefix}_), or correct the checklist_id_prefix in milestone_index if the item's ID is actually correct.
See also: E309 [prefix] shape (a prefix collision between two milestones, vs. W610's single-milestone prefix mismatch).
Trigger: Fired by specdev upstream-backlog (analysis/upstream_backlog.py) when an ambiguity record's impact[] matches none of the three classifier rules used to bucket ambiguities by upstream step, falling through to the unclassified bucket. Applies equally to plan.ambiguities[] (16a) and execution.emergent_ambiguities[] (16b/16c) records.
Resolution: Informational only — review the flagged ambiguity's impact[] text and, if appropriate, extend the classifier rules in upstream_backlog.py to recognize the pattern; no per-artifact fix is required.
Promotable: No. Deliberately excluded from PROMOTABLE_PAIRS — informational only, never promoted.
Trigger: Fired by specdev upstream-backlog whenever its --status filter (default open) excludes at least one record that otherwise satisfies the --severity threshold. DEVSPEC-123: a bare invocation against a milestone plan where most ambiguities had already been resolved looked like the tool was scanning nothing, when in fact execution.emergent_ambiguities[] was being scanned correctly — the resolved records were just silently outside the default view.
Resolution: Informational only — re-run with --status all (or --status resolved) to see the hidden records. The JSON payload also carries this count as summary.hidden_by_status_count, which is always present (0 when nothing is hidden) for programmatic consumers.
Promotable: No. Deliberately excluded from PROMOTABLE_PAIRS — informational only, never promoted.
Trigger: Historical/registered code for "a step in tools/step_order.json is not registered in entry_key_registry.json" (R001). As of the registry-generator hardening (W3), this check moved to the toolkit's own unit test suite (tests/unit/toolkit_invariants/test_step_registry_coverage.py, T-step-registry-coverage) since the generator now enforces step coverage as an internal contract, making a host-runtime check redundant. E620 is retained in ERROR_CODES for historical reference and backward compatibility with external tooling that parses it, but specdev registry-check no longer emits it.
Resolution: Not applicable at host-runtime (the check no longer fires there). If a step is missing from the registry, run specdev registry-generate and confirm pytest tests/unit/toolkit_invariants/test_step_registry_coverage.py passes.
Promotable: No (error only).
Trigger: R002 — specdev registry-check's _check_phantom_basenames finds a basename registered in tools/entry_key_registry.json that does not appear in tools/extraction_paths.json.
Resolution: Remove or rename the orphaned entry in entry_key_registry.json, or add the corresponding entry to extraction_paths.json. Run specdev registry-generate to regenerate both files consistently.
Promotable: No (error only).
Trigger: R003 — specdev registry-check finds that a registered (array_path, id_field) pair no longer matches the live spec file's structure: the declared array path is not an array, or its entries lack the registered id_field. A missing array path is drift only at the top level; a missing nested array path (e.g. a task's acceptance_criteria) is not flagged here, since nested arrays may be legitimately optional per schema — a present-but-wrong-shape nested array still fires E622.
Resolution: Run specdev registry-generate to regenerate entry_key_registry.json/extraction_paths.json from the current schemas, or fix the schema/spec structure if the drift indicates an unintended shape change.
Promotable: No (error only).
Trigger: R004 — a host spec file has an array of dict items bearing an *_id (or id) field that is not declared in the registry as a known (basename, array_path) pair — a novel array the generator doesn't yet know about.
Resolution: If the array should be tracked (e.g. for cross-step ID validation), add it to the registry source and run specdev registry-generate. If it's intentionally untracked free-form data, no action is required — R004 is advisory (WARNING, not ERROR) precisely because the generator may not yet cover every new array shape.
Promotable: No (warning only).
Module: specdev_tools.core.errors
Raised when: The toolkit cannot detect the git root in a submodule deployment.
Common triggers:
- Running from a detached HEAD state in the submodule
--repo-rootpointing to the wrong directory
Resolution: Pass --git-root pointing to the host repo's git root, and --spec-root pointing to the spec directory.
Module: specdev_tools.core.errors
Raised when: A schema URI cannot be resolved from tools/schema_registry.json.
Common triggers:
- Missing entry in
tools/schema_registry.json --repo-rootnot pointing to the toolkit directory- Schema file referenced in the registry doesn't exist on disk
Resolution: Check tools/schema_registry.json for the expected URI mapping and verify --repo-root points to the devspec_toolkit directory.