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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Character limit validator -- enforces maxLength on FRAME fields.
|
|
2
|
+
|
|
3
|
+
Core governance fields (ids, names, rules, checks, command_refs, pass_conditions)
|
|
4
|
+
are enforced -- exceeding their maxLength blocks the load.
|
|
5
|
+
|
|
6
|
+
Advisory/descriptive fields (code_style, git, architecture notes, environment
|
|
7
|
+
descriptions) are warned -- the load continues but the caller sees the warning.
|
|
8
|
+
|
|
9
|
+
Character limits are defined in the finalized schema. This validator checks them.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from framesdkpy.validators.result import ValidationResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Field limit registry
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Each entry: (dotted_path, max_chars, enforced_or_advisory)
|
|
21
|
+
# Core governance fields: enforced (error if exceeded)
|
|
22
|
+
# Descriptive blocks: advisory (warning if exceeded)
|
|
23
|
+
|
|
24
|
+
_FIELD_LIMITS: list[tuple[str, int, str]] = [
|
|
25
|
+
# Core governance fields -- enforced
|
|
26
|
+
("*.id", 100, "enforced"),
|
|
27
|
+
("*.name", 100, "enforced"),
|
|
28
|
+
("facts.profile.name", 100, "enforced"),
|
|
29
|
+
("facts.profile.summary", 300, "enforced"),
|
|
30
|
+
("facts.sources[].id", 100, "enforced"),
|
|
31
|
+
("facts.sources[].path", 200, "enforced"),
|
|
32
|
+
("facts.quirks[].id", 100, "enforced"),
|
|
33
|
+
("facts.open_questions[].id", 100, "enforced"),
|
|
34
|
+
("rules.rules[].id", 100, "enforced"),
|
|
35
|
+
("rules.policies[].id", 100, "enforced"),
|
|
36
|
+
("rules.donts[].id", 100, "enforced"),
|
|
37
|
+
("rules.ask_first[].id", 100, "enforced"),
|
|
38
|
+
("rules.hints[].id", 100, "enforced"),
|
|
39
|
+
("rules.commands.*.run", 500, "enforced"),
|
|
40
|
+
("rules.commands.*.purpose", 300, "enforced"),
|
|
41
|
+
("map.groups[].id", 100, "enforced"),
|
|
42
|
+
("map.groups[].label", 150, "enforced"),
|
|
43
|
+
("map.groups[].paths[]", 300, "enforced"),
|
|
44
|
+
("map.entrypoints[].id", 100, "enforced"),
|
|
45
|
+
("expect.must_hold[].id", 100, "enforced"),
|
|
46
|
+
("expect.checks.*.name", 100, "enforced"),
|
|
47
|
+
("expect.checks.*.command_ref", 200, "enforced"),
|
|
48
|
+
("expect.checks.*.pass_condition", 200, "enforced"),
|
|
49
|
+
("expect.proof[].id", 100, "enforced"),
|
|
50
|
+
("acts.runs[].id", 100, "enforced"),
|
|
51
|
+
("acts.runs[].actor", 100, "enforced"),
|
|
52
|
+
("acts.blockers[].id", 100, "enforced"),
|
|
53
|
+
|
|
54
|
+
# Advisory blocks -- warned
|
|
55
|
+
("facts.architecture.summary", 500, "advisory"),
|
|
56
|
+
("facts.architecture.*", 500, "advisory"),
|
|
57
|
+
("facts.technology.*", 100, "advisory"),
|
|
58
|
+
("facts.sources[].purpose", 300, "advisory"),
|
|
59
|
+
("facts.quirks[].description", 200, "advisory"),
|
|
60
|
+
("facts.quirks[].why", 300, "advisory"),
|
|
61
|
+
("facts.open_questions[].question", 300, "advisory"),
|
|
62
|
+
("facts.open_questions[].context", 300, "advisory"),
|
|
63
|
+
("rules.rules[].rule", 500, "advisory"),
|
|
64
|
+
("rules.policies[].name", 150, "advisory"),
|
|
65
|
+
("rules.policies[].rule", 500, "advisory"),
|
|
66
|
+
("rules.donts[].rule", 300, "advisory"),
|
|
67
|
+
("rules.ask_first[].trigger", 300, "advisory"),
|
|
68
|
+
("rules.ask_first[].reason", 300, "advisory"),
|
|
69
|
+
("rules.hints[].hint", 300, "advisory"),
|
|
70
|
+
("rules.code_style", 1000, "advisory"),
|
|
71
|
+
("rules.git", 1000, "advisory"),
|
|
72
|
+
("map.structure", 800, "advisory"),
|
|
73
|
+
("map.paths[].path", 200, "advisory"),
|
|
74
|
+
("map.paths[].purpose", 300, "advisory"),
|
|
75
|
+
("map.entrypoints[].path", 200, "advisory"),
|
|
76
|
+
("map.managed_paths[].path", 200, "advisory"),
|
|
77
|
+
("expect.outcomes.*.summary", 300, "advisory"),
|
|
78
|
+
("expect.must_hold[].statement", 300, "advisory"),
|
|
79
|
+
("expect.checks.*.what", 300, "advisory"),
|
|
80
|
+
("expect.checks.*.how", 200, "advisory"),
|
|
81
|
+
("expect.proof[].description", 300, "advisory"),
|
|
82
|
+
("acts.summary", 500, "advisory"),
|
|
83
|
+
("acts.runs[].goal", 300, "advisory"),
|
|
84
|
+
("acts.runs[].input_summary", 300, "advisory"),
|
|
85
|
+
("acts.runs[].output_summary", 300, "advisory"),
|
|
86
|
+
("acts.runs[].checks[].reason", 200, "advisory"),
|
|
87
|
+
("acts.blockers[].description", 300, "advisory"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Validation
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_limits(data: dict, file_stem: str) -> ValidationResult:
|
|
97
|
+
"""Check character limits on all fields in a FRAME data dict.
|
|
98
|
+
|
|
99
|
+
Walks the dict tree, matches each value against the field limit registry,
|
|
100
|
+
and reports any violations.
|
|
101
|
+
"""
|
|
102
|
+
result = ValidationResult()
|
|
103
|
+
_walk_and_check(data, file_stem, result)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _walk_and_check(data, path: str, result: ValidationResult):
|
|
108
|
+
"""Recursively walk a dict and check field limits."""
|
|
109
|
+
if isinstance(data, dict):
|
|
110
|
+
for key, value in data.items():
|
|
111
|
+
current_path = f"{path}.{key}"
|
|
112
|
+
_check_value(value, current_path, result)
|
|
113
|
+
_walk_and_check(value, current_path, result)
|
|
114
|
+
elif isinstance(data, list):
|
|
115
|
+
for i, item in enumerate(data):
|
|
116
|
+
current_path = f"{path}[{i}]"
|
|
117
|
+
_check_value(item, current_path, result)
|
|
118
|
+
_walk_and_check(item, current_path, result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _check_value(value, path: str, result: ValidationResult):
|
|
122
|
+
"""Check a single value against the limit registry."""
|
|
123
|
+
if not isinstance(value, str):
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Find matching limit rules for this path
|
|
127
|
+
for pattern, max_chars, severity in _FIELD_LIMITS:
|
|
128
|
+
if not _path_matches(path, pattern):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if len(value) > max_chars:
|
|
132
|
+
msg = f"Field exceeds maxLength of {max_chars} chars (got {len(value)})"
|
|
133
|
+
if severity == "enforced":
|
|
134
|
+
result.add_error(
|
|
135
|
+
path=path, message=msg, code="limit_exceeded",
|
|
136
|
+
expected=f"maxLength: {max_chars}", actual=f"length: {len(value)}",
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
result.add_warning(path=path, message=msg, code="limit_advisory")
|
|
140
|
+
break # First match wins -- don't report the same field twice
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _path_matches(actual_path: str, pattern: str) -> bool:
|
|
144
|
+
"""Check if a dot-separated path matches a pattern with * and [] wildcards.
|
|
145
|
+
|
|
146
|
+
Pattern: 'facts.profile.name' matches exactly.
|
|
147
|
+
Pattern: 'facts.sources[].id' matches any index: 'facts.sources[0].id'.
|
|
148
|
+
Pattern: 'rules.commands.*.run' matches any command name.
|
|
149
|
+
Pattern: 'facts.architecture.*' matches any architecture sub-field.
|
|
150
|
+
Pattern: '*.id' matches any top-level field ending in '.id'.
|
|
151
|
+
"""
|
|
152
|
+
# Normalize array indices: replace [0], [1], etc. with []
|
|
153
|
+
import re
|
|
154
|
+
actual_normalized = re.sub(r'\[\d+\]', '[]', actual_path)
|
|
155
|
+
|
|
156
|
+
# Split both paths into segments
|
|
157
|
+
actual_segs = actual_normalized.split(".")
|
|
158
|
+
pattern_segs = pattern.split(".")
|
|
159
|
+
|
|
160
|
+
if len(actual_segs) != len(pattern_segs):
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
for a, p in zip(actual_segs, pattern_segs):
|
|
164
|
+
if p == "*":
|
|
165
|
+
continue # Wildcard matches any segment
|
|
166
|
+
if a != p:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
return True
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Validation result objects -- returned by all validators.
|
|
2
|
+
|
|
3
|
+
Validators never raise exceptions for validation failures. They return
|
|
4
|
+
ValidationResult objects. The caller decides: abort, fix and retry,
|
|
5
|
+
or continue with warnings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class ValidationError:
|
|
15
|
+
"""A blocking validation failure. Must be fixed before loading."""
|
|
16
|
+
|
|
17
|
+
path: str
|
|
18
|
+
"""Dotted path to the failing field, e.g. 'facts.profile.name'."""
|
|
19
|
+
|
|
20
|
+
message: str
|
|
21
|
+
"""Human-readable explanation of what went wrong."""
|
|
22
|
+
|
|
23
|
+
code: str
|
|
24
|
+
"""Machine-readable code: 'missing_required', 'type_error', 'enum_error', 'limit_exceeded'."""
|
|
25
|
+
|
|
26
|
+
expected: str | None = None
|
|
27
|
+
"""What was expected, e.g. 'string', 'maxLength: 100'."""
|
|
28
|
+
|
|
29
|
+
actual: str | None = None
|
|
30
|
+
"""What was found, e.g. 'int', 'length: 245'."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class ValidationWarning:
|
|
35
|
+
"""A non-blocking issue. Load continues but the caller should know."""
|
|
36
|
+
|
|
37
|
+
path: str
|
|
38
|
+
"""Dotted path to the field with the warning."""
|
|
39
|
+
|
|
40
|
+
message: str
|
|
41
|
+
"""Human-readable explanation."""
|
|
42
|
+
|
|
43
|
+
code: str
|
|
44
|
+
"""Machine-readable code: 'missing_optional', 'limit_advisory', 'unknown_field'."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class ValidationResult:
|
|
49
|
+
"""Aggregate result from all validators in the pipeline.
|
|
50
|
+
|
|
51
|
+
is_valid() returns True if there are zero blocking errors.
|
|
52
|
+
is_clean() returns True if there are no errors AND no warnings.
|
|
53
|
+
merge() combines results from multiple validators into one.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
errors: list[ValidationError] = field(default_factory=list)
|
|
57
|
+
"""Blocking errors. The load cannot proceed until these are fixed."""
|
|
58
|
+
|
|
59
|
+
warnings: list[ValidationWarning] = field(default_factory=list)
|
|
60
|
+
"""Non-blocking warnings. The load proceeds but the caller sees these."""
|
|
61
|
+
|
|
62
|
+
def is_valid(self) -> bool:
|
|
63
|
+
"""True if no blocking errors. Warnings don't block."""
|
|
64
|
+
return len(self.errors) == 0
|
|
65
|
+
|
|
66
|
+
def is_clean(self) -> bool:
|
|
67
|
+
"""True if no errors AND no warnings."""
|
|
68
|
+
return len(self.errors) == 0 and len(self.warnings) == 0
|
|
69
|
+
|
|
70
|
+
def merge(self, other: ValidationResult) -> ValidationResult:
|
|
71
|
+
"""Combine results from multiple validators."""
|
|
72
|
+
return ValidationResult(
|
|
73
|
+
errors=self.errors + other.errors,
|
|
74
|
+
warnings=self.warnings + other.warnings,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def add_error(self, path: str, message: str, code: str,
|
|
78
|
+
expected: str | None = None, actual: str | None = None) -> None:
|
|
79
|
+
"""Convenience: add a blocking error."""
|
|
80
|
+
self.errors.append(ValidationError(
|
|
81
|
+
path=path, message=message, code=code,
|
|
82
|
+
expected=expected, actual=actual,
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
def add_warning(self, path: str, message: str, code: str) -> None:
|
|
86
|
+
"""Convenience: add a non-blocking warning."""
|
|
87
|
+
self.warnings.append(ValidationWarning(
|
|
88
|
+
path=path, message=message, code=code,
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
def summary(self) -> str:
|
|
92
|
+
"""One-line summary for logging."""
|
|
93
|
+
parts = []
|
|
94
|
+
if self.errors:
|
|
95
|
+
parts.append(f"{len(self.errors)} error(s)")
|
|
96
|
+
if self.warnings:
|
|
97
|
+
parts.append(f"{len(self.warnings)} warning(s)")
|
|
98
|
+
if not parts:
|
|
99
|
+
return "valid"
|
|
100
|
+
return ", ".join(parts)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Schema validator -- validates FRAME YAML data against JSON Schema definitions.
|
|
2
|
+
|
|
3
|
+
Uses jsonschema library. Fails on type errors, missing required fields, enum
|
|
4
|
+
violations, const violations, and unknown fields rejected by closed schema
|
|
5
|
+
objects.
|
|
6
|
+
|
|
7
|
+
Cross-file $ref links (./frame.schema.json) are resolved locally from the
|
|
8
|
+
schemas/json/ directory -- no network requests.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
from jsonschema import Draft202012Validator
|
|
18
|
+
from jsonschema.exceptions import ValidationError as JsonschemaError
|
|
19
|
+
from referencing import Registry, Resource
|
|
20
|
+
|
|
21
|
+
from framesdkpy.validators.result import ValidationResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Schema loading with local $ref resolution
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _schemas_dir() -> Path:
|
|
29
|
+
"""Locate the schemas directory bundled with the package."""
|
|
30
|
+
this_file = Path(__file__).resolve()
|
|
31
|
+
# frame/validators/schema_validator.py → frame/validators → frame/
|
|
32
|
+
package_root = this_file.parent.parent
|
|
33
|
+
schemas = package_root / "schemas"
|
|
34
|
+
if not schemas.is_dir():
|
|
35
|
+
raise FileNotFoundError(
|
|
36
|
+
f"FRAME schemas directory not found at {schemas}. "
|
|
37
|
+
f"Expected framesdkpy/schemas/ in the FrameSDK package."
|
|
38
|
+
)
|
|
39
|
+
return schemas
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Pre-load all schemas into a local registry so cross-file $ref links resolve
|
|
43
|
+
# without network requests. Each schema gets registered under its $id URI.
|
|
44
|
+
def _build_registry() -> Registry:
|
|
45
|
+
"""Load all 6 JSON schemas into a referencing.Registry for local $ref resolution."""
|
|
46
|
+
resources: list[tuple[str, Resource]] = []
|
|
47
|
+
schemas_dir = _schemas_dir()
|
|
48
|
+
for schema_file in sorted(schemas_dir.glob("*.schema.json")):
|
|
49
|
+
schema = json.loads(schema_file.read_text())
|
|
50
|
+
schema_uri = schema.get("$id", "")
|
|
51
|
+
if schema_uri:
|
|
52
|
+
resources.append((schema_uri, Resource.from_contents(schema)))
|
|
53
|
+
return Registry().with_resources(resources)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Build once at module load time
|
|
57
|
+
_SCHEMA_REGISTRY = _build_registry()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_validator(file_stem: str) -> Draft202012Validator:
|
|
61
|
+
"""Load a FRAME JSON Schema and return a validator with local $ref resolution."""
|
|
62
|
+
schema_path = _schemas_dir() / f"{file_stem}.schema.json"
|
|
63
|
+
if not schema_path.exists():
|
|
64
|
+
raise FileNotFoundError(f"Schema file not found: {schema_path}")
|
|
65
|
+
schema = json.loads(schema_path.read_text())
|
|
66
|
+
return Draft202012Validator(schema, registry=_SCHEMA_REGISTRY)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Validation
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_against_schema(data: dict, file_stem: str) -> ValidationResult:
|
|
75
|
+
"""Validate FRAME data against its JSON Schema.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: Clean dict from the translator (YAML quirks already resolved).
|
|
79
|
+
file_stem: One of 'facts', 'rules', 'map', 'expect', 'acts'.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
ValidationResult with errors for type/enum/required violations,
|
|
83
|
+
warnings for missing optional fields.
|
|
84
|
+
"""
|
|
85
|
+
result = ValidationResult()
|
|
86
|
+
validator = _load_validator(file_stem)
|
|
87
|
+
|
|
88
|
+
# Collect errors manually so we can classify by code
|
|
89
|
+
for e in validator.iter_errors(data):
|
|
90
|
+
path = ".".join(str(p) for p in e.absolute_path) if e.absolute_path else "$"
|
|
91
|
+
code = _map_error_code(e)
|
|
92
|
+
|
|
93
|
+
if code in (
|
|
94
|
+
"missing_required",
|
|
95
|
+
"type_error",
|
|
96
|
+
"enum_error",
|
|
97
|
+
"const_error",
|
|
98
|
+
"unknown_field",
|
|
99
|
+
"schema_error",
|
|
100
|
+
):
|
|
101
|
+
result.add_error(
|
|
102
|
+
path=path or file_stem,
|
|
103
|
+
message=e.message,
|
|
104
|
+
code=code,
|
|
105
|
+
expected=str(e.validator_value) if e.validator_value else None,
|
|
106
|
+
actual=str(e.instance)[:200] if e.instance is not None else None,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
result.add_warning(path=path or file_stem, message=e.message, code=code)
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _map_error_code(error: JsonschemaError) -> str:
|
|
115
|
+
"""Map a jsonschema error to our validation code system."""
|
|
116
|
+
validator = error.validator
|
|
117
|
+
if validator == "required":
|
|
118
|
+
return "missing_required"
|
|
119
|
+
if validator in ("type", "pattern", "format"):
|
|
120
|
+
return "type_error"
|
|
121
|
+
if validator == "enum":
|
|
122
|
+
return "enum_error"
|
|
123
|
+
if validator == "const":
|
|
124
|
+
return "const_error"
|
|
125
|
+
if validator == "additionalProperties":
|
|
126
|
+
return "unknown_field"
|
|
127
|
+
if validator == "maxLength":
|
|
128
|
+
return "limit_exceeded"
|
|
129
|
+
return "schema_error" # Catch-all for unexpected validation failures
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Convenience: validate a YAML file directly
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_yaml_file(file_path: str | Path) -> ValidationResult:
|
|
138
|
+
"""Validate a single YAML FRAME file against its schema.
|
|
139
|
+
|
|
140
|
+
Parses YAML, normalizes quirks, then validates.
|
|
141
|
+
"""
|
|
142
|
+
path = Path(file_path)
|
|
143
|
+
yaml_data = yaml.safe_load(path.read_text())
|
|
144
|
+
if not yaml_data or not isinstance(yaml_data, dict):
|
|
145
|
+
result = ValidationResult()
|
|
146
|
+
result.add_error(str(path), "File is empty or not a YAML object", "type_error")
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
stem = path.stem # 'facts' from 'facts.yaml'
|
|
150
|
+
from framesdkpy.translators.normalizer import normalize_dict
|
|
151
|
+
clean = normalize_dict(yaml_data)
|
|
152
|
+
return validate_against_schema(clean, stem)
|
|
@@ -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,29 @@
|
|
|
1
|
+
framesdkpy/__init__.py,sha256=O6zfAF_uI344COwKg_KSQtsPWe0LhufRmbR1C3vTVAE,882
|
|
2
|
+
framesdkpy/loaders/__init__.py,sha256=L7lo1Z-AKuMaJ32viOE-2yfrRs9KOTUKucKsQChTAAI,2584
|
|
3
|
+
framesdkpy/loaders/assembler.py,sha256=UZm25BvnvJxPYjX111lwFTjUiiTrwdMZAhA0sKITn_Q,8374
|
|
4
|
+
framesdkpy/loaders/yaml_reader.py,sha256=ZLAaJK2XjCBT7xeu6z0lM1xWv7FJUmoza9qP5KVTiyM,2012
|
|
5
|
+
framesdkpy/models/__init__.py,sha256=GmRM-g-MS4Xkjc8DMMRytmg47JS8mOsLSH9fv5Jqa30,1657
|
|
6
|
+
framesdkpy/models/acts_model.py,sha256=ZlrYagsp1NBgR88Oa2wIWiZciyjwNTc7Wa7mGoEf5Fo,4641
|
|
7
|
+
framesdkpy/models/base.py,sha256=_5HIzG72Z-ojh52IGVx18Vo-3pjnlQe7pYZaOFJ2MIY,2505
|
|
8
|
+
framesdkpy/models/expect_model.py,sha256=JsgthOvyuvKPqXH9DdYs1l3Kizq0T3dC66FCj1Avh04,4077
|
|
9
|
+
framesdkpy/models/facts_model.py,sha256=rarijLCKdXOwxu1ysOFfoVTHA0XYNt6AUAhQkbayoko,5241
|
|
10
|
+
framesdkpy/models/frame_model.py,sha256=sT0RvMBaAmJeLycQgU__vmMCgWtpXfxbayZAPWyUMP0,1466
|
|
11
|
+
framesdkpy/models/map_model.py,sha256=-_NT9esHzHicsnXAwVVvLzjIPCcfV6WokcTBDHZW_Mg,4889
|
|
12
|
+
framesdkpy/models/rules_model.py,sha256=-OAh8nUXzDi_M_rU61kEYk3Ol8x9JgKATOn1BwuMw3o,5511
|
|
13
|
+
framesdkpy/schemas/acts.schema.json,sha256=SaRwbOS3FAuJg1gby54nX55QW5t2nHvoekiMkjo0bso,4228
|
|
14
|
+
framesdkpy/schemas/expect.schema.json,sha256=rbXJHSL7j898zafkGlDVwtxLeLixZ3DKnRl2BOCOF5o,3766
|
|
15
|
+
framesdkpy/schemas/facts.schema.json,sha256=As5SY7bTYxds8VoNExMkMx41yTSc4cTloMGmiuNR_xY,6011
|
|
16
|
+
framesdkpy/schemas/frame.schema.json,sha256=MaF3dp5TPUL-wlS3FGheWNTC7lmqJTjFS-KJXzinGpE,3211
|
|
17
|
+
framesdkpy/schemas/map.schema.json,sha256=jOTyE6fQ2MsB04t4vrsy3jE9QgLDsKG-EHwtL7bRRZY,4154
|
|
18
|
+
framesdkpy/schemas/rules.schema.json,sha256=C2sRt3__uglo9JuL6UUJ9dinI84xsxTNGisckwqnOiQ,4941
|
|
19
|
+
framesdkpy/translators/__init__.py,sha256=GWVGQMsR1ipn0wUmvp_MagC2pvMAx5I9ulS6fINsJG0,653
|
|
20
|
+
framesdkpy/translators/normalizer.py,sha256=EE5OO2974trhezVx6kt1m8aBTn7strLoLUAN8VN0QFM,3710
|
|
21
|
+
framesdkpy/translators/yaml_to_json.py,sha256=k_TxQ3FBjGRcRJzP7qPOgU0YzyJwQhc1i-YO-ml4NmY,2893
|
|
22
|
+
framesdkpy/validators/__init__.py,sha256=Ac4v6hEiJK8fnFKTrOg5aIVGOn72Gt80aJiwu8DKaaU,2406
|
|
23
|
+
framesdkpy/validators/cross_file_validator.py,sha256=K7IRhSBPQaEZDG6cM_gwYwKCQZuVRObw_LwJeqpTvqU,2738
|
|
24
|
+
framesdkpy/validators/limits_validator.py,sha256=q8FNWRGdKQ5x4iUokZCOdgCMVfiVXe4sPv11n33xXu8,6817
|
|
25
|
+
framesdkpy/validators/result.py,sha256=UPc8LUUnka84TqWjUAyXr9Q3IjJNrB7jc4t_1g1mC-8,3306
|
|
26
|
+
framesdkpy/validators/schema_validator.py,sha256=fVUw0nzt3KkFo6_CdVCLGDf8iATtEArNZ2quPOwZy34,5581
|
|
27
|
+
framesdkpy-0.3.0.dist-info/METADATA,sha256=BO3erz3B60pA1SPW4KlKqxQxl9EOf3UEQbhY7Q2OwcU,3707
|
|
28
|
+
framesdkpy-0.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
29
|
+
framesdkpy-0.3.0.dist-info/RECORD,,
|