feat(schema): persist and instantiate scoped multi-role relation schemas
ci / python (pull_request) Successful in 17s
ci / lean (pull_request) Successful in 1m6s
ci / image (pull_request) Has been skipped

This commit is contained in:
2026-10-28 21:26:55 +08:00
parent 7d2c6f470b
commit 29fe3e1aa5
10 changed files with 2068 additions and 13 deletions
+59 -10
View File
@@ -1,4 +1,4 @@
"""Roles and simplices: the parameters of a declaration as a small typed schema.
"""Semantic multi-role relation schemas, not geometric simplices.
A Lean declaration is a bundle of roles: the objects it is about (``(G : Type*)``),
the facts it assumes (``(h : a ∣ b)``), and the goal it asserts or the thing it
@@ -8,8 +8,8 @@ be read off the prose of a book, so the two can be compared role by role.
Design notes (from the archived ``schema-simplex`` prototype):
* a role may be *constrained* to a closed set of allowed values (``choices``);
* a simplex is itself a role, so simplices nest (a lemma used by another is a role
of it), which is how a dependency structure appears;
* a simplex is itself a role, so explicitly supplied schemas can be composed;
nesting does not discover a proof dependency;
* validation lives in ``__init__``. The prototype put it in a dataclass
``__post_init__`` next to a hand-written ``__init__``, where it never ran.
"""
@@ -41,7 +41,7 @@ class Role:
choices: Sequence[Any] | None = None,
attrs: dict[str, Any] | None = None,
) -> None:
if not name:
if not isinstance(name, str) or not name.strip():
raise ValueError("a role needs a name")
if kind not in KINDS:
raise ValueError(f"unknown role kind {kind!r}; expected one of {KINDS}")
@@ -49,6 +49,8 @@ class Role:
self.kind = kind
self.type_text = type_text
self.context = context
if isinstance(choices, (str, bytes)):
raise ValueError("choices must be a value sequence, not a string")
self.choices: list[Any] | None = list(choices) if choices is not None else None
self.attrs: dict[str, Any] = dict(attrs or {})
@@ -83,12 +85,21 @@ class Role:
class Simplex(Role):
"""A reusable bundle of at least two roles, keyed by alias. Itself a role."""
def __init__(self, name: str, roles: dict[str, Role], *, context: str = "") -> None:
super().__init__(name, KIND_SIMPLEX, context=context)
def __init__(self, name: str, roles: dict[str, Role], *, context: str = "",
attrs: dict[str, Any] | None = None) -> None:
super().__init__(name, KIND_SIMPLEX, context=context, attrs=attrs)
if len(roles) < MIN_ROLES:
raise ValueError(
f"simplex {name!r} needs at least {MIN_ROLES} roles, got {len(roles)}"
)
if any(not isinstance(alias, str) or not alias or not isinstance(role, Role)
for alias, role in roles.items()):
raise ValueError("simplex slots need nonempty aliases and Role values")
for role in roles.values():
references = role.attrs.get("references", [])
if (not isinstance(references, list)
or any(not isinstance(alias, str) or alias not in roles for alias in references)):
raise ValueError("role references must name slots in this simplex")
self.roles = dict(roles)
def by_kind(self, kind: str) -> list[Role]:
@@ -105,16 +116,16 @@ class Simplex(Role):
return out
def as_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"roles": {alias: role.as_dict() for alias, role in self.roles.items()},
}
return {**super().as_dict(),
"roles": {alias: role.as_dict() for alias, role in self.roles.items()}}
class State:
"""A role together with a value; unresolved until it has one."""
def __init__(self, role: Role, value: Any = None) -> None:
if value is not None:
role.check_value(value)
self.role = role
self.value = value
@@ -133,3 +144,41 @@ class Fact(State):
class Goal(State):
"""To be resolved."""
def role_from_dict(data: dict[str, Any]) -> Role:
"""Restore JSON role/schema data without executing code or loading pickle."""
kind = data.get("kind", KIND_SIMPLEX if "roles" in data else KIND_OBJECT)
if kind == KIND_SIMPLEX:
return Simplex(data["name"], {alias: role_from_dict(role)
for alias, role in data["roles"].items()},
context=data.get("context", ""), attrs=data.get("attrs"))
return Role(data["name"], kind, type_text=data.get("type", ""),
context=data.get("context", ""), choices=data.get("choices"),
attrs=data.get("attrs"))
class SimplexInstance:
"""Values bind local slot aliases; a resolved goal is supplied, not proved here."""
def __init__(self, schema: Simplex, facts: dict[str, Any] | None = None,
goals: Sequence[str] | None = None) -> None:
self.schema = schema
facts = facts or {}
goal_aliases = list(goals) if goals is not None else [
alias for alias, role in schema.roles.items() if role.kind in (KIND_GOAL, KIND_OWN)]
if (len(set(goal_aliases)) != len(goal_aliases)
or not (set(facts) | set(goal_aliases)) <= set(schema.roles)
or set(facts) & set(goal_aliases)):
raise ValueError("facts/goals need distinct, declared slot aliases")
self.facts = {alias: Fact(schema.roles[alias], value) for alias, value in facts.items()}
self.goals = {alias: Goal(schema.roles[alias]) for alias in goal_aliases}
def resolve(self, alias: str, value: Any) -> None:
if alias not in self.goals or value is None:
raise ValueError("resolve needs a declared goal and a non-None value")
self.goals[alias].role.check_value(value)
self.goals[alias].value = value
def is_completed(self) -> bool:
return bool(self.goals) and all(goal.is_resolved() for goal in self.goals.values())