"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance. The contract resolver is the bridge between the consumer's declared intent (a contract YAML) and the platform's executable representation (a Target Stack JSON instance). It: 1. Loads and validates the contract against schemas/contract.schema.json. 2. Looks up the module name in modules/registry.json. 3. If the module is an L1 primitive: builds a stack instance directly from the interface.json + contract inputs. 4. If the module is an L2 composition: loads the composition.json, expands children to stack resources, resolves wires to ref: expressions, and emits the full stack instance. The output is a JSON instance valid against schemas/stack.schema.json, ready for the Terraform adapter to compile. CLI: contract_resolver.py """ import json import os import sys import yaml import jsonschema def _load_json(path): with open(path, "r") as fh: return json.load(fh) def _load_yaml(path): with open(path, "r") as fh: return yaml.safe_load(fh) def _resolve_wire_value(wire, contract_inputs, child_outputs): """Resolve a wire 'from' reference to a concrete value. Wire 'from' can be: - "contract.inputs." — a contract input value - ".outputs." — a reference to another child's output Returns either a concrete value (string/number/boolean) or a "ref:." string for cross-child references. For multi-resource L1s (e.g. vpc which expands to vpc-vpc, vpc-subnet, vpc-routetable), the ref must point to the sub-resource that actually produces the output, not the child id. The child_outputs table maps childId -> {outputName -> resourceId} so the ref uses the correct resource id. """ from_expr = wire["from"] to_expr = wire["to"] # If the 'from' is a contract input, use the concrete value if from_expr.startswith("contract.inputs."): input_name = from_expr[len("contract.inputs."):] if input_name in contract_inputs: return contract_inputs[input_name] # Check for default default = wire.get("default") if default is not None: return default return None # If the 'from' is a child output, emit a ref: expression if "." in from_expr: parts = from_expr.split(".", 2) if len(parts) >= 3 and parts[1] == "outputs": child_id = parts[0] output_name = parts[2] # Look up the sub-resource that produces this output. # child_outputs[child_id] is a dict {outputName -> resourceId}. # If the child is a single-resource L1, the resourceId == child_id. # If multi-resource, the resourceId is the expanded sub-resource id. child_out_map = child_outputs.get(child_id, {}) resource_id = child_out_map.get(output_name, child_id) return f"ref:{resource_id}.{output_name}" return None def resolve_l1(contract, registry, repo_root): """Resolve a contract referencing an L1 primitive to a stack instance.""" module_name = contract["module"] module_ref = f"{module_name}@1.0.0" inputs = contract.get("inputs", {}) environment = contract.get("environment", "dev") # Load the interface entry = registry[module_name]["1.0.0"] iface_path = os.path.join(repo_root, entry["interface"]) iface = _load_json(iface_path) # Build the stack instance stack_instance = { "version": "1.0.0", "stack": { "name": module_name, "kind": "l1", "depth": 1, }, "resources": [ { "id": iface.get("type", module_name).split(":")[-1] if ":" in iface.get("type", "") else module_name, "type": iface["type"], "module": module_ref, "inputs": dict(inputs), "outputs": { out_name: {"type": out_spec.get("type", "string")} for out_name, out_spec in iface.get("outputs", {}).items() }, } ], } # Add NFRs if present in the interface nfrs = iface.get("nfrs", {}) if nfrs: stack_instance["resources"][0]["nfrs"] = nfrs return stack_instance def resolve_l2(contract, registry, repo_root): """Resolve a contract referencing an L2 composition to a stack instance.""" module_name = contract["module"] inputs = contract.get("inputs", {}) # Load the composition entry = registry[module_name]["1.0.0"] comp_path = os.path.join(repo_root, entry["interface"]) composition = _load_json(comp_path) # Track child outputs for wire resolution # child_outputs[childId] = {outputName: resourceId} # For single-resource L1s, resourceId == childId # For multi-resource L1s, resourceId is the expanded sub-resource id child_outputs = {} resources = [] # Expand children to resources for child in composition["children"]: child_id = child["id"] child_module = child["module"] child_name = child_module.split("@")[0] # Load the child's interface to get type and outputs child_entry = registry[child_name]["1.0.0"] child_iface_path = os.path.join(repo_root, child_entry["interface"]) child_iface = _load_json(child_iface_path) # Build the output->resourceId map for this child child_out_map = {} # For multi-resource L1s (like vpc), the first resource type is the # primary; the adapter handles expansion. Use the interface's type # or the first resource in the interface's resources array. if "resources" in child_iface and child_iface["resources"]: # Multi-resource L1: create one resource per sub-resource for sub_res in child_iface["resources"]: res_id = f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}" if len(child_iface["resources"]) > 1 else child_id resource = { "id": res_id, "type": sub_res["type"], "module": child_module, "inputs": {}, "outputs": { out: {"type": "string"} for out in sub_res.get("outputs", []) }, } resources.append(resource) # Map each output to this sub-resource's id for out_name in sub_res.get("outputs", []): child_out_map[out_name] = res_id else: # Single-resource L1 resource = { "id": child_id, "type": child_iface["type"], "module": child_module, "inputs": {}, "outputs": { out_name: {"type": out_spec.get("type", "string")} for out_name, out_spec in child_iface.get("outputs", {}).items() }, } resources.append(resource) # Map each output to the child id for out_name in child_iface.get("outputs", {}): child_out_map[out_name] = child_id # Also map interface-level outputs (for L1s that declare outputs at the # interface level rather than per-resource) for out_name in child_iface.get("outputs", {}): if out_name not in child_out_map: child_out_map[out_name] = child_id child_outputs[child_id] = child_out_map # Resolve wires to populate inputs for wire in composition.get("wires", []): to_expr = wire["to"] # Parse "to": ".inputs." to_parts = to_expr.split(".") if len(to_parts) != 3 or to_parts[1] != "inputs": continue target_child = to_parts[0] input_name = to_parts[2] value = _resolve_wire_value(wire, inputs, child_outputs) if value is not None: # Find the target resource and set the input for res in resources: if res["id"] == target_child or res["id"].startswith(f"{target_child}-"): res["inputs"][input_name] = value break # Build the stack instance stack_instance = { "version": "1.0.0", "stack": { "name": module_name, "kind": "l2", "depth": composition.get("depth", 1), }, "resources": resources, } # P1-7: Process the composition's outputs[] array to build stack.outputs. # Each output wire: {"from": ".outputs.", "to": "stack.outputs."} # The child_outputs map (childId -> {outputName: resourceId}) resolves # the source to a resource id, which the adapter uses to emit # `output "" { value = aws_.. }`. stack_outputs = {} for out_wire in composition.get("outputs", []): from_expr = out_wire.get("from", "") to_expr = out_wire.get("to", "") # Parse "to": "stack.outputs." to_parts = to_expr.split(".") if len(to_parts) != 3 or to_parts[1] != "outputs": continue out_name = to_parts[2] # Parse "from": ".outputs." from_parts = from_expr.split(".") if len(from_parts) != 3 or from_parts[1] != "outputs": continue src_child = from_parts[0] src_output = from_parts[2] # Resolve the source resource id from child_outputs child_out_map = child_outputs.get(src_child, {}) src_resource_id = child_out_map.get(src_output, src_child) stack_outputs[out_name] = { "type": "string", "from": src_resource_id, "output": src_output, } if stack_outputs: stack_instance["outputs"] = stack_outputs return stack_instance def resolve(contract_path, repo_root=None): """Resolve a consumer contract to a Target Stack instance. Args: contract_path: Path to the contract YAML file. repo_root: Root of the ACDL repo (defaults to two levels up from this file). Returns: A dict representing the Target Stack instance. """ if repo_root is None: repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Load contract contract = _load_yaml(contract_path) # Load schemas contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json")) # Validate contract against schema jsonschema.validate(contract, contract_schema) # Load registry registry = _load_json(os.path.join(repo_root, "modules", "registry.json")) module_name = contract["module"] if module_name not in registry: raise ValueError(f"module '{module_name}' not found in registry") # Determine if L1 or L2 entry = registry[module_name]["1.0.0"] interface_path = entry["interface"] is_l2 = "l2" in interface_path or "composition" in interface_path if is_l2: stack_instance = resolve_l2(contract, registry, repo_root) else: stack_instance = resolve_l1(contract, registry, repo_root) # Validate against stack schema stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json")) jsonschema.validate(stack_instance, stack_schema) return stack_instance if __name__ == "__main__": if len(sys.argv) != 3: print("usage: contract_resolver.py ", file=sys.stderr) sys.exit(2) result = resolve(sys.argv[1]) with open(sys.argv[2], "w") as fh: json.dump(result, fh, indent=2) print(f"resolver: resolved {sys.argv[1]} -> {sys.argv[2]}", file=sys.stderr)