framesdkpy 0.3.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.
- framesdkpy/__init__.py +33 -0
- framesdkpy/loaders/__init__.py +79 -0
- framesdkpy/loaders/assembler.py +217 -0
- framesdkpy/loaders/yaml_reader.py +57 -0
- framesdkpy/models/__init__.py +95 -0
- framesdkpy/models/acts_model.py +139 -0
- framesdkpy/models/base.py +64 -0
- framesdkpy/models/expect_model.py +127 -0
- framesdkpy/models/facts_model.py +163 -0
- framesdkpy/models/frame_model.py +42 -0
- framesdkpy/models/map_model.py +157 -0
- framesdkpy/models/rules_model.py +181 -0
- framesdkpy/schemas/acts.schema.json +116 -0
- framesdkpy/schemas/expect.schema.json +105 -0
- framesdkpy/schemas/facts.schema.json +256 -0
- framesdkpy/schemas/frame.schema.json +114 -0
- framesdkpy/schemas/map.schema.json +119 -0
- framesdkpy/schemas/rules.schema.json +140 -0
- framesdkpy/translators/__init__.py +24 -0
- framesdkpy/translators/normalizer.py +106 -0
- framesdkpy/translators/yaml_to_json.py +88 -0
- framesdkpy/validators/__init__.py +74 -0
- framesdkpy/validators/cross_file_validator.py +85 -0
- framesdkpy/validators/limits_validator.py +169 -0
- framesdkpy/validators/result.py +100 -0
- framesdkpy/validators/schema_validator.py +152 -0
- framesdkpy-0.3.0.dist-info/METADATA +89 -0
- framesdkpy-0.3.0.dist-info/RECORD +29 -0
- framesdkpy-0.3.0.dist-info/WHEEL +4 -0
framesdkpy/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""FRAME Python SDK v0.3.0.
|
|
2
|
+
|
|
3
|
+
The stable Python interface for reading FRAME project context files.
|
|
4
|
+
Tools should import from this package root when they only need the public API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from framesdkpy.loaders import FrameLoadError, load_frame
|
|
8
|
+
from framesdkpy.models import FRAME, FrameActs, FrameExpect, FrameFacts, FrameMap, FrameRules
|
|
9
|
+
from framesdkpy.translators import (
|
|
10
|
+
translate_directory,
|
|
11
|
+
translate_file,
|
|
12
|
+
translate_to_dict,
|
|
13
|
+
translate_to_json_string,
|
|
14
|
+
)
|
|
15
|
+
from framesdkpy.validators import ValidationResult, validate_file, validate_frame
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"FRAME",
|
|
19
|
+
"FrameFacts",
|
|
20
|
+
"FrameRules",
|
|
21
|
+
"FrameMap",
|
|
22
|
+
"FrameExpect",
|
|
23
|
+
"FrameActs",
|
|
24
|
+
"load_frame",
|
|
25
|
+
"FrameLoadError",
|
|
26
|
+
"validate_frame",
|
|
27
|
+
"validate_file",
|
|
28
|
+
"ValidationResult",
|
|
29
|
+
"translate_file",
|
|
30
|
+
"translate_directory",
|
|
31
|
+
"translate_to_dict",
|
|
32
|
+
"translate_to_json_string",
|
|
33
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""FRAME loader -- reads YAML from a directory and returns a typed FRAME object.
|
|
2
|
+
|
|
3
|
+
The full pipeline: discover → parse → normalize → validate → assemble → return.
|
|
4
|
+
|
|
5
|
+
Public API:
|
|
6
|
+
load_frame(".haxaml/") → FRAME
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from framesdkpy.loaders.yaml_reader import discover_frame_dir, read_raw_yaml
|
|
14
|
+
from framesdkpy.loaders.assembler import assemble_frame, FrameLoadError
|
|
15
|
+
from framesdkpy.translators.normalizer import normalize_dict
|
|
16
|
+
from framesdkpy.validators import validate_against_schema, validate_limits
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_frame(dir_path: str | Path) -> "FRAME":
|
|
20
|
+
"""Load all 5 FRAME YAML files from a directory and return a typed FRAME object.
|
|
21
|
+
|
|
22
|
+
Pipeline:
|
|
23
|
+
1. Discover 5 files in the directory (strict -- all must be present)
|
|
24
|
+
2. Parse raw YAML into dicts
|
|
25
|
+
3. Normalize each dict (YAML quirks → clean types)
|
|
26
|
+
4. Validate each dict against JSON Schema + character limits
|
|
27
|
+
5. Assemble into a typed FRAME object (cross-file checks, model construction)
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
FileNotFoundError: Directory doesn't exist or a file is missing.
|
|
31
|
+
FrameLoadError: Validation failed (schema, limits, or cross-file).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
FRAME with facts populated and optional other parts.
|
|
35
|
+
"""
|
|
36
|
+
from framesdkpy.models.frame_model import FRAME
|
|
37
|
+
|
|
38
|
+
# 1. Discover
|
|
39
|
+
files = discover_frame_dir(dir_path)
|
|
40
|
+
|
|
41
|
+
# 2. Parse raw YAML
|
|
42
|
+
raw = read_raw_yaml(files)
|
|
43
|
+
|
|
44
|
+
# 3-4. Normalize + validate each file
|
|
45
|
+
validated: dict[str, dict] = {}
|
|
46
|
+
all_errors: list = []
|
|
47
|
+
all_warnings: list = []
|
|
48
|
+
|
|
49
|
+
for stem, raw_dict in raw.items():
|
|
50
|
+
clean = normalize_dict(raw_dict)
|
|
51
|
+
|
|
52
|
+
# Schema validation
|
|
53
|
+
schema_result = validate_against_schema(clean, stem)
|
|
54
|
+
if not schema_result.is_valid():
|
|
55
|
+
all_errors.extend(schema_result.errors)
|
|
56
|
+
all_warnings.extend(schema_result.warnings)
|
|
57
|
+
|
|
58
|
+
# Character limit validation
|
|
59
|
+
limits_result = validate_limits(clean, stem)
|
|
60
|
+
if not limits_result.is_valid():
|
|
61
|
+
all_errors.extend(limits_result.errors)
|
|
62
|
+
all_warnings.extend(limits_result.warnings)
|
|
63
|
+
|
|
64
|
+
validated[stem] = clean
|
|
65
|
+
|
|
66
|
+
# Block on any validation errors
|
|
67
|
+
if all_errors:
|
|
68
|
+
raise FrameLoadError(
|
|
69
|
+
f"Validation failed with {len(all_errors)} error(s): "
|
|
70
|
+
+ "; ".join(e.message for e in all_errors[:5]),
|
|
71
|
+
errors=all_errors,
|
|
72
|
+
warnings=all_warnings,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# 5. Assemble (includes cross-file check)
|
|
76
|
+
return assemble_frame(validated)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["load_frame", "FrameLoadError"]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Assembler -- builds a typed FRAME object from 5 validated and normalized dicts.
|
|
2
|
+
|
|
3
|
+
Runs cross-file consistency checks, then constructs the five typed models
|
|
4
|
+
and composes them into one FRAME object.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from framesdkpy.models.frame_model import FRAME
|
|
10
|
+
from framesdkpy.models.facts_model import FrameFacts, Profile, Architecture, Technology, Source, Quirk, OpenQuestion
|
|
11
|
+
from framesdkpy.models.rules_model import FrameRules, Policy, CoreRule, Command, Dont, AskFirst, Hint
|
|
12
|
+
from framesdkpy.models.map_model import FrameMap, Group, PathEntry, Entrypoint, ManagedPath, UnmappedPath
|
|
13
|
+
from framesdkpy.models.expect_model import FrameExpect, MustHold, Check, Proof
|
|
14
|
+
from framesdkpy.models.acts_model import FrameActs, Run, RunCheck, Blocker
|
|
15
|
+
|
|
16
|
+
from framesdkpy.validators.cross_file_validator import validate_cross_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FrameLoadError(ValueError):
|
|
20
|
+
"""Raised when a validated FRAME directory cannot be assembled.
|
|
21
|
+
|
|
22
|
+
Contains the validation result with all errors that prevented assembly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, errors: list, warnings: list):
|
|
26
|
+
self.errors = errors
|
|
27
|
+
self.warnings = warnings
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Sub-model constructors -- each converts a normalized dict to a typed model
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
from dataclasses import fields as dc_fields
|
|
37
|
+
|
|
38
|
+
def _strip_extra(data: dict, model_class) -> dict:
|
|
39
|
+
"""Keep only keys that match the model's field names."""
|
|
40
|
+
field_names = {f.name for f in dc_fields(model_class)}
|
|
41
|
+
return {k: v for k, v in data.items() if k in field_names}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_facts(data: dict) -> FrameFacts:
|
|
45
|
+
prof = data.get("profile", {})
|
|
46
|
+
arch = data.get("architecture", {})
|
|
47
|
+
tech = data.get("technology")
|
|
48
|
+
|
|
49
|
+
return FrameFacts(
|
|
50
|
+
frame=data["frame"],
|
|
51
|
+
profile=Profile(
|
|
52
|
+
name=prof["name"],
|
|
53
|
+
summary=prof["summary"],
|
|
54
|
+
repo_shape=prof.get("repo_shape"),
|
|
55
|
+
delivery_family=prof.get("delivery_family"),
|
|
56
|
+
),
|
|
57
|
+
architecture=Architecture(
|
|
58
|
+
summary=arch["summary"],
|
|
59
|
+
backend_layers=arch.get("backend_layers"),
|
|
60
|
+
frontend_layers=arch.get("frontend_layers"),
|
|
61
|
+
data_flow=arch.get("data_flow"),
|
|
62
|
+
deployment_topology=arch.get("deployment_topology"),
|
|
63
|
+
),
|
|
64
|
+
technology=Technology(
|
|
65
|
+
language=tech.get("language") if tech else None,
|
|
66
|
+
framework=tech.get("framework") if tech else None,
|
|
67
|
+
database=tech.get("database") if tech else None,
|
|
68
|
+
extensions=tech.get("extensions") if tech else None,
|
|
69
|
+
) if tech else None,
|
|
70
|
+
sources=[Source(**_strip_extra(s, Source)) for s in data.get("sources", [])],
|
|
71
|
+
quirks=[Quirk(**_strip_extra(q, Quirk)) for q in data.get("quirks", [])],
|
|
72
|
+
open_questions=[OpenQuestion(**_strip_extra(o, OpenQuestion)) for o in data.get("open_questions", [])],
|
|
73
|
+
classification=data.get("classification"),
|
|
74
|
+
environments=data.get("environments"),
|
|
75
|
+
persistence=data.get("persistence"),
|
|
76
|
+
evidence=data.get("evidence", []),
|
|
77
|
+
links=data.get("links", []),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_rules(data: dict) -> FrameRules:
|
|
82
|
+
commands_dict: dict[str, Command] = {}
|
|
83
|
+
|
|
84
|
+
for name, cmd_data in data.get("commands", {}).items():
|
|
85
|
+
commands_dict[name] = Command(**_strip_extra(cmd_data, Command))
|
|
86
|
+
|
|
87
|
+
return FrameRules(
|
|
88
|
+
frame=data["frame"],
|
|
89
|
+
governance_level=data.get("governance_level", "normal"),
|
|
90
|
+
rules=[CoreRule(**_strip_extra(r, CoreRule)) for r in data.get("rules", [])],
|
|
91
|
+
policies=[Policy(**_strip_extra(p, Policy)) for p in data.get("policies", [])],
|
|
92
|
+
commands=commands_dict,
|
|
93
|
+
donts=[Dont(**_strip_extra(d, Dont)) for d in data.get("donts", [])],
|
|
94
|
+
ask_first=[AskFirst(**_strip_extra(a, AskFirst)) for a in data.get("ask_first", [])],
|
|
95
|
+
hints=[Hint(**_strip_extra(h, Hint)) for h in data.get("hints", [])],
|
|
96
|
+
code_style=data.get("code_style"),
|
|
97
|
+
git=data.get("git"),
|
|
98
|
+
evidence=data.get("evidence", []),
|
|
99
|
+
links=data.get("links", []),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _build_map(data: dict) -> FrameMap:
|
|
104
|
+
"""Build FrameMap from a normalized map dict."""
|
|
105
|
+
return FrameMap(
|
|
106
|
+
frame=data["frame"],
|
|
107
|
+
structure=data.get("structure"),
|
|
108
|
+
roots=data.get("roots"),
|
|
109
|
+
groups=[Group(**_strip_extra(g, Group)) for g in data.get("groups", [])],
|
|
110
|
+
paths=[PathEntry(**_strip_extra(p, PathEntry)) for p in data.get("paths", [])],
|
|
111
|
+
entrypoints=[Entrypoint(**_strip_extra(e, Entrypoint)) for e in data.get("entrypoints", [])],
|
|
112
|
+
managed_paths=[ManagedPath(**_strip_extra(m, ManagedPath)) for m in data.get("managed_paths", [])],
|
|
113
|
+
unmapped_paths=[UnmappedPath(**_strip_extra(u, UnmappedPath)) for u in data.get("unmapped_paths", [])],
|
|
114
|
+
evidence=data.get("evidence", []),
|
|
115
|
+
links=data.get("links", []),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_expect(data: dict) -> FrameExpect:
|
|
120
|
+
"""Build FrameExpect from a normalized expect dict."""
|
|
121
|
+
checks_dict: dict[str, Check] = {}
|
|
122
|
+
for name, chk_data in data.get("checks", {}).items():
|
|
123
|
+
checks_dict[name] = Check(**_strip_extra(chk_data, Check))
|
|
124
|
+
|
|
125
|
+
return FrameExpect(
|
|
126
|
+
frame=data["frame"],
|
|
127
|
+
outcomes=data.get("outcomes"),
|
|
128
|
+
must_hold=[MustHold(**_strip_extra(m, MustHold)) for m in data.get("must_hold", [])],
|
|
129
|
+
checks=checks_dict,
|
|
130
|
+
done_when=data.get("done_when"),
|
|
131
|
+
proof=[Proof(**_strip_extra(p, Proof)) for p in data.get("proof", [])],
|
|
132
|
+
handoff=data.get("handoff"),
|
|
133
|
+
evidence=data.get("evidence", []),
|
|
134
|
+
links=data.get("links", []),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _build_acts(data: dict) -> FrameActs:
|
|
139
|
+
"""Build FrameActs from a normalized acts dict."""
|
|
140
|
+
runs: list[Run] = []
|
|
141
|
+
for run_data in data.get("runs", []):
|
|
142
|
+
# Build nested RunCheck objects
|
|
143
|
+
checks_list: list[RunCheck] = []
|
|
144
|
+
for rc_data in run_data.get("checks", []) or []:
|
|
145
|
+
checks_list.append(RunCheck(**_strip_extra(rc_data, RunCheck)))
|
|
146
|
+
|
|
147
|
+
run_data_copy = {k: v for k, v in run_data.items() if k != "checks"}
|
|
148
|
+
runs.append(Run(checks=checks_list if checks_list else None, **_strip_extra(run_data_copy, Run)))
|
|
149
|
+
|
|
150
|
+
return FrameActs(
|
|
151
|
+
frame=data["frame"],
|
|
152
|
+
summary=data.get("summary"),
|
|
153
|
+
runs=runs,
|
|
154
|
+
blockers=[Blocker(**_strip_extra(b, Blocker)) for b in data.get("blockers", [])],
|
|
155
|
+
handoff=data.get("handoff"),
|
|
156
|
+
evidence=data.get("evidence", []),
|
|
157
|
+
links=data.get("links", []),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Main assembly function
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_BUILDERS = {
|
|
167
|
+
"facts": _build_facts,
|
|
168
|
+
"rules": _build_rules,
|
|
169
|
+
"map": _build_map,
|
|
170
|
+
"expect": _build_expect,
|
|
171
|
+
"acts": _build_acts,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def assemble_frame(parts: dict[str, dict]) -> FRAME:
|
|
176
|
+
"""Build a typed FRAME object from 5 validated and normalized dicts.
|
|
177
|
+
|
|
178
|
+
Runs cross-file consistency check first. Raises FrameLoadError if
|
|
179
|
+
schema versions don't match or file/role fields are inconsistent.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
parts: Dict mapping file stem to normalized dict.
|
|
183
|
+
e.g., {"facts": {...}, "rules": {...}, ...}
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
FRAME object with all five typed parts.
|
|
187
|
+
"""
|
|
188
|
+
required_parts = {"facts", "rules", "map", "expect", "acts"}
|
|
189
|
+
missing_parts = sorted(required_parts - set(parts))
|
|
190
|
+
if missing_parts:
|
|
191
|
+
raise FrameLoadError(
|
|
192
|
+
f"Missing FRAME part(s): {', '.join(missing_parts)}",
|
|
193
|
+
errors=[],
|
|
194
|
+
warnings=[],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Cross-file consistency must pass before assembly
|
|
198
|
+
cross_result = validate_cross_file(parts)
|
|
199
|
+
if not cross_result.is_valid():
|
|
200
|
+
raise FrameLoadError(
|
|
201
|
+
f"Cross-file validation failed: {cross_result.summary()}",
|
|
202
|
+
errors=cross_result.errors,
|
|
203
|
+
warnings=cross_result.warnings,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
built = {}
|
|
207
|
+
for stem in ["facts", "rules", "map", "expect", "acts"]:
|
|
208
|
+
builder = _BUILDERS[stem]
|
|
209
|
+
built[stem] = builder(parts[stem])
|
|
210
|
+
|
|
211
|
+
return FRAME(
|
|
212
|
+
facts=built["facts"],
|
|
213
|
+
rules=built["rules"],
|
|
214
|
+
map=built["map"],
|
|
215
|
+
expect=built["expect"],
|
|
216
|
+
acts=built["acts"],
|
|
217
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""YAML reader -- discovers and parses FRAME YAML files from a directory.
|
|
2
|
+
|
|
3
|
+
Strict single-directory discovery: exactly 5 files must be present.
|
|
4
|
+
No fuzzy matching, no parent/sibling search, no scattered file pickup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# All 5 are discovered. Facts and Rules are required for any project.
|
|
15
|
+
# Map is required if the repo has structure; empty repos can omit it.
|
|
16
|
+
# Expect and Acts grow over time.
|
|
17
|
+
_EXPECTED_FILES = ["facts", "rules", "map", "expect", "acts"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def discover_frame_dir(dir_path: str | Path) -> dict[str, Path]:
|
|
21
|
+
"""Verify exactly 5 FRAME YAML files exist in a directory.
|
|
22
|
+
|
|
23
|
+
Returns a dict mapping file stem to resolved Path. Raises FileNotFoundError
|
|
24
|
+
if the directory doesn't exist or any expected file is missing.
|
|
25
|
+
"""
|
|
26
|
+
directory = Path(dir_path).resolve()
|
|
27
|
+
if not directory.is_dir():
|
|
28
|
+
raise FileNotFoundError(f"FRAME directory not found: {directory}")
|
|
29
|
+
|
|
30
|
+
found: dict[str, Path] = {}
|
|
31
|
+
for stem in _EXPECTED_FILES:
|
|
32
|
+
yaml_path = directory / f"{stem}.yaml"
|
|
33
|
+
if not yaml_path.is_file():
|
|
34
|
+
raise FileNotFoundError(
|
|
35
|
+
f"Missing FRAME file: {stem}.yaml in {directory}. "
|
|
36
|
+
f"All 5 files (facts, rules, map, expect, acts) must be in the same directory."
|
|
37
|
+
)
|
|
38
|
+
found[stem] = yaml_path
|
|
39
|
+
|
|
40
|
+
return found
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_raw_yaml(file_stems: dict[str, Path]) -> dict[str, dict]:
|
|
44
|
+
"""Parse YAML files into raw Python dicts.
|
|
45
|
+
|
|
46
|
+
Returns a dict mapping file stem to raw parsed dict. YAML quirks (yes→True,
|
|
47
|
+
~→None) are handled by the normalizer downstream -- this is raw parsing only.
|
|
48
|
+
"""
|
|
49
|
+
raw: dict[str, dict] = {}
|
|
50
|
+
for stem, path in file_stems.items():
|
|
51
|
+
data = yaml.safe_load(path.read_text())
|
|
52
|
+
if data is None:
|
|
53
|
+
data = {}
|
|
54
|
+
if not isinstance(data, dict):
|
|
55
|
+
raise ValueError(f"Expected YAML object in {stem}.yaml, got {type(data).__name__}")
|
|
56
|
+
raw[stem] = data
|
|
57
|
+
return raw
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""FRAME models -- typed data carriers for all five FRAME parts.
|
|
2
|
+
|
|
3
|
+
Primary import for downstream tools:
|
|
4
|
+
from framesdkpy.models import FRAME, FrameFacts, FrameRules, FrameMap, FrameExpect, FrameActs
|
|
5
|
+
|
|
6
|
+
Sub-models for deeper access:
|
|
7
|
+
from framesdkpy.models import Profile, Command, Check, Run, Blocker, etc.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from framesdkpy.models.frame_model import FRAME
|
|
11
|
+
|
|
12
|
+
from framesdkpy.models.facts_model import (
|
|
13
|
+
FrameFacts,
|
|
14
|
+
Profile,
|
|
15
|
+
Architecture,
|
|
16
|
+
Technology,
|
|
17
|
+
Source,
|
|
18
|
+
Quirk,
|
|
19
|
+
OpenQuestion,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from framesdkpy.models.rules_model import (
|
|
23
|
+
FrameRules,
|
|
24
|
+
Policy,
|
|
25
|
+
CoreRule,
|
|
26
|
+
Command,
|
|
27
|
+
Dont,
|
|
28
|
+
AskFirst,
|
|
29
|
+
Hint,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from framesdkpy.models.map_model import (
|
|
33
|
+
FrameMap,
|
|
34
|
+
Group,
|
|
35
|
+
PathEntry,
|
|
36
|
+
Entrypoint,
|
|
37
|
+
ManagedPath,
|
|
38
|
+
UnmappedPath,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from framesdkpy.models.expect_model import (
|
|
42
|
+
FrameExpect,
|
|
43
|
+
MustHold,
|
|
44
|
+
Check,
|
|
45
|
+
Proof,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from framesdkpy.models.acts_model import (
|
|
49
|
+
FrameActs,
|
|
50
|
+
Run,
|
|
51
|
+
RunCheck,
|
|
52
|
+
Blocker,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
from framesdkpy.models.base import FrameBaseModel
|
|
56
|
+
|
|
57
|
+
# Everything a downstream tool needs, one import away
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Top-level
|
|
60
|
+
"FRAME",
|
|
61
|
+
"FrameBaseModel",
|
|
62
|
+
# Facts
|
|
63
|
+
"FrameFacts",
|
|
64
|
+
"Profile",
|
|
65
|
+
"Architecture",
|
|
66
|
+
"Technology",
|
|
67
|
+
"Source",
|
|
68
|
+
"Quirk",
|
|
69
|
+
"OpenQuestion",
|
|
70
|
+
# Rules
|
|
71
|
+
"FrameRules",
|
|
72
|
+
"Policy",
|
|
73
|
+
"CoreRule",
|
|
74
|
+
"Command",
|
|
75
|
+
"Dont",
|
|
76
|
+
"AskFirst",
|
|
77
|
+
"Hint",
|
|
78
|
+
# Map
|
|
79
|
+
"FrameMap",
|
|
80
|
+
"Group",
|
|
81
|
+
"PathEntry",
|
|
82
|
+
"Entrypoint",
|
|
83
|
+
"ManagedPath",
|
|
84
|
+
"UnmappedPath",
|
|
85
|
+
# Expect
|
|
86
|
+
"FrameExpect",
|
|
87
|
+
"MustHold",
|
|
88
|
+
"Check",
|
|
89
|
+
"Proof",
|
|
90
|
+
# Acts
|
|
91
|
+
"FrameActs",
|
|
92
|
+
"Run",
|
|
93
|
+
"RunCheck",
|
|
94
|
+
"Blocker",
|
|
95
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""FrameActs model and sub-models -- checked activity record.
|
|
2
|
+
|
|
3
|
+
Mirrors schemas/json/acts.schema.json exactly.
|
|
4
|
+
Acts is run history -- what went in, what came out, what changed, what checks ran.
|
|
5
|
+
Size cap: 50KB. Exceeded → oldest runs auto-rotated to acts_archive/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from framesdkpy.models.base import FrameBaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Sub-models
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class RunCheck(FrameBaseModel):
|
|
22
|
+
"""A check considered or executed during a single run.
|
|
23
|
+
|
|
24
|
+
Collapsed from the old checks_seen/checks_ran split into one field with status.
|
|
25
|
+
status: ran (check was executed) or skipped (considered but not applicable).
|
|
26
|
+
result: only present when status=ran. pass or fail.
|
|
27
|
+
reason: only present when status=skipped. Why it was skipped.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
"""Ref to expect.checks.<name>. This is the check that was considered or executed."""
|
|
32
|
+
|
|
33
|
+
status: str
|
|
34
|
+
"""Enum: ran, skipped. ran = executed, skipped = considered but not applicable."""
|
|
35
|
+
|
|
36
|
+
result: str | None = None
|
|
37
|
+
"""Enum: pass, fail. Only set when status=ran."""
|
|
38
|
+
|
|
39
|
+
reason: str | None = None
|
|
40
|
+
"""Why skipped. Only set when status=skipped. maxLength: 200."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class Run(FrameBaseModel):
|
|
45
|
+
"""A single agent session run. Has an id for cross-referencing.
|
|
46
|
+
|
|
47
|
+
work_kind: tags for what kind of work was done (code, test, review, docs, deploy).
|
|
48
|
+
status: the overall run outcome. Pass means all checks passed.
|
|
49
|
+
Pass_with_risks means passed but with warnings. Fail means something broke.
|
|
50
|
+
Needs_clarification means the agent couldn't determine the outcome.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
id: str
|
|
54
|
+
"""Stable identifier. Used for cross-referencing from other Acts entries. maxLength: 100."""
|
|
55
|
+
|
|
56
|
+
actor: str
|
|
57
|
+
"""Who or what did the work (agent name, model, or human). maxLength: 100."""
|
|
58
|
+
|
|
59
|
+
goal: str
|
|
60
|
+
"""What the run was trying to accomplish. maxLength: 300."""
|
|
61
|
+
|
|
62
|
+
status: str
|
|
63
|
+
"""Enum: pass, pass_with_risks, fail, needs_clarification."""
|
|
64
|
+
|
|
65
|
+
work_kind: list[str] | None = None
|
|
66
|
+
"""Tags: code, test, review, docs, deploy."""
|
|
67
|
+
|
|
68
|
+
keywords: list[str] | None = None
|
|
69
|
+
"""Topic tags for searchable retrieval."""
|
|
70
|
+
|
|
71
|
+
input_summary: str | None = None
|
|
72
|
+
"""What went into the run. maxLength: 300."""
|
|
73
|
+
|
|
74
|
+
output_summary: str | None = None
|
|
75
|
+
"""What came out. maxLength: 300."""
|
|
76
|
+
|
|
77
|
+
touched: list[str] | None = None
|
|
78
|
+
"""Files, paths, or surfaces touched during the run."""
|
|
79
|
+
|
|
80
|
+
changed_facts: list[str] | None = None
|
|
81
|
+
"""Facts that were modified during the run."""
|
|
82
|
+
|
|
83
|
+
rules_followed: list[str] | None = None
|
|
84
|
+
"""Rule or policy ids that were applied."""
|
|
85
|
+
|
|
86
|
+
checks: list[RunCheck] | None = None
|
|
87
|
+
"""Checks considered or executed this run. Each has status and optional result/reason."""
|
|
88
|
+
|
|
89
|
+
links: list[dict] = field(default_factory=list)
|
|
90
|
+
"""Typed links from this entry to other FRAME refs."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True)
|
|
94
|
+
class Blocker(FrameBaseModel):
|
|
95
|
+
"""Something preventing progress. Has an id for cross-referencing."""
|
|
96
|
+
|
|
97
|
+
id: str
|
|
98
|
+
"""Stable identifier. maxLength: 100."""
|
|
99
|
+
|
|
100
|
+
description: str
|
|
101
|
+
"""What is blocking progress. maxLength: 300."""
|
|
102
|
+
|
|
103
|
+
links: list[dict] = field(default_factory=list)
|
|
104
|
+
"""Typed links from this entry to other FRAME refs."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Main model
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(slots=True)
|
|
113
|
+
class FrameActs(FrameBaseModel):
|
|
114
|
+
"""Run history -- what happened across agent sessions.
|
|
115
|
+
|
|
116
|
+
Populated from acts.yaml. Grows over time as agents work on the project.
|
|
117
|
+
Older runs auto-rotate to acts_archive/ when the file exceeds 50KB.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
frame: dict = field(default_factory=dict)
|
|
121
|
+
"""Shared FRAME header block. Required by every FRAME file."""
|
|
122
|
+
|
|
123
|
+
summary: str | None = None
|
|
124
|
+
"""Quick overview of recent activity. maxLength: 500."""
|
|
125
|
+
|
|
126
|
+
runs: list[Run] = field(default_factory=list)
|
|
127
|
+
"""Per-session run records. Each has a stable id for cross-referencing."""
|
|
128
|
+
|
|
129
|
+
blockers: list[Blocker] = field(default_factory=list)
|
|
130
|
+
"""Things preventing progress. Each has a stable id."""
|
|
131
|
+
|
|
132
|
+
handoff: dict | None = None
|
|
133
|
+
"""What the next agent needs to know. Free-form."""
|
|
134
|
+
|
|
135
|
+
evidence: list[dict] = field(default_factory=list)
|
|
136
|
+
"""Evidence entries supporting Acts claims."""
|
|
137
|
+
|
|
138
|
+
links: list[dict] = field(default_factory=list)
|
|
139
|
+
"""Typed links from this file to other FRAME refs."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared base class for all FRAME models.
|
|
2
|
+
|
|
3
|
+
Provides serialization (to_dict, to_json) and developer-friendly repr.
|
|
4
|
+
Every FRAME model inherits from FrameBaseModel. This ensures uniform
|
|
5
|
+
output format across all five parts regardless of which tool consumes them.
|
|
6
|
+
|
|
7
|
+
Null preservation: to_dict() keeps nulls in the output so the JSON shape
|
|
8
|
+
is always complete. Cross-language tools (frame-js, frame-cpp) can rely
|
|
9
|
+
on every key existing even if its value is null.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class FrameBaseModel:
|
|
21
|
+
"""All FRAME models inherit this for uniform serialization.
|
|
22
|
+
|
|
23
|
+
Serialization preserves nulls -- if a field is None, the key appears in
|
|
24
|
+
the dict with a null value. This keeps the JSON shape consistent across
|
|
25
|
+
tools and languages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict[str, Any]:
|
|
29
|
+
"""Recursive serialization to a JSON-compatible dict. Preserves nulls.
|
|
30
|
+
|
|
31
|
+
Nested FrameBaseModel instances and lists of models are handled
|
|
32
|
+
recursively. Regular dicts, lists, strings, and primitives pass through.
|
|
33
|
+
"""
|
|
34
|
+
result: dict[str, Any] = {}
|
|
35
|
+
for field in fields(self):
|
|
36
|
+
value = getattr(self, field.name)
|
|
37
|
+
result[field.name] = self._serialize_value(value)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
def to_json(self, indent: int = 2) -> str:
|
|
41
|
+
"""Serialize to a JSON string. Passes through to_dict() first."""
|
|
42
|
+
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _serialize_value(value: Any) -> Any:
|
|
46
|
+
"""Recursively convert a value to a JSON-safe representation.
|
|
47
|
+
|
|
48
|
+
- FrameBaseModel → call to_dict()
|
|
49
|
+
- list of models → list of dicts
|
|
50
|
+
- dict → recurse into values
|
|
51
|
+
- None, str, int, float, bool → pass through
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, FrameBaseModel):
|
|
54
|
+
return value.to_dict()
|
|
55
|
+
if isinstance(value, list):
|
|
56
|
+
return [FrameBaseModel._serialize_value(item) for item in value]
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
return {k: FrameBaseModel._serialize_value(v) for k, v in value.items()}
|
|
59
|
+
return value # Primitive: str, int, float, bool, None
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
"""Developer-friendly display showing class name and field count."""
|
|
63
|
+
field_names = [f.name for f in fields(self)]
|
|
64
|
+
return f"{type(self).__name__}({', '.join(field_names)})"
|