Files
nova-platform/core/contract_resolver.py
T
CIAgent 2443909362 docs(milestone): complete v1.0-nova-platform (release v0.1.6)
---ci---
project: nova-platform
milestone: v1.0
status: complete
requirements:
  covered: [REQ-01,REQ-02,REQ-03,REQ-04,REQ-05,REQ-06,REQ-07,REQ-08,REQ-09,REQ-10,REQ-11,REQ-12,REQ-13,REQ-14,REQ-15,REQ-16,REQ-17,REQ-18,REQ-19,REQ-20,REQ-21,REQ-22,REQ-23,REQ-24,REQ-25,REQ-26,REQ-27,REQ-28,REQ-29,REQ-30,REQ-31,REQ-32,REQ-33,REQ-34,REQ-35,REQ-36,REQ-37,REQ-38]
  partial: []
---/ci---

v1.0 milestone complete: simplified infrastructure-delivery platform
derived from Nova (acdl). 6 phases (P0-P5 + P6 final). 38 REQ-IDs.
38 decisions (D-001..D-038). 76 tests pass. Engine boundary holds.
Happy paths green (check-only + CI). 13 L1 + 2 L2 modules. 5 terraform
roots. Shell reproducibility. Zero OOS files.

Tags: v0.1.0 (P0) → v0.1.1..v0.1.5 (P1..P5) → v0.1.6 (P6 = milestone
release on v0.1 patch line).
2026-08-25 02:18:35 +00:00

181 lines
6.3 KiB
Python

"""Nova Platform — Contract Resolver.
Resolves a validated consumer contract to a Stack instance (a flat dict
conforming to schemas/stack.schema.json).
Flow:
1. Validate the contract dict against schemas/contract.schema.json.
2. Load the environment via core.environment_check.check().
3. Build an interpolation context {'env': env, 'contract': contract}.
4. For each infrastructure entry: look up the module + version in the
registry, interpolate ${env.*} / ${contract.*} tokens in inputs,
and emit a flat stack resource {module, version, inputs}.
5. Return the stack dict.
Engine-agnostic: no aws_*, no Terraform terms, no module paths. The stack
carries NO 'source' field (D-037/C-1 grill fix) — the adapter loads the
registry to map module -> terraform_dir. L2 is opaque (D-012): a single
stack resource, no children/wires expansion.
"""
import json
import re
from pathlib import Path
import jsonschema
import yaml
_TOKEN_RE = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")
class ModuleNotFoundError(KeyError):
"""Raised when a contract references a module not in the registry."""
class VersionNotFoundError(KeyError):
"""Raised when a contract references a version not in the registry."""
def _lookup_dotted(context, dotted):
parts = dotted.split(".")
cur = context
for part in parts:
if isinstance(cur, dict) and part in cur:
cur = cur[part]
else:
raise KeyError(dotted)
return cur
def _expand_vars(value, context):
if isinstance(value, str):
def _replace(match):
token = match.group(1)
try:
resolved = _lookup_dotted(context, token)
except KeyError:
raise ValueError(f"unresolved interpolation token: ${{{token}}}")
if isinstance(resolved, (dict, list)):
return json.dumps(resolved)
return str(resolved)
return _TOKEN_RE.sub(_replace, value)
if isinstance(value, dict):
return {k: _expand_vars(v, context) for k, v in value.items()}
if isinstance(value, list):
return [_expand_vars(v, context) for v in value]
return value
def _latest_version(registry, module_name):
versions = registry[module_name]
non_deprecated = [(v, e) for v, e in versions.items()
if not e.get("deprecated", False)]
if not non_deprecated:
non_deprecated = list(versions.items())
non_deprecated.sort(key=lambda x: [int(p) for p in x[0].split(".")],
reverse=True)
return non_deprecated[0][0]
def _load_schema(path):
with open(path) as fh:
return json.load(fh)
def resolve(contract, registry, modules_dir, environments_dir=None,
repo_root=None):
"""Resolve a validated contract dict to a flat Stack dict.
Args:
contract: validated contract dict (must conform to
schemas/contract.schema.json).
registry: modules/registry.json loaded as a dict.
modules_dir: Path to the modules/ directory (unused for L2-opaque
resolution but kept per D-011 for future interface.json reads).
environments_dir: Path to core/environments/. If None, derived from
repo_root / 'core' / 'environments'.
repo_root: Path to the repo root. If None, derived from modules_dir
parent's parent (modules_dir is <root>/modules).
Returns:
A flat stack dict conforming to schemas/stack.schema.json:
{contract_id, contract_name, environment, resources: [{module,
version, inputs}]}.
Raises:
ModuleNotFoundError: contract references an unknown module.
VersionNotFoundError: contract references an unknown version.
jsonschema.ValidationError: contract does not conform to schema.
ValueError: unresolved interpolation token.
"""
if repo_root is None:
repo_root = Path(modules_dir).parent.parent
if environments_dir is None:
environments_dir = Path(repo_root) / "core" / "environments"
contract_schema_path = Path(repo_root) / "schemas" / "contract.schema.json"
contract_schema = _load_schema(contract_schema_path)
jsonschema.validate(contract, contract_schema)
from core import environment_check
env = environment_check.check(contract["environment"], environments_dir)
# Expose 'environment' as an alias for the env's 'name' field so
# ${env.environment} resolves (the env JSON uses 'name', but contracts
# reference the environment by ${env.environment}).
env["environment"] = env.get("name", contract["environment"])
context = {"env": env, "contract": contract}
resources = []
for item in contract["infrastructure"]:
module_name = item["module"]
if module_name not in registry:
raise ModuleNotFoundError(module_name)
version = item.get("version")
if version is None:
version = _latest_version(registry, module_name)
elif version not in registry[module_name]:
raise VersionNotFoundError(f"{module_name}@{version}")
inputs = _expand_vars(item.get("inputs", {}), context)
resources.append({
"module": module_name,
"version": version,
"inputs": inputs,
})
return {
"contract_id": contract["id"],
"contract_name": contract["name"],
"environment": contract["environment"],
"resources": resources,
}
def main(argv=None):
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
argv = argv or sys.argv[1:]
if len(argv) < 2:
print("usage: contract_resolver.py <contract.yaml> [out.json]",
file=sys.stderr)
return 2
contract_path = argv[0]
out_path = argv[1] if len(argv) > 1 else None
repo_root = Path(__file__).resolve().parent.parent
with open(contract_path) as fh:
contract = yaml.safe_load(fh)
with open(repo_root / "modules" / "registry.json") as fh:
registry = json.load(fh)
stack = resolve(contract, registry, repo_root / "modules",
repo_root=repo_root)
if out_path:
with open(out_path, "w") as fh:
json.dump(stack, fh, indent=2)
else:
print(json.dumps(stack, indent=2))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())