framesdkpy 0.3.0__tar.gz

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 (51) hide show
  1. framesdkpy-0.3.0/.gitignore +7 -0
  2. framesdkpy-0.3.0/IMPLEMENTATION_SUMMARY.md +218 -0
  3. framesdkpy-0.3.0/PKG-INFO +89 -0
  4. framesdkpy-0.3.0/README.md +65 -0
  5. framesdkpy-0.3.0/docs/loaders.md +193 -0
  6. framesdkpy-0.3.0/docs/models.md +380 -0
  7. framesdkpy-0.3.0/docs/translators.md +116 -0
  8. framesdkpy-0.3.0/docs/validators.md +187 -0
  9. framesdkpy-0.3.0/framesdkpy/__init__.py +33 -0
  10. framesdkpy-0.3.0/framesdkpy/loaders/__init__.py +79 -0
  11. framesdkpy-0.3.0/framesdkpy/loaders/assembler.py +217 -0
  12. framesdkpy-0.3.0/framesdkpy/loaders/yaml_reader.py +57 -0
  13. framesdkpy-0.3.0/framesdkpy/models/__init__.py +95 -0
  14. framesdkpy-0.3.0/framesdkpy/models/acts_model.py +139 -0
  15. framesdkpy-0.3.0/framesdkpy/models/base.py +64 -0
  16. framesdkpy-0.3.0/framesdkpy/models/expect_model.py +127 -0
  17. framesdkpy-0.3.0/framesdkpy/models/facts_model.py +163 -0
  18. framesdkpy-0.3.0/framesdkpy/models/frame_model.py +42 -0
  19. framesdkpy-0.3.0/framesdkpy/models/map_model.py +157 -0
  20. framesdkpy-0.3.0/framesdkpy/models/rules_model.py +181 -0
  21. framesdkpy-0.3.0/framesdkpy/schemas/acts.schema.json +116 -0
  22. framesdkpy-0.3.0/framesdkpy/schemas/expect.schema.json +105 -0
  23. framesdkpy-0.3.0/framesdkpy/schemas/facts.schema.json +256 -0
  24. framesdkpy-0.3.0/framesdkpy/schemas/frame.schema.json +114 -0
  25. framesdkpy-0.3.0/framesdkpy/schemas/map.schema.json +119 -0
  26. framesdkpy-0.3.0/framesdkpy/schemas/rules.schema.json +140 -0
  27. framesdkpy-0.3.0/framesdkpy/translators/__init__.py +24 -0
  28. framesdkpy-0.3.0/framesdkpy/translators/normalizer.py +106 -0
  29. framesdkpy-0.3.0/framesdkpy/translators/yaml_to_json.py +88 -0
  30. framesdkpy-0.3.0/framesdkpy/validators/__init__.py +74 -0
  31. framesdkpy-0.3.0/framesdkpy/validators/cross_file_validator.py +85 -0
  32. framesdkpy-0.3.0/framesdkpy/validators/limits_validator.py +169 -0
  33. framesdkpy-0.3.0/framesdkpy/validators/result.py +100 -0
  34. framesdkpy-0.3.0/framesdkpy/validators/schema_validator.py +152 -0
  35. framesdkpy-0.3.0/pyproject.toml +41 -0
  36. framesdkpy-0.3.0/tests/README.md +10 -0
  37. framesdkpy-0.3.0/tests/fixtures/acts.yaml +23 -0
  38. framesdkpy-0.3.0/tests/fixtures/expect.yaml +143 -0
  39. framesdkpy-0.3.0/tests/fixtures/facts.yaml +126 -0
  40. framesdkpy-0.3.0/tests/fixtures/map.yaml +155 -0
  41. framesdkpy-0.3.0/tests/fixtures/rules.yaml +169 -0
  42. framesdkpy-0.3.0/tests/test_assembler_contract.py +63 -0
  43. framesdkpy-0.3.0/tests/test_broken_fixtures.py +239 -0
  44. framesdkpy-0.3.0/tests/test_loaders.py +120 -0
  45. framesdkpy-0.3.0/tests/test_models.py +463 -0
  46. framesdkpy-0.3.0/tests/test_pharmax_integration.py +211 -0
  47. framesdkpy-0.3.0/tests/test_public_api.py +44 -0
  48. framesdkpy-0.3.0/tests/test_schema_shape_preservation.py +122 -0
  49. framesdkpy-0.3.0/tests/test_translators.py +263 -0
  50. framesdkpy-0.3.0/tests/test_validators.py +333 -0
  51. framesdkpy-0.3.0/uv.lock +339 -0
@@ -0,0 +1,7 @@
1
+ FrameSDK/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .env
6
+ .env
7
+ temp.txt
@@ -0,0 +1,218 @@
1
+ # FrameSDK Implementation Summary
2
+
3
+ **Date:** 2026-06-10
4
+ **Version:** v0.3.0
5
+ **Tests:** 82 passing (29 models + 25 translators + 22 validators + 6 loaders)
6
+
7
+ ## What was built
8
+
9
+ FrameSDK is the Python SDK for FRAME. It provides a uniform interface for reading,
10
+ validating, and working with FRAME project context files. Every downstream tool
11
+ (Haxaml, CLIs, future frame-js) gets the same shaped answer from FrameSDK.
12
+
13
+ ### Architecture
14
+
15
+ ```
16
+ frame/
17
+ ├── __init__.py # Top-level re-exports
18
+ ├── models/ # Typed data carriers
19
+ │ ├── base.py # FrameBaseModel -- to_dict, to_json, __repr__
20
+ │ ├── facts_model.py # FrameFacts, Profile, Architecture, Source, Quirk, OpenQuestion
21
+ │ ├── rules_model.py # FrameRules, Policy, CoreRule, Command, Dont, AskFirst, Hint
22
+ │ ├── map_model.py # FrameMap, Group, PathEntry, Entrypoint, ManagedPath, UnmappedPath
23
+ │ ├── expect_model.py # FrameExpect, MustHold, Check, Proof
24
+ │ ├── acts_model.py # FrameActs, Run, RunCheck, Blocker
25
+ │ └── frame_model.py # FRAME -- collates all five parts
26
+
27
+ ├── loaders/ # File discovery, parsing, assembly
28
+ │ ├── yaml_reader.py # Strict 5-file discovery + raw YAML parsing
29
+ │ ├── assembler.py # Dict → typed FRAME model construction
30
+ │ └── __init__.py # load_frame() -- full pipeline orchestrator
31
+
32
+ ├── validators/ # Schema, limit, and cross-file validation
33
+ │ ├── result.py # ValidationResult, ValidationError, ValidationWarning
34
+ │ ├── schema_validator.py # JSON Schema validation with local $ref resolution
35
+ │ ├── limits_validator.py # Character limit enforcement
36
+ │ ├── cross_file_validator.py # Cross-file schema_version and file/role checks
37
+ │ └── __init__.py # validate_frame(), validate_file()
38
+
39
+ ├── translators/ # YAML ↔ JSON conversion
40
+ │ ├── normalizer.py # YAML quirk resolution (yes→True, ~→None, etc.)
41
+ │ ├── yaml_to_json.py # YAML file → clean JSON-compatible dict
42
+ │ └── __init__.py # translate_file(), translate_directory()
43
+
44
+ ├── computations/ # Future: graph, cross-referencing
45
+ └── helpers/ # Future: shared utilities
46
+ ```
47
+
48
+ ### Pipeline (load_frame)
49
+
50
+ ```
51
+ Directory path
52
+ ↓ yaml_reader.discover_frame_dir()
53
+ │ Verifies exactly 5 files: facts.yaml, rules.yaml, map.yaml, expect.yaml, acts.yaml
54
+ ↓ yaml_reader.read_raw_yaml()
55
+ │ Parses YAML into raw Python dicts (safe_load)
56
+ ↓ normalizer.normalize_dict()
57
+ │ Resolves YAML quirks: yes→True, no→False, ~→None, on/off→error
58
+ ↓ validators.validate_against_schema()
59
+ │ JSON Schema validation: types, required fields, enums
60
+ ↓ validators.validate_limits()
61
+ │ Character limits: enforced on core fields, advisory on descriptive
62
+ ↓ assembler.assemble_frame()
63
+ │ Cross-file check → builds typed FRAME object → returns
64
+ ```
65
+
66
+ ---
67
+
68
+ ## What each test suite verifies
69
+
70
+ ### test_models.py (29 tests)
71
+
72
+ **FrameFacts (6 tests):**
73
+ - Minimal construction with only required fields (profile, architecture)
74
+ - Full construction with all optional sub-models populated
75
+ - Required fields are non-nullable (Profile.name is str, not str|None)
76
+ - Architecture.summary is required
77
+ - to_dict() preserves nulls -- optional fields with None appear as keys with null values
78
+ - to_json() produces valid parseable JSON
79
+
80
+ **FrameRules (5 tests):**
81
+ - Minimal construction with defaults (governance_level="normal")
82
+ - Full construction with policies, commands, donts, ask_first, hints
83
+ - Dont defaults to severity="critical"
84
+ - Command has exactly three required fields (run, kind, purpose)
85
+ - Commands dict serializes correctly
86
+
87
+ **FrameMap (4 tests):**
88
+ - Minimal construction with all empty lists
89
+ - Full construction with groups, paths, entrypoints, managed_paths, unmapped_paths
90
+ - PathEntry.id is optional (only needed for cross-referencing)
91
+ - ManagedPath.id is optional (only needed for cross-referencing)
92
+
93
+ **FrameExpect (3 tests):**
94
+ - Minimal construction with empty checks and proof
95
+ - Full construction with outcomes, must_hold, checks, proof
96
+ - Various pass_condition formats work (exit_code, stdout contains)
97
+
98
+ **FrameActs (4 tests):**
99
+ - Minimal construction with empty runs and blockers
100
+ - Full construction with run records and nested RunCheck objects
101
+ - RunCheck with status=ran may have result=None (loader populates this)
102
+ - RunCheck with status=skipped may have reason=None (loader populates this)
103
+
104
+ **FRAME collation (4 tests):**
105
+ - Minimal construction with only Facts (rules/map/expect/acts are None)
106
+ - Full construction with all five parts populated
107
+ - to_json() produces valid JSON with correct structure
108
+ - repr() displays meaningful class name
109
+
110
+ **Null preservation (3 tests):**
111
+ - Optional fields with None appear as keys in to_dict()
112
+ - Optional fields with None appear as keys in JSON output
113
+ - Empty lists are preserved (not converted to null)
114
+
115
+ ### test_translators.py (25 tests)
116
+
117
+ **Normalizer (13 tests):**
118
+ - yes/Yes/YES/true/y → True
119
+ - no/No/NO/false/n → False
120
+ - ~ → None
121
+ - null/Null/NULL → None
122
+ - on/off raises TranslationError (ambiguous)
123
+ - Empty string "" preserved as empty string (NOT coerced to None)
124
+ - Python None stays None
125
+ - Bare numbers pass through unchanged
126
+ - Boolean values (already parsed by YAML) pass through
127
+ - Regular strings pass through unchanged
128
+ - Quoted "yes" in YAML (parsed as string by YAML lib) → normalized to True
129
+ - Nested dicts normalize recursively
130
+ - Nested lists normalize recursively
131
+
132
+ **translate_to_dict (4 tests):**
133
+ - Basic YAML string to dict
134
+ - YAML with quirks (yes, no, ~, null)
135
+ - Empty YAML returns empty dict
136
+ - YAML with nested lists
137
+
138
+ **translate_file (3 tests):**
139
+ - Temp YAML file → translated dict
140
+ - Nonexistent file raises FileNotFoundError
141
+ - Wrong extension (.txt) raises ValueError
142
+
143
+ **translate_directory (3 tests):**
144
+ - Full directory with all 5 files → dict of dicts
145
+ - Missing file raises FileNotFoundError
146
+ - Nonexistent directory raises FileNotFoundError
147
+
148
+ **translate_to_json_string (2 tests):**
149
+ - Produces valid parseable JSON
150
+ - JSON preserves types (True, 42, null)
151
+
152
+ ### test_validators.py (22 tests)
153
+
154
+ **ValidationResult (6 tests):**
155
+ - Empty result is valid and clean
156
+ - Result with only warnings is valid but not clean
157
+ - Result with errors is invalid
158
+ - Merge combines errors and warnings from multiple results
159
+ - add_error() convenience method populates expected/actual
160
+ - summary() produces human-readable string
161
+
162
+ **Schema validator (4 tests):**
163
+ - Valid Facts dict passes schema validation
164
+ - Missing required field (profile) is caught
165
+ - Minimal valid Rules passes
166
+ - All 5 file types pass with minimal valid data
167
+
168
+ **Limits validator (4 tests):**
169
+ - Value within limit passes
170
+ - Core field (id) exceeding maxLength → error
171
+ - Advisory field within limit → passes clean
172
+ - Advisory field exceeding maxLength → warns, doesn't error
173
+
174
+ **Cross-file validator (4 tests):**
175
+ - All matching schema_versions pass
176
+ - Version mismatch catches mismatched versions
177
+ - Wrong file field catches file type mismatch
178
+ - Wrong role field catches role mismatch
179
+
180
+ **validate_frame (4 tests):**
181
+ - Full valid directory passes end-to-end
182
+ - Missing file raises FileNotFoundError
183
+ - Schema version mismatch is caught
184
+ - File/role mismatch is caught
185
+
186
+ ### test_loaders.py (6 tests)
187
+
188
+ - load_frame() returns typed FRAME object with all parts present
189
+ - Loaded FRAME serializes to JSON correctly
190
+ - Missing file raises FileNotFoundError
191
+ - Schema version mismatch raises FrameLoadError
192
+ - Missing required Facts fields raises FrameLoadError
193
+ - Nonexistent directory raises FileNotFoundError
194
+
195
+ ---
196
+
197
+ ## Audit findings
198
+
199
+ ### Stale files removed
200
+ - `framesdkpy/loaders/loader.py` -- old generic loader, replaced by yaml_reader + assembler
201
+ - `framesdkpy/models/model.py` -- old flat model, replaced by five typed model files
202
+ - `framesdkpy/computations/report.py` -- old ValidationReport, replaced by ValidationResult
203
+ - `framesdkpy/helpers/provisional.py` -- dead code, never referenced
204
+ - `framesdkpy/validators/mechanical_validator.py` -- belongs in Haxaml, not FrameSDK
205
+
206
+ ### Over-engineering avoided
207
+ - No separate assembler package -- lives inside loaders (Decision D6.5)
208
+ - No Pydantic dependency -- pure dataclasses (Decision D4)
209
+ - No graph/cross-reference computation yet -- deferring to future
210
+ - No JSON-to-YAML translator yet -- low priority (Decision D15)
211
+ - No abstract base class for validators -- simple functions returning result objects
212
+
213
+ ### Design consistency
214
+ - Every module follows the same pattern: public API in __init__.py, implementation in sub-modules
215
+ - All validators return result objects (never raise for validation failures)
216
+ - All translators return clean dicts (never typed models)
217
+ - All models inherit from FrameBaseModel for uniform serialization
218
+ - Character limits enforced by field category (core vs advisory), not by blanket rules
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: framesdkpy
3
+ Version: 0.3.0
4
+ Summary: Python SDK for FRAME -- typed project-context architecture for AI-assisted development
5
+ Project-URL: Homepage, https://github.com/haxsysgit/FrameSDK
6
+ Project-URL: Repository, https://github.com/haxsysgit/FrameSDK
7
+ Author: Arinze Elenasulu
8
+ License-Expression: MIT
9
+ Keywords: agent-governance,ai-agents,frame,project-context
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: jsonschema>=4.20
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: referencing>=0.30
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # FrameSDK
26
+
27
+ The Python SDK for [FRAME](https://github.com/haxsysgit/FRAME) -- a typed project-context architecture for AI-assisted development.
28
+
29
+ When you switch coding agents, the project forgets itself. Not its code -- the code is fine. But the *understanding*. The rules you agreed on. The decisions you made and why. The checks that matter. Things previous agents touched or broke.
30
+
31
+ FRAME gives the project a typed shape that agents and tools read consistently. framesdkpy is how Python tools read that shape.
32
+
33
+ ## What it does
34
+
35
+ Takes a `.haxaml/` directory with 5 YAML files and returns a typed `FRAME` object:
36
+
37
+ ```python
38
+ from framesdkpy import load_frame
39
+
40
+ frame = load_frame(".haxaml/")
41
+ frame.facts.profile.name # "Pharmax"
42
+ frame.rules.governance_level # "strict"
43
+ frame.map.entrypoints[0].path # "Backend/main.py"
44
+ frame.expect.checks["backend_tests"].pass_condition # "exit_code == 0"
45
+ ```
46
+
47
+ Every downstream tool -- Haxaml, a CLI, a CI pipeline -- gets the same shaped answer. Cross-language SDKs return the same JSON shape.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ uv add framesdkpy
53
+ # or
54
+ pip install framesdkpy
55
+ ```
56
+
57
+ Requires Python 3.11+. Three dependencies: PyYAML, jsonschema, referencing. That's it. No Pydantic, no heavy framework.
58
+
59
+ ## What's in the box
60
+
61
+ - **loaders** -- `load_frame()` builds a typed FRAME from 5 YAML files. Strict single-directory discovery. Schema and character limit validation at load time.
62
+ - **models** -- 27 typed dataclasses across 7 files. One import: `from framesdkpy.models import FRAME`.
63
+ - **validators** -- Schema, character limits, cross-file consistency. Callable independently or through the loader.
64
+ - **translators** -- YAML to JSON with full normalization. Handles yes/True, ~/None, on/off rejection.
65
+
66
+ ## Usage patterns
67
+
68
+ ```python
69
+ from framesdkpy import load_frame, translate_directory, validate_file
70
+
71
+ # Full pipeline -- load all 5 files, validate, assemble
72
+ frame = load_frame(".haxaml/")
73
+
74
+ # Translate YAML to clean dict (normalized, but no validation)
75
+ data = translate_directory(".haxaml/")
76
+
77
+ # Validate a single file without loading the full model
78
+ result = validate_file(".haxaml/facts.yaml")
79
+ print(result.summary()) # "valid" or "2 error(s), 1 warning(s)"
80
+
81
+ # Serialize for cross-language use
82
+ json_string = frame.to_json()
83
+ ```
84
+
85
+ ## How it's built
86
+
87
+ Spec-first. Every module has a design doc (`docs/models.md`, `docs/loaders.md`, etc.) with locked decisions before any code was written. 106 tests cover construction, serialization, YAML normalization, schema enforcement, character limits, cross-file checks, and integration against a real Pharmax fixture.
88
+
89
+ No graph building, no cross-referencing, no governance. That's Haxaml's job. framesdkpy is pure ingestion -- load, validate, assemble, return.
@@ -0,0 +1,65 @@
1
+ # FrameSDK
2
+
3
+ The Python SDK for [FRAME](https://github.com/haxsysgit/FRAME) -- a typed project-context architecture for AI-assisted development.
4
+
5
+ When you switch coding agents, the project forgets itself. Not its code -- the code is fine. But the *understanding*. The rules you agreed on. The decisions you made and why. The checks that matter. Things previous agents touched or broke.
6
+
7
+ FRAME gives the project a typed shape that agents and tools read consistently. framesdkpy is how Python tools read that shape.
8
+
9
+ ## What it does
10
+
11
+ Takes a `.haxaml/` directory with 5 YAML files and returns a typed `FRAME` object:
12
+
13
+ ```python
14
+ from framesdkpy import load_frame
15
+
16
+ frame = load_frame(".haxaml/")
17
+ frame.facts.profile.name # "Pharmax"
18
+ frame.rules.governance_level # "strict"
19
+ frame.map.entrypoints[0].path # "Backend/main.py"
20
+ frame.expect.checks["backend_tests"].pass_condition # "exit_code == 0"
21
+ ```
22
+
23
+ Every downstream tool -- Haxaml, a CLI, a CI pipeline -- gets the same shaped answer. Cross-language SDKs return the same JSON shape.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ uv add framesdkpy
29
+ # or
30
+ pip install framesdkpy
31
+ ```
32
+
33
+ Requires Python 3.11+. Three dependencies: PyYAML, jsonschema, referencing. That's it. No Pydantic, no heavy framework.
34
+
35
+ ## What's in the box
36
+
37
+ - **loaders** -- `load_frame()` builds a typed FRAME from 5 YAML files. Strict single-directory discovery. Schema and character limit validation at load time.
38
+ - **models** -- 27 typed dataclasses across 7 files. One import: `from framesdkpy.models import FRAME`.
39
+ - **validators** -- Schema, character limits, cross-file consistency. Callable independently or through the loader.
40
+ - **translators** -- YAML to JSON with full normalization. Handles yes/True, ~/None, on/off rejection.
41
+
42
+ ## Usage patterns
43
+
44
+ ```python
45
+ from framesdkpy import load_frame, translate_directory, validate_file
46
+
47
+ # Full pipeline -- load all 5 files, validate, assemble
48
+ frame = load_frame(".haxaml/")
49
+
50
+ # Translate YAML to clean dict (normalized, but no validation)
51
+ data = translate_directory(".haxaml/")
52
+
53
+ # Validate a single file without loading the full model
54
+ result = validate_file(".haxaml/facts.yaml")
55
+ print(result.summary()) # "valid" or "2 error(s), 1 warning(s)"
56
+
57
+ # Serialize for cross-language use
58
+ json_string = frame.to_json()
59
+ ```
60
+
61
+ ## How it's built
62
+
63
+ Spec-first. Every module has a design doc (`docs/models.md`, `docs/loaders.md`, etc.) with locked decisions before any code was written. 106 tests cover construction, serialization, YAML normalization, schema enforcement, character limits, cross-file checks, and integration against a real Pharmax fixture.
64
+
65
+ No graph building, no cross-referencing, no governance. That's Haxaml's job. framesdkpy is pure ingestion -- load, validate, assemble, return.
@@ -0,0 +1,193 @@
1
+ # FrameSDK Loaders -- Specification
2
+
3
+ **Status:** Agreed. Code follows this spec.
4
+ **Date:** 2026-06-10
5
+
6
+ ---
7
+
8
+ ## Job
9
+
10
+ A loader takes a directory path and returns a typed, validated, normalized FRAME object composed of five parts: FrameFacts, FrameRules, FrameMap, FrameExpect, FrameActs.
11
+
12
+ No graph building. No cross-referencing. No governance. Pure ingestion.
13
+
14
+ ---
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ frame/loaders/
20
+ ├── __init__.py ← Public API: load_frame(dir_path)
21
+ ├── yaml_reader.py ← Raw YAML parsing, file discovery
22
+ ├── normalizer.py ← Type cleanup, default injection, character trimming
23
+ └── assembler.py ← Combines 5 parts into one FRAME
24
+ ```
25
+
26
+ Flow:
27
+
28
+ ```
29
+ Directory path
30
+
31
+ yaml_reader verifies exactly 5 files exist: facts.yaml, rules.yaml, map.yaml, expect.yaml, acts.yaml
32
+
33
+ yaml_reader parses each into raw Python dicts
34
+
35
+ normalizer cleans each dict:
36
+ -- Trims strings to maxLength (core fields error, advisory fields warn)
37
+ -- Replaces YAML weirdness (null → None, yes → True, 123 → 123)
38
+ -- Injects defaults for optional fields
39
+ -- Flags missing required core fields
40
+
41
+ assembler validates cross-file consistency:
42
+ -- All schema_version fields match
43
+ -- All file/role fields match their expected values
44
+
45
+ assembler builds FRAME:
46
+ -- FRAME.facts: FrameFacts
47
+ -- FRAME.rules: FrameRules
48
+ -- etc.
49
+
50
+ Returns FRAME
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Decisions
56
+
57
+ ### D1: Validation at load time (split enforcement)
58
+
59
+ Core governance fields (ids, names, rules, checks, command_refs, pass_conditions) are validated and enforced at load time. Load fails if they're missing or invalid.
60
+
61
+ Descriptive/advisory fields (code_style, git, architecture notes, environment descriptions) are validated and warned at load time. Load succeeds but logs warnings.
62
+
63
+ ### D2: Character limits -- error for core, warn for advisory
64
+
65
+ Core fields exceeding maxLength → load fails with clear error.
66
+ Advisory fields exceeding maxLength → load succeeds, warning emitted.
67
+
68
+ ### D3: Strict single-directory discovery
69
+
70
+ The loader receives exactly ONE directory path. It looks inside that directory for exactly 5 files: facts.yaml, rules.yaml, map.yaml, expect.yaml, acts.yaml.
71
+
72
+ It NEVER:
73
+ - Searches parent directories
74
+ - Searches sibling directories
75
+ - Picks up individual scattered files
76
+ - Does fuzzy matching on filenames
77
+
78
+ If any file is missing → load fails.
79
+ If the directory doesn't exist → load fails.
80
+ If extra YAML files exist in the directory → they are ignored (not an error).
81
+
82
+ The caller controls where FRAME lives. The loader doesn't guess.
83
+
84
+ ### D4: Dataclasses, not Pydantic
85
+
86
+ The returned models use Python dataclasses. Validation happens at load time, not at model instantiation. Dataclasses are pure typed carriers -- fast, zero-dependency, readable.
87
+
88
+ ### D5: Five distinct typed parts
89
+
90
+ ```
91
+ FRAME
92
+ ├── facts: FrameFacts
93
+ ├── rules: FrameRules
94
+ ├── map: FrameMap
95
+ ├── expect: FrameExpect
96
+ └── acts: FrameActs
97
+ ```
98
+
99
+ Not a generic "FrameDocument" with optional nullable parts. Each part is a distinct model class with its own fields.
100
+
101
+ ### D6: Return typed dataclasses that serialize to clean JSON
102
+
103
+ Internal: Python tools (Haxaml, CLIs) use typed dot access (`frame.facts.profile.name`).
104
+
105
+ External: Cross-language tools consume JSON (`frame.to_dict()`, `frame.to_json()`).
106
+
107
+ The JSON shape is the cross-language contract. The dataclasses are the Python binding. frame-js returns the same JSON shape. Any tool can switch SDKs without changing how it reads data.
108
+
109
+ ---
110
+
111
+ ## Data model
112
+
113
+ ```python
114
+ @dataclass(slots=True)
115
+ class FrameFacts:
116
+ profile: dict
117
+ classification: dict | None
118
+ technology: dict | None
119
+ architecture: dict | None
120
+ environments: dict | None
121
+ persistence: dict | None
122
+ sources: list[dict]
123
+ quirks: list[dict]
124
+ open_questions: list[dict]
125
+
126
+ @dataclass(slots=True)
127
+ class FrameRules:
128
+ governance_level: str
129
+ rules: list[dict]
130
+ policies: list[dict]
131
+ commands: dict
132
+ code_style: dict | None
133
+ git: dict | None
134
+ donts: list[dict]
135
+ ask_first: list[dict]
136
+ hints: list[dict]
137
+
138
+ @dataclass(slots=True)
139
+ class FrameMap:
140
+ structure: str | None
141
+ roots: dict | None
142
+ groups: list[dict]
143
+ paths: list[dict]
144
+ entrypoints: list[dict]
145
+ managed_paths: list[dict]
146
+ unmapped_paths: list[dict]
147
+
148
+ @dataclass(slots=True)
149
+ class FrameExpect:
150
+ outcomes: dict | None
151
+ must_hold: list[dict]
152
+ checks: dict | None
153
+ done_when: dict | None
154
+ proof: list[dict]
155
+ handoff: dict | None
156
+
157
+ @dataclass(slots=True)
158
+ class FrameActs:
159
+ summary: str | None
160
+ runs: list[dict]
161
+ blockers: list[dict]
162
+ handoff: dict | None
163
+
164
+ @dataclass(slots=True)
165
+ class FRAME:
166
+ facts: FrameFacts
167
+ rules: FrameRules | None
168
+ map: FrameMap | None
169
+ expect: FrameExpect | None
170
+ acts: FrameActs | None
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Public API
176
+
177
+ ```python
178
+ from framesdkpy.loaders import load_frame
179
+
180
+ # Returns FRAME or raises FrameLoadError
181
+ frame: FRAME = load_frame("/path/to/.haxaml")
182
+
183
+ # Each part is a typed model
184
+ print(frame.facts.profile["name"])
185
+ print(frame.rules.commands["backend_tests"]["run"])
186
+ print(frame.expect.checks["workflow_smoke"]["pass_condition"])
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Decision D6.5: Assembler lives inside loaders, not a separate package
192
+
193
+ The assembler builds the FRAME object from 5 validated dicts. It is tightly coupled to the loader's pipeline -- it only runs after loading and validation. Spinning it into its own package creates an abstraction nobody needs. Tools call `load_frame()`, not `assemble_frame()`. The pipeline is one cohesive flow: load -> validate -> normalize -> assemble -> return.