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,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://frame.dev/schemas/0.3.0/map.schema.json",
|
|
4
|
+
"title": "FRAME Map",
|
|
5
|
+
"description": "Where things live -- roots, groups, paths, entrypoints, and managed paths.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["frame"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"frame": {
|
|
11
|
+
"allOf": [
|
|
12
|
+
{ "$ref": "./frame.schema.json#/$defs/frame_header" },
|
|
13
|
+
{
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"file": { "const": "map" },
|
|
17
|
+
"role": { "const": "repo_context_map" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"structure": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"maxLength": 800,
|
|
25
|
+
"description": "Quick visual overview of repo layout. Flat text: 'Backend/ → FastAPI (routes→services→models), Frontend/ → Vue 3 (views→stores→services)'."
|
|
26
|
+
},
|
|
27
|
+
"roots": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": true,
|
|
30
|
+
"description": "Top-level directory purposes. Keys are directory names."
|
|
31
|
+
},
|
|
32
|
+
"groups": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"description": "Logical groupings of paths. Supports wildcards for flexible coverage.",
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": ["id", "label", "paths"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
41
|
+
"label": { "type": "string", "maxLength": 150 },
|
|
42
|
+
"paths": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": { "type": "string", "maxLength": 300 },
|
|
45
|
+
"description": "Wildcards allowed (e.g. 'Backend/app/**/*.py')."
|
|
46
|
+
},
|
|
47
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"paths": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"description": "Critical individual files. Explicit paths only.",
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["path", "purpose"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
60
|
+
"path": { "type": "string", "maxLength": 200 },
|
|
61
|
+
"purpose": { "type": "string", "maxLength": 300 },
|
|
62
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"entrypoints": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"description": "CLI/API/web entry points. Explicit paths only.",
|
|
69
|
+
"items": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"required": ["id", "path", "kind"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
75
|
+
"path": { "type": "string", "maxLength": 200 },
|
|
76
|
+
"kind": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["cli", "api", "web", "script"]
|
|
79
|
+
},
|
|
80
|
+
"description": { "type": "string", "maxLength": 300 },
|
|
81
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"managed_paths": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"description": "Paths under special rules. Supports wildcards.",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"required": ["path", "rule"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
94
|
+
"path": { "type": "string", "maxLength": 200, "description": "Wildcards allowed (e.g. 'node_modules/**', '*.pyc')." },
|
|
95
|
+
"rule": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"enum": ["generated", "config", "immutable"]
|
|
98
|
+
},
|
|
99
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"unmapped_paths": {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"description": "Paths not yet placed in the map. Honest about gaps.",
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"required": ["path", "reason"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"path": { "type": "string", "maxLength": 200 },
|
|
112
|
+
"reason": { "type": "string", "maxLength": 200 }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"evidence": { "$ref": "./frame.schema.json#/$defs/evidence_list" },
|
|
117
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://frame.dev/schemas/0.3.0/rules.schema.json",
|
|
4
|
+
"title": "FRAME Rules",
|
|
5
|
+
"description": "How to work safely in this repo. Commands, constraints, policies, and triggers that need human approval.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["frame"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"frame": {
|
|
11
|
+
"allOf": [
|
|
12
|
+
{ "$ref": "./frame.schema.json#/$defs/frame_header" },
|
|
13
|
+
{
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"file": { "const": "rules" },
|
|
17
|
+
"role": { "const": "project_instruction_blueprint" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"governance_level": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": ["relaxed", "normal", "strict"],
|
|
25
|
+
"default": "normal",
|
|
26
|
+
"description": "Controls how aggressively Haxaml enforces rules, ask_first, and validation. Does NOT affect mechanical validator (always runs)."
|
|
27
|
+
},
|
|
28
|
+
"rules": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"description": "Core behavioral constraints.",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["id", "rule"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
37
|
+
"rule": { "type": "string", "maxLength": 500 },
|
|
38
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"policies": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "Durable project policies (role access, lifecycle, audit, auth). Moved from Facts.",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["id", "name", "rule"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
51
|
+
"name": { "type": "string", "maxLength": 150 },
|
|
52
|
+
"rule": { "type": "string", "maxLength": 500 },
|
|
53
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"commands": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Named shell commands. Keys are command names used by command_ref.",
|
|
60
|
+
"additionalProperties": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"required": ["run", "kind", "purpose"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"run": { "type": "string", "maxLength": 500, "description": "Shell command to execute." },
|
|
66
|
+
"kind": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": ["setup", "verify", "run"],
|
|
69
|
+
"description": "setup=install deps, verify=validation check, run=server/interactive."
|
|
70
|
+
},
|
|
71
|
+
"purpose": { "type": "string", "maxLength": 300 },
|
|
72
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"code_style": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": true,
|
|
79
|
+
"description": "Formatting, naming, conventions. Advisory limit: 1000 chars total."
|
|
80
|
+
},
|
|
81
|
+
"git": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"additionalProperties": true,
|
|
84
|
+
"description": "Branch strategy, commit style, PR rules. Advisory limit: 1000 chars total."
|
|
85
|
+
},
|
|
86
|
+
"donts": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"description": "Things you must never do.",
|
|
89
|
+
"items": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"required": ["id", "rule"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
95
|
+
"rule": { "type": "string", "maxLength": 300 },
|
|
96
|
+
"severity": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"enum": ["critical", "warning"],
|
|
99
|
+
"default": "critical"
|
|
100
|
+
},
|
|
101
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"ask_first": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"description": "Triggers that need human approval before the agent proceeds.",
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"required": ["id", "trigger_type", "trigger", "reason"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
114
|
+
"trigger_type": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"enum": ["file_pattern", "task_pattern"]
|
|
117
|
+
},
|
|
118
|
+
"trigger": { "type": "string", "maxLength": 300 },
|
|
119
|
+
"reason": { "type": "string", "maxLength": 300 },
|
|
120
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"hints": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"description": "Skill references, known gotchas, task-specific guidance.",
|
|
127
|
+
"items": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"additionalProperties": false,
|
|
130
|
+
"required": ["id", "hint"],
|
|
131
|
+
"properties": {
|
|
132
|
+
"id": { "$ref": "./frame.schema.json#/$defs/stable_id" },
|
|
133
|
+
"hint": { "type": "string", "maxLength": 300 }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"evidence": { "$ref": "./frame.schema.json#/$defs/evidence_list" },
|
|
138
|
+
"links": { "$ref": "./frame.schema.json#/$defs/links" }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""FRAME translators -- YAML/JSON conversion with normalization.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
translate_file("facts.yaml") → dict
|
|
5
|
+
translate_directory(".haxaml/") → {"facts": {...}, "rules": {...}, ...}
|
|
6
|
+
translate_to_dict(yaml_string) → dict
|
|
7
|
+
translate_to_json_string(yaml) → str
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from framesdkpy.translators.yaml_to_json import (
|
|
11
|
+
translate_file,
|
|
12
|
+
translate_directory,
|
|
13
|
+
translate_to_dict,
|
|
14
|
+
translate_to_json_string,
|
|
15
|
+
)
|
|
16
|
+
from framesdkpy.translators.normalizer import TranslationError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"translate_file",
|
|
20
|
+
"translate_directory",
|
|
21
|
+
"translate_to_dict",
|
|
22
|
+
"translate_to_json_string",
|
|
23
|
+
"TranslationError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""YAML type normalizer -- resolves YAML quirks into clean JSON-compatible types.
|
|
2
|
+
|
|
3
|
+
Handles the full YAML 1.2 quirk table defined in the translators spec:
|
|
4
|
+
yes/no → True/False
|
|
5
|
+
~/null → None
|
|
6
|
+
Date-like strings preserved as strings
|
|
7
|
+
Empty strings and empty fields handled correctly
|
|
8
|
+
|
|
9
|
+
Raises TranslationError on ambiguous input (on/off).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
from datetime import date, datetime
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TranslationError(ValueError):
|
|
19
|
+
"""Raised when YAML input is ambiguous and cannot be safely translated."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, path: str, value: Any, reason: str):
|
|
22
|
+
self.path = path
|
|
23
|
+
self.value = value
|
|
24
|
+
self.reason = reason
|
|
25
|
+
super().__init__(f"{path}: {reason} (got {value!r})")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_yaml_value(value: Any, path: str = "$") -> Any:
|
|
29
|
+
"""Resolve a single YAML value to a JSON-compatible type.
|
|
30
|
+
|
|
31
|
+
Recursively handles dicts and lists. The path argument tracks position
|
|
32
|
+
for error messages -- use normalize_dict() for top-level entry.
|
|
33
|
+
"""
|
|
34
|
+
# Dict: recurse into values, tracking keys for error paths
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
result: dict[str, Any] = {}
|
|
37
|
+
for k, v in value.items():
|
|
38
|
+
result[k] = normalize_yaml_value(v, f"{path}.{k}")
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
# List: recurse into items
|
|
42
|
+
if isinstance(value, list):
|
|
43
|
+
return [normalize_yaml_value(item, f"{path}[{i}]") for i, item in enumerate(value)]
|
|
44
|
+
|
|
45
|
+
# None / null
|
|
46
|
+
if value is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# Date/datetime: PyYAML SafeLoader parses bare YYYY-MM-DD scalars into
|
|
50
|
+
# date objects. FRAME preserves dates as strings so JSON output is stable
|
|
51
|
+
# across languages and serializers.
|
|
52
|
+
if isinstance(value, datetime):
|
|
53
|
+
return value.isoformat()
|
|
54
|
+
if isinstance(value, date):
|
|
55
|
+
return value.isoformat()
|
|
56
|
+
|
|
57
|
+
# String: check for YAML quirks
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
return _normalize_string(value, path)
|
|
60
|
+
|
|
61
|
+
# Numbers and booleans pass through as-is (YAML already parsed them correctly)
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalize_string(value: str, path: str) -> Any:
|
|
66
|
+
"""Normalize a YAML string -- handle yes/no, null, empty, date-like."""
|
|
67
|
+
|
|
68
|
+
# Empty string: preserve as-is (not coerced to null)
|
|
69
|
+
if value == "":
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
# Explicit null values
|
|
73
|
+
if value.lower() in ("null", "~"):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
# YAML 1.2 booleans (only yes/no -- on/off are ambiguous and rejected)
|
|
77
|
+
if value.lower() in ("yes", "true", "y"):
|
|
78
|
+
return True
|
|
79
|
+
if value.lower() in ("no", "false", "n"):
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# Ambiguous: on/off are YAML 1.1 booleans. YAML 1.2 doesn't treat them as
|
|
83
|
+
# booleans, but some parsers do. We fail loudly rather than guessing.
|
|
84
|
+
if value.lower() in ("on", "off"):
|
|
85
|
+
raise TranslationError(
|
|
86
|
+
path, value,
|
|
87
|
+
"'on'/'off' is ambiguous in YAML. Use explicit 'true' or 'false' or quote as string.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Date-like strings (YYYY-MM-DD, YYYY/MM/DD): YAML parsers may convert these
|
|
91
|
+
# to datetime objects. We preserve them as strings for FRAME consistency.
|
|
92
|
+
# The caller's YAML loader should use a loader that doesn't auto-parse dates.
|
|
93
|
+
# If a datetime slips through (parsed by the YAML lib), we catch it above in
|
|
94
|
+
# the type check -- datetimes are normalized back to strings before JSON/schema validation.
|
|
95
|
+
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""Normalize an entire YAML dict. Entry point for the translator."""
|
|
101
|
+
return normalize_yaml_value(data, "$")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalize_list(data: list[Any]) -> list[Any]:
|
|
105
|
+
"""Normalize a YAML list. Convenience for array-only files."""
|
|
106
|
+
return normalize_yaml_value(data, "$")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""YAML to JSON translator -- reads FRAME YAML files and produces clean JSON.
|
|
2
|
+
|
|
3
|
+
Uses the normalizer to resolve YAML quirks, then validates the output against
|
|
4
|
+
the JSON Schema shape. Returns a clean dict ready for the validator or assembler.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from framesdkpy.translators.normalizer import normalize_dict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Use PyYAML SafeLoader, then normalize parsed values into JSON-compatible FRAME values.
|
|
18
|
+
# This prevents YAML quirks from reaching the normalizer.
|
|
19
|
+
_YAML_LOADER = yaml.SafeLoader
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def translate_to_dict(yaml_string: str) -> dict:
|
|
23
|
+
"""Parse a YAML string into a clean JSON-compatible dict.
|
|
24
|
+
|
|
25
|
+
Handles all YAML to JSON normalization per the translators spec.
|
|
26
|
+
Raises TranslationError on ambiguous input.
|
|
27
|
+
"""
|
|
28
|
+
raw = yaml.load(yaml_string, Loader=_YAML_LOADER)
|
|
29
|
+
if raw is None:
|
|
30
|
+
return {}
|
|
31
|
+
if not isinstance(raw, dict):
|
|
32
|
+
raise ValueError(f"Expected YAML object, got {type(raw).__name__}")
|
|
33
|
+
return normalize_dict(raw)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def translate_file(file_path: str | Path) -> dict:
|
|
37
|
+
"""Read a YAML file and translate to a clean JSON-compatible dict.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
file_path: Path to a .yaml FRAME file (facts.yaml, rules.yaml, etc.)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Clean dict with all YAML quirks resolved.
|
|
44
|
+
"""
|
|
45
|
+
path = Path(file_path)
|
|
46
|
+
if not path.exists():
|
|
47
|
+
raise FileNotFoundError(f"FRAME file not found: {path}")
|
|
48
|
+
if path.suffix not in (".yaml", ".yml"):
|
|
49
|
+
raise ValueError(f"Expected .yaml file, got {path.suffix}: {path}")
|
|
50
|
+
|
|
51
|
+
yaml_string = path.read_text()
|
|
52
|
+
return translate_to_dict(yaml_string)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def translate_directory(dir_path: str | Path) -> dict[str, dict]:
|
|
56
|
+
"""Read all 5 FRAME YAML files from a directory.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
dir_path: Directory containing facts.yaml, rules.yaml, map.yaml,
|
|
60
|
+
expect.yaml, acts.yaml.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dict mapping file stem to translated dict, e.g.:
|
|
64
|
+
{"facts": {...}, "rules": {...}, "map": {...}, "expect": {...}, "acts": {...}}
|
|
65
|
+
"""
|
|
66
|
+
directory = Path(dir_path)
|
|
67
|
+
if not directory.is_dir():
|
|
68
|
+
raise FileNotFoundError(f"FRAME directory not found: {directory}")
|
|
69
|
+
|
|
70
|
+
expected_files = ["facts", "rules", "map", "expect", "acts"]
|
|
71
|
+
result: dict[str, dict] = {}
|
|
72
|
+
|
|
73
|
+
for stem in expected_files:
|
|
74
|
+
yaml_file = directory / f"{stem}.yaml"
|
|
75
|
+
if not yaml_file.exists():
|
|
76
|
+
raise FileNotFoundError(
|
|
77
|
+
f"Missing FRAME file: {stem}.yaml in {directory}. "
|
|
78
|
+
f"All 5 files must be in the same directory."
|
|
79
|
+
)
|
|
80
|
+
result[stem] = translate_file(yaml_file)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def translate_to_json_string(yaml_string: str, indent: int = 2) -> str:
|
|
86
|
+
"""Parse YAML and return a JSON string directly."""
|
|
87
|
+
data = translate_to_dict(yaml_string)
|
|
88
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""FRAME validators -- schema, character limits, and cross-file consistency.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
validate_file("facts.yaml") → ValidationResult (single file)
|
|
5
|
+
validate_frame(".haxaml/") → ValidationResult (all 5 + cross-file)
|
|
6
|
+
validate_against_schema(data, "facts") → ValidationResult (in-memory)
|
|
7
|
+
validate_limits(data, "facts") → ValidationResult (in-memory)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from framesdkpy.validators.result import ValidationResult, ValidationError, ValidationWarning
|
|
11
|
+
from framesdkpy.validators.schema_validator import validate_against_schema, validate_yaml_file
|
|
12
|
+
from framesdkpy.validators.limits_validator import validate_limits
|
|
13
|
+
from framesdkpy.validators.cross_file_validator import validate_cross_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_file(file_path: str) -> ValidationResult:
|
|
17
|
+
"""Validate a single FRAME YAML file against schema + limits.
|
|
18
|
+
|
|
19
|
+
Does NOT run cross-file checks (only one file).
|
|
20
|
+
"""
|
|
21
|
+
result = validate_yaml_file(file_path)
|
|
22
|
+
|
|
23
|
+
# Also run limits validation
|
|
24
|
+
import yaml
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from framesdkpy.translators.normalizer import normalize_dict
|
|
27
|
+
|
|
28
|
+
path = Path(file_path)
|
|
29
|
+
raw = yaml.safe_load(path.read_text())
|
|
30
|
+
if raw and isinstance(raw, dict):
|
|
31
|
+
clean = normalize_dict(raw)
|
|
32
|
+
limits_result = validate_limits(clean, path.stem)
|
|
33
|
+
result = result.merge(limits_result)
|
|
34
|
+
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_frame(dir_path: str) -> ValidationResult:
|
|
39
|
+
"""Validate all 5 FRAME files in a directory + cross-file consistency.
|
|
40
|
+
|
|
41
|
+
Steps:
|
|
42
|
+
1. Schema validation for each file
|
|
43
|
+
2. Character limit validation for each file
|
|
44
|
+
3. Cross-file consistency (versions, file/role matching)
|
|
45
|
+
"""
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from framesdkpy.translators.yaml_to_json import translate_directory
|
|
48
|
+
|
|
49
|
+
# Translate all 5 files to clean dicts
|
|
50
|
+
parts = translate_directory(dir_path)
|
|
51
|
+
|
|
52
|
+
result = ValidationResult()
|
|
53
|
+
|
|
54
|
+
# Schema + limits for each file
|
|
55
|
+
for stem, data in parts.items():
|
|
56
|
+
result = result.merge(validate_against_schema(data, stem))
|
|
57
|
+
result = result.merge(validate_limits(data, stem))
|
|
58
|
+
|
|
59
|
+
# Cross-file consistency
|
|
60
|
+
result = result.merge(validate_cross_file(parts))
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"ValidationResult",
|
|
67
|
+
"ValidationError",
|
|
68
|
+
"ValidationWarning",
|
|
69
|
+
"validate_file",
|
|
70
|
+
"validate_frame",
|
|
71
|
+
"validate_against_schema",
|
|
72
|
+
"validate_limits",
|
|
73
|
+
"validate_cross_file",
|
|
74
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Cross-file validator -- checks consistency across all 5 FRAME files.
|
|
2
|
+
|
|
3
|
+
Verifies:
|
|
4
|
+
- All schema_version fields match
|
|
5
|
+
- All file/role fields match expected values (facts file must have file: facts)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from framesdkpy.validators.result import ValidationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_EXPECTED_FILE_MAP = {
|
|
14
|
+
"facts": "facts",
|
|
15
|
+
"rules": "rules",
|
|
16
|
+
"map": "map",
|
|
17
|
+
"expect": "expect",
|
|
18
|
+
"acts": "acts",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_EXPECTED_ROLE_MAP = {
|
|
22
|
+
"facts": "current_project_truth",
|
|
23
|
+
"rules": "project_instruction_blueprint",
|
|
24
|
+
"map": "repo_context_map",
|
|
25
|
+
"expect": "project_correctness_contract",
|
|
26
|
+
"acts": "checked_activity_record",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_cross_file(parts: dict[str, dict]) -> ValidationResult:
|
|
31
|
+
"""Check cross-file consistency across all 5 translated FRAME parts.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
parts: Dict mapping file stem to translated dict.
|
|
35
|
+
e.g., {"facts": {...}, "rules": {...}, ...}
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ValidationResult with errors for mismatched versions or file/role fields.
|
|
39
|
+
"""
|
|
40
|
+
result = ValidationResult()
|
|
41
|
+
|
|
42
|
+
# Gather schema_version from every file that has a frame header
|
|
43
|
+
versions: dict[str, str] = {}
|
|
44
|
+
for stem, data in parts.items():
|
|
45
|
+
header = data.get("frame", {})
|
|
46
|
+
version = header.get("schema_version")
|
|
47
|
+
if version:
|
|
48
|
+
versions[stem] = version
|
|
49
|
+
|
|
50
|
+
# All present schema_versions must match
|
|
51
|
+
if len(set(versions.values())) > 1:
|
|
52
|
+
details = ", ".join(f"{s}={v}" for s, v in versions.items())
|
|
53
|
+
result.add_error(
|
|
54
|
+
path="frame.schema_version",
|
|
55
|
+
message=f"Schema version mismatch across files: {details}",
|
|
56
|
+
code="schema_version_mismatch",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Each file's 'file' and 'role' fields must match expected values
|
|
60
|
+
for stem, data in parts.items():
|
|
61
|
+
header = data.get("frame", {})
|
|
62
|
+
|
|
63
|
+
expected_file = _EXPECTED_FILE_MAP.get(stem)
|
|
64
|
+
actual_file = header.get("file")
|
|
65
|
+
if expected_file and actual_file and actual_file != expected_file:
|
|
66
|
+
result.add_error(
|
|
67
|
+
path=f"{stem}.frame.file",
|
|
68
|
+
message=f"File field mismatch in {stem}.yaml",
|
|
69
|
+
code="file_role_mismatch",
|
|
70
|
+
expected=expected_file,
|
|
71
|
+
actual=actual_file,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expected_role = _EXPECTED_ROLE_MAP.get(stem)
|
|
75
|
+
actual_role = header.get("role")
|
|
76
|
+
if expected_role and actual_role and actual_role != expected_role:
|
|
77
|
+
result.add_error(
|
|
78
|
+
path=f"{stem}.frame.role",
|
|
79
|
+
message=f"Role field mismatch in {stem}.yaml",
|
|
80
|
+
code="file_role_mismatch",
|
|
81
|
+
expected=expected_role,
|
|
82
|
+
actual=actual_role,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return result
|