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.
- framesdkpy-0.3.0/.gitignore +7 -0
- framesdkpy-0.3.0/IMPLEMENTATION_SUMMARY.md +218 -0
- framesdkpy-0.3.0/PKG-INFO +89 -0
- framesdkpy-0.3.0/README.md +65 -0
- framesdkpy-0.3.0/docs/loaders.md +193 -0
- framesdkpy-0.3.0/docs/models.md +380 -0
- framesdkpy-0.3.0/docs/translators.md +116 -0
- framesdkpy-0.3.0/docs/validators.md +187 -0
- framesdkpy-0.3.0/framesdkpy/__init__.py +33 -0
- framesdkpy-0.3.0/framesdkpy/loaders/__init__.py +79 -0
- framesdkpy-0.3.0/framesdkpy/loaders/assembler.py +217 -0
- framesdkpy-0.3.0/framesdkpy/loaders/yaml_reader.py +57 -0
- framesdkpy-0.3.0/framesdkpy/models/__init__.py +95 -0
- framesdkpy-0.3.0/framesdkpy/models/acts_model.py +139 -0
- framesdkpy-0.3.0/framesdkpy/models/base.py +64 -0
- framesdkpy-0.3.0/framesdkpy/models/expect_model.py +127 -0
- framesdkpy-0.3.0/framesdkpy/models/facts_model.py +163 -0
- framesdkpy-0.3.0/framesdkpy/models/frame_model.py +42 -0
- framesdkpy-0.3.0/framesdkpy/models/map_model.py +157 -0
- framesdkpy-0.3.0/framesdkpy/models/rules_model.py +181 -0
- framesdkpy-0.3.0/framesdkpy/schemas/acts.schema.json +116 -0
- framesdkpy-0.3.0/framesdkpy/schemas/expect.schema.json +105 -0
- framesdkpy-0.3.0/framesdkpy/schemas/facts.schema.json +256 -0
- framesdkpy-0.3.0/framesdkpy/schemas/frame.schema.json +114 -0
- framesdkpy-0.3.0/framesdkpy/schemas/map.schema.json +119 -0
- framesdkpy-0.3.0/framesdkpy/schemas/rules.schema.json +140 -0
- framesdkpy-0.3.0/framesdkpy/translators/__init__.py +24 -0
- framesdkpy-0.3.0/framesdkpy/translators/normalizer.py +106 -0
- framesdkpy-0.3.0/framesdkpy/translators/yaml_to_json.py +88 -0
- framesdkpy-0.3.0/framesdkpy/validators/__init__.py +74 -0
- framesdkpy-0.3.0/framesdkpy/validators/cross_file_validator.py +85 -0
- framesdkpy-0.3.0/framesdkpy/validators/limits_validator.py +169 -0
- framesdkpy-0.3.0/framesdkpy/validators/result.py +100 -0
- framesdkpy-0.3.0/framesdkpy/validators/schema_validator.py +152 -0
- framesdkpy-0.3.0/pyproject.toml +41 -0
- framesdkpy-0.3.0/tests/README.md +10 -0
- framesdkpy-0.3.0/tests/fixtures/acts.yaml +23 -0
- framesdkpy-0.3.0/tests/fixtures/expect.yaml +143 -0
- framesdkpy-0.3.0/tests/fixtures/facts.yaml +126 -0
- framesdkpy-0.3.0/tests/fixtures/map.yaml +155 -0
- framesdkpy-0.3.0/tests/fixtures/rules.yaml +169 -0
- framesdkpy-0.3.0/tests/test_assembler_contract.py +63 -0
- framesdkpy-0.3.0/tests/test_broken_fixtures.py +239 -0
- framesdkpy-0.3.0/tests/test_loaders.py +120 -0
- framesdkpy-0.3.0/tests/test_models.py +463 -0
- framesdkpy-0.3.0/tests/test_pharmax_integration.py +211 -0
- framesdkpy-0.3.0/tests/test_public_api.py +44 -0
- framesdkpy-0.3.0/tests/test_schema_shape_preservation.py +122 -0
- framesdkpy-0.3.0/tests/test_translators.py +263 -0
- framesdkpy-0.3.0/tests/test_validators.py +333 -0
- framesdkpy-0.3.0/uv.lock +339 -0
|
@@ -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.
|