knowcode 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. knowcode-0.1.0.dist-info/METADATA +175 -0
  2. knowcode-0.1.0.dist-info/RECORD +63 -0
  3. knowcode-0.1.0.dist-info/WHEEL +4 -0
  4. knowcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. runtime/__init__.py +4 -0
  6. runtime/artifact/__init__.py +1 -0
  7. runtime/artifact/builder.py +179 -0
  8. runtime/cli/__init__.py +1 -0
  9. runtime/cli/animation.py +278 -0
  10. runtime/cli/app.py +309 -0
  11. runtime/cli/auth.py +171 -0
  12. runtime/cli/telemetry.py +91 -0
  13. runtime/exceptions/__init__.py +1 -0
  14. runtime/exceptions/errors.py +99 -0
  15. runtime/repository/__init__.py +13 -0
  16. runtime/repository/discovery.py +64 -0
  17. runtime/repository/models.py +103 -0
  18. runtime/repository/paths.py +50 -0
  19. runtime/repository/validator.py +100 -0
  20. runtime/services/__init__.py +1 -0
  21. runtime/services/ingest_service.py +105 -0
  22. runtime/services/init_service.py +45 -0
  23. runtime/services/semantic_sync_service.py +55 -0
  24. runtime/services/status_service.py +40 -0
  25. runtime/services/sync_service.py +57 -0
  26. runtime/templates/KNOWCODE_LOADER.md.j2 +24 -0
  27. runtime/templates/README_KNOWLEDGE.md.j2 +12 -0
  28. runtime/templates/README_STRUCTURE.md.j2 +19 -0
  29. runtime/templates/__init__.py +1 -0
  30. runtime/templates/active_context.md.j2 +3 -0
  31. runtime/templates/ingest_legacy.md.j2 +15 -0
  32. runtime/templates/raw_readme.md.j2 +9 -0
  33. runtime/templates/sync_reconciliation.md.j2 +17 -0
  34. runtime/templates/synthesize_knowledge.md.j2 +32 -0
  35. runtime/templates/track_intent.md.j2 +14 -0
  36. structural_engine/__init__.py +3 -0
  37. structural_engine/diff/__init__.py +1 -0
  38. structural_engine/diff/generator.py +92 -0
  39. structural_engine/diff/models.py +48 -0
  40. structural_engine/engine.py +192 -0
  41. structural_engine/logs/__init__.py +1 -0
  42. structural_engine/logs/generator.py +33 -0
  43. structural_engine/parser/__init__.py +7 -0
  44. structural_engine/parser/discovery.py +165 -0
  45. structural_engine/parser/extractors/base.py +44 -0
  46. structural_engine/parser/languages/javascript/adapter.py +149 -0
  47. structural_engine/parser/languages/python/adapter.py +174 -0
  48. structural_engine/parser/languages/typescript/adapter.py +165 -0
  49. structural_engine/parser/models.py +186 -0
  50. structural_engine/parser/parser.py +160 -0
  51. structural_engine/parser/resolvers/calls.py +105 -0
  52. structural_engine/parser/tree_sitter/registry.py +61 -0
  53. structural_engine/reports/__init__.py +1 -0
  54. structural_engine/reports/generator.py +77 -0
  55. structural_engine/results.py +54 -0
  56. structural_engine/revisions/__init__.py +1 -0
  57. structural_engine/revisions/tracker.py +32 -0
  58. structural_engine/snapshot/__init__.py +1 -0
  59. structural_engine/snapshot/generator.py +58 -0
  60. structural_engine/snapshot/loader.py +59 -0
  61. structural_engine/state/__init__.py +1 -0
  62. structural_engine/state/manager.py +169 -0
  63. structural_engine/state/models.py +34 -0
@@ -0,0 +1,105 @@
1
+ """Call resolver.
2
+
3
+ Transforms RawCalls into CALLS relationships.
4
+ Implements the conservative two-pass resolution strategy.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import defaultdict
10
+
11
+ from structural_engine.parser.models import Entity, EntityType, RawCall, Relationship, RelationshipType
12
+
13
+
14
+ def resolve_calls(
15
+ entities: list[Entity],
16
+ relationships: list[Relationship],
17
+ raw_calls: list[RawCall],
18
+ ) -> list[Relationship]:
19
+ """Resolve raw calls into structural CALLS relationships.
20
+
21
+ Uses a conservative strategy. If a call target cannot be uniquely
22
+ resolved, it is dropped. Missing edges are allowed; incorrect edges
23
+ are forbidden.
24
+
25
+ Parameters
26
+ ----------
27
+ entities : list[Entity]
28
+ All extracted entities.
29
+ relationships : list[Relationship]
30
+ Existing extracted relationships (like IMPORTS, CONTAINS).
31
+ raw_calls : list[RawCall]
32
+ Unresolved call extractions.
33
+
34
+ Returns
35
+ -------
36
+ list[Relationship]
37
+ A list of new Relationship objects of type CALLS.
38
+ """
39
+ resolved_calls: list[Relationship] = []
40
+
41
+ # Build a global map of function/method names to their full entity IDs.
42
+ name_to_ids: dict[str, list[str]] = defaultdict(list)
43
+ for entity in entities:
44
+ if entity.type in (EntityType.FUNCTION, EntityType.METHOD):
45
+ name_to_ids[entity.name].append(entity.id)
46
+
47
+ # Build an import map: source_file_id -> set of target_modules
48
+ # For Pass 1 (Import Guided) we'd use this.
49
+ imports: dict[str, set[str]] = defaultdict(set)
50
+ for rel in relationships:
51
+ if rel.type == RelationshipType.IMPORTS:
52
+ imports[rel.source_id].add(rel.target_id)
53
+
54
+ for call in raw_calls:
55
+ candidates = name_to_ids.get(call.target_name, [])
56
+
57
+ # Pass 2: Global Unique Match
58
+ # If there is exactly one function/method in the entire repository
59
+ # with this name, we resolve it.
60
+ if len(candidates) == 1:
61
+ resolved_calls.append(
62
+ Relationship(
63
+ source_id=call.caller_id,
64
+ target_id=candidates[0],
65
+ type=RelationshipType.CALLS,
66
+ )
67
+ )
68
+ continue
69
+
70
+ # Pass 1: Import Guided (simplified for V1)
71
+ # If there are multiple candidates, we could check if the caller's file
72
+ # imports the module containing the candidate.
73
+ # This requires tracking the file containing each candidate.
74
+ if len(candidates) > 1:
75
+ caller_file_id = call.source_file
76
+ caller_imports = imports.get(caller_file_id, set())
77
+
78
+ valid_candidates = []
79
+ for candidate_id in candidates:
80
+ # candidate_id format: src/auth.py::verify_token
81
+ candidate_file_id = candidate_id.split("::")[0]
82
+ # If it's in the same file, it's a strong candidate
83
+ if candidate_file_id == caller_file_id:
84
+ valid_candidates.append(candidate_id)
85
+ else:
86
+ # Check if caller imports the candidate's module
87
+ # For simplicity, we check if the candidate_file_id is a substring
88
+ # of any import target.
89
+ candidate_module_base = candidate_file_id.replace(".py", "").replace(".ts", "").replace(".js", "")
90
+ # e.g. src/auth -> auth
91
+ candidate_module_name = candidate_module_base.split("/")[-1]
92
+
93
+ if candidate_module_name in caller_imports:
94
+ valid_candidates.append(candidate_id)
95
+
96
+ if len(valid_candidates) == 1:
97
+ resolved_calls.append(
98
+ Relationship(
99
+ source_id=call.caller_id,
100
+ target_id=valid_candidates[0],
101
+ type=RelationshipType.CALLS,
102
+ )
103
+ )
104
+
105
+ return resolved_calls
@@ -0,0 +1,61 @@
1
+ """Tree-sitter grammar registry.
2
+
3
+ Loads and caches tree-sitter language grammars.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import tree_sitter
9
+ import tree_sitter_python
10
+ import tree_sitter_javascript
11
+ import tree_sitter_typescript
12
+
13
+ from structural_engine.parser.models import Language
14
+
15
+ _REGISTRY: dict[Language, tree_sitter.Language] = {}
16
+
17
+ def get_language(lang: Language) -> tree_sitter.Language:
18
+ """Get the tree-sitter Language object for a given supported language.
19
+
20
+ Parameters
21
+ ----------
22
+ lang : Language
23
+ The language to load.
24
+
25
+ Returns
26
+ -------
27
+ tree_sitter.Language
28
+ The compiled tree-sitter language.
29
+ """
30
+ if lang in _REGISTRY:
31
+ return _REGISTRY[lang]
32
+
33
+ ts_lang = None
34
+ if lang == Language.PYTHON:
35
+ ts_lang = tree_sitter.Language(tree_sitter_python.language())
36
+ elif lang == Language.JAVASCRIPT:
37
+ ts_lang = tree_sitter.Language(tree_sitter_javascript.language())
38
+ elif lang == Language.TYPESCRIPT:
39
+ ts_lang = tree_sitter.Language(tree_sitter_typescript.language_typescript())
40
+ else:
41
+ raise ValueError(f"Unsupported language: {lang}")
42
+
43
+ _REGISTRY[lang] = ts_lang
44
+ return ts_lang
45
+
46
+ def get_parser(lang: Language) -> tree_sitter.Parser:
47
+ """Create a new tree-sitter parser for the given language.
48
+
49
+ Parameters
50
+ ----------
51
+ lang : Language
52
+ The language to parse.
53
+
54
+ Returns
55
+ -------
56
+ tree_sitter.Parser
57
+ A configured parser instance.
58
+ """
59
+ ts_lang = get_language(lang)
60
+ parser = tree_sitter.Parser(ts_lang)
61
+ return parser
@@ -0,0 +1 @@
1
+ # Reports subsystem.
@@ -0,0 +1,77 @@
1
+ """Report generator.
2
+
3
+ Creates human-readable Markdown summaries of structural diffs.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from runtime.repository.models import RepositoryPaths
9
+ from structural_engine.diff.models import StructuralDiff
10
+
11
+
12
+ def generate(diff: StructuralDiff, revision_id: str, paths: RepositoryPaths) -> str:
13
+ """Generate and write a human-readable report of the diff.
14
+
15
+ Parameters
16
+ ----------
17
+ diff : StructuralDiff
18
+ The computed structural changes.
19
+ revision_id : str
20
+ The ID of the new structural revision (e.g. ``S-014``).
21
+ paths : RepositoryPaths
22
+ Canonical paths; writes to ``paths.reports_dir``.
23
+
24
+ Returns
25
+ -------
26
+ str
27
+ The filename of the created report (e.g. ``R-014.md``), or
28
+ ``none`` if no report was generated (e.g., no changes).
29
+ """
30
+ if not diff.has_changes:
31
+ return "none"
32
+
33
+ report_filename = f"R-{revision_id[2:]}.md"
34
+ report_path = paths.reports_dir / report_filename
35
+
36
+ lines = [
37
+ f"# Structural Sync Report: {revision_id}",
38
+ "",
39
+ "## Affected Components",
40
+ ]
41
+
42
+ for comp in sorted(diff.affected_components):
43
+ lines.append(f"- `{comp}/`")
44
+ lines.append("")
45
+
46
+ if diff.entities_added:
47
+ lines.append("## Entities Added")
48
+ for e in sorted(diff.entities_added, key=lambda x: x.id):
49
+ lines.append(f"- **{e.type.name}**: `{e.id}`")
50
+ lines.append("")
51
+
52
+ if diff.entities_removed:
53
+ lines.append("## Entities Removed")
54
+ for e in sorted(diff.entities_removed, key=lambda x: x.id):
55
+ lines.append(f"- **{e.type.name}**: `{e.id}`")
56
+ lines.append("")
57
+
58
+ if diff.entities_modified:
59
+ lines.append("## Entities Modified (Line Shifts / Signature Changes)")
60
+ for e in sorted(diff.entities_modified, key=lambda x: x.id):
61
+ lines.append(f"- **{e.type.name}**: `{e.id}` (Lines {e.start_line}-{e.end_line})")
62
+ lines.append("")
63
+
64
+ if diff.relationships_added:
65
+ lines.append("## Relationships Added")
66
+ for r in sorted(diff.relationships_added, key=lambda x: (x.source_id, x.target_id)):
67
+ lines.append(f"- `{r.source_id}` **{r.type.name}** `{r.target_id}`")
68
+ lines.append("")
69
+
70
+ if diff.relationships_removed:
71
+ lines.append("## Relationships Removed")
72
+ for r in sorted(diff.relationships_removed, key=lambda x: (x.source_id, x.target_id)):
73
+ lines.append(f"- `{r.source_id}` **{r.type.name}** `{r.target_id}`")
74
+ lines.append("")
75
+
76
+ report_path.write_text("\n".join(lines), encoding="utf-8")
77
+ return report_filename
@@ -0,0 +1,54 @@
1
+ """Structural Engine result contracts.
2
+
3
+ These models form the strict return boundary between the Engine and the Runtime.
4
+ The Runtime only imports these results and the ``StructuralEngine`` facade.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class InitializationResult:
15
+ """Result of a successful KnowCode initialization."""
16
+
17
+ success: bool
18
+ structural_revision: str
19
+ snapshot_file: str
20
+ message: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SyncResult:
25
+ """Result of a sync operation.
26
+
27
+ If ``changes_detected`` is False, the other fields reflect the existing
28
+ unchanged state.
29
+ """
30
+
31
+ success: bool
32
+ changes_detected: bool
33
+ structural_revision: str
34
+ snapshot_file: str
35
+ report_file: str
36
+ message: str
37
+ affected_components: frozenset[str] = field(default_factory=frozenset)
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class StructuralStatusResult:
42
+ """Current status of the structural state.
43
+
44
+ Provides a comprehensive view of the engine's internal state
45
+ plus unowned passthrough fields (like semantic_revision).
46
+ """
47
+
48
+ initialized: bool
49
+ structural_revision: str
50
+ semantic_revision: str
51
+ current_snapshot: str
52
+ latest_report: str
53
+ last_sync: datetime | None
54
+ repository_root: str
@@ -0,0 +1 @@
1
+ # Revisions subsystem.
@@ -0,0 +1,32 @@
1
+ """Revision tracking logic.
2
+
3
+ Calculates the next structural revision ID based on the current ID.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ def get_next_revision(current_id: str) -> str:
10
+ """Calculate the next sequential revision ID.
11
+
12
+ Parameters
13
+ ----------
14
+ current_id : str
15
+ The current structural revision ID (e.g. ``S-014``).
16
+
17
+ Returns
18
+ -------
19
+ str
20
+ The next sequential ID (e.g. ``S-015``).
21
+ If the current_id format is invalid, returns ``S-001``.
22
+ """
23
+ if not current_id.startswith("S-") or len(current_id) != 5:
24
+ # Fallback for corrupted state or unparseable initial state
25
+ return "S-001"
26
+
27
+ try:
28
+ current_num = int(current_id[2:])
29
+ next_num = current_num + 1
30
+ return f"S-{next_num:03d}"
31
+ except ValueError:
32
+ return "S-001"
@@ -0,0 +1 @@
1
+ # Snapshot subsystem.
@@ -0,0 +1,58 @@
1
+ """Snapshot generator and persister.
2
+
3
+ Serializes structural snapshots to JSON for deterministic persistence.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from dataclasses import asdict
10
+
11
+ from runtime.repository.models import RepositoryPaths
12
+ from structural_engine.parser.models import StructuralSnapshot
13
+
14
+
15
+ def persist(snapshot: StructuralSnapshot, revision_id: str, paths: RepositoryPaths) -> str:
16
+ """Serialize and write a StructuralSnapshot to disk.
17
+
18
+ Parameters
19
+ ----------
20
+ snapshot : StructuralSnapshot
21
+ The structural truth to persist.
22
+ revision_id : str
23
+ The ID to use for the snapshot file (e.g. ``S-014``).
24
+ paths : RepositoryPaths
25
+ Canonical paths; writes to ``paths.snapshots_dir``.
26
+
27
+ Returns
28
+ -------
29
+ str
30
+ The filename of the created snapshot (e.g. ``S-014.json``).
31
+ """
32
+ snapshot_filename = f"{revision_id}.json"
33
+ snapshot_path = paths.snapshots_dir / snapshot_filename
34
+
35
+ # We manually handle the enum serialization by transforming the output of asdict.
36
+ # A cleaner approach in a real app would be a custom JSONEncoder, but this is explicit.
37
+
38
+ entities = []
39
+ for entity in snapshot.entities:
40
+ d = asdict(entity)
41
+ d["type"] = d["type"].name # Enum to string
42
+ entities.append(d)
43
+
44
+ relationships = []
45
+ for rel in snapshot.relationships:
46
+ d = asdict(rel)
47
+ d["type"] = d["type"].name # Enum to string
48
+ relationships.append(d)
49
+
50
+ data = {
51
+ "entities": entities,
52
+ "relationships": relationships,
53
+ }
54
+
55
+ with open(snapshot_path, "w", encoding="utf-8") as f:
56
+ json.dump(data, f, indent=2)
57
+
58
+ return snapshot_filename
@@ -0,0 +1,59 @@
1
+ """Snapshot loader.
2
+
3
+ Deserializes JSON snapshots back into frozen model tuples.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from runtime.repository.models import RepositoryPaths
12
+ from structural_engine.parser.models import (
13
+ Entity,
14
+ EntityType,
15
+ Relationship,
16
+ RelationshipType,
17
+ StructuralSnapshot,
18
+ )
19
+
20
+
21
+ def load(snapshot_id: str, paths: RepositoryPaths) -> StructuralSnapshot | None:
22
+ """Load a StructuralSnapshot from disk.
23
+
24
+ Parameters
25
+ ----------
26
+ snapshot_id : str
27
+ The filename or ID of the snapshot (e.g. ``S-014`` or ``S-014.json``).
28
+ paths : RepositoryPaths
29
+ Canonical paths; reads from ``paths.snapshots_dir``.
30
+
31
+ Returns
32
+ -------
33
+ StructuralSnapshot | None
34
+ The hydrated snapshot tuple, or None if the file doesn't exist.
35
+ """
36
+ if not snapshot_id.endswith(".json"):
37
+ snapshot_id = f"{snapshot_id}.json"
38
+
39
+ snapshot_path = paths.snapshots_dir / snapshot_id
40
+ if not snapshot_path.is_file():
41
+ return None
42
+
43
+ with open(snapshot_path, "r", encoding="utf-8") as f:
44
+ data = json.load(f)
45
+
46
+ entities = []
47
+ for ed in data.get("entities", []):
48
+ ed["type"] = EntityType[ed["type"]]
49
+ entities.append(Entity(**ed))
50
+
51
+ relationships = []
52
+ for rd in data.get("relationships", []):
53
+ rd["type"] = RelationshipType[rd["type"]]
54
+ relationships.append(Relationship(**rd))
55
+
56
+ return StructuralSnapshot(
57
+ entities=tuple(entities),
58
+ relationships=tuple(relationships),
59
+ )
@@ -0,0 +1 @@
1
+ # State subsystem.
@@ -0,0 +1,169 @@
1
+ """State Manager.
2
+
3
+ Manages the persistent ``state.yaml`` file, maintaining strict stewardship
4
+ boundaries. Uses a round-trip YAML parser to preserve human comments
5
+ and unowned fields (like ``semantic_revision``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timezone
11
+
12
+ from ruamel.yaml import YAML
13
+ import structlog
14
+
15
+ from runtime.repository.models import RepositoryPaths
16
+ from structural_engine.state.models import StructuralState
17
+
18
+ logger = structlog.get_logger(__name__)
19
+
20
+
21
+ class StateManager:
22
+ """Manages read/write operations for state.yaml."""
23
+
24
+ def __init__(self) -> None:
25
+ """Initialize the state manager."""
26
+ self.yaml = YAML()
27
+ self.yaml.preserve_quotes = True
28
+ self.yaml.indent(mapping=2, sequence=4, offset=2)
29
+
30
+ def initialize(self, paths: RepositoryPaths) -> None:
31
+ """Create the initial state.yaml file.
32
+
33
+ Writes the default structural fields plus the baseline
34
+ ``semantic_revision``.
35
+
36
+ Parameters
37
+ ----------
38
+ paths : RepositoryPaths
39
+ The canonical path contract.
40
+ """
41
+ initial_state = {
42
+ "structural_revision": "S-001",
43
+ "current_snapshot": "S-001",
44
+ "latest_report": "none",
45
+ "last_sync": datetime.now(timezone.utc).isoformat(),
46
+ "semantic_revision": "none",
47
+ }
48
+
49
+ with open(paths.state_file, "w", encoding="utf-8") as f:
50
+ self.yaml.dump(initial_state, f)
51
+
52
+ logger.info(
53
+ "state_manager.initialized",
54
+ file=str(paths.state_file),
55
+ structural_revision="S-001",
56
+ )
57
+
58
+ def load(self, paths: RepositoryPaths) -> StructuralState:
59
+ """Load the structural state from disk.
60
+
61
+ Parameters
62
+ ----------
63
+ paths : RepositoryPaths
64
+ The canonical path contract.
65
+
66
+ Returns
67
+ -------
68
+ StructuralState
69
+ The strictly typed state subset owned by the Engine.
70
+ """
71
+ with open(paths.state_file, "r", encoding="utf-8") as f:
72
+ data = self.yaml.load(f)
73
+
74
+ state = StructuralState(
75
+ structural_revision=data["structural_revision"],
76
+ current_snapshot=data["current_snapshot"],
77
+ latest_report=data["latest_report"],
78
+ last_sync=datetime.fromisoformat(data["last_sync"]),
79
+ )
80
+
81
+ logger.debug(
82
+ "state_manager.loaded",
83
+ revision=state.structural_revision,
84
+ )
85
+ return state
86
+
87
+ def update(
88
+ self,
89
+ paths: RepositoryPaths,
90
+ snapshot_id: str,
91
+ revision_id: str,
92
+ report_id: str,
93
+ ) -> None:
94
+ """Update the structural state fields while preserving everything else.
95
+
96
+ Performs a safe read-modify-write cycle. The ``semantic_revision``
97
+ field and any human comments are explicitly preserved.
98
+
99
+ Parameters
100
+ ----------
101
+ paths : RepositoryPaths
102
+ The canonical path contract.
103
+ snapshot_id : str
104
+ The filename of the new snapshot.
105
+ revision_id : str
106
+ The new structural revision ID.
107
+ report_id : str
108
+ The filename of the new sync report.
109
+ """
110
+ # 1. Read existing
111
+ with open(paths.state_file, "r", encoding="utf-8") as f:
112
+ data = self.yaml.load(f)
113
+
114
+ # 2. Modify only our fields
115
+ data["structural_revision"] = revision_id
116
+ data["current_snapshot"] = snapshot_id
117
+ data["latest_report"] = report_id
118
+ data["last_sync"] = datetime.now(timezone.utc).isoformat()
119
+
120
+ # 3. Write back
121
+ with open(paths.state_file, "w", encoding="utf-8") as f:
122
+ self.yaml.dump(data, f)
123
+
124
+ logger.info(
125
+ "state_manager.updated",
126
+ file=str(paths.state_file),
127
+ revision=revision_id,
128
+ )
129
+
130
+ def increment_semantic_revision(self, paths: RepositoryPaths) -> str:
131
+ """Safely increment the semantic_revision field.
132
+
133
+ Parameters
134
+ ----------
135
+ paths : RepositoryPaths
136
+ The canonical path contract.
137
+
138
+ Returns
139
+ -------
140
+ str
141
+ The new semantic revision ID.
142
+ """
143
+ with open(paths.state_file, "r", encoding="utf-8") as f:
144
+ data = self.yaml.load(f)
145
+
146
+ current = data.get("semantic_revision", "none")
147
+ if current == "none":
148
+ new_rev = "M-001"
149
+ else:
150
+ try:
151
+ # e.g. "M-016" -> "M-017"
152
+ prefix, num_str = current.split("-")
153
+ new_num = int(num_str) + 1
154
+ new_rev = f"{prefix}-{new_num:03d}"
155
+ except ValueError:
156
+ # Fallback if corrupted
157
+ new_rev = "M-001"
158
+
159
+ data["semantic_revision"] = new_rev
160
+
161
+ with open(paths.state_file, "w", encoding="utf-8") as f:
162
+ self.yaml.dump(data, f)
163
+
164
+ logger.info(
165
+ "state_manager.semantic_bumped",
166
+ file=str(paths.state_file),
167
+ semantic_revision=new_rev,
168
+ )
169
+ return new_rev
@@ -0,0 +1,34 @@
1
+ """State domain models.
2
+
3
+ Defines the Structural Engine's partial view of ``state.yaml``.
4
+ The Engine only cares about the fields it manages.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+
14
+ class StructuralState(BaseModel):
15
+ """The Structural Engine's typed view of state.yaml.
16
+
17
+ This model represents the strict, 4-field stewardship boundary.
18
+ It does not contain ``semantic_revision``, which is owned by
19
+ humans/AI agents.
20
+ """
21
+
22
+ structural_revision: str
23
+ """The latest structural revision ID (e.g. ``S-015``)."""
24
+
25
+ current_snapshot: str
26
+ """The filename of the current snapshot (e.g. ``S-015`` or ``S-015.json``)."""
27
+
28
+ latest_report: str
29
+ """The filename of the latest sync report (e.g. ``R-015.md``) or ``none``."""
30
+
31
+ last_sync: datetime = Field(default_factory=datetime.utcnow)
32
+ """Timestamp of the last structural synchronization."""
33
+
34
+ model_config = ConfigDict(frozen=True)