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

1"""M14.0: stub state implementations for the retain FSM. 

2 

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. 

8 

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) 

15 

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 

21 

22The engine wraps each state in step-log tracking + error handling, so 

23state bodies should focus on the work, not the scaffolding. 

24""" 

25 

26from __future__ import annotations 

27 

28from typing import TYPE_CHECKING 

29 

30from astrocyte.pipeline.retain_fsm.engine import Complete, Failed 

31 

32if TYPE_CHECKING: 

33 from astrocyte.pipeline.retain_fsm.context import RetainContext, RetainServices 

34 

35 

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. 

41 

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" 

53 

54 

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" 

69 

70 

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() 

79 

80 

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} 

88 

89 

90def register_default_states(fsm) -> None: # type: ignore[no-untyped-def] 

91 """Register the M14.0 stub states on a fresh :class:`RetainFSM`. 

92 

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)