Skip to content

Commit 36ba813

Browse files
committed
Validate kernel identifiers and constants
Closes #1 Closes #2
1 parent 9a4d2e4 commit 36ba813

6 files changed

Lines changed: 231 additions & 22 deletions

File tree

src/se_kernel/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Application orchestration for se_kernel."""
1+
"""app.py - Application orchestration for se_kernel."""
22

33
from pathlib import Path
44

src/se_kernel/types.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Typed structures for se-kernel artifacts."""
2+
3+
from typing import TypedDict
4+
5+
6+
class ArtifactMeta(TypedDict):
7+
"""Shared artifact metadata."""
8+
9+
version: str
10+
status: str
11+
12+
13+
class IdentifierGlobal(TypedDict):
14+
"""Global identifier rules."""
15+
16+
character_set: str
17+
case: str
18+
separator: str
19+
allow_unicode: bool
20+
21+
22+
class IdentifierKind(TypedDict):
23+
"""Identifier kind definition."""
24+
25+
summary: str
26+
pattern: str
27+
examples: list[str]
28+
29+
30+
class IdentifierRules(TypedDict):
31+
"""Identifier invariant rules."""
32+
33+
must_be_stable: bool
34+
must_be_ascii: bool
35+
must_be_lowercase: bool
36+
must_use_hyphen_separator: bool
37+
must_not_embed_semantics: bool
38+
39+
40+
# WHY: TOML key is global, not global_ and global is a Python keyword.
41+
# For direct loaded TOML, use functional syntax instead:
42+
IdentifiersData = TypedDict(
43+
"IdentifiersData",
44+
{
45+
"meta": ArtifactMeta,
46+
"global": IdentifierGlobal,
47+
"kind": dict[str, IdentifierKind],
48+
"rules": IdentifierRules,
49+
},
50+
)
51+
52+
53+
class ReservedValues(TypedDict):
54+
"""Reserved value list."""
55+
56+
values: list[str]
57+
58+
59+
class ConstantsData(TypedDict):
60+
"""constants.toml structure."""
61+
62+
meta: ArtifactMeta
63+
reserved: dict[str, ReservedValues]
64+
base_types: dict[str, str]

src/se_kernel/validate.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Validation for se-kernel artifacts."""
2+
3+
from se_kernel.types import ConstantsData, IdentifiersData
4+
5+
6+
def validate_identifiers(data: IdentifiersData) -> list[str]:
7+
"""Validate identifiers.toml structure."""
8+
errors: list[str] = []
9+
10+
if not data["kind"]:
11+
errors.append("identifiers.toml: [kind] must not be empty.")
12+
13+
for kind_name, kind_def in data["kind"].items():
14+
if not kind_def.get("summary", "").strip():
15+
errors.append(f"identifiers.toml: [kind.{kind_name}] must define summary.")
16+
17+
if not kind_def.get("pattern", "").strip():
18+
errors.append(f"identifiers.toml: [kind.{kind_name}] must define pattern.")
19+
20+
examples = kind_def.get("examples", [])
21+
if not examples:
22+
errors.append(f"identifiers.toml: [kind.{kind_name}] must define examples.")
23+
24+
return errors
25+
26+
27+
def validate_constants(data: ConstantsData) -> list[str]:
28+
"""Validate constants.toml structure."""
29+
errors: list[str] = []
30+
31+
if not data["reserved"]:
32+
errors.append("constants.toml: [reserved] must not be empty.")
33+
34+
for group_name, group_def in data["reserved"].items():
35+
values = group_def.get("values", [])
36+
if not values:
37+
errors.append(
38+
f"constants.toml: [reserved.{group_name}] must define values."
39+
)
40+
41+
if not data["base_types"]:
42+
errors.append("constants.toml: [base_types] must not be empty.")
43+
44+
return errors

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Minimal CLI smoke tests for se_kernel."""
1+
"""tests/test_cli.py - Minimal CLI smoke tests for se_kernel."""
22

33
from se_kernel.app import run_show
44

tests/test_validate.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Validation tests for se-kernel artifacts."""
2+
3+
from typing import Any, cast
4+
5+
from se_kernel.types import ConstantsData, IdentifiersData
6+
from se_kernel.validate import validate_constants, validate_identifiers
7+
8+
9+
def make_valid_identifiers() -> IdentifiersData:
10+
"""Return valid identifiers data."""
11+
return {
12+
"meta": {"version": "0.1.0", "status": "draft"},
13+
"global": {
14+
"character_set": "ascii-lower-hyphen",
15+
"case": "lower",
16+
"separator": "-",
17+
"allow_unicode": False,
18+
},
19+
"kind": {
20+
"repo": {
21+
"summary": "Canonical repository identifier.",
22+
"pattern": "se-{name}",
23+
"examples": ["se-kernel"],
24+
}
25+
},
26+
"rules": {
27+
"must_be_stable": True,
28+
"must_be_ascii": True,
29+
"must_be_lowercase": True,
30+
"must_use_hyphen_separator": True,
31+
"must_not_embed_semantics": True,
32+
},
33+
}
34+
35+
36+
def make_valid_constants() -> ConstantsData:
37+
"""Return valid constants data."""
38+
return {
39+
"meta": {"version": "0.1.0", "status": "draft"},
40+
"reserved": {
41+
"repo_classes": {"values": ["constitution", "kernel"]},
42+
},
43+
"base_types": {
44+
"identifier": "string",
45+
"string_list": "list[string]",
46+
},
47+
}
48+
49+
50+
def test_validate_identifiers_accepts_valid_data() -> None:
51+
"""Valid identifiers data should produce no errors."""
52+
errors = validate_identifiers(make_valid_identifiers())
53+
assert errors == []
54+
55+
56+
def test_validate_identifiers_requires_kind_entries() -> None:
57+
"""Identifier kinds must not be empty."""
58+
data = make_valid_identifiers()
59+
data["kind"] = {}
60+
61+
errors = validate_identifiers(data)
62+
63+
assert "identifiers.toml: [kind] must not be empty." in errors
64+
65+
66+
def test_validate_identifiers_requires_summary_pattern_and_examples() -> None:
67+
"""Identifier kinds must define summary, pattern, and examples."""
68+
data = cast(dict[str, Any], make_valid_identifiers())
69+
data["kind"]["repo"] = {"summary": "", "pattern": "", "examples": []}
70+
71+
errors = validate_identifiers(cast(IdentifiersData, data))
72+
73+
assert "identifiers.toml: [kind.repo] must define summary." in errors
74+
assert "identifiers.toml: [kind.repo] must define pattern." in errors
75+
assert "identifiers.toml: [kind.repo] must define examples." in errors
76+
77+
78+
def test_validate_constants_accepts_valid_data() -> None:
79+
"""Valid constants data should produce no errors."""
80+
errors = validate_constants(make_valid_constants())
81+
assert errors == []
82+
83+
84+
def test_validate_constants_requires_reserved_values() -> None:
85+
"""Reserved groups must define values."""
86+
data = make_valid_constants()
87+
data["reserved"]["repo_classes"] = {"values": []}
88+
89+
errors = validate_constants(data)
90+
91+
assert "constants.toml: [reserved.repo_classes] must define values." in errors
92+
93+
94+
def test_validate_constants_requires_base_types() -> None:
95+
"""Base types must not be empty."""
96+
data = make_valid_constants()
97+
data["base_types"] = {}
98+
99+
errors = validate_constants(data)
100+
101+
assert "constants.toml: [base_types] must not be empty." in errors

uv.lock

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)