refactor(modules): remove thin-composition layer; rewrite all module READMEs
The L2 thin-composition layer (composition.json + contract_resolver.py + contract schema + sample contracts) has been removed completely. The implementation was unsatisfactory and is deferred for a later redesign. - Delete: composition.json x2, contract_resolver.py, contracts/ x2, contract.schema.json - Patch: run_platform.sh now loads a pre-existing IR instance instead of resolving a contract (the downstream adapter/checkov/confidence/outbox pipeline is unchanged) - Prune: L2 entries removed from registry.json (L1 entries unchanged) - Rewrite: all 7 L1 module READMEs in plain language (no jargon), each with Resources/Inputs/Outputs/Usage/Compliance-extension-points/Versioning sections derived from interface.json - Add: 2 L2 placeholder READMEs noting the composition is under redesign - Add: modules-ir/README.md catalog index + README-TEMPLATE.md ---ci--- project: acdl phase: 17 milestone: v1.3 status: execute ---/ci---
This commit is contained in:
@@ -1,256 +0,0 @@
|
||||
"""ACDL Contract Resolver — resolve a contract to a Target Stack IR instance.
|
||||
|
||||
ARCHITECTURE.md §12.8: the contract declares intent in IR-typed terms;
|
||||
the resolver resolves the contract to a target stack (list of L1
|
||||
instances + inputs + relationships); the adapter compiles the target
|
||||
stack to a plan.
|
||||
|
||||
Steps:
|
||||
1. Load the contract (YAML -> dict).
|
||||
2. Validate the contract against schemas/contract.schema.json.
|
||||
3. Look up the L2 in modules-ir/registry.json.
|
||||
4. Load the L2's composition.json (the thin-composition tree).
|
||||
5. Map the contract's inputs through the composition's wires to the
|
||||
child L1s' inputs. Two wire kinds:
|
||||
- passthrough: {target, input} (or an array of the same) -> the
|
||||
concrete contract value.
|
||||
- child->child: {target, input, source:"child:<id>.<output>"} ->
|
||||
a "ref:<ir_resource_id>.<output>" string (value known at apply
|
||||
time only).
|
||||
A wire value may be a single object or an array of objects (for
|
||||
contract inputs that fan out to multiple children); both forms are
|
||||
iterated.
|
||||
6. Emit an IR instance {version, stack:{name, kind:l2, depth},
|
||||
resources:[<L1 instances with concrete inputs>], relationships:[...]}.
|
||||
Multi-resource L1s (interface.json has a `resources` array) expand
|
||||
into one IR resource per entry, id `<child_id>-<type_suffix>` where
|
||||
type_suffix is the last IR-type segment with underscores stripped;
|
||||
single-resource L1s keep the child id verbatim.
|
||||
7. Validate the IR instance against schemas/ir.schema.json.
|
||||
|
||||
CLI: contract_resolver.py <contract.yaml> <out_ir.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
import jsonschema
|
||||
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, "r") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _iter_wire_targets(wire_value):
|
||||
"""Yield each target-spec from a wire value (single object or array)."""
|
||||
if isinstance(wire_value, list):
|
||||
for spec in wire_value:
|
||||
yield spec
|
||||
elif isinstance(wire_value, dict):
|
||||
yield wire_value
|
||||
|
||||
|
||||
def _type_suffix(ir_type):
|
||||
"""Last segment of an IR type, underscores stripped (e.g. aws:ec2:vpc -> vpc,
|
||||
aws:elbv2:targetgroup -> targetgroup, aws:ecs:task_definition -> taskdefinition)."""
|
||||
return ir_type.rsplit(":", 1)[-1].replace("_", "")
|
||||
|
||||
|
||||
def _resolve_child_ref(source, child_id, l1_iface, child_ir_ids):
|
||||
"""Resolve a "child:<id>.<output>" source to "ref:<ir_resource_id>.<output>".
|
||||
|
||||
The ir_resource_id is the producing child's sub-resource that
|
||||
declares the output. For single-resource L1s that is the child id;
|
||||
for multi-resource L1s the L1's `resources` array is scanned for
|
||||
which sub-resource declares the output (exact match, then a
|
||||
singular->plural fallback so e.g. `subnet_ids` matches a per-resource
|
||||
`subnet_id`). The ref's output name is the per-resource output name
|
||||
when matched that way, else the source output name verbatim.
|
||||
"""
|
||||
prefix = "child:"
|
||||
if not source.startswith(prefix):
|
||||
raise ValueError(f"unsupported wire source {source!r}")
|
||||
body = source[len(prefix):]
|
||||
src_child_id, src_output = body.split(".", 1)
|
||||
if src_child_id != child_id:
|
||||
# Cross-child reference: look up the producing child's first IR
|
||||
# resource id (the child->child wiring table is keyed by child id
|
||||
# by the caller; this branch is unused for v1.2's wires but kept
|
||||
# for completeness).
|
||||
ir_resource_id = child_ir_ids.get(src_child_id, src_child_id)
|
||||
return f"ref:{ir_resource_id}.{src_output}"
|
||||
# Same-child reference: find the producing sub-resource.
|
||||
resources = l1_iface.get("resources")
|
||||
if not resources:
|
||||
return f"ref:{child_id}.{src_output}"
|
||||
for idx, sub in enumerate(resources):
|
||||
sub_outputs = sub.get("outputs", [])
|
||||
if src_output in sub_outputs:
|
||||
ir_id = child_ir_ids[child_id][idx]
|
||||
return f"ref:{ir_id}.{src_output}"
|
||||
# Singular->plural fallback (subnet_ids -> subnet_id).
|
||||
singular = src_output[:-1] if src_output.endswith("s") else src_output
|
||||
for idx, sub in enumerate(resources):
|
||||
sub_outputs = sub.get("outputs", [])
|
||||
if singular in sub_outputs:
|
||||
ir_id = child_ir_ids[child_id][idx]
|
||||
return f"ref:{ir_id}.{singular}"
|
||||
# No per-resource match: point at the first sub-resource, keep the
|
||||
# source output name verbatim.
|
||||
ir_id = child_ir_ids[child_id][0]
|
||||
return f"ref:{ir_id}.{src_output}"
|
||||
|
||||
|
||||
def resolve(contract_path, repo_root=None):
|
||||
"""Resolve a contract YAML to an IR instance dict."""
|
||||
rr = repo_root or REPO_ROOT
|
||||
|
||||
# 1. Load the contract YAML.
|
||||
with open(contract_path, "r") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
|
||||
# 2. Validate the contract against the contract schema.
|
||||
contract_schema = _load_json(os.path.join(rr, "schemas/contract.schema.json"))
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# 3. Look up the L2 in the registry.
|
||||
stack_name = contract["stack"]
|
||||
registry = _load_json(os.path.join(rr, "modules-ir/registry.json"))
|
||||
if stack_name not in registry:
|
||||
raise ValueError(f"stack {stack_name!r} not in registry")
|
||||
versions = registry[stack_name]
|
||||
# Pick the highest 1.x.x (spike: just take the first non-deprecated).
|
||||
entry = next(v for v in versions.values() if not v.get("deprecated", False))
|
||||
|
||||
# 4. Load the L2's composition.json.
|
||||
composition_key = entry.get("composition") or entry.get("interface")
|
||||
composition = _load_json(os.path.join(rr, composition_key))
|
||||
|
||||
# 5. Map the contract's inputs through the wires to the child L1s' inputs.
|
||||
wires = composition.get("wires", {})
|
||||
contract_inputs = contract.get("inputs", {})
|
||||
children = composition.get("children", [])
|
||||
|
||||
# Pre-load every child's L1 interface + compute IR resource ids.
|
||||
child_ifaces = {}
|
||||
child_ir_ids = {}
|
||||
for child in children:
|
||||
child_id = child["id"]
|
||||
child_module = child["module"]
|
||||
l1_name, l1_version = child_module.split("@", 1)
|
||||
l1_entry = registry.get(l1_name, {}).get(l1_version)
|
||||
if not l1_entry:
|
||||
raise ValueError(f"L1 {child_module!r} not in registry")
|
||||
l1_iface = _load_json(os.path.join(rr, l1_entry["interface"]))
|
||||
child_ifaces[child_id] = l1_iface
|
||||
sub_resources = l1_iface.get("resources")
|
||||
if sub_resources:
|
||||
child_ir_ids[child_id] = [
|
||||
f"{child_id}-{_type_suffix(sub['type'])}" for sub in sub_resources
|
||||
]
|
||||
else:
|
||||
child_ir_ids[child_id] = [child_id]
|
||||
|
||||
# Build each child's mapped inputs (concrete values + ref strings).
|
||||
child_inputs_map = {child["id"]: {} for child in children}
|
||||
for wire_name, wire_value in wires.items():
|
||||
for spec in _iter_wire_targets(wire_value):
|
||||
target = spec.get("target")
|
||||
if target not in child_inputs_map:
|
||||
continue
|
||||
input_name = spec["input"]
|
||||
source = spec.get("source")
|
||||
if source:
|
||||
# Child->child reference: emit a ref string.
|
||||
src_child_id = source[len("child:"):].split(".", 1)[0]
|
||||
child_inputs_map[target][input_name] = _resolve_child_ref(
|
||||
source, src_child_id, child_ifaces[src_child_id], child_ir_ids
|
||||
)
|
||||
else:
|
||||
# Contract->child passthrough.
|
||||
if wire_name in contract_inputs:
|
||||
child_inputs_map[target][input_name] = contract_inputs[wire_name]
|
||||
|
||||
# 6. Emit the IR instance.
|
||||
resources = []
|
||||
relationships = []
|
||||
for child in children:
|
||||
child_id = child["id"]
|
||||
child_module = child["module"]
|
||||
l1_iface = child_ifaces[child_id]
|
||||
l1_outputs = l1_iface.get("outputs", {})
|
||||
child_inputs = child_inputs_map[child_id]
|
||||
sub_resources = l1_iface.get("resources")
|
||||
ir_ids = child_ir_ids[child_id]
|
||||
if sub_resources:
|
||||
for idx, sub in enumerate(sub_resources):
|
||||
ir_id = ir_ids[idx]
|
||||
sub_in_names = sub.get("inputs", [])
|
||||
sub_out_names = sub.get("outputs", [])
|
||||
sub_inputs = {
|
||||
n: child_inputs[n] for n in sub_in_names if n in child_inputs
|
||||
}
|
||||
sub_outputs = {
|
||||
n: l1_outputs[n] for n in sub_out_names if n in l1_outputs
|
||||
}
|
||||
resources.append({
|
||||
"id": ir_id,
|
||||
"type": sub["type"],
|
||||
"module": child_module,
|
||||
"inputs": sub_inputs,
|
||||
"outputs": sub_outputs,
|
||||
})
|
||||
relationships.append({"from": "root", "to": ir_id, "kind": "parent"})
|
||||
# Resolve intra-L1 refs (refs between sub-resources of the same L1).
|
||||
intra_refs = l1_iface.get("intra_refs", [])
|
||||
for iref in intra_refs:
|
||||
from_type, from_input = iref["from"].split(".", 1)
|
||||
to_type, to_output = iref["to"].split(".", 1)
|
||||
from_ir_id = next((ir_ids[i] for i, s in enumerate(sub_resources) if s["type"] == from_type), None)
|
||||
to_ir_id = next((ir_ids[i] for i, s in enumerate(sub_resources) if s["type"] == to_type), None)
|
||||
if from_ir_id and to_ir_id:
|
||||
for r in resources:
|
||||
if r["id"] == from_ir_id:
|
||||
r["inputs"][from_input] = f"ref:{to_ir_id}.{to_output}"
|
||||
else:
|
||||
resources.append({
|
||||
"id": child_id,
|
||||
"type": l1_iface["type"],
|
||||
"module": child_module,
|
||||
"inputs": child_inputs,
|
||||
"outputs": l1_outputs,
|
||||
})
|
||||
relationships.append({"from": "root", "to": child_id, "kind": "parent"})
|
||||
|
||||
ir_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": composition["name"],
|
||||
"kind": composition["kind"],
|
||||
"depth": composition["depth"],
|
||||
},
|
||||
"resources": resources,
|
||||
"relationships": relationships,
|
||||
}
|
||||
|
||||
# 7. Validate the IR instance against the IR schema.
|
||||
ir_schema = _load_json(os.path.join(rr, "schemas/ir.schema.json"))
|
||||
jsonschema.validate(ir_instance, ir_schema)
|
||||
|
||||
return ir_instance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: contract_resolver.py <contract.yaml> <out_ir.json>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
ir = resolve(sys.argv[1])
|
||||
with open(sys.argv[2], "w") as fh:
|
||||
json.dump(ir, fh, indent=2)
|
||||
print(f"resolver: emitted IR to {sys.argv[2]}", file=sys.stderr)
|
||||
Reference in New Issue
Block a user