From b178a980fc561ce82821405707ebe24da85c09dc Mon Sep 17 00:00:00 2001 From: Rickard von Haugwitz Date: Sat, 13 Jun 2026 21:40:28 +0200 Subject: [PATCH 1/2] Add target extension contract --- README.md | 7 + schemas/target_extension.schema.json | 152 ++++++++++++++++++ src/command_generation/__init__.py | 12 ++ .../schemas/target_extension.schema.json | 152 ++++++++++++++++++ src/command_generation/target_extension.py | 110 +++++++++++++ tests/test_public_api.py | 102 ++++++++++++ 6 files changed, 535 insertions(+) create mode 100644 schemas/target_extension.schema.json create mode 100644 src/command_generation/schemas/target_extension.schema.json create mode 100644 src/command_generation/target_extension.py diff --git a/README.md b/README.md index b278632..ec866e1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Generated runtimes should be self-contained with respect to this package. If a g - `generate_command_packages(..., check=True|False)` checks or writes generated files. - `CommandGenerationHostManifest` declares host roots, custom primitive registry entries, target bindings, and optional host-owned runtime support. - `PrimitiveRegistry` and `PrimitiveDefinition` describe portable or host-owned primitives with target support. +- `TargetExtensionContract`, `validate_target_extension_contract(...)`, and `target_support_matrix_entries(...)` define how new generated targets declare projection rules, runtime dependencies, callable/wrapper shape, packaging, conformance execution, and matrix support without owning product semantics. - `process_case_from_contract(...)`, `CliConformanceTarget`, and `run_cli_conformance_case(...)` provide the generic black-box runner for contract-owned CLI/process conformance cases. - `operation_case_from_contract(...)`, `FunctionConformanceTarget`, and `run_function_conformance_case(...)` provide the generic JSON-shaped operation conformance runner for direct implementation adapters. - `contract_conformance_cases_manifest()` and `load_contract_conformance_case(...)` expose package-owned reusable conformance cases. @@ -53,6 +54,12 @@ That split matters for conformance: - use process conformance for parser behavior, stdout/stderr, exit codes, and wrapper state; - do not let a CLI flag name become the semantic contract unless the operation input is intentionally named the same way. +## Target Extension Contract + +Target implementations are declared with `command-generation/target-extension/v1`. The contract records target identity, projection rules, runtime dependency boundaries, direct operation callable shape, wrapper/adapter responsibilities, packaging layout, conformance execution, and support-declaration rules for matrix inclusion. + +Targets must not own product operation semantics or require per-operation feature maintenance. Ordinary behavior remains in host-owned operation IR, primitive refs, schemas, and input/output/error cases. Target maintenance is limited to runtime dependency updates, target compatibility work, and projection/runtime-adapter bugs. + ## Conformance Strategy - Store behavior examples in host-owned contract resources as stable input/expected-output cases. diff --git a/schemas/target_extension.schema.json b/schemas/target_extension.schema.json new file mode 100644 index 0000000..d66b194 --- /dev/null +++ b/schemas/target_extension.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://command-generation.local/schemas/target_extension.schema.json", + "title": "Command-generation target extension contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "target_id", + "implementation_status", + "projection_rules", + "runtime_dependencies", + "operation_callable_surface", + "wrapper_adapter_shape", + "packaging_output_layout", + "conformance_execution", + "support_declaration", + "product_semantics_boundary", + "maintenance_boundary" + ], + "properties": { + "schema_version": { + "const": "command-generation/target-extension/v1" + }, + "target_id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_.-]*$" + }, + "implementation_status": { + "enum": ["planned", "implemented"] + }, + "projection_rules": { + "type": "object", + "additionalProperties": true, + "required": ["source", "target_owns"], + "properties": { + "source": { + "const": "operation-ir" + }, + "target_owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "runtime_dependencies": { + "type": "object", + "additionalProperties": true, + "required": ["boundary"], + "properties": { + "boundary": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "operation_callable_surface": { + "type": "object", + "additionalProperties": true, + "required": ["adapter_id", "input_model"], + "properties": { + "adapter_id": { + "type": "string", + "minLength": 1 + }, + "input_model": { + "const": "operation-values" + } + } + }, + "wrapper_adapter_shape": { + "type": "object", + "additionalProperties": true, + "required": ["owns"], + "properties": { + "owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "packaging_output_layout": { + "type": "object", + "additionalProperties": true, + "required": ["owns"], + "properties": { + "owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "conformance_execution": { + "type": "object", + "additionalProperties": true, + "required": ["runner", "case_model"], + "properties": { + "runner": { + "type": "string", + "minLength": 1 + }, + "case_model": { + "const": "input-output-error" + } + } + }, + "support_declaration": { + "type": "object", + "additionalProperties": true, + "required": ["matrix_inclusion", "adapter_ids"], + "properties": { + "matrix_inclusion": { + "enum": ["automatic-when-target-implemented", "manual"] + }, + "adapter_ids": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + } + }, + "product_semantics_boundary": { + "type": "object", + "additionalProperties": true, + "required": ["target_owns_product_semantics", "rule"], + "properties": { + "target_owns_product_semantics": { + "const": false + }, + "rule": { + "type": "string", + "minLength": 1 + } + } + }, + "maintenance_boundary": { + "type": "object", + "additionalProperties": true, + "required": ["per_operation_feature_maintenance", "allowed"], + "properties": { + "per_operation_feature_maintenance": { + "const": false + }, + "allowed": { + "type": "array", + "items": { "type": "string" } + } + } + } + } +} diff --git a/src/command_generation/__init__.py b/src/command_generation/__init__.py index ff60ddf..74cb39d 100644 --- a/src/command_generation/__init__.py +++ b/src/command_generation/__init__.py @@ -29,6 +29,13 @@ from command_generation.ir import command_package_schema_path, load_command_package_ir from command_generation.primitive_registry import BUILTIN_PORTABLE_PRIMITIVES, PrimitiveDefinition, PrimitiveRegistry from command_generation.primitive_executor import PrimitiveContext, PrimitiveExecutionError, execute_primitive, run_operation_steps +from command_generation.target_extension import ( + TargetExtensionContract, + TargetExtensionContractError, + target_extension_schema_path, + target_support_matrix_entries, + validate_target_extension_contract, +) __all__ = [ "BUILTIN_PORTABLE_PRIMITIVES", @@ -47,6 +54,8 @@ "PrimitiveExecutionError", "ProcessConformanceCase", "PrimitiveRegistry", + "TargetExtensionContract", + "TargetExtensionContractError", "canonical_command_artifacts", "command_package_schema_path", "contract_conformance_cases_manifest", @@ -66,4 +75,7 @@ "run_operation_steps", "selected_contract_fields", "selected_result_fields", + "target_extension_schema_path", + "target_support_matrix_entries", + "validate_target_extension_contract", ] diff --git a/src/command_generation/schemas/target_extension.schema.json b/src/command_generation/schemas/target_extension.schema.json new file mode 100644 index 0000000..d66b194 --- /dev/null +++ b/src/command_generation/schemas/target_extension.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://command-generation.local/schemas/target_extension.schema.json", + "title": "Command-generation target extension contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "target_id", + "implementation_status", + "projection_rules", + "runtime_dependencies", + "operation_callable_surface", + "wrapper_adapter_shape", + "packaging_output_layout", + "conformance_execution", + "support_declaration", + "product_semantics_boundary", + "maintenance_boundary" + ], + "properties": { + "schema_version": { + "const": "command-generation/target-extension/v1" + }, + "target_id": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_.-]*$" + }, + "implementation_status": { + "enum": ["planned", "implemented"] + }, + "projection_rules": { + "type": "object", + "additionalProperties": true, + "required": ["source", "target_owns"], + "properties": { + "source": { + "const": "operation-ir" + }, + "target_owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "runtime_dependencies": { + "type": "object", + "additionalProperties": true, + "required": ["boundary"], + "properties": { + "boundary": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "operation_callable_surface": { + "type": "object", + "additionalProperties": true, + "required": ["adapter_id", "input_model"], + "properties": { + "adapter_id": { + "type": "string", + "minLength": 1 + }, + "input_model": { + "const": "operation-values" + } + } + }, + "wrapper_adapter_shape": { + "type": "object", + "additionalProperties": true, + "required": ["owns"], + "properties": { + "owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "packaging_output_layout": { + "type": "object", + "additionalProperties": true, + "required": ["owns"], + "properties": { + "owns": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "conformance_execution": { + "type": "object", + "additionalProperties": true, + "required": ["runner", "case_model"], + "properties": { + "runner": { + "type": "string", + "minLength": 1 + }, + "case_model": { + "const": "input-output-error" + } + } + }, + "support_declaration": { + "type": "object", + "additionalProperties": true, + "required": ["matrix_inclusion", "adapter_ids"], + "properties": { + "matrix_inclusion": { + "enum": ["automatic-when-target-implemented", "manual"] + }, + "adapter_ids": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + } + }, + "product_semantics_boundary": { + "type": "object", + "additionalProperties": true, + "required": ["target_owns_product_semantics", "rule"], + "properties": { + "target_owns_product_semantics": { + "const": false + }, + "rule": { + "type": "string", + "minLength": 1 + } + } + }, + "maintenance_boundary": { + "type": "object", + "additionalProperties": true, + "required": ["per_operation_feature_maintenance", "allowed"], + "properties": { + "per_operation_feature_maintenance": { + "const": false + }, + "allowed": { + "type": "array", + "items": { "type": "string" } + } + } + } + } +} diff --git a/src/command_generation/target_extension.py b/src/command_generation/target_extension.py new file mode 100644 index 0000000..749eb43 --- /dev/null +++ b/src/command_generation/target_extension.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from importlib import resources +from pathlib import Path +from typing import Any + +from jsonschema import Draft202012Validator + + +class TargetExtensionContractError(ValueError): + """Raised when a target extension contract would make targets own behavior.""" + + +@dataclass(frozen=True) +class TargetExtensionContract: + target_id: str + implementation_status: str + projection_rules: Mapping[str, Any] + runtime_dependencies: Mapping[str, Any] + operation_callable_surface: Mapping[str, Any] + wrapper_adapter_shape: Mapping[str, Any] + packaging_output_layout: Mapping[str, Any] + conformance_execution: Mapping[str, Any] + support_declaration: Mapping[str, Any] + product_semantics_boundary: Mapping[str, Any] + maintenance_boundary: Mapping[str, Any] + + @classmethod + def from_mapping(cls, raw: Mapping[str, Any]) -> "TargetExtensionContract": + validate_target_extension_contract(raw) + return cls( + target_id=str(raw["target_id"]), + implementation_status=str(raw["implementation_status"]), + projection_rules=dict(raw["projection_rules"]), + runtime_dependencies=dict(raw["runtime_dependencies"]), + operation_callable_surface=dict(raw["operation_callable_surface"]), + wrapper_adapter_shape=dict(raw["wrapper_adapter_shape"]), + packaging_output_layout=dict(raw["packaging_output_layout"]), + conformance_execution=dict(raw["conformance_execution"]), + support_declaration=dict(raw["support_declaration"]), + product_semantics_boundary=dict(raw["product_semantics_boundary"]), + maintenance_boundary=dict(raw["maintenance_boundary"]), + ) + + @property + def adapter_ids(self) -> tuple[str, ...]: + values = self.support_declaration.get("adapter_ids", ()) + if isinstance(values, Sequence) and not isinstance(values, (str, bytes)): + return tuple(str(value) for value in values) + return () + + @property + def matrix_inclusion(self) -> str: + return str(self.support_declaration.get("matrix_inclusion", "manual")) + + +def target_extension_schema_path() -> Path: + return Path(str(resources.files("command_generation.schemas").joinpath("target_extension.schema.json"))) + + +def _load_schema() -> dict[str, Any]: + return json.loads(target_extension_schema_path().read_text(encoding="utf-8")) + + +def validate_target_extension_contract(raw: Mapping[str, Any]) -> None: + errors = sorted(Draft202012Validator(_load_schema()).iter_errors(raw), key=lambda error: list(error.path)) + if errors: + messages = [] + for error in errors: + location = ".".join(str(part) for part in error.path) or "" + messages.append(f"{location}: {error.message}") + raise TargetExtensionContractError("invalid target extension contract:\n" + "\n".join(messages)) + + semantics = raw.get("product_semantics_boundary", {}) + if isinstance(semantics, Mapping) and semantics.get("target_owns_product_semantics") is True: + raise TargetExtensionContractError("target extension contract must not let a target own product operation semantics") + + maintenance = raw.get("maintenance_boundary", {}) + if isinstance(maintenance, Mapping) and maintenance.get("per_operation_feature_maintenance") is True: + raise TargetExtensionContractError("target extension contract must not require per-operation feature maintenance") + + +def target_support_matrix_entries( + contracts: Sequence[TargetExtensionContract | Mapping[str, Any]], *, operation_id: str, case_id: str +) -> tuple[dict[str, Any], ...]: + entries: list[dict[str, Any]] = [] + for contract_or_mapping in contracts: + contract = ( + contract_or_mapping + if isinstance(contract_or_mapping, TargetExtensionContract) + else TargetExtensionContract.from_mapping(contract_or_mapping) + ) + if contract.implementation_status != "implemented": + continue + if contract.matrix_inclusion != "automatic-when-target-implemented": + continue + for adapter_id in contract.adapter_ids: + entries.append( + { + "operation_id": operation_id, + "case_id": case_id, + "target_id": contract.target_id, + "adapter_id": adapter_id, + "source": "target-extension support declaration", + } + ) + return tuple(entries) diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 7275c8f..75bbe1a 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -33,8 +33,12 @@ render_outputs, run_function_conformance_case, run_cli_conformance_case, + target_extension_schema_path, + target_support_matrix_entries, + validate_target_extension_contract, ) from command_generation import generator +from command_generation.target_extension import TargetExtensionContract, TargetExtensionContractError def _maturity_policy() -> dict[str, object]: @@ -780,3 +784,101 @@ def test_primitive_registry_round_trips_host_metadata() -> None: def test_builtin_registry_declares_portable_primitives() -> None: assert "filesystem.read" in BUILTIN_PORTABLE_PRIMITIVES.ids() assert "output.emit" in BUILTIN_PORTABLE_PRIMITIVES.ids() + + +def _target_extension_contract(**overrides: object) -> dict[str, object]: + contract: dict[str, object] = { + "schema_version": "command-generation/target-extension/v1", + "target_id": "python", + "implementation_status": "implemented", + "projection_rules": { + "source": "operation-ir", + "target_owns": ["syntax projection", "runtime imports"], + }, + "runtime_dependencies": { + "boundary": ["standard library json", "generated package resources"], + }, + "operation_callable_surface": { + "adapter_id": "python.function", + "input_model": "operation-values", + }, + "wrapper_adapter_shape": { + "owns": ["argv parsing", "exit-code mapping"], + }, + "packaging_output_layout": { + "owns": ["module path", "resource layout"], + }, + "conformance_execution": { + "runner": "function", + "case_model": "input-output-error", + }, + "support_declaration": { + "matrix_inclusion": "automatic-when-target-implemented", + "adapter_ids": ["python.function"], + }, + "product_semantics_boundary": { + "target_owns_product_semantics": False, + "rule": "Product behavior remains in operation IR, primitive refs, and host-owned runtime primitives.", + }, + "maintenance_boundary": { + "per_operation_feature_maintenance": False, + "allowed": ["runtime dependency updates", "target compatibility fixes", "projection bugs"], + }, + } + contract.update(overrides) + return contract + + +def test_target_extension_contract_validates_and_projects_matrix_entries() -> None: + contract = TargetExtensionContract.from_mapping(_target_extension_contract()) + + assert target_extension_schema_path().name == "target_extension.schema.json" + assert contract.target_id == "python" + assert target_support_matrix_entries( + [contract], + operation_id="todo.list.report", + case_id="todo.list.operation", + ) == ( + { + "operation_id": "todo.list.report", + "case_id": "todo.list.operation", + "target_id": "python", + "adapter_id": "python.function", + "source": "target-extension support declaration", + }, + ) + + +def test_target_extension_support_matrix_waits_for_implemented_target() -> None: + assert ( + target_support_matrix_entries( + [_target_extension_contract(implementation_status="planned")], + operation_id="todo.list.report", + case_id="todo.list.operation", + ) + == () + ) + + +def test_target_extension_contract_rejects_product_semantics_ownership() -> None: + contract = _target_extension_contract( + product_semantics_boundary={ + "target_owns_product_semantics": True, + "rule": "bad target owns behavior", + } + ) + + with pytest.raises(TargetExtensionContractError, match="target_owns_product_semantics"): + validate_target_extension_contract(contract) + + +def test_target_extension_contract_rejects_per_operation_feature_maintenance() -> None: + contract = _target_extension_contract( + maintenance_boundary={ + "per_operation_feature_maintenance": True, + "allowed": ["add feature logic in each target"], + } + ) + + with pytest.raises(TargetExtensionContractError, match="per_operation_feature_maintenance"): + validate_target_extension_contract(contract) From 70392e88b1f4ce627c9e4d844869d1a1ea623723 Mon Sep 17 00:00:00 2001 From: Rickard von Haugwitz Date: Sat, 13 Jun 2026 22:31:13 +0200 Subject: [PATCH 2/2] Guard target extension schema copies --- tests/test_public_api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 75bbe1a..5745e84 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -246,6 +246,13 @@ def test_package_owned_schema_loads_fixture_manifest(tmp_path: Path) -> None: assert loaded["packages"][0]["id"] == "todo-fixture" +def test_target_extension_schema_copies_match() -> None: + repo_root = Path(__file__).resolve().parents[1] + source_schema = repo_root / "schemas" / "target_extension.schema.json" + + assert source_schema.read_bytes() == target_extension_schema_path().read_bytes() + + def test_non_aw_fixture_renders_and_runs_python_command(tmp_path: Path) -> None: manifest = _fixture_manifest(tmp_path)