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.
- knowcode-0.1.0.dist-info/METADATA +175 -0
- knowcode-0.1.0.dist-info/RECORD +63 -0
- knowcode-0.1.0.dist-info/WHEEL +4 -0
- knowcode-0.1.0.dist-info/entry_points.txt +2 -0
- runtime/__init__.py +4 -0
- runtime/artifact/__init__.py +1 -0
- runtime/artifact/builder.py +179 -0
- runtime/cli/__init__.py +1 -0
- runtime/cli/animation.py +278 -0
- runtime/cli/app.py +309 -0
- runtime/cli/auth.py +171 -0
- runtime/cli/telemetry.py +91 -0
- runtime/exceptions/__init__.py +1 -0
- runtime/exceptions/errors.py +99 -0
- runtime/repository/__init__.py +13 -0
- runtime/repository/discovery.py +64 -0
- runtime/repository/models.py +103 -0
- runtime/repository/paths.py +50 -0
- runtime/repository/validator.py +100 -0
- runtime/services/__init__.py +1 -0
- runtime/services/ingest_service.py +105 -0
- runtime/services/init_service.py +45 -0
- runtime/services/semantic_sync_service.py +55 -0
- runtime/services/status_service.py +40 -0
- runtime/services/sync_service.py +57 -0
- runtime/templates/KNOWCODE_LOADER.md.j2 +24 -0
- runtime/templates/README_KNOWLEDGE.md.j2 +12 -0
- runtime/templates/README_STRUCTURE.md.j2 +19 -0
- runtime/templates/__init__.py +1 -0
- runtime/templates/active_context.md.j2 +3 -0
- runtime/templates/ingest_legacy.md.j2 +15 -0
- runtime/templates/raw_readme.md.j2 +9 -0
- runtime/templates/sync_reconciliation.md.j2 +17 -0
- runtime/templates/synthesize_knowledge.md.j2 +32 -0
- runtime/templates/track_intent.md.j2 +14 -0
- structural_engine/__init__.py +3 -0
- structural_engine/diff/__init__.py +1 -0
- structural_engine/diff/generator.py +92 -0
- structural_engine/diff/models.py +48 -0
- structural_engine/engine.py +192 -0
- structural_engine/logs/__init__.py +1 -0
- structural_engine/logs/generator.py +33 -0
- structural_engine/parser/__init__.py +7 -0
- structural_engine/parser/discovery.py +165 -0
- structural_engine/parser/extractors/base.py +44 -0
- structural_engine/parser/languages/javascript/adapter.py +149 -0
- structural_engine/parser/languages/python/adapter.py +174 -0
- structural_engine/parser/languages/typescript/adapter.py +165 -0
- structural_engine/parser/models.py +186 -0
- structural_engine/parser/parser.py +160 -0
- structural_engine/parser/resolvers/calls.py +105 -0
- structural_engine/parser/tree_sitter/registry.py +61 -0
- structural_engine/reports/__init__.py +1 -0
- structural_engine/reports/generator.py +77 -0
- structural_engine/results.py +54 -0
- structural_engine/revisions/__init__.py +1 -0
- structural_engine/revisions/tracker.py +32 -0
- structural_engine/snapshot/__init__.py +1 -0
- structural_engine/snapshot/generator.py +58 -0
- structural_engine/snapshot/loader.py +59 -0
- structural_engine/state/__init__.py +1 -0
- structural_engine/state/manager.py +169 -0
- 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)
|