Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/axiom-explicit-rulespec-roots.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Require every `AxiomEngine` caller to provide a non-empty explicit sequence of
canonical RuleSpec roots and forward that exact authority boundary to the
current Axiom dense loader. The adapter no longer relies on the retired
implicit-root interface, and its canonical-layout fixture plus engine-free
regressions pin missing, empty, scalar, and forwarded-root behavior. Root and
module validation errors also propagate instead of being mistaken for an
optional entity with no derived rules.
24 changes: 22 additions & 2 deletions packages/microcosm-frame/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,28 @@ no operator ever re-derives structure or silently corrupts weights:
- **US unit structure**: `assign_us_unit_structure` builds the PolicyEngine
entity systems (tax units delegated to `microunit`, install via
`microcosm-frame[us]`) and returns a validated frame.
- **The `RulesEngine` protocol** plus a lazy `policyengine_us` adapter
(install via `microcosm-frame[policyengine]`).
- **The `RulesEngine` protocol** plus lazy `policyengine_us` (install via
`microcosm-frame[policyengine]`) and Axiom adapters. The Axiom adapter's HDF5
support installs via `microcosm-frame[axiom]`; the engine itself currently
installs from an `axiom-rules-engine` checkout.

The Axiom adapter requires callers to declare the exact RuleSpec authority
roots used for compilation. It does not infer them from the module path,
environment, current directory, or sibling checkouts:

```python
from pathlib import Path

from microcosm.frame.adapters.axiom import AxiomEngine

rulespec_be = Path("/absolute/path/to/rulespec-be")
module = rulespec_be / "be/statutes/income_tax/individual/rate_scale.yaml"
engine = AxiomEngine(module, rulespec_roots=(rulespec_be,))
```

The sequence must be non-empty. On first engine-backed use, Axiom validates
that every supplied root is an absolute, canonical `rulespec-<country>`
checkout and resolves imports only through those roots.

See the repository `DESIGN.md` for the charter and
`tests/test_contracts.py` for the behavioral guarantees the kernel makes.
49 changes: 45 additions & 4 deletions packages/microcosm-frame/src/microcosm/frame/adapters/axiom.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
``Child``, ``Vehicle``, ...) are invisible to the kernel: their variables
resolve and materialize only once a frame entity is mapped to them.

RuleSpec authority roots
------------------------
Filesystem compilation requires a non-empty, explicit sequence of canonical
``rulespec-<country>`` roots. The adapter forwards exactly the caller-supplied
roots to Axiom; it never searches the working directory, environment, module
ancestors, or sibling checkouts. Axiom remains the authority for validating
that each root is absolute, canonical, and structurally valid when the module
is compiled lazily.

Inputs are declared by usage, not typed
---------------------------------------
The dense surface enumerates input *names* per entity but carries no input
Expand Down Expand Up @@ -97,8 +106,11 @@ class AxiomEngine:
Args:
module: Path to the RuleSpec module to compile (e.g.
``rulespec-be/be/statutes/income_tax/individual/rate_scale.yaml``).
Imports resolve through the country-monorepo layout the file
lives in; compilation happens in-process on first engine use.
Compilation happens in-process on first engine use.
rulespec_roots: Non-empty explicit sequence of canonical
``rulespec-<country>`` checkout roots. These are forwarded exactly
to Axiom as its filesystem authority boundary; the adapter never
infers roots from the module, environment, or working directory.
schema: The frame-side entity structure (:data:`BE_SCHEMA` for the
Belgian pilot).
contract: Column-parity contract for :meth:`write_dataset` exports.
Expand All @@ -122,6 +134,7 @@ def __init__(
module: str | Path,
schema: EntitySchema = BE_SCHEMA,
*,
rulespec_roots: Sequence[str | Path],
contract: ExportContract | None = None,
defaults: Mapping[str, object] | None = None,
entity_names: Mapping[str, str] | None = None,
Expand All @@ -131,7 +144,22 @@ def __init__(
raise ValueError(
f"arithmetic must be 'decimal' or 'f64', got {arithmetic!r}."
)
if isinstance(rulespec_roots, (str, Path)):
raise TypeError(
"rulespec_roots must be a non-empty sequence of explicit "
"rulespec-<country> root paths, not a scalar path."
)
roots = tuple(rulespec_roots)
if not roots:
raise ValueError(
"at least one explicit rulespec-<country> root is required"
)
if not all(isinstance(root, (str, Path)) for root in roots):
raise TypeError(
"rulespec_roots entries must each be a str or pathlib.Path."
)
self._module = Path(module)
self._rulespec_roots = tuple(Path(root) for root in roots)
self._schema = schema
self._contract = contract if contract is not None else ExportContract.empty()
self._defaults = dict(defaults or {})
Expand Down Expand Up @@ -431,9 +459,22 @@ def _program(self, frame_entity: str, *, missing_ok: bool = False) -> Any:
engine = self._import_engine()
try:
program = engine.CompiledDenseProgram.from_file(
self._module, entity=engine_entity
self._module,
rulespec_roots=self._rulespec_roots,
entity=engine_entity,
)
except ValueError as exc:
missing_entity = (
"dense compilation could not find derived outputs for entity "
f"`{engine_entity}`"
)
except ValueError:
if str(exc) != missing_entity:
# The native surface currently reports both an entity with no
# derived outputs and authority-root/module validation failures
# as ValueError. Only the exact former condition is optional;
# root or module failures must propagate instead of becoming a
# silently absent program under missing_ok=True.
raise
self._programs[frame_entity] = None
if missing_ok:
return None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Self-contained RuleSpec module for the Axiom adapter's behavioral tests:
# Self-contained canonical-root RuleSpec module for the Axiom adapter's tests:
# a two-bracket personal income tax with a boolean exemption predicate and a
# monthly benefit on the person, plus a household-entity housing allowance —
# small enough to hand-verify, shaped like the Belgian pilot (person +
# household scopes, no relations).
format: rulespec/v1
module:
summary: |-
populace-frame test fixture: person-scope two-bracket income tax with an
microcosm-frame test fixture: person-scope two-bracket income tax with an
exemption predicate and a monthly benefit, and a household-scope housing
allowance. Values are round numbers chosen for hand verification.
units:
Expand Down
Loading
Loading