Coverage for astrocyte/pipeline/retain_fsm/states.py: 95%
19 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
1"""M14.0: stub state implementations for the retain FSM.
3This module is intentionally minimal — M14.0 ships only the engine
4scaffold and three sentinel states (``INIT``, ``READ``, ``COMPLETE``)
5so end-to-end tests can verify the scaffold drives transitions
6correctly. The real extraction / compile / wiki / supersedes states
7are added in M14.1 through M14.5.
9Each state function is an async coroutine taking ``(ctx, services)`` and
10returning one of:
11- a state name string (transition to that state next)
12- ``Complete()`` (terminal success)
13- ``Failed(reason)`` (terminal failure)
14- ``Parallel(branches, join)`` (fan out + join)
16State implementations should:
17- Read inputs from ``ctx`` (e.g. ``ctx.md_text``)
18- Append outputs to mutable list fields on ``ctx`` (e.g. ``ctx.facts``)
19- Use ``services.store`` / ``services.provider`` / etc. for I/O
20- Return the next state name or terminal sentinel
22The engine wraps each state in step-log tracking + error handling, so
23state bodies should focus on the work, not the scaffolding.
24"""
26from __future__ import annotations
28from typing import TYPE_CHECKING
30from astrocyte.pipeline.retain_fsm.engine import Complete, Failed
32if TYPE_CHECKING:
33 from astrocyte.pipeline.retain_fsm.context import RetainContext, RetainServices
36async def state_init(
37 ctx: RetainContext,
38 services: RetainServices, # noqa: ARG001 -- M14.1+ uses
39) -> str | Failed:
40 """Entry state: validate inputs and transition to READ.
42 M14.0 minimal — just sanity-check we have the bare inputs needed.
43 M14.1+ may add bank-state preflight (existing-doc detection,
44 incremental-vs-fresh routing).
45 """
46 if not ctx.bank_id:
47 return Failed("INIT: bank_id is required")
48 if not ctx.source_id:
49 return Failed("INIT: source_id is required")
50 if not ctx.md_text or not ctx.md_text.strip():
51 return Failed("INIT: md_text is empty")
52 return "READ"
55async def state_read(
56 ctx: RetainContext,
57 services: RetainServices, # noqa: ARG001 -- M14.1+ uses
58) -> str | Failed:
59 """Parse / tokenise the source. M14.0 stub: no-op pass-through to
60 COMPLETE. M14.1 will:
61 - Run PageIndex md_to_tree
62 - Save document + sections via services.store
63 - Set ctx.document_id and populate ctx.sections
64 - Transition to a parallel block (EXTRACT_FACTS + EXTRACT_ENTITIES + EMBED)
65 """
66 # Stub for M14.0 — exists so engine tests can verify the INIT→READ
67 # transition fires. Real implementation in M14.1.
68 return "COMPLETE"
71async def state_complete(
72 ctx: RetainContext, # noqa: ARG001
73 services: RetainServices, # noqa: ARG001
74) -> Complete:
75 """Terminal success state. Exists so the engine can resolve
76 ``COMPLETE`` as a registered state name (avoids special-casing the
77 string in the engine — every transition target is a real state)."""
78 return Complete()
81# Default registry: convenience for callers that don't want to register
82# each state by hand. Use :func:`register_default_states` on an FSM.
83DEFAULT_STATES: dict[str, object] = {
84 "INIT": state_init,
85 "READ": state_read,
86 "COMPLETE": state_complete,
87}
90def register_default_states(fsm) -> None: # type: ignore[no-untyped-def]
91 """Register the M14.0 stub states on a fresh :class:`RetainFSM`.
93 Typed loosely to avoid a circular import; in practice callers pass
94 a :class:`~astrocyte.pipeline.retain_fsm.engine.RetainFSM`.
95 """
96 for name, fn in DEFAULT_STATES.items():
97 fsm.register(name, fn)