pyfcstm.diagnostics.codes
Loader for the structured diagnostic code registry.
This module loads codes.yaml (the single source of truth for diagnostic
codes emitted by pyfcstm.model) at import time and exposes the parsed
table as CODE_REGISTRY. Downstream consumers — including the
research_ideas LLM agent loop, IDE integrations, and the future
jsfcstm visualization layer — can mirror this registry to drive their
own dispatch logic without depending on exception message text.
The loader performs structural validation on import so that schema drift
in codes.yaml fails fast. Validation failures raise
CodesSchemaError (subclass of ValueError), so callers can
distinguish “the diagnostics package is structurally broken” from a generic
business-level ValueError further up the stack.
The module contains:
CodeFieldSpec- Per-field schema describing arefspayload key.CodeSpec- Full specification for one diagnostic code.CodesSchemaError- Raised whencodes.yamlis structurally invalid.CODE_REGISTRY- Mappingcode -> CodeSpecloaded at import time.load_codes()- Parse a YAML file path and return the registry.
Note
_ALLOWED_REF_TYPES and _ALLOWED_SEVERITIES are documentation-level
enumerations used to validate the YAML schema. They do not enforce
runtime isinstance checks on emitted ModelDiagnostic.refs values
— type-checking refs payloads at emit time is the emitter’s responsibility
(see PR-2 of issue #103). The schema’s job is to give downstream tooling
a contract to mirror, not to act as a runtime type system.
Example:
>>> from pyfcstm.diagnostics import CODE_REGISTRY
>>> spec = CODE_REGISTRY['E_UNDEFINED_VAR']
>>> spec.severity
'error'
>>> 'var_name' in spec.refs_schema
True
CODE_REGISTRY
- pyfcstm.diagnostics.codes.CODE_REGISTRY: Mapping[str, CodeSpec] = mappingproxy({'E_UNDEFINED_VAR': CodeSpec(code='E_UNDEFINED_VAR', severity='error', description='A guard, effect, or lifecycle-action block references a name that was never declared with a top-level `def` statement.', refs_schema=mappingproxy({'var_name': CodeFieldSpec(name='var_name', type='str', required=True, description='The undeclared identifier as it appeared in the source.', enum=None), 'referenced_in': CodeFieldSpec(name='referenced_in', type='str', required=True, description='Which kind of block contained the reference.', enum=('guard', 'effect', 'enter', 'during', 'exit', 'during_aspect', 'init')), 'state_path': CodeFieldSpec(name='state_path', type='str_or_null', required=False, description='Dotted path of the state that owns the offending block, when the reference is inside a state body (not a guard/effect on a transition).', enum=None), 'expr_text': CodeFieldSpec(name='expr_text', type='str_or_null', required=False, description='Original expression text containing the reference.', enum=None), 'is_temporary': CodeFieldSpec(name='is_temporary', type='bool', required=False, description='``True`` when the identifier looked like a block-local temporary (the read occurred before the variable was assigned within the same ``enter`` / ``during`` / ``exit`` / ``effect`` block). Layer 2 merges what jsfcstm previously emitted as ``fcstm.readBeforeAssignTemporary`` into this single ``E_*`` code plus this flag. Defaults to ``False`` / absent for "real" undefined variables that have no matching ``def`` declaration anywhere.', enum=None)}), example_dsl='def int x = 0;\nstate Root { state A; state B; A -> B : if [unknown_var > 0]; }\n', capability='pure_static', for_llm=ForLlmSpec(summary="A variable identifier appears in an expression (guard, effect, or action block) but has not been declared with ``def`` at the file top, has not been assigned earlier in the same block (so the block-local temporary path doesn't apply), and is not a known function name. The model refuses to bind unknown identifiers because the generated code would not compile.", recommended_actions=(mappingproxy({'kind': 'add_definition', 'target': 'file_top', 'rationale': 'If the variable is genuinely program state, declare it with ``def <type> <name> = <initial>;`` at the file top so the generated runtime allocates storage for it.'}), mappingproxy({'kind': 'assign_first', 'target': 'same_block', 'rationale': 'If the name was meant to be a block-local temporary, assign to it before reading. Block-local temps only exist after their first assignment in the enclosing concrete block.'}), mappingproxy({'kind': 'fix_typo', 'target': 'expression', 'rationale': 'If the identifier was meant to be an existing variable, correct the spelling. ``refs.var_name`` carries the exact spelling from the source.'})), do_not=('Do not declare the variable inside an action block expecting it to leak out — block-local temps are discarded at block exit.', 'Do not rely on Python-style implicit None for the missing identifier; the generated runtime will fail to compile.')), emit_tier='static_pipeline'), 'E_DUPLICATE_VAR': CodeSpec(code='E_DUPLICATE_VAR', severity='error', description='A top-level `def` statement re-declares an identifier that was already defined earlier in the file.', refs_schema=mappingproxy({'var_name': CodeFieldSpec(name='var_name', type='str', required=True, description='The duplicated identifier.', enum=None), 'previous_span': CodeFieldSpec(name='previous_span', type='Span', required=False, description='Source position of the first declaration.', enum=None)}), example_dsl='def int x = 0;\ndef int x = 1;\nstate Root { state A; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='Two ``def`` declarations at the file top use the same variable name. The model allocates one storage slot per name, so a duplicate definition is ambiguous.', recommended_actions=(mappingproxy({'kind': 'drop_duplicate', 'target': 'variable_definition', 'rationale': 'If both definitions are meant to declare the same logical variable, keep the first one and delete the second.'}), mappingproxy({'kind': 'rename_one', 'target': 'variable_definition', 'rationale': 'If both definitions are meant to be separate variables, rename the second to give it its own storage slot.'})), do_not=('Do not assume the second ``def`` overrides the first — the generated runtime treats this as an error and refuses to compile.',)), emit_tier='static_pipeline'), 'E_MISSING_STATE': CodeSpec(code='E_MISSING_STATE', severity='error', description='A transition target references a state path that cannot be resolved in the surrounding hierarchy.', refs_schema=mappingproxy({'state_path': CodeFieldSpec(name='state_path', type='str', required=True, description='The unresolved dotted state path.', enum=None), 'referenced_from': CodeFieldSpec(name='referenced_from', type='str_or_null', required=False, description='Dotted path of the state from which the reference was made.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=False, description='Short tag describing the resolution failure mode.', enum=('not_found', 'parent_missing', 'ambiguous', 'event_path_not_found'))}), example_dsl='state Root { state A; A -> NoSuch; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='A transition references a state name that does not exist in the machine. The transition target / source must resolve to a state declared in this file or imported.', recommended_actions=(mappingproxy({'kind': 'fix_typo', 'target': 'transition', 'rationale': 'If the name was meant to refer to an existing state, correct the spelling. ``refs.state_name`` carries the missing token.'}), mappingproxy({'kind': 'declare_state', 'target': 'composite_state', 'rationale': 'If the state is genuinely missing, add ``state <name>;`` (or a composite block) under the appropriate composite parent.'})), do_not=('Do not silently delete the transition; the original intent of the missing state is lost.',)), emit_tier='static_pipeline'), 'E_DUPLICATE_STATE': CodeSpec(code='E_DUPLICATE_STATE', severity='error', description='Two states share the same name within the same parent scope.', refs_schema=mappingproxy({'state_name': CodeFieldSpec(name='state_name', type='str', required=True, description='The duplicated state name.', enum=None), 'parent_path': CodeFieldSpec(name='parent_path', type='str', required=True, description='Dotted path of the parent that contains both states.', enum=None), 'previous_span': CodeFieldSpec(name='previous_span', type='Span', required=False, description='Source position of the first declaration.', enum=None)}), example_dsl='state Root { state A; state A; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='Two state declarations under the same composite parent use the same name. State names must be unique inside their immediate composite scope.', recommended_actions=(mappingproxy({'kind': 'drop_duplicate', 'target': 'state_definition', 'rationale': 'If both declarations describe the same logical state, keep one and remove the other (merge any actions/transitions onto the survivor).'}), mappingproxy({'kind': 'rename_one', 'target': 'state_definition', 'rationale': 'If the two declarations are meant to be distinct states, rename one to give it its own identity inside the parent.'})), do_not=('Do not assume the second declaration overrides the first — duplicate state names produce a hard error.',)), emit_tier='static_pipeline'), 'E_EVENT_REF_INVALID': CodeSpec(code='E_EVENT_REF_INVALID', severity='error', description="The textual form of an event reference is syntactically invalid (e.g. bare '/', trailing dots, going beyond the root state).", refs_schema=mappingproxy({'event_ref': CodeFieldSpec(name='event_ref', type='str', required=True, description='The raw event reference text.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing why the reference is invalid.', enum=('empty', 'bare_slash', 'invalid_absolute', 'invalid_relative', 'trailing_dots', 'beyond_root'))}), example_dsl='state Root { state A; state B; A -> B : /; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='An event reference (``::``, ``:``, or ``/`` scoped) cannot resolve to a known event under the indicated scope. The grammar accepts the syntactic form, but the resolver cannot bind it to a real event object.', recommended_actions=(mappingproxy({'kind': 'fix_scope_operator', 'target': 'transition', 'rationale': 'The three scope operators do not interchange freely. ``::`` is source-local, ``:`` is parent-chain, ``/`` is root-absolute. Pick the one that matches where the event is actually declared.'}), mappingproxy({'kind': 'declare_event', 'target': 'composite_state', 'rationale': 'If the event was supposed to exist, add an ``event <name>;`` declaration in the matching scope.'})), do_not=("Do not invent a new event by silently aliasing — the dispatcher won't fire on an undeclared name.",)), emit_tier='lookup_api'), 'E_EVENT_NOT_FOUND': CodeSpec(code='E_EVENT_NOT_FOUND', severity='error', description='An event reference parses correctly but does not resolve to any event defined in the targeted scope.', refs_schema=mappingproxy({'event_ref': CodeFieldSpec(name='event_ref', type='str', required=True, description='The raw event reference text.', enum=None), 'scope': CodeFieldSpec(name='scope', type='str', required=True, description='Resolution scope used.', enum=('local', 'chain', 'absolute')), 'searched_from': CodeFieldSpec(name='searched_from', type='str_or_null', required=False, description="Dotted state path of the **caller** that initiated the lookup (i.e. the state on which `.resolve_event()` was invoked, or, for `StateMachine.resolve_event`, the state machine's root). This is the originating location of the bad reference in source — not the effective search root, which is always the model root for absolute references.", enum=None)}), example_dsl='state Root { state A; state B; A -> B :: NoEvent; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='A dotted event path could not be resolved against the assembled state hierarchy. The path syntax was valid but no event with that name exists at the resolved state.', recommended_actions=(mappingproxy({'kind': 'fix_event_path', 'target': 'transition', 'rationale': 'Confirm the dotted path matches the state hierarchy that actually exists after imports have been merged.'}), mappingproxy({'kind': 'declare_event', 'target': 'composite_state', 'rationale': 'Add the missing ``event <name>;`` declaration under the intended state.'})), do_not=('Do not paper over the error by introducing a wildcard alias — events are uniquely bound by their scope.',)), emit_tier='lookup_api'), 'E_DANGLING_TRANSITION': CodeSpec(code='E_DANGLING_TRANSITION', severity='error', description='A transition cannot resolve either its source or its target.', refs_schema=mappingproxy({'src': CodeFieldSpec(name='src', type='str_or_null', required=False, description='Raw source state expression as written in the DSL.', enum=None), 'tgt': CodeFieldSpec(name='tgt', type='str_or_null', required=False, description='Raw target state expression as written in the DSL.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing the dangling reason.', enum=('src_not_found', 'tgt_not_found', 'both_not_found'))}), example_dsl='state Root { state A; NoSuch -> A; }\n', capability='pure_static', for_llm=ForLlmSpec(summary="A transition's source or target points at a state that exists neither in the local composite scope nor in any ancestor scope. The transition cannot be wired into the dispatch graph.", recommended_actions=(mappingproxy({'kind': 'fix_typo', 'target': 'transition', 'rationale': 'If the dangling name is misspelled, correct it. ``refs.state_path`` carries the location of the offending transition.'}), mappingproxy({'kind': 'declare_target_state', 'target': 'composite_state', 'rationale': 'Declare the missing state if the transition was intentional.'}), mappingproxy({'kind': 'drop_transition', 'target': 'transition', 'rationale': 'Remove the transition if it was speculative — leaving a dangling reference blocks the whole file.'})), do_not=('Do not pretend the transition resolves to ``[*]`` — that has a specific entry/exit semantics.',)), emit_tier='static_pipeline'), 'E_TYPE_MISMATCH': CodeSpec(code='E_TYPE_MISMATCH', severity='error', description='A guard, effect, or assignment uses an expression whose type does not match the expected category (arithmetic vs boolean).', refs_schema=mappingproxy({'expected': CodeFieldSpec(name='expected', type='str', required=True, description="Expected category. One of: 'numeric', 'boolean'.", enum=None), 'actual': CodeFieldSpec(name='actual', type='str', required=True, description='Actual category produced by the expression.', enum=None), 'expr_text': CodeFieldSpec(name='expr_text', type='str', required=True, description='Original expression text.', enum=None)}), example_dsl='def int x = 0;\nstate Root { state A; state B; A -> B : if [x]; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='An expression mixes an arithmetic and a boolean subexpression in a way the grammar does not allow. ``num_expression`` and ``cond_expression`` are strictly separated; comparison operators bridge them, ternary ``? :`` converts boolean → arithmetic.', recommended_actions=(mappingproxy({'kind': 'wrap_in_ternary', 'target': 'expression', 'rationale': 'If you wanted to assign a boolean test, wrap it as ``(cond) ? 1 : 0`` so the right-hand side is arithmetic.'}), mappingproxy({'kind': 'add_comparison', 'target': 'guard', 'rationale': 'If you wanted to gate on a numeric value, compare against a constant (``counter > 0``) instead of using the value directly as a boolean.'})), do_not=('Do not rely on numeric → boolean implicit coercion; the generated runtimes treat the two domains as distinct.',)), emit_tier='partial_static_pipeline'), 'E_FORCED_TRANSITION_EXPANSION': CodeSpec(code='E_FORCED_TRANSITION_EXPANSION', severity='error', description='A forced transition (`!State -> ...` or `!*`) cannot be expanded because the source or target reference is invalid.', refs_schema=mappingproxy({'original_raw': CodeFieldSpec(name='original_raw', type='str', required=True, description='Raw forced-transition text as written.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing why expansion failed.', enum=('src_not_found', 'tgt_not_found', 'src_is_root', 'wildcard_no_descendants'))}), example_dsl='state Root { state A; !NoSuch -> A; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='A forced transition (``!Source -> Target`` or ``!* -> Target``) failed to expand cleanly — typically because the source name cannot be found, or because the expansion would create an empty/inconsistent fan-out under the current scope.', recommended_actions=(mappingproxy({'kind': 'fix_source', 'target': 'forced_transition', 'rationale': 'Confirm the ``!Name`` source is a real state inside the current composite scope. ``!*`` only matches direct substates; deeper descendants need explicit names.'}), mappingproxy({'kind': 'relocate_forced_transition', 'target': 'composite_state', 'rationale': 'Move the forced transition into the composite whose substates you actually want to receive the event.'})), do_not=('Do not add an ``effect { ... }`` block to a forced transition — forced transitions cannot carry effects and the expansion would fail anyway.',)), emit_tier='static_pipeline'), 'E_INITIAL_TRANSITION_INVALID': CodeSpec(code='E_INITIAL_TRANSITION_INVALID', severity='error', description='A composite state either lacks an entry transition (`[*] -> child`) or declares one whose target is not a direct child of the composite.', refs_schema=mappingproxy({'composite_path': CodeFieldSpec(name='composite_path', type='str', required=True, description='Dotted path of the offending composite state.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing the failure.', enum=('missing_entry', 'target_not_child'))}), example_dsl='state Root { state Outer { state Inner; } }\n', capability='pure_static', for_llm=ForLlmSpec(summary='A composite state lacks a valid ``[*] -> Child`` initial transition, or its declared initial target is not actually a child of this composite. The runtime cannot decide which child to enter when this composite is activated.', recommended_actions=(mappingproxy({'kind': 'add_initial_transition', 'target': 'composite_state', 'rationale': 'Add ``[*] -> <ChildName>;`` inside the composite, picking a direct substate as the entry point.'}), mappingproxy({'kind': 'fix_initial_target', 'target': 'initial_transition', 'rationale': 'If an initial transition already exists, retarget it at a direct child of this composite (not a grandchild).'})), do_not=('Do not rely on a default ordering of substates; the runtime requires an explicit initial transition.',)), emit_tier='static_pipeline'), 'E_DUPLICATE_FUNCTION_NAME': CodeSpec(code='E_DUPLICATE_FUNCTION_NAME', severity='error', description='Two named lifecycle actions (enter / during / exit / during-aspect) within the same state share the same name. Named actions are referenced via `ref <name>` and must be uniquely identifiable.', refs_schema=mappingproxy({'function_name': CodeFieldSpec(name='function_name', type='str', required=True, description='The duplicated named-action name.', enum=None), 'state_path': CodeFieldSpec(name='state_path', type='str', required=True, description='Dotted path of the state declaring the duplicates.', enum=None), 'stage': CodeFieldSpec(name='stage', type='str', required=True, description='Lifecycle stage where the duplicate appears.', enum=('enter', 'during', 'exit', 'during_aspect'))}), example_dsl='state Root {\n state A {\n enter Foo { }\n enter Foo { }\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='Two named action / function blocks share the same name within the same scope. Named blocks must be uniquely identifiable so ``ref`` lookups can bind to a single definition.', recommended_actions=(mappingproxy({'kind': 'rename_one', 'target': 'named_action', 'rationale': 'Give one of the named blocks a distinct identifier so ``ref <name>`` calls resolve unambiguously.'}), mappingproxy({'kind': 'drop_duplicate', 'target': 'named_action', 'rationale': 'If the two declarations are meant to be the same logical block, keep one and update its callers to reference it.'})), do_not=('Do not assume the second declaration shadows the first — the resolver treats this as a hard error.',)), emit_tier='static_pipeline'), 'E_DURING_ASPECT_INVALID': CodeSpec(code='E_DURING_ASPECT_INVALID', severity='error', description="A `during` block is declared inconsistently with the host state's leaf/composite kind. Leaf states must use `during { ... }` without an aspect modifier; composite states must use `during before { ... }` or `during after { ... }` (the aspect is mandatory on composites). The global ``>> during before/after`` aspect form is **also** invalid on a leaf state — aspect actions fan out to every descendant leaf, and a leaf has nothing to fan into.", refs_schema=mappingproxy({'state_path': CodeFieldSpec(name='state_path', type='str', required=True, description='Dotted path of the offending state.', enum=None), 'state_kind': CodeFieldSpec(name='state_kind', type='str', required=True, description='Kind of the host state.', enum=('leaf', 'composite')), 'aspect': CodeFieldSpec(name='aspect', type='str_or_null', required=False, description="The aspect token that was actually used. ``'before'`` or ``'after'`` for ``during before`` / ``during after`` and ``>> during before/after``; ``null`` when the user wrote a bare ``during`` on a composite state. ``null`` is intentional — the consumer should branch on the bare-during case rather than treat it as an enum value.", enum=None)}), example_dsl='state Root {\n state A {\n during before { }\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='A ``during before`` / ``during after`` or ``>> during ...`` aspect declaration was placed on a state that cannot legally receive it (typically a leaf state with no descendant for the aspect to fan out to).', recommended_actions=(mappingproxy({'kind': 'move_to_composite', 'target': 'aspect_declaration', 'rationale': 'Aspect actions only make sense on composite states. Move the declaration to a composite ancestor whose descendants should receive the aspect.'}), mappingproxy({'kind': 'drop_aspect', 'target': 'aspect_declaration', 'rationale': 'If the aspect was speculative and the state is genuinely a leaf, remove the aspect declaration entirely.'})), do_not=('Do not convert the leaf into a composite just to host the aspect — that changes the runtime entry/exit semantics.',)), emit_tier='static_pipeline'), 'E_PSEUDO_NOT_LEAF': CodeSpec(code='E_PSEUDO_NOT_LEAF', severity='error', description='A state was declared with the `pseudo` keyword but has nested substates. Pseudo states must be leaves: they skip ancestor `>> during` aspects and only make sense without inner structure.', refs_schema=mappingproxy({'state_path': CodeFieldSpec(name='state_path', type='str', required=True, description='Dotted path of the offending pseudo state.', enum=None)}), example_dsl='state Root {\n pseudo state Outer {\n state Inner;\n [*] -> Inner;\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='A ``pseudo state`` declaration was given a composite body (or otherwise treated as if it had descendants). Pseudo states must be leaves: they cannot host substates because their semantics explicitly skip ancestor ``>> during`` aspects.', recommended_actions=(mappingproxy({'kind': 'drop_pseudo_marker', 'target': 'state_definition', 'rationale': 'If the state genuinely needs substates, remove the ``pseudo`` keyword and treat it as a normal composite.'}), mappingproxy({'kind': 'flatten_pseudo', 'target': 'state_definition', 'rationale': 'If the state is meant to be a transient pseudo (e.g. an internal junction), remove its substates and turn it back into a leaf.'})), do_not=('Do not rely on pseudo states for normal hierarchy modeling — they exist solely to bypass ``>> during`` aspect application.',)), emit_tier='static_pipeline'), 'E_NAMED_FUNCTION_REF_NOT_FOUND': CodeSpec(code='E_NAMED_FUNCTION_REF_NOT_FOUND', severity='error', description='A `ref` lifecycle action could not resolve its target named action. Either the path walks through a state that does not exist, or the final segment is not a named action defined inside the resolved state.', refs_schema=mappingproxy({'ref_path': CodeFieldSpec(name='ref_path', type='str', required=True, description='Dotted path of the unresolved reference.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing the resolution failure.', enum=('state_not_found', 'named_function_not_found')), 'missing_segment': CodeFieldSpec(name='missing_segment', type='str_or_null', required=False, description="The path segment that failed to resolve. For 'state_not_found' this is the state name that does not exist under the parent path; for 'named_function_not_found' this is the action name searched for in the resolved state.", enum=None)}), example_dsl='state Root {\n state A {\n enter ref NoSuch.NoSuch;\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='An ``enter ref ...`` / ``exit ref ...`` / ``during ref ...`` clause points at a named action that does not exist. The resolver could not find a matching named block in the indicated scope.', recommended_actions=(mappingproxy({'kind': 'fix_ref_path', 'target': 'action_ref', 'rationale': 'Confirm the dotted ref path matches a real named action. Use ``/`` to anchor at the root, or a relative name for the current state.'}), mappingproxy({'kind': 'declare_named_action', 'target': 'composite_state', 'rationale': 'Add the missing ``enter <Name> { ... }`` / similar block in the resolving scope.'})), do_not=('Do not silently swap ``ref`` with an inline block — that loses the deduplication that ``ref`` was meant to provide.',)), emit_tier='static_pipeline'), 'E_IMPORT_NOT_FOUND': CodeSpec(code='E_IMPORT_NOT_FOUND', severity='error', description='An ``import`` statement points at a source file that cannot be found, cannot be read, or fails to parse.', refs_schema=mappingproxy({'source_path': CodeFieldSpec(name='source_path', type='str', required=True, description='The source-path token from the import statement, exactly as it appeared in the DSL.', enum=None), 'alias': CodeFieldSpec(name='alias', type='str', required=True, description='The alias under which the import was declared.', enum=None), 'host_state_path': CodeFieldSpec(name='host_state_path', type='str', required=True, description='Dotted path of the composite state hosting the import statement.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Why resolution failed.', enum=('file_not_found', 'read_error', 'parse_error', 'no_root_state'))}), example_dsl='state System {\n import "missing.fcstm" as Sub;\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='An import target could not be resolved. Inspect ``refs.reason`` to branch the fix: ``file_not_found`` means the path is wrong or the file does not exist; ``read_error`` means the file exists but the I/O layer rejected it (permissions / encoding); ``parse_error`` means the file was loaded but is not valid FCSTM DSL; ``no_root_state`` means the file parses but declares no ``state Root { ... }`` block. The four reasons need different fixes — do not collapse them. Cross-end note (I2 from PR #115 final review): jsfcstm currently only distinguishes ``file_not_found`` and ``no_root_state``; its workspace index treats I/O failures and parse failures alike as ``missing`` and emits ``file_not_found`` for both. When consuming jsfcstm-side diagnostics, treat ``file_not_found`` as "any pre-no_root_state failure" and inspect the imported file directly to disambiguate read vs parse failures.', recommended_actions=(mappingproxy({'kind': 'fix_path', 'target': 'import_statement', 'applies_to_reason': 'file_not_found', 'rationale': 'For ``file_not_found``, adjust the source_path to a real file relative to the host file, or remove the import if the sub-machine is no longer required.'}), mappingproxy({'kind': 'create_file', 'target': 'import_target', 'applies_to_reason': 'file_not_found', 'rationale': 'For ``file_not_found``, if the file should exist, create it with at least a top-level ``state Root { ... }`` so its root state can be merged in.'}), mappingproxy({'kind': 'fix_permissions', 'target': 'import_target', 'applies_to_reason': 'read_error', 'rationale': 'For ``read_error``, the file exists but the I/O layer could not read it. Check file permissions, encoding (FCSTM expects UTF-8 or auto-detected text), and that the path is not a directory.'}), mappingproxy({'kind': 'fix_dsl_in_imported_file', 'target': 'import_target', 'applies_to_reason': 'parse_error', 'rationale': 'For ``parse_error``, the file was read but contains invalid FCSTM DSL. Open the imported file directly; the underlying grammar parse error is reported with its own line/column when you run ``pyfcstm`` on it standalone.'}), mappingproxy({'kind': 'add_root_state', 'target': 'import_target', 'applies_to_reason': 'no_root_state', 'rationale': 'For ``no_root_state``, the file parses but declares no top-level ``state <Name> { ... }``. Add one — the merge needs a root state to inline.'})), do_not=('Do not silently rename the alias to dodge the missing file.', 'Do not add a placeholder state under the alias — the import must resolve to a real file.', 'Do not give the same recommended_action regardless of reason — the LLM consumer should branch on ``refs.reason`` and apply only the matching ``applies_to_reason`` actions.')), emit_tier='static_pipeline'), 'E_IMPORT_CIRCULAR': CodeSpec(code='E_IMPORT_CIRCULAR', severity='error', description='A cycle was detected while resolving ``import`` statements between two or more state-machine source files.', refs_schema=mappingproxy({'source_path': CodeFieldSpec(name='source_path', type='str', required=True, description='The source-path token that closed the cycle.', enum=None), 'alias': CodeFieldSpec(name='alias', type='str', required=True, description='The alias under which the cyclic import was declared.', enum=None), 'host_state_path': CodeFieldSpec(name='host_state_path', type='str', required=True, description='Dotted path of the composite state hosting the import.', enum=None), 'cycle_chain': CodeFieldSpec(name='cycle_chain', type='list[str]', required=True, description='Ordered list of file paths that participate in the cycle, starting and ending at the same file.', enum=None)}), example_dsl='# a.fcstm\nstate A { import "b.fcstm" as B; }\n# b.fcstm\nstate B { import "a.fcstm" as A; }\n', capability='pure_static', for_llm=ForLlmSpec(summary='Two or more state-machine files import each other transitively. The compiler refuses to resolve cycles to keep the merged hierarchy tractable.', recommended_actions=(mappingproxy({'kind': 'refactor', 'target': 'import_statement', 'rationale': 'Break the cycle by extracting the shared state(s) into a third file that both ends import without depending on each other.'}), mappingproxy({'kind': 'drop_back_edge', 'target': 'import_statement', 'rationale': 'If the cycle is accidental, remove the import statement that closes the loop (the one named in ``source_path``).'})), do_not=('Do not duplicate the imported states inline to break the cycle — that defeats the purpose of imports.', 'Do not introduce a stub alias that re-exports the cycle from another file.')), emit_tier='static_pipeline'), 'E_IMPORT_ALIAS_CONFLICT': CodeSpec(code='E_IMPORT_ALIAS_CONFLICT', severity='error', description='An ``import`` alias clashes with an existing child state (or with another import alias) under the same composite state.', refs_schema=mappingproxy({'alias': CodeFieldSpec(name='alias', type='str', required=True, description='The conflicting alias name.', enum=None), 'host_state_path': CodeFieldSpec(name='host_state_path', type='str', required=True, description='Dotted path of the composite state hosting the import.', enum=None), 'conflicting_kind': CodeFieldSpec(name='conflicting_kind', type='str', required=True, description='What the alias clashed with.', enum=('existing_substate', 'previous_import_alias'))}), example_dsl='state System {\n state Worker;\n import "worker.fcstm" as Worker;\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='The import alias collides with an already-defined name in the same composite scope. The merged hierarchy cannot have two children sharing a name.', recommended_actions=(mappingproxy({'kind': 'rename_alias', 'target': 'import_statement', 'rationale': 'Pick a unique alias for the import; references to the imported machine elsewhere in the file must follow the new alias.'}), mappingproxy({'kind': 'rename_other', 'target': 'existing_substate', 'rationale': 'If the existing substate is the one that should be renamed, update its declaration and every transition referencing it.'})), do_not=('Do not silently drop one of the conflicting definitions.', 'Do not add a numeric suffix to the alias as an automatic fix without checking downstream references.')), emit_tier='static_pipeline'), 'E_IMPORT_DUPLICATE_MAPPING': CodeSpec(code='E_IMPORT_DUPLICATE_MAPPING', severity='error', description='Two or more mapping clauses under the same ``import { ... }`` block target the same source name (event or variable), or two source names target the same host name.', refs_schema=mappingproxy({'alias': CodeFieldSpec(name='alias', type='str', required=True, description='The import alias whose mapping is duplicated.', enum=None), 'mapping_kind': CodeFieldSpec(name='mapping_kind', type='str', required=True, description='Which mapping flavor was duplicated.', enum=('event', 'variable')), 'duplicated_name': CodeFieldSpec(name='duplicated_name', type='str', required=True, description='The source-side or target-side name that appears more than once.', enum=None), 'direction': CodeFieldSpec(name='direction', type='str', required=True, description='Which side of the mapping was duplicated.', enum=('source_duplicated', 'target_duplicated')), 'host_state_path': CodeFieldSpec(name='host_state_path', type='str', required=True, description='Dotted path of the composite state hosting the import.', enum=None)}), example_dsl='state System {\n import "sub.fcstm" as Sub {\n def x = a;\n def x = b;\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='An import-mapping block contains a duplicate source or target name. Each source identifier can be mapped at most once; each target host slot can receive at most one source.', recommended_actions=(mappingproxy({'kind': 'drop_duplicate', 'target': 'mapping_clause', 'rationale': 'Delete the duplicated mapping line — keep the intended one.'}), mappingproxy({'kind': 'rename_target', 'target': 'mapping_clause', 'rationale': 'If both sources are needed, give them distinct host-side names via a fresh mapping target.'})), do_not=('Do not merge mappings by concatenating their values.', 'Do not introduce wildcard selectors to mask the duplicate.')), emit_tier='static_pipeline'), 'E_IMPORT_MAPPING_INVALID': CodeSpec(code='E_IMPORT_MAPPING_INVALID', severity='error', description='An import-mapping clause refers to a source name that does not exist in the imported machine, uses an invalid selector / template, or points at a host target that cannot host the mapped element.', refs_schema=mappingproxy({'alias': CodeFieldSpec(name='alias', type='str', required=True, description='The import alias whose mapping is malformed.', enum=None), 'mapping_kind': CodeFieldSpec(name='mapping_kind', type='str', required=True, description='Mapping flavor.', enum=('event', 'variable')), 'host_state_path': CodeFieldSpec(name='host_state_path', type='str', required=True, description='Dotted path of the composite state hosting the import.', enum=None), 'reason': CodeFieldSpec(name='reason', type='str', required=True, description='Short tag describing the failure mode.', enum=('source_not_found', 'target_invalid', 'selector_invalid', 'template_invalid', 'empty_path', 'host_root_mismatch')), 'detail': CodeFieldSpec(name='detail', type='str', required=True, description='Human-readable specifics of the mapping failure (mapping text, selector / template literal, or path that failed to resolve).', enum=None)}), example_dsl='state System {\n import "sub.fcstm" as Sub {\n def x = no_such_var;\n }\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary='An ``import { ... }`` mapping clause is malformed or points at a non-existent element on the source side / a non-receivable element on the host side.', recommended_actions=(mappingproxy({'kind': 'fix_source', 'target': 'mapping_clause', 'rationale': 'Update the source-side name to match a real def / event in the imported file.'}), mappingproxy({'kind': 'fix_target', 'target': 'mapping_clause', 'rationale': 'Update the host-side target to a valid identifier that the merge can attach the imported element to.'}), mappingproxy({'kind': 'drop_clause', 'target': 'mapping_clause', 'rationale': 'Remove the mapping if it was speculative — unmapped imports still merge under their alias.'})), do_not=('Do not invent fake source names to make the selector match.', 'Do not silently widen the selector to a wildcard if the original was specific.')), emit_tier='static_pipeline'), 'W_UNREACHABLE_STATE': CodeSpec(code='W_UNREACHABLE_STATE', severity='warning', description="A state is not reachable from the model's root entry path via any sequence of normal or forced transitions.", refs_schema=mappingproxy({'state_path': CodeFieldSpec(name='state_path', type='str', required=True, description='Dotted path of the unreachable state.', enum=None)}), example_dsl='state Root {\n state Idle;\n state Orphan;\n [*] -> Idle;\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary="This state cannot be entered at runtime; the surrounding transitions never lead to it. It is dead from the machine's perspective.", recommended_actions=(mappingproxy({'kind': 'add_inbound_transition', 'target': 'state', 'rationale': 'Add a transition that reaches the state if it was meant to be used.'}), mappingproxy({'kind': 'remove_state', 'target': 'state', 'rationale': 'Remove the state if it was added speculatively.'})), do_not=('Do not add a self-loop to mask the unreachability.',)), emit_tier='static_pipeline'), 'W_GUARD_CONST_FALSE': CodeSpec(code='W_GUARD_CONST_FALSE', severity='warning', description="A transition's guard condition reduces to the literal ``false`` after constant folding, so the transition can never fire.", refs_schema=mappingproxy({'transition_span': CodeFieldSpec(name='transition_span', type='Span', required=False, description='Source span of the transition declaration.', enum=None), 'folded_value': CodeFieldSpec(name='folded_value', type='bool', required=True, description='Constant-folded guard value (always ``false``).', enum=None)}), example_dsl='state Root {\n state A;\n state B;\n [*] -> A;\n A -> B : if [false];\n}\n', capability='const_fold', for_llm=ForLlmSpec(summary="The transition's guard is statically known to be ``false`` and the transition is effectively dead code.", recommended_actions=(mappingproxy({'kind': 'fix_guard', 'target': 'transition', 'rationale': 'Rewrite the guard so it can become true under some valid state.'}), mappingproxy({'kind': 'remove_transition', 'target': 'transition', 'rationale': 'Remove the transition if it was placeholder / debug.'})), do_not=('Do not silently remove the guard — that would change the semantics.',)), emit_tier='static_pipeline'), 'W_UNUSED_EVENT': CodeSpec(code='W_UNUSED_EVENT', severity='warning', description='An ``event`` declaration is never referenced by any transition.', refs_schema=mappingproxy({'event_qualified_name': CodeFieldSpec(name='event_qualified_name', type='str', required=True, description='Fully qualified event name (e.g. ``Root.SubA.Pause``).', enum=None), 'scope': CodeFieldSpec(name='scope', type='str', required=True, description='Scope at which the event is declared.', enum=('local', 'chain', 'absolute'))}), example_dsl='state Root {\n event Unused;\n state A;\n state B;\n [*] -> A;\n A -> B :: SomethingElse;\n}\n', capability='pure_static', for_llm=ForLlmSpec(summary="An event is declared but never triggers any transition, so it is unused from the machine's perspective.", recommended_actions=(mappingproxy({'kind': 'wire_event', 'target': 'transition', 'rationale': 'Add a transition that uses the event if it was meant to be emitted.'}), mappingproxy({'kind': 'remove_event', 'target': 'event_declaration', 'rationale': 'Remove the event if it was added speculatively.'})), do_not=('Do not rename the event to dodge the warning — that just shifts the problem.',)), emit_tier='static_pipeline')})
Mapping
code -> CodeSpecloaded fromcodes.yamlat import time. Wrapped intypes.MappingProxyTypeso downstream callers cannot mutate the registry by accident.
CodesSchemaError
- class pyfcstm.diagnostics.codes.CodesSchemaError[source]
Raised when
codes.yamlis structurally invalid.Subclasses
ValueErrorso genericexcept ValueErrorhandlers still catch it, but downstream tooling that wants to distinguish “diagnostics package broken” from a domain-levelValueErrorcan use a tighter handler.
CodeFieldSpec
- class pyfcstm.diagnostics.codes.CodeFieldSpec(name: str, type: str, required: bool, description: str, enum: Tuple[str, ...] | None = None)[source]
Schema for a single field inside
CodeSpec.refs_schema.- Parameters:
name (str) – Field name as it will appear in
ModelDiagnostic.refs.type (str) – Field type token. Must be one of the allowed type tokens documented at the top of
codes.yaml.required (bool) – Whether this field must be present when the diagnostic is emitted.
description (str) – Human-readable explanation of the field.
enum (Optional[Tuple[str, ...]]) – Optional tuple of allowed string values for the field. When present, downstream emit-test infrastructure (and any future runtime validator) checks that
refs[field]is a member of the tuple.Nonemeans the field has no enumeration constraint.
ForLlmSpec
- class pyfcstm.diagnostics.codes.ForLlmSpec(summary: str, recommended_actions: Tuple[Mapping[str, Any], ...], do_not: Tuple[str, ...])[source]
Structured guidance attached to a diagnostic code for downstream LLM consumers.
Layer 2 (issue #104) requires this for every emitted code —
E_*,W_*, andI_*— so that LLM agent loops can read structured fix recommendations instead of regex-ing the human-readablemessage. PR-A originally grandfathered the 14 Layer 1E_*codes; PR-A-fix I-a backfilled them so this field is now expected on every catalogued code.- Parameters:
summary (str) – One-line description aimed at LLM consumers.
recommended_actions (Tuple[Mapping[str, Any], ...]) – Ordered list of concrete fix suggestions. Each entry is a free-form dict; downstream tooling is expected to treat the list as a hint rather than a closed schema.
do_not (Tuple[str, ...]) – List of anti-pattern strings the LLM should avoid.
CodeSpec
- class pyfcstm.diagnostics.codes.CodeSpec(code: str, severity: str, description: str, refs_schema: Mapping[str, CodeFieldSpec], example_dsl: str | None = None, capability: str = 'pure_static', for_llm: ForLlmSpec | None = None, emit_tier: str = 'static_pipeline')[source]
Full specification for a single diagnostic code.
- Parameters:
code (str) – Stable code identifier (e.g.
'E_UNDEFINED_VAR').severity (str) –
'error','warning', or'info'.description (str) – Human-readable description of when the code fires.
refs_schema (Mapping[str, CodeFieldSpec]) – Mapping
field_name -> CodeFieldSpecdescribing the structured payload for diagnostics with this code. The mapping itself is atypes.MappingProxyTypeso downstream callers cannot mutate the registry by accident.example_dsl (str, optional) – Minimal DSL snippet that triggers the code, defaults to
None.capability (str, optional) – Which analysis tier this code belongs to. Layer 2 declares this required when present; unset means
'pure_static'for grandfathered Layer 1 codes.for_llm (ForLlmSpec, optional) – Structured guidance for downstream LLM consumers. Backfilled across all
E_*codes by PR-A-fix I-a; new codes are expected to ship with one. Still typed asOptionalso the loader can tolerate forward-compatibility cases.emit_tier (str, optional) – Which emit pipeline actually fires this code.
'static_pipeline'(default) means the code fires duringparse_dsl_node_to_state_machine/ the equivalent jsfcstmcollectDocumentDiagnosticsstatic analysis pass.'lookup_api'means the code only fires through explicit runtime resolver APIs (e.g.State.resolve_event) and is never produced by the static pipeline.'partial_static_pipeline'marks codes whose static-pipeline emit is implemented on one end only (typically jsfcstm) — downstream LLM consumers should not block waiting for the missing end. PR-A-fix I-b makes the field explicit so dispatchers can register handlers based on the actual emit channel.
load_codes
- pyfcstm.diagnostics.codes.load_codes(path: str) Dict[str, CodeSpec][source]
Load and validate a
codes.yamlfile from disk.- Parameters:
path (str) – Filesystem path to the YAML file.
- Returns:
Mapping
code -> CodeSpecparsed from the file.- Return type:
Dict[str, CodeSpec]
- Raises:
FileNotFoundError – If
pathdoes not exist.CodesSchemaError – If the YAML structure does not match the expected schema, or if a code uses an unknown severity / type token. Subclasses
ValueErrorfor backwards compatibility with genericexcept ValueErrorhandlers.
Example:
>>> import os >>> from pyfcstm.diagnostics.codes import load_codes >>> path = os.path.join(os.path.dirname(__file__), 'codes.yaml')