diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4a403c7df..71e95baf4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.1" + ".": "0.20.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d408506..bbdb496f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 PRs do **not** edit this file directly. release-please maintains it from the Conventional Commit history on `main` (#684). +## [0.20.0](https://github.com/Brad-Edwards/aces/compare/v0.19.1...v0.20.0) (2026-07-13) + + +### Features + +* add experiment authoring-input contract and MCP authoring surface ([#711](https://github.com/Brad-Edwards/aces/issues/711)) ([ac5f697](https://github.com/Brad-Edwards/aces/commit/ac5f69719c87f7fb4fa85aa2c3a141c834a6ec73)) +* define associated artifact manifest contracts ([#741](https://github.com/Brad-Edwards/aces/issues/741)) ([066b3f0](https://github.com/Brad-Edwards/aces/commit/066b3f0c6103deb732795ebba90c4822d798f942)) +* define backend-neutral proposition semantics ([#744](https://github.com/Brad-Edwards/aces/issues/744)) ([ccb4913](https://github.com/Brad-Edwards/aces/commit/ccb49134395ff51120710981e209d97b791129c5)) +* enforce closed SDL phase contracts ([#742](https://github.com/Brad-Edwards/aces/issues/742)) ([b2f0756](https://github.com/Brad-Edwards/aces/commit/b2f0756440aa18b43e33ca685ee0a666f64b10aa)) +* enforce portable SDL identifiers ([#736](https://github.com/Brad-Edwards/aces/issues/736)) ([40af65b](https://github.com/Brad-Edwards/aces/commit/40af65b9513023a380b8121ad412686a9e08dc8c)) +* enforce TechVault realization disclosure ([#735](https://github.com/Brad-Edwards/aces/issues/735)) ([47e7220](https://github.com/Brad-Edwards/aces/commit/47e72201cd3b36b6b1c45467add2246a0b4186b4)) +* expose completeness profiles through MCP ([#755](https://github.com/Brad-Edwards/aces/issues/755)) ([23021cf](https://github.com/Brad-Edwards/aces/commit/23021cf027e787bf66303539a652905c701e1075)) +* **libvirt:** add guest-observed realization probes (ASR-519) ([#737](https://github.com/Brad-Edwards/aces/issues/737)) ([ef4cbed](https://github.com/Brad-Edwards/aces/commit/ef4cbed0a5eabb2e873e90bae2f05a049a39b9f9)) +* **libvirt:** publish configuration-bound realization envelopes ([#730](https://github.com/Brad-Edwards/aces/issues/730)) ([7d48c05](https://github.com/Brad-Edwards/aces/commit/7d48c0531db294c18826382dec0ef6209197495c)) +* publish revision-pinned SDL lineage ledger ([#745](https://github.com/Brad-Edwards/aces/issues/745)) ([04ccd2f](https://github.com/Brad-Edwards/aces/commit/04ccd2f5bdd8e5ab53730942defcd6e63bca74c8)) +* publish scientific scenario completeness profiles ([#751](https://github.com/Brad-Edwards/aces/issues/751)) ([d2774d9](https://github.com/Brad-Edwards/aces/commit/d2774d980a376553e34a5e7032199232d37f32b4)) +* **sdl:** add DSL-115 authoring specificity helper ([#710](https://github.com/Brad-Edwards/aces/issues/710)) ([a6036f4](https://github.com/Brad-Edwards/aces/commit/a6036f4cb0049c42217f28304070fc3b197d35b9)) +* **sdl:** add realization-envelope membership/subsumption/witness relation ([#668](https://github.com/Brad-Edwards/aces/issues/668)) ([#685](https://github.com/Brad-Edwards/aces/issues/685)) ([24d3e09](https://github.com/Brad-Edwards/aces/commit/24d3e097bae4d4e90032ab985f12702c8ad798f9)) +* **sdl:** define canonical YAML source profile ([#732](https://github.com/Brad-Edwards/aces/issues/732)) ([04d0682](https://github.com/Brad-Edwards/aces/commit/04d068208381410cc56468c9db2dcff92b2aa263)) + + +### Bug Fixes + +* derive honest version identifiers, consolidate on release-please (GOV-901) ([#750](https://github.com/Brad-Edwards/aces/issues/750)) ([526e777](https://github.com/Brad-Edwards/aces/commit/526e7770f178798db6ea5691f7770ba62d998b0f)) +* reject ambiguous SDL mapping keys ([#731](https://github.com/Brad-Edwards/aces/issues/731)) ([2a70e6a](https://github.com/Brad-Edwards/aces/commit/2a70e6ae92d96f3e7542d3f930ac6fb240244102)) + + +### Documentation + +* define ecosystem evolution governance policy ([#718](https://github.com/Brad-Edwards/aces/issues/718)) ([73d1a0f](https://github.com/Brad-Edwards/aces/commit/73d1a0ffd6f487ffc1d5dcfa63366bd069997482)) + ## [0.19.1](https://github.com/Brad-Edwards/aces/compare/v0.19.0...v0.19.1) (2026-07-06) diff --git a/contracts/README.md b/contracts/README.md index 221ee422e..42f9e620c 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -8,6 +8,8 @@ The goal of this bucket is organizational clarity: - `fixtures/` contains valid and invalid payload corpora for those contracts - `profiles/` contains capability profiles and separately versioned scientific-completeness taxonomy/assessment declarations +- `concept-authority/` contains canonical concept, vocabulary, reference-model, + and behavioral-relation catalogs - `realization-envelopes/` contains configuration-bound backend realization declarations whose identity is carried through manifests, plans, and snapshots @@ -68,7 +70,10 @@ apparatus-context, study/collection, capture specification, raw evidence record, and derived measure schemas under `contracts/schemas/experiment-core/`. These contracts are archival design artifacts for scientific experiment records; they do not add runtime execution, capture, storage, scheduling, statistical engines, -or API behavior by themselves. +or API behavior by themselves. Claim-bearing study and benchmark records bind +their conclusions to `aces-behavioral-relations@rev1`, including population or +case scope, measurement projection, evidence boundary, limitations, and +explicit nonclaims. `experiment-run-v1` is the canonical run provenance record. It carries the task/run/apparatus context, result and evidence pointers, traceability links to diff --git a/contracts/concept-authority/behavioral-relations-v1.json b/contracts/concept-authority/behavioral-relations-v1.json new file mode 100644 index 000000000..42176bdfc --- /dev/null +++ b/contracts/concept-authority/behavioral-relations-v1.json @@ -0,0 +1,2211 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "bibliography": [ + { + "source_id": "park-1981", + "title": "Concurrency and Automata on Infinite Sequences", + "authors": [ + "David M. R. Park" + ], + "publication_year": 1981, + "publication_venue": "Theoretical Computer Science, LNCS 104", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0017309" + } + }, + { + "source_id": "milner-1980", + "title": "A Calculus of Communicating Systems", + "authors": [ + "Robin Milner" + ], + "publication_year": 1980, + "publication_venue": "Lecture Notes in Computer Science 92", + "edition_or_version": "first edition", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/3-540-10235-3" + } + }, + { + "source_id": "van-glabbeek-1990", + "title": "The Linear Time-Branching Time Spectrum", + "authors": [ + "Rob J. van Glabbeek" + ], + "publication_year": 1990, + "publication_venue": "CONCUR 1990, LNCS 458", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0039066" + } + }, + { + "source_id": "abadi-lamport-1991", + "title": "The Existence of Refinement Mappings", + "authors": [ + "Martín Abadi", + "Leslie Lamport" + ], + "publication_year": 1991, + "publication_venue": "Theoretical Computer Science 82(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0304-3975(91)90224-P" + } + }, + { + "source_id": "lynch-vaandrager-1995", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "authors": [ + "Nancy A. Lynch", + "Frits W. Vaandrager" + ], + "publication_year": 1995, + "publication_venue": "Information and Computation 121(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1006/inco.1995.1134" + } + }, + { + "source_id": "fagin-halpern-moses-vardi-1995", + "title": "Reasoning About Knowledge", + "authors": [ + "Ronald Fagin", + "Joseph Y. Halpern", + "Yoram Moses", + "Moshe Y. Vardi" + ], + "publication_year": 1995, + "publication_venue": "MIT Press", + "edition_or_version": "hardcover first edition", + "immutable_locator": { + "kind": "isbn", + "value": "9780262061629" + } + }, + { + "source_id": "alur-henzinger-kupferman-vardi-1998", + "title": "Alternating Refinement Relations", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman", + "Moshe Y. Vardi" + ], + "publication_year": 1998, + "publication_venue": "CONCUR 1998, LNCS 1466", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0055622" + } + }, + { + "source_id": "alur-henzinger-kupferman-2002", + "title": "Alternating-Time Temporal Logic", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman" + ], + "publication_year": 2002, + "publication_venue": "Journal of the ACM 49(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/585265.585270" + } + }, + { + "source_id": "larsen-skou-1991", + "title": "Bisimulation Through Probabilistic Testing", + "authors": [ + "Kim G. Larsen", + "Arne Skou" + ], + "publication_year": 1991, + "publication_venue": "Information and Computation 94(1)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0890-5401(91)90030-6" + } + }, + { + "source_id": "wellek-2010", + "title": "Testing Statistical Hypotheses of Equivalence and Noninferiority", + "authors": [ + "Stefan Wellek" + ], + "publication_year": 2010, + "publication_venue": "Chapman and Hall/CRC", + "edition_or_version": "second edition", + "immutable_locator": { + "kind": "isbn", + "value": "9781439808184" + } + }, + { + "source_id": "bueno-1997", + "title": "Empirical Adequacy: A Partial Structures Approach", + "authors": [ + "Otávio Bueno" + ], + "publication_year": 1997, + "publication_venue": "Studies in History and Philosophy of Science Part A 28(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/S0039-3681(97)00012-5" + } + } + ], + "relations": { + "structural-validity": { + "relation_id": "structural-validity", + "display_name": "Structural validity", + "relation_class": "predicate", + "definition": "A single artifact satisfies its published closed structural schema.", + "left_carrier": "An artifact payload.", + "right_carrier": "The published schema selected by the artifact discriminator.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Schema validity is a predicate over an artifact and schema, not a transition-system relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Schema validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact payload and one schema revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Conformance to the declared structural shape.", + "proof_obligation": "Validate the complete payload against the named published schema." + }, + "bounded_evidence": [ + "JSON Schema and closed-model validation of named artifacts." + ], + "explicit_non_claims": [ + "Does not establish semantic validity, executability, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Backend equivalence", + "Participant strategic behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "contracts/schemas/" + ] + }, + "source_refs": [ + "milner-1980" + ] + }, + "semantic-validity": { + "relation_id": "semantic-validity", + "display_name": "Semantic validity", + "relation_class": "predicate", + "definition": "A structurally admitted artifact satisfies the named cross-reference and domain invariants.", + "left_carrier": "A parsed ACES artifact.", + "right_carrier": "The named semantic invariant set.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Semantic validity checks a static artifact model rather than matching transitions." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Semantic validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one admitted artifact and one invariant revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The named static semantic invariants.", + "proof_obligation": "Run every invariant in the declared semantic profile without error." + }, + "bounded_evidence": [ + "SemanticValidator results and invariant mutation tests." + ], + "explicit_non_claims": [ + "Does not establish realization, execution success, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime conformance", + "Backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/aces_sdl/validator/" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "capability-declaration": { + "relation_id": "capability-declaration", + "display_name": "Capability declaration", + "relation_class": "predicate", + "definition": "An apparatus declares support for governed capability and contract identifiers.", + "left_carrier": "A processor, backend, or participant manifest.", + "right_carrier": "The governed capability and contract vocabulary.", + "initial_states": "Not applicable; this is a declaration predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A declaration is not execution behavior." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Manifest consumer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one manifest revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Portable declared support metadata.", + "proof_obligation": "Validate the manifest and resolve every governed identifier." + }, + "bounded_evidence": [ + "Manifest schema validation and capability-gap conformance cases." + ], + "explicit_non_claims": [ + "Does not prove that a declared capability works for every input." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/aces_contracts/manifest_authority.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "profile-satisfaction": { + "relation_id": "profile-satisfaction", + "display_name": "Profile satisfaction", + "relation_class": "predicate", + "definition": "An artifact bundle satisfies every required concern in a named profile revision.", + "left_carrier": "An artifact or bundle.", + "right_carrier": "A governed profile with required concerns.", + "initial_states": "Not applicable; this is a profile predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Profile satisfaction aggregates named gates; it is not a behavioral matching relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Profile evaluator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact bundle and one profile revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The conjunction of the profile's required concerns.", + "proof_obligation": "Evaluate every required concern with the profile's named validator." + }, + "bounded_evidence": [ + "Scientific completeness and backend-profile case results." + ], + "explicit_non_claims": [ + "Does not promote profile satisfaction to behavioral equivalence or empirical adequacy." + ], + "incompatible_claim_surfaces": [ + "Behavioral equivalence", + "Scientific adequacy" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "contracts/profiles/" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "bounded-probe-success": { + "relation_id": "bounded-probe-success", + "display_name": "Bounded fixture or probe success", + "relation_class": "empirical", + "definition": "Every named finite fixture or probe in the disclosed run produced its expected result.", + "left_carrier": "A concrete implementation run.", + "right_carrier": "A finite, enumerated fixture or probe set.", + "initial_states": "The concrete initial state selected by each named case.", + "transition_signature": { + "applicability": "applicable", + "labels": "The actions exercised by the named cases.", + "transition_relation": "Only transitions actually exercised by the finite cases.", + "observable_actions": "Case outputs and sanitized diagnostics.", + "hidden_actions": "No hidden action unless a governed projection declares one.", + "stuttering_actions": "Stuttering is explicit and relation-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Case reporter", + "policy_ref": "behavioral-relations/bounded-probe-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only states reached by named cases.", + "traces": "Only enumerated finite traces.", + "schedulers": "Only schedulers exercised by the harness.", + "strategies": "Only strategies exercised by the harness.", + "environments": "Only named environments.", + "observations": "Only observations emitted by named cases." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Success of the enumerated cases.", + "proof_obligation": "Execute every named case and compare its bounded expected result." + }, + "bounded_evidence": [ + "Fixture-suite and target-probe reports with exact case identifiers." + ], + "explicit_non_claims": [ + "Does not quantify over untested transitions, schedulers, strategies, or environments.", + "Does not establish trace equivalence, simulation, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal conformance", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/aces_conformance/conformance.py" + ] + }, + "source_refs": [ + "park-1981", + "van-glabbeek-1990" + ] + }, + "canonical-artifact-identity": { + "relation_id": "canonical-artifact-identity", + "display_name": "Canonical artifact identity", + "relation_class": "predicate", + "definition": "Two artifacts have identical canonical bytes or digest under one named serialization profile.", + "left_carrier": "One canonical artifact.", + "right_carrier": "Another canonical artifact under the same profile.", + "initial_states": "Not applicable; this is artifact identity.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Digest identity compares canonical representations, not enabled behavior." + }, + "observation_projection": { + "applicability": "identity", + "subject": "Canonical serializer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "For the two named canonical artifacts.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Canonical byte identity under the named profile.", + "proof_obligation": "Canonicalize both artifacts with the same revision and compare bytes or collision-resistant digests." + }, + "bounded_evidence": [ + "Canonicalization and digest equality tests." + ], + "explicit_non_claims": [ + "Does not establish common provenance, equal executions, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Trace comparison", + "Backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/aces_sdl/canonical.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-membership": { + "relation_id": "realization-envelope-membership", + "display_name": "Realization-envelope membership", + "relation_class": "set-relation", + "definition": "A concrete or requested point belongs to a governed realization envelope.", + "left_carrier": "A realization point.", + "right_carrier": "A closed realization-envelope set.", + "initial_states": "Not applicable; this is set membership.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope membership is set-theoretic support, not a transition match." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "For one point and one envelope revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Membership in the declared support set.", + "proof_obligation": "Evaluate every envelope dimension and closure rule for the point." + }, + "bounded_evidence": [ + "Witness and negative-probe envelope tests." + ], + "explicit_non_claims": [ + "Does not establish that execution succeeds or that behavior refines an abstract runtime." + ], + "incompatible_claim_surfaces": [ + "Runtime trace inclusion", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/realization/envelope-semantics.md" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-subsumption": { + "relation_id": "realization-envelope-subsumption", + "display_name": "Realization-envelope subsumption", + "relation_class": "set-relation", + "definition": "Every point admitted by one realization envelope is admitted by another under the named closure rules.", + "left_carrier": "One realization-envelope set.", + "right_carrier": "Another realization-envelope set.", + "initial_states": "Not applicable; this is set inclusion.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope subsumption compares support sets rather than transition systems." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "Universally over points in the left envelope.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Set inclusion of declared realization support.", + "proof_obligation": "Prove or decide inclusion for every governed envelope dimension." + }, + "bounded_evidence": [ + "Finite witness and mutation tests for current envelope operators." + ], + "explicit_non_claims": [ + "Does not establish behavioral refinement, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime behavior", + "Participant behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/aces_contracts/realization_envelope.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "trace-inclusion": { + "relation_id": "trace-inclusion", + "display_name": "Projected trace inclusion", + "relation_class": "behavioral", + "definition": "Every projected concrete trace belongs to the abstract trace set under a declared projection.", + "left_carrier": "Concrete implementation transition system.", + "right_carrier": "Abstract ACES transition system.", + "initial_states": "Related concrete and abstract initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Labels in the declared concrete and abstract alphabets.", + "transition_relation": "Concrete and abstract labelled transition relations.", + "observable_actions": "Actions retained by the governed projection.", + "hidden_actions": "Only actions explicitly hidden by the projection.", + "stuttering_actions": "Concrete stuttering must be permitted by the abstract obligation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named observer or abstraction", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All reachable related states.", + "traces": "Universally over projected concrete traces.", + "schedulers": "All admitted schedulers unless narrowed.", + "strategies": "Outside scope unless the systems are strategic.", + "environments": "All admitted environments unless narrowed.", + "observations": "Through the named projection only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Abstract trace safety for projected concrete executions.", + "proof_obligation": "Show Proj(Traces_concrete) is a subset of Traces_abstract under stated fairness and divergence assumptions." + }, + "bounded_evidence": [ + "Finite target probes can falsify but cannot prove universal inclusion." + ], + "explicit_non_claims": [ + "Does not establish completeness, reverse inclusion, trace equivalence, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance report" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "van-glabbeek-1990", + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "trace-equivalence": { + "relation_id": "trace-equivalence", + "display_name": "Trace equivalence", + "relation_class": "behavioral", + "definition": "Two systems have equal projected trace sets under the same declared alphabet and projection.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "Paired initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "A shared declared label alphabet.", + "transition_relation": "The two labelled transition relations.", + "observable_actions": "Labels retained by the common projection.", + "hidden_actions": "Labels hidden by the common projection.", + "stuttering_actions": "Stuttering treatment must be identical." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All reachable states contributing traces.", + "traces": "Universally over both trace sets.", + "schedulers": "All admitted schedulers.", + "strategies": "Outside scope unless strategies are encoded.", + "environments": "All declared environments.", + "observations": "Through one common projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality of projected trace languages.", + "proof_obligation": "Prove both projected trace inclusions under identical assumptions." + }, + "bounded_evidence": [ + "Finite trace comparison may refute but cannot establish equality." + ], + "explicit_non_claims": [ + "Does not preserve branching structure and does not imply bisimulation." + ], + "incompatible_claim_surfaces": [ + "Finite backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "van-glabbeek-1990" + ] + }, + "forward-simulation": { + "relation_id": "forward-simulation", + "display_name": "Forward simulation", + "relation_class": "behavioral", + "definition": "A relation maps each concrete step to an abstract matching path while preserving related states.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Every concrete initial state relates to an abstract initial state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under a declared matching function.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Labels exposed by the abstraction.", + "hidden_actions": "Labels mapped to hidden or stuttering abstract behavior.", + "stuttering_actions": "Explicit abstract stuttering where allowed." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable states.", + "traces": "All concrete traces induced by matched steps.", + "schedulers": "All admitted concrete choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Usually projected trace inclusion and named safety properties.", + "proof_obligation": "Supply a simulation relation and discharge initiality plus step-correspondence obligations." + }, + "bounded_evidence": [ + "Tests may exercise candidate obligations on finite models only." + ], + "explicit_non_claims": [ + "Successful probes do not establish a simulation relation." + ], + "incompatible_claim_surfaces": [ + "Current conformance results" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995", + "abadi-lamport-1991" + ] + }, + "backward-simulation": { + "relation_id": "backward-simulation", + "display_name": "Backward simulation", + "relation_class": "behavioral", + "definition": "A relation reasons from abstract successor possibilities back to concrete predecessors to establish implementation inclusion where forward simulation is insufficient.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Initial and reachable-state coverage follow the selected backward-simulation theorem.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under the theorem's matching rule.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Declared external labels.", + "hidden_actions": "Declared internal labels.", + "stuttering_actions": "History, prophecy, and stuttering treatment must be explicit." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over theorem-defined related states.", + "traces": "All represented implementation behaviors.", + "schedulers": "All admitted nondeterministic choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Implementation behavior inclusion under the cited theorem's assumptions.", + "proof_obligation": "Supply a backward simulation relation and discharge its reachability, initiality, and step obligations." + }, + "bounded_evidence": [ + "Finite model tests can exercise examples but do not prove a backend relation." + ], + "explicit_non_claims": [ + "Does not follow from result equality or a forward-only sampled trace." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995" + ] + }, + "data-refinement": { + "relation_id": "data-refinement", + "display_name": "Data refinement", + "relation_class": "behavioral", + "definition": "Concrete data states represent abstract states through a retrieve relation while operations preserve that relation.", + "left_carrier": "Concrete state and operation space.", + "right_carrier": "Abstract state and operation space.", + "initial_states": "Concrete and abstract initial states related by the retrieve relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Operation invocations and observations.", + "transition_relation": "Concrete and abstract operation relations.", + "observable_actions": "Client-visible operation effects.", + "hidden_actions": "Internal representation steps.", + "stuttering_actions": "Stuttering and enabledness obligations are method-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Client observation", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All states satisfying the retrieve relation.", + "traces": "All operation histories covered by the refinement method.", + "schedulers": "All admitted operation nondeterminism.", + "strategies": "Outside scope unless strategies are modeled.", + "environments": "All client environments under stated preconditions.", + "observations": "Client-visible results only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "The abstract operation contract and selected client-observable properties.", + "proof_obligation": "Define the retrieve relation and discharge initialization, applicability/enabledness, and correctness obligations." + }, + "bounded_evidence": [ + "Contract and operation tests are bounded evidence only." + ], + "explicit_non_claims": [ + "An SDL transformation function is not data refinement without these obligations." + ], + "incompatible_claim_surfaces": [ + "SDL phase transformation" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "strong-bisimulation": { + "relation_id": "strong-bisimulation", + "display_name": "Strong Park-Milner bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches every labelled step immediately in both directions.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "The two initial states belong to the bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common label alphabet including internal labels.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Every label is matched exactly.", + "hidden_actions": "Hidden labels are still labels and must match immediately.", + "stuttering_actions": "Only explicitly labelled stuttering steps can match." + }, + "observation_projection": { + "applicability": "identity", + "subject": "External comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "Universally over every related state pair.", + "traces": "All branching continuations.", + "schedulers": "All nondeterministic branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All transition-system environments encoded in state.", + "observations": "Identity observation of labels." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Branching structure and modal properties under the chosen semantics.", + "proof_obligation": "Exhibit a symmetric relation closed under immediate labelled steps in both directions." + }, + "bounded_evidence": [ + "Finite algorithms can decide the relation only for supplied finite models." + ], + "explicit_non_claims": [ + "One shared trace, result, digest, or terminal observation is insufficient." + ], + "incompatible_claim_surfaces": [ + "Finite probes", + "Digest comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "park-1981", + "milner-1980" + ] + }, + "weak-bisimulation": { + "relation_id": "weak-bisimulation", + "display_name": "Weak or observational bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches visible actions through closure over explicitly hidden tau steps.", + "left_carrier": "One labelled transition system with tau.", + "right_carrier": "Another labelled transition system with tau.", + "initial_states": "Initial states related after the selected tau closure.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common visible alphabet plus the declared tau label.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Visible labels match through weak transitions.", + "hidden_actions": "Only the explicitly governed tau label is hidden.", + "stuttering_actions": "Tau closure and stuttering are explicit; divergence treatment is declared." + }, + "observation_projection": { + "applicability": "required", + "subject": "Observer that hides tau", + "policy_ref": "behavioral-relations/tau-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states under tau closure.", + "traces": "All weak traces and branching continuations.", + "schedulers": "All nondeterministic tau and visible branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All encoded environments.", + "observations": "Through the named hiding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Observation-preserving branching behavior under stated divergence and termination semantics.", + "proof_obligation": "Exhibit a weak bisimulation relation and discharge both directional weak-step obligations." + }, + "bounded_evidence": [ + "The catalog's hidden-action example demonstrates the definition on a finite toy model." + ], + "explicit_non_claims": [ + "Backend-internal work is not tau unless a governed projection declares it." + ], + "incompatible_claim_surfaces": [ + "Undeclared backend hiding" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "future", + "evidence_refs": [ + "contracts/concept-authority/behavioral-relations-v1.json#worked_examples" + ] + }, + "source_refs": [ + "milner-1980", + "van-glabbeek-1990" + ] + }, + "participant-projected-history-equivalence": { + "relation_id": "participant-projected-history-equivalence", + "display_name": "Participant-projected history equivalence", + "relation_class": "epistemic", + "definition": "Two finite or complete histories have equal projections for one participant under one observation-boundary revision.", + "left_carrier": "One global or backend history.", + "right_carrier": "Another global or backend history.", + "initial_states": "The compared histories share a declared participant and starting information state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant-visible events after projection.", + "transition_relation": "Underlying history extension relations.", + "observable_actions": "Events admitted by the participant observation boundary.", + "hidden_actions": "Events removed or redacted by the boundary.", + "stuttering_actions": "No additional stuttering assumption beyond projected history equality." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "States represented in the compared histories.", + "traces": "Finite histories unless a universal claim is separately proved.", + "schedulers": "Only schedulers represented in the histories.", + "strategies": "No strategic quantification.", + "environments": "Only the named environment/run context.", + "observations": "One participant and one policy revision." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality or indistinguishability of the named participant's projected histories.", + "proof_obligation": "Apply the existing participant observation boundary to both histories and compare the resulting ordered visible records." + }, + "bounded_evidence": [ + "Participant behavior-history and observation-envelope tests on named histories." + ], + "explicit_non_claims": [ + "Does not imply equality of global state, future behavior, knowledge, or strategy." + ], + "incompatible_claim_surfaces": [ + "Global-state comparison", + "Strategic equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995", + "milner-1980" + ] + }, + "epistemic-indistinguishability": { + "relation_id": "epistemic-indistinguishability", + "display_name": "Epistemic indistinguishability", + "relation_class": "epistemic", + "definition": "Two worlds are indistinguishable to an agent when they occupy the same governed information set.", + "left_carrier": "One epistemic world/state.", + "right_carrier": "Another epistemic world/state.", + "initial_states": "Worlds in the same agent-indexed accessibility or information relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Optional temporal or action labels of the epistemic model.", + "transition_relation": "The declared interpreted-system or Kripke transition relation.", + "observable_actions": "Agent-observable propositions and events.", + "hidden_actions": "Facts excluded by the information projection.", + "stuttering_actions": "Stuttering is model-specific." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named agent", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All worlds in the selected information relation.", + "traces": "Histories only when the interpreted-system model includes them.", + "schedulers": "All schedulers represented in the model.", + "strategies": "Strategies are outside the relation itself.", + "environments": "All environments represented by possible worlds.", + "observations": "One agent or explicitly named group." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Truth of formulas invariant over the selected information set, subject to the logic.", + "proof_obligation": "Define possible worlds, the agent-indexed indistinguishability relation, valuation, and any temporal interaction." + }, + "bounded_evidence": [ + "Equal projected finite histories may be evidence for a bounded information-state comparison." + ], + "explicit_non_claims": [ + "Does not follow from global-state equality and does not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current participant conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995" + ] + }, + "alternating-strategic-equivalence": { + "relation_id": "alternating-strategic-equivalence", + "display_name": "Alternating or strategic equivalence", + "relation_class": "strategic", + "definition": "Two game structures preserve the abilities of named coalitions under explicit strategy and environment quantifiers.", + "left_carrier": "One concurrent or alternating game structure.", + "right_carrier": "Another concurrent or alternating game structure.", + "initial_states": "Related initial game states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Joint actions, chance outcomes, and state transitions.", + "transition_relation": "Both game transition functions or relations.", + "observable_actions": "Player observations and public actions.", + "hidden_actions": "Hidden information under the named observation partitions.", + "stuttering_actions": "Stuttering, simultaneous moves, and scheduler steps are explicit." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named players or coalition", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related game states.", + "traces": "Outcome paths induced by quantified strategies.", + "schedulers": "Schedulers and chance kernels explicitly quantified.", + "strategies": "Coalitions and strategy classes universally/existentially quantified as declared.", + "environments": "Adversarial environment choices explicitly quantified.", + "observations": "Player-indexed observation partitions." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Environment and scheduler choices are explicit." + }, + "concurrency": { + "status": "supported", + "treatment": "Joint and simultaneous moves are part of the game structure." + }, + "probability": { + "status": "parameterized", + "treatment": "Chance kernels must be declared when present." + }, + "time": { + "status": "parameterized", + "treatment": "Timed strategies require an explicit clock model." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Concurrent action order is part of the declared game semantics." + } + }, + "preservation": { + "property": "Coalition ability for the stated objective class.", + "proof_obligation": "Define players, legal joint actions, observations, strategy class, coalitions, chance, scheduler/fairness, objectives, and an alternating relation in both directions." + }, + "bounded_evidence": [ + "Finite recorded joint-action traces can only falsify selected cases." + ], + "explicit_non_claims": [ + "Capability declarations and shared probe outcomes do not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current multi-agent conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "alur-henzinger-kupferman-2002" + ] + }, + "probabilistic-bisimulation": { + "relation_id": "probabilistic-bisimulation", + "display_name": "Probabilistic bisimulation", + "relation_class": "behavioral", + "definition": "Related states match labelled probability distributions over equivalence classes under the selected probabilistic process model.", + "left_carrier": "One probabilistic labelled transition system.", + "right_carrier": "Another probabilistic labelled transition system.", + "initial_states": "Initial states belong to the probabilistic bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Shared visible and hidden labels of the probabilistic model.", + "transition_relation": "Labelled transitions to probability distributions.", + "observable_actions": "Labels retained by the projection.", + "hidden_actions": "Declared hidden labels.", + "stuttering_actions": "Weak variants require an explicit probabilistic tau closure." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Probabilistic process observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states.", + "traces": "All probabilistic traces or branching distributions required by the variant.", + "schedulers": "All nondeterministic schedulers explicitly quantified.", + "strategies": "Outside scope unless combined with games.", + "environments": "All admitted probabilistic environments.", + "observations": "Through the named projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Scheduler quantification is mandatory." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The process-composition semantics must be named." + }, + "probability": { + "status": "supported", + "treatment": "Probability distributions are matched over relation classes." + }, + "time": { + "status": "outside-scope", + "treatment": "Continuous or timed probability needs another variant." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The base relation uses labelled branching structure." + } + }, + "preservation": { + "property": "Probability mass over related behavior classes.", + "proof_obligation": "Exhibit a relation whose matched transitions assign equal probability to every relation-closed class under the chosen variant." + }, + "bounded_evidence": [ + "Statistical samples may refute parameters but do not prove distributional branching equivalence." + ], + "explicit_non_claims": [ + "Statistical similarity or equal sample means is not probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Ordinary empirical study" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "larsen-skou-1991" + ] + }, + "statistical-similarity": { + "relation_id": "statistical-similarity", + "display_name": "Statistical similarity", + "relation_class": "empirical", + "definition": "A predeclared metric over sampled populations lies within a stated similarity criterion with uncertainty.", + "left_carrier": "One sampled population or system output distribution.", + "right_carrier": "Another sampled population, target distribution, or reference data.", + "initial_states": "The preregistered sampling frame and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Statistical similarity compares sampled measures, not transition systems unless a separate model binds them." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled observations only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by the sampling design.", + "strategies": "Strategies represented by the sampling design.", + "environments": "The preregistered population and apparatus.", + "observations": "The named metric/estimand projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is represented through the sampling model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency matters only through measured outcomes." + }, + "probability": { + "status": "supported", + "treatment": "The sampling distribution and uncertainty method are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Sampling windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Event partial order is not inferred from aggregate metrics." + } + }, + "preservation": { + "property": "The stated similarity criterion for the named estimand and population.", + "proof_obligation": "Predeclare population, sampling frame, metric, criterion, uncertainty method, and decision rule; then execute the study." + }, + "bounded_evidence": [ + "Experiment runs, derived measures, and uncertainty intervals." + ], + "explicit_non_claims": [ + "Does not establish behavioral, epistemic, strategic, or probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "statistical-equivalence": { + "relation_id": "statistical-equivalence", + "display_name": "Statistical equivalence", + "relation_class": "empirical", + "definition": "A preregistered equivalence test supports that a named estimand lies within a stated equivalence margin for the sampled population.", + "left_carrier": "One sampled population or treatment.", + "right_carrier": "Another sampled population, treatment, or reference.", + "initial_states": "The preregistered sampling frame, allocation, and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A statistical equivalence test does not compare enabled transitions." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled units only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by allocation/sampling.", + "strategies": "Strategies represented by sampled conditions.", + "environments": "The stated target population.", + "observations": "The named estimand and measurement projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is handled by the statistical model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is only a measured covariate unless modeled." + }, + "probability": { + "status": "supported", + "treatment": "Equivalence margins, error rates, and uncertainty are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Study windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Aggregate equivalence does not preserve event order." + } + }, + "preservation": { + "property": "Equivalence of the named estimand within the preregistered margin.", + "proof_obligation": "Specify the equivalence hypotheses, margin, error control, sampling design, and analysis before observing results." + }, + "bounded_evidence": [ + "Experiment-study analysis and derived measures." + ], + "explicit_non_claims": [ + "Statistical equivalence is not behavioral equivalence or proof of implementation conformance." + ], + "incompatible_claim_surfaces": [ + "Bisimulation claim" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "empirical-adequacy": { + "relation_id": "empirical-adequacy", + "display_name": "Empirical adequacy", + "relation_class": "empirical", + "definition": "Observed evidence supports a purpose-relative, bounded adequacy claim for a named phenomenon and intended use.", + "left_carrier": "A model, language, implementation, or method under study.", + "right_carrier": "A defined empirical target, task, or phenomenon.", + "initial_states": "The preregistered study population, tasks, and apparatus.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Empirical adequacy may study behavior but is not itself a transition-system equivalence." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study audience", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Observed study units.", + "traces": "Observed runs and tasks.", + "schedulers": "Schedulers represented by the protocol.", + "strategies": "Strategies represented by participant/task sampling.", + "environments": "The named target population and intended use.", + "observations": "The preregistered measures and coding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Uncontrolled variation is handled as a validity limitation." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is measured only when the protocol names it." + }, + "probability": { + "status": "parameterized", + "treatment": "Sampling and uncertainty must be reported." + }, + "time": { + "status": "parameterized", + "treatment": "Study period and temporal validity are explicit." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Adequacy does not imply partial-order preservation." + } + }, + "preservation": { + "property": "Fitness for the explicitly stated empirical purpose within the study boundary.", + "proof_obligation": "Predeclare tasks, population, measures, success/falsification criteria, analysis, limitations, and evidence lineage." + }, + "bounded_evidence": [ + "Independent parser, authoring, review, and diagnostic-recovery studies." + ], + "explicit_non_claims": [ + "Repeated bounded observations do not establish universal semantics, conformance, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Universal language equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "bueno-1997", + "wellek-2010" + ] + } + }, + "claim_surfaces": [ + { + "surface_id": "sdl-transformation", + "intended_relation_ids": [ + "structural-validity", + "semantic-validity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Deterministic phase functions and finite invariant, round-trip, canonicalization, and property tests.", + "prohibited_relation_ids": [ + "data-refinement", + "forward-simulation", + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "No universal behavioral refinement or equivalence is currently proved." + ] + }, + { + "surface_id": "backend-realization", + "intended_relation_ids": [ + "realization-envelope-membership", + "bounded-probe-success", + "trace-inclusion" + ], + "evidence_boundary": "Envelope checks and named target probes are bounded evidence; projected trace inclusion is the intended universal obligation and remains deliberately unproved.", + "prohibited_relation_ids": [ + "trace-equivalence", + "backward-simulation", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Provisioning, snapshots, witnesses, and negative probes do not prove behavioral equivalence." + ] + }, + { + "surface_id": "backend-comparison", + "intended_relation_ids": [ + "bounded-probe-success", + "statistical-similarity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Only named invariants, probes, digests, populations, metrics, and uncertainty procedures are compared.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "A shared result, digest, or finite trace is not universal same behavior." + ] + }, + { + "surface_id": "participant-visible-behavior", + "intended_relation_ids": [ + "participant-projected-history-equivalence" + ], + "evidence_boundary": "Comparison is parameterized by participant, observation-boundary policy revision, redaction, order, simultaneity, and run context.", + "prohibited_relation_ids": [ + "epistemic-indistinguishability", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Equal projected histories do not expose hidden global state or prove future knowledge or strategy equivalence." + ] + }, + { + "surface_id": "multi-agent-interaction", + "intended_relation_ids": [ + "bounded-probe-success", + "alternating-strategic-equivalence", + "probabilistic-bisimulation" + ], + "evidence_boundary": "Current evidence is structural and finite; strategic and probabilistic relations are definitions for future governed models.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Current joint-action, chance, simultaneous-move, and mean-field records do not prove strategic equivalence." + ] + }, + { + "surface_id": "independent-adequacy-study", + "intended_relation_ids": [ + "empirical-adequacy", + "statistical-similarity", + "statistical-equivalence" + ], + "evidence_boundary": "Claims bind to a preregistered population, task set, metric or coding scheme, uncertainty, falsification criteria, and limitations.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Bounded observations and statistical findings cannot be promoted to universal behavioral proof." + ] + } + ], + "worked_examples": { + "finite-probe-counterexample": { + "example_id": "finite-probe-counterexample", + "purpose": "Two implementations pass the same finite visible probe a, but the left system has an additional enabled b transition that the right system cannot match.", + "left_system": { + "states": [ + "l0", + "l1", + "l2" + ], + "initial_state": "l0", + "transitions": [ + { + "source": "l0", + "action": "a", + "target": "l1" + }, + { + "source": "l0", + "action": "b", + "target": "l2" + } + ] + }, + "right_system": { + "states": [ + "r0", + "r1" + ], + "initial_state": "r0", + "transitions": [ + { + "source": "r0", + "action": "a", + "target": "r1" + } + ] + }, + "tested_visible_trace": [ + "a" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": false, + "evidence_boundary": "The shared a probe is evidence only for that finite trace; the unmatched b branch refutes strong bisimulation.", + "explicit_non_claims": [ + "This toy counterexample is not evidence about any ACES backend." + ] + }, + "hidden-action-counterexample": { + "example_id": "hidden-action-counterexample", + "purpose": "The abstract system performs visible send directly; the backend performs governed hidden tau and then send.", + "left_system": { + "states": [ + "a0", + "a1" + ], + "initial_state": "a0", + "transitions": [ + { + "source": "a0", + "action": "send", + "target": "a1" + } + ] + }, + "right_system": { + "states": [ + "b0", + "b1", + "b2" + ], + "initial_state": "b0", + "transitions": [ + { + "source": "b0", + "action": "tau", + "target": "b1" + }, + { + "source": "b1", + "action": "send", + "target": "b2" + } + ] + }, + "tested_visible_trace": [ + "send" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": true, + "evidence_boundary": "Strong matching fails on tau; weak visible-trace matching succeeds only under the declared tau-hiding projection and finite termination assumptions.", + "explicit_non_claims": [ + "The example does not declare arbitrary backend-internal work hidden and does not prove an ACES backend relation." + ] + } + } +} diff --git a/contracts/concept-authority/concept-families-v1.json b/contracts/concept-authority/concept-families-v1.json index a613a7e94..cdea96168 100644 --- a/contracts/concept-authority/concept-families-v1.json +++ b/contracts/concept-authority/concept-families-v1.json @@ -71,6 +71,22 @@ "Run or study concepts must not be used to classify nodes, accounts, observables, tools, or events." ] }, + "behavioral-relations": { + "title": "Behavioral Relations", + "description": "Revisioned predicates and relations used to state validation, conformance, comparison, refinement, equivalence, and empirical claims.", + "provenance": "native", + "extension_scope": "ACES claim semantics that bind a named relation to carriers, transition and observation policies, quantifier scope, evidence boundary, assurance status, and explicit nonclaims.", + "relation_rules": [ + "May classify claims about `scenarios`, `apparatus-declarations`, `realization-and-disclosure`, `tasks-runs-studies`, participant histories, and `provenance-and-evidence` without reclassifying the underlying artifacts.", + "Must bind participant-visible comparisons to the governed observation projection and must keep finite evidence separate from universal trace, simulation, refinement, or bisimulation obligations.", + "Must use `actions-and-events` and `observables` for the labels and observations carried by a transition system; this family classifies the relation over carriers, not those cyber-domain objects." + ], + "non_ambiguity_constraints": [ + "Must not be used as a synonym for structural validity, schema conformance, execution success, artifact identity, profile satisfaction, or empirical adequacy; each is a distinct governed relation.", + "A relation name without its carrier, projection, quantifier, evidence boundary, and assurance status must not be treated as a behavioral claim.", + "Bounded probes, equal digests, equal result tuples, and shared projected observations must not be promoted to behavioral, epistemic, strategic, probabilistic, timed, or partial-order equivalence." + ] + }, "episodes": { "title": "Episodes", "description": "Participant runtime episode identity, lifecycle state, and history boundaries.", diff --git a/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json b/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json new file mode 100644 index 000000000..41025c484 --- /dev/null +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/invalid/missing-taxonomy-id.json @@ -0,0 +1,8 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_revision": "rev1", + "bibliography": [], + "relations": {}, + "claim_surfaces": [], + "worked_examples": {} +} diff --git a/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json new file mode 100644 index 000000000..48782f68a --- /dev/null +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json @@ -0,0 +1,107 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "bibliography": [ + { + "source_id": "milner-1980", + "title": "A Calculus of Communicating Systems", + "authors": ["Robin Milner"], + "publication_year": 1980, + "publication_venue": "Lecture Notes in Computer Science", + "edition_or_version": "First edition", + "immutable_locator": {"kind": "doi", "value": "10.1007/3-540-10235-3"} + } + ], + "relations": { + "bounded-probe-success": { + "relation_id": "bounded-probe-success", + "display_name": "Bounded probe success", + "relation_class": "predicate", + "definition": "Every named finite case has its expected result.", + "left_carrier": "A concrete implementation run.", + "right_carrier": "A finite named case set.", + "initial_states": "The setup state of each named case.", + "transition_signature": { + "applicability": "applicable", + "labels": "Named case actions.", + "transition_relation": "Transitions exercised by each case.", + "observable_actions": "Case-reported actions.", + "hidden_actions": "Harness actions not reported by the case.", + "stuttering_actions": "Declared no-op harness actions." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Case reporter", + "policy_ref": "behavioral-relations/bounded-probe-projection", + "policy_revision": "rev1", + "redaction_scope": "Only named case outputs are retained.", + "order_treatment": "Case order is preserved.", + "simultaneity_treatment": "Only explicitly declared simultaneity is retained." + }, + "projection_required": true, + "direction": "unary", + "quantification": { + "states": "States reached by named cases.", + "traces": "Enumerated finite traces.", + "schedulers": "Schedulers exercised by the harness.", + "strategies": "Strategies exercised by the harness.", + "environments": "Named environments.", + "observations": "Case-reported observations." + }, + "dimensions": { + "nondeterminism": {"status": "parameterized", "treatment": "Only sampled choices are covered."}, + "concurrency": {"status": "parameterized", "treatment": "Only exercised interleavings are covered."}, + "probability": {"status": "outside-scope", "treatment": "No distribution claim is made."}, + "time": {"status": "parameterized", "treatment": "Only the case clock is covered."}, + "partial_order": {"status": "outside-scope", "treatment": "No event-structure claim is made."} + }, + "preservation": { + "property": "Expected outcomes of named cases.", + "proof_obligation": "Execute each named case and compare its bounded expected result." + }, + "bounded_evidence": ["A passing fixture suite."], + "explicit_non_claims": ["Finite success does not establish trace equivalence or bisimulation."], + "incompatible_claim_surfaces": ["Universal behavioral equivalence."], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": ["implementations/python/packages/aces_conformance/conformance.py"] + }, + "source_refs": ["milner-1980"] + } + }, + "claim_surfaces": [ + { + "surface_id": "fixture-surface", + "intended_relation_ids": ["bounded-probe-success"], + "evidence_boundary": "The one relation in this minimal fixture.", + "prohibited_relation_ids": ["bounded-probe-success"], + "explicit_non_claims": ["This fixture is not the canonical claim-surface policy."] + } + ], + "worked_examples": { + "minimal-probe": { + "example_id": "minimal-probe", + "purpose": "Exercise the transition-system fixture shape.", + "left_system": { + "states": ["l0", "l1"], + "initial_state": "l0", + "transitions": [{"source": "l0", "action": "a", "target": "l1"}] + }, + "right_system": { + "states": ["r0", "r1"], + "initial_state": "r0", + "transitions": [{"source": "r0", "action": "a", "target": "r1"}] + }, + "tested_visible_trace": ["a"], + "hidden_action": "tau", + "expected_strong_bisimulation": true, + "expected_weak_matching": true, + "evidence_boundary": "One visible action in a minimal fixture.", + "explicit_non_claims": ["The fixture does not prove the canonical catalog's relation set."] + } + } +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/vulnerability-severity-summary.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/vulnerability-severity-summary.json new file mode 100644 index 000000000..12fe51aef --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/vulnerability-severity-summary.json @@ -0,0 +1,33 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-vulnerability-severity-summary-001", + "measure_version": "1.0.0", + "measure_kind": "summary", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "vulnerability-severity-summary", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "scanner-finding-severity-counter", + "method_version": "1.0.0", + "name": "Scanner finding severity counter", + "description": "Derives a time-scoped severity count from raw scanner evidence.", + "parameters": [] + }, + "source_evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-vulnerability-scan-001", + "ref_version": "1.0.0" + } + ], + "generated_at": "2026-07-13T00:01:05Z", + "value_status": "reported", + "value": "critical=0,high=1,medium=2,low=0,unknown=0", + "uncertainty": "Counts are valid only for the scanner database and immutable image digest named by the source evidence.", + "limitations": [ + "Scanner findings are time-sensitive analysis and do not become authored SDL vulnerabilities." + ], + "provenance_refs": [] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/docker-network-endpoint-observation.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/docker-network-endpoint-observation.json new file mode 100644 index 000000000..c9b3b60b3 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/docker-network-endpoint-observation.json @@ -0,0 +1,37 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-docker-network-endpoint-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-runtime-inventory-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "docker-network-endpoint-observation", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-runtime-inventory-001", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "docker-network-inspect", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "artifact", + "captured_at": "2026-07-13T00:00:05Z", + "capture_window_ref": "inventory-window", + "raw_content": { + "content_uri": "runs/run-runtime-inventory-001/docker-network-inspect.json", + "content_checksum": { + "algorithm": "sha256", + "value": "2222222222222222222222222222222222222222222222222222222222222222" + }, + "payload_summary": "Docker network inspection preserving generated network and endpoint IDs, generated DNS identity, MAC address, driver, and IPAM output without promoting them to SDL declaration identity." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/runtime-health-observation.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/runtime-health-observation.json new file mode 100644 index 000000000..b540be463 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/runtime-health-observation.json @@ -0,0 +1,37 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-runtime-health-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-runtime-inventory-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "container-health-observation", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-runtime-inventory-001", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "container-runtime-inspect", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "observation", + "captured_at": "2026-07-13T00:00:00Z", + "capture_window_ref": "inventory-window", + "raw_content": { + "content_uri": "runs/run-runtime-inventory-001/container-health.json", + "content_checksum": { + "algorithm": "sha256", + "value": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "payload_summary": "Container health status, failing streak, timestamps, exit codes, and bounded check-output metadata captured separately from the authored healthcheck condition." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/scanner-vulnerability-observation.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/scanner-vulnerability-observation.json new file mode 100644 index 000000000..4dbbd7745 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/scanner-vulnerability-observation.json @@ -0,0 +1,37 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-vulnerability-scan-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-runtime-inventory-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "vulnerability-scan-output", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-runtime-inventory-001", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "vulnerability-scanner", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "artifact", + "captured_at": "2026-07-13T00:01:00Z", + "capture_window_ref": "inventory-window", + "raw_content": { + "content_uri": "runs/run-runtime-inventory-001/vulnerability-scan.json", + "content_checksum": { + "algorithm": "sha256", + "value": "3333333333333333333333333333333333333333333333333333333333333333" + }, + "payload_summary": "Machine-readable vulnerability findings with scanner version, advisory database version, scan time, and immutable image digest." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [] +} diff --git a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json index 03b4a1aa7..735ad39bb 100644 --- a/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json @@ -10,6 +10,24 @@ "research_questions": [ "How often does the baseline participant policy achieve initial foothold under the fixed task protocol?" ], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "relation_id": "empirical-adequacy", + "subject": "TechVault baseline study", + "left_carrier_ref": "study-techvault-baseline@1.0.0", + "observation_projection_ref": "analysis-plan:baseline-proportion", + "observation_projection_revision": "experiment-study/v1", + "quantifier_scope": "sampled-population", + "evidence_scope": "statistical", + "evidence_boundary": "The one predeclared baseline condition and included evaluation runs.", + "assurance_status": "deliberately-unproved", + "evidence_refs": ["analysis-plan:baseline-proportion"], + "limitations": ["The fixture has one run and cannot establish population adequacy."], + "explicit_non_claims": ["No behavioral equivalence or generalizability claim."] + } + ], "membership": { "task": { "target_ref": { diff --git a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json index 9b98e2486..a1ba85410 100644 --- a/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json +++ b/contracts/fixtures/profiles/scientific-completeness-taxonomy-v1/valid/minimal.json @@ -16,6 +16,23 @@ "title": "Valid SDL fragment", "intended_claim": "The source is admitted as SDL.", "explicit_non_claims": ["No deployability or scientific-adequacy claim."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "relation_id": "semantic-validity", + "subject": "One SDL source", + "left_carrier_ref": "fixture:source", + "quantifier_scope": "single-artifact", + "evidence_scope": "structural", + "evidence_boundary": "Validation of the fixture source only.", + "assurance_status": "tested", + "evidence_refs": [], + "limitations": ["The validation result is local to this artifact."], + "explicit_non_claims": ["No behavioral equivalence claim."] + } + ], + "non_claimed_relation_ids": ["trace-equivalence"], "dispositions": {"source-profile-validity": "required"}, "example_refs": [] } diff --git a/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json b/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json index 2b1918d20..f9c354385 100644 --- a/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json +++ b/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json @@ -208,8 +208,15 @@ "evidence": [{"kind": "implementation", "path": "implementations/python/packages/aces_sdl/participant_behavior_specification.py", "claim": "Current behavior specification surface."}] }, { - "concern_id": "behavioral-relation-taxonomy", "status": "missing", "limitation": "ACES explicitly disclaims bisimulation today; the cross-relation taxonomy and conflation audit are tracked separately.", "issue_refs": ["#747"], - "evidence": [{"kind": "normative-spec", "path": "specs/formal/objectives/proposition-and-assertion-semantics.md", "claim": "Current explicit non-bisimulation boundary."}] + "concern_id": "behavioral-relation-taxonomy", "status": "implemented", "limitation": "The revisioned taxonomy, bindings, finite-evidence gate, and counterexamples are implemented; universal simulation, refinement, equivalence, bisimulation, epistemic, strategic, probabilistic, timed, and partial-order proofs remain deliberately unproved or future.", "issue_refs": [], + "evidence": [ + {"kind": "normative-spec", "path": "specs/formal/behavioral-relations/README.md", "claim": "Normative relation definitions, dimensions, claim surfaces, assurance states, and counterexamples."}, + {"kind": "published-schema", "path": "contracts/schemas/concept-authority/behavioral-relations-v1.json", "contract_id": "behavioral-relations-v1", "claim": "Published closed contract for the revisioned taxonomy."}, + {"kind": "implementation", "path": "tools/check_behavioral_relation_claims.py", "claim": "Semantic claim-binding and overclaim gate."}, + {"kind": "conformance", "path": "implementations/python/packages/aces_conformance/conformance.py", "claim": "Backend conformance reports disclose bounded-probe relation, projection, case boundary, and nonclaims."}, + {"kind": "test", "path": "implementations/python/tests/test_behavioral_relations.py", "claim": "Catalog, binding, counterexample, and property-based relation tests."}, + {"kind": "documentation", "path": "docs/research/behavioral-relations/conflation-audit-2026-07-13.md", "claim": "Claim-surface conflation audit and remediation record."} + ] }, { "concern_id": "apparatus-constraints", "status": "external-contract", "limitation": "Apparatus constraints/context scope claims to a realization; they do not guarantee availability.", diff --git a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json index 6f7cbf996..f19f183c0 100644 --- a/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json +++ b/contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json @@ -54,6 +54,17 @@ "title": "Valid SDL fragment", "intended_claim": "The document is admitted as a structurally and semantically valid SDL authoring fragment.", "explicit_non_claims": ["Does not claim deployability, realization, experimental control, reproducibility, backend conformance, or behavioral equivalence."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev1", "relation_id": "semantic-validity", + "subject": "An artifact admitted by the valid-sdl-fragment profile", "left_carrier_ref": "scientific-scenario-completeness@rev1:valid-sdl-fragment", + "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "Parser and semantic validator acceptance for the submitted SDL artifact under its declared profile.", + "assurance_status": "tested", "evidence_refs": ["implementations/python/packages/aces_sdl/validator/", "implementations/python/tests/"], + "limitations": ["Acceptance applies only to the submitted artifact and the declared SDL profile revision."], + "explicit_non_claims": ["Semantic validity does not establish backend realization or behavioral equivalence."] + } + ], + "non_claimed_relation_ids": ["profile-satisfaction", "bounded-probe-success", "trace-equivalence", "strong-bisimulation", "empirical-adequacy"], "example_refs": ["examples/completeness/rev1/valid-sdl-fragment/minimal.sdl.yaml"], "dispositions": { "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", @@ -74,6 +85,17 @@ "title": "Deployable scenario intent", "intended_claim": "The authored scenario carries sufficient portable intent to request realization subject to an explicit apparatus and backend envelope.", "explicit_non_claims": ["Does not claim that every backend can realize the scenario, that deployment succeeded, or that realized behavior is equivalent."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev1", "relation_id": "profile-satisfaction", + "subject": "An artifact assessed against deployable-scenario-intent", "left_carrier_ref": "candidate-scenario", "right_carrier_ref": "scientific-scenario-completeness@rev1:deployable-scenario-intent", + "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions for one candidate artifact and its named external contract witnesses.", + "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], + "limitations": ["Profile satisfaction is conditional on every required concern and external witness; it says nothing about execution."], + "explicit_non_claims": ["No backend realization, trace inclusion, or equivalence claim."] + } + ], + "non_claimed_relation_ids": ["bounded-probe-success", "trace-inclusion", "trace-equivalence", "strong-bisimulation", "empirical-adequacy"], "example_refs": [], "dispositions": { "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", @@ -94,6 +116,17 @@ "title": "Participant-evaluation scenario", "intended_claim": "The scenario and bound experiment contracts define participant-visible work, perception, action, episode control, and outcome evidence for evaluation.", "explicit_non_claims": ["Does not claim participant implementations are strategically equivalent, that hidden global state is visible, or that finite probes prove bisimulation."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev1", "relation_id": "profile-satisfaction", + "subject": "An artifact assessed against participant-evaluation-scenario", "left_carrier_ref": "candidate-scenario-and-experiment-bindings", "right_carrier_ref": "scientific-scenario-completeness@rev1:participant-evaluation-scenario", + "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The required concern dispositions and contract witnesses for one bound participant-evaluation scenario.", + "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], + "limitations": ["Profile satisfaction records declared evaluation structure; observed participant histories require a separate projection-bound comparison."], + "explicit_non_claims": ["No projected-history, epistemic, strategic, or bisimulation equivalence claim."] + } + ], + "non_claimed_relation_ids": ["participant-projected-history-equivalence", "epistemic-indistinguishability", "alternating-strategic-equivalence", "strong-bisimulation", "empirical-adequacy"], "example_refs": [], "dispositions": { "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", @@ -113,6 +146,17 @@ "title": "Controlled experiment scenario", "intended_claim": "The scenario is bound to a pre-run design with controlled variation, allocation, apparatus, timing, capture, and analysis intent.", "explicit_non_claims": ["Does not claim causal validity, successful execution, adequate power, reproducibility, or backend behavioral equivalence."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev1", "relation_id": "profile-satisfaction", + "subject": "An artifact assessed against controlled-experiment-scenario", "left_carrier_ref": "candidate-scenario-and-study-design", "right_carrier_ref": "scientific-scenario-completeness@rev1:controlled-experiment-scenario", + "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The pre-run design declarations and required concern witnesses for one controlled-experiment input.", + "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], + "limitations": ["Design completeness precedes execution and cannot establish empirical adequacy or causal validity."], + "explicit_non_claims": ["No empirical adequacy, statistical equivalence, trace equivalence, or bisimulation claim."] + } + ], + "non_claimed_relation_ids": ["empirical-adequacy", "statistical-equivalence", "trace-equivalence", "strong-bisimulation", "alternating-strategic-equivalence"], "example_refs": [], "dispositions": { "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", @@ -132,6 +176,17 @@ "title": "Reproducible benchmark or study input", "intended_claim": "The scenario and governed experiment bundle preserve sufficient design, apparatus, participant, evidence, lineage, and analysis context for a bounded reproduction attempt.", "explicit_non_claims": ["Does not guarantee bitwise replay, equal outcomes, scientific validity, generalizability, or behavioral equivalence across apparatuses."], + "behavioral_claims": [ + { + "taxonomy_id": "aces-behavioral-relations", "taxonomy_revision": "rev1", "relation_id": "profile-satisfaction", + "subject": "An artifact assessed against reproducible-benchmark-study-input", "left_carrier_ref": "candidate-scenario-and-governed-experiment-bundle", "right_carrier_ref": "scientific-scenario-completeness@rev1:reproducible-benchmark-study-input", + "quantifier_scope": "single-artifact", "evidence_scope": "structural", "evidence_boundary": "The preserved design, apparatus, participant, evidence, lineage, and analysis declarations for one benchmark or study input.", + "assurance_status": "defined", "evidence_refs": ["tools/check_scientific_scenario_completeness.py"], + "limitations": ["Input completeness supports a bounded reproduction attempt but does not establish the outcome of that attempt."], + "explicit_non_claims": ["No empirical adequacy, statistical equivalence, behavioral equivalence, or generalizability claim."] + } + ], + "non_claimed_relation_ids": ["empirical-adequacy", "statistical-equivalence", "statistical-similarity", "trace-equivalence", "strong-bisimulation"], "example_refs": [], "dispositions": { "source-profile-validity": "required", "closed-structural-model": "required", "semantic-reference-integrity": "required", "topology-and-resource-intent": "required", diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json index 53cc46e9d..cbfca2b5f 100644 --- a/contracts/provenance/sdl-lineage-ledger-v1.json +++ b/contracts/provenance/sdl-lineage-ledger-v1.json @@ -1,6 +1,6 @@ { "schema_version": "sdl-lineage-ledger/v1", - "reviewed_on": "2026-07-12", + "reviewed_on": "2026-07-13", "citations": [ { "citation_id": "ocr-sdl-v0.21.2", @@ -1184,6 +1184,37 @@ } ] }, + { + "subject_id": "sdl-field:realization", + "subject_kind": "top_level_field", + "disposition": "current", + "authority": { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "pointer": "#/properties/realization", + "contract_id": "sdl-authoring-input-v1" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "symbol_or_pointer": "#/properties/realization" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external systems may be intellectual precedents but do not define this subject.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/formal/realization/explicitness-and-realization.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, { "subject_id": "sdl-field:relationships", "subject_kind": "top_level_field", @@ -2171,6 +2202,36 @@ } ] }, + { + "subject_id": "concept-family:behavioral-relations", + "subject_kind": "concept_family", + "disposition": "current", + "authority": { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "pointer": "#/families/behavioral-relations" + }, + "claims": [ + { + "plane": "semantics", + "classification": "aces_native", + "source_refs": [], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/concept-families-v1.json", + "symbol_or_pointer": "#/families/behavioral-relations" + } + ], + "source_boundaries": [], + "divergence": "Defined by the current ACES normative contract; external publications define individual formal relations but do not define the ACES claim-binding and assurance policy.", + "compatibility": "not_applicable", + "citation_refs": [], + "internal_authority_refs": [ + "specs/formal/behavioral-relations/README.md" + ], + "compatibility_direction": "not_applicable" + } + ] + }, { "subject_id": "concept-family:episodes", "subject_kind": "concept_family", diff --git a/contracts/schema-publication-manifest.json b/contracts/schema-publication-manifest.json index e26bb949a..9a5b68027 100644 --- a/contracts/schema-publication-manifest.json +++ b/contracts/schema-publication-manifest.json @@ -58,6 +58,16 @@ "content_hash": "7796e171b81cc47e15446d77097c63d56564235b96a5f82e8e813753d4f7b709" } }, + { + "contract_id": "behavioral-relations-v1", + "schema_path": "contracts/schemas/concept-authority/behavioral-relations-v1.json", + "stability": "draft", + "content_hash": "6527fb300e23678a778fd44282f6fa12c8c86119b707ab0874bd4cf19ddb5e29", + "last_change": { + "summary": "Initial publication of the ADR-081 revisioned behavioral-relation taxonomy, formal dimensions, claim surfaces, assurance states, source identities, and counterexamples (issue #747).", + "content_hash": "6527fb300e23678a778fd44282f6fa12c8c86119b707ab0874bd4cf19ddb5e29" + } + }, { "contract_id": "concept-families-v1", "schema_path": "contracts/schemas/concept-authority/concept-families-v1.json", @@ -160,10 +170,10 @@ "contract_id": "experiment-study-v1", "schema_path": "contracts/schemas/experiment-core/experiment-study-v1.json", "stability": "draft", - "content_hash": "d180220068358f42d6ae850eccb9991dc9284db4b244ef70960837ea68d9b098", + "content_hash": "48f758096bf386e097ffa1f9e7e4e9a32781925428be47b42e2f430ba7e681e3", "last_change": { - "summary": "Extended shared experiment artifact roles and typed references for associated-artifact parents (ADR-077, issue #738).", - "content_hash": "d180220068358f42d6ae850eccb9991dc9284db4b244ef70960837ea68d9b098" + "summary": "Required revisioned, evidence-bounded behavioral or empirical claim bindings for claim-bearing studies and benchmarks (ADR-081, issue #747).", + "content_hash": "48f758096bf386e097ffa1f9e7e4e9a32781925428be47b42e2f430ba7e681e3" } }, { @@ -180,20 +190,20 @@ "contract_id": "instantiated-scenario-snapshot-v1", "schema_path": "contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json", "stability": "draft", - "content_hash": "8670e3cf3aafc452f25311e4eb98ed82bb89561faaaf368a9232448c8440a3c3", + "content_hash": "8e8048de4b5161c1a02d0900811f388b0d57a92b6d8434b0242f0a5254e9b377", "last_change": { - "summary": "Published backend-neutral propositions and assertions in canonical instantiated scenario snapshots.", - "content_hash": "8670e3cf3aafc452f25311e4eb98ed82bb89561faaaf368a9232448c8440a3c3" + "summary": "Added SEM-218 realization designation provenance and clarified Node.services reachability semantics (issue #748).", + "content_hash": "8e8048de4b5161c1a02d0900811f388b0d57a92b6d8434b0242f0a5254e9b377" } }, { "contract_id": "instantiated-scenario-v1", "schema_path": "contracts/schemas/sdl/instantiated-scenario-v1.json", "stability": "draft", - "content_hash": "58d4f2e69b91ad1639717d7892499bd1b2f007e23c0cb980314cbd701c2cd000", + "content_hash": "515cacc41d24c643e7655de70a40dc1b6305a14b9d840083d5d9ff694d58df29", "last_change": { - "summary": "Published backend-neutral propositions, assertions, role-constrained truth references, and explicit condition-to-proposition bindings.", - "content_hash": "58d4f2e69b91ad1639717d7892499bd1b2f007e23c0cb980314cbd701c2cd000" + "summary": "Added SEM-218 scoped realization records and clarified Node.services reachability semantics (issue #748).", + "content_hash": "515cacc41d24c643e7655de70a40dc1b6305a14b9d840083d5d9ff694d58df29" } }, { @@ -406,10 +416,10 @@ "contract_id": "runtime-snapshot-v1", "schema_path": "contracts/schemas/snapshots/runtime-snapshot-v1.json", "stability": "draft", - "content_hash": "4e20e5c91ccd425c88be75a0e55be97f07d311e6499fc6bf73c3db4b37fa51bf", + "content_hash": "b1453a4764c14bf9ec1497030563ac0c774b17b1883d15fcbb2027d66f5dcbf9", "last_change": { - "summary": "Added portable proposition truth results as a distinct runtime snapshot carrier outside ordinary evaluation result envelopes.", - "content_hash": "4e20e5c91ccd425c88be75a0e55be97f07d311e6499fc6bf73c3db4b37fa51bf" + "summary": "Added redacted governing-scope references to SEM-218 runtime realization provenance.", + "content_hash": "b1453a4764c14bf9ec1497030563ac0c774b17b1883d15fcbb2027d66f5dcbf9" } }, { @@ -436,20 +446,20 @@ "contract_id": "scientific-completeness-taxonomy-v1", "schema_path": "contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json", "stability": "draft", - "content_hash": "87eabab61eb27b6d66288ebce40b628e7405ba3c66ad116fd98b493c4b9bff21", + "content_hash": "d1c83d89fc161199208d2eb82f7f5ddebd61ffc9af6d67c9ddac4d84e0a03328", "last_change": { - "summary": "Initial publication of the REV1 scientific-scenario completeness taxonomy, atomic concern, intended-use disposition, and explicit non-claim contract for GOV-942.", - "content_hash": "87eabab61eb27b6d66288ebce40b628e7405ba3c66ad116fd98b493c4b9bff21" + "summary": "Bound every REV1 intended-use claim and nonclaim to the ADR-081 behavioral-relation taxonomy (issue #747).", + "content_hash": "d1c83d89fc161199208d2eb82f7f5ddebd61ffc9af6d67c9ddac4d84e0a03328" } }, { "contract_id": "sdl-authoring-input-v1", "schema_path": "contracts/schemas/sdl/sdl-authoring-input-v1.json", "stability": "draft", - "content_hash": "17f941e916ffcbeb3e36b894029332326257bc7e4d1ab3419bc6e06c7f06f801", + "content_hash": "57e984be7783a5eb54ee210da434d2883aa468617b0ee8c2a42de1a769ff30b5", "last_change": { - "summary": "Published backend-neutral proposition and assertion authoring, role-constrained objective/event/workflow/participant references, and explicit probe bindings.", - "content_hash": "17f941e916ffcbeb3e36b894029332326257bc7e4d1ab3419bc6e06c7f06f801" + "summary": "Published SEM-218 scoped realization defaults and clarified Node.services reachability semantics (issue #748).", + "content_hash": "57e984be7783a5eb54ee210da434d2883aa468617b0ee8c2a42de1a769ff30b5" } }, { diff --git a/contracts/schemas/README.md b/contracts/schemas/README.md index e5cf6f1f1..09aa0c6ec 100644 --- a/contracts/schemas/README.md +++ b/contracts/schemas/README.md @@ -96,6 +96,20 @@ instead they must declare non-empty `extension_scope`, `relation_rules`, and provenance, and governance concepts explicit without letting them silently fork shared cyber-domain concepts. +## Behavioral-Relation Authority + +The `behavioral-relations-v1` schema publishes the closed, revisioned relation +catalog used by validation, SDL transformations, backend realization and +comparison, participant-visible behavior, multi-agent claims, and independent +adequacy studies. It separates relation identity from evidence and assurance, +requires formal dimension treatment, and carries revision-pinned bibliography +coordinates and executable counterexamples. + +The canonical catalog lives at +`contracts/concept-authority/behavioral-relations-v1.json`; consumer claims use +the shared `BehavioralClaimBindingModel` rather than copying relation meaning. +Finite evidence cannot be bound to a universal quantifier. + ## UCO Alignment Evidence The `uco-alignment-v1` schema publishes the machine-readable UCO alignment diff --git a/contracts/schemas/concept-authority/behavioral-relations-v1.json b/contracts/schemas/concept-authority/behavioral-relations-v1.json new file mode 100644 index 000000000..fd9833d1e --- /dev/null +++ b/contracts/schemas/concept-authority/behavioral-relations-v1.json @@ -0,0 +1,791 @@ +{ + "$defs": { + "BehavioralBibliographySourceModel": { + "additionalProperties": false, + "properties": { + "authors": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Authors", + "type": "array" + }, + "edition_or_version": { + "minLength": 1, + "title": "Edition Or Version", + "type": "string" + }, + "immutable_locator": { + "$ref": "#/$defs/ImmutablePublicationLocatorModel" + }, + "publication_venue": { + "minLength": 1, + "title": "Publication Venue", + "type": "string" + }, + "publication_year": { + "maximum": 2100, + "minimum": 1900, + "title": "Publication Year", + "type": "integer" + }, + "source_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Source Id", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + } + }, + "required": [ + "source_id", + "title", + "authors", + "publication_year", + "publication_venue", + "edition_or_version", + "immutable_locator" + ], + "title": "BehavioralBibliographySourceModel", + "type": "object" + }, + "BehavioralClaimSurfaceModel": { + "additionalProperties": false, + "properties": { + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "intended_relation_ids": { + "items": { + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "minItems": 1, + "title": "Intended Relation Ids", + "type": "array" + }, + "prohibited_relation_ids": { + "items": { + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "minItems": 1, + "title": "Prohibited Relation Ids", + "type": "array" + }, + "surface_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Surface Id", + "type": "string" + } + }, + "required": [ + "surface_id", + "intended_relation_ids", + "evidence_boundary", + "prohibited_relation_ids", + "explicit_non_claims" + ], + "title": "BehavioralClaimSurfaceModel", + "type": "object" + }, + "BehavioralRelationDefinitionModel": { + "additionalProperties": false, + "properties": { + "assurance": { + "$ref": "#/$defs/RelationAssuranceModel" + }, + "bounded_evidence": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Bounded Evidence", + "type": "array" + }, + "definition": { + "minLength": 1, + "title": "Definition", + "type": "string" + }, + "dimensions": { + "$ref": "#/$defs/RelationDimensionsModel" + }, + "direction": { + "enum": [ + "unary", + "left-to-right", + "right-to-left", + "symmetric" + ], + "title": "Direction", + "type": "string" + }, + "display_name": { + "minLength": 1, + "title": "Display Name", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "incompatible_claim_surfaces": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Incompatible Claim Surfaces", + "type": "array" + }, + "initial_states": { + "minLength": 1, + "title": "Initial States", + "type": "string" + }, + "left_carrier": { + "minLength": 1, + "title": "Left Carrier", + "type": "string" + }, + "observation_projection": { + "$ref": "#/$defs/ObservationProjectionModel" + }, + "preservation": { + "$ref": "#/$defs/RelationPreservationModel" + }, + "projection_required": { + "title": "Projection Required", + "type": "boolean" + }, + "quantification": { + "$ref": "#/$defs/RelationQuantificationModel" + }, + "relation_class": { + "enum": [ + "predicate", + "set-relation", + "behavioral", + "epistemic", + "strategic", + "empirical" + ], + "title": "Relation Class", + "type": "string" + }, + "relation_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Relation Id", + "type": "string" + }, + "right_carrier": { + "minLength": 1, + "title": "Right Carrier", + "type": "string" + }, + "source_refs": { + "items": { + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "minItems": 1, + "title": "Source Refs", + "type": "array" + }, + "transition_signature": { + "$ref": "#/$defs/TransitionSignatureModel" + } + }, + "required": [ + "relation_id", + "display_name", + "relation_class", + "definition", + "left_carrier", + "right_carrier", + "initial_states", + "transition_signature", + "observation_projection", + "projection_required", + "direction", + "quantification", + "dimensions", + "preservation", + "bounded_evidence", + "explicit_non_claims", + "incompatible_claim_surfaces", + "assurance", + "source_refs" + ], + "title": "BehavioralRelationDefinitionModel", + "type": "object" + }, + "BehavioralWorkedExampleModel": { + "additionalProperties": false, + "properties": { + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "example_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Example Id", + "type": "string" + }, + "expected_strong_bisimulation": { + "title": "Expected Strong Bisimulation", + "type": "boolean" + }, + "expected_weak_matching": { + "title": "Expected Weak Matching", + "type": "boolean" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "hidden_action": { + "minLength": 1, + "title": "Hidden Action", + "type": "string" + }, + "left_system": { + "$ref": "#/$defs/ExampleTransitionSystemModel" + }, + "purpose": { + "minLength": 1, + "title": "Purpose", + "type": "string" + }, + "right_system": { + "$ref": "#/$defs/ExampleTransitionSystemModel" + }, + "tested_visible_trace": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Tested Visible Trace", + "type": "array" + } + }, + "required": [ + "example_id", + "purpose", + "left_system", + "right_system", + "tested_visible_trace", + "hidden_action", + "expected_strong_bisimulation", + "expected_weak_matching", + "evidence_boundary", + "explicit_non_claims" + ], + "title": "BehavioralWorkedExampleModel", + "type": "object" + }, + "ExampleTransitionModel": { + "additionalProperties": false, + "properties": { + "action": { + "minLength": 1, + "title": "Action", + "type": "string" + }, + "source": { + "minLength": 1, + "title": "Source", + "type": "string" + }, + "target": { + "minLength": 1, + "title": "Target", + "type": "string" + } + }, + "required": [ + "source", + "action", + "target" + ], + "title": "ExampleTransitionModel", + "type": "object" + }, + "ExampleTransitionSystemModel": { + "additionalProperties": false, + "properties": { + "initial_state": { + "minLength": 1, + "title": "Initial State", + "type": "string" + }, + "states": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "States", + "type": "array" + }, + "transitions": { + "items": { + "$ref": "#/$defs/ExampleTransitionModel" + }, + "minItems": 1, + "title": "Transitions", + "type": "array" + } + }, + "required": [ + "states", + "initial_state", + "transitions" + ], + "title": "ExampleTransitionSystemModel", + "type": "object" + }, + "ImmutablePublicationLocatorModel": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "doi", + "isbn" + ], + "title": "Kind", + "type": "string" + }, + "value": { + "minLength": 1, + "title": "Value", + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "title": "ImmutablePublicationLocatorModel", + "type": "object" + }, + "ObservationProjectionModel": { + "additionalProperties": false, + "properties": { + "applicability": { + "enum": [ + "required", + "parameterized", + "identity", + "not-applicable" + ], + "title": "Applicability", + "type": "string" + }, + "order_treatment": { + "minLength": 1, + "title": "Order Treatment", + "type": "string" + }, + "policy_ref": { + "minLength": 1, + "title": "Policy Ref", + "type": "string" + }, + "policy_revision": { + "minLength": 1, + "title": "Policy Revision", + "type": "string" + }, + "redaction_scope": { + "minLength": 1, + "title": "Redaction Scope", + "type": "string" + }, + "simultaneity_treatment": { + "minLength": 1, + "title": "Simultaneity Treatment", + "type": "string" + }, + "subject": { + "minLength": 1, + "title": "Subject", + "type": "string" + } + }, + "required": [ + "applicability", + "subject", + "policy_ref", + "policy_revision", + "redaction_scope", + "order_treatment", + "simultaneity_treatment" + ], + "title": "ObservationProjectionModel", + "type": "object" + }, + "RelationAssuranceModel": { + "additionalProperties": false, + "properties": { + "definition_status": { + "enum": [ + "defined", + "future" + ], + "title": "Definition Status", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "implementation_status": { + "enum": [ + "implemented", + "partial", + "not-implemented", + "not-applicable" + ], + "title": "Implementation Status", + "type": "string" + }, + "proof_status": { + "enum": [ + "proved", + "model-checked", + "deliberately-unproved", + "future", + "not-applicable" + ], + "title": "Proof Status", + "type": "string" + }, + "test_status": { + "enum": [ + "tested", + "bounded", + "not-tested", + "not-applicable" + ], + "title": "Test Status", + "type": "string" + } + }, + "required": [ + "definition_status", + "implementation_status", + "test_status", + "proof_status" + ], + "title": "RelationAssuranceModel", + "type": "object" + }, + "RelationDimensionTreatmentModel": { + "additionalProperties": false, + "properties": { + "status": { + "enum": [ + "supported", + "parameterized", + "abstracted", + "outside-scope" + ], + "title": "Status", + "type": "string" + }, + "treatment": { + "minLength": 1, + "title": "Treatment", + "type": "string" + } + }, + "required": [ + "status", + "treatment" + ], + "title": "RelationDimensionTreatmentModel", + "type": "object" + }, + "RelationDimensionsModel": { + "additionalProperties": false, + "properties": { + "concurrency": { + "$ref": "#/$defs/RelationDimensionTreatmentModel" + }, + "nondeterminism": { + "$ref": "#/$defs/RelationDimensionTreatmentModel" + }, + "partial_order": { + "$ref": "#/$defs/RelationDimensionTreatmentModel" + }, + "probability": { + "$ref": "#/$defs/RelationDimensionTreatmentModel" + }, + "time": { + "$ref": "#/$defs/RelationDimensionTreatmentModel" + } + }, + "required": [ + "nondeterminism", + "concurrency", + "probability", + "time", + "partial_order" + ], + "title": "RelationDimensionsModel", + "type": "object" + }, + "RelationPreservationModel": { + "additionalProperties": false, + "properties": { + "proof_obligation": { + "minLength": 1, + "title": "Proof Obligation", + "type": "string" + }, + "property": { + "minLength": 1, + "title": "Property", + "type": "string" + } + }, + "required": [ + "property", + "proof_obligation" + ], + "title": "RelationPreservationModel", + "type": "object" + }, + "RelationQuantificationModel": { + "additionalProperties": false, + "properties": { + "environments": { + "minLength": 1, + "title": "Environments", + "type": "string" + }, + "observations": { + "minLength": 1, + "title": "Observations", + "type": "string" + }, + "schedulers": { + "minLength": 1, + "title": "Schedulers", + "type": "string" + }, + "states": { + "minLength": 1, + "title": "States", + "type": "string" + }, + "strategies": { + "minLength": 1, + "title": "Strategies", + "type": "string" + }, + "traces": { + "minLength": 1, + "title": "Traces", + "type": "string" + } + }, + "required": [ + "states", + "traces", + "schedulers", + "strategies", + "environments", + "observations" + ], + "title": "RelationQuantificationModel", + "type": "object" + }, + "TransitionSignatureModel": { + "additionalProperties": false, + "properties": { + "applicability": { + "enum": [ + "applicable", + "not-applicable" + ], + "title": "Applicability", + "type": "string" + }, + "hidden_actions": { + "minLength": 1, + "title": "Hidden Actions", + "type": "string" + }, + "labels": { + "minLength": 1, + "title": "Labels", + "type": "string" + }, + "not_applicable_rationale": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Not Applicable Rationale" + }, + "observable_actions": { + "minLength": 1, + "title": "Observable Actions", + "type": "string" + }, + "stuttering_actions": { + "minLength": 1, + "title": "Stuttering Actions", + "type": "string" + }, + "transition_relation": { + "minLength": 1, + "title": "Transition Relation", + "type": "string" + } + }, + "required": [ + "applicability", + "labels", + "transition_relation", + "observable_actions", + "hidden_actions", + "stuttering_actions" + ], + "title": "TransitionSignatureModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/behavioral-relations-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "bibliography": { + "items": { + "$ref": "#/$defs/BehavioralBibliographySourceModel" + }, + "minItems": 1, + "title": "Bibliography", + "type": "array" + }, + "claim_surfaces": { + "items": { + "$ref": "#/$defs/BehavioralClaimSurfaceModel" + }, + "minItems": 1, + "title": "Claim Surfaces", + "type": "array" + }, + "relations": { + "minProperties": 1, + "patternProperties": { + "^[a-z][a-z0-9-]*$": { + "$ref": "#/$defs/BehavioralRelationDefinitionModel" + } + }, + "title": "Relations", + "type": "object" + }, + "schema_version": { + "const": "behavioral-relations/v1", + "default": "behavioral-relations/v1", + "title": "Schema Version", + "type": "string" + }, + "taxonomy_id": { + "const": "aces-behavioral-relations", + "default": "aces-behavioral-relations", + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + }, + "worked_examples": { + "minProperties": 1, + "patternProperties": { + "^[a-z][a-z0-9-]*$": { + "$ref": "#/$defs/BehavioralWorkedExampleModel" + } + }, + "title": "Worked Examples", + "type": "object" + } + }, + "required": [ + "taxonomy_revision", + "bibliography", + "relations", + "claim_surfaces", + "worked_examples" + ], + "title": "BehavioralRelationCatalogModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Relation map keys, bibliography references, claim-surface relation references, and worked-example keys must resolve exactly inside one taxonomy revision.", + "id": "behavioral-relations-reference-resolution", + "inputs": [ + { + "contract_id": "behavioral-relations-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.behavioral_relations.BehavioralRelationCatalogModel" + } + ], + "x-aces-semantic-profile": { + "contract_id": "behavioral-relations-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/experiment-core/experiment-study-v1.json b/contracts/schemas/experiment-core/experiment-study-v1.json index e8cbc31bc..573669eb8 100644 --- a/contracts/schemas/experiment-core/experiment-study-v1.json +++ b/contracts/schemas/experiment-core/experiment-study-v1.json @@ -1,5 +1,164 @@ { "$defs": { + "BehavioralClaimBindingModel": { + "additionalProperties": false, + "description": "A bounded claim tied to one revisioned behavioral-relation definition.\n\nThis is deliberately a claim *binding*, not another relation registry. The\ncatalog owns relation meaning; consumers supply the subject, carriers,\nquantifier/evidence boundary, assurance state, and explicit limitations.", + "properties": { + "assurance_status": { + "enum": [ + "defined", + "implemented", + "tested", + "model-checked", + "proved", + "deliberately-unproved", + "future" + ], + "title": "Assurance Status", + "type": "string" + }, + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "evidence_scope": { + "enum": [ + "structural", + "finite", + "statistical", + "model-check", + "proof" + ], + "title": "Evidence Scope", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "left_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Left Carrier Ref" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_projection_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Ref" + }, + "observation_projection_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Revision" + }, + "quantifier_scope": { + "enum": [ + "single-artifact", + "finite-cases", + "sampled-population", + "all-admitted-inputs", + "all-traces", + "all-strategies" + ], + "title": "Quantifier Scope", + "type": "string" + }, + "relation_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Relation Id", + "type": "string" + }, + "right_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Right Carrier Ref" + }, + "subject": { + "minLength": 1, + "title": "Subject", + "type": "string" + }, + "taxonomy_id": { + "minLength": 1, + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "subject", + "quantifier_scope", + "evidence_scope", + "evidence_boundary", + "assurance_status", + "limitations", + "explicit_non_claims" + ], + "title": "BehavioralClaimBindingModel", + "type": "object" + }, "ExperimentAnalysisPlanModel": { "additionalProperties": false, "description": "Analysis plan metadata for metrics, uncertainty, and missing data.", @@ -1248,6 +1407,9 @@ "analysis_plan": { "type": "object" }, + "behavioral_claims": { + "minItems": 1 + }, "research_questions": { "minItems": 1 }, @@ -1260,6 +1422,7 @@ }, "required": [ "research_questions", + "behavioral_claims", "run_allocation", "analysis_plan", "validity_notes" @@ -1280,6 +1443,13 @@ ], "default": null }, + "behavioral_claims": { + "items": { + "$ref": "#/$defs/BehavioralClaimBindingModel" + }, + "title": "Behavioral Claims", + "type": "array" + }, "description": { "minLength": 1, "title": "Description", @@ -1412,7 +1582,7 @@ "type": "object", "x-aces-invariants": [ { - "description": "Study and benchmark records must include research questions, run allocation, a substantive analysis plan, and validity notes.", + "description": "Study and benchmark records must include research questions, revisioned behavioral claim bindings, run allocation, a substantive analysis plan, and validity notes.", "id": "claim-bearing-study-analysis-plan-required", "inputs": [ { @@ -1499,6 +1669,22 @@ "level": "error", "validator": "aces_contracts.contracts.validate_experiment_study_against_tasks_and_runs" }, + { + "description": "Every study behavioral claim must resolve against the canonical taxonomy revision, include a required observation projection, and keep bounded evidence out of universal quantifiers.", + "id": "study-behavioral-claim-catalog-resolution", + "inputs": [ + { + "contract_id": "experiment-study-v1", + "instance_path": "#/behavioral_claims" + }, + { + "contract_id": "behavioral-relations-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.behavioral_relations.validate_behavioral_claim_binding" + }, { "description": "Study report_artifacts and export_artifacts created_at values must be valid RFC 3339 date-times, including only known valid UTC leap-second instants.", "id": "study-archival-times-rfc3339-valid", diff --git a/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json b/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json index cdb8b28a3..4764d5516 100644 --- a/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json +++ b/contracts/schemas/profiles/scientific-completeness-taxonomy-v1.json @@ -1,5 +1,164 @@ { "$defs": { + "BehavioralClaimBindingModel": { + "additionalProperties": false, + "description": "A bounded claim tied to one revisioned behavioral-relation definition.\n\nThis is deliberately a claim *binding*, not another relation registry. The\ncatalog owns relation meaning; consumers supply the subject, carriers,\nquantifier/evidence boundary, assurance state, and explicit limitations.", + "properties": { + "assurance_status": { + "enum": [ + "defined", + "implemented", + "tested", + "model-checked", + "proved", + "deliberately-unproved", + "future" + ], + "title": "Assurance Status", + "type": "string" + }, + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "evidence_scope": { + "enum": [ + "structural", + "finite", + "statistical", + "model-check", + "proof" + ], + "title": "Evidence Scope", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "left_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Left Carrier Ref" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_projection_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Ref" + }, + "observation_projection_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Revision" + }, + "quantifier_scope": { + "enum": [ + "single-artifact", + "finite-cases", + "sampled-population", + "all-admitted-inputs", + "all-traces", + "all-strategies" + ], + "title": "Quantifier Scope", + "type": "string" + }, + "relation_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Relation Id", + "type": "string" + }, + "right_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Right Carrier Ref" + }, + "subject": { + "minLength": 1, + "title": "Subject", + "type": "string" + }, + "taxonomy_id": { + "minLength": 1, + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "subject", + "quantifier_scope", + "evidence_scope", + "evidence_boundary", + "assurance_status", + "limitations", + "explicit_non_claims" + ], + "title": "BehavioralClaimBindingModel", + "type": "object" + }, "CompletenessConcernModel": { "additionalProperties": false, "properties": { @@ -36,6 +195,14 @@ "CompletenessProfileModel": { "additionalProperties": false, "properties": { + "behavioral_claims": { + "items": { + "$ref": "#/$defs/BehavioralClaimBindingModel" + }, + "minItems": 1, + "title": "Behavioral Claims", + "type": "array" + }, "dispositions": { "patternProperties": { "^[a-z][a-z0-9-]*$": { @@ -67,6 +234,15 @@ "title": "Intended Claim", "type": "string" }, + "non_claimed_relation_ids": { + "items": { + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" + }, + "minItems": 1, + "title": "Non Claimed Relation Ids", + "type": "array" + }, "profile_id": { "pattern": "^[a-z][a-z0-9-]*$", "title": "Profile Id", @@ -83,6 +259,8 @@ "title", "intended_claim", "explicit_non_claims", + "behavioral_claims", + "non_claimed_relation_ids", "dispositions" ], "title": "CompletenessProfileModel", @@ -155,6 +333,22 @@ ], "level": "error", "validator": "aces_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel" + }, + { + "description": "Each profile must carry resolved behavioral claim bindings and disjoint, catalog-resolved nonclaimed relation ids.", + "id": "scientific-completeness-behavioral-claim-resolution", + "inputs": [ + { + "contract_id": "scientific-completeness-taxonomy-v1", + "instance_path": "#/profiles" + }, + { + "contract_id": "behavioral-relations-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.scientific_completeness.CompletenessProfileModel.validate_behavioral_claims" } ], "x-aces-semantic-profile": { diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index 6a6aa962b..33b323844 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -451,6 +451,16 @@ "title": "AssetValueLevel", "type": "string" }, + "AuthorRealizationPosture": { + "description": "Authored default posture, distinct from capability support modes.", + "enum": [ + "closed", + "open", + "unspecified" + ], + "title": "AuthorRealizationPosture", + "type": "string" + }, "BehaviorSpecificationExtensionValue": { "anyOf": [ { @@ -4567,6 +4577,14 @@ "title": "Imports", "type": "array" }, + "realization_designations": { + "default": [], + "items": { + "$ref": "#/$defs/RealizationDesignationRecord" + }, + "title": "Realization Designations", + "type": "array" + }, "selected_profile": { "anyOf": [ { @@ -6794,6 +6812,52 @@ "title": "PropositionBasis", "type": "string" }, + "RealizationDesignationRecord": { + "additionalProperties": false, + "description": "Portable authored designation carried across SDL document phases.", + "properties": { + "field_pointer": { + "default": "", + "maxLength": 4096, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "title": "Field Pointer", + "type": "string" + }, + "namespace": { + "default": [], + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 31, + "title": "Namespace", + "type": "array" + }, + "posture": { + "$ref": "#/$defs/AuthorRealizationPosture" + } + }, + "required": [ + "posture" + ], + "title": "RealizationDesignationRecord", + "type": "object" + }, "Relationship": { "additionalProperties": false, "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", @@ -8764,7 +8828,7 @@ }, "RuntimeConfiguration": { "additionalProperties": false, - "description": "Observed runtime configuration facts attached to a VM node.", + "description": "Declarative runtime state required by a VM node.\n\nCaptured observations remain in evidence records unless an author\ndeliberately promotes the fact to one of these contract fields.", "properties": { "app_authorizations": { "items": { @@ -8872,17 +8936,6 @@ "title": "Forwarding Agents", "type": "array" }, - "health": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthObservation" - }, - { - "type": "null" - } - ], - "default": null - }, "identity_authorities": { "items": { "$ref": "#/$defs/RuntimeIdentityAuthority" @@ -8976,13 +9029,6 @@ "title": "Orchestration Authorities", "type": "array" }, - "package_vulnerabilities": { - "items": { - "$ref": "#/$defs/RuntimePackageVulnerabilityFinding" - }, - "title": "Package Vulnerabilities", - "type": "array" - }, "packages": { "items": { "$ref": "#/$defs/RuntimePackage" @@ -9052,7 +9098,7 @@ }, "RuntimeContainerConfiguration": { "additionalProperties": false, - "description": "Observed container runtime host and security configuration facts.", + "description": "Required container runtime host and security configuration facts.", "properties": { "autoremove": { "anyOf": [ @@ -11228,7 +11274,7 @@ }, "RuntimeDependencyManifest": { "additionalProperties": false, - "description": "A dependency manifest visible in the realized runtime artifact.", + "description": "A dependency manifest required in the runtime artifact.", "properties": { "ecosystem": { "not": { @@ -11278,7 +11324,7 @@ }, "RuntimeDeviceMapping": { "additionalProperties": false, - "description": "A host device mapping observed in runtime configuration.", + "description": "A host device mapping required in runtime configuration.", "properties": { "container_path": { "not": { @@ -11466,7 +11512,7 @@ "type": "object" }, "RuntimeEnvironmentValueClassification": { - "description": "Sensitivity classification for an observed runtime environment value.", + "description": "Sensitivity classification for a required runtime environment value.", "enum": [ "plain", "redacted", @@ -11480,7 +11526,7 @@ }, "RuntimeEnvironmentVariable": { "additionalProperties": false, - "description": "Observed runtime environment variable with provenance and sensitivity.", + "description": "Required runtime environment variable with provenance and sensitivity.", "properties": { "description": { "default": "", @@ -11551,7 +11597,7 @@ "type": "object" }, "RuntimeEnvironmentVariableProvenance": { - "description": "Origin class for an observed runtime environment variable.", + "description": "Required origin class for a runtime environment variable.", "enum": [ "compose", "image", @@ -11566,7 +11612,7 @@ }, "RuntimeExtraHost": { "additionalProperties": false, - "description": "An observed extra host mapping in runtime configuration.", + "description": "A required extra host mapping in runtime configuration.", "properties": { "address": { "not": { @@ -13332,140 +13378,6 @@ "title": "RuntimeForwardingTransformKind", "type": "string" }, - "RuntimeHealthObservation": { - "additionalProperties": false, - "description": "Observed runtime health status and healthcheck log facts.", - "properties": { - "description": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Description", - "type": "string" - }, - "failing_streak": { - "anyOf": [ - { - "type": "integer" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Failing Streak" - }, - "log": { - "items": { - "$ref": "#/$defs/RuntimeHealthcheckLog" - }, - "title": "Log", - "type": "array" - }, - "status": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthStatus" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Status" - } - }, - "title": "RuntimeHealthObservation", - "type": "object" - }, - "RuntimeHealthStatus": { - "description": "Observed health status for a runtime node or container.", - "enum": [ - "none", - "starting", - "healthy", - "unhealthy", - "unknown", - "other" - ], - "title": "RuntimeHealthStatus", - "type": "string" - }, - "RuntimeHealthcheckLog": { - "additionalProperties": false, - "description": "An observed runtime healthcheck log entry.", - "properties": { - "end": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "End", - "type": "string" - }, - "exit_code": { - "anyOf": [ - { - "type": "integer" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Exit Code" - }, - "output": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Output", - "type": "string" - }, - "output_redacted": { - "anyOf": [ - { - "type": "boolean" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": false, - "title": "Output Redacted" - }, - "start": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Start", - "type": "string" - } - }, - "title": "RuntimeHealthcheckLog", - "type": "object" - }, "RuntimeIdentityAttribute": { "additionalProperties": false, "description": "Observed identity attribute or bounded setting.\n\nSecret-bearing names must not carry raw values. This keeps directory\npasswords, Kerberos keys, keytabs, tokens, and client secrets out of\nfixtures, diagnostics, schemas, and generated runtime artifacts.", @@ -14171,7 +14083,7 @@ }, "RuntimeInitProcess": { "additionalProperties": false, - "description": "Observed backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", + "description": "Required backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", "properties": { "argv": { "items": { @@ -16070,7 +15982,7 @@ }, "RuntimeNamespaceConfiguration": { "additionalProperties": false, - "description": "Observed namespace modes for a runtime node or container.", + "description": "Required namespace modes for a runtime node or container.", "properties": { "cgroup": { "default": "", @@ -16118,7 +16030,7 @@ }, "RuntimeNetworkBackendDetail": { "additionalProperties": false, - "description": "Observed backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", + "description": "Required backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", "properties": { "description": { "default": "", @@ -16925,7 +16837,7 @@ "type": "string" }, "RuntimeNetworkDriver": { - "description": "Observed backend network driver class for a realized network.", + "description": "Backend network driver class required by the scenario.", "enum": [ "bridge", "overlay", @@ -16941,7 +16853,7 @@ }, "RuntimeNetworkEndpoint": { "additionalProperties": false, - "description": "An observed container network endpoint (per-network attachment).\n\n``network`` references a declared switch-backed infrastructure network by\nname. Per-network ``aliases``, observed ``dns_names``, and backend-generated\n``generated_dns_names`` are kept as three distinct lists; generated names\nare not stable scenario identity.", + "description": "A required container network attachment.\n\n``network`` references a declared switch-backed infrastructure network by\nname. Runtime-generated identifiers and names are capture evidence, not\ndeclaration identity.", "properties": { "aliases": { "items": { @@ -16964,24 +16876,6 @@ ], "default": null }, - "backend_generated": { - "anyOf": [ - { - "type": "boolean" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Backend Generated" - }, "description": { "default": "", "not": { @@ -17000,29 +16894,6 @@ "title": "Dns Names", "type": "array" }, - "endpoint_id": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Endpoint Id", - "type": "string" - }, - "endpoint_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Endpoint Id Stability" - }, "gateway": { "default": "", "not": { @@ -17031,16 +16902,6 @@ "title": "Gateway", "type": "string" }, - "generated_dns_names": { - "items": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - "title": "Generated Dns Names", - "type": "array" - }, "ip_address": { "default": "", "not": { @@ -17081,29 +16942,6 @@ }, "title": "Network", "type": "string" - }, - "network_id": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Network Id", - "type": "string" - }, - "network_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Network Id Stability" } }, "required": [ @@ -17112,20 +16950,9 @@ "title": "RuntimeNetworkEndpoint", "type": "object" }, - "RuntimeNetworkIdStability": { - "description": "Stability class for a backend-generated network or endpoint identifier.\n\nDistinct from :class:`RuntimeFilesystemStability`: filesystem stability and\nbackend-identifier stability are different concepts (ADR-025).", - "enum": [ - "stable", - "ephemeral", - "unknown", - "other" - ], - "title": "RuntimeNetworkIdStability", - "type": "string" - }, "RuntimeNetworkRealization": { "additionalProperties": false, - "description": "Observed container network realization facts for a node.\n\n``hostname`` and ``domainname`` are the container's network identity facts,\nkept distinct from per-network aliases and DNS names.", + "description": "Declarative container network state for a node.\n\n``hostname`` and ``domainname`` are required container network identities,\nkept distinct from per-network aliases and DNS names.", "properties": { "description": { "default": "", @@ -17404,7 +17231,7 @@ }, "RuntimeOperationalPolicy": { "additionalProperties": false, - "description": "Observed restart and resource-limit policy for a runtime node.", + "description": "Required restart and resource-limit policy for a runtime node.", "properties": { "description": { "default": "", @@ -17788,7 +17615,7 @@ }, "RuntimePackage": { "additionalProperties": false, - "description": "A package observed in a runtime image or node.", + "description": "A package required in a runtime image or node.", "properties": { "architecture": { "default": "", @@ -17844,124 +17671,6 @@ "title": "RuntimePackage", "type": "object" }, - "RuntimePackageVulnerabilityFinding": { - "additionalProperties": false, - "description": "A scanner-derived CVE/advisory finding for an observed package.", - "properties": { - "advisory_url": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Advisory Url", - "type": "string" - }, - "fixed_version": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Fixed Version", - "type": "string" - }, - "id": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Id", - "type": "string" - }, - "image_digest": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Image Digest", - "type": "string" - }, - "installed_version": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Installed Version", - "type": "string" - }, - "package_name": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Package Name", - "type": "string" - }, - "scan_time": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scan Time", - "type": "string" - }, - "scanner": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner", - "type": "string" - }, - "scanner_database": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner Database", - "type": "string" - }, - "scanner_version": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner Version", - "type": "string" - }, - "severity": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimePackageVulnerabilitySeverity" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Severity" - } - }, - "required": [ - "id", - "package_name", - "installed_version", - "scanner", - "image_digest", - "scan_time" - ], - "title": "RuntimePackageVulnerabilityFinding", - "type": "object" - }, - "RuntimePackageVulnerabilitySeverity": { - "description": "Scanner-derived package finding severity.", - "enum": [ - "unknown", - "low", - "medium", - "high", - "critical", - "other" - ], - "title": "RuntimePackageVulnerabilitySeverity", - "type": "string" - }, "RuntimePlatformApplication": { "additionalProperties": false, "description": "Node-scoped runtime inventory for a security platform application.\n\n``service`` references the owning same-node ``Node.services[].name``. The\ninventory is observation metadata above transport; it never duplicates the\ninbound HTTP route surface (``runtime.applications``) or mutates\n``Node.services``. ``authorization_ref`` is the ``app_authorization_id`` of\nthe platform's internal RBAC store (resolved by the semantic validator).", @@ -18977,7 +18686,7 @@ }, "RuntimePublishedPort": { "additionalProperties": false, - "description": "An observed host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", + "description": "A required host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", "properties": { "container_port": { "anyOf": [ @@ -19103,7 +18812,7 @@ }, "RuntimeResourceLimits": { "additionalProperties": false, - "description": "Observed runtime/cgroup resource limits for a node.", + "description": "Required runtime/cgroup resource limits for a node.", "properties": { "cpu": { "anyOf": [ @@ -19208,7 +18917,7 @@ "type": "object" }, "RuntimeRestartPolicy": { - "description": "Portable restart policy classification observed at runtime.", + "description": "Portable restart policy required by the scenario.", "enum": [ "no", "always", @@ -21085,7 +20794,7 @@ }, "RuntimeSoftwareComponent": { "additionalProperties": false, - "description": "A software component observed as part of a runtime node's state.", + "description": "A software component required as part of a runtime node's state.", "properties": { "component_id": { "not": { @@ -21230,7 +20939,7 @@ }, "RuntimeSoftwareComponentHash": { "additionalProperties": false, - "description": "Digest attached to an observed runtime software component.", + "description": "Digest required for a runtime software component.", "properties": { "algorithm": { "not": { @@ -21255,15 +20964,11 @@ "type": "object" }, "RuntimeSoftwareComponentProvenance": { - "description": "Origin class for an observed runtime software component fact.", + "description": "Required source origin for a runtime software component.", "enum": [ "package_manager", "dependency_manifest", - "sbom", - "scanner", "image_metadata", - "filesystem", - "process_inspection", "operator", "self_reported", "unknown", @@ -21273,7 +20978,7 @@ "type": "string" }, "RuntimeSoftwareComponentType": { - "description": "Portable type for a software component observed on a runtime node.", + "description": "Portable type for a software component required on a runtime node.", "enum": [ "application", "framework", @@ -21947,7 +21652,7 @@ }, "ServicePort": { "additionalProperties": false, - "description": "A network service exposed by a node. From OCSF NetworkEndpoint.", + "description": "Authored identity of a node-local transport binding.\n\nA service declaration does not authorize traffic, prove a live listener,\npublish a host port, or classify an internal/external audience. Traffic\nauthorization belongs to infrastructure ACLs; observed bind state and host\npublication belong to their dedicated runtime surfaces.", "properties": { "description": { "default": "", diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index 9eba3f1c1..8b0c917aa 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -451,6 +451,16 @@ "title": "AssetValueLevel", "type": "string" }, + "AuthorRealizationPosture": { + "description": "Authored default posture, distinct from capability support modes.", + "enum": [ + "closed", + "open", + "unspecified" + ], + "title": "AuthorRealizationPosture", + "type": "string" + }, "BehaviorSpecificationExtensionValue": { "anyOf": [ { @@ -4116,6 +4126,14 @@ "title": "Imports", "type": "array" }, + "realization_designations": { + "default": [], + "items": { + "$ref": "#/$defs/RealizationDesignationRecord" + }, + "title": "Realization Designations", + "type": "array" + }, "selected_profile": { "anyOf": [ { @@ -6343,6 +6361,52 @@ "title": "PropositionBasis", "type": "string" }, + "RealizationDesignationRecord": { + "additionalProperties": false, + "description": "Portable authored designation carried across SDL document phases.", + "properties": { + "field_pointer": { + "default": "", + "maxLength": 4096, + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "title": "Field Pointer", + "type": "string" + }, + "namespace": { + "default": [], + "items": { + "allOf": [ + { + "not": { + "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" + } + } + ], + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 31, + "title": "Namespace", + "type": "array" + }, + "posture": { + "$ref": "#/$defs/AuthorRealizationPosture" + } + }, + "required": [ + "posture" + ], + "title": "RealizationDesignationRecord", + "type": "object" + }, "Relationship": { "additionalProperties": false, "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", @@ -8313,7 +8377,7 @@ }, "RuntimeConfiguration": { "additionalProperties": false, - "description": "Observed runtime configuration facts attached to a VM node.", + "description": "Declarative runtime state required by a VM node.\n\nCaptured observations remain in evidence records unless an author\ndeliberately promotes the fact to one of these contract fields.", "properties": { "app_authorizations": { "items": { @@ -8421,17 +8485,6 @@ "title": "Forwarding Agents", "type": "array" }, - "health": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthObservation" - }, - { - "type": "null" - } - ], - "default": null - }, "identity_authorities": { "items": { "$ref": "#/$defs/RuntimeIdentityAuthority" @@ -8525,13 +8578,6 @@ "title": "Orchestration Authorities", "type": "array" }, - "package_vulnerabilities": { - "items": { - "$ref": "#/$defs/RuntimePackageVulnerabilityFinding" - }, - "title": "Package Vulnerabilities", - "type": "array" - }, "packages": { "items": { "$ref": "#/$defs/RuntimePackage" @@ -8601,7 +8647,7 @@ }, "RuntimeContainerConfiguration": { "additionalProperties": false, - "description": "Observed container runtime host and security configuration facts.", + "description": "Required container runtime host and security configuration facts.", "properties": { "autoremove": { "anyOf": [ @@ -10777,7 +10823,7 @@ }, "RuntimeDependencyManifest": { "additionalProperties": false, - "description": "A dependency manifest visible in the realized runtime artifact.", + "description": "A dependency manifest required in the runtime artifact.", "properties": { "ecosystem": { "not": { @@ -10827,7 +10873,7 @@ }, "RuntimeDeviceMapping": { "additionalProperties": false, - "description": "A host device mapping observed in runtime configuration.", + "description": "A host device mapping required in runtime configuration.", "properties": { "container_path": { "not": { @@ -11015,7 +11061,7 @@ "type": "object" }, "RuntimeEnvironmentValueClassification": { - "description": "Sensitivity classification for an observed runtime environment value.", + "description": "Sensitivity classification for a required runtime environment value.", "enum": [ "plain", "redacted", @@ -11029,7 +11075,7 @@ }, "RuntimeEnvironmentVariable": { "additionalProperties": false, - "description": "Observed runtime environment variable with provenance and sensitivity.", + "description": "Required runtime environment variable with provenance and sensitivity.", "properties": { "description": { "default": "", @@ -11100,7 +11146,7 @@ "type": "object" }, "RuntimeEnvironmentVariableProvenance": { - "description": "Origin class for an observed runtime environment variable.", + "description": "Required origin class for a runtime environment variable.", "enum": [ "compose", "image", @@ -11115,7 +11161,7 @@ }, "RuntimeExtraHost": { "additionalProperties": false, - "description": "An observed extra host mapping in runtime configuration.", + "description": "A required extra host mapping in runtime configuration.", "properties": { "address": { "not": { @@ -12881,140 +12927,6 @@ "title": "RuntimeForwardingTransformKind", "type": "string" }, - "RuntimeHealthObservation": { - "additionalProperties": false, - "description": "Observed runtime health status and healthcheck log facts.", - "properties": { - "description": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Description", - "type": "string" - }, - "failing_streak": { - "anyOf": [ - { - "type": "integer" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Failing Streak" - }, - "log": { - "items": { - "$ref": "#/$defs/RuntimeHealthcheckLog" - }, - "title": "Log", - "type": "array" - }, - "status": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthStatus" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Status" - } - }, - "title": "RuntimeHealthObservation", - "type": "object" - }, - "RuntimeHealthStatus": { - "description": "Observed health status for a runtime node or container.", - "enum": [ - "none", - "starting", - "healthy", - "unhealthy", - "unknown", - "other" - ], - "title": "RuntimeHealthStatus", - "type": "string" - }, - "RuntimeHealthcheckLog": { - "additionalProperties": false, - "description": "An observed runtime healthcheck log entry.", - "properties": { - "end": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "End", - "type": "string" - }, - "exit_code": { - "anyOf": [ - { - "type": "integer" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Exit Code" - }, - "output": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Output", - "type": "string" - }, - "output_redacted": { - "anyOf": [ - { - "type": "boolean" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": false, - "title": "Output Redacted" - }, - "start": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Start", - "type": "string" - } - }, - "title": "RuntimeHealthcheckLog", - "type": "object" - }, "RuntimeIdentityAttribute": { "additionalProperties": false, "description": "Observed identity attribute or bounded setting.\n\nSecret-bearing names must not carry raw values. This keeps directory\npasswords, Kerberos keys, keytabs, tokens, and client secrets out of\nfixtures, diagnostics, schemas, and generated runtime artifacts.", @@ -13720,7 +13632,7 @@ }, "RuntimeInitProcess": { "additionalProperties": false, - "description": "Observed backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", + "description": "Required backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", "properties": { "argv": { "items": { @@ -15619,7 +15531,7 @@ }, "RuntimeNamespaceConfiguration": { "additionalProperties": false, - "description": "Observed namespace modes for a runtime node or container.", + "description": "Required namespace modes for a runtime node or container.", "properties": { "cgroup": { "default": "", @@ -15667,7 +15579,7 @@ }, "RuntimeNetworkBackendDetail": { "additionalProperties": false, - "description": "Observed backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", + "description": "Required backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", "properties": { "description": { "default": "", @@ -16474,7 +16386,7 @@ "type": "string" }, "RuntimeNetworkDriver": { - "description": "Observed backend network driver class for a realized network.", + "description": "Backend network driver class required by the scenario.", "enum": [ "bridge", "overlay", @@ -16490,7 +16402,7 @@ }, "RuntimeNetworkEndpoint": { "additionalProperties": false, - "description": "An observed container network endpoint (per-network attachment).\n\n``network`` references a declared switch-backed infrastructure network by\nname. Per-network ``aliases``, observed ``dns_names``, and backend-generated\n``generated_dns_names`` are kept as three distinct lists; generated names\nare not stable scenario identity.", + "description": "A required container network attachment.\n\n``network`` references a declared switch-backed infrastructure network by\nname. Runtime-generated identifiers and names are capture evidence, not\ndeclaration identity.", "properties": { "aliases": { "items": { @@ -16513,24 +16425,6 @@ ], "default": null }, - "backend_generated": { - "anyOf": [ - { - "type": "boolean" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Backend Generated" - }, "description": { "default": "", "not": { @@ -16549,29 +16443,6 @@ "title": "Dns Names", "type": "array" }, - "endpoint_id": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Endpoint Id", - "type": "string" - }, - "endpoint_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Endpoint Id Stability" - }, "gateway": { "default": "", "not": { @@ -16580,16 +16451,6 @@ "title": "Gateway", "type": "string" }, - "generated_dns_names": { - "items": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - }, - "title": "Generated Dns Names", - "type": "array" - }, "ip_address": { "default": "", "not": { @@ -16630,29 +16491,6 @@ }, "title": "Network", "type": "string" - }, - "network_id": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Network Id", - "type": "string" - }, - "network_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Network Id Stability" } }, "required": [ @@ -16661,20 +16499,9 @@ "title": "RuntimeNetworkEndpoint", "type": "object" }, - "RuntimeNetworkIdStability": { - "description": "Stability class for a backend-generated network or endpoint identifier.\n\nDistinct from :class:`RuntimeFilesystemStability`: filesystem stability and\nbackend-identifier stability are different concepts (ADR-025).", - "enum": [ - "stable", - "ephemeral", - "unknown", - "other" - ], - "title": "RuntimeNetworkIdStability", - "type": "string" - }, "RuntimeNetworkRealization": { "additionalProperties": false, - "description": "Observed container network realization facts for a node.\n\n``hostname`` and ``domainname`` are the container's network identity facts,\nkept distinct from per-network aliases and DNS names.", + "description": "Declarative container network state for a node.\n\n``hostname`` and ``domainname`` are required container network identities,\nkept distinct from per-network aliases and DNS names.", "properties": { "description": { "default": "", @@ -16953,7 +16780,7 @@ }, "RuntimeOperationalPolicy": { "additionalProperties": false, - "description": "Observed restart and resource-limit policy for a runtime node.", + "description": "Required restart and resource-limit policy for a runtime node.", "properties": { "description": { "default": "", @@ -17337,7 +17164,7 @@ }, "RuntimePackage": { "additionalProperties": false, - "description": "A package observed in a runtime image or node.", + "description": "A package required in a runtime image or node.", "properties": { "architecture": { "default": "", @@ -17393,124 +17220,6 @@ "title": "RuntimePackage", "type": "object" }, - "RuntimePackageVulnerabilityFinding": { - "additionalProperties": false, - "description": "A scanner-derived CVE/advisory finding for an observed package.", - "properties": { - "advisory_url": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Advisory Url", - "type": "string" - }, - "fixed_version": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Fixed Version", - "type": "string" - }, - "id": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Id", - "type": "string" - }, - "image_digest": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Image Digest", - "type": "string" - }, - "installed_version": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Installed Version", - "type": "string" - }, - "package_name": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Package Name", - "type": "string" - }, - "scan_time": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scan Time", - "type": "string" - }, - "scanner": { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner", - "type": "string" - }, - "scanner_database": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner Database", - "type": "string" - }, - "scanner_version": { - "default": "", - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "title": "Scanner Version", - "type": "string" - }, - "severity": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimePackageVulnerabilitySeverity" - }, - { - "not": { - "pattern": "\\$\\{([a-z0-9][a-z0-9_-]{0,63})\\}" - }, - "type": "string" - } - ], - "default": "unknown", - "title": "Severity" - } - }, - "required": [ - "id", - "package_name", - "installed_version", - "scanner", - "image_digest", - "scan_time" - ], - "title": "RuntimePackageVulnerabilityFinding", - "type": "object" - }, - "RuntimePackageVulnerabilitySeverity": { - "description": "Scanner-derived package finding severity.", - "enum": [ - "unknown", - "low", - "medium", - "high", - "critical", - "other" - ], - "title": "RuntimePackageVulnerabilitySeverity", - "type": "string" - }, "RuntimePlatformApplication": { "additionalProperties": false, "description": "Node-scoped runtime inventory for a security platform application.\n\n``service`` references the owning same-node ``Node.services[].name``. The\ninventory is observation metadata above transport; it never duplicates the\ninbound HTTP route surface (``runtime.applications``) or mutates\n``Node.services``. ``authorization_ref`` is the ``app_authorization_id`` of\nthe platform's internal RBAC store (resolved by the semantic validator).", @@ -18526,7 +18235,7 @@ }, "RuntimePublishedPort": { "additionalProperties": false, - "description": "An observed host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", + "description": "A required host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", "properties": { "container_port": { "anyOf": [ @@ -18652,7 +18361,7 @@ }, "RuntimeResourceLimits": { "additionalProperties": false, - "description": "Observed runtime/cgroup resource limits for a node.", + "description": "Required runtime/cgroup resource limits for a node.", "properties": { "cpu": { "anyOf": [ @@ -18757,7 +18466,7 @@ "type": "object" }, "RuntimeRestartPolicy": { - "description": "Portable restart policy classification observed at runtime.", + "description": "Portable restart policy required by the scenario.", "enum": [ "no", "always", @@ -20634,7 +20343,7 @@ }, "RuntimeSoftwareComponent": { "additionalProperties": false, - "description": "A software component observed as part of a runtime node's state.", + "description": "A software component required as part of a runtime node's state.", "properties": { "component_id": { "not": { @@ -20779,7 +20488,7 @@ }, "RuntimeSoftwareComponentHash": { "additionalProperties": false, - "description": "Digest attached to an observed runtime software component.", + "description": "Digest required for a runtime software component.", "properties": { "algorithm": { "not": { @@ -20804,15 +20513,11 @@ "type": "object" }, "RuntimeSoftwareComponentProvenance": { - "description": "Origin class for an observed runtime software component fact.", + "description": "Required source origin for a runtime software component.", "enum": [ "package_manager", "dependency_manifest", - "sbom", - "scanner", "image_metadata", - "filesystem", - "process_inspection", "operator", "self_reported", "unknown", @@ -20822,7 +20527,7 @@ "type": "string" }, "RuntimeSoftwareComponentType": { - "description": "Portable type for a software component observed on a runtime node.", + "description": "Portable type for a software component required on a runtime node.", "enum": [ "application", "framework", @@ -21496,7 +21201,7 @@ }, "ServicePort": { "additionalProperties": false, - "description": "A network service exposed by a node. From OCSF NetworkEndpoint.", + "description": "Authored identity of a node-local transport binding.\n\nA service declaration does not authorize traffic, prove a live listener,\npublish a host port, or classify an internal/external audience. Traffic\nauthorization belongs to infrastructure ACLs; observed bind state and host\npublication belong to their dedicated runtime surfaces.", "properties": { "description": { "default": "", diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index 1ce9bed71..426b7fc97 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -348,6 +348,16 @@ "title": "AssetValueLevel", "type": "string" }, + "AuthorRealizationPosture": { + "description": "Authored default posture, distinct from capability support modes.", + "enum": [ + "closed", + "open", + "unspecified" + ], + "title": "AuthorRealizationPosture", + "type": "string" + }, "BehaviorSpecificationExtensionValue": { "anyOf": [ { @@ -5015,6 +5025,63 @@ "title": "PropositionBasis", "type": "string" }, + "RealizationDesignation": { + "additionalProperties": false, + "description": "Scenario-root SEM-218 designation table.", + "properties": { + "default": { + "$ref": "#/$defs/AuthorRealizationPosture", + "default": "unspecified" + }, + "scopes": { + "default": [], + "items": { + "$ref": "#/$defs/RealizationScopeDesignation" + }, + "title": "Scopes", + "type": "array" + } + }, + "title": "RealizationDesignation", + "type": "object" + }, + "RealizationScopeDesignation": { + "additionalProperties": false, + "description": "One typed lexical override at a canonical composed-model scope.", + "properties": { + "field_pointer": { + "maxLength": 4096, + "minLength": 1, + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "title": "Field Pointer", + "type": "string" + }, + "namespace": { + "default": [], + "items": { + "maxLength": 64, + "minLength": 1, + "not": { + "pattern": "[^a-z0-9_-]" + }, + "pattern": "^[a-z0-9]", + "type": "string" + }, + "maxItems": 31, + "title": "Namespace", + "type": "array" + }, + "posture": { + "$ref": "#/$defs/AuthorRealizationPosture" + } + }, + "required": [ + "field_pointer", + "posture" + ], + "title": "RealizationScopeDesignation", + "type": "object" + }, "Relationship": { "additionalProperties": false, "description": "A typed directed edge between two named scenario elements.\n\nSource and target can reference any named element in the scenario:\nnodes, features, accounts, entities, or infrastructure entries.\nThe validator checks that both endpoints resolve.\n\nThe ``properties`` dict carries type-specific metadata (e.g.,\n``trust_type: parent-child`` for AD trusts, ``protocol: SAML``\nfor federation). It's a flat dict rather than typed sub-models\nbecause relationship properties vary widely and we don't want\nto gate expressiveness on pre-modeling every variant.\n\n``database_access`` and ``mail_access`` are typed exceptions where\nprotocol/auth details need structural validation rather than prose.\n``forwarding_edge``, ``service_integration``, and ``proxy_upstream`` are\nthe same kind of typed exception for, respectively, a forwarding /\nintel-sync agent's inter-node trust edge (SCN-010 \u00a75.7), a platform\nconsumer-to-engine service integration, and a reverse-proxy/gateway\nroute-to-origin upstream hop. Each keeps protocol/auth/topology facts\nstructurally validated and cross-referable rather than buried in prose.", @@ -6462,7 +6529,7 @@ }, "RuntimeConfiguration": { "additionalProperties": false, - "description": "Observed runtime configuration facts attached to a VM node.", + "description": "Declarative runtime state required by a VM node.\n\nCaptured observations remain in evidence records unless an author\ndeliberately promotes the fact to one of these contract fields.", "properties": { "app_authorizations": { "items": { @@ -6563,17 +6630,6 @@ "title": "Forwarding Agents", "type": "array" }, - "health": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthObservation" - }, - { - "type": "null" - } - ], - "default": null - }, "identity_authorities": { "items": { "$ref": "#/$defs/RuntimeIdentityAuthority" @@ -6667,13 +6723,6 @@ "title": "Orchestration Authorities", "type": "array" }, - "package_vulnerabilities": { - "items": { - "$ref": "#/$defs/RuntimePackageVulnerabilityFinding" - }, - "title": "Package Vulnerabilities", - "type": "array" - }, "packages": { "items": { "$ref": "#/$defs/RuntimePackage" @@ -6743,7 +6792,7 @@ }, "RuntimeContainerConfiguration": { "additionalProperties": false, - "description": "Observed container runtime host and security configuration facts.", + "description": "Required container runtime host and security configuration facts.", "properties": { "autoremove": { "anyOf": [ @@ -8460,7 +8509,7 @@ }, "RuntimeDependencyManifest": { "additionalProperties": false, - "description": "A dependency manifest visible in the realized runtime artifact.", + "description": "A dependency manifest required in the runtime artifact.", "properties": { "ecosystem": { "title": "Ecosystem", @@ -8495,7 +8544,7 @@ }, "RuntimeDeviceMapping": { "additionalProperties": false, - "description": "A host device mapping observed in runtime configuration.", + "description": "A host device mapping required in runtime configuration.", "properties": { "container_path": { "title": "Container Path", @@ -8640,7 +8689,7 @@ "type": "object" }, "RuntimeEnvironmentValueClassification": { - "description": "Sensitivity classification for an observed runtime environment value.", + "description": "Sensitivity classification for a required runtime environment value.", "enum": [ "plain", "redacted", @@ -8654,7 +8703,7 @@ }, "RuntimeEnvironmentVariable": { "additionalProperties": false, - "description": "Observed runtime environment variable with provenance and sensitivity.", + "description": "Required runtime environment variable with provenance and sensitivity.", "properties": { "description": { "default": "", @@ -8707,7 +8756,7 @@ "type": "object" }, "RuntimeEnvironmentVariableProvenance": { - "description": "Origin class for an observed runtime environment variable.", + "description": "Required origin class for a runtime environment variable.", "enum": [ "compose", "image", @@ -8722,7 +8771,7 @@ }, "RuntimeExtraHost": { "additionalProperties": false, - "description": "An observed extra host mapping in runtime configuration.", + "description": "A required extra host mapping in runtime configuration.", "properties": { "address": { "title": "Address", @@ -10142,116 +10191,6 @@ "title": "RuntimeForwardingTransformKind", "type": "string" }, - "RuntimeHealthObservation": { - "additionalProperties": false, - "description": "Observed runtime health status and healthcheck log facts.", - "properties": { - "description": { - "default": "", - "title": "Description", - "type": "string" - }, - "failing_streak": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Failing Streak" - }, - "log": { - "items": { - "$ref": "#/$defs/RuntimeHealthcheckLog" - }, - "title": "Log", - "type": "array" - }, - "status": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeHealthStatus" - }, - { - "type": "string" - } - ], - "default": "unknown", - "title": "Status" - } - }, - "title": "RuntimeHealthObservation", - "type": "object" - }, - "RuntimeHealthStatus": { - "description": "Observed health status for a runtime node or container.", - "enum": [ - "none", - "starting", - "healthy", - "unhealthy", - "unknown", - "other" - ], - "title": "RuntimeHealthStatus", - "type": "string" - }, - "RuntimeHealthcheckLog": { - "additionalProperties": false, - "description": "An observed runtime healthcheck log entry.", - "properties": { - "end": { - "default": "", - "title": "End", - "type": "string" - }, - "exit_code": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Exit Code" - }, - "output": { - "default": "", - "title": "Output", - "type": "string" - }, - "output_redacted": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ], - "default": false, - "title": "Output Redacted" - }, - "start": { - "default": "", - "title": "Start", - "type": "string" - } - }, - "title": "RuntimeHealthcheckLog", - "type": "object" - }, "RuntimeIdentityAttribute": { "additionalProperties": false, "description": "Observed identity attribute or bounded setting.\n\nSecret-bearing names must not carry raw values. This keeps directory\npasswords, Kerberos keys, keytabs, tokens, and client secrets out of\nfixtures, diagnostics, schemas, and generated runtime artifacts.", @@ -10805,7 +10744,7 @@ }, "RuntimeInitProcess": { "additionalProperties": false, - "description": "Observed backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", + "description": "Required backend-injected container init / PID-1 reaper configuration.\n\nModels authored container runtime intent \u2014 for example Docker Compose\n``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is\ndistinct from the observed PID-1 process recorded in\n``runtime.processes``; see ADR-027.", "properties": { "argv": { "items": { @@ -12328,7 +12267,7 @@ }, "RuntimeNamespaceConfiguration": { "additionalProperties": false, - "description": "Observed namespace modes for a runtime node or container.", + "description": "Required namespace modes for a runtime node or container.", "properties": { "cgroup": { "default": "", @@ -12361,7 +12300,7 @@ }, "RuntimeNetworkBackendDetail": { "additionalProperties": false, - "description": "Observed backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", + "description": "Required backend network driver and IPAM detail for an endpoint's network.\n\n``driver_options`` and ``ipam_options`` are the bounded extension seam for\nbackend-native key/value facts; raw backend inspect payloads are not stored.", "properties": { "description": { "default": "", @@ -13010,7 +12949,7 @@ "type": "string" }, "RuntimeNetworkDriver": { - "description": "Observed backend network driver class for a realized network.", + "description": "Backend network driver class required by the scenario.", "enum": [ "bridge", "overlay", @@ -13026,7 +12965,7 @@ }, "RuntimeNetworkEndpoint": { "additionalProperties": false, - "description": "An observed container network endpoint (per-network attachment).\n\n``network`` references a declared switch-backed infrastructure network by\nname. Per-network ``aliases``, observed ``dns_names``, and backend-generated\n``generated_dns_names`` are kept as three distinct lists; generated names\nare not stable scenario identity.", + "description": "A required container network attachment.\n\n``network`` references a declared switch-backed infrastructure network by\nname. Runtime-generated identifiers and names are capture evidence, not\ndeclaration identity.", "properties": { "aliases": { "items": { @@ -13046,21 +12985,6 @@ ], "default": null }, - "backend_generated": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Backend Generated" - }, "description": { "default": "", "title": "Description", @@ -13073,35 +12997,11 @@ "title": "Dns Names", "type": "array" }, - "endpoint_id": { - "default": "", - "title": "Endpoint Id", - "type": "string" - }, - "endpoint_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "type": "string" - } - ], - "default": "unknown", - "title": "Endpoint Id Stability" - }, "gateway": { "default": "", "title": "Gateway", "type": "string" }, - "generated_dns_names": { - "items": { - "type": "string" - }, - "title": "Generated Dns Names", - "type": "array" - }, "ip_address": { "default": "", "title": "Ip Address", @@ -13130,23 +13030,6 @@ "network": { "title": "Network", "type": "string" - }, - "network_id": { - "default": "", - "title": "Network Id", - "type": "string" - }, - "network_id_stability": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimeNetworkIdStability" - }, - { - "type": "string" - } - ], - "default": "unknown", - "title": "Network Id Stability" } }, "required": [ @@ -13155,20 +13038,9 @@ "title": "RuntimeNetworkEndpoint", "type": "object" }, - "RuntimeNetworkIdStability": { - "description": "Stability class for a backend-generated network or endpoint identifier.\n\nDistinct from :class:`RuntimeFilesystemStability`: filesystem stability and\nbackend-identifier stability are different concepts (ADR-025).", - "enum": [ - "stable", - "ephemeral", - "unknown", - "other" - ], - "title": "RuntimeNetworkIdStability", - "type": "string" - }, "RuntimeNetworkRealization": { "additionalProperties": false, - "description": "Observed container network realization facts for a node.\n\n``hostname`` and ``domainname`` are the container's network identity facts,\nkept distinct from per-network aliases and DNS names.", + "description": "Declarative container network state for a node.\n\n``hostname`` and ``domainname`` are required container network identities,\nkept distinct from per-network aliases and DNS names.", "properties": { "description": { "default": "", @@ -13389,7 +13261,7 @@ }, "RuntimeOperationalPolicy": { "additionalProperties": false, - "description": "Observed restart and resource-limit policy for a runtime node.", + "description": "Required restart and resource-limit policy for a runtime node.", "properties": { "description": { "default": "", @@ -13686,7 +13558,7 @@ }, "RuntimePackage": { "additionalProperties": false, - "description": "A package observed in a runtime image or node.", + "description": "A package required in a runtime image or node.", "properties": { "architecture": { "default": "", @@ -13724,91 +13596,6 @@ "title": "RuntimePackage", "type": "object" }, - "RuntimePackageVulnerabilityFinding": { - "additionalProperties": false, - "description": "A scanner-derived CVE/advisory finding for an observed package.", - "properties": { - "advisory_url": { - "default": "", - "title": "Advisory Url", - "type": "string" - }, - "fixed_version": { - "default": "", - "title": "Fixed Version", - "type": "string" - }, - "id": { - "title": "Id", - "type": "string" - }, - "image_digest": { - "title": "Image Digest", - "type": "string" - }, - "installed_version": { - "title": "Installed Version", - "type": "string" - }, - "package_name": { - "title": "Package Name", - "type": "string" - }, - "scan_time": { - "title": "Scan Time", - "type": "string" - }, - "scanner": { - "title": "Scanner", - "type": "string" - }, - "scanner_database": { - "default": "", - "title": "Scanner Database", - "type": "string" - }, - "scanner_version": { - "default": "", - "title": "Scanner Version", - "type": "string" - }, - "severity": { - "anyOf": [ - { - "$ref": "#/$defs/RuntimePackageVulnerabilitySeverity" - }, - { - "type": "string" - } - ], - "default": "unknown", - "title": "Severity" - } - }, - "required": [ - "id", - "package_name", - "installed_version", - "scanner", - "image_digest", - "scan_time" - ], - "title": "RuntimePackageVulnerabilityFinding", - "type": "object" - }, - "RuntimePackageVulnerabilitySeverity": { - "description": "Scanner-derived package finding severity.", - "enum": [ - "unknown", - "low", - "medium", - "high", - "critical", - "other" - ], - "title": "RuntimePackageVulnerabilitySeverity", - "type": "string" - }, "RuntimePlatformApplication": { "additionalProperties": false, "description": "Node-scoped runtime inventory for a security platform application.\n\n``service`` references the owning same-node ``Node.services[].name``. The\ninventory is observation metadata above transport; it never duplicates the\ninbound HTTP route surface (``runtime.applications``) or mutates\n``Node.services``. ``authorization_ref`` is the ``app_authorization_id`` of\nthe platform's internal RBAC store (resolved by the semantic validator).", @@ -14597,7 +14384,7 @@ }, "RuntimePublishedPort": { "additionalProperties": false, - "description": "An observed host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", + "description": "A required host-published port binding for a container endpoint.\n\nHost IP, host port, container port, and protocol are kept distinct; this is\na runtime/host exposure fact, not a node service declaration (ADR-025).", "properties": { "container_port": { "anyOf": [ @@ -14696,7 +14483,7 @@ }, "RuntimeResourceLimits": { "additionalProperties": false, - "description": "Observed runtime/cgroup resource limits for a node.", + "description": "Required runtime/cgroup resource limits for a node.", "properties": { "cpu": { "anyOf": [ @@ -14783,7 +14570,7 @@ "type": "object" }, "RuntimeRestartPolicy": { - "description": "Portable restart policy classification observed at runtime.", + "description": "Portable restart policy required by the scenario.", "enum": [ "no", "always", @@ -16257,7 +16044,7 @@ }, "RuntimeSoftwareComponent": { "additionalProperties": false, - "description": "A software component observed as part of a runtime node's state.", + "description": "A software component required as part of a runtime node's state.", "properties": { "component_id": { "title": "Component Id", @@ -16360,7 +16147,7 @@ }, "RuntimeSoftwareComponentHash": { "additionalProperties": false, - "description": "Digest attached to an observed runtime software component.", + "description": "Digest required for a runtime software component.", "properties": { "algorithm": { "title": "Algorithm", @@ -16379,15 +16166,11 @@ "type": "object" }, "RuntimeSoftwareComponentProvenance": { - "description": "Origin class for an observed runtime software component fact.", + "description": "Required source origin for a runtime software component.", "enum": [ "package_manager", "dependency_manifest", - "sbom", - "scanner", "image_metadata", - "filesystem", - "process_inspection", "operator", "self_reported", "unknown", @@ -16397,7 +16180,7 @@ "type": "string" }, "RuntimeSoftwareComponentType": { - "description": "Portable type for a software component observed on a runtime node.", + "description": "Portable type for a software component required on a runtime node.", "enum": [ "application", "framework", @@ -16901,7 +16684,7 @@ }, "ServicePort": { "additionalProperties": false, - "description": "A network service exposed by a node. From OCSF NetworkEndpoint.", + "description": "Authored identity of a node-local transport binding.\n\nA service declaration does not authorize traffic, prove a live listener,\npublish a host port, or classify an internal/external audience. Traffic\nauthorization belongs to infrastructure ACLs; observed bind state and host\npublication belong to their dedicated runtime surfaces.", "properties": { "description": { "default": "", @@ -18338,6 +18121,18 @@ "title": "Propositions", "type": "object" }, + "realization": { + "anyOf": [ + { + "$ref": "#/$defs/RealizationDesignation" + }, + { + "type": "null" + } + ], + "default": null, + "x-aces-realization-dimension": false + }, "relationships": { "additionalProperties": { "$ref": "#/$defs/Relationship" diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index b4b5414a4..cbcbeecfa 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -3903,6 +3903,19 @@ "title": "Field Path", "type": "string" }, + "governing_scope": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Governing Scope" + }, "provenance": { "$ref": "#/$defs/ExplicitnessProvenance" }, diff --git a/docs/aces/inventory/asset-inventory-methodology.md b/docs/aces/inventory/asset-inventory-methodology.md index 53a2f2232..fb2c6baaf 100644 --- a/docs/aces/inventory/asset-inventory-methodology.md +++ b/docs/aces/inventory/asset-inventory-methodology.md @@ -204,14 +204,25 @@ instead of pretending a local build recipe exists. binds, service DNS names, static IPs, and healthcheck dependencies become visible. -10. Attempt maximal ACES specification. +10. Attempt maximal ACES mapping without silently promoting evidence. - For every participant-discoverable fact, attempt to represent it in ACES. + For every participant-discoverable fact, identify the most specific ACES + carrier. First preserve the exact observation in the source evidence bundle + and `mapping-ledger.yaml`. Then decide whether it is authored/defaulted + contract state, a planned or realized choice, an observation, or a derived + interpretation. Schema compatibility alone does not authorize copying a + captured value into SDL. + + Attempt to represent deliberate scenario requirements in ACES SDL. Use the most specific existing SDL surface available: nodes, infrastructure, services, features, content, accounts, relationships, agents, objectives, workflows, variables, or a contract surface if one - already exists. The question at this stage is not "should we specify - less?" It is "can ACES fully specify this discovered world fact?" + already exists. The questions at this stage are "where can ACES preserve + this fact exactly?" and, separately, "does the scenario author intend to + require it?" Generated Docker network/endpoint IDs, generated DNS + identities, health results, and scanner version/database/time/findings + remain evidence by default. A deliberate promotion into SDL makes the value + ordinary exact, constrained, or open contract state. Record the result in `mapping-ledger.yaml` instead of relying on prose. Each captured fact must have one of these dispositions: diff --git a/docs/api/processor.rst b/docs/api/processor.rst index 02a87cd8d..050006d4f 100644 --- a/docs/api/processor.rst +++ b/docs/api/processor.rst @@ -25,7 +25,38 @@ Compiler Processor Models ---------------- -.. automodule:: aces_processor.models +The runtime data models live in the ``aces_processor.models`` package; its +``__init__`` re-exports the full public surface, and the classes and functions +are documented here from the subdomain modules that define them. + +.. automodule:: aces_processor.models.resources + :members: + +.. automodule:: aces_processor.models.behavior_resources + :members: + +.. automodule:: aces_processor.models.action_results + :members: + +.. automodule:: aces_processor.models.attribution + :members: + +.. automodule:: aces_processor.models.outcome + :members: + +.. automodule:: aces_processor.models.outcome_interpretation_validation + :members: + +.. automodule:: aces_processor.models.temporal + :members: + +.. automodule:: aces_processor.models.history_event + :members: + +.. automodule:: aces_processor.models.behavior_history_violations + :members: + +.. automodule:: aces_processor.models.runtime_model :members: Manifest Authority diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index b1290e71c..9696703c6 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -210,3 +210,4 @@ adr-080-revision-pinned-sdl-lineage-and-provenance-ledger | [078](adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md) | Closed SDL Phase Contracts and Portable Derivation Evidence | accepted | 2026-07-12 | | [079](adr-079-backend-neutral-proposition-and-truth-semantics.md) | Backend-Neutral Proposition and Truth Semantics | accepted | 2026-07-12 | | [080](adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md) | Revision-Pinned SDL Lineage And Provenance Ledger | accepted | 2026-07-12 | +| [081](adr-081-behavioral-relation-taxonomy-and-claim-discipline.md) | Behavioral-Relation Taxonomy And Claim Discipline | accepted | 2026-07-13 | diff --git a/docs/decisions/adrs/adr-025-container-network-realization-surface.md b/docs/decisions/adrs/adr-025-container-network-realization-surface.md index 35513d46e..f3080ab6c 100644 --- a/docs/decisions/adrs/adr-025-container-network-realization-surface.md +++ b/docs/decisions/adrs/adr-025-container-network-realization-surface.md @@ -208,3 +208,4 @@ creating a second network schema elsewhere. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-05-25 | 4b959c3 | Corrected the control-plane API cross-reference from `aces_processor.control_plane_api` to `aces_runtime.control_plane_api`. | +| 2026-07-13 | #417 | Corrected the carrier boundary: deliberately required addresses, names, MACs, backend configuration, and published bindings remain declarative runtime contract state, while generated network/endpoint IDs, stability metadata, generated DNS identities, and incidental inspect output move to evidence and mapping ledgers. | diff --git a/docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md b/docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md index 4b26e9952..5eb4e6560 100644 --- a/docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md +++ b/docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md @@ -224,3 +224,9 @@ different facts. — adjacent container-security practice identifying images, runtime configuration, orchestrators, and host interactions as distinct security-relevant surfaces. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-13 | #417 | Refined the six-way carrier boundary: participant-discoverable facts are preserved in evidence by default and enter authored `Node.runtime` only through a deliberate exact, constrained, or open scenario requirement; observation alone is not promotion authority. | diff --git a/docs/decisions/adrs/adr-034-runtime-software-component-inventory.md b/docs/decisions/adrs/adr-034-runtime-software-component-inventory.md index f0e758b7a..55682d2cf 100644 --- a/docs/decisions/adrs/adr-034-runtime-software-component-inventory.md +++ b/docs/decisions/adrs/adr-034-runtime-software-component-inventory.md @@ -91,3 +91,9 @@ remains evidence/provenance input rather than the normative schema. - Future references from components to packages, manifests, files, or processes must add semantic validation and module-reference support together, rather than publishing dangling string conventions. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-13 | #417 | Clarified that authored software components describe required final state; scanner, SBOM, filesystem, and process-inspection capture methods remain evidence provenance and are no longer accepted as component provenance values in SDL. | diff --git a/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md b/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md index 3b38ad4b6..fe3fba343 100644 --- a/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md +++ b/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md @@ -131,3 +131,9 @@ Rejected alternatives: would make raw secret omission advisory instead of executable. - Folding credential strength into credential posture was rejected because it would conflate presence/redaction with quality or assurance. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-13 | #417 | Narrowed the authored SDL boundary: health results, generated network identity, scanner-derived package findings, and scanner capture provenance use evidence/derived carriers rather than `Node.runtime`; the shared redaction invariant continues to govern the remaining declarative runtime fields. | diff --git a/docs/decisions/adrs/adr-070-realization-envelope-semantics.md b/docs/decisions/adrs/adr-070-realization-envelope-semantics.md index 42df3519a..fe8337349 100644 --- a/docs/decisions/adrs/adr-070-realization-envelope-semantics.md +++ b/docs/decisions/adrs/adr-070-realization-envelope-semantics.md @@ -124,8 +124,12 @@ plus structural closure checks, and keeps witness generation deterministic. Posture applies at one of five scopes: field, node, topology, app, or scenario. Most-specific-wins selects the effective posture for a concrete field or child -scope, but a more-specific posture may not silently widen a closed enclosing -scope. Equal-specificity conflicts are diagnostics, not merge order. +scope. The `overrideable` rule applies when a more-specific explicit envelope +domain binding widens an enclosing binding; it does not prohibit the SEM-218 +lexical author-default cascade from overriding inherited open/closed posture in +either direction. Equal path-specificity is broken by semantic scope +specificity (field, node, topology/app, scenario), and equal-specificity +conflicts are diagnostics rather than merge order. Open means the expression leaves the value to a downstream realizer at a point the SDL semantics declares realizable. Constrained means the value must fall @@ -133,6 +137,10 @@ inside a typed domain. Exact means the domain is a singleton. Closed-world scope means no unspecified realizable dimensions under that scope are portable members of the set. +An effective `open-world` closure overlay replaces inherited `closed-world` +state at the same or a descendant scope. This replacement is closure-cascade +resolution, not widening of an explicit domain binding. + ### 5. Backend manifest carriage uses configuration-bound identity `backend-manifest-v2.realization_support` discloses coarse support but cannot by diff --git a/docs/decisions/adrs/adr-081-behavioral-relation-taxonomy-and-claim-discipline.md b/docs/decisions/adrs/adr-081-behavioral-relation-taxonomy-and-claim-discipline.md new file mode 100644 index 000000000..ce358a27d --- /dev/null +++ b/docs/decisions/adrs/adr-081-behavioral-relation-taxonomy-and-claim-discipline.md @@ -0,0 +1,130 @@ +# ADR-081: Behavioral-Relation Taxonomy And Claim Discipline + +## Status + +accepted + +## Date + +2026-07-13 + +## Classification + +Classification: FM2 + +Required artifacts: an explicit invariant list, a revisioned formal +specification, a closed machine-readable catalog and published JSON Schema, +typed claim bindings, counterexample and property-based tests, consumer +integration, a semantic policy gate, and an auditable delivery assessment. + +Waivers: universal refinement, simulation, equivalence, bisimulation, +epistemic, strategic, probabilistic, timed, and partial-order proofs are outside +this change. Their catalog assurance states remain deliberately unproved or +future. No theorem prover, model checker, game solver, probability kernel, or +new runtime engine is introduced. + +## Context + +ACES previously used validation, conformance, refinement, equivalence, and +behavior-history language across different artifact boundaries without one +revisioned relation authority. Schema acceptance, bounded fixture success, +digest equality, a terminal participant record, and a study result could +therefore be read as stronger statements than their evidence supported. + +This was not only a vocabulary problem. Each formal relation depends on +carriers, transition labels, hidden-action treatment, observation projection, +quantifiers, nondeterminism, concurrency, probability, time, partial order, +and a preservation obligation. Each empirical relation depends on a bounded +population, metric, uncertainty procedure, and falsification criteria. A flat +word list cannot preserve those distinctions. + +## Decision + +1. The ACES-native concept family `behavioral-relations` owns relation and + claim-binding discipline. It classifies claims about other artifacts; it + does not reclassify the scenarios, actions, observations, apparatuses, + participants, runs, studies, or evidence to which those claims refer. +2. `contracts/concept-authority/behavioral-relations-v1.json` is the single + machine-readable catalog for taxonomy `aces-behavioral-relations`, revision + `rev1`. `specs/formal/behavioral-relations/README.md` is its normative + reader-facing formalization. Relation meaning must not be duplicated in + consumer-local registries. +3. Every relation definition states carrier types, initial-state treatment, + transition signature, observation projection, direction, quantification, + nondeterminism, concurrency, probability, time, partial-order treatment, + preservation and proof obligations, bounded evidence, nonclaims, assurance + state, and revision-pinned primary sources. +4. Relation identity, report truth, and assurance are independent. Definition, + implementation, test, and proof statuses cannot be promoted into one another. +5. Claim-bearing consumers use `BehavioralClaimBindingModel`. A binding names + taxonomy revision, relation, subject, carriers, projection when required, + quantifier scope, evidence scope and boundary, assurance status, evidence, + limitations, and explicit nonclaims. Universal quantifiers require + model-check or proof evidence. +6. Backend conformance reports bind to `bounded-probe-success` and enumerate + their finite cases. The intended backend-realization obligation is projected + `trace-inclusion`, but revision `rev1` does not claim its proof. Fixture, + target-probe, snapshot, and negative-boundary success do not establish a + reverse direction or any equivalence. +7. Participant comparisons use + `participant-projected-history-equivalence` only when both histories share a + named participant and observation-policy revision. A single terminal + projected history is reported as a bounded record, not as an equivalence. +8. Study and benchmark contracts carry revisioned empirical claim bindings. + Independent studies under issue #729 can report only the population, + measures, uncertainty, criteria, and limitations they actually support. +9. Scientific completeness REV1 binds each profile's intended claim to the + catalog and enumerates non-claimed relations. Profile satisfaction remains + distinct from empirical adequacy and behavioral proof. +10. `tools/check_behavioral_relation_claims.py` validates structured bindings + and scans live claim surfaces for high-confidence positive relation + assertions. It permits formal definitions, explicit weaker nonclaims, and + relation-bound claims with evidence boundaries; it is not a keyword ban. + +## Invariants + +- Catalog keys and embedded relation ids are identical and unique. +- Every bibliography and claim-surface reference resolves inside the catalog. +- A projection-required relation has both projection identity and revision in + every consumer binding. +- A finite or statistical evidence scope cannot carry a universal quantifier. +- Claimed and explicitly non-claimed relation ids cannot overlap on one + scientific completeness profile. +- Claim-bearing studies and benchmarks carry at least one resolved behavioral + or empirical claim binding. +- Every conformance report states its finite case boundary and explicit + equivalence/bisimulation nonclaims. +- Strong and weak hidden-action semantics remain distinct in executable + counterexamples. +- Catalog growth preserves concept-authority, schema-publication, package-corpus, + and SDL-lineage parity. + +## Consequences + +### Positive + +- Reviewers can tell structural validity, bounded conformance, intended + universal obligations, behavioral equivalence, and empirical adequacy apart. +- Participant, multi-agent, and study conclusions expose the assumptions that + make their relation meaningful. +- Future formal work can strengthen an assurance axis without changing relation + identity or inventing another claim vocabulary. +- High-confidence overclaims fail deterministically in repository policy. + +### Negative + +- Claim-bearing artifacts become more verbose because relation identity and + evidence boundaries are explicit. +- Adding a relation or strengthening assurance requires coordinated catalog, + schema, documentation, test, and consumer updates. +- Revision `rev1` deliberately makes several desired universal relations visible + as unproved rather than presenting existing probes as their substitute. + +### Limits + +This decision establishes a taxonomy, binding contract, and enforcement seam. +It does not prove backend trace inclusion, any simulation or refinement, +trace equivalence, strong or weak bisimulation, participant knowledge, +coalitional strategic ability, probabilistic behavior, timed behavior, +partial-order behavior, statistical equivalence, or empirical adequacy. Those +claims require their own governed models and evidence. diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 993700902..866cce0e3 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -129,6 +129,9 @@ adrs: - date: 2026-05-25 ref: 4b959c3 summary: "Corrected the control-plane API cross-reference from `aces_processor.control_plane_api` to `aces_runtime.control_plane_api`." + - date: 2026-07-13 + ref: "#417" + summary: "Moved generated network identity and incidental inspect output from authored Node.runtime to evidence while retaining deliberately required network state." - id: ADR-026 path: docs/decisions/adrs/adr-026-application-http-surface-inventory.md pin: 8818f870d79498f3322fd5edb9d2269948d04079ef5fbc7238811911ab7f5373 @@ -161,9 +164,17 @@ adrs: - id: ADR-033 path: docs/decisions/adrs/adr-033-scenario-delivery-boundary-for-runtime-node-state.md pin: da01715747264384779a9bb203f4081ca401edaf7a947ecc48d791a18c97b69b + amendments: + - date: 2026-07-13 + ref: "#417" + summary: "Refined participant-discoverable capture versus deliberate authored runtime contract promotion across the six carrier categories." - id: ADR-034 path: docs/decisions/adrs/adr-034-runtime-software-component-inventory.md pin: eb62ece791c5c0b442d5c77d89867832c01f0f5bc108b577f8636089be7c647e + amendments: + - date: 2026-07-13 + ref: "#417" + summary: "Clarified authored software components as required final state and moved scanner/SBOM/filesystem/process capture methods to evidence provenance." - id: ADR-035 path: docs/decisions/adrs/adr-035-service-manager-unit-state-runtime-surface.md pin: a8eb4df39f8b88781fee62ce83a2d2abee3951ebbeed5dc0c544a2a929ffad06 @@ -267,6 +278,10 @@ adrs: - id: ADR-056 path: docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md pin: 3aa602de53f7607cf531b4a9da3f58192eb87ad21ddd92846fd2ae2a7526a4c3 + amendments: + - date: 2026-07-13 + ref: "#417" + summary: "Narrowed observation-only health, generated network identity, and scanner facts out of authored Node.runtime while retaining shared redaction semantics." - id: ADR-057 path: docs/decisions/adrs/adr-057-runtime-secret-name-classifier-boundaries.md pin: 44c638bf47df8545b8af8a3850476fabc46a4ea42b3a8003b4f9ea1e89f72f49 @@ -343,3 +358,6 @@ adrs: - id: ADR-080 path: docs/decisions/adrs/adr-080-revision-pinned-sdl-lineage-and-provenance-ledger.md pin: e5b44525b5c2bfcef5516e3661c6d3d3a6cb816c10d4c4bb05d720554cea6663 + - id: ADR-081 + path: docs/decisions/adrs/adr-081-behavioral-relation-taxonomy-and-claim-discipline.md + pin: c8e096b85edb7ca97217a8bb0ac74b8817f966b9cf42b66f114dc3a45ed83a90 diff --git a/docs/decisions/issue-39-processor-models-package-split-preflight.md b/docs/decisions/issue-39-processor-models-package-split-preflight.md new file mode 100644 index 000000000..a015fe875 --- /dev/null +++ b/docs/decisions/issue-39-processor-models-package-split-preflight.md @@ -0,0 +1,94 @@ +# Issue 39 Processor Models Package Split Preflight + +Date: 2026-07-13 + +Issue: #39. + +Requirement: none. The GitHub issue is the implementation contract. + +This note records the guardrails for converting `aces_processor.models` from a +module to a package. It does not implement the split or change behavior. + +## Binding Decisions + +- ADR-015 enforces a 500-line cap, not the issue's older 600-line cap. Every + non-test Python file in the replacement package, including `__init__.py`, + must be at or below 500 lines once the allowlist entry is removed. +- ADR-036 keeps `aces_processor` as the compiled-model layer and requires + runtime callers to use the public `aces_processor.models` surface. Its + submodules are implementation detail, not a new public API. +- `aces_contracts` owns neutral runtime/backend DTOs. The current facade + deliberately re-exports several of them; preserve their identities rather + than copying, wrapping, or redefining them. +- Release-please owns `CHANGELOG.md`; despite the issue acceptance criterion, + this feature PR must not edit it or add a fragment. + +## Architecture Boundary + +`models/__init__.py` is the only public facade. It must retain every existing +external import from `aces_processor.models`, including contract re-exports +verified by `test_runtime_contract_boundaries.py`; no current external import +line is to move to a child module. + +Split by the existing dependency direction, not arbitrary line ranges: + +- compiled-resource records and workflow/resource records depend only on the + shared model foundation; +- participant action contracts and compiled observation/outcome declarations + own their current SEM-211/213/215 contract checks; +- participant result, attribution, outcome, and temporal value records own + their parsing and local invariant checks; +- the history-event record owns payload decoding; history validation remains + one ordered coordinator over visibility/authorization, grounding, temporal, + contract, and joint-action checks. + +Private helpers may move with the smallest domain that owns their invariant. +Do not create a new DTO, validation service, exception hierarchy, registry, or +plugin layer. Keep the two public history iterators as the single validation +coordination seam so a future validation concern is added once to the existing +ordered flow. + +The module currently has no `__all__`. Do not add one casually: it changes +star-import behavior. Preserve the current facade namespace intentionally, +including the shared-contract aliases used by callers, and compare its public +name set and exported-object identities with the pre-split module before +calling the refactor complete. Preserve public classes' and functions' +observable module/serialization behavior where compatibility requires it; +moving a definition changes `__module__`, repr and pickle metadata unless that +is handled deliberately. + +## Cross-Cutting Guardrails + +- **Schemas and validation:** retain `aces_contracts` DTOs, their validators, + `require_compiled_address`, and the existing participant semantic validators. + Do not duplicate address parsing, enum coercion, result-envelope checks, or + SEM-211/213/215 rules in new modules. +- **Errors and observability:** preserve `Diagnostic`/`Severity`, current + `ValueError`/`TypeError` conditions, iterator ordering, and diagnostic text. + This refactor adds no logging; it must not expose payloads, credentials, + environment values, or tracebacks. +- **Security and operating surface:** the split passes only Python import, + dataclass, contract-validation, and repository-policy layers. It must add no + auth, secret, environment, subprocess, network, filesystem, persistence, or + control-plane behavior; therefore no new OS-level token or error-envelope + exposure is permitted. +- **Repository policy:** retain processor-only dependencies on + `aces_backend_protocols`, `aces_contracts`, and `aces_sdl`; do not import + `aces_runtime`. Use package-relative internal imports to avoid facade + partial-initialization cycles. Remove only the allowlist entry for the + deleted module and do not alter policy code or its fixed reference set. + +## Verification And Anti-Patterns + +Treat package conversion as a simultaneous delete of `models.py` and creation +of `models/`; never leave both import candidates. Verify the exact external +import grep is unchanged, the facade re-exports its names, the shared DTOs are +object-identical, all new files meet the 500-line cap, and the canonical nox +`verify` session passes. + +Avoid direct external imports of child modules, circular imports through the +facade, a fat `__init__.py`, changing dataclass defaults/field order or helper +evaluation order, moving runtime DTO ownership back from `aces_contracts`, and +using this mechanical split to fix incidental semantic behavior. No schema, +runtime API, persistence, logging, CLI, compatibility-wrapper, or test-relaxing +change is in scope. diff --git a/docs/decisions/issue-417-runtime-contract-observation-boundary-preflight.md b/docs/decisions/issue-417-runtime-contract-observation-boundary-preflight.md new file mode 100644 index 000000000..2edbf2560 --- /dev/null +++ b/docs/decisions/issue-417-runtime-contract-observation-boundary-preflight.md @@ -0,0 +1,371 @@ +# Issue 417 Runtime Contract / Observation Boundary Preflight + +Date: 2026-07-13 + +Issue: #417. + +This note fixes the architecture boundary for the upcoming implementation. It +is guidance only: it does not change SDL syntax, models, schemas, fixtures, +compiler behavior, evidence contracts, or inventory tooling. + +## Binding Sources + +- `specs/sdl/document-model.md` makes normalized and instantiated SDL authored + scenario contracts. `Node.runtime` is inside those closed authoring forms. +- ADR-004 and ADR-036 define the SDL -> processor -> runtime -> backend package + and lifecycle direction. The compiler turns the complete node into desired + provisioning payloads; it is not an inventory importer. +- SEM-218 (`specs/formal/realization/explicitness-and-realization.md`) and + `aces_sdl.explicitness` distinguish exact, constrained, and open authoring + concerns and author/processor/backend realization origin. They do not + classify capture evidence or observation strength. +- ADR-064 and ADR-066 separate capture intent, raw captured evidence, derived + measures, operational observability, and scenario-native observability. +- ADR-065's `ExperimentRealizedFormDisclosureModel` is the archival carrier for + a backend/processor/operator choice made while realizing an underspecified + run concern. It is not a generic runtime inventory. +- `docs/aces/inventory/asset-inventory-methodology.md` owns the capture bundle, + `mapping-ledger.yaml`, `capture-limits.txt`, evidence checksum, and + correspondence-check workflow. Its maximal-capture inclusion rule does not + decide which captured facts become SDL requirements. +- ADR-056 and ADR-057 own observed-value redaction and secret-name handling. + The issue #516 inventory guidance separately permits authoritative source + bundles to retain participant-discoverable scenario-target secrets while + withholding operator/out-of-scenario material. +- ADR-009, ADR-061, `contracts/README.md`, and + `contracts/schema-publication-manifest.json` own schema authority and + evolution. Published schemas are hand-governed authority; generated-schema + tooling proves Python parity and does not replace that review direction. + +## Architectural Diagnosis + +The current carrier semantics and some model/documentation wording disagree. + +1. `Node.runtime` is accepted by `SDLModel`, included in the published + authoring and instantiated schemas, canonicalized as scenario meaning, and + serialized by `aces_processor.compiler._compile_node_runtimes()` into the + desired `NodeRuntime.spec`. Planning then compares that payload with a live + `RuntimeSnapshot`. A value authored there is therefore a scenario + requirement, regardless of whether its Python class or docstring says + "observed". +2. `runtime.health` currently carries health status and check logs; + `runtime.network` carries backend IDs and generated endpoint identity; and + `runtime.package_vulnerabilities` carries scanner, database, and scan-time + state. Those are capture results, yet the authoring carrier promotes them + into desired state. +3. `model_dump()` includes model defaults in compiled node payloads, while + `aces_sdl.explicitness` visits only `model_fields_set`. A serialized value is + therefore not proof that the author wrote it. A global `exclude_unset=True` + change would also be wrong because some defaults are normative SDL defaults. +4. SEM-218 currently classifies authored `unknown` and `other` enum sentinels + as open realization. That is useful for an authored open contract, but it + makes `runtime.health.status: unknown` especially misleading: it is neither + an observed unknown result nor evidence that a health check ran. +5. The inventory methodology correctly requires maximal capture, but its + current "attempt maximal ACES specification" wording can be read as an + instruction to copy every schema-compatible observation into SDL. Schema + compatibility is not semantic authority to promote evidence into a + requirement. + +The implementation must correct this at the carrier boundary. A compiler +denylist, backend filter, prose-only convention, or inventory-tool heuristic +would leave canonical SDL meaning and validation inconsistent. + +## Architecture Decisions And Guardrails + +### `Node.runtime` is declarative contract state + +Every field reachable from authored `Node.runtime` states one of these: + +- exact state the scenario requires; +- a typed constraint on acceptable state; or +- an explicitly open realization point admitted by SEM-218. + +It is not a place to preserve what one capture happened to observe. Presence in +authored SDL is the deliberate promotion decision; do not add a second generic +`origin`, `phase`, `observed`, or `is_requirement` flag to every runtime model. +Observation provenance is determined by its evidence carrier, not by a tag on +an SDL field. + +The six issue #160 categories remain distinct: + +| Category | Authority and meaning | Correct carrier | +| --- | --- | --- | +| authored | Explicit scenario declaration | SDL source/normalized/instantiated forms and `model_fields_set` | +| defaulted | SDL semantic default applied without explicit author text | phase contract plus explicitness/default provenance; never inferred from a dumped value | +| planned | Processor desired operation after compilation/reconciliation | `RuntimeModel`, typed plans, `PlannedResource` | +| realized | Value/structure selected by a backend, processor, or operator at an admitted open/constrained point | live `realization_provenance`; archival `ExperimentRealizedFormDisclosureModel`, with evidence refs when asserted | +| observed | Concrete fact read during one realization/capture | source evidence bundle, `mapping-ledger.yaml`, and `ExperimentEvidenceRecordModel` when published portably | +| derived | Interpretation computed from evidence | `ExperimentDerivedMeasureModel`, result/analysis/report carriers with source-evidence refs | + +Do not overload `ExplicitnessProvenance.BACKEND_REALIZED` as observation +strength: it says who selected a value, not whether Docker, a guest probe, or a +scanner independently observed it. + +### Container health reuses conditions and evidence contracts + +- A healthcheck definition is already a `Condition` (`command`, `interval`, + `timeout`, `retries`, `start_period`) bound through `Node.conditions`. +- Required healthy truth belongs in the existing + proposition/assertion/objective/evidence-requirement path when it affects + readiness or success. +- Check status, failing streak, timestamps, exit code, and output are observed + evidence. They must not remain an authored `runtime.health` observation bag. +- `unknown` means an authored open taxonomy point only where the owning + contract admits one. Capture absence or indeterminate health belongs in + evidence loss/limitations or `capture-limits.txt`, not `status: unknown` in + SDL. + +The regression example must show a condition bound to a container node and a +separate observed health result/evidence record. It must not encode the result +as desired runtime state. + +### Network contract and endpoint evidence stay separate + +- `infrastructure`, node links, and `Node.services` remain topology and service + authority. +- Runtime network values such as a specifically required hostname, alias, + static address, gateway, MAC, publication binding, or backend detail may + remain in SDL only when the author intentionally requires that exact or + constrained state. +- Docker/runtime network IDs, endpoint IDs, generated DNS names, generated + container identities, and observed driver/IPAM output belong in the capture + bundle and mapping ledger by default. A value is promoted to SDL only when + the scenario deliberately makes it contract state; its presence then carries + ordinary exact/constrained/open semantics. +- Stable ACES declaration IDs and backend-native/generated IDs remain different + concepts. Generated IDs must not become declaration keys, reference targets, + canonical addresses, or participant identity. +- A backend choice that materially realizes an open/constrained authored + concern may also receive a realized-form disclosure. Mere observation of an + incidental Docker ID does not justify one. + +Do not create a second network topology, Docker-specific SDL dialect, raw +inspect DTO, or backend-ID registry. + +### Software inventory and scanner state use different carriers + +- `runtime.packages`, `runtime.software_components`, and + `runtime.dependency_manifests` may describe required final scenario state at + the selected granularity. An exact captured package row becomes SDL only + through a deliberate requirement decision. +- Scanner identity/version/database, scan time, raw findings, and advisory + snapshot state are capture/analysis facts. The current + `RuntimePackageVulnerabilityFinding` shape is therefore not valid authored + runtime contract state. +- Raw SBOM, package-list, manifest, and scanner output belongs in the evidence + bundle or an `ExperimentEvidenceRecordModel` artifact reference/URI plus + checksum. Vulnerability interpretation and severity summaries belong in + derived measures or analysis. +- Top-level authored `vulnerabilities` remains the scenario weakness + declaration surface. A scanner finding is not automatically that authored + declaration. +- Audit mixed provenance enums such as + `RuntimeSoftwareComponentProvenance`: artifact/source origin may be contract + state, while scanner or process-inspection method is evidence provenance. + Do not preserve the mixture merely for compatibility and do not add a second + universal provenance model. + +### Defaults and compilation must preserve meaning + +- Use existing `model_fields_set`, `ExplicitnessRecord`, instantiation + provenance, and typed compiler/plan contracts. Do not infer authored intent + from `model_dump()` output. +- Distinguish a normative SDL default from an empty/sentinel serialization + default. Only the former may become desired state, and its provenance must + remain defaulted/processor-derived rather than author-declared. +- Do not globally switch node serialization to `exclude_unset=True`; that would + silently remove normative defaults across unrelated SDL surfaces. +- Observation-only fields should be unreachable from the authoring schema. + Do not maintain a matching compiler denylist or backend scrubber as duplicate + validation. +- The compiler and planner must continue consuming one closed, validated + instantiated scenario. Inventory evidence must never enter compilation as a + hidden side channel. + +### Existing evidence and provenance carriers are sufficient + +- `mapping-ledger.yaml` owns fact-level evidence paths, discovery vantage, + mapping disposition, caveats, and correspondence checks for inventory + capture. `capture-limits.txt` owns skipped/withheld/indeterminate capture. +- `ExperimentCaptureSpecModel` says what should be captured; + `ExperimentEvidenceRecordModel` says what was captured; and + `ExperimentDerivedMeasureModel` says what was inferred. Preserve this split. +- `ExperimentRealizedFormDisclosureModel` records realization choices, not a + full exact inventory. Its bounded summary cannot replace raw evidence. +- `RuntimeSnapshot` is live reconciliation/control state. Do not use + `RuntimeSnapshot.metadata`, `SnapshotEntry.payload`, `OperationStatus` + details, audit blobs, or logs as an archival capture ledger. +- No new evidence root, runtime-observation SDL tree, provenance graph, + exception hierarchy, persistence store, or inventory workflow is justified. + +## Required Incumbents + +- SDL ingress and phase validation: `parse_sdl()` / `parse_sdl_file()`, + `SDLModel`, `Scenario`, `InstantiatedScenario`, `instantiate_scenario()`, + `SemanticValidator`, `SDLParseError`, `SDLValidationError`, and + `SDLInstantiationError`. +- Author/default/realization semantics: `model_fields_set`, + `classify_scenario_explicitness()`, `ExplicitnessRecord`, + `derive_instantiated_explicitness()`, `CompiledRealizationRequirement`, and + `realization_disclosure()`. +- Compiler/planner/runtime contracts: `_compile_node_runtimes()`, + `RuntimeModel`, `NodeRuntime`, `PlannedResource`, `ProvisioningPlan`, + `RuntimeSnapshot`, `SnapshotEntry`, `RealizationProvenanceEntry`, + `ApplyResult`, and `OperationStatus`. +- Health truth/evaluation: `Condition`, node condition bindings, + `Proposition`, `Assertion`, `EvidenceRequirement`, condition compilation, and + proposition-truth result contracts. +- Evidence and provenance: `ExperimentCaptureSpecModel`, + `ExperimentEvidenceRecordModel`, `ExperimentDerivedMeasureModel`, + `ExperimentRealizedFormDisclosureModel`, run traceability, and the inventory + methodology's ledger/capture-limit/correspondence contracts. +- Observability classification: `ObservabilityEvidencePlane`, + `PLANE_BY_CONTRACT_ID`, `PLANE_BY_SDL_SECTION`, + `classify_contract_plane()`, and `assert_single_primary_plane()`. +- Schema authority: the published SDL and experiment schemas, + `ContractModel`, `schema_bundle()`, the SDL YAML and contract fixture corpora, + `contracts/schema-publication-manifest.json`, + `tools/check_generated_schemas.py`, `tools/check_schema_publication.py`, and + `tools/check_sdl_catalog_parity.py`. +- Diagnostics and API safety, if a portable carrier crosses HTTP: + `Diagnostic`, `Severity`, `RuntimeSnapshotEnvelopeModel`, + `OperationReceiptModel`, `OperationStatusModel`, + `ControlPlaneSecurityConfig.strict_defaults()`, request-size guards, + idempotency fingerprints, audit events, and the redacted FastAPI 500 + envelope. + +## Security And Whole-Path Gates + +- **YAML/parser shape:** source still passes bounded safe YAML decoding, + canonical-key handling, closed `SDLModel` construction, semantic validation, + instantiation, and full revalidation. Unknown observation bags fail closed; + no `source` shorthand or literal-key scope may be repurposed. +- **SDL/contract shape:** published SDL schemas, Python models, section/runtime + catalogs, canonicalization, instantiated schemas, and compiler admission must + agree. A docs-only distinction is insufficient while the schema accepts an + observation-only authored field. +- **Evidence shape:** portable capture artifacts pass closed `ContractModel` + validation, capture-spec/requirement binding, RFC 3339 checks, raw-content + artifact/URI/checksum rules, sensitivity/redaction/loss disclosure, unique + refs, run traceability, and observability-plane classification. The mapping + ledger separately passes its canonical APTL validator until ACES owns an + executable ledger schema. +- **Schema publication:** review the hand-governed schema change, keep Python + `schema_bundle()` in parity, add positive and negative fixtures, update the + publication manifest's hash/change ledger, and apply ADR-061 compatibility + rules. Accepted ADR corrections require ADR-059 supersession or recorded + amendment; never silently edit ADR-025/033/034/056. +- **Secret handling:** SDL, portable schemas, fixtures, diagnostics, logs, + audit details, summaries, and error envelopes must not expose operator + secrets, credentials, private keys, bearer tokens, raw environment dumps, + scanner payloads, or backend inspect objects. Authoritative inventory source + bundles may retain participant-discoverable scenario-target facts under the + issue #516 boundary; sanitized/public derivatives must declare redaction and + loss rather than replace the source bundle. +- **Authentication/authorization:** issue #417 needs no new route. If evidence + or snapshot data later crosses HTTP, reuse fail-closed bearer/proxy identity, + backend/operator/auditor role checks, target scoping, request-size limits, + idempotency, request fingerprints, and audit. Artifact dereference is a + separate authorized read from reading a summary/ref. +- **Config and environment binding:** add no environment variable, token, + credential, or config binding. Scanner and Docker configuration are capture + parameters/evidence metadata, not SDL config. Runtime values still pass their + existing field validators and ADR-056/057 redaction helpers where applicable. +- **OS/process exposure:** capture tools must not put credentials, raw evidence, + or sensitive host paths in process argv. Use bounded files/artifact refs, + fixed argument vectors, no shell interpolation, controlled paths, checksums, + and explicit capture limits. Generated network IDs are data, never command or + path authority. +- **Error envelopes:** parsing uses structured SDL errors; planning/runtime uses + addressed `Diagnostic` values; HTTP retains redacted internal errors. + Diagnostics name safe SDL paths, categories, and refs, not raw health output, + scanner JSON, Docker inspect payloads, secrets, or full tracebacks. +- **Persistence:** live state continues through `RuntimeSnapshot` and + `ControlPlaneStore`; archival capture continues through evidence artifacts + and run traceability. Do not duplicate either store or persist claim-bearing + observations only in `metadata` or logs. +- **Policy/workflow:** preserve package boundaries, compatibility wrappers, + concept authority, SDL lineage coverage, generated-schema parity, schema + publication governance, ADR pinning, scientific-completeness checks, and the + canonical nox verification graph. + +## Extensibility Boundary + +The seam is carrier plus capture granularity, not backend product name. + +- SDL contract state remains parameterized through existing variables, + explicitness classes, typed runtime fields, and canonical references. +- Evidence remains parameterized by requirement, source refs, capture window, + media type, artifact/checksum, sensitivity/redaction, provenance refs, and + ledger fact IDs. +- Realized-form disclosure remains parameterized by concern kind, realization + basis/authority, realized ref or bounded summary, and evidence refs. + +A future Podman/Kubernetes identifier, another health evaluator, a second SBOM +format, or another scanner should vary those existing parameters and evidence +media types. It must not require another SDL observation tree or a +Docker/Trivy-specific schema. A genuinely new portable runtime contract field +belongs on the smallest existing typed runtime family and remains a requirement +when authored. + +## Regression And Conformance Guardrails + +- Include a declarative container healthcheck/condition and separate observed + health evidence. Reject or migrate authored health result/log state. +- Include a Docker network endpoint capture containing generated network and + endpoint IDs, generated DNS identity, and MAC data in the evidence/ledger + side, while SDL contains only deliberately required topology/runtime facts. +- Include scanner metadata and vulnerability output as evidence and any + interpreted summary as derived analysis; ensure it is not accepted as + authored runtime state. +- Assert author/default distinction: an omitted model field must not become an + author-declared exact requirement merely because a compiled payload contains + its default. +- Assert `unknown` does not mean "capture could not determine this" in SDL. +- Exercise raw source, normalized authoring schema, Pydantic model, semantic + validator, instantiation, compiler/plan payload, generated-schema parity, + valid/invalid contract fixtures, and relevant inventory ledger validation. +- Keep hermetic tests independent of Docker and scanners. Native capture tests, + if any, remain opt-in; committed fixtures are bounded and secret-reviewed. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating every participant-discoverable fact as authored scenario intent; +- using `runtime.health.status: unknown` to mean no observation was available; +- copying Docker network/endpoint IDs, generated names, MACs, inspect maps, or + scanner timestamps into SDL because the current model accepts them; +- using a capture specification, backend capability, plan, success receipt, or + runtime snapshot payload as proof of observation; +- using realized-form disclosures as a generic exact inventory or putting + observations in `RuntimeSnapshot.metadata`; +- a compiler/backend denylist that duplicates the authoring schema boundary; +- a global `exclude_unset` serialization change; +- overloading SEM-218 explicitness/origin with observation strength; +- adding generic metadata/provenance/observation bags, duplicate validators, + exception trees, logging paths, persistence stores, schema registries, + reference resolvers, or inventory workflows; +- hand-editing only generated schemas, or regenerating schemas without the + hand-governed contract and publication-manifest review; +- silently rewriting accepted ADRs whose original decisions placed observed + facts under authored `Node.runtime`; +- leaking raw health output, scanner reports, environment/process dumps, + backend payloads, target/operator secrets, or stack traces through fixtures, + diagnostics, logs, API responses, or public evidence summaries. + +## Non-Goals + +- Implementing issue #417 or selecting exact field removals/version migrations + in this preflight. +- Building Docker/Compose/Podman/Kubernetes inspectors, health runners, SBOM or + vulnerability scanners, capture scheduling, retention, artifact storage, or + report generation. +- Creating a new observed-runtime SDL schema, evidence root, realization + provenance graph, API, persistence system, exception hierarchy, logging + stack, or workflow pipeline. +- Redesigning topology, services, propositions/assertions, experiment-core, + participant visibility, control-plane security, or inventory redaction. +- Claiming a mapping ledger or evidence bundle proves an authored SDL contract + has been realized; that remains correspondence/conformance work. diff --git a/docs/decisions/issue-539-realization-posture-cascade-preflight.md b/docs/decisions/issue-539-realization-posture-cascade-preflight.md new file mode 100644 index 000000000..1b72913d7 --- /dev/null +++ b/docs/decisions/issue-539-realization-posture-cascade-preflight.md @@ -0,0 +1,295 @@ +# Issue 539 realization-posture cascade preflight + +Status: implementation guardrail for issue #539; not an implementation plan or +a new semantic authority. + +## Decision summary + +Issue #539 extends the SEM-218 designation authority. It does not create a +second realization system. The author-facing cascade must lower into the +existing explicitness, realization-envelope, planner, and provenance contracts. + +The repository currently has three similarly named but distinct concepts. They +must remain distinct: + +- an author scope default: `closed`, `open`, or `unspecified` (delegated); +- a resolved realization-envelope closure: `closed-world` or `open-world`; +- an apparatus capability: `RealizationSupportMode` (`exact-only`, + `constrained`, or `open-realization`). + +`RealizationSupportMode` must not be reused as the authoring enum. Capability is +not intent, and `exact-only` is not closed-world. A resolved author default may +reuse the existing `Closure` vocabulary. Explicit leaf exact/constrained/open +intent continues to use the SEM-218 explicitness/posture vocabulary and wins +over every inherited scope default. + +## Authoring and resolution boundary + +There must be one scenario-root, typed, SEM-218-owned designation surface. It +may contain the scenario default and scoped override entries, but must not add a +generic realization field to every SDL model, a generic annotation bag, or a +second manifest family. `unspecified` is the one authored spelling for delegated +intent; do not add `delegated` as a second accepted alias. This is the narrowly +justified addition to the SDL surface reserved by SEM-218's staged designation +authority. + +The designation surface is authoring-phase machinery. It belongs on the +`Scenario`/expansion side; it must not be added to the common executable +`ScenarioContent` shape or survive as an unresolved executable field on +`InstantiatedScenario`. Typed designation records—including an explicit root +`unspecified` marker awaiting selected-apparatus context—travel in the existing +expansion/instantiation provenance aggregate and compiled carrier instead. +Preserve the disjoint phase contracts and phase equations established by +ADR-078. + +Scope targets need one canonical structured identity. Reuse the repository's +namespace-segment machinery and its existing RFC 6901 field-pointer convention +(`ResolvedImportProvenance.namespace`, `CapabilityConstraint.field_pointer`, +and language-edit pointers are incumbents). Do not expose the permissive dotted +`_PATH_TOKEN_RE` syntax as new author input, and do not introduce JSONPath, +wildcards, or a parallel grammar. Convert the canonical identity to structured +relation-engine tokens at one checked boundary; never recover scope by splitting +an arbitrary string on dots. + +The authored form and the resolved form are different phase contracts: + +- Explicit `unspecified` is unresolved author intent. It inherits a + concrete declaration from an outer scope; at the outermost unresolved scope + it is resolved only after the processor has a selected apparatus context. +- Resolved envelopes remain context-free set expressions. An unresolved value + must never enter `member()`, `subsumes()`, `witness()`, or negative-probe + generation. +- Omitting the designation surface entirely is not the same as explicitly + delegating at the root. Omission preserves the legacy effective closed-world + behavior. This distinction must survive Pydantic defaulting, module + expansion, instantiation, compilation, and serialization. + +Composition must qualify and rewrite scoped entries with the existing symbol +map, namespace tuple, and composition-provenance machinery before resolving the +cascade. A default at the root of an imported module governs only declarations +owned by that module namespace; it must not leak onto host or sibling-module +declarations. Import order is not specificity, and composition must not erase +the distinction between an omitted surface and an explicit `unspecified` entry. + +Resolution is deterministic semantic specificity, never input-list order: + +1. an explicit leaf exact/constrained/open declaration; +2. the most-specific concrete scoped default; +3. an inherited concrete outer default; +4. for an explicitly delegated root, the selected processor/apparatus default; +5. for an omitted surface, the legacy closed-world fallback. + +Conflicting declarations at equal path and scope are validation errors. +Identical duplicates should either normalize to one declaration or be rejected +consistently; they must not create order-dependent behavior. + +An inner open default under an outer closed default, and the inverse, are valid +cascade operations. This is distinct from widening an explicit envelope domain +binding. ADR-070 and `specs/formal/realization/envelope-semantics.md` currently +require `overrideable` for widening under a fixed parent; implementation must +clarify that the rule remains applicable to explicit domain bindings, not to +the new lexical closure cascade. + +The current envelope engine is not yet a correct cascade implementation: +`effective_constraints()` does not use `EnvelopeBinding.scope`, permissive path +tokenization can skip invalid characters, and open-world closure overlays do +not remove inherited closed-world state. These are canonical-engine gaps to +resolve, not reasons to add a second resolver. The canonical engine must parse +the full path or reject it; a tokenizer that silently discards unmatched text is +not a shape validator. + +## Meaning of open and closed + +Open author intent applies only to realization points admitted by the one +designation authority. It does not make arbitrary SDL keys valid, synthesize +undeclared topology, relax references or identities, or allow a backend to +invent secret-bearing configuration. `SDLModel(extra="forbid")` remains in +force in both modes. + +Closed means that an unspecified *admitted realization point* cannot be chosen +by the backend. It must not be implemented as "every optional Pydantic field +must be authored"; that would change the meaning of existing schemas and break +backward compatibility. Deterministic schema/processor defaults remain +processor-derived rather than backend-realized. + +Collection creation, opaque-artifact internals, and other absent values become +backend-realizable only when their owning semantic rule admits them. A broad +scenario-level open default cannot by itself turn every absent model field into +a realization slot. + +## Canonical incumbents to extend + +- `aces_sdl.explicitness`, `SemanticValidator._verify_explicitness()`, and + `classify_authoring_specificity()` own author specificity. Extend or compose + this authority; do not add a parallel classifier. +- `aces_sdl.phase_contracts.ExplicitnessProvenanceRecord` and the existing + expansion/instantiation provenance aggregates carry author/processor + derivation across phases. A scope default is not itself leaf explicitness. If + the existing leaf record cannot express canonical scope identity, governing + source, and effective closure without semantic overloading, add one closed, + typed sibling record inside the same provenance aggregate. Do not encode it in + `reason`, a private attribute, or a metadata sidecar. Extend the incumbent + uniqueness and composition-prefix validators with the same identity rules. +- `aces_contracts.realization_envelope` and + `aces_sdl.realization_envelope` own the set expression, membership, + subsumption, witnesses, and negative probes. Keep one relation engine. +- `aces_processor.semantics.realization.CompiledRealizationRequirement`, + `resolve_realization_concern()`, and `realization_support_diagnostics()` own + compiled realization demand and apparatus matching. Open requirements must + be carried and checked there; do not add a planner-only matcher. +- `aces_backend_protocols.backend_manifest.RealizationSupportDeclaration` and + its contract model remain apparatus capability declarations. A scoped open + request is accepted only when the selected domain explicitly declares + `OPEN_REALIZATION` and satisfies any finer offered-envelope constraint. +- The existing realization-envelope `subsumes()` relation is the fine-grained + requested/offered compatibility seam. The planner currently checks only a + concrete instance with `member()`; route a compiled author request through + the canonical relation API rather than encoding backend names or capability + exceptions in the SDL resolver. +- `aces_processor.semantics.realization.realization_disclosure()` and + `aces_contracts.runtime_state.RealizationProvenanceEntry` own disclosure. + Backend-filled open slots must be recorded as `backend-realized`, including a + stable governing-scope reference, without copying the realized value. +- `aces_runtime.backend_calls`, `RuntimeTarget`, and the snapshot contract own + the live backend boundary. Preserve fail-closed result validation and baseline + snapshot behavior on disclosure failure. +- `aces_runtime.control_plane_store` owns runtime snapshot persistence. Extend + the existing serializer and schema if the provenance shape changes; do not + add a posture ledger or store. + +The current compiler deliberately skips open explicitness records, the current +support gate does not reject unsupported open requests, and disclosure skips +paths without a plan-side baseline. All three stages must consume one compiled +carrier so planning and provenance cannot disagree about which slots were open. + +## Cross-cutting gates + +The design must pass every existing gate below. + +### SDL shape and semantic validation + +`parse_sdl()` remains the only YAML entry point. Its size, alias, tag, scalar, +key-normalization, JSON-domain, and closed-Pydantic checks remain unchanged. +The typed designation surface must then pass `SemanticValidator`, canonical +scope/path resolution, duplicate/conflict detection, and reference validation. +CLI and MCP authoring operations must continue to call this same parser and +must not interpret the cascade independently. + +If the public SDL shape changes, update the authoritative Pydantic contract, +`contracts/schemas/sdl/sdl-authoring-input-v1.json`, valid and invalid fixtures, +`specs/sdl/sections.md`, language metadata where applicable, and the schema +publication manifest/generator parity checks together. The JSON Schema remains +the hand-governed authority under ADR-009; update +`contracts/schema-publication-manifest.json` (`last_change` and digest inputs) +and pass `tools/check_generated_schemas.py` and +`tools/check_schema_publication.py` rather than treating a generated schema as +the source of truth. + +### Apparatus configuration and policy + +`BackendManifestV2Model`, `BackendManifest`, +`RealizationSupportDeclaration`, concept-family bindings, and realization- +envelope identity validation remain the apparatus shape gates. Delegation must +not be inferred from `realization_support`, an offered envelope, backend name, +or environment variable: those describe capability, not the apparatus's chosen +default policy. + +The planner must reject, with the existing structured diagnostic model, an open +request unsupported by the selected apparatus. There is no silent approximation +or fallback from open to constrained/exact. + +### Runtime, persistence, and error envelopes + +Backend `ApplyResult`, address-transition, snapshot, and realization-disclosure +validation remain fail closed. Extend the existing phase/snapshot DTOs and +`control_plane_store` serialization atomically if governing-scope provenance is +published. Preserve unknown-field rejection and schema/model differential tests. + +The current `control_plane_api_models._snapshot_model()` mapping omits +`realization_provenance` even though the runtime snapshot contract already owns +that field. Delivery of governing-scope disclosure must close this existing +mapping gap and prove contract -> store -> readback -> authenticated snapshot +API preservation. Do not claim provenance delivery based only on the in-memory +DTO or persistence serializer. + +Use the existing diagnostic and API error envelopes. Diagnostics may include a +canonical path, scope, domain, requirement kind, declaration reference, and +digest; they must not include raw authored or realized values. Runtime catch-all +handling must continue to expose only the generic internal-error response and +log exception type rather than payload contents. Do not introduce a new +exception hierarchy for cascade failures. + +### Authentication, secrets, and OS exposure + +Issue #539 requires no new endpoint. Any existing control-plane read or mutation +that exposes the extended snapshot remains behind the incumbent bearer/trusted- +proxy identity, role/target authorization, request-size, idempotency, and audit +gates. + +Open posture never bypasses `RuntimeEnvironmentVariable`, `ImageBuildArg`, or +`ImageEnvironmentDefault` redaction validation, evidence redaction checks, or +the rule that redacted/operator-secret values are omitted. Do not configure +posture or delegated defaults with secrets, environment variables, command-line +arguments, or backend-specific process flags. No token, author value, realized +value, or unredacted configuration may enter argv, logs, audit events, +diagnostics, fixtures, or provenance. + +## Extensibility seam + +Root delegation needs one injected resolver at the processor/planner boundary, +parameterized by the selected processor/apparatus compatibility context. Until +a typed apparatus default exists, its agreed fallback remains closed. EXP-721 +or a later apparatus-default contract must be able to supply this resolver +without changing SDL cascade resolution, the envelope algebra, or backend +implementations. + +The same canonical path/scope resolver must later admit opaque-artifact scopes +(issue #377) and new realization domains through registered semantic ownership, +not by editing a global switch or adding backend-condition branches. +The extension parameter is therefore the registered realization concern/domain +and selected-apparatus default resolver, not another posture enum or resolver +implementation. + +## Specification and verification guardrails + +The implementing change must reconcile and update, as one semantic change: + +- `specs/formal/realization/explicitness-and-realization.md`; +- `docs/explain/reference/explicitness-realization-semantics.md`; +- ADR-070 and `specs/formal/realization/envelope-semantics.md` for cascade versus + explicit-binding widening; +- the SEM-218 row in + `docs/explain/reference/shared-semantic-integrity.md`. + +The existing specification currently describes designation as staged while +other wording describes SEM-218 enforcement as complete. Do not leave that +coverage state internally contradictory after delivery. + +Required verification properties include both override directions, explicit +leaf precedence, explicit root delegation versus omission, module-namespace +isolation, equal-specificity conflicts, order independence, unsupported-open +planner rejection, successful backend-realized disclosure with governing scope, +redacted diagnostics, contract/store/authenticated-API round trips, and +schema/model differential coverage. Include dotted and escaped-key scope cases +so path canonicalization cannot pass only simple identifiers. These are +contract properties, not separate workflow logic. + +## Non-goals and prohibited shortcuts + +- No new manifest family, generic annotation/metadata bag, second registry, + constraint DSL, path grammar, relation engine, matcher, provenance sidecar, + persistence store, exception hierarchy, logger, or validation pipeline. +- No realization field mixed into every nested Pydantic model, unresolved + designation on the instantiated executable shape, or dotted-string scope + matching exposed as an author contract. +- No relaxation of `extra="forbid"`, identity/reference validation, semantic + invariants, secret redaction, authorization, or backend result validation. +- No inference that missing author input means open, or that advertised backend + support grants permission to realize an exact/closed request. +- No conversion of unspecified optional schema fields into realization points + without an owning semantic designation. +- No backend-name branching, process-environment posture switch, raw-value + diagnostics, or silent approximation. +- No compatibility logic in `implementations/python/src/aces`; owning packages + remain the only implementation authority. +- No new API or control-plane workflow is required by this issue. diff --git a/docs/decisions/issue-728-reproducible-related-work-comparison-preflight.md b/docs/decisions/issue-728-reproducible-related-work-comparison-preflight.md new file mode 100644 index 000000000..229491044 --- /dev/null +++ b/docs/decisions/issue-728-reproducible-related-work-comparison-preflight.md @@ -0,0 +1,338 @@ +# Issue 728 Reproducible Related-Work Comparison Preflight + +Date: 2026-07-13 + +Issue: #728. This is a requirement-free run; the issue title, body, and +acceptance criteria are the contract. + +This note fixes the architecture guardrails for rebuilding the related-work +comparison. It does not select the corpus, extract or score sources, execute +authoring tasks, rewrite the public comparison, or implement a checker. No new +ADR is needed: ADR-009, ADR-014, ADR-021, ADR-061, ADR-077, and ADR-080 already +govern authority, workflow, claim evidence, schema publication, artifact +identity, and source pinning. This note supersedes only the prose-only deferral +in the issue #508 preflight now that issue #728 explicitly requires a frozen +extraction matrix and reproduction bundle. + +## Decision Boundary + +The comparison is non-normative research synthesis. It may report what the +evidence demonstrates about ACES, but it must not define ACES semantics, change +SDL validity, or become a capability contract for external systems. + +Keep three independently revisioned concerns: + +1. **Protocol**: inclusion/exclusion rules, scope strata, system identities, the + twelve axes, task and negative-case definitions, rubrics, evidence rules, + analysis rules, and an amendment log. These are frozen before extraction. +2. **Extraction snapshot**: pinned sources and the observations, rationales, + limitations, and evidence locators for one assessed corpus. A source refresh + creates a new snapshot; it does not silently rewrite the protocol. +3. **Analysis and publication**: Pareto results, any sensitivity analyses, and + bounded public claims derived from an exact protocol and extraction + snapshot. Headline conclusions are outputs, never independently maintained + booleans or prose-only assertions. + +The machine-readable extraction surface belongs under the existing +`docs/research/related-work-comparison/` tree with its protocol and source log. +It is the single input to the reader-facing matrix. The public page at +`docs/explain/sdl/related-work-comparison.md` must be generated from it or +mechanically checked against it. `README.md`, `lineage.md`, and the changelog +must not carry parallel copies of the matrix. + +Do not publish the research shape as an ACES JSON Schema or add it to +`aces_contracts`. It is not an ecosystem interoperability contract. A future +external interchange format requires a separate authority and schema decision; +until then, one focused offline repository checker owns the closed research +shape and cross-record invariants. + +## Axis Boundaries + +The protocol must retain all twelve issue-required axes as distinct stable ids. +Subcriteria may be atomic, but one result must not stand in for another axis. + +| Axis | Boundary that prevents conflation | +| --- | --- | +| Expressive breadth | Representable concern/task coverage, not correctness, maturity, or quality. | +| Semantic precision | Defined meaning, reference rules, and ambiguity control, not parser strictness or feature count. | +| Formal analyzability | Explicit formal models, decidable properties, or executable analyses, not ordinary schema/semantic validation. | +| Concrete-syntax soundness | Source-profile definition, parsing, canonicalization, diagnostics, and rejecting negative cases, not semantic breadth. | +| Composition/versioning | Reuse, imports, identity, evolution, compatibility, and migration, not release cadence alone. | +| Experiment design | Tasks, factors, allocation, stochastic controls, measures, studies, and validity limits, not scenario objectives or workflow steps alone. | +| Participant modeling | Actions, observations, visibility, episodes, outcomes, and multi-participant interaction, not a generic actor/role field. | +| Provenance/evidence | Capture intent, evidence identity, lineage, integrity, redaction, and derivation, not the mere existence of logs or signatures. | +| Interoperability | A defined exchange or conformance boundary between independent implementations, not backend count or format export alone. | +| Usability | Predeclared authoring-task observations such as completion, effort, errors, diagnostics, and documentation support, not breadth by proxy. | +| Implementation maturity | Shipped executable behavior, releases, conformance evidence, maintenance, and independent use, not accepted designs or formal prose. | +| Governance/community | Standards/governance process, independent participation, adoption, and maintenance continuity, not repository popularity or implementation maturity. | + +Cyber DEM and Cyber FOM, CRACK and VSDL, and CRACK/KYPO/CyRIS must not share +composite score cells. Each assessed system or version has one stable +`system_id` and one declared scope stratum. Related artifacts may be linked, but +their evidence and outcomes remain independently attributable. + +## Measurement And Evidence Model + +The protocol, not a hard-coded table, defines the system, axis, and task sets. +The checker derives rectangular coverage from those ids so adding a system is a +data change and changing an axis meaning requires a protocol revision. + +For every system-axis cell, preserve separately: + +- applicability and the scope rationale; +- axis-specific observed result or measurement; +- contributing representative tasks and negative cases; +- scoring/rubric rationale; +- primary-source evidence refs with exact locators; +- extraction method and, where relevant, tool/version identity; +- assessor, independent-review or adjudication status, and preserved disagreement + for judgment-bearing results; +- limitations, missing evidence, and confidence; and +- the extraction snapshot and protocol revisions. + +Do not collapse applicability, observation, evidence strength, implementation +status, and quality into `yes`/`partial`/`no`. `not applicable`, `not observed`, +`not implemented`, and `not evaluated` have different meanings. Out-of-scope +cells are neither wins nor zeroes. A missing primary source is an evidence gap +that constrains or blocks the claim, not evidence of absence. + +Every positive, partial, negative, and out-of-scope cell needs primary-source +support and a reproducible rationale. An absence claim must record the pinned +source boundaries and search/extraction procedure that failed to find the +capability. A citation to a project home page is not a cell rationale. + +ACES implementation claims require both meaning and delivery evidence when the +axis concerns executable behavior. An accepted ADR or formal specification can +establish intended semantics but cannot by itself establish implementation. +Use the published schema/fixture corpus, production parser/validator/compiler +or conformance path, and focused tests as applicable. Preserve `partial`, +`missing`, and `deliberately-excluded` outcomes from the current scientific- +completeness delivery assessment instead of upgrading them from prose. + +Representative authoring tasks must declare the authored requirement, unit of +observation, applicability, permitted assistance, inputs, expected artifacts, +and success/partial/failure criteria before execution. Negative cases must name +the single injected defect and the expected rejection or diagnostic. For ACES, +run cases through the production `sdl-yaml/v1` source boundary, published +schema, `parse_sdl`/`parse_sdl_file`, and `SemanticValidator`; do not use a +comparison-only parser or acceptance shim. For a system with no applicable +concrete syntax or executable tool, use source extraction and record the task as +not applicable rather than treating non-execution as failure. + +Usability observations must also disclose author population, relevant prior +experience, assistance, repetitions, timing/error collection, and missing or +abandoned attempts. Maintainer-only or single-author task evidence is +exploratory and cannot support a generalized usability claim. Judgment-bearing +rubrics need an independent second extraction or an explicit adjudication and +disagreement record; agreement is evidence about scoring reliability, not proof +that the rubric measures the right construct. + +## Corpus And Source Freeze + +Pre-register included and excluded systems, the comparison unit, and scope +strata before recording outcomes. Cross-scope observations may be informative, +but only like-for-like, applicable measurements may support a comparative or +Pareto claim. + +Every source entry must have an immutable or drift-detectable identity: + +- Git source: repository, full 40-hex commit, exact artifact path, and license + disposition where bytes or code are retained; +- standard: maintaining body, exact edition/version/status, stable locator, and + section/page locator; +- publication: title, authors, year, venue, DOI or other stable identifier, and + exact cited location; and +- mutable official documentation: product/release context, retrieval date, + exact page/section, and content digest or an archived immutable locator. If it + cannot be frozen, record that limitation and do not use it for an unqualified + headline claim. + +The assessed ACES version is also a corpus member. Pin an already-existing +release or commit plus the relevant repository artifact paths; never record +`HEAD`, `dev`, a branch name, or "current". The containing Git commit freezes the +comparison bundle itself, avoiding an impossible self-referential commit hash. + +ADR-080 and `sdl-lineage-ledger-v1` remain the authority for ACES derivation, +lineage, compatibility, and third-party notice disposition. Do not add +comparison scores or tasks to that ledger. Where a comparison source already +has a lineage citation/source id, reference it or mechanically require the +shared immutable identity fields to agree; do not fork its DOI, version, or +commit. Comparison-only sources stay in the research bundle rather than being +added to lineage merely to obtain a source registry. + +Archive the extraction matrix and bounded evidence metadata, not unrestricted +copies of third-party standards, papers, repositories, or documentation. Keep +paraphrases and precise locators; retain source bytes only when licensing and +repository policy permit it. A digest detects drift but does not prove +authenticity or continued availability. + +## Analysis And Public Claims + +The primary result is the per-axis evidence surface plus scope-qualified Pareto +strengths. There is no default total score or total-order winner. + +Pareto analysis is valid only over declared, directionally comparable metrics +and a declared applicable system set. Nominal labels and out-of-scope cells must +not be coerced into ordinal numbers merely to calculate a frontier. + +Any aggregate or weighted analysis must be a separate, versioned analysis model +that declares metric normalization, direction, missing/out-of-scope handling, +weights, weight rationale, and the range of "reasonable" alternative weights +before results are examined. Preserve the sensitivity results. If reasonable +alternatives reverse a headline, disclose the reversal and narrow or withdraw +the headline; never select weights after extraction to restore a preferred +winner. Derived tables must be recomputable from observations and analysis +parameters rather than copied back into cells. + +Keep these claims explicitly distinct: + +- **broadest combined surface observed in this corpus**: a corpus-, version-, + task-, axis-, and measurement-qualified breadth statement; +- **highest quality**: prohibited unless a separate quality construct, valid + measures, evidence, and justified analysis model support it; and +- **standardized/mature**: claims about governance, implementation, adoption, + and release evidence, never inferred from breadth or semantic precision. + +Every public conclusion must reference the exact protocol, extraction snapshot, +analysis model, and supporting cell/observation ids. Public wording follows +`docs/explain/reference/documentation-style-guide.md`: uncertainty, reversals, +ACES gaps, and corpus limits are part of the claim, not footnotes that can be +dropped from summaries. The protocol must retain construct, corpus-selection, +source-availability, assessor, implementation-version, and external-validity +threats plus residual limits, following ADR-021's falsification-first boundary. + +## Canonical Incumbents And Reuse + +| Concern | Canonical incumbent and required boundary | +| --- | --- | +| Normative authority | ADR-009/019 and `specs/authority/authority-boundary.yaml`. `docs/` consumes authority; comparison data does not define SDL meaning. | +| Claim evidence | ADR-021's claim statement, threats, falsification protocol, pass/fail criteria, evidence artifacts, and evidence status. Do not create a second maturity-claim vocabulary. | +| Documentation and publication | `documentation-style-guide.md`, `canonical-reference-map.md`, the existing comparison page, research directory, docs index, and Sphinx build. | +| External source identity | ADR-080, `contracts/provenance/sdl-lineage-ledger-v1.json`, `tools/check_sdl_lineage.py`, and `docs/research/lineage/source-audit-2026-07-12.md`. Reuse identity rules without turning lineage into a scoring ledger. | +| ACES delivery truth | `specs/sdl/scientific-scenario-completeness.md`, its REV1 taxonomy and dated delivery assessment, `tools/check_scientific_scenario_completeness.py`, `limitations.md`, and `scenario-delivery-drift-audit.md`. | +| Concrete syntax | `sdl-yaml/v1`, `SDLParserLimits`, safe YAML loading, mapping-key analysis, closed `SDLModel` shapes, `parse_sdl`, `SemanticValidator`, `sdl-authoring-input-v1`, and example/schema negative controls. | +| Semantics and formal analysis | `specs/sdl/`, `specs/formal/`, semantic coverage policy, and the existing parser/instantiate/compiler/planner tests. Formal prose and executable analyzers remain distinguishable. | +| Composition and evolution | SDL module/import/lock semantics, ADR-053/075/076/078, `specs/evolution/`, and deprecation governance. | +| Experiment design | ADR-055/068/074, `experiment-task-v1`, `experiment-study-v1`, `ExperimentAnalysisPlanModel`, and experiment-core validity rules are methodological incumbents. Do not force a literature corpus into an ACES runtime study contract. | +| Participant modeling | ADR-013/020/022/054/067/069 and `specs/formal/participant-semantics/`. Generic roles, workflows, rewards, and episodes are not interchangeable. | +| Provenance and evidence | ADR-064/065/066/077, experiment evidence/run contracts, associated-artifact integrity rules, and observability/evidence-plane separation. A comparison bundle is not captured runtime evidence. | +| Interoperability and maturity | Published processor/backend manifests, backend/semantic profiles, realization envelopes, conformance fixtures/runners, and distribution-visible releases. Declared support and independently demonstrated conformance remain separate. | +| Diagnostics and workflow | `tools.policy.common.PolicyFailure`, `safe_repo_path`, `noxfile.py` `SessionReporter`, ADR-014, `tools/verify_all.py`, repo policy, contracts checks, and Sphinx. | + +There is no controller, API DTO, service, runtime repository, or persistence +store in this feature. Do not add one. `ControlPlaneStore`, runtime snapshots, +operation envelopes, and the experiment archive are not homes for literature- +comparison state. + +## Validation, Security, And Operational Layers + +1. **Research shape and path gate:** load only fixed, checked-in data as inert + content; reject duplicate/unknown ids and fields, unsafe paths, missing refs, + non-rectangular coverage, and unpinned source identities. Resolve every + repository path through `safe_repo_path` and apply bounded file/count limits. +2. **ACES source/shape gate:** ACES authoring cases pass the existing UTF-8, + source-size/token/graph limits, YAML 1.2 safe loader, duplicate/conflicting-key + checks, closed Pydantic model, published JSON Schema, semantic validation, + and post-instantiation validation where instantiation is claimed. The + comparison checker observes those results; it does not replace them. +3. **Contract/config gate:** add no runtime config, environment binding, schema, + profile, manifest, or package DTO. `tools/check_json_artifacts.py` remains + scoped to published contract artifacts; do not add comparison-specific + branches to it. A focused checker owns only research-bundle and publication- + drift invariants. +4. **Authentication/authorization gate:** no API, control-plane endpoint, live + repository credential, or authorization path is in scope. Source acquisition + happens outside offline verification. Do not add a weaker public endpoint for + comparison data. +5. **Secret and URI gate:** protocol, URLs, commands, task outputs, diagnostics, + and source metadata must not contain credentials, bearer tokens, private + keys, cookies, private prompts, environment dumps, URI userinfo, or secret- + bearing query parameters. Follow ADR-077's safe-locator rule and existing + gitleaks/private-key hygiene. Never place credentials in process argv. +6. **OS and supply-chain gate:** normal nox/CI checks perform no network fetch, + clone, package installation from compared projects, shell evaluation, or + third-party code execution. If executable external task evidence is gathered, + do it in a separately controlled, pinned, resource-bounded environment with + no repository secrets or privileged host access, then commit only bounded + results and provenance. +7. **Error-envelope and observability gate:** checker failures use bounded + `PolicyFailure` records with stable rule ids and system/axis/task locations, + reported once through `SessionReporter`. ACES task failures retain + `SDLParseError`, `SDLValidationError`, `SDLInstantiationError`, and existing + `Diagnostic`/`Severity` surfaces. Do not add a comparison exception hierarchy, + logger, telemetry path, raw payload dump, or traceback-as-output contract. +8. **Persistence and integrity gate:** Git-tracked protocol, extraction, and + derived artifacts are the durable record. Add no database, mutable cache, + runtime metadata, audit blob, or object store. Git and pinned source identity + establish reproducibility; checksums establish byte identity, not trust or + authenticity. + +The focused checker should be wired once into the canonical `noxfile.py` graph +through `SessionReporter`, alongside the existing lineage/evidence integrity +checks, rather than through a new CI workflow. Its tests should follow existing +mutation and non-vacuity patterns: a missing cell/evidence ref, mutable source, +composite system, suppressed weighting reversal, stale public summary, or +ACES implementation claim with no executable evidence must fail. + +## Extensibility Seam + +The stable join is: + +```text +(protocol_revision, extraction_snapshot, system_id, axis_id, task_or_case_id) +``` + +Derived claims additionally name `(analysis_model_id, analysis_revision, +weight_profile_id-or-none)`. System, source, task, and axis ids live in data, not +parallel Python constants or hand-authored table columns. Adding a system or +task extends the preregistered corpus; refreshing a source creates a new +extraction snapshot; changing an axis or rubric creates a protocol revision; +and adding alternate weights creates an analysis profile without rewriting the +observations. + +## Gotchas And Anti-Patterns + +Avoid: + +- composite system columns or silently borrowing one component's strength for + another component; +- feature counting, binary presence labels, or document section counts as a + proxy for semantics, usability, maturity, or quality; +- treating a formal specification as an executable analyzer, a parser rejection + as semantic precision, a timestamp as time semantics, a log as provenance, a + format export as interoperability, or repository activity as adoption; +- treating `not found` as `no`, `out of scope` as zero, missing evidence as + negative evidence, or inaccessible sources as reproducible evidence; +- using accepted ADRs, proposed syntax, roadmap issues, examples, or the dated + scientific-completeness taxonomy alone as proof of implemented behavior; +- counting ACES runtime inventory fields while ignoring missing authored syntax, + partial episode/time behavior, or deliberately excluded scoring semantics; +- post-hoc dimensions, task selection, exclusions, normalizations, weights, or + definitions of "reasonable" that favor the observed result; +- a stored winner flag, manually synchronized public table, hidden sensitivity + reversal, or Pareto calculation over incomparable or nominal values; +- copying the comparison into README/lineage, putting scores in the lineage + ledger, or registering the research matrix as a backend/semantic/profile + contract; +- a second source registry that disagrees with existing lineage identities, a + second validator/exception/logging stack, or a second CI workflow; +- live-link availability as a CI invariant, unpinned external execution in CI, + or committing third-party source copies without license review; and +- promotional claims such as "most comprehensive" or "highest quality" without + the exact bounded evidence and analysis the words require. + +## Non-Goals And Implementation Boundaries + +- Do not change SDL grammar, schemas, models, parser behavior, semantic + validation, compiler/planner/runtime behavior, backend conformance, or + experiment contracts to improve a comparison result. +- Do not implement missing syntax, time semantics, formal verification, + participant behavior, usability tooling, interoperability, or governance + maturity as part of this issue. +- Do not create a universal research-evidence schema, bibliographic service, + scoring service, API/UI, database, registry, source mirror, or external + execution platform. +- Do not claim compatibility, standardization, scientific validity, independent + adoption, implementation maturity, or quality beyond the pinned evidence. +- Do not require a total-order conclusion. A defensible outcome may be a set of + Pareto strengths, explicit evidence gaps, and no overall winner. diff --git a/docs/decisions/issue-747-behavioral-relation-taxonomy-preflight.md b/docs/decisions/issue-747-behavioral-relation-taxonomy-preflight.md new file mode 100644 index 000000000..bc514c161 --- /dev/null +++ b/docs/decisions/issue-747-behavioral-relation-taxonomy-preflight.md @@ -0,0 +1,355 @@ +# Issue 747 Behavioral-Relation Taxonomy Preflight + +Date: 2026-07-13 + +Issue: #747. + +Requirement: none. The issue title, body, and acceptance criteria are the +authoritative contract. + +This note fixes the architecture guardrails for the behavioral-relation +taxonomy and conflation audit. It does not publish the taxonomy, change a +schema or report, classify every repository statement, add a policy gate, or +claim a new proof. A dedicated ADR is required when the new shared concept +family is registered: ADR-012/062 governance deliberately rejects a concept +family with no owning ADR. Existing ADRs establish the carrier boundaries but +none currently owns the cross-repository behavioral-relation vocabulary. + +Classify the taxonomy, claim-binding constraints, and semantic coverage gate as +FM2 under `specs/formal/assurance-policy.yaml`: they constrain relations and +evidence across existing graphs and carriers. A later change that adds or +changes executable runtime transition semantics, an abstract state-machine +model, or model-checking behavior is FM3 and needs its own assurance artifacts; +the taxonomy must not smuggle that work into an FM2 catalog change. + +## Decision Boundary + +The taxonomy answers: **what relation or deliberately weaker predicate is a +claim about, over which carriers and observations, under which quantifiers, +and with what evidence or proof status?** It does not turn every claim into +behavioral equivalence. + +Validity, profile satisfaction, finite probe success, artifact identity, +realization-envelope membership, behavioral relations, epistemic relations, +strategic relations, and statistical claims are different kinds of claim. +Some are predicates or set relations rather than transition-system relations. +The catalog must say so explicitly instead of inventing transition labels for +a non-behavioral claim. + +ACES must have one machine-readable, revision-labelled claim/relation catalog +under `contracts/concept-authority/`, backed by one formal specification under +`specs/formal/behavioral-relations/`. Register `behavioral-relations` as a +concept family through the existing concept-authority governance. Reader prose, +reports, profiles, studies, tests, and policy checks reference stable relation +ids from that catalog; they must not keep private copies of the relation list. +The formal prose may explain the mathematics, but its tables and the catalog +must be generated from or mechanically checked against one another. + +Do not put this authority in backend profiles, scientific-completeness +profiles, semantic-invariant profiles, validation-strength profiles, +realization envelopes, or participant-runtime DTOs. Those are consumers with +different owners. Do not create a generic claim graph: when ADR-072's +validation-basis disclosure becomes executable, relation reference and +evidence boundary compose with that disclosure rather than replacing it. + +## Canonical Relation Record + +Every catalog entry needs stable identity and enough structure to prevent a +name from carrying several meanings: + +- catalog revision, stable relation id, display name, and class + (`predicate`, `set-relation`, `behavioral`, `epistemic`, `strategic`, or + `empirical`); +- left and right carrier/state spaces and, where applicable, initial states; +- action/transition labels, transition relation, and the declared partition + between observable, hidden, and stuttering actions, or an explicit + not-applicable rationale; +- observation/information-state projection, including its subject/audience, + policy revision, redaction scope, and treatment of order and simultaneity; +- direction, quantification over states, traces, schedulers, strategies, + environments, and observations, plus existential/universal choices; +- explicit treatment of nondeterminism, concurrency, probability, time, and + partial order, each recorded as supported, parameterized, abstracted, or + outside the relation's scope; +- the property legitimately preserved and the proof obligation that would + establish it; +- separately, the bounded evidence that current ACES tooling can produce; +- explicit non-claims and incompatible claim surfaces; +- assurance status with evidence references, distinguishing `defined`, + `implemented`, `tested`, `model-checked`, `proved`, + `deliberately-unproved`, and `future`; and +- references into a revision-pinned primary-source bibliography. + +Do not compress those assurance states into one maturity number. A relation +can be mathematically defined but not implemented, or tested on finite cases +without being proved. A proof status attaches to a named proposition, model +revision, assumptions, and proof artifact; it never attaches to a relation +name in the abstract. + +The catalog must distinguish at least structural validity, semantic validity, +capability declaration/profile satisfaction, bounded fixture/probe success, +canonical artifact or digest identity, realization-envelope membership and +subsumption, trace inclusion, trace equivalence, forward simulation, backward +simulation/data refinement, strong bisimulation, weak/observational +bisimulation, participant-relative observation equivalence, epistemic +indistinguishability, alternating/strategic equivalence, probabilistic +variants, statistical similarity/equivalence, and empirical adequacy. Digest +identity and realization subsumption belong in the catalog as explicit +non-behavioral controls because they are common sources of overclaim. + +The bibliography is part of this same revisioned authority. Use stable source +ids pinned to an exact paper version, proceedings/book edition, DOI, ISBN, or +equivalent immutable publication identity. Each relation cites source ids. +Do not create a second bibliography registry or rely on floating web pages. +Primary sources must cover Park/Milner bisimulation, the linear-time versus +branching-time spectrum, refinement/simulation, weak/observational +equivalence, epistemic models, and alternating/game-based equivalences. + +## Relation Decisions By Claim Surface + +| Surface | Relation ACES may name now | Evidence/status boundary | Prohibited promotion | +| --- | --- | --- | --- | +| SDL to instantiated or canonical artifact | A deterministic phase function plus phase-specific invariant preservation on admitted inputs. Canonical serialization/digest equality is artifact identity only. | Existing unit, fixture, round-trip, canonicalization, and property tests are finite/tested evidence. Universal preservation remains deliberately unproved unless a proposition and proof artifact are supplied. | Do not call phase success data refinement, simulation, trace equivalence, or bisimulation. | +| Abstract ACES runtime to a backend realization | The intended universal relation is one-way projected trace inclusion/soundness, parameterized by an explicit abstraction/projection: `Proj(Traces_backend) subseteq Traces_abstract`. | Current target conformance supplies bounded fixture/probe evidence only. Forward simulation may later discharge trace inclusion, but is not established by a successful run. | Do not infer backward simulation, completeness, trace equivalence, or bisimulation from provisioning, snapshot, episode, witness, or negative-probe success. | +| Backend to backend | Finite invariant agreement, matched named probes, or a separately defined statistical study relation over stated metrics/populations. | Existing cross-backend corpus evidence is a bounded comparison with explicit non-claims. | A shared result, digest, compiled address set, terminal record, or finite trace is not universal same behavior. | +| Participant-visible behavior | Participant-relative projected-history equivalence/indistinguishability parameterized by participant, observation-boundary policy and revision, redaction, visible ordering/simultaneity, and disclosed stochastic context. | Reuse the participant-runtime projection and `ParticipantObservationBoundary`; evidence can establish equality of selected projected histories. | Never compare global state as participant-visible state or infer future behavioral/strategic equivalence from one equal observation history. | +| Multi-agent interaction | Current ACES may claim structural validity and finite trace/capability coverage for recorded joint action, chance, simultaneous/parallel activity, and mean-field surfaces. | Strong/weak, epistemic, probabilistic, alternating, and strategic relations can be defined in the taxonomy but remain future/unproved unless their complete model and quantifiers exist. | Do not call two systems strategically equivalent without game states, players, legal joint actions, observation partitions, strategy class, coalition quantifiers, chance kernel, scheduler/fairness, and winning objective. | +| Independent adequacy study | A bounded empirical or statistical relation with named population, sampling frame, metric/estimand, equivalence or similarity criterion, uncertainty, and limitations. | Reuse experiment task/run/study/evidence/derived-measure and falsification-status contracts. Evidence classification references the catalog relation id. | Statistical equivalence/non-inferiority is not behavioral equivalence; repeated observations cannot be promoted to a universal proof. | + +The abstract-runtime row does not retroactively turn the existing realization +envelope into behavioral refinement. Envelope membership, subsumption, +witnesses, and negative probes are set-theoretic realization-support relations. +The participant row also does not create a new projection: the formal +participant runtime already defines `O_(p, projection, t)` and projected +history indistinguishability. The taxonomy names and constrains that seam. + +At present ACES should formalize the claim distinctions above, participant +projection equivalence, and the intended projected-trace-inclusion obligation. +Full trace equivalence, forward/backward simulation proofs, data refinement, +strong/weak bisimulation proofs, timed or partial-order bisimulation, +probabilistic bisimulation, and alternating/strategic equivalence are future +work. They are inappropriate on current conformance, completeness, replay, +and backend-comparison surfaces unless a later artifact establishes their +models and proof obligations. + +## Required Counterexamples And Examples + +The normative specification must make the quantifier boundary concrete: + +- A finite-probe counterexample needs two labelled transition systems that + share the tested successful trace while one has another enabled transition + the other cannot match. Both implementations pass the finite probe; they are + not bisimilar. Extend the same example or add a game variant where an + unprobed coalition has a winning choice in only one structure, showing why + probe agreement is not strategic equivalence. +- A hidden-action example needs an abstract visible action and a backend path + with a declared hidden `tau` step before or after it. Strong matching fails + on the unmatched hidden step; weak matching may succeed through `tau` closure + only under the declared hiding projection. Divergence sensitivity, stability, + and termination treatment must be explicit, not silently assumed. + +These examples explain definitions; they are not evidence that ACES backends +satisfy the relations. + +## Existing Cross-Cutting Contracts To Reuse + +| Concern | Canonical incumbent and boundary | +| --- | --- | +| Normative authority and packaging | ADR-009/012/019/061/062, `specs/authority/authority-boundary.yaml`, `contracts/concept-authority/`, `aces_contracts.corpus.CONCEPT_AUTHORITY`, `corpus_family_root()`, `ContractModel(extra="forbid")`, `schema_bundle()`, `contracts/schema-publication-manifest.json`, contract fixtures, and generated-corpus parity. Add one catalog/model/loader path, not a second registry. | +| Assurance and claim strength | ADR-021's `untested`/`partial`/`demonstrated`/`refuted` falsification statuses; ADR-072's validation-strength and validation-basis disclosure; `specs/formal/assurance-policy.yaml`. Relation identity, validation strength, evidence status, and proof status remain independent axes. | +| SDL lifecycle | ADR-016/078, `load_sdl_yaml`/`parse_sdl`, `SDLModel`, `SemanticValidator`, `instantiate_scenario()`, canonical snapshots and phase contracts, and `docs/explain/reference/shared-semantic-integrity.md`. The taxonomy classifies their claims and does not become another parser or semantic validator. | +| Realization and refinement language | ADR-070, `specs/formal/realization/`, `aces_contracts.realization_envelope`, and `aces_sdl.realization_envelope`. Preserve membership/subsumption/witness semantics and classify them as non-behavioral. | +| Participant observation and information flow | ADR-022/054/067, `specs/formal/participant-semantics/`, `specs/formal/participant-runtime/`, `ParticipantObservationBoundary`, `ParticipantViewRule`, projected histories, observation kernels, joint action, chance, simultaneous moves, and mean-field records. Reuse the existing projection rather than adding a global-state view or duplicate DTO. | +| Conformance | Existing `ConformanceCaseResult`, `BackendConformanceReport`, `run_fixture_suite()`, `run_target_conformance()`, CLI `_report_payload()`, backend profile loader/path confinement, `schema_bundle()`, `Diagnostic`, and committed reports under `docs/conformance/`. Enrich the existing disclosure; do not add a parallel equivalence report. | +| Scientific completeness | `contracts/profiles/scientific-completeness/`, `specs/sdl/scientific-scenario-completeness.md`, and `tools/check_scientific_scenario_completeness.py`. Every REV1 claim references the new catalog; the existing phrase coverage must not become an independent relation list. | +| Studies and empirical evidence | ADR-055/064/065/066/068/074 and existing experiment authoring, task, run, study, allocation, apparatus, evidence, traceability, derived-measure, uncertainty, missingness, multiplicity, and realized-form contracts. Issue #729 must bind relation/evidence classification here, not create a study store or claim graph. | +| Errors and observability | Existing SDL parse/validation/instantiation errors, collect-all semantic diagnostics, `Diagnostic`/`Severity`, conformance report diagnostics, `tools.policy.common.PolicyFailure`, and `SessionReporter`. No new exception hierarchy, logger, audit channel, or telemetry stack is warranted. | +| Persistence | Static taxonomy in the checked-in and packaged contract corpus; current conformance report carriers for conformance; experiment-core records for archival studies; `RuntimeSnapshot`/`ControlPlaneStore` for live state. Relation classification does not justify a database, cache, audit blob, or runtime metadata bag. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, ADR-014, the canonical `noxfile.py` policy/contracts/docs/verify graph, `tools/verify_all.py`, repository policy, requirement governance, schema publication, generated-schema, JSON artifact, concept-authority, assurance, semantic-coverage, and docs checks. Wire a focused checker once into this graph. | + +Compatibility package `implementations/python/src/aces` remains logic-free. +Owning packages must keep dependency direction and must not import through the +`aces.*` compatibility namespace. + +## Conflation Audit And Semantic Gate + +The initial audit is a human semantic classification, not a search-and-replace +exercise. It covers ACES-authored normative specifications, ADRs, design and +explanatory documentation, public API/CLI text, conformance reports, backend +and scientific profiles, runtime probes, examples, tests, and academic claim +prose. Vendored or archived third-party source text is not an ACES claim and +must not be rewritten merely because it contains a keyword. + +Known high-risk incumbents that require explicit classification include: + +- `specs/formal/participant-runtime/README.md` currently describes an + implementation as refining the design when concrete traces project to valid + abstract traces. That sentence must either name projected trace inclusion + with direction and quantification or use weaker language; it does not define + simulation or data refinement as written. +- `_cross_backend_corpus_backend_runs.py` uses + `behavior-history-equivalent` for a terminal participant observation. That + needs an explicit participant projection and finite-history relation or a + weaker recorded-observation statement. +- cross-backend digests, compiled address equality, and evidence-surface + presence are finite invariant agreement only. Preserve the ledger's existing + non-claim that it is not an equivalence proof. +- realization-envelope witnesses and negative probes, conformance fixtures and + injected-adapter probes, replay-support claims, serializability, statistical + equivalence, simulation backends, and cyber observables all use potentially + overloaded words in legitimate domain-specific senses. The gate must + recognize their qualifiers rather than ban the words. + +The machine gate should consume relation ids from the canonical catalog and +use `tools.policy.common.PolicyFailure`, `safe_repo_path`, exception handling, +and JSON output conventions. It should detect high-risk affirmative claim +patterns and structured claim fields, then accept either: + +1. a precise catalog relation reference plus subject, projection/quantifier + where applicable, evidence boundary, assurance status, and non-claims; or +2. deliberately weaker language that states the bounded observation and says + why no equivalence relation is established. + +For prose, keep the relation reference and boundary in the same paragraph or a +stable claim block. For structured reports/profiles, use typed fields rather +than searching prose. If a reviewed audit inventory is retained, key entries +by repository path plus stable statement/anchor identity, not mutable line +numbers. Exceptions must be narrow, reasoned, path-confined, and governed by +the existing policy exception mechanism. Do not hard-code a second relation +vocabulary in the checker, reduce the audit to keyword banning, or allow a +negation token anywhere on a line to suppress an unrelated positive claim. + +Conformance output in particular must say the subject/profile, exact fixtures +and probe ids, whether execution used fixtures, an injected adapter, a live +daemon, or a guest, the finite quantification, relation/evidence class, +projection if any, limitations, and explicit non-claims. The existing `passed` +boolean remains an aggregation of named cases; it is never a universal +behavioral-equivalence result. Reuse and extend `BackendConformanceReport` and +its CLI serialization rather than creating a new output envelope. + +## Security And Operational Layers + +This work is static authority, validation, and bounded reporting. The layers it +passes through, and those it deliberately does not, are: + +1. **Repository path and data ingress:** load only fixed catalog, schema, + profile, report, and source paths as inert data through + `aces_contracts.corpus` and existing JSON/model loaders. Any checker-owned + evidence or audit path passes `safe_repo_path`; reject absolute paths, + `..`, symlink escape, duplicate identities, dynamic import/evaluation, and + network fetches. +2. **Contract/config shape:** the catalog and any new structured relation + reference use closed `ContractModel` shapes, generated published schemas, + `x-aces-invariants` where JSON Schema cannot express the rule, valid/invalid + fixtures, the schema-publication manifest/hash/change ledger, generated + bundle parity, JSON artifact checks, and concept-authority governance. Do + not store relation ids in free-form metadata. +3. **SDL and experiment validators:** if an audited claim is carried by SDL, + it still passes safe YAML/source limits, duplicate-key checks, + `SDLModel(extra="forbid")`, semantic validation, instantiation and + post-instantiation validation. Experiment claims still pass their existing + contract and cross-artifact validators. Relation classification observes + those results and does not bypass or duplicate them. +4. **Backend/profile validation:** conformance still uses the closed backend + profile id grammar, root confinement, manifest/capability checks, schema + bundle, semantic checks, and bounded target cases. A relation reference + cannot turn unsupported gaps or not-run probes into passes. +5. **Authentication and authorization:** no new endpoint or auth decision is in + scope. If a later API exposes relation evaluation, it must reuse + `ControlPlaneSecurityConfig.strict_defaults()`, verified identity, + target-bound role authorization, request-size limits, idempotency, + fingerprints, and audit events; no taxonomy-specific weaker endpoint. +6. **Secrets and information flow:** catalogs, reports, diagnostics, logs, + examples, and studies carry safe ids, refs, digests, counts, and bounded + summaries only. Never copy tokens, keys, credentials, prompts, hidden truth, + redacted participant state, raw evidence, backend object representations, + environment dumps, or rejected payloads into a relation disclosure. + Participant-relative claims use the existing information-flow projection. +7. **Environment and OS/process exposure:** no environment binding, daemon, + shell evaluation, network service, or new subprocess is needed. Policy runs + use fixed argv in the nox environment. Do not put claim prose, untrusted + paths, evidence content, credentials, or tokens in process argv, and do not + use `shell=True`. +8. **Error envelopes and observability:** malformed contract content fails via + bounded Pydantic/contract diagnostics; conformance uses `Diagnostic` and + must retain the existing sanitized load-error behavior; policy drift uses + `PolicyFailure`; CLI JSON keeps the existing report envelope. Do not echo + rejected documents, backend exception representations, raw values, full + tracebacks, or hidden observations. `SessionReporter` is the workflow + observability seam; no new logger is needed. +9. **Persistence and distribution:** the versioned catalog is checked-in + contract authority and packaged by the existing corpus. Conformance and + study classifications remain in their existing report/experiment carriers. + Do not persist claims in snapshots, audit blobs, tags, caches, or a new + service. + +## Extensibility Seam + +The reusable key is `(taxonomy_id, taxonomy_revision, relation_id)`. A claim +binding adds subject/left and right carrier refs, projection ref and revision, +quantifier/evidence boundary, assurance status, evidence/proof refs, and +limitations. Relation semantics change by publishing a new taxonomy revision; +adding a new relation adds a catalog entry; adding a new consumer adds a claim +binding without modifying the catalog's meaning. + +Probability, time, partial order, scheduler/fairness, strategy class, +coalitions, chance kernels, simultaneous moves, mean-field aggregation, and +observation policies are explicit parameters or declared unsupported +dimensions. They are not booleans hidden in a generic `equivalent` relation. +This seam permits later probabilistic, timed, partial-order, or alternating +relations without re-editing every report schema or redefining existing ids. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating schema/semantic validity, capability declaration, profile + satisfaction, admission, deployability, or realization support as behavior; +- turning finite fixtures, probes, snapshots, traces, outcomes, digests, + canonical JSON, or terminal observations into a universal quantifier; +- calling a projection a refinement without direction, retrieve/abstraction + relation, quantification, and preservation obligation; +- hiding all backend-internal work as `tau` without a governed projection, or + omitting divergence, fairness, quiescence, failure, and termination choices; +- treating trace equality as branching equivalence, participant observation + equality as epistemic or strategic equivalence, or global state as visible; +- treating repeated runs, deterministic seeds, replay-support provenance, or + reproduction of one result as behavioral equivalence or reproducibility of + every execution; +- treating a statistical equivalence margin as bisimulation or empirical + adequacy as proof of implementation conformance; +- one overloaded `relation`, `profile`, `conformance`, `equivalent`, + `refinement`, or `status` string with no namespace/revision; +- duplicating the participant projection, transition system, concept catalog, + schema registry, validation-basis disclosure, evidence graph, report DTO, + exception hierarchy, logger, persistence store, policy exception format, or + CI workflow; +- editing generated schemas directly, loading taxonomy files by ad hoc + repository-relative paths, or implementing logic in the compatibility + package; and +- a regex gate that bans terms, blesses any nearby negation, scans vendored + literature as ACES claims, or forces mathematically false boilerplate. + +## Non-Goals And Implementation Boundaries + +- Do not prove that any current backend implements the abstract runtime, that + two backends are bisimilar, or that any coalition has the same strategy. +- Do not add a model checker, theorem prover, strategy engine, scheduler, + probability engine, game solver, replay engine, or statistical analysis + engine. +- Do not change SDL grammar, canonicalization, participant runtime behavior, + realization-envelope semantics, backend behavior, or experiment semantics + merely to name their claim boundaries. +- Do not make behavioral equivalence a prerequisite for structural/semantic + validity, profile declaration, ordinary conformance, or REV1 matrix + construction. Those surfaces must instead disclose the weaker relation they + establish. +- Do not backfill a generic relation field into every artifact. Add a reference + only to governed claim surfaces, and compose with ADR-072 disclosure when it + becomes available. +- Do not certify external scientific adequacy, backend completeness, + participant knowledge, replay reproducibility, or universal information-flow + security. The taxonomy states definitions, evidence boundaries, status, and + deliberate non-claims. +- Do not rewrite quoted primary-source terminology or archived third-party + research text. Audit ACES's own use and contextualize citations. diff --git a/docs/decisions/issue-748-node-services-reachability-preflight.md b/docs/decisions/issue-748-node-services-reachability-preflight.md new file mode 100644 index 000000000..7b891baa4 --- /dev/null +++ b/docs/decisions/issue-748-node-services-reachability-preflight.md @@ -0,0 +1,205 @@ +# Issue 748 Node Services Reachability Preflight + +Date: 2026-07-13 + +Issue: #748. Requirement: none; the issue is the delivery contract. + +This note records the repository-wide architecture boundary for authored node +services, traffic authorization, and backend realization. It does not implement +the SDL, processor, backend, or conformance changes. + +## Decision + +`Node.services[]` is the positive authored or curated identity of a node-local +transport binding: optional service name, container/node-side port, and +transport protocol. A declaration can be a realization concern for a backend +that supports or synthesizes services, but it is not by itself: + +- permission for any source to reach the port; +- a bind address, listener-scope observation, or proof that a daemon is live; +- a host-published port, NAT rule, image `EXPOSE` declaration, or firewall rule; +- an `internal` / `external` audience classification. + +Consequently, a backend MUST NOT derive ingress, host publication, or an ACL +allow rule merely because a service is declared. Traffic authorization must be +authored through the existing `infrastructure.*.acls` policy surface, or a +future separately governed traffic-policy surface. The current ACL model does +not yet specify a portable default action, rule-order calculus, or end-to-end +reachability proof, so this issue must not claim that an ACL list alone proves +identical reachability across backends. The portable invariant for this issue +is narrower and fail-closed: a service declaration never grants reachability. + +Do not add `ServicePort.role`. In the current canonical implementation, +`SDLModel(extra="forbid")` and both published SDL schemas reject that key; it is +not silently dropped. A free-form `role: internal` would conflate transport +identity, topology, participant or login roles, protocol-local roles, and +traffic authorization while still failing to identify an allowed source. If a +later requirement needs audience policy, it belongs on the authorization +surface as a typed source selector with defined evaluation and defaults, not as +a descriptive service label. + +## Existing Authority And Incumbents + +- ADR-025 keeps container-side `Node.services`, image exposed ports, and + `runtime.network.published_ports` distinct. ADR-033 keeps transport bindings, + runtime state, delivery mechanics, and evidence distinct. ADR-043 keeps + authored service identity, observed `runtime.service_listeners`, and host + publication distinct. These decisions already own the semantic boundary; a + new ADR or competing endpoint schema is not justified. +- `aces_sdl.nodes.ServicePort`, `Node.validate_unique_service_ports()`, the + shared `parse_int_or_var()` helper, `SDLModel`, `parse_sdl()`, + `SemanticValidator`, and `instantiate_scenario()` are the canonical + authoring, shape, semantic, and concrete-revalidation path. +- `aces_processor.compiler._compile_node_runtimes()` already carries the full + node and matching infrastructure declarations in the existing node resource + payload. Named services already participate in canonical aliases. Do not add + a second service resource, DTO at the compiler boundary, or parallel plan. +- `RealizationConcern.SERVICE`, `RealizationConcern.ACL`, + `ConcernDisposition`, `ObservationStrength`, and + `TransformationKind.SERVICE_SYNTHESIS` already provide the backend claim + vocabulary. `ProvisionerCapabilities.supports_acls`, + `RealizationSupportDeclaration`, `BackendRealizationEnvelopeModel`, and + `backend_manifest_payload()` are the existing capability/configuration + surfaces. Do not add a standalone `supports_services` dialect. +- `aces_reference_backend.realization`, its existing `NetworkSpec` / + `ContainerSpec` driver boundary, `ReferenceProvisioner`, and + `DeploymentDriver` are the reference-backend seams. The libvirt + `ServiceSpec` and fail-closed ACL translator are useful behavioral precedent, + but they are backend-owned types and must not be imported to create a + cross-backend dependency. +- `Diagnostic`, `ApplyResult`, `OperationReceipt`, `OperationStatus`, and + `RuntimeSnapshot` are the error, result, and observation envelopes. Snapshot + payload preservation proves carriage only; it is not evidence that a service + listened or an ACL was enforced. + +Two current gaps must not be hidden by the change. The reference manifest says +`supports_acls=True` although neither reference interpreter nor driver realizes +ACLs. That claim must either become false or be backed by real fail-closed +enforcement. Also, the planner's coarse ACL capability check currently inspects +network resources only, while `InfraNode.acls` and the libvirt backend support +node-attached ACLs. The canonical capability gate must cover every materialized +infrastructure entry rather than duplicating location-specific checks. + +## Cross-Cutting Gates + +- **Source and parser gate:** retain `_source_validation.py` UTF-8, byte, + scalar, alias, depth, node, tag, and directive limits. Unknown service fields + continue through `SDLParseError` and bounded, source-anchored + `sdl.model.invalid` diagnostics; do not add a permissive YAML preprocessor or + silently delete `role`. +- **SDL model and semantic gate:** retain closed-world Pydantic models, shared + port bounds, protocol-plus-port and name uniqueness, ACL action/port parsing, + and `SemanticValidator` network-reference checks. Authoring errors remain + `SDLParseError` or `SDLValidationError`; do not add duplicate validators or a + service exception hierarchy. Description text must never be parsed as + policy. +- **Instantiation and contract gate:** concrete scenarios revalidate through + `instantiate_scenario()`. The hand-governed schemas under + `contracts/schemas/` remain normative; `schema_bundle()` is the reference + implementation's compatibility proof, not authority to change them. Any + future public model change must update the Python SDL model and every affected + authoring/instantiated schema together, record the change in + `contracts/schema-publication-manifest.json`, and keep + `tools/check_generated_schemas.py` passing. Never update only the generator, + one published phase, or one copy of `ServicePort`. +- **Compiler and planner gate:** use the existing node resource payload and + canonical addresses. A required or claimed service/ACL runtime effect that a + selected backend cannot honor must fail before side effects through the + existing realization-support / capability `Diagnostic` path. Descriptor-only + carriage may succeed only when the selected backend configuration discloses + that disposition and claims no runtime effect. Merely echoing the authored + payload into a snapshot must not satisfy realization or non-approximation + evidence. +- **Manifest and configuration gate:** machine-readable claims remain bound to + the selected backend configuration and rendered by + `backend_manifest_payload()`. If a realization envelope is published, its + existing configuration and envelope digests plus complete service/ACL concern + disclosures are authoritative. Do not create an environment-variable flag, + unvalidated config dictionary, or backend-local capability vocabulary. +- **Interpreter and error-envelope gate:** reference interpretation stays pure + and driver-neutral, shape-checks plan mappings, and returns stable redacted + `Diagnostic` values. `aces_runtime.backend_calls` remains the exception and + return-contract boundary. Driver failures preserve the baseline snapshot and + use `ApplyResult`; do not expose native exceptions, tracebacks, runtime ids, + or raw rule payloads. +- **Host / OS gate:** the OCI driver retains its closed Docker/Podman runtime + allowlist, fixed argv lists, bounded timeout, image trust policy, private + stdout/stderr handling, ownership labels, and rollback. `Node.services` must + never produce `--publish` / `-p` arguments. Firewall or namespace mutation is + privileged host behavior and is outside this issue; a future implementation + would need an explicit, ownership-checked, reversible driver mechanism rather + than shell fragments or host-global rules assembled from free-form input. +- **HTTP, auth, and secret gate:** this change needs no new API, credential, + secret, environment binding, or process argument. If plans traverse the + remote control plane, reuse `ControlPlaneSecurityConfig.strict_defaults()`, + request-size guards, identity and role authorization, target binding, + idempotency, audit, and the redacted FastAPI exception handler unchanged. +- **Persistence and observability gate:** use the existing control-plane store, + runtime snapshot, operation status, realization provenance, and evidence + surfaces. Do not add a repository, migration, sidecar ledger, logger, or raw + backend-output field. Structured diagnostics and observation strength are the + observability surface. + +## Conformance Boundary + +Conformance must distinguish four claims instead of treating them as one: + +1. parsing and compilation preserve a service declaration; +2. a backend interpreter extracts or explicitly rejects it; +3. a driver realizes the declared listener or ACL effect; +4. an independent daemon/guest/network probe observes the claimed effect. + +A snapshot that repeats the provisioning payload proves only the first claim. +Service and ACL remain separate realization concerns, so a service-positive +probe cannot certify traffic authorization and an ACL-positive probe cannot +certify that a daemon is listening. Any backend claim of reachability needs a +positive authorized path and a negative unauthorized path, ordinary +`OperationStatus` / `Diagnostic` refusal without state mutation, and evidence at +the observation strength it declares. Backends that disclose service or ACL as +`descriptor-only` or `unsupported` are conformant only when they do not claim +the corresponding runtime effect. + +Reuse the existing conformance scenario parameter while it remains the active +bridge; the realization-envelope witness seam is its governed replacement. +Tests belong in the existing SDL model/parser/schema, processor, pure backend +realization, OCI argv/redaction, libvirt ACL enforcement, and target-conformance +families rather than a second service-specific harness. + +## Extension Seam + +The immediate seam is the existing node resource's `spec.node.services` +collection paired with independent service and ACL realization-concern +disclosures for the selected backend configuration. Preserve every concrete +entry, including unnamed services, and preserve or reject the authored +protocol; never coerce an unknown protocol to TCP or discard an unnamed entry. + +The next reasonable extension is richer source selection. Its parameter belongs +on a typed traffic-authorization rule and should identify governed network, +CIDR, zone, or principal selectors with explicit default and evaluation +semantics. That allows a future same-range, cross-range, or externally sourced +policy without reinterpreting `Node.services` or baking one backend's topology +choice into the portable service contract. + +## Non-Goals And Anti-Patterns + +- Do not implement shifter's same-range-derived firewall policy as the ACES + default, infer audience from `internal`, topology membership, node roles, or + service names, or auto-generate allow rules from services. +- Do not turn `Node.services` into observed listener state, host publication, + image metadata, daemon configuration, an ACL container, or a reachability + result. Do not move ACL fields onto `ServicePort`. +- Do not add `role`, `audience`, or a free-form policy bag without a separately + specified source-selector and evaluation contract. +- Do not silently drop unnamed services, unsupported protocols, unresolved + CIDRs, invalid ACL rules, or unsupported backend concerns. In particular, do + not copy libvirt's current unknown-protocol-to-TCP fallback. +- Do not claim ACL support from extraction, snapshot carriage, labels, or test + doubles. Do not claim service realization from a declared port or container + metadata alone. +- Do not redesign ACL ordering/default semantics, backend privilege management, + control-plane APIs, persistence, logging, runtime listener inventory, + published-port semantics, image build metadata, or protocol-specific runtime + inventories in this issue. +- Do not add implementation under `implementations/python/src/aces/`, hand-edit + generated schemas, fork diagnostics/exceptions, or introduce a second + compiler/backend workflow. diff --git a/docs/decisions/issue-760-sem-218-provenance-preservation-preflight.md b/docs/decisions/issue-760-sem-218-provenance-preservation-preflight.md new file mode 100644 index 000000000..34d1b67ff --- /dev/null +++ b/docs/decisions/issue-760-sem-218-provenance-preservation-preflight.md @@ -0,0 +1,103 @@ +# Issue 760 SEM-218 Provenance Preservation Preflight + +Date: 2026-07-13 + +Issue: #760. Requirement: none; the issue is the delivery contract. + +This note narrows the existing issue #491 SEM-218 runtime-realization preflight +to the provenance-loss defect. It records architecture guardrails only and does +not implement the compiler or runtime change. + +## Decision + +`ExplicitnessRecord.provenance` is the canonical origin classification at the +SDL-to-runtime boundary. The compiler must preserve that value on the existing +`CompiledRealizationRequirement`; the honoured branch of +`realization_disclosure()` must copy it to the existing +`RealizationProvenanceEntry`. Only a value selected by the backend because the +declared value was not honoured is `BACKEND_REALIZED`. The compiled field must +be required rather than defaulting to `AUTHOR_DECLARED`; a default would let a +future constructor silently recreate the same attribution bug. + +This is an internal typed-carrier repair. It does not add a public field: the +published `runtime-snapshot-v1` schema, `RealizationProvenanceEntryModel`, and +control-plane snapshot serializer already admit and persist all three values. +Therefore the implementation must not version or regenerate the public schema, +add another provenance enum, or create a sidecar persistence channel. + +The canonical classifier resolves the wording overlap in SEM-218 I5: +parameter substitution is `PROCESSOR_DERIVED`, whether the selected parameter +value came from a caller binding or an SDL default. `AUTHOR_DECLARED` is +reserved for a value that survives instantiation without substitution. This +follows `derive_instantiated_explicitness()` and the issue contract; runtime +code must not infer origin from equality, explicitness class, plan presence, or +backend success. + +## Existing Boundaries To Reuse + +- Classification and validation: `aces_sdl.explicitness`, + `derive_instantiated_explicitness()`, and the closed SDL/Pydantic admission + path remain the only origin authority. +- Compilation and planning: `_compile_realization_requirements()`, + `RuntimeModel.realization_requirements`, `CompiledRealizationRequirement`, + and `realization_support_diagnostics()` remain the single typed path. The + added carrier is metadata and must not enter backend `resource_payload()`. +- Runtime enforcement: `realization_disclosure()` and + `aces_runtime.backend_calls._call_backend_apply()` remain the fail-closed + adapter boundary. Existing `runtime.backend-contract-invalid` diagnostics, + rollback behavior, and snapshot acceptance order do not change. +- Observation and persistence: `RealizationProvenanceEntry`, + `RuntimeSnapshot.realization_provenance`, `RealizationProvenanceEntryModel`, + `RuntimeSnapshotEnvelopeModel`, `_snapshot_payload()`, and + `_snapshot_from_payload()` remain the only disclosure and persistence path. +- Verification: extend the existing SEM-218 differential/compiler and runtime + tests. The decisive case instantiates a realization concern through a + parameter/default substitution, compiles it, has the backend honour it, and + observes `PROCESSOR_DERIVED` in the runtime ledger. + +## Cross-Cutting Guardrails + +- Security and exposure: provenance is an enum label, not the realized value. + Do not add authored or realized values to diagnostics, logs, audit details, + fixtures, environment variables, process arguments, or error envelopes. + No authentication, secret, configuration, subprocess, or OS interface is + introduced by this repair. +- Validation: reuse enum typing at the dataclass and published Pydantic contract + boundaries. Do not add duplicate string validation or a new exception type. + Compiled-address validation in `CompiledRealizationRequirement.__post_init__` + remains unchanged. +- Errors and observability: an invalid backend result still uses the existing + structured `Diagnostic` path. Provenance preservation itself is not a new + warning, metric, log event, or exception surface. +- Schema and compatibility: because `processor-derived` is already a valid + published enum member, no schema-publication manifest entry, fixture-shape + change, contract version bump, or downstream compatibility shim is required. +- Persistence: the existing control-plane round trip already serializes enum + values. Do not introduce repository, database, migration, metadata-map, or + generic-details storage for compiled provenance. + +## Extension Seam + +The seam is the provenance field on the existing compiled requirement, keyed +with the already parameterized `field_path`, `address`, `domain`, and +`requirement_kind`. Future realization concerns can flow through that carrier +and the existing concern-path/kind mappings. They must not require a +backend-specific provenance vocabulary or another runtime gate. + +## Non-Goals And Anti-Patterns + +- Do not change explicitness classification, parameter binding semantics, + realization-support matching, exactness/non-approximation behavior, concern + coverage, backend manifests, SDL syntax, or public contract shapes. +- Do not treat `PROCESSOR_DERIVED` as backend realization, or infer provenance + from whether the backend honoured a value. Honouring answers whether the + backend substituted; the compiled provenance answers where the honoured + value originated. +- Do not reclassify SDL at runtime, inspect `record.variables` outside the + classifier, use a boolean such as `authored`, or duplicate the three-value + enum in processor/runtime packages. +- Do not place provenance in backend payloads, snapshot `metadata`, apply-result + `details`, logs, or a private APTL-facing contract. +- Do not alter accepted ADRs. The existing SEM-218 authority and issue #491 + architecture are sufficient; this defect does not justify a new abstraction + or ADR. diff --git a/docs/decisions/issue-767-explicitness-provenance-identity-preflight.md b/docs/decisions/issue-767-explicitness-provenance-identity-preflight.md new file mode 100644 index 000000000..600a2a1d4 --- /dev/null +++ b/docs/decisions/issue-767-explicitness-provenance-identity-preflight.md @@ -0,0 +1,187 @@ +# Issue 767 Explicitness Provenance Identity Preflight + +Date: 2026-07-13 + +Issue: #767. Requirement: none; the issue is the delivery contract. + +This note records architecture guardrails for the import-expansion provenance +defect. It does not implement the parser change or its regression tests. + +## Decision + +`ExplicitnessProvenanceRecord.model_path` is the canonical address of a field +in the expanded executable scenario. It is not the address of a source-module +field and it is not an import-event identity. Consequently, +`ExpansionProvenance.explicitness` is a projection of imported author intent +onto fields that survive in the expanded scenario, not a complete ledger of +every classified source-module container and metadata field. + +Composition must retain imported declaration descendants whose paths survive +expansion, and rewrite their declaration identity through the same symbol map +used for the imported payload. It must not retain module-local `name`, +`version`, or a section aggregate such as `behavior_specifications`: those +source fields do not become distinct fields in the host scenario. The root +scenario's classifier supplies its one surviving root metadata and aggregate +record during final instantiation. + +Keep the existing uniqueness invariant: one provenance object has at most one +explicitness record per final `model_path`. Adding import namespace or source +to that identity would permit conflicting classifications for one concrete +field and would break the existing path-keyed instantiation and compiler +consumers. Do not weaken `_require_unique()` and do not deduplicate records +after construction. The producer must emit the right projection before the +closed phase contract validates it. + +The current development baseline already filters imported records to +descendants of composition-owned hashmap sections, which prevents the reported +0.20.0 collisions. That behavior landed incidentally and is not sufficient as +the issue's durable evidence: regression coverage must make the projection, +namespace rewriting, uniqueness, nested-import behavior, and downstream +explicitness preservation explicit. + +## Existing Boundaries To Reuse + +- Composition ownership remains in `aces_sdl.composition.expand_sdl_modules()`. + Import resolution, binding, namespace rewriting, merging, traversal limits, + and provenance construction must remain one transition rather than a second + post-processing workflow. +- `_composition_provenance.prefixed_explicitness()` and + `_prefixed_model_path()` are the canonical portable transformation seam. + Reuse `_module_symbols.symbol_index()` and its `HASHMAP_SECTIONS`; they are + the authority for composition-renamed declarations, including generated + `__private` identities. Do not infer prefixes by splitting arbitrary dotted + text or duplicate the symbol table. +- `_bind_scenario_content()`, `derive_instantiated_explicitness()`, and + `_merge_expanded_provenance()` own binding and the replacement of freshly + classified imported paths with portable records. Parameter identities must + continue to be qualified through the import namespace at composition. +- `ExplicitnessProvenanceRecord`, `ExpansionProvenance`, + `InstantiationProvenance`, `_validate_derivation_collections()`, and + `_require_unique()` are the closed validation contract. Do not add a second + record shape, uniqueness validator, DTO, or exception hierarchy. +- `InstantiatedScenario.explicitness` and the compiler's existing + path-keyed SEM-218 lookup are the downstream contract. A record is useful + only if its path addresses the same field in the concrete scenario. +- ADR-053, ADR-076, ADR-078, `specs/formal/sdl-phases/README.md`, and the + normative SEM-218 specification already govern composition identity, phase + closure, and explicitness. This bug does not need a new ADR or a new + provenance concept. +- Verification belongs with the existing composed/nested import tests in + `test_sdl_phase_contracts.py` and `test_sdl_module_registry.py`. The + repository workflow remains `.ground-control.yaml`, `.gc/plan-rules.md`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/verify_all.py`, and the canonical nox graph. + +There are two similarly named section catalogs with different ownership: +`_module_symbols.HASHMAP_SECTIONS` excludes variables and governs importable +declaration identities; `_mapping_scopes.HASHMAP_SECTIONS` also includes +variables and governs authored mapping-key interpretation. Composition +provenance must use the former. Do not create a third catalog or substitute the +source-mapping catalog merely because the names match. + +## Cross-Cutting Layers + +- **Source and migration gate:** every root and imported document must still + pass `_source_validation.py`, `_load_normalized_data()`, the selected + `SDLMigrationPolicy`, parser resource limits, canonical key handling, and + closed `SDLModel` construction. The fix must not preprocess YAML or special + case private downstream content. +- **Import trust and filesystem gate:** retain `ImportDecl` validation, + `module_registry.resolve_import()`, local-base resolution, lockfile checks, + version/digest/export validation, registry trust/signature policy, cycle + detection, and `CompositionBudget`. Provenance projection occurs only after + the import has passed those gates; it must not create another file read or + resolver path. +- **Namespace and merge gate:** use the descriptor-restricted symbol index, + `_namespace_payload()`, and `_merge_sections()`. A retained explicitness path + must address the exact exported or private identity that was merged. Root + metadata must not be fabricated as paths such as `core.name`, because no + corresponding field exists in the expanded model. +- **Phase-contract gate:** `FrozenPhaseModel(extra="forbid")`, qualified + parameter closure, unique final model paths, portable source validation, and + finite scalar constraints remain fail-closed. Producer bugs must not be + hidden by relaxing Pydantic validation. +- **Instantiation and semantic gate:** final binding must continue through + `instantiate_scenario()`, provenance merging, `InstantiatedScenario` + admission, and `SemanticValidator`. The root-derived record and imported + portable record for a surviving path must not coexist; the portable record + replaces the reclassified imported path through the existing merge. +- **Compiler/runtime gate:** downstream SEM-218 consumers continue to look up + one record by concrete model path. No source namespace, module id, import + record, backend payload field, runtime provenance enum, or realization rule + should be added for this parser defect. +- **Error-envelope and observability gate:** authored failures remain + `SDLParseError`, `SDLValidationError`, or `SDLInstantiationError` at their + existing phase. If closed provenance validation detects an internal + composition inconsistency, the parser boundary must convert the underlying + `ValidationError` to a bounded `SDLParseError` and retain exception chaining; + it must not expose a raw Pydantic dump, source body, binding values, local + paths, or traceback. CLI and MCP adapters keep their existing redacted + envelopes. No logger, metric, warning, or advisory is needed for successful + projection. +- **Secret, auth, config, and OS gate:** this change introduces no credential, + environment binding, auth/authz surface, subprocess, process argument, + network call, daemon, or host mutation. Do not include import contents, + parameter values, registry credentials, or absolute host paths in new + diagnostics or fixtures. +- **Persistence and schema gate:** expansion provenance remains the existing + internal typed carrier and instantiation provenance remains the existing + serialized carrier. No database, repository, cache, migration, sidecar + ledger, public field, schema version, or publication-manifest change is + warranted unless implementation changes the public record shape—which this + issue does not require. + +## Extension Seam And Gotchas + +The extension seam is the existing composition-provenance path projection and +rewrite, driven by the composition section identity strategy and symbol map. +The next reasonable variation is another importable section shape. A hashmap +section should extend the canonical composition section catalog; a list-based +section needs an explicit stable-identity-to-final-position rewrite in the same +seam. It must not be admitted by broad string-prefix matching. + +In particular, scenario-level `forwarding_agents` is imported and renamed by +stable `forwarding_agent_id`, but its explicitness paths are positional +(`forwarding_agents....`). The current hashmap-only projection excludes +those records. Issue #767 must not silently include them without correctly +rebasing positions after root and sibling merges. Adding complete imported +explicitness for list-based declarations is a separate bounded semantic change; +the present fix must neither corrupt those paths nor claim that it solved them. + +Nested imports are another required guardrail. The retained path may already +contain an inner namespace. Prefix only the declaration identity resolved by +the outer symbol map, preserve the remaining suffix exactly, and qualify every +referenced parameter tuple once per import level. Reclassification, string +replacement of every matching segment, or source-path-derived namespaces will +misaddress nested declarations. + +Avoid: + +- treating `model_path`, import namespace, module id, source URI, and authored + source location as interchangeable identities; +- namespacing source-only `name`, `version`, variables, import declarations, + module descriptors, or section aggregate records; +- merging duplicate records by first/last writer, equality of classification, + weakest/strongest class, or empty reason text; +- weakening uniqueness to `(source, model_path)` or + `(namespace, model_path)` while downstream consumers remain keyed by path; +- filtering only the three paths observed in the private report instead of + applying the surviving-expanded-field rule; +- rerunning explicitness classification in composition or the compiler; +- adding a generic provenance database, source-coordinate schema, compatibility + wrapper logic under `implementations/python/src/aces/`, or a second import + workflow; and +- editing accepted ADRs, published schemas, `CHANGELOG.md`, or the committed + package version for this narrow repair. + +## Non-Goals + +- Changing SEM-218 classification, realization posture, parameter/default + semantics, module export policy, import trust policy, or semantic validation. +- Preserving explicitness for source fields that do not exist in the expanded + scenario, or making expansion provenance a complete source-lineage ledger. +- Redesigning list-section identity, including positional forwarding-agent + provenance, as part of the reported aggregate-path collision fix. +- Changing public contracts, canonical digest profiles, compiler/runtime + payloads, control-plane APIs, persistence, logging, CLI commands, or MCP + operations. diff --git a/docs/explain/reference/explicitness-realization-semantics.md b/docs/explain/reference/explicitness-realization-semantics.md index 4ba4c1183..385ef7f82 100644 --- a/docs/explain/reference/explicitness-realization-semantics.md +++ b/docs/explain/reference/explicitness-realization-semantics.md @@ -52,6 +52,8 @@ Build on these existing surfaces before adding anything new: - SDL parsing and closed models: `aces_sdl.parser`, `aces_sdl.SDLModel`, and Pydantic `extra="forbid"` model boundaries +- scoped author intent: `aces_sdl.realization_designation`, plus the + designation records in expansion and instantiation provenance - static semantics: `SemanticValidator` and `SDLValidationError` - instantiation: `instantiate_scenario()` and `SDLInstantiationError` - shared semantic helpers: `aces_sdl.semantics.*` and @@ -89,7 +91,9 @@ The implementation must pass every layer it touches: - instantiation gate: defaults and parameters may fill open concerns only when the owning semantic rule permits it; concrete scenarios must be revalidated. - contract/schema gate: external payload shape belongs in `ContractModel` - descendants and generated schemas. Do not hand-edit `contracts/schemas/`. + descendants and the coordinated hand-governed/reference-model schema + surface. Reconcile model, schema, publication manifest, and generator parity + in one change. - manifest/profile gate: supported contract versions, concept bindings, binding scopes, realization support modes, exact requirement kinds, and controlled vocabulary terms must resolve through existing authority helpers. @@ -123,6 +127,41 @@ additional exact requirement kinds for other artifact families; that should require adding governed terms and shared semantic checks, not rewriting planner or conformance call sites. +Explicit root delegation reaches planning through the injected +`apparatus_realization_default` resolver. Its current fallback is closed; a +future typed apparatus-default contract can supply a different choice without +changing SDL cascade rules or backend implementations. + +## Scoped Default Cascade + +The one author-facing inherited-default surface is the optional scenario-root +`realization` block. It carries a `default` posture and zero or more typed scope +entries. Each scope identity is a namespace tuple plus a canonical RFC 6901 +field pointer; dotted paths, wildcards, and JSONPath are not accepted author +syntax. + +Resolution preserves these distinctions: + +- an explicit exact, constrained, or open leaf is authoritative; +- otherwise the most-specific concrete scoped posture wins, independent of + source order; +- `unspecified` inherits an enclosing concrete posture, or delegates the root + decision to the selected-apparatus resolver; +- omitting `realization` preserves the legacy closed behavior and is not the + same state as explicit root delegation. + +Composition rewrites declaration pointers through the import symbol map and +qualifies designation records with the import namespace. A module default can +therefore govern that module's declarations without leaking to its host or a +sibling module. The authoring block itself is removed during expansion and +instantiation; typed records travel in the existing phase-provenance aggregates +and then in compiled realization requirements. + +Open affects only registered realization concerns. It does not relax closed +Pydantic shapes, invent topology, bypass references, or make every absent +optional field backend-realizable. The apparatus support mode remains a +capability disclosure rather than author intent. + ## Authoring Specificity `classify_authoring_specificity()` is the DSL-115 helper for reviewing @@ -148,12 +187,19 @@ author-intent source. Planner rejection belongs in the existing `aces_processor.planner.plan()` / `ExecutionPlan.diagnostics` path. Unsupported exact or constrained requirement -kinds should be reported as stable `Diagnostic` objects that name the compiled +kinds and open demand without explicit `OPEN_REALIZATION` support should be +reported as stable `Diagnostic` objects that name the compiled resource address, SDL field path or equivalent field identifier, requirement kind, and missing `realization_support` capability. Do not introduce a SEM-218-specific exception hierarchy or throw from normal planning for this case. +When a backend publishes a finer realization envelope, project that offer onto +the compiled open concern paths and evaluate it with the canonical +`subsumes(offered, requested)` relation. Keep unrelated offered bindings out of +the projection so a constraint on an exact, unrelated field cannot narrow the +author's open concern by accident. + Keep realization-support matching as one manifest-bound helper over `BackendManifest.realization_support` / `RealizationSupportDeclaration`. `domain`, `supported_exact_requirement_kinds`, and @@ -170,6 +216,7 @@ Avoid: - using `realization_support` as a substitute for authoring semantics - treating `open-realization` as permission to ignore exact declarations - treating missing data as equivalent to backend freedom +- accepting dotted, partially tokenized, wildcard, or JSONPath scope targets - silently downgrading an exact requirement into a constrained or best-effort realization - adding a second exception hierarchy, schema registry, vocabulary table, or @@ -187,17 +234,10 @@ Avoid: This note is implementation guidance for the SEM-218 normative spec at {download}`specs/formal/realization/explicitness-and-realization.md <../../../specs/formal/realization/explicitness-and-realization.md>`. -It does not itself add SDL syntax, define exact-requirement-kinds, or -change manifest payloads — those are governed by the spec and by the -controlled-vocabulary / reference-model authorities. The PR that -introduced the spec promoted the SEM-218 row in the SEM-200 coverage -table to `partial` and transitioned the requirement from `DRAFT` to -`ACTIVE` in Ground Control. The staged work that lifted the row from -`partial` to `active` — the SEM-218 classifier in `SemanticValidator`, -the typed compiler emission, the planner gate, the runtime -non-approximation gate (`aces_processor.semantics.realization`, invoked -from the `aces_runtime` adapter boundary), and the -SEM-218 `realization_provenance` fields on the runtime snapshot envelope — -is now complete, and the coverage row reads `active`. Treat the prose -above as architecture guidance for that work; treat the spec as the -binding contract. +It does not itself define exact-requirement-kind vocabularies or change +manifest payloads — those are governed by the spec and by the +controlled-vocabulary / reference-model authorities. The SEM-218 coverage row +is `active`: the classifier, typed scoped designation cascade, compiler carrier, +planner gate, runtime non-approximation/disclosure gate, and snapshot +provenance delivery are implemented together. Treat the prose above as +architecture guidance for those surfaces and the spec as the binding contract. diff --git a/docs/explain/reference/fm-classification-ledger.yaml b/docs/explain/reference/fm-classification-ledger.yaml index 3dfb94952..1e2593ab0 100644 --- a/docs/explain/reference/fm-classification-ledger.yaml +++ b/docs/explain/reference/fm-classification-ledger.yaml @@ -469,3 +469,16 @@ entries: - kind: unit_tests path: implementations/python/tests/test_sdl_lineage.py waived_artifacts: [] + - adr: ADR-081 + surface: Behavioral-relation taxonomy and claim discipline + fm_level: FM2 + delivered_artifacts: + - kind: invariant_list + path: specs/formal/behavioral-relations/README.md + - kind: unit_tests + path: implementations/python/tests/test_behavioral_relations.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_contracts/behavioral_relations.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_behavioral_relations.py + waived_artifacts: [] diff --git a/docs/explain/reference/reference-emulation-backend.md b/docs/explain/reference/reference-emulation-backend.md index 71f538ed4..4af5742e7 100644 --- a/docs/explain/reference/reference-emulation-backend.md +++ b/docs/explain/reference/reference-emulation-backend.md @@ -58,6 +58,21 @@ ignores extras it does not use. (a partial failure rolls back what succeeded), and containers are attached to every network their plan declares. +### Service and ACL boundary + +The interpreter preserves each authored `Node.services[]` entry as a typed +service descriptor on the portable container specification, including unnamed +services and non-TCP protocols. Both bundled drivers treat those descriptors as +descriptor-only: they do not publish host ports, configure daemons, synthesize +allow rules, or claim that a listener exists. Reachability therefore never +follows from a service declaration. + +Traffic authorization remains the separate `infrastructure.*.acls` concern. +The reference backend declares `supports_acls=false` because neither bundled +driver enforces ACLs; the planner rejects such a plan before apply. A future +driver that realizes services or ACLs must use the existing realization-concern +and diagnostic surfaces and provide evidence for the runtime effect it claims. + ```python from aces_reference_backend import create_reference_backend_target from aces_reference_backend.drivers.oci import OciDeploymentDriver diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 34159e32c..0398f781d 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -233,29 +233,29 @@ so they are tracked by their own requirements, not here. | Deterministic module composition and canonical-identity stability across expansion | DSL-103, SEM-205 | authoring, validation, compilation | `implementations/python/packages/aces_sdl/composition.py`, `implementations/python/packages/aces_sdl/module_registry.py`, `specs/formal/composition-readiness.md`, `implementations/python/tests/test_sdl_module_registry.py` | active | | Instantiation, closed portable phase contracts, and revalidation of concrete scenarios | RUN-301 | instantiation, validation | `docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md`, `specs/formal/sdl-phases/README.md`, `contracts/schemas/sdl/instantiated-scenario-v1.json`, `contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json`, `implementations/python/packages/aces_sdl/phase_contracts.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/tests/test_sdl_phase_contracts.py`, `implementations/python/tests/test_instantiated_scenario_schema.py`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_run_300_lifecycle.py` | active | | Objective windows, referenced scopes, reachability, and refresh | SEM-202 | validation, compilation, planning | `implementations/python/packages/aces_sdl/semantics/objectives.py`, `specs/formal/objectives/README.md`, `specs/formal/objectives/window-consistency.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py` | active | -| Declarative objective actor binding, target resolution, success interpretation, and dependency ordering | DSL-112, SEM-207 | authoring, validation, instantiation, compilation, planning | `implementations/python/packages/aces_sdl/objectives.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `specs/formal/objectives/README.md`, `specs/formal/objectives/declarative-objective-semantics.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py`, `implementations/python/tests/test_sdl_validator.py` | active | +| Declarative objective actor binding, target resolution, success interpretation, and dependency ordering | DSL-112, SEM-207 | authoring, validation, instantiation, compilation, planning | `implementations/python/packages/aces_sdl/objectives.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `specs/formal/objectives/README.md`, `specs/formal/objectives/declarative-objective-semantics.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py`, `implementations/python/tests/test_sdl_validator.py` | active | | Workflow control semantics (branching, joins, calling, retry, completion, history) | DSL-113, SEM-203 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/orchestration.py`, `implementations/python/packages/aces_sdl/semantics/workflow.py`, `specs/formal/workflows/README.md`, `specs/formal/workflows/state-machine.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_sdl_models.py` | active | | Workflow compensation semantics (registration, triggering, ordering, observation) | SEM-204 | validation, compilation, execution, observation | `implementations/python/packages/aces_sdl/semantics/workflow.py`, `specs/formal/workflows/compensation.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_manager.py` | partial | -| Backend-neutral proposition truth and assertion-composed objective success (probe conditions remain separate implementation bindings; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/propositions.py`, `implementations/python/packages/aces_sdl/conditions.py`, `implementations/python/packages/aces_sdl/semantics/propositions.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `specs/formal/objectives/proposition-and-assertion-semantics.md`, `implementations/python/tests/test_proposition_semantics.py`, `implementations/python/tests/test_truth_result_contracts.py`, `implementations/python/tests/test_fm2_semantics.py` | active | +| Backend-neutral proposition truth and assertion-composed objective success (probe conditions remain separate implementation bindings; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/aces_sdl/propositions.py`, `implementations/python/packages/aces_sdl/conditions.py`, `implementations/python/packages/aces_sdl/semantics/propositions.py`, `implementations/python/packages/aces_sdl/semantics/objective_semantics.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `specs/formal/objectives/proposition-and-assertion-semantics.md`, `implementations/python/tests/test_proposition_semantics.py`, `implementations/python/tests/test_truth_result_contracts.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Runtime compiled representation and canonical addresses | RUN-302 | compilation | `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Planner dependency, ordering, refresh, and applicability semantics | RUN-303 | planning | `implementations/python/packages/aces_processor/semantics/planner.py`, `implementations/python/packages/aces_processor/planner.py`, `specs/formal/planner/README.md`, `specs/formal/planner/dependency-ordering.md`, `implementations/python/tests/test_semantics_planner.py`, `implementations/python/tests/test_runtime_planner.py` | active | -| Live execution state and lifecycle (snapshots, results, history) | RUN-304, API-402 | execution, observation | `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/result_contracts.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/tests/test_runtime_manager.py`, `implementations/python/tests/test_runtime_models.py` | active | +| Live execution state and lifecycle (snapshots, results, history) | RUN-304, API-402 | execution, observation | `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/result_contracts.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/tests/test_runtime_manager.py`, `implementations/python/tests/test_runtime_models.py` | active | | Runtime result and evaluator-result contracts | ASR-503, API-402 | execution, observation | `implementations/python/packages/aces_runtime/result_contracts.py`, `specs/formal/runtime-contracts/README.md`, `specs/formal/runtime-contracts/workflow-results.md`, `specs/formal/runtime-contracts/evaluator-results.md`, `implementations/python/tests/test_runtime_contracts.py`, `implementations/python/tests/test_run_311_participant_episode_lifecycle.py` | active | | Control-plane semantics (auth, durable state, idempotency, audit) | API-403, API-404 | execution, observation | `implementations/python/packages/aces_runtime/control_plane_api.py`, `implementations/python/packages/aces_runtime/control_plane_security.py`, `implementations/python/packages/aces_runtime/control_plane_store.py`, `implementations/python/tests/test_runtime_control_plane.py`, `implementations/python/tests/test_runtime_control_plane_api.py` | active | | Backend and processor identity, capability, and compatibility manifests | API-401, API-412 | planning, execution | `implementations/python/packages/aces_processor/manifest.py`, `implementations/python/packages/aces_processor/capabilities.py`, `implementations/python/packages/aces_contracts/apparatus.py`, `implementations/python/packages/aces_contracts/manifest_authority.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py` | active | | Concept authority, controlled vocabularies, reference models, and semantic profiles (meta-layer) | GOV-920 | authoring, validation, compilation, planning, execution | `specs/concept-authority/concept-authority.md`, `specs/concept-authority/semantic-profiles.md`, `implementations/python/packages/aces_contracts/semantic_profiles.py`, `implementations/python/packages/aces_contracts/controlled_vocabularies.py`, `implementations/python/packages/aces_contracts/reference_models.py`, `docs/explain/reference/shared-concept-model.md`, `implementations/python/tests/test_concept_authority.py`, `implementations/python/tests/test_semantic_profiles.py` | active | | Participant episode lifecycle boundaries (initialization, reset, completion, timeout, truncation, interruption) | RUN-311, SEM-222 | execution, observation | `docs/decisions/adrs/adr-013-participant-episode-lifecycle-boundaries.md`, `implementations/python/tests/test_run_311_participant_episode_lifecycle.py` | partial | | Declarative participant framing (identity, role, starting conditions, authority anchors, operating scope) | ACT-601 | authoring, validation | `implementations/python/packages/aces_sdl/agents.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `docs/decisions/adrs/adr-020-declarative-participant-framing-boundaries.md`, `implementations/python/tests/test_sdl_models.py`, `implementations/python/tests/test_sdl_validator.py` | active | -| Participant behavior semantics (actions, observations, state transitions) | ACT-602, SEM-208 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | -| Multi-participant interaction and participant-local histories | SEM-209, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/packages/aces_conformance/conformance.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | -| Visibility and information-boundary semantics | SEM-210 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | -| Participant preconditions, effects, failure, causality, and attribution semantics | SEM-211, SEM-212 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_action_semantics.py`, `implementations/python/packages/aces_sdl/participant_attribution_semantics.py`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/packages/aces_conformance/conformance.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/tests/test_sem_211_participant_action_semantics.py`, `implementations/python/tests/test_sem_212_participant_attribution_semantics.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Participant behavior semantics (actions, observations, state transitions) | ACT-602, SEM-208 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Multi-participant interaction and participant-local histories | SEM-209, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/packages/aces_conformance/conformance.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Visibility and information-boundary semantics | SEM-210 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_sdl/semantics/participant_behavior.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | +| Participant preconditions, effects, failure, causality, and attribution semantics | SEM-211, SEM-212 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_action_semantics.py`, `implementations/python/packages/aces_sdl/participant_attribution_semantics.py`, `implementations/python/packages/aces_sdl/participant_behavior.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/packages/aces_conformance/conformance.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/tests/test_sem_211_participant_action_semantics.py`, `implementations/python/tests/test_sem_212_participant_attribution_semantics.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | | Participant temporal, tool/affordance, and decision-surface semantics | SEM-213, SEM-219, SEM-220 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | | Participant reference trajectories, demonstrations, budgets, and quota/exhaustion semantics | SEM-221, SEM-223 | — | — | planned | -| Participant outcome interpretation | SEM-215 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_outcome_semantics.py`, `implementations/python/packages/aces_sdl/semantics/participant_outcome.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/tests/test_sem_215_participant_outcome_interpretation.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | +| Participant outcome interpretation | SEM-215 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_outcome_semantics.py`, `implementations/python/packages/aces_sdl/semantics/participant_outcome.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/tests/test_sem_215_participant_outcome_interpretation.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | | Derived operational context views (portable meaning and comparability) | SEM-214 | execution, observation | `specs/formal/participant-semantics/README.md`, `specs/formal/runtime-contracts/participant-backend-contracts.md`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/packages/aces_runtime/participant_retrieval.py`, `implementations/python/packages/aces_runtime/control_plane_api_participant_retrieval.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_control_plane.py`, `implementations/python/tests/test_runtime_control_plane_api.py` | active | | Boundary semantics for runtime-observable state, captured evidence, derived evaluations, analysis outputs, and audience-specific views | SEM-216 | execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md`, `implementations/python/packages/aces_contracts/contracts.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `contracts/schemas/experiment-core/experiment-evidence-record-v1.json`, `implementations/python/tests/test_sem_216_boundary_semantics.py`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_contracts.py` | active | | Evidence, evaluation, view-boundary, and observability-plane semantics | SEM-224, SEM-225, DSL-123, DSL-124 | authoring, validation, execution, observation | `docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md`, `specs/formal/observability-evidence-plane.md`, `specs/sdl/observability-and-evidence.md` | partial | | External knowledge bindings semantics | SEM-217 | validation, execution | `specs/formal/participant-semantics/README.md`, `docs/explain/reference/shared-concept-model.md`, `implementations/python/packages/aces_contracts/semantic_binding_effects.py`, `implementations/python/tests/test_sem_217_knowledge_bindings.py` | active | -| Explicitness and realization semantics (binding declarations vs processor/backend realization) | SEM-218 | validation, instantiation, compilation, planning, execution, observation | `specs/formal/realization/explicitness-and-realization.md`, `specs/formal/realization/README.md`, `docs/explain/reference/explicitness-realization-semantics.md`, `implementations/python/packages/aces_sdl/explicitness.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/packages/aces_contracts/apparatus.py`, `implementations/python/packages/aces_contracts/vocabulary.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/packages/aces_contracts/runtime_state.py`, `implementations/python/packages/aces_backend_protocols/manifest.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/packages/aces_processor/planner.py`, `implementations/python/packages/aces_processor/semantics/realization.py`, `implementations/python/packages/aces_runtime/backend_calls.py`, `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/control_plane_store.py`, `implementations/python/tests/test_sem_218_explicitness.py`, `implementations/python/tests/test_sem_218_realization.py`, `implementations/python/tests/test_sem_218_runtime_realization.py`, `implementations/python/tests/test_runtime_planner.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py`, `implementations/python/tests/test_runtime_contracts.py` | active | +| Explicitness and realization semantics (binding declarations vs processor/backend realization) | SEM-218 | authoring, validation, instantiation, compilation, planning, execution, observation | `specs/formal/realization/explicitness-and-realization.md`, `specs/formal/realization/README.md`, `docs/explain/reference/explicitness-realization-semantics.md`, `implementations/python/packages/aces_sdl/explicitness.py`, `implementations/python/packages/aces_sdl/realization_designation.py`, `implementations/python/packages/aces_sdl/phase_contracts.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/packages/aces_contracts/apparatus.py`, `implementations/python/packages/aces_contracts/vocabulary.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/packages/aces_contracts/runtime_state.py`, `implementations/python/packages/aces_backend_protocols/manifest.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/models/`, `implementations/python/packages/aces_processor/planner.py`, `implementations/python/packages/aces_processor/semantics/realization.py`, `implementations/python/packages/aces_runtime/backend_calls.py`, `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/control_plane_store.py`, `implementations/python/packages/aces_runtime/control_plane_api_models.py`, `implementations/python/tests/test_sem_218_explicitness.py`, `implementations/python/tests/test_sem_218_realization.py`, `implementations/python/tests/test_sem_218_realization_designation.py`, `implementations/python/tests/test_sem_218_runtime_realization.py`, `implementations/python/tests/test_runtime_planner.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py`, `implementations/python/tests/test_runtime_contracts.py` | active | | Clock, time-domain, advancement/pacing/synchronization, and temporal ordering/causality semantics | SEM-227, SEM-228, SEM-229 | — | — | planned | diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 32dcc6637..ed90dce43 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -58,6 +58,17 @@ semantics, examples, or code from that source. raw SBOM documents, scanner output, or invocation/capability semantics into the SDL schema. +The authored/defaulted/planned/realized/observed/derived distinction tested by +[issue #160](https://github.com/Brad-Edwards/aces/issues/160) is a carrier +boundary, not a vocabulary tag. SDL and `model_fields_set` carry authored and +defaulted meaning; compiler plans carry planned operations; realization +provenance and realized-form disclosures carry admitted choices; evidence +records and inventory ledgers carry observations; derived-measure contracts +carry interpretations. Docker identifiers, health results, and scanner state +therefore remain evidence even when an SDL model could otherwise represent the +same scalar shape. Deliberate authoring is the only promotion into scenario +requirements. + ## Directory, Domain, And Identity Authority Semantics The `runtime.identity_authorities` surface is issue #401's response to an diff --git a/docs/explain/sdl/related-work-comparison.md b/docs/explain/sdl/related-work-comparison.md index 4b0a8739d..c5f6fd0ce 100644 --- a/docs/explain/sdl/related-work-comparison.md +++ b/docs/explain/sdl/related-work-comparison.md @@ -1,398 +1,128 @@ # Related-Work Comparison -This page positions ACES against precedent systems dimension by dimension. It is -the comparison a peer review asks for first: what can ACES express that the -precedents cannot, and where do the precedents still lead ACES. - -It is an evidence surface, not a ranking and not a marketing claim. ACES leads on -some dimensions and trails on others, and several ACES surfaces are formally -specified but still materializing in the runtime. For element-by-element source -provenance see [Design Precedents](precedents.md); for the narrative source map -see [Lineage and Prior Work](lineage.md). Every non-ACES cell below is grounded -in the precedent's own documentation, standard text, source, or -originating-author literature, with the full audit trail in the -[related-work comparison research notes](../../research/related-work-comparison/search-log.md). -ACES cells cite repository specs, ADRs, and contracts; this page does not define -new ACES semantics. - -## How To Read The Matrix - -Each cell is one of: - -- **yes** — the system treats the dimension as a first-class capability. -- **partial** — the system addresses part of the dimension, or addresses it as a - side effect of another mechanism rather than as a first-class construct. -- **no** — the system does not address the dimension. -- **oos** — out of scope: the dimension is outside the system's design purpose. - -Columns: - -- **ACES** — this repository. -- **OCR SDL** — Open Cyber Range Scenario Definition Language. -- **CybORG** — the CAGE Challenge reinforcement-learning gym. -- **CACAO** — OASIS CACAO Security Playbooks v2.0. -- **Cyber DEM/FOM** — SISO Cyber Data Exchange Model and Cyber Federation Object - Model. -- **CRACK\*** — academic range DSLs; the column leads with CRACK and notes KYPO - and CyRIS where they differ. - -The eight dimensions are defined in [Dimensions](#dimensions) below. - -## Matrix - -| Dimension | ACES | OCR SDL | CybORG | CACAO | Cyber DEM/FOM | CRACK\* | -| --------- | ---- | ------- | ------ | ----- | ------------- | ------- | -| 1. Runtime inventory depth | yes | no | partial | no | partial | partial | -| 2. Typed relationship subtypes | yes | partial | partial | partial | yes | yes | -| 3. Participant behavior / episode contracts | partial | partial | yes | no | partial | partial | -| 4. Authoring vs. instantiation separation | yes | partial | partial | yes | oos | yes | -| 5. Backend agnosticism + conformance | yes | partial | partial | partial | yes | no | -| 6. Declarative objectives / workflows | yes | yes | partial | yes | no | yes | -| 7. Provenance / disclosure surfaces | yes | partial | no | yes | partial | no | -| 8. Time semantics status | partial | partial | partial | partial | yes | no | - -\* CRACK ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)), -with KYPO ([Vykopal et al. 2017](https://doi.org/10.5220/0006428203100321)) and -CyRIS ([Pham et al. 2016](https://doi.org/10.1145/3011077.3011087)) noted where -they differ. - -## Dimensions - -Each dimension below states its definition, then a one-line justification and -citation per system. Competitor citations are summarized here and recorded in -full in the [research notes](../../research/related-work-comparison/search-log.md). - -### 1. Runtime inventory depth - -Observed runtime node and service state — service listeners, identity and -directory authorities, datastores, mail and DNS services, application RBAC -stores, software components — modeled as first-class declarative inventory, -distinct from authored topology. - -- **ACES — yes.** A typed `Node.runtime.*` family models observed state - (identity authorities, application authorizations, datastore/mail/DNS - services, security-monitoring managers, detection engines, service listeners, - and more) as inventory distinct from authored nodes; see the runtime sections - of [precedents.md](precedents.md) ("Deliberate Omissions") and - [lineage.md](lineage.md), governed by ADRs such as - [ADR-043](../../decisions/adrs/adr-043-runtime-service-listener-surface.md) - and - [ADR-054](../../decisions/adrs/adr-054-participant-runtime-observable-lifecycle.md). -- **OCR SDL — no.** Nodes are VMs or switches and Features are - service/configuration/artifact deployment actions, with no observed-state - inventory ([SDL Reference](https://documentation.opencyberrange.ee/docs/sdl/reference/)). -- **CybORG — partial.** Host YAML and the simulator's finite-state machine track - services, processes, and sessions, but as internal simulation state rather than - a separate declarative inventory surface - ([Standen et al. 2021, §2](https://arxiv.org/abs/2108.09118)). -- **CACAO — no.** Agents and targets are command-dispatch references; runtime - service state is out of scope - ([CACAO v2.0 §7](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — partial.** Defines exchangeable cyber objects (Device, - System, Service, Network, Data) as transmitted model-state, not an authored - per-node inventory - ([SISO-STD-025-2023](https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf)). -- **CRACK\* — partial.** CRACK node types carry Datalog `runtime` predicates that - verify authored facts on deployed nodes, not a structured inventory record; - KYPO/CyRIS topology is static - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). - -### 2. Typed relationship subtypes - -Typed, directed relationship edges between scenario elements, beyond plain -topology or workflow-routing links. - -- **ACES — yes.** Seven STIX-derived relationship types plus typed-detail edges - (forwarding, service-integration, proxy-upstream), with typed runtime - relationship subtypes specified in - [ADR-052](../../decisions/adrs/adr-052-typed-runtime-relationship-subtypes.md) - (see also the STIX mapping in [precedents.md](precedents.md)). -- **OCR SDL — partial.** Network links and deploy-order dependencies are - structurally distinct string references, with no trust/integration edge type - ([SDL-parser source](https://github.com/Open-Cyber-Range/SDL-parser)). -- **CybORG — partial.** Host-subnet, process parent/child, and NACL relations are - implicit in the YAML and FSM, with no named typed-edge vocabulary - ([Standen et al. 2021, §2](https://arxiv.org/abs/2108.09118)). -- **CACAO — partial.** Workflow steps carry typed routing edges - (`on_success`/`on_failure`, `on_true`/`on_false`, `cases`), but there is no - general typed inter-object relationship graph - ([CACAO v2.0 §4](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — yes.** A first-class `Relationship` object with a - `RelationshipType` enumeration of ten directed subtypes (Administers, - ComponentOf, ContainedIn, ProvidedBy, ResidesOn and inverses) - ([cyberdem-python](https://github.com/cmu-sei/cyberdem-python)). -- **CRACK\* — yes.** CRACK defines typed, directed TOSCA relationships - (`SetsWeakPassword`, `SetsEnumerableUsername`) and typed capability kinds - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). - -### 3. Participant behavior / episode contracts - -Participant actions, observations, rewards, episodes, and partial observability -as first-class semantics. - -- **ACES — partial.** A formal participant-semantics specification defines - actions, observations, visibility, causality, and outcomes portable across - human, AI-agent, scripted, and simulated participants - ([ADR-022](../../decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md), - `specs/formal/participant-semantics/`), - with backend-facing contracts in - [ADR-060](../../decisions/adrs/adr-060-participant-backend-facing-contract-surface.md); - the executed episode runtime is still materializing. -- **OCR SDL — partial.** Entities hold exercise roles and receive event-triggered - injects, and Metrics score trainees, but there are no agent - action/observation/reward/episode semantics - ([SDL Reference](https://documentation.opencyberrange.ee/docs/sdl/reference/)). -- **CybORG — yes.** Per-agent action spaces, role-filtered observations, - per-agent rewards, partial observability, and bounded episodes are the system's - core ([Standen et al. 2021, §2](https://arxiv.org/abs/2108.09118)). -- **CACAO — no.** No agent observation, reward, or episode concept; CACAO - orchestrates command execution - ([CACAO v2.0 §1, §7](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — partial.** Typed action/effect events carry actor, target, - and phase, but there is no observation model, reward, or episode boundary - ([cyberdem-python](https://github.com/cmu-sei/cyberdem-python)). -- **CRACK\* — partial.** CRACK `Principal` nodes carry role and knowledge linked - to Goals, as static assignments rather than behavioral contracts - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). - -### 4. Authoring vs. instantiation separation - -A logical authored scenario surface distinct from concrete deployment or -instantiation. - -- **ACES — yes.** A logical scenario surface is kept separate from backend - realization, with instantiation-time variable resolution - (`specs/sdl/variables-and-instantiation.md`, - [explicitness-realization-semantics.md](../reference/explicitness-realization-semantics.md)). -- **OCR SDL — partial.** Node templates and `count`/infrastructure addressing - separate capability from instances, but the language is the VM-deployment spec - ([SDL Reference](https://documentation.opencyberrange.ee/docs/sdl/reference/); - [VMware Handlers](https://documentation.opencyberrange.ee/docs/handlers/vmware-handlers/)). -- **CybORG — partial.** A scenario deploys to sim or emulation and re-randomizes - on reset via a `ScenarioGenerator`, but the boundary is a Python-code - interface, not a declared schema - ([Standen et al. 2021, §3](https://arxiv.org/abs/2108.09118)). -- **CACAO — yes.** `playbook_variables` are authored and `__variable__` - substitution resolves values at execution time - ([CACAO v2.0 §3.1, §10.18](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — oos.** A runtime data-exchange model between running - federates; there is no authored scenario compiled to an instance - ([SISO-STD-025-2023](https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf)). -- **CRACK\* — yes.** CRACK separates the SDL specification from instantiation via - a TOSCA orchestrator that generates deploy scripts; KYPO separates a sandbox - definition from pool allocation - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837); - [Vykopal et al. 2017](https://doi.org/10.5220/0006428203100321)). - -### 5. Backend agnosticism + conformance - -The same scenario realized across multiple backends, with a conformance -contract between definition and backend. - -- **ACES — yes.** A backend-agnostic boundary with a defined backend-conformance - model and conformance profiles - ([backend-conformance.md](../reference/backend-conformance.md), - `contracts/profiles/backend/`, - [ADR-009](../../decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure.md)); - the conformance contract is defined while concrete backend implementations are - still being built. -- **OCR SDL — partial.** Ranger exposes a platform-agnostic gRPC handler - contract, but only a VMware handler ships and no conformance suite was found - ([Ranger docs](https://documentation.opencyberrange.ee/docs/ranger/)). -- **CybORG — partial.** A dual sim+emulation design sits behind one gym - interface, but backend equivalence is asserted, not a published conformance - contract ([Standen et al. 2021, §2–3](https://arxiv.org/abs/2108.09118)). -- **CACAO — partial.** Producer/consumer conformance classes and multi-engine - command types support exchange, though the spec notes playbooks require - per-environment modification and defines no equivalence test suite - ([CACAO v2.0 §11](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — yes.** Independent of, but translatable to, HLA/TENA/DIS/JSON; - the Cyber FOM is an IEEE 1516-compliant HLA FOM inheriting formal federate - conformance - ([SISO Cyber FOM / SIRL](https://www.sisostandards.org/news/690125/Publication-of-Cyber-FOM-and-SIRL-Users-Guide.htm)). -- **CRACK\* — no.** CRACK and KYPO target OpenStack only; CyRIS supports KVM and - AWS but with no cross-backend conformance suite - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). - -### 6. Declarative objectives / workflows - -Objectives and workflow graphs (branching, parallel, joins) as authored -constructs. - -- **ACES — yes.** Declarative objectives (actor-target-window-success, where - success references observable `conditions`) and a workflow graph (decisions, - switch/case, parallel, joins, retries, cancel and timeout, compensation). - Unlike OCR, ACES carries **no** in-SDL scoring chain: the OCR-inherited - `metrics`/`evaluations`/`tlos`/`goals` sections were removed by - [ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md), and - graded scoring/reward lives in the experiment/evaluator plane (ADR-055/064/069) - ([objective-semantics.md](../reference/objective-semantics.md), - [assessment-semantics.md](../reference/assessment-semantics.md), - `specs/formal/objectives/`, `specs/formal/workflows/`). -- **OCR SDL — yes.** A Goals → TLOs → Evaluations → Metrics → Conditions scoring - chain and Stories → Scripts → Events → Injects timelines with parallel - storylines and AND-gated triggers - ([SDL Reference](https://documentation.opencyberrange.ee/docs/sdl/reference/)). -- **CybORG — partial.** Objectives are encoded in reward-calculator classes and - termination conditions computed in code, not declared - ([Standen et al. 2021](https://arxiv.org/abs/2108.09118)). -- **CACAO — yes.** A declarative workflow graph with eight step types - (start, end, action, playbook-action, parallel, if-, while-, switch-condition) - and nested playbook invocation - ([CACAO v2.0 §4](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — no.** No objective or workflow construct; `CyberOrder` is a - command/control directive - ([cyberdem-python](https://github.com/cmu-sei/cyberdem-python)). -- **CRACK\* — yes.** CRACK `Goal` subtypes (CanReach, GainPrivilege, Knows) and - `Invariant` types are first-class declarative constructs verified against the - model ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). - -### 7. Provenance / disclosure surfaces - -Run provenance and evidence, and participant information-boundary / disclosure, -as explicit artifacts. - -- **ACES — yes.** Participant information-boundary projection plus runtime value - redaction and credential-posture classification - ([ADR-056](../../decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md), - [ADR-057](../../decisions/adrs/adr-057-runtime-secret-name-classifier-boundaries.md)), - participant-implementation provenance - ([ADR-041](../../decisions/adrs/adr-041-participant-implementation-manifest-and-provenance.md)), - and experiment-core run records - ([ADR-055](../../decisions/adrs/adr-055-experiment-core-contract-boundary.md)). -- **OCR SDL — partial.** Inject stdout/stderr capture and timestamped per-entity - events provide runtime evidence, but the SDL has no first-class - provenance/disclosure construct - ([Ranger / Executor docs](https://documentation.opencyberrange.ee/docs/ranger/)). -- **CybORG — no.** Observation filtering is an RL mechanism, not a declared - disclosure or provenance artifact - ([Standen et al. 2021, §2](https://arxiv.org/abs/2108.09118)). -- **CACAO — yes.** First-class digital signatures (JSON Signature Scheme, - quantum-safe option) and TLP/IEP/statement data markings on all objects - ([CACAO v2.0 §2.4, §2.5](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — partial.** A `SensitivityType` classification and - data-state attributes track information state, but there is no participant - disclosure-boundary or evidence artifact - ([cyberdem-python](https://github.com/cmu-sei/cyberdem-python)). -- **CRACK\* — no.** CRACK `Knows` is a verification predicate, not a provenance - artifact; KYPO collects observational event logs only (partial), not an - authored construct - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837); - [KYPO dataset, PMC10770710](https://pmc.ncbi.nlm.nih.gov/articles/PMC10770710/)). - -### 8. Time semantics status - -Clock authority, logical or virtual time, ordering, causality, and -pacing/synchronization. - -- **ACES — partial.** ACES separates timestamp, ordering, clock authority, - pacing, and causality at the lineage level and is materializing the authoring - surface (the - [Runtime, Time, And Causality](lineage.md#runtime-time-and-causality) - section; [SEM-213 temporal-participant preflight](../../decisions/sem-213-temporal-participant-preflight.md)). - The full time/clock authoring model is **not complete**. -- **OCR SDL — partial.** Relative script offsets and `speed` multipliers provide - narrative pacing, with no clock authority or causality - ([SDL Reference](https://documentation.opencyberrange.ee/docs/sdl/reference/)). -- **CybORG — partial.** Discrete steps with fixed agent order, and variable - action durations in CAGE 4, but no clock-authority or causality specification - ([Standen et al. 2021, §2](https://arxiv.org/abs/2108.09118)). -- **CACAO — partial.** Step `delay`/`timeout` and playbook - `valid_from`/`valid_until`, with no clock-authority or causal-ordering - semantics - ([CACAO v2.0 §3.1, §4.1](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Cyber DEM/FOM — yes.** Event timestamps and durations plus the inherited HLA - time-management stack (Time Stamp Order delivery, lookahead, GALT, - time-constrained/regulating roles) - ([SISO Cyber FOM](https://www.sisostandards.org/news/690125/Publication-of-Cyber-FOM-and-SIRL-Users-Guide.htm); - [IEEE 1516](https://standards.ieee.org/ieee/1516/3744/)). -- **CRACK\* — no.** CRACK's Datalog encoding is atemporal; KYPO records - timestamps for analysis (partial) but not as a scenario-language semantic - ([Russo et al. 2018, IEEE NCA](https://doi.org/10.1109/NCA.2018.8548324); - [KYPO dataset, PMC10770710](https://pmc.ncbi.nlm.nih.gov/articles/PMC10770710/)). - -## Where Precedents Lead ACES - -The matrix is not one-directional. Several precedents are more mature than ACES -on dimensions they were built for, and the documentation states this directly. - -- **Time semantics and federated time management — Cyber DEM/FOM, TENA, HLA.** - The Cyber FOM inherits IEEE 1516 HLA time management: Time Stamp Order - delivery, lookahead, GALT, and conservative/optimistic execution with - distributed causality. ACES cites this literature but its time-authoring - surface is partial and explicitly incomplete - ([IEEE 1516](https://standards.ieee.org/ieee/1516/3744/)). -- **Standardization and federation interoperability — Cyber DEM/FOM, TENA, HLA.** - SISO-STD-025-2023 is an approved multi-vendor standard with HLA-conformant - federation and canonical mappings to HLA/TENA/DIS/JSON. ACES has a defined - conformance model but no equivalent standards body, conformance authority, or - multi-vendor implementor community - ([SISO-STD-025-2023](https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf)). -- **Declarative workflow taxonomy and signed provenance — CACAO.** CACAO ships an - OASIS-standardized eight-step workflow taxonomy with conditional and looping - steps, plus first-class digital signatures and TLP/IEP data markings — a - signed-provenance mechanism ACES does not have - ([CACAO v2.0](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/security-playbooks-v2.0.html)). -- **Executed RL episode discipline — CybORG.** CybORG's per-agent observation, - reward, partial-observability, and multi-agent episode machinery is a working, - iterated implementation; ACES specifies participant semantics formally but the - executed episode runtime is still materializing - ([Standen et al. 2021](https://arxiv.org/abs/2108.09118)). -- **Formal scenario verification — CRACK.** CRACK encodes scenarios into Datalog, - checks them against validation goals, and turns verification traces into - runtime conformance tests. ACES's SMT/formal scenario verification is deferred - (see [precedents.md](precedents.md), "VSDL SMT verification") - ([Russo et al. 2020](https://doi.org/10.1016/j.cose.2020.101837)). -- **Operational training-runtime maturity — OCR SDL.** Open Cyber Range ships a - deployed scoring and narrative-orchestration runtime (Ranger, VMware handlers, - Deputy package library, participant UI); ACES's comparable runtime is younger - ([Open Cyber Range docs](https://documentation.opencyberrange.ee/docs/)). - -## Where ACES Leads - -ACES's distinguishing contributions, against this set, are the depth of -first-class **runtime inventory** (dimension 1) and the consistent **separation -of authored meaning, instantiation, backend realization, participant -implementations, runtime state, and evidence** (dimensions 4, 5, 7). No system -in this comparison models observed runtime node state — identity authorities, -datastores, detection engines, application RBAC stores, and similar — as a -typed, redaction-aware inventory distinct from authored topology. Those gains are -expressivity gains in the authoring and runtime-modeling layer; they do not -extend to the federated time management, standardization, or formal-verification -maturity the precedents above hold. - -## Precedents Scoped Out Of The Matrix - -These README lineage precedents are not comparison columns -because their purpose is not scenario authoring or runtime scenario modeling; -each is accounted for here so coverage is complete. - -- **OCSF** — a normalized security event/finding schema. ACES borrows its - observation/evidence style; it is not a scenario language - (). -- **STIX 2.1** — a cyber-threat-intelligence object/relationship model. ACES - adapts its typed-relationship pattern; STIX models threat intelligence, not - scenarios - (). -- **TENA** — runtime test-range integration middleware, not an authoring DSL; its - federation maturity is reflected in dimensions 5 and 8 - (). -- **IEEE HLA (1516)** — a distributed-simulation interoperability architecture - and the substrate the Cyber FOM builds on, not an authoring DSL; represented - through the Cyber DEM/FOM column - (). -- **MITRE CALDERA** — an adversary-emulation execution platform scenarios may - bind to, not a scenario DSL (). -- **Atomic Red Team** — a library of ATT&CK-mapped atomic test definitions; - test-execution content, not a scenario DSL - (). - -## References - -- [Related-work comparison research notes](../../research/related-work-comparison/index.md) - and [search log](../../research/related-work-comparison/search-log.md) — the - primary sources and grounded findings behind every non-ACES cell. -- [Design Precedents](precedents.md) — element-level source mapping. -- [Lineage and Prior Work](lineage.md) — narrative source map. -- [Documentation Style Guide](../reference/documentation-style-guide.md) — the - accuracy-before-persuasion and citation rules this page follows. +The frozen comparison supports no overall winner. ACES has the strongest +expressive-breadth result inside the scenario-authoring stratum, while VSDL and +CRACK have stronger formal-analysis evidence, OCR SDL has stronger deployed +authoring maturity evidence, CybORG has stronger executed participant-modeling +evidence, and CACAO and Cyber FOM have stronger standards governance in their +respective scopes. + +This result is a reproducible research snapshot, not a marketing ranking. It +distinguishes breadth from quality, implementation maturity, standardization, +and community governance. It also retains ACES features that are partial, +missing, external to SDL, or deliberately excluded. + +## Reproduction Bundle + +The comparison is split into independently revisioned artifacts: + +- [`protocol-v1.json`](../../research/related-work-comparison/protocol-v1.json) + pre-registers the corpus, scope strata, twelve axes, rubrics, authoring tasks, + negative cases, source rules, and analysis rules. +- [`extraction-snapshot-2026-07-13.json`](../../research/related-work-comparison/extraction-snapshot-2026-07-13.json) + freezes every source identity and records every system-axis cell and + system-case walkthrough with exact primary-source locators. +- [`analysis-v1.json`](../../research/related-work-comparison/analysis-v1.json) + records scope-qualified Pareto frontiers, predeclared weight profiles, ranking + reversals, and bounded public claims. + +The systems remain independent observations. Cyber DEM is not merged with +Cyber FOM, and CRACK is not merged with VSDL. Systems with unlike purposes stay +in separate scope strata; `out of scope` is not treated as a zero. + +## Evidence Matrix And Findings + +The block below is generated from the frozen artifacts and checked by +`tools/check_related_work_comparison.py`. Each ordinal level follows its +axis-specific rubric. It is not an interval measurement and cannot be summed +into a universal quality score. + + +Frozen snapshot: `snapshot-2026-07-13` under protocol `protocol-v1`. +Scores are axis-specific ordinal evidence levels: 0 absent, 1 limited, 2 substantial, 3 strong; +`oos` means the axis is outside the system's declared scope and is never treated as zero. + +| Axis | ACES | OCR SDL | CybORG | CACAO 2.0 | Cyber DEM | Cyber FOM | CRACK | VSDL | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Expressive breadth | 3 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | +| Semantic precision | 3 | 1 | 2 | 3 | 3 | 3 | 3 | 3 | +| Formal analyzability | 1 | 0 | 1 | 1 | 1 | 2 | 3 | 3 | +| Concrete-syntax soundness | 3 | 2 | 2 | 3 | 2 | 3 | 2 | 2 | +| Composition and versioning | 3 | 1 | 2 | 3 | 2 | 3 | 2 | 3 | +| Experiment design | 2 | 1 | 3 | oos | oos | oos | 0 | 0 | +| Participant modeling | 2 | 1 | 3 | oos | oos | oos | 1 | 1 | +| Provenance and evidence | 3 | 1 | 1 | 3 | 1 | 1 | 0 | 0 | +| Interoperability | 2 | 1 | 2 | 3 | 3 | 3 | 0 | 0 | +| Usability | 1 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | +| Implementation maturity | 1 | 2 | 3 | 3 | 2 | 3 | 1 | 1 | +| Governance and community | 1 | 1 | 2 | 3 | 3 | 3 | 0 | 0 | + +### Evidence-bounded findings + +- **No Overall Winner.** Evidence status: `partial`. No overall winner is supported: the four declared scope strata are analyzed separately, and the scenario-authoring first-ranked system changes across the four declared weight profiles. +- **Scope Qualified Breadth.** Evidence status: `partial`. Within the four-system scenario-authoring stratum and this frozen rubric, ACES is the sole expressive-breadth leader at level 3. This is the broadest combined surface observed in this corpus; `highest quality` is not supported, and standardization or maturity do not follow from breadth. +- **Maturity Governance.** Evidence status: `partial`. Against ACES's recorded level 1, CACAO 2.0 and Cyber FOM each record level 3 for governance and community, while CybORG records level 3 and OCR SDL level 2 for implementation maturity. These are axis-specific evidence comparisons, not cross-scope overall rankings or adoption claims. +- **Sensitivity.** Evidence status: `partial`. Across the declared scenario-authoring profiles, breadth and composition ranks ACES first, formal rigor ranks VSDL first, and maturity and governance ranks OCR SDL first; this observed reversal prohibits a weight-independent winner claim. + +### Sensitivity of scenario-authoring rankings + +| Weight profile | First-ranked system | Recorded totals | +| --- | --- | --- | +| `equal-evidence` | ACES | ACES=25, VSDL=16, CRACK=15, OCR SDL=15 | +| `breadth-and-composition` | ACES | ACES=42, OCR SDL=21, VSDL=21, CRACK=18 | +| `formal-rigor` | VSDL | VSDL=39, CRACK=37, ACES=32, OCR SDL=12 | +| `maturity-and-governance` | OCR SDL | OCR SDL=26, ACES=22, CRACK=10, VSDL=10 | + +### ACES delivery limits retained in the matrix + +- **Formal analyzability (1).** Full solver-backed whole-scenario verification remains outside the current implementation. +- **Experiment design (2).** Controlled allocation and factor/study concerns live in experiment contracts, and several delivery rows remain external or incomplete. +- **Participant modeling (2).** Participant budgets, reference trajectories, hidden benchmark assets, and verifier/adjudication remain missing or partial. +- **Interoperability (2).** Published profiles exist, but independent multi-vendor conformance and substitution evidence remains limited. +- **Usability (1).** No independent author population, completion-time, effort, or error study is frozen in this corpus. +- **Implementation maturity (1).** The delivery assessment retains partial, missing, external-contract, and deliberately-excluded concerns; formal prose is not counted as shipped behavior. +- **Governance and community (1).** Governance is explicit but project-led; standardization and independent adoption are not claimed. + +Cell rationales, exact source locators, task walkthroughs, and source digests are in the +[frozen extraction snapshot](../../research/related-work-comparison/extraction-snapshot-2026-07-13.json). + + +## Interpretation Boundaries + +The matrix is only one view over the extraction snapshot. A level of `3` means +strong evidence against that axis's own rubric; it does not mean that the +system is better overall. `Usability` remains at most `2` because this corpus +contains documentation and source walkthroughs, not an independent author +study with disclosed population, effort, completion, and error measurements. + +The sensitivity table deliberately shows reversals. Weighting formal rigor +places VSDL first; weighting maturity and governance places OCR SDL first; +weighting breadth and composition places ACES first. Selecting a weighting +after seeing those results would violate the protocol. + +The source walkthroughs do not execute third-party code. Normal verification +is offline and checks the archived metadata, digests, evidence references, +rectangular coverage, recomputed analysis, and this page's generated block. + +## Reproduce The Checks + +From the repository root: + +```bash +implementations/python/.venv/bin/python tools/check_related_work_comparison.py +implementations/python/.venv/bin/python -m pytest \ + implementations/python/tests/test_related_work_comparison.py +``` + +The research [index](../../research/related-work-comparison/index.md) describes +the extraction workflow. The [search log](../../research/related-work-comparison/search-log.md) +records the frozen source set and retrieval decisions. + +## Related Material + +- [Design Precedents](precedents.md) maps individual ACES concepts to lineage. +- [Lineage and Prior Work](lineage.md) gives the narrative source map. +- [Scientific Scenario Completeness](scientific-scenario-completeness.md) + separates intended-use profiles from current delivery status. +- [Documentation Style Guide](../reference/documentation-style-guide.md) + defines the accuracy, citation, and current-state rules used here. diff --git a/docs/explain/sdl/scientific-scenario-completeness.md b/docs/explain/sdl/scientific-scenario-completeness.md index 7f1a6f323..f4ca455d9 100644 --- a/docs/explain/sdl/scientific-scenario-completeness.md +++ b/docs/explain/sdl/scientific-scenario-completeness.md @@ -14,7 +14,9 @@ The current assessment is deliberately conservative. Only `valid-sdl-fragment` is complete. The four stronger profiles expose their blocking concerns directly, including authored/observed-state binding, specificity, teardown, credentials, time and clocks, participant budgets, -verifiers, hidden assets, trajectories, and behavioral-relation semantics. +verifiers, hidden assets, and trajectories. Behavioral-relation semantics are +now implemented as `aces-behavioral-relations@rev1`, while the stronger formal +relations it defines retain their honest unproved or future assurance states. These profiles are scope contracts, not validators that silently strengthen ordinary SDL parsing. They also do not certify a backend, an experiment result, diff --git a/docs/explain/sdl/sections.md b/docs/explain/sdl/sections.md index 85af6540a..a6726efb5 100644 --- a/docs/explain/sdl/sections.md +++ b/docs/explain/sdl/sections.md @@ -25,14 +25,14 @@ The OCR scoring pipeline sections (`metrics`, `evaluations`, `tlos`, `goals`) were removed from the SDL by [ADR-073](../../decisions/adrs/adr-073-scoring-reward-language-scope.md); graded scoring, reward, and evaluation outputs now live in the experiment/evaluator -plane (ADR-055/064/069). `conditions` (observable state) remain. +plane (ADR-055/064/069). Declarative `conditions` remain. | Section | Type | Purpose | |---------|------|---------| | `nodes` | `dict[str, Node]` | VMs and network switches — the compute/network topology | | `infrastructure` | `dict[str, InfraNode]` | Deployment topology: counts, links, dependencies, IP/CIDR, ACLs | | `features` | `dict[str, Feature]` | Software (Service/Configuration/Artifact) deployed to VMs | -| `conditions` | `dict[str, Condition]` | Health checks (command+interval or library source) — observable state | +| `conditions` | `dict[str, Condition]` | Declarative health/readiness checks (command+interval or library source) | | `vulnerabilities` | `dict[str, Vulnerability]` | CWE-classified vulnerabilities assigned to nodes/features | | `entities` | `dict[str, Entity]` | Teams, organizations, people (recursive, with exercise roles) | | `injects` | `dict[str, Inject]` | Actions between entities during exercises | @@ -88,7 +88,7 @@ nodes: operator: # longhand username: ops entities: [blue-team.alice] # binds to entity - services: # exposed network services + services: # authored node-local transport bindings - port: 80 protocol: tcp name: http @@ -201,14 +201,6 @@ nodes: dns_options: [ndots:0] dns_search: [techvault.local] group_add: [adm, "101"] - health: - status: healthy - failing_streak: 0 - log: - - start: "2026-05-20T12:00:00Z" - end: "2026-05-20T12:00:01Z" - exit_code: 0 - output: ok packages: - manager: apk name: musl @@ -218,7 +210,7 @@ nodes: name: shuffle-backend version: 1.2.3 component_type: application - provenance: scanner + provenance: package-manager ecosystem: go purl: "pkg:golang/github.com/frikky/shuffle@1.2.3" cpe: "cpe:2.3:a:shuffle:shuffle:1.2.3:*:*:*:*:*:*:*" @@ -234,15 +226,6 @@ nodes: - ecosystem: go path: /app/go.mod format: go-module - package_vulnerabilities: - - id: CVE-2026-12345 - package_name: musl - installed_version: 1.2.4-r2 - fixed_version: 1.2.5-r0 - severity: high - scanner: trivy - image_digest: sha256:abc123 - scan_time: "2026-05-20T12:00:00Z" local_identity: # observed /etc/passwd, /etc/group, sudo facts users: - username: www-data @@ -265,23 +248,17 @@ nodes: run_as_users: [root] commands: ["/usr/bin/systemctl restart wazuh-agent"] nopasswd: true - network: # observed container network realization + network: # deliberately required runtime network state hostname: techvault-webapp domainname: techvault.local endpoints: - network: aptl-dmz # references a switch-backed infrastructure entry - network_id: 7f2c1ad4e9b3... - network_id_stability: stable - endpoint_id: 3a9c7e0d3f5b... - endpoint_id_stability: ephemeral - backend_generated: true ip_address: 172.20.0.20 ip_prefix_length: 24 gateway: 172.20.0.1 mac_address: 02:42:ac:14:00:14 aliases: [aptl-webapp, webapp] dns_names: [aptl-webapp, webapp] - generated_dns_names: [3a9c7e0d3f5b] backend: driver: bridge ipam_driver: default @@ -747,17 +724,33 @@ When `features`, `conditions`, or `injects` use the `{name: role}` form, the rol Concrete service bindings on a VM must be unique by `protocol` + `port`. Reusing `53/tcp` and `53/udp` is valid; declaring `443/tcp` twice on the same node is rejected. If a service binding also has a `name`, that `name` must be unique within the node and can be targeted directly as `nodes..services.`. -`runtime` captures observed facts about realized VM/container nodes. It covers -participant-observable and analysis-relevant runtime state that is distinct -from authored deployment intent and top-level authored declarations such as -feature placement or service bindings; it does not exclude host-published -bindings, application routes, daemon policy, databases, identity authorities, -DNS service logical state, security-monitoring manager inventory, or other -participant-interactable state merely because the evidence came from Docker, -Compose, a scanner, or a backend inspector. Mounts describe realized -filesystem attachments, including filesystem type, propagation, stability, -whether a backend generated the source, and sensitivity classifications for -the source and option strings. Mount sources or options classified as +A `services` entry identifies an authored node-local transport binding. It does +not authorize any source to reach the port, prove a live listener, publish a +host port, or classify an `internal`/`external` audience. Traffic authorization +is declared separately through `infrastructure.*.acls`; observed bind state is +recorded in `runtime.service_listeners`, and host publication is recorded in +`runtime.network.published_ports`. Service entries are closed-world objects, so +an unmodeled field such as `role` is rejected rather than interpreted as policy +or silently dropped. + +`runtime` is authored declarative contract state for VM/container nodes. Every +field present there requires exact state, constrains acceptable state, or marks +an explicitly open realization point. A value does not become an SDL +requirement merely because Docker, a scanner, or a participant-visible probe +reported it. Captured facts stay in a source evidence bundle and +`ExperimentEvidenceRecordModel`; an author must deliberately promote a fact to +the smallest semantically correct SDL field before it can affect compilation. +Model defaults are likewise not proof of author declaration: explicitness is +tracked through `model_fields_set` and SEM-218, not inferred from +`model_dump()` output. + +Some historical runtime type and field names describe the observation that +motivated the surface. Their carrier semantics are nevertheless declarative: +presence in authored `Node.runtime` makes the value contract state. Mounts +describe required filesystem attachments, including filesystem type, +propagation, stability, whether a backend may generate the source, and +sensitivity classifications for the source and option strings. Mount sources +or options classified as `redacted` or `operator_secret` must omit the raw value. This sensitivity vocabulary is an ACES runtime contract, not an adopted taxonomy from Docker, Compose, or the cited scenario-language precedents. `filesystem_inventory` records @@ -780,20 +773,26 @@ posture — `default`, `unconfined`, a named profile, or a profile path) and such as `seccomp:unconfined` or `no-new-privileges`); a seccomp posture is a distinct security control from `privileged`, so it is recorded separately (see [ADR-028](../../decisions/adrs/adr-028-container-seccomp-security-options-surface.md)); -`health` records observed health status and bounded healthcheck log facts; `packages` records package-manager rows; `software_components` records node-local software identity at component granularity with stable ACES ids, component type, version, purl/CPE/hash identifiers, package or manifest -lineage, and runtime paths when known; `dependency_manifests` records observed -manifest files; and `package_vulnerabilities` records scanner-derived -CVE/advisory findings tied to an image digest and scan time. Software -components are WHAT-IS state, not invocation surfaces, process snapshots, HTTP -route inventory, build provenance, or authored deployment intent (see +lineage, and runtime paths when required; and `dependency_manifests` records +required manifest files. Software components are required final state, not +invocation surfaces, process snapshots, HTTP route inventory, or scanner +capture method (see [ADR-056](../../decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md) for the cross-surface observed-value and credential-posture inventory, and [ADR-034](../../decisions/adrs/adr-034-runtime-software-component-inventory.md)). -Package findings are separate from the top-level `vulnerabilities` section, -which remains the CWE-classified scenario vulnerability surface. + +Container health results are evidence, not `runtime` fields. Put the authored +healthcheck definition in `conditions`, bind it through `Node.conditions`, and +record status, failing streak, timestamps, exit codes, and output separately; +the valid portable fixture +[`runtime-health-observation.json`](../../../contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/runtime-health-observation.json) +shows that carrier. Scanner identity/version/database, scan time, raw findings, +and advisory snapshot state likewise belong in evidence. Derived severity +counts belong in `ExperimentDerivedMeasureModel`; they do not automatically +become top-level authored `vulnerabilities`. `runtime.service_manager_units` records observed service-manager unit lifecycle state — what `systemctl` exposes from inside a realized range node. @@ -844,22 +843,16 @@ distinct from the top-level `accounts` provisioning surface, and service accounts recorded here are not implicitly compiled into account placements (see [ADR-024](../../decisions/adrs/adr-024-local-identity-inventory-surface.md)). -`runtime.network` records the observed container network realization — the -facts visible from inside the realized range or by a harness, distinct from the -`infrastructure` topology declaration. `hostname` and `domainname` are the -container's network identity. Each `endpoints` entry is a per-network -attachment: `network` references a declared switch-backed infrastructure entry, -and the entry carries the realized `ip_address`, `ip_prefix_length`, `gateway`, -and `mac_address`; backend `network_id`/`endpoint_id` each with an explicit -`stable`/`ephemeral` stability classification; a `backend_generated` flag; and -three distinct name lists — stable per-network `aliases`, observed `dns_names`, -and backend-`generated_dns_names` (such as a container-ID-prefixed DNS name, -which is not stable scenario identity). The optional `backend` block records -observable network `driver` and `ipam_driver` plus bounded backend-native -`driver_options`/`ipam_options` maps — not raw engine inspect payloads. -`published_ports` records host-published bindings, keeping container port, host -IP, host port, and protocol distinct; this is host exposure observed at -runtime, separate from the authored `services` declaration and image-default +`runtime.network` records deliberately required container network state, +distinct from the `infrastructure` topology declaration. `hostname`, +`domainname`, aliases/DNS names, addresses, prefix, gateway, MAC, backend +configuration, and published bindings are contract facts when authored. +Docker network IDs, endpoint IDs, generated DNS names, and incidental inspect +output are not accepted by the SDL schema; preserve them in evidence and the +inventory mapping ledger instead. The valid portable fixture +[`docker-network-endpoint-observation.json`](../../../contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/docker-network-endpoint-observation.json) +demonstrates the evidence side. `published_ports` keeps container port, host IP, +host port, and protocol distinct from the authored `services` declaration and image-default `source.build.config.exposed_ports` (see [ADR-025](../../decisions/adrs/adr-025-container-network-realization-surface.md)). diff --git a/docs/explain/sdl/validation.md b/docs/explain/sdl/validation.md index b5464130f..47f1f565c 100644 --- a/docs/explain/sdl/validation.md +++ b/docs/explain/sdl/validation.md @@ -84,8 +84,7 @@ Runtime software components must have stable, concrete `component_id` values that are unique within the node runtime block. Runtime filesystem inventory UID/GID and size fields are non-negative, mode is stored as octal permission bits, and content digests must carry both the digest -algorithm and value. Runtime healthcheck entries marked as redacted must omit -raw output. Runtime mount sources/options and local-control bind sources +algorithm and value. Runtime mount sources/options and local-control bind sources classified as `redacted` or `operator_secret` must omit the corresponding raw value; the Python models and generated JSON Schemas both reject non-empty raw values for redacted/operator-secret labels accepted by the value-normalization @@ -389,11 +388,13 @@ evidence refs in event evidence, and participant-episode status sources in terminal participant-episode history for the same participant and episode. This also means the validator only enforces what the current SDL syntax can -actually express. Node `runtime` metadata covers observed VM configuration -facts such as mounts, path-local control interfaces, process identity, runtime -filesystem inventory, container host/security configuration, health observations, -package inventory, software component identity, dependency manifests, and -scanner-derived package findings. +actually express. Node `runtime` is declarative contract state. The closed +model rejects observation-only health results, generated network/endpoint +identifiers and generated DNS identity, scanner capture provenance, and +scanner-derived package findings. Those facts use experiment evidence records, +inventory ledgers, and derived measures instead. Packages, software components, +dependency manifests, addresses, aliases, MACs, and backend details remain SDL +only when an author deliberately requires them. The `source.build` block covers observed container image build provenance: base image and digest, layer chain, structured build-recipe instructions, build arguments, copied sources, image-default configuration, source-input mapping, diff --git a/docs/index.md b/docs/index.md index 5c453bdec..1e004fb9a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -145,6 +145,7 @@ research/scoring-scope/index research/validation-admission-profiles/index research/primary/index research/lineage/source-audit-2026-07-12 +research/behavioral-relations/conflation-audit-2026-07-13 research/related-work-comparison/index research/participant-backend-contracts/index ``` diff --git a/docs/research/behavioral-relations/conflation-audit-2026-07-13.md b/docs/research/behavioral-relations/conflation-audit-2026-07-13.md new file mode 100644 index 000000000..c1f54a428 --- /dev/null +++ b/docs/research/behavioral-relations/conflation-audit-2026-07-13.md @@ -0,0 +1,75 @@ +# Behavioral-Relation Conflation Audit + +Date: 2026-07-13 + +Issue: #747 + +Taxonomy coordinate: `aces-behavioral-relations@rev1` + +## Method + +The audit inspected live claim-bearing prose, contracts, reports, examples, and +implementation strings under `docs/`, `specs/`, `examples/`, +`contracts/profiles/`, `contracts/fixtures/`, and +`implementations/python/packages/`. It searched for equivalence, +bisimulation, refinement, participant-history, conformance, identity, profile, +and empirical-adequacy language, then classified each occurrence by subject, +relation, quantifier, evidence boundary, and assurance state. + +Generated schemas, tests containing seeded counterexamples, planning preflight +records, and timestamped `tools/real-daemon/evidence/` captures are not live +claim surfaces. The archived captures remain immutable evidence of what an old +run emitted; their legacy terminology is recorded below instead of rewritten. + +## Results + +| Surface | Previous risk | Revision-1 classification and boundary | Result | +| --- | --- | --- | --- | +| SDL schema and semantic validation | “valid” could be read as executable or conformant | `structural-validity` and `semantic-validity`; one artifact and named profile only | Bound in the taxonomy and scientific completeness REV1 | +| Normalize/expand/instantiate/canonicalize phases | Determinism and digest equality could be read as refinement or same behavior | `canonical-artifact-identity` plus phase invariants; no simulation, refinement, trace, or bisimulation proof | Existing nonclaims retained; catalog claim surface added | +| Realization envelopes | Admission could be read as successful realization | `realization-envelope-membership` or `realization-envelope-subsumption`; no execution result | Catalog separates set relations from runtime behavior | +| Backend fixture and target conformance | “conformance” did not expose finite quantification | `bounded-probe-success`; report enumerates cases, projection revision, limitations, and nonclaims | Implemented in `BackendConformanceReport` and CLI JSON | +| Backend realization obligation | “refines this design” asserted an unnamed universal relation | Intended `trace-inclusion` under a named participant projection; definition present, proof deliberately unproved | Normative participant-runtime prose corrected | +| Libvirt terminal participant record | The legacy `behavior-history-equivalent` label is not a current relation claim; it conflated one projected record with a comparison | Single `participant-projected-history` record with observation policy, redaction, ordering, simultaneity, and explicit nonclaimed relation ids | Artifact producer and example corrected | +| Cross-backend corpus coverage | Coverage note repeated the same legacy equivalence label | Bounded participant-projected record; explicitly no equivalence comparison | Producer corrected | +| Participant-history comparisons | Projection assumptions could remain implicit | `participant-projected-history-equivalence`; same participant and projection revision required | Defined and binding-enforced | +| Multi-agent records | Joint actions or shared results could be read as strategic equivalence | Current evidence is structural/finite; `alternating-strategic-equivalence` and `probabilistic-bisimulation` are future and unproved | Catalog makes the missing game/probability obligations explicit | +| Scientific completeness REV1 | The behavioral-relation concern existed but had no executable authority | Every profile binds its intended relation and enumerates nonclaimed relation ids | Concern moved from missing to implemented with executable evidence | +| Study and benchmark records | A study conclusion could omit relation and population boundary | Claim-bearing studies require revisioned claim bindings; empirical claims state population, projection/measurement plan, scope, evidence, and limitations | Implemented in `ExperimentStudyModel` | +| Independent studies under #729 | Planned results could be promoted to universal conformance or equivalence | `empirical-adequacy`, `statistical-similarity`, or `statistical-equivalence` only as preregistered and bounded | Contract seam is ready; results remain future evidence | +| Proposition/assertion result equality | Equal evaluator projections could be read as bisimulation | Explicit nonclaim already states the missing labelled-transition and matching obligations | No semantic change required | +| Historical real-daemon capture | Archived JSON contains `behavior-history-equivalent`, which is not a current relation claim | Historical, timestamped evidence; not a current authority or producer | Preserved unchanged and excluded from the live gate | + +## Assurance Summary + +Implemented and bounded-tested now: + +- validity, declaration, profile, envelope, canonical-identity, and finite-probe + relations; +- revisioned claim-binding validation; +- conformance-report disclosure; +- participant-projection metadata; and +- semantic policy enforcement. + +Defined but deliberately unproved or future: + +- universal backend `trace-inclusion`; +- trace equivalence, simulation, data refinement, and strong/weak bisimulation; +- epistemic and alternating strategic relations; +- probabilistic, timed, and partial-order relations; and +- empirical or statistical conclusions not yet supported by a completed, + preregistered study. + +## Gate Behavior + +`tools/check_behavioral_relation_claims.py` is intentionally not a forbidden-word +list. Definitions and explicit nonclaims are permitted. A high-confidence +positive assertion is permitted when the surrounding claim identifies the +governed relation and an evidence boundary. Structured bindings are parsed and +validated against the canonical catalog, so unknown relation ids, revision +drift, missing projections, and finite-to-universal promotion fail directly. + +The executable counterexamples demonstrate why this distinction matters: a +shared finite trace can pass while an unmatched branch refutes strong +bisimulation, and a hidden prefix can preserve one visible trace while strong +matching fails. These are evidence-boundary failures, not vocabulary failures. diff --git a/docs/research/related-work-comparison/analysis-v1.json b/docs/research/related-work-comparison/analysis-v1.json new file mode 100644 index 000000000..6e8e24b50 --- /dev/null +++ b/docs/research/related-work-comparison/analysis-v1.json @@ -0,0 +1,536 @@ +{ + "analysis_id": "analysis-v1", + "protocol_revision": "protocol-v1", + "snapshot_id": "snapshot-2026-07-13", + "generated_at": "2026-07-13T05:40:00Z", + "score_method": "Axis-specific 0..3 ordinal evidence levels; out-of-scope values are excluded. Pareto analysis is within scope strata. Weight profiles use recorded integer weights and retain all reversals.", + "pareto_groups": [ + { + "group_id": "scenario-authoring-pareto", + "scope_stratum": "scenario-authoring", + "system_ids": [ + "aces", + "ocr-sdl", + "crack", + "vsdl" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "experiment-design", + "participant-modeling", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "frontier_system_ids": [ + "aces", + "ocr-sdl", + "vsdl" + ] + }, + { + "group_id": "agent-simulation-pareto", + "scope_stratum": "agent-simulation", + "system_ids": [ + "cyborg" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "experiment-design", + "participant-modeling", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "frontier_system_ids": [ + "cyborg" + ] + }, + { + "group_id": "playbook-orchestration-pareto", + "scope_stratum": "playbook-orchestration", + "system_ids": [ + "cacao-v2" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "frontier_system_ids": [ + "cacao-v2" + ] + }, + { + "group_id": "federation-exchange-pareto", + "scope_stratum": "federation-exchange", + "system_ids": [ + "cyber-dem", + "cyber-fom" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "frontier_system_ids": [ + "cyber-fom" + ] + } + ], + "weight_profiles": [ + { + "profile_id": "equal-evidence", + "scope_stratum": "scenario-authoring", + "weights": { + "expressive-breadth": 1, + "semantic-precision": 1, + "formal-analyzability": 1, + "concrete-syntax-soundness": 1, + "composition-versioning": 1, + "experiment-design": 1, + "participant-modeling": 1, + "provenance-evidence": 1, + "interoperability": 1, + "usability": 1, + "implementation-maturity": 1, + "governance-community": 1 + }, + "totals": { + "aces": 25, + "crack": 15, + "ocr-sdl": 15, + "vsdl": 16 + }, + "ranking": [ + "aces", + "vsdl", + "crack", + "ocr-sdl" + ] + }, + { + "profile_id": "breadth-and-composition", + "scope_stratum": "scenario-authoring", + "weights": { + "expressive-breadth": 5, + "semantic-precision": 0, + "formal-analyzability": 0, + "concrete-syntax-soundness": 0, + "composition-versioning": 3, + "experiment-design": 3, + "participant-modeling": 2, + "provenance-evidence": 2, + "interoperability": 1, + "usability": 0, + "implementation-maturity": 0, + "governance-community": 0 + }, + "totals": { + "aces": 42, + "crack": 18, + "ocr-sdl": 21, + "vsdl": 21 + }, + "ranking": [ + "aces", + "ocr-sdl", + "vsdl", + "crack" + ] + }, + { + "profile_id": "formal-rigor", + "scope_stratum": "scenario-authoring", + "weights": { + "expressive-breadth": 0, + "semantic-precision": 4, + "formal-analyzability": 5, + "concrete-syntax-soundness": 3, + "composition-versioning": 2, + "experiment-design": 0, + "participant-modeling": 0, + "provenance-evidence": 0, + "interoperability": 0, + "usability": 0, + "implementation-maturity": 0, + "governance-community": 0 + }, + "totals": { + "aces": 32, + "crack": 37, + "ocr-sdl": 12, + "vsdl": 39 + }, + "ranking": [ + "vsdl", + "crack", + "aces", + "ocr-sdl" + ] + }, + { + "profile_id": "maturity-and-governance", + "scope_stratum": "scenario-authoring", + "weights": { + "expressive-breadth": 0, + "semantic-precision": 0, + "formal-analyzability": 0, + "concrete-syntax-soundness": 1, + "composition-versioning": 0, + "experiment-design": 0, + "participant-modeling": 0, + "provenance-evidence": 0, + "interoperability": 3, + "usability": 3, + "implementation-maturity": 5, + "governance-community": 5 + }, + "totals": { + "aces": 22, + "crack": 10, + "ocr-sdl": 26, + "vsdl": 10 + }, + "ranking": [ + "ocr-sdl", + "aces", + "crack", + "vsdl" + ] + } + ], + "sensitivity": { + "profile_ids": [ + "equal-evidence", + "breadth-and-composition", + "formal-rigor", + "maturity-and-governance" + ], + "ranking_reversal_observed": true, + "winning_systems": [ + "aces", + "ocr-sdl", + "vsdl" + ], + "disclosure": "The first-ranked scenario-authoring system changes across reasonable predeclared profiles, so no weight-independent total-order headline is permitted." + }, + "claims": [ + { + "claim_id": "no-overall-winner", + "kind": "no-overall-winner", + "statement": "No overall winner is supported: the four declared scope strata are analyzed separately, and the scenario-authoring first-ranked system changes across the four declared weight profiles.", + "system_ids": [ + "aces", + "ocr-sdl", + "crack", + "vsdl", + "cyborg", + "cacao-v2", + "cyber-dem", + "cyber-fom" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "experiment-design", + "participant-modeling", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "evidence_status": "partial", + "threats_to_validity": [ + "The four declared scope strata and selected systems do not exhaust every cyber-range or orchestration comparison frame.", + "The ordinal evidence levels remain dependent on the frozen rubric and on primary sources available at the pinned revisions." + ], + "falsification": { + "protocol": "Recompute scope membership and every declared scenario-authoring weight profile from the frozen protocol and observation matrix.", + "objective_pass_criteria": [ + "All systems remain in one declared scope stratum and no Pareto calculation mixes strata.", + "The recomputed first-ranked scenario-authoring system changes across the declared profiles." + ], + "objective_fail_criteria": [ + "A published overall winner is derived by combining unlike scope strata.", + "Recomputed declared profiles produce one invariant first-ranked system while the claim reports a reversal." + ], + "allowed_evidence_classes": [ + "documentation", + "executable", + "normative", + "publication", + "source-code" + ], + "disallowed_evidence_classes": [ + "marketing-claim", + "secondary-summary", + "unpinned-live-content" + ], + "evidence_artifact_refs": [ + "protocol-v1", + "snapshot-2026-07-13", + "analysis-v1" + ] + }, + "derivation": { + "kind": "scope-and-sensitivity", + "scope_strata": [ + "agent-simulation", + "federation-exchange", + "playbook-orchestration", + "scenario-authoring" + ], + "profile_winners": { + "breadth-and-composition": "aces", + "equal-evidence": "aces", + "formal-rigor": "vsdl", + "maturity-and-governance": "ocr-sdl" + }, + "ranking_reversal_observed": true + } + }, + { + "claim_id": "scope-qualified-breadth", + "kind": "scope-qualified-breadth", + "statement": "Within the four-system scenario-authoring stratum and this frozen rubric, ACES is the sole expressive-breadth leader at level 3. This is the broadest combined surface observed in this corpus; `highest quality` is not supported, and standardization or maturity do not follow from breadth.", + "system_ids": [ + "aces", + "ocr-sdl", + "crack", + "vsdl" + ], + "axis_ids": [ + "expressive-breadth" + ], + "evidence_status": "partial", + "threats_to_validity": [ + "The four-system scenario-authoring corpus is not an exhaustive census of scenario languages.", + "Expressive breadth is not a validated proxy for quality, maturity, standardization, or usability." + ], + "falsification": { + "protocol": "Recompute every scenario-authoring expressive-breadth score from the frozen observation cells and identify all systems at the maximum level.", + "objective_pass_criteria": [ + "ACES has level 3 and is the only system at the recomputed maximum within the declared scenario-authoring stratum." + ], + "objective_fail_criteria": [ + "A frozen scenario-authoring observation exceeds or ties ACES on expressive breadth.", + "The public wording generalizes the corpus-bounded maximum into highest quality, maturity, or standardization." + ], + "allowed_evidence_classes": [ + "documentation", + "executable", + "normative", + "publication", + "source-code" + ], + "disallowed_evidence_classes": [ + "marketing-claim", + "secondary-summary", + "unpinned-live-content" + ], + "evidence_artifact_refs": [ + "protocol-v1", + "snapshot-2026-07-13", + "analysis-v1" + ] + }, + "derivation": { + "kind": "axis-maximum", + "scope_stratum": "scenario-authoring", + "axis_id": "expressive-breadth", + "system_scores": { + "aces": 3, + "crack": 2, + "ocr-sdl": 2, + "vsdl": 2 + }, + "max_score": 3, + "leading_system_ids": [ + "aces" + ] + } + }, + { + "claim_id": "maturity-governance", + "kind": "maturity-governance", + "statement": "Against ACES's recorded level 1, CACAO 2.0 and Cyber FOM each record level 3 for governance and community, while CybORG records level 3 and OCR SDL level 2 for implementation maturity. These are axis-specific evidence comparisons, not cross-scope overall rankings or adoption claims.", + "system_ids": [ + "aces", + "cacao-v2", + "cyber-fom", + "cyborg", + "ocr-sdl" + ], + "axis_ids": [ + "implementation-maturity", + "governance-community" + ], + "evidence_status": "partial", + "threats_to_validity": [ + "Implementation and governance evidence can change after the pinned revisions and retrieval dates.", + "Primary-source implementation or standards evidence is not equivalent to independent adoption, usability, or operational-quality evidence." + ], + "falsification": { + "protocol": "Recompute the four declared maturity and governance cell comparisons directly from the frozen observation matrix.", + "objective_pass_criteria": [ + "Each named comparison has a strictly higher recorded axis level than the corresponding ACES level." + ], + "objective_fail_criteria": [ + "Any named system does not exceed ACES on the declared axis after recomputation.", + "The public wording promotes axis-specific evidence into an overall ranking or independent-adoption claim." + ], + "allowed_evidence_classes": [ + "documentation", + "executable", + "normative", + "publication", + "source-code" + ], + "disallowed_evidence_classes": [ + "marketing-claim", + "secondary-summary", + "unpinned-live-content" + ], + "evidence_artifact_refs": [ + "protocol-v1", + "snapshot-2026-07-13", + "analysis-v1" + ] + }, + "derivation": { + "kind": "cell-comparisons", + "comparisons": [ + { + "axis_id": "governance-community", + "left_system_id": "cacao-v2", + "left_score": 3, + "operator": "greater-than", + "right_system_id": "aces", + "right_score": 1 + }, + { + "axis_id": "governance-community", + "left_system_id": "cyber-fom", + "left_score": 3, + "operator": "greater-than", + "right_system_id": "aces", + "right_score": 1 + }, + { + "axis_id": "implementation-maturity", + "left_system_id": "cyborg", + "left_score": 3, + "operator": "greater-than", + "right_system_id": "aces", + "right_score": 1 + }, + { + "axis_id": "implementation-maturity", + "left_system_id": "ocr-sdl", + "left_score": 2, + "operator": "greater-than", + "right_system_id": "aces", + "right_score": 1 + } + ] + } + }, + { + "claim_id": "weight-sensitivity", + "kind": "sensitivity", + "statement": "Across the declared scenario-authoring profiles, breadth and composition ranks ACES first, formal rigor ranks VSDL first, and maturity and governance ranks OCR SDL first; this observed reversal prohibits a weight-independent winner claim.", + "system_ids": [ + "aces", + "ocr-sdl", + "vsdl" + ], + "axis_ids": [ + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "experiment-design", + "participant-modeling", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community" + ], + "evidence_status": "partial", + "threats_to_validity": [ + "Only the preregistered weight profiles are evaluated; other disclosed weights can produce other rankings.", + "Every weighted total remains dependent on axis-specific ordinal evidence levels and their frozen rubric." + ], + "falsification": { + "protocol": "Recompute each declared scenario-authoring weight profile from frozen observation scores and compare the first-ranked systems.", + "objective_pass_criteria": [ + "Breadth and composition ranks ACES first, formal rigor ranks VSDL first, and maturity and governance ranks OCR SDL first.", + "The recomputed set of first-ranked systems contains more than one system." + ], + "objective_fail_criteria": [ + "Any recorded profile winner differs from the recomputed winner.", + "The public wording suppresses the observed reversal or claims weight-independent robustness." + ], + "allowed_evidence_classes": [ + "documentation", + "executable", + "normative", + "publication", + "source-code" + ], + "disallowed_evidence_classes": [ + "marketing-claim", + "secondary-summary", + "unpinned-live-content" + ], + "evidence_artifact_refs": [ + "protocol-v1", + "snapshot-2026-07-13", + "analysis-v1" + ] + }, + "derivation": { + "kind": "profile-winners", + "profile_winners": { + "breadth-and-composition": "aces", + "formal-rigor": "vsdl", + "maturity-and-governance": "ocr-sdl" + }, + "ranking_reversal_observed": true + } + } + ] +} diff --git a/docs/research/related-work-comparison/extraction-snapshot-2026-07-13.json b/docs/research/related-work-comparison/extraction-snapshot-2026-07-13.json new file mode 100644 index 000000000..7bf4f09c1 --- /dev/null +++ b/docs/research/related-work-comparison/extraction-snapshot-2026-07-13.json @@ -0,0 +1,2973 @@ +{ + "snapshot_id": "snapshot-2026-07-13", + "protocol_revision": "protocol-v1", + "frozen_at": "2026-07-13T05:40:00Z", + "assessor": "Repository research extraction for issue 728", + "review_status": "awaiting-independent-replication", + "limitations": [ + "External authoring cases are source walkthroughs; normal CI does not fetch or execute compared systems.", + "No independent author population, blinded second extraction, or adjudication was available for this snapshot.", + "Ordinal levels summarize axis-specific evidence and are not interval measurements or a universal quality scale.", + "Repository and documentation activity is not treated as independent adoption evidence." + ], + "sources": [ + { + "source_id": "aces-authoring-schema", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES SDL authoring input schema", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/contracts/schemas/sdl/sdl-authoring-input-v1.json", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "56e04827667774e707263d5cfc2db57ce814af291c60d875ce5eb2602d4dff38", + "artifact_path": "contracts/schemas/sdl/sdl-authoring-input-v1.json", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-parser-runtime", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES production SDL parser", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/implementations/python/packages/aces_sdl/parser.py", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "52bc5c496ecb99ba5e3f76b085d5b73b38faf7fc3bf8f7623f969b6fd052d001", + "artifact_path": "implementations/python/packages/aces_sdl/parser.py", + "primary": true, + "evidence_class": "executable", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-parser-tests", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES SDL parser tests", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/implementations/python/tests/test_sdl_parser.py", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "ccd17a2de4f1ab8e9e423a313d1d0edcd4f276e29d7bab3c7804365502475f3c", + "artifact_path": "implementations/python/tests/test_sdl_parser.py", + "primary": true, + "evidence_class": "executable", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-participant-semantics", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES formal participant semantics", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/specs/formal/participant-semantics/README.md", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "d138b80c59429f6088d3429ab0d9fbbf9555926077274b1520cb0bff2b055265", + "artifact_path": "specs/formal/participant-semantics/README.md", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-experiment-study", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES experiment study contract", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/contracts/schemas/experiment-core/experiment-study-v1.json", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "cace7ada489fa94485d050c6a360a72d36dce8613c06842ee350d33943d6eabf", + "artifact_path": "contracts/schemas/experiment-core/experiment-study-v1.json", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-scientific-assessment", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES scientific-scenario delivery assessment", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "aea4e86e5aac6740611258ffa854ba1ab69cd09d6806e9fee6d32e40613daa9f", + "artifact_path": "contracts/profiles/scientific-completeness/delivery-assessment-2026-07-12.json", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-composition-spec", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES SDL document and composition model", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/specs/sdl/document-model.md", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "ff4cce428d152f6101e34d02a8547876309b461d36e4743183875a518f7b0d84", + "artifact_path": "specs/sdl/document-model.md", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-lineage-ledger", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES revision-pinned SDL lineage ledger", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/contracts/provenance/sdl-lineage-ledger-v1.json", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "a322c68cee90b3186cfa0faaee5e1aa9bafe9d0e2e96e0146461ea81b0247eab", + "artifact_path": "contracts/provenance/sdl-lineage-ledger-v1.json", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-backend-profiles", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES backend conformance profiles", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/contracts/profiles/backend/orchestration-capable.json", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "f70b8505a5c0055416db86c533e2e5bf08b11e5a514f076223b6d6c36215a092", + "artifact_path": "contracts/profiles/backend/orchestration-capable.json", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-getting-started", + "system_id": "aces", + "kind": "repository-internal", + "title": "ACES author getting-started guide", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/docs/explain/getting-started.md", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "ae0565cb19e2d4968f99455137ba3c516559acc2054d27742d254d367efeb750", + "artifact_path": "docs/explain/getting-started.md", + "primary": true, + "evidence_class": "documentation", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "aces-authority-adr", + "system_id": "aces", + "kind": "repository-internal", + "title": "ADR-009 normative authority boundary", + "locator": "https://github.com/Brad-Edwards/aces/blob/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0/docs/decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure.md", + "version": "pre-change dev revision", + "revision": "1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "retrieved_at": "2026-07-13", + "content_sha256": "557dc1e25c3d9abecb7b3bd8b5bbeb272e907cba04125b59a3e9b71d321b6e07", + "artifact_path": "docs/decisions/adrs/adr-009-normative-artifact-authority-and-repository-structure.md", + "primary": true, + "evidence_class": "normative", + "license_note": "Repository-owned content under the project license." + }, + { + "source_id": "ocr-parser-source", + "system_id": "ocr-sdl", + "kind": "git", + "title": "Open Cyber Range SDL parser source", + "locator": "https://github.com/Open-Cyber-Range/SDL-parser/tree/fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "version": "repository snapshot", + "revision": "fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": "sdl-parser/src", + "primary": true, + "evidence_class": "source-code", + "license_note": "Metadata and precise source locators only; source bytes are not copied." + }, + { + "source_id": "ocr-sdl-reference", + "system_id": "ocr-sdl", + "kind": "official-doc", + "title": "Open Cyber Range SDL Reference Guide", + "locator": "https://documentation.opencyberrange.ee/docs/sdl/reference/", + "version": "retrieved documentation snapshot", + "revision": "retrieved-2026-07-13", + "retrieved_at": "2026-07-13", + "content_sha256": "96d4585aec45b92548d6615de8b0db3c2a076a9d8ddec4877edc0f6098cfcbc4", + "artifact_path": null, + "primary": true, + "evidence_class": "documentation", + "license_note": "Digest and paraphrased findings only; documentation bytes are not archived here." + }, + { + "source_id": "cyborg-source", + "system_id": "cyborg", + "kind": "git", + "title": "CybORG source and scenario corpus", + "locator": "https://github.com/cage-challenge/CybORG/tree/2742b5e0ce4330c9b14006b38acd3b5ebe00d6fd", + "version": "repository snapshot", + "revision": "2742b5e0ce4330c9b14006b38acd3b5ebe00d6fd", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": "CybORG", + "primary": true, + "evidence_class": "source-code", + "license_note": "Metadata and precise source locators only; source bytes are not copied." + }, + { + "source_id": "cyborg-paper", + "system_id": "cyborg", + "kind": "publication", + "title": "CybORG: A Gym for the Development of Autonomous Cyber Agents", + "locator": "https://arxiv.org/pdf/2108.09118v1", + "version": "arXiv version 1", + "revision": "arXiv:2108.09118v1", + "retrieved_at": "2026-07-13", + "content_sha256": "69fc40a8ca959adb3333a2716fa4bc6f52bf1e3bebf76129fcf42fe9891ed783", + "artifact_path": null, + "primary": true, + "evidence_class": "publication", + "license_note": "Digest and paraphrased findings only; paper bytes are not archived here." + }, + { + "source_id": "cacao-standard", + "system_id": "cacao-v2", + "kind": "standard", + "title": "CACAO Security Playbooks Version 2.0", + "locator": "https://docs.oasis-open.org/cacao/security-playbooks/v2.0/cs01/security-playbooks-v2.0-cs01.pdf", + "version": "Committee Specification 01, 27 November 2023", + "revision": "CACAO-v2.0-CS01", + "retrieved_at": "2026-07-13", + "content_sha256": "584eae31ad4be42b6363a600d433d22d460b1f749916a0e3c4cca90ab8b2f428", + "artifact_path": null, + "primary": true, + "evidence_class": "normative", + "license_note": "Digest and paraphrased findings only; OASIS notices remain authoritative." + }, + { + "source_id": "cyber-dem-standard", + "system_id": "cyber-dem", + "kind": "standard", + "title": "SISO Cyber Data Exchange Model", + "locator": "https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf", + "version": "SISO-STD-025-2023", + "revision": "SISO-STD-025-2023", + "retrieved_at": "2026-07-13", + "content_sha256": "e351e2c73ede003cea62c1ec0c22acb4430220b4d033c4de625645c6a8560e72", + "artifact_path": null, + "primary": true, + "evidence_class": "normative", + "license_note": "Digest and paraphrased findings only; standard bytes are not archived here." + }, + { + "source_id": "cyber-dem-source", + "system_id": "cyber-dem", + "kind": "git", + "title": "Cyber DEM Python reference implementation", + "locator": "https://github.com/cmu-sei/cyberdem-python/tree/577543d53fa9ffb2b818a91e0ed61881c9bd7bfa", + "version": "repository snapshot", + "revision": "577543d53fa9ffb2b818a91e0ed61881c9bd7bfa", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": "cyberdem", + "primary": true, + "evidence_class": "source-code", + "license_note": "Metadata and precise source locators only; source bytes are not copied." + }, + { + "source_id": "cyber-fom-standard", + "system_id": "cyber-fom", + "kind": "standard", + "title": "SISO Cyber Federation Object Model", + "locator": "https://www.sisostandards.org/page/StandardsProducts", + "version": "SISO-STD-025.3-2024", + "revision": "SISO-STD-025.3-2024", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": null, + "primary": true, + "evidence_class": "normative", + "license_note": "Exact standard identity and paraphrased findings only; standard bytes are not archived here." + }, + { + "source_id": "crack-paper", + "system_id": "crack", + "kind": "publication", + "title": "Building next generation Cyber Ranges with CRACK", + "locator": "https://doi.org/10.1016/j.cose.2020.101837", + "version": "Computers & Security 95, article 101837", + "revision": "doi:10.1016/j.cose.2020.101837", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": null, + "primary": true, + "evidence_class": "publication", + "license_note": "Bibliographic metadata and paraphrased findings only; paper bytes are not archived here." + }, + { + "source_id": "crack-source", + "system_id": "crack", + "kind": "git", + "title": "CRACK TOSCA type and runtime definitions", + "locator": "https://github.com/enricorusso/CRACK/tree/1f22c729a379d09001f446d19e01a5561eed6ca9", + "version": "repository snapshot", + "revision": "1f22c729a379d09001f446d19e01a5561eed6ca9", + "retrieved_at": "2026-07-13", + "content_sha256": null, + "artifact_path": "types", + "primary": true, + "evidence_class": "source-code", + "license_note": "Metadata and precise source locators only; source bytes are not copied." + }, + { + "source_id": "vsdl-paper", + "system_id": "vsdl", + "kind": "publication", + "title": "Automating the Generation of Cyber Range Virtual Scenarios with VSDL", + "locator": "https://arxiv.org/pdf/2001.06681v1", + "version": "arXiv version 1", + "revision": "arXiv:2001.06681v1", + "retrieved_at": "2026-07-13", + "content_sha256": "d8b505602afee6962c39f02d6691fc5ab7aba77acb3ea35760bb8211e85381b4", + "artifact_path": null, + "primary": true, + "evidence_class": "publication", + "license_note": "Digest and paraphrased findings only; paper bytes are not archived here." + } + ], + "observations": [ + { + "system_id": "aces", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The closed authoring schema covers topology, runtime inventory, objectives, workflows, variables, imports, and related concern families.", + "evidence_refs": [ + { + "source_id": "aces-authoring-schema", + "locator": "properties and closed object definitions" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Closed schemas, reference catalogs, and semantic specifications define meaning beyond field presence.", + "evidence_refs": [ + { + "source_id": "aces-authoring-schema", + "locator": "x-aces-invariants and reference-bearing properties" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Formal fragments exist for participants, workflows, objectives, and other surfaces, but full solver-backed scenario verification is absent.", + "evidence_refs": [ + { + "source_id": "aces-participant-semantics", + "locator": "formal model, invariants, and conformance boundaries" + } + ], + "confidence": "high", + "limitations": [ + "Full solver-backed whole-scenario verification remains outside the current implementation." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The production parser enforces bounded UTF-8/YAML input, closed models, reference validation, and negative controls.", + "evidence_refs": [ + { + "source_id": "aces-parser-runtime", + "locator": "parse_sdl and parse_sdl_file production boundary" + }, + { + "source_id": "aces-parser-tests", + "locator": "negative parser and source-profile tests" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Module/import expansion, identities, digests, provenance, and evolution rules are explicit.", + "evidence_refs": [ + { + "source_id": "aces-composition-spec", + "locator": "module expansion and normalized/instantiated document forms" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "experiment-design", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Experiment contracts cover tasks, factors, studies, runs, measures, and provenance, while several scientific concerns remain external or incomplete.", + "evidence_refs": [ + { + "source_id": "aces-experiment-study", + "locator": "study, task, run, factor, and analysis-plan bindings" + }, + { + "source_id": "aces-scientific-assessment", + "locator": "factor through study-record delivery rows" + } + ], + "confidence": "high", + "limitations": [ + "Controlled allocation and factor/study concerns live in experiment contracts, and several delivery rows remain external or incomplete." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "participant-modeling", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Actions, observations, visibility, and episode semantics exist, but budgets, trajectories, adjudication, and information-flow admission remain incomplete.", + "evidence_refs": [ + { + "source_id": "aces-participant-semantics", + "locator": "actions, observations, visibility, episodes, and outcomes" + }, + { + "source_id": "aces-scientific-assessment", + "locator": "participant declaration through verifier/adjudication rows" + } + ], + "confidence": "high", + "limitations": [ + "Participant budgets, reference trajectories, hidden benchmark assets, and verifier/adjudication remain missing or partial." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Revision-pinned lineage plus experiment evidence references and derivation surfaces provide broad provenance coverage.", + "evidence_refs": [ + { + "source_id": "aces-lineage-ledger", + "locator": "revision-pinned citations and derivation claims" + }, + { + "source_id": "aces-experiment-study", + "locator": "evidence and analysis-plan references" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Published backend profiles and conformance apparatus define a portable boundary, but independent multi-vendor evidence remains limited.", + "evidence_refs": [ + { + "source_id": "aces-backend-profiles", + "locator": "backend profile corpus and conformance declarations" + } + ], + "confidence": "high", + "limitations": [ + "Published profiles exist, but independent multi-vendor conformance and substitution evidence remains limited." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "usability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A maintained guide and examples exist, but no independent authoring-task study with effort/error measures is present.", + "evidence_refs": [ + { + "source_id": "aces-getting-started", + "locator": "current author workflow and validation commands" + } + ], + "confidence": "high", + "limitations": [ + "No independent author population, completion-time, effort, or error study is frozen in this corpus." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Parser and validation behavior ship with tests, while the delivery assessment records numerous partial, missing, external, and excluded concerns.", + "evidence_refs": [ + { + "source_id": "aces-parser-runtime", + "locator": "shipping production parser" + }, + { + "source_id": "aces-parser-tests", + "locator": "executable parser regression corpus" + }, + { + "source_id": "aces-scientific-assessment", + "locator": "implemented, partial, missing, external, and excluded rows" + } + ], + "confidence": "high", + "limitations": [ + "The delivery assessment retains partial, missing, external-contract, and deliberately-excluded concerns; formal prose is not counted as shipped behavior." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "aces", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Repository ADR and authority governance is explicit but remains project-led rather than a standards-body or broad independent community process.", + "evidence_refs": [ + { + "source_id": "aces-authority-adr", + "locator": "repository authority and decision process" + } + ], + "confidence": "high", + "limitations": [ + "Governance is explicit but project-led; standardization and independent adoption are not claimed." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The language covers VMs, switches, features, vulnerabilities, stories, events, injects, metrics, objectives, and roles, with narrower runtime/experiment semantics.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "Nodes, Infrastructure, Features, Stories, Scripts, Metrics, TLOs, and Goals" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The parser defines structural and reference rules, while many meanings remain tied to platform behavior rather than a formal semantic model.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "sdl-parser/src entity, infrastructure, evaluation, and reference checks" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "No solver, formal transition system, or executable whole-scenario formal analysis was observed in the pinned parser and reference.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "parser source; no solver or formal-analysis layer found" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A revision-pinned Rust parser and parser snapshots cover the YAML surface and common invalid references.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "sdl-parser/src and snapshot-backed parser tests" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Versioned digital-library sources are supported, but reusable scenario composition and compatibility semantics are limited.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "Node/Feature source name and semantic version fields" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "experiment-design", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Metrics, evaluations, TLOs, and narrative timelines support training evaluation without controlled allocation, factor, or study semantics.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "Metrics, Evaluations, TLOs, Goals, Stories, and Scripts" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "participant-modeling", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Roles, entities, and inject delivery model participants statically, without action/observation/reward episode contracts.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "Node roles, Entities, Stories, and event/inject delivery" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Events, inject outputs, timestamps, and metrics provide bounded records but not a full evidence identity and derivation chain.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "event, inject, and metric source structures" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The parser and handler boundary support platform integration, but an independent conformance suite was not found.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "parser library and exported language bindings" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "usability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The official reference and complete example support author walkthroughs; no independent effort/error study was found.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "SDL Reference Guide and complete example" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A maintained parser and operational platform documentation support deployed use, without broad independent conformance evidence.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "revision-pinned Rust parser and tests" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "ocr-sdl", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Public project governance is maintainer-led and does not establish a formal multi-party standards process.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "repository metadata and maintainer-owned source" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Scenarios, hosts, services, processes, agents, actions, observations, rewards, and episodes provide broad simulation-environment coverage.", + "evidence_refs": [ + { + "source_id": "cyborg-paper", + "locator": "paper sections 2-3: scenario, simulation, emulation, agents, and environment" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The common interface and simulator classes define behavior precisely in code, with less language-level normative semantics.", + "evidence_refs": [ + { + "source_id": "cyborg-paper", + "locator": "paper environment and agent interface definitions" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The environment is executable and analyzable through simulation, but it does not provide solver-backed scenario semantics.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "source reward/action/state machinery; no scenario solver" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Scenario YAML is parsed by FileReaderScenarioGenerator with executable tests, though it is not a published closed interchange schema.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "FileReaderScenarioGenerator and scenario_files/Scenario1b.yaml" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "ScenarioGenerator enables reusable generation and reset behavior; explicit compatibility/migration semantics are limited.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "ScenarioGenerator and repository release/source organization" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "experiment-design", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Rewards, resets, seeds, scenarios, and episodes support controlled agent experiments, with study-level provenance handled externally.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "scenario generator, reward calculators, reset, and episode controls" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "participant-modeling", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Actions, role-filtered observations, rewards, partial observability, and bounded episodes are central executable constructs.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "Actions, Observation, RewardCalculator, agents, and episodes" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Scenario and result structures preserve bounded context, but a first-class evidence lineage and custody contract is not present.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "result/observation structures and scenario source" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A common interface spans simulation and emulation, but a published independent conformance standard is not evidenced.", + "evidence_refs": [ + { + "source_id": "cyborg-paper", + "locator": "paper common simulation/emulation interface" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "usability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Tutorials and scenarios support users, while no disclosed independent author-task effort/error study was found.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "tutorials and scenario generator source" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The pinned source contains a working simulator, scenario corpus, tests, and challenge use.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "executable simulator, tests, and scenario corpus" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyborg", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "An originating research program and public challenge community exist without formal standards-body governance.", + "evidence_refs": [ + { + "source_id": "cyborg-paper", + "locator": "originating paper and public challenge repository" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The standard is broad for security playbooks, workflow steps, commands, variables, agents, targets, markings, and signatures, not range topology.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "sections 1-10 object classes and workflow steps" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Normative object/property requirements, identifiers, and conformance clauses define precise playbook semantics.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "normative property tables and conformance language" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Machine-readable structural rules exist, but no solver or formal behavioral analysis is part of the standard.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "sections 3-11; no formal solver or model analysis" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Normative JSON object constraints, mandatory properties, identifier rules, and conformance classes provide a strong syntax boundary.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "normative JSON object requirements and conformance section" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Playbook references, identifiers, modified versions, variables, extensions, and version compatibility are explicit.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "sections 1.2.1, 3.1, 4, and 11.1.1" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "experiment-design", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Controlled experiment design is outside the security-playbook standard's declared scope.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "scope in section 1; experiment design is outside the playbook standard" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "participant-modeling", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Agents and targets dispatch commands; participant action/observation/reward episodes are outside scope.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "agents and targets in section 7; participant episodes are outside scope" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Digital signatures, data markings, created-by identity, and external references provide strong provenance/disclosure support.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "sections 2.4, 2.5, 9, and 10.15 data markings and signatures" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Producer/consumer conformance and standardized exchange across organizational boundaries are explicit.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "section 11 producer/consumer conformance" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "usability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Detailed normative examples and property tables support authoring, but no independent author-task study was found.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "normative examples and object property tables" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "An approved OASIS Committee Specification with mandatory features provides strong maturity evidence for the standard surface.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "Committee Specification 01 and mandatory features" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cacao-v2", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "OASIS TC process, named participation, ballots, and a published Committee Specification provide formal governance.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "OASIS TC, Committee Specification status, and participant list" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The model covers cyber objects, relationships, events, effects, data, services, and networks for runtime exchange rather than scenario authoring.", + "evidence_refs": [ + { + "source_id": "cyber-dem-source", + "locator": "cyberdem/base object, event, relationship, and effect classes" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A SISO standard and typed reference implementation define object/event meaning and enumerations.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "SISO-STD-025-2023 model definitions" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Typed model validation exists, but no scenario-level solver or formal behavioral analyzer is part of the DEM.", + "evidence_refs": [ + { + "source_id": "cyber-dem-source", + "locator": "reference-model enumerations and validation; no scenario solver" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The standard and reference implementation provide typed representation, though concrete interchange validation evidence is less complete than a conformance suite.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "standard representation plus Python type validation" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The numbered standard family provides version identity and related object/event models, with limited authored composition semantics.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "standard model family and version identity" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "experiment-design", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Experiment design is outside the runtime data-exchange model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "standard scope; experiment design is not a DEM concern" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "participant-modeling", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Participant action/observation episodes are outside the data-exchange model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "standard scope; agent episodes are not a DEM concern" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Sensitivity and event/data metadata are present, but evidence identity, custody, and derivation are not first-class.", + "evidence_refs": [ + { + "source_id": "cyber-dem-source", + "locator": "Data sensitivity/status and event metadata in cyberdem/base" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The solution-independent model is explicitly designed for unambiguous translation among simulation interoperability solutions.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "SISO-STD-025-2023 solution-independent translation boundary" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "usability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A standard and reference implementation exist, but no independent author-task study was located.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "standard and reference implementation documentation" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A published standard plus a reference implementation provide maintained delivery evidence, with limited independent implementation evidence in this snapshot.", + "evidence_refs": [ + { + "source_id": "cyber-dem-source", + "locator": "published standard and Python reference implementation" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-dem", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The SISO standards-product process provides recognized multi-party governance.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "SISO standards-product process and product identity" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The FOM realizes Cyber DEM objects/events for HLA federation exchange; its breadth is bounded by that runtime purpose.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "SISO-STD-025.3-2024 HLA-specific implementation of Cyber DEM" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The HLA-specific standard defines a precise federation object-model binding.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "standardized HLA object-model binding" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "HLA/FOM structural and federation constraints provide substantial analyzability, without scenario-design solver semantics.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "HLA FOM structural and federation constraints" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A standardized HLA federation object model provides a strong concrete exchange definition.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "HLA federation object-model standard" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The FOM is explicitly versioned and derived from the Cyber DEM within a standard family.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "Cyber DEM derivation and HLA FOM version identity" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "experiment-design", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Experiment design is outside the HLA federation object model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "standard scope; experiment design is not a Cyber FOM concern" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "participant-modeling", + "applicability": "out-of-scope", + "score": null, + "measure": "not-applicable", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Participant action/observation episodes are outside the federation object model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "standard scope; participant episodes are not a Cyber FOM concern" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Cyber DEM event/data metadata is carried into federation exchange, but evidence lineage and custody are not first-class.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "inherited Cyber DEM event/data attributes; no evidence-chain contract" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "This is the standardized HLA-specific implementation of the solution-independent Cyber DEM.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "HLA-specific standardized federation implementation" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "usability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Standard documentation exists, but no independent author-task study was located.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "standard product documentation; no author study located" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A published HLA-specific SISO standard provides mature standardized delivery evidence.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "published HLA-specific SISO standard" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "cyber-fom", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The SISO standards-product process provides recognized multi-party governance.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "SISO standards-product process" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "CRACK covers TOSCA-modeled systems, software, users, principals, goals, invariants, vulnerabilities, runtime predicates, and deployment.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "paper design, verification, generation, and automated-test scope" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Datalog encoding gives precise meaning to goals, invariants, and verification predicates.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "paper Datalog encoding and verification model" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Scenario specifications are encoded into Datalog, checked against validation goals, and used to derive runtime tests.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "paper Datalog validation goals and verification traces" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "TOSCA type definitions and the framework pipeline provide implemented syntax validation, though no independent negative corpus is frozen here.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "types/sdl*.yaml TOSCA types and validation pipeline" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "TOSCA inheritance and plugins support reuse; explicit revision compatibility and migration semantics are limited.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "TOSCA type inheritance and plugin composition" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "experiment-design", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Goals and principals support exercise intent, but factors, allocation, measures, and study/run design are absent.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "goals/principals support exercise intent but not controlled study design" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "participant-modeling", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Principals carry roles/knowledge and goals, without action/observation/reward episode semantics.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "types/sdl-principal.yaml and sdl-goal.yaml" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Verification traces become runtime tests, but a structured evidence identity and derivation contract is not present.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "runtime predicate/test generation; no evidence-lineage object" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The pinned implementation targets OpenStack and does not publish an independent backend conformance boundary.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "OpenStack-specific plugin and deployment types" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "usability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The paper and examples provide a maintainer workflow; no independent author-task study was found.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "paper workflow and repository examples; no author study located" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A working research prototype and source snapshot exist, without current release or independent adoption evidence in this corpus.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "paper prototype and pinned source snapshot" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "crack", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The evidence is an originating-author project and publication, not a formal standards or broad community process.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "originating-author project and publication record" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "expressive-breadth", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "VSDL represents networks, nodes, hardware, software, files, users, privileges, vulnerabilities, temporal changes, and deployment intent.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "sections 4-5: nodes, networks, hardware, software, data, users, vulnerabilities, and time" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "semantic-precision", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "QFLIA assertions define scenario meaning and distinctness constraints precisely.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "section 4.2 QFLIA translation and distinctness invariants" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "formal-analyzability", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The compiler translates VSDL to SMT and uses satisfiability/model generation as the central validation mechanism.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "sections 3-5 SMT satisfiability and model generation" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "concrete-syntax-soundness", + "applicability": "applicable", + "score": 2, + "measure": "substantial", + "extraction_method": "pinned-source-and-document-review", + "rationale": "A grammar and compiler workflow are described with examples, but a pinned public source/negative fixture corpus was not located.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "section 4.1 grammar excerpt and compiler workflow" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "composition-versioning", + "applicability": "applicable", + "score": 3, + "measure": "strong", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Statements and infrastructures compose additively with satisfiability checks; explicit version compatibility/migration is absent.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "paper compositionality claim and additive scenario statements" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "experiment-design", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The language supports training infrastructures but not controlled factor, allocation, measure, or study/run contracts.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "paper training setting; no controlled experiment-design contract" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "participant-modeling", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Red/blue timing appears in examples, without portable participant action/observation/reward episode semantics.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "red/blue team timing in examples, without action/observation episodes" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "provenance-evidence", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Generated SMT models and scripts are outputs, but no evidence identity, lineage, or custody contract is defined.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "generated model/scripts; no structured evidence-lineage surface" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "interoperability", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Generated OpenStack/Terraform/Packer scripts bind to one realization stack rather than an independent conformance boundary.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "OpenStack/Terraform/Packer output rather than independent conformance" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "usability", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "Worked examples show the authoring process, but no disclosed independent effort/error study was found.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "worked examples; no disclosed author-task study" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "implementation-maturity", + "applicability": "applicable", + "score": 1, + "measure": "limited", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The paper describes a working prototype; no pinned maintained source release or independent deployment corpus is included.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "working prototype described in the originating paper" + } + ], + "confidence": "moderate", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + }, + { + "system_id": "vsdl", + "axis_id": "governance-community", + "applicability": "applicable", + "score": 0, + "measure": "absent", + "extraction_method": "pinned-source-and-document-review", + "rationale": "The originating publication supplies peer-review evidence, not standards-body or sustained community governance.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "originating publication; no standards/community governance found" + } + ], + "confidence": "high", + "limitations": [ + "This ordinal result is a source-grounded single-assessor extraction, not an independent usability or adoption study." + ], + "review_status": "single-assessor-awaiting-independent-replication" + } + ], + "task_observations": [ + { + "system_id": "aces", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "supported", + "method": "repository-execution", + "rationale": "Schema-backed topology, participants, objectives, and validation provide a native artifact path.", + "evidence_refs": [ + { + "source_id": "aces-authoring-schema", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "Repository execution covers existing production/tests only; it is not an independent author study." + ] + }, + { + "system_id": "aces", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Module/import expansion records identities, digests, namespaces, and resolved composition provenance.", + "evidence_refs": [ + { + "source_id": "aces-composition-spec", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "Repository execution covers existing production/tests only; it is not an independent author study." + ] + }, + { + "system_id": "aces", + "case_id": "author-controlled-experiment", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Experiment contracts cover the design, while the delivery assessment keeps allocation and related obligations bounded.", + "evidence_refs": [ + { + "source_id": "aces-experiment-study", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "Repository execution covers existing production/tests only; it is not an independent author study." + ] + }, + { + "system_id": "aces", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "supported", + "method": "repository-execution", + "rationale": "Production parser and semantic tests reject undeclared references.", + "evidence_refs": [ + { + "source_id": "aces-parser-tests", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "Repository execution covers existing production/tests only; it is not an independent author study." + ] + }, + { + "system_id": "aces", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Structural and semantic contradictions are checked in bounded domains, but no universal solver covers the whole SDL.", + "evidence_refs": [ + { + "source_id": "aces-participant-semantics", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "Repository execution covers existing production/tests only; it is not an independent author study." + ] + }, + { + "system_id": "ocr-sdl", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Nodes, infrastructure, roles, Goals/TLOs, and stories cover the brief within the OCR deployment model.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "ocr-sdl", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Digital-library source versions support component reuse, but scenario composition/compatibility is not explicit.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "ocr-sdl", + "case_id": "author-controlled-experiment", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Metrics and TLOs support evaluation, while factor allocation, study identity, and evidence lineage are absent.", + "evidence_refs": [ + { + "source_id": "ocr-sdl-reference", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "ocr-sdl", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Parser reference checks require declared nodes, scripts, events, metrics, and related objects.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "ocr-sdl", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Several structural conflicts are rejected, but no general semantic contradiction analysis is evidenced.", + "evidence_refs": [ + { + "source_id": "ocr-parser-source", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyborg", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Scenario files and agents cover hosts, services, roles, and objectives through rewards, but not a portable authored objective language.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyborg", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "ScenarioGenerator supports reuse, while explicit revision compatibility is external to the artifact.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyborg", + "case_id": "author-controlled-experiment", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Episodes, rewards, resets, and seeds support experiments, while first-class factor/study/evidence declarations are incomplete.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyborg", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "FileReaderScenarioGenerator checks many references, but this snapshot does not freeze a dedicated dangling-reference result.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyborg", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Executable simulator invariants catch bounded conflicts, without a declared whole-scenario contradiction contract.", + "evidence_refs": [ + { + "source_id": "cyborg-source", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cacao-v2", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Playbook steps, agents, targets, and goals cover workflow intent but not multi-host range topology.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cacao-v2", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Sub-playbooks, identifiers, modified versions, variables, and compatibility clauses provide explicit composition.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cacao-v2", + "case_id": "author-controlled-experiment", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Controlled participant experiments are outside the playbook standard's scope.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cacao-v2", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Normative identifier/reference requirements require resolvable playbook objects.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cacao-v2", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Workflow constraints reject bounded invalid structures, while general logical satisfiability is not defined.", + "evidence_refs": [ + { + "source_id": "cacao-standard", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-dem", + "case_id": "author-multihost-objective", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Scenario authoring is outside the runtime exchange model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-dem", + "case_id": "author-versioned-composition", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Reusable authored-unit composition is outside the runtime exchange model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-dem", + "case_id": "author-controlled-experiment", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Controlled experiment design is outside the runtime exchange model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-dem", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Typed relationship/object identifiers constrain references, but no frozen executable negative case is included.", + "evidence_refs": [ + { + "source_id": "cyber-dem-source", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-dem", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Normative object/event constraints cover bounded conflicts, without scenario satisfiability analysis.", + "evidence_refs": [ + { + "source_id": "cyber-dem-standard", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-fom", + "case_id": "author-multihost-objective", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Scenario authoring is outside the HLA federation object model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-fom", + "case_id": "author-versioned-composition", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Reusable authored-unit composition is outside the HLA federation object model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-fom", + "case_id": "author-controlled-experiment", + "applicability": "out-of-scope", + "outcome": "not-applicable", + "method": "source-walkthrough", + "rationale": "Controlled experiment design is outside the HLA federation object model's scope.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-fom", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "FOM class/object references are normatively constrained, but no frozen executable negative case is included.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "cyber-fom", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "HLA/FOM constraints cover bounded federation-model conflicts, not scenario satisfiability.", + "evidence_refs": [ + { + "source_id": "cyber-fom-standard", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "crack", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "TOSCA system/software/user/principal/goal types and deployment generation cover the representative scenario.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "crack", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "TOSCA inheritance/plugins support reuse, while revision compatibility and migration are implicit.", + "evidence_refs": [ + { + "source_id": "crack-source", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "crack", + "case_id": "author-controlled-experiment", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Goals and principals support exercise intent, while factors, allocation, measures, and study boundaries are absent.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "crack", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Datalog verification and TOSCA reference structure detect invalid scenario relationships.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "crack", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "The Datalog encoding reports unsatisfied validation goals and produces verification traces.", + "evidence_refs": [ + { + "source_id": "crack-paper", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "vsdl", + "case_id": "author-multihost-objective", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "VSDL nodes, networks, software, users, temporal guards, and constraints cover the representative infrastructure brief.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "case walkthrough: author-multihost-objective" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "vsdl", + "case_id": "author-versioned-composition", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Statements and infrastructures compose, while revision identity and compatibility are not explicit.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "case walkthrough: author-versioned-composition" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "vsdl", + "case_id": "author-controlled-experiment", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "The language builds training infrastructure but does not model factors, allocation, measures, or study/run evidence.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "case walkthrough: author-controlled-experiment" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "vsdl", + "case_id": "negative-dangling-reference", + "applicability": "applicable", + "outcome": "partial", + "method": "source-walkthrough", + "rationale": "Compiler translation implies declared identifiers, but the paper does not freeze a dedicated dangling-reference diagnostic.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "case walkthrough: negative-dangling-reference" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + }, + { + "system_id": "vsdl", + "case_id": "negative-contradictory-constraints", + "applicability": "applicable", + "outcome": "supported", + "method": "source-walkthrough", + "rationale": "Contradictory QFLIA constraints produce an unsatisfiable solver result before deployment generation.", + "evidence_refs": [ + { + "source_id": "vsdl-paper", + "locator": "case walkthrough: negative-contradictory-constraints" + } + ], + "limitations": [ + "External-system cases are reproducible source walkthroughs, not execution of third-party code or an independent author study." + ] + } + ] +} diff --git a/docs/research/related-work-comparison/index.md b/docs/research/related-work-comparison/index.md index 5425231b7..cac78e991 100644 --- a/docs/research/related-work-comparison/index.md +++ b/docs/research/related-work-comparison/index.md @@ -1,41 +1,33 @@ -# Related-Work Comparison Research +# Reproducible Related-Work Comparison -Issue: #508 (review LIT-1), also resolving review LIT-4. +Issue: #728. Requirement: ASR-534. -Purpose: gather the primary-source basis for a feature-by-feature comparison of -ACES against precedent systems before writing -[`docs/explain/sdl/related-work-comparison.md`](../../explain/sdl/related-work-comparison.md). -The comparison answers the first question of any peer review of a new language: -dimension by dimension, what can ACES express that the precedents cannot, and -where do the precedents still lead ACES. +This directory is the reproduction bundle for the reader-facing +[Related-Work Comparison](../../explain/sdl/related-work-comparison.md). It +replaces the earlier prose-only `yes`/`partial`/`no` table with a preregistered +protocol, a frozen extraction snapshot, and recomputable analysis. -These notes do not characterize competitor capabilities from memory. Every -non-ACES claim in the comparison page is grounded in a precedent's own -documentation, standard text, source, or originating-author literature, recorded -in [`search-log.md`](search-log.md) with the exact source and a supporting -finding. +The result is deliberately bounded. It supports no universal winner or +`highest quality` claim. Within the scenario-authoring stratum, different +predeclared weighting profiles place ACES, VSDL, or OCR SDL first. Systems with +different purposes remain in separate scope strata. -## Relationship To Prior Research +## Bundle Contents -Element-level provenance and the source map already exist and are not repeated -here: - -- [`docs/explain/sdl/precedents.md`](../../explain/sdl/precedents.md) — element-by-element - source mapping. -- [`docs/explain/sdl/lineage.md`](../../explain/sdl/lineage.md) — narrative - source map by concern area. -- `specs/formal/participant-semantics/README.md` — participant-semantics - primary-source review. - -This note covers the question specific to issue #508: how ACES and the -precedents compare across named expressivity dimensions, with each cell -traceable to a primary source. - -## Contents - -- [Search log](search-log.md) — the source rule, tooling, per-system primary - sources with URLs/DOIs, and the grounded findings behind every non-ACES cell - in the comparison matrix. +- [`protocol-v1.json`](protocol-v1.json) freezes inclusion and exclusion rules, + system identities, scope strata, the twelve required axes and rubrics, three + representative authoring tasks, two negative cases, and analysis rules. +- [`extraction-snapshot-2026-07-13.json`](extraction-snapshot-2026-07-13.json) + records pinned primary sources, all 96 system-axis observations, and all 40 + system-case walkthroughs. Every observation includes an exact locator, + rationale, confidence, limitations, and review state. +- [`analysis-v1.json`](analysis-v1.json) records scope-qualified Pareto + frontiers, four weight profiles, recomputed totals, the observed ranking + reversals, and the claims permitted on the public page. Every claim carries + ADR-021 evidence status, threats to validity, falsification criteria, named + evidence artifacts, and a structured derivation from recomputed cells. +- [`search-log.md`](search-log.md) records source acquisition, revision and + digest choices, corpus boundaries, and the extraction method. :::{toctree} :hidden: @@ -43,29 +35,44 @@ traceable to a primary source. search-log ::: -## Source Rule - -- Non-ACES cells cite the precedent's primary documentation: the maintaining - body's standard text, the originating authors' peer-reviewed papers or - technical reports, official project documentation, or the project's own - source. Secondary summaries are used only to locate primary sources or where - no primary source is available, and are identified as secondary. -- ACES cells cite repository authority: specs under `specs/`, ADRs under - `docs/decisions/adrs/`, contracts under `contracts/`, or the reference notes - in `docs/explain/`. The comparison page is explanatory synthesis; it is not a - new authority for ACES semantics. -- Where a capability could not be grounded in a primary source, it is recorded - as a confidence gap rather than asserted. - -## Scope Boundaries - -- The comparison is an evidence surface, not a ranking. Cells are `yes`, - `partial`, `no`, or `out of scope`, each with a one-line justification and a - citation. -- At least one dimension honestly favors a precedent. In practice several do: - HLA-grounded time management and federation interoperability (SISO Cyber - DEM/FOM, TENA/HLA), OASIS-standardized workflow taxonomy and signed - provenance (CACAO), executed RL episode discipline (CybORG), and formal - scenario verification (CRACK). -- Maturity is stated honestly: several ACES surfaces are formally specified but - still materializing in the runtime, and ACES time semantics are not complete. +## Reproduce + +Run the focused offline gate and its mutation tests from the repository root: + +```bash +implementations/python/.venv/bin/python tools/check_related_work_comparison.py +implementations/python/.venv/bin/python -m pytest \ + implementations/python/tests/test_related_work_comparison.py +``` + +The checker performs no network access and executes no compared project. It +validates bounded closed JSON shapes, source pins, safe locators, primary-source +coverage, rectangular matrices, ACES executable evidence, task and negative-case +coverage, Pareto and sensitivity recomputation, and reader-page parity. +It also rejects a public claim whose evidence status, falsification record, +declared scope, derivation, or canonical statement drifts from the frozen +observations and weight-profile results. + +## Extraction And Review Status + +The 2026-07-13 snapshot uses two evidence methods: + +- repository execution for existing ACES production and negative-test + boundaries where the cited repository paths already exercise the case; +- source walkthroughs for external systems, using pinned standards, + publications, official documentation, and source revisions. + +The external cases are not executions of third-party code and are not a user +study. The snapshot has one extraction pass and awaits independent replication +or adjudication, so every public claim retains ADR-021 status `partial`. +Usability therefore remains limited to documentation and walkthrough evidence; +repository activity is not treated as independent adoption. + +## Authority Boundary + +These artifacts are non-normative research synthesis. They report observed +evidence about ACES and precedents but do not define SDL meaning, validity, +runtime behavior, or external-system capability. ACES semantics remain in +`specs/`, published contracts, accepted ADRs, and implementation tests. A source +refresh creates a new extraction snapshot; a rubric change creates a new +protocol revision. diff --git a/docs/research/related-work-comparison/protocol-v1.json b/docs/research/related-work-comparison/protocol-v1.json new file mode 100644 index 000000000..673f96973 --- /dev/null +++ b/docs/research/related-work-comparison/protocol-v1.json @@ -0,0 +1,345 @@ +{ + "protocol_id": "aces-related-work-comparison", + "revision": "protocol-v1", + "registered_at": "2026-07-13", + "title": "ACES reproducible related-work comparison protocol", + "purpose": "Compare the observed, revision-pinned capabilities of ACES and selected precedent systems without converting unlike scopes into a universal quality ranking.", + "inclusion_criteria": [ + "Include ACES as the assessed system and independently identify each precedent named by issue 728.", + "Include a precedent only when an originating publication, maintaining-body standard, official documentation, or source repository supports cell-level extraction.", + "Use a fixed system version or full Git revision and retain the exact locator for every observation.", + "Retain systems with different purposes in separate scope strata; cross-stratum observations are descriptive only." + ], + "exclusion_criteria": [ + "Exclude systems represented only by secondary summaries or unpinned mutable material.", + "Exclude composite identities such as Cyber DEM/FOM or CRACK/VSDL; related systems receive independent rows.", + "Exclude feature counts that cannot be mapped to an axis rubric and primary-source rationale.", + "Exclude repository popularity, marketing language, and roadmap intent as implementation or adoption evidence." + ], + "scope_strata": [ + { + "stratum_id": "scenario-authoring", + "label": "Scenario authoring and realization", + "description": "Languages whose primary unit is an authored cyber-range scenario or infrastructure intent." + }, + { + "stratum_id": "agent-simulation", + "label": "Agent simulation environment", + "description": "An executable environment centered on agent actions, observations, rewards, and episodes." + }, + { + "stratum_id": "playbook-orchestration", + "label": "Security playbook orchestration", + "description": "A standardized workflow-exchange language rather than a cyber-range scenario language." + }, + { + "stratum_id": "federation-exchange", + "label": "Federation data exchange", + "description": "Runtime cyber object/event exchange and its HLA-specific federation realization." + } + ], + "systems": [ + { + "system_id": "aces", + "name": "ACES", + "version_label": "dev at 1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0", + "scope_stratum": "scenario-authoring", + "inclusion_rationale": "The subject system, pinned to the pre-implementation dev revision and assessed against repository authority plus executable delivery evidence.", + "exclusion_rationale": null + }, + { + "system_id": "ocr-sdl", + "name": "OCR SDL", + "version_label": "SDL-parser fe83e8281fc4b954967fbaa5a0d099007ddcb06c", + "scope_stratum": "scenario-authoring", + "inclusion_rationale": "A deployed YAML cyber-range authoring language with official documentation and a revision-pinned parser implementation.", + "exclusion_rationale": null + }, + { + "system_id": "cyborg", + "name": "CybORG", + "version_label": "paper 2108.09118v1 and source 2742b5e0ce4330c9b14006b38acd3b5ebe00d6fd", + "scope_stratum": "agent-simulation", + "inclusion_rationale": "An executable autonomous-cyber-operations gym explicitly named by the issue and grounded in an originating paper plus source.", + "exclusion_rationale": null + }, + { + "system_id": "cacao-v2", + "name": "CACAO 2.0", + "version_label": "OASIS Committee Specification 01, 27 November 2023", + "scope_stratum": "playbook-orchestration", + "inclusion_rationale": "A standardized security-playbook language that supplies a workflow and governance comparison boundary.", + "exclusion_rationale": null + }, + { + "system_id": "cyber-dem", + "name": "Cyber DEM", + "version_label": "SISO-STD-025-2023", + "scope_stratum": "federation-exchange", + "inclusion_rationale": "The solution-independent cyber object/event model named by the issue, assessed separately from its HLA realization.", + "exclusion_rationale": null + }, + { + "system_id": "cyber-fom", + "name": "Cyber FOM", + "version_label": "SISO-STD-025.3-2024", + "scope_stratum": "federation-exchange", + "inclusion_rationale": "The HLA-specific implementation of the Cyber DEM, independently attributable and standardized after the base model.", + "exclusion_rationale": null + }, + { + "system_id": "crack", + "name": "CRACK", + "version_label": "Computers & Security 95:101837 and source 1f22c729a379d09001f446d19e01a5561eed6ca9", + "scope_stratum": "scenario-authoring", + "inclusion_rationale": "A TOSCA/Datalog cyber-range scenario framework with formal verification and deployment testing evidence.", + "exclusion_rationale": null + }, + { + "system_id": "vsdl", + "name": "VSDL", + "version_label": "arXiv:2001.06681v1", + "scope_stratum": "scenario-authoring", + "inclusion_rationale": "A constraint-based virtual scenario language with SMT semantics, temporal guards, composition, and generated deployment scripts.", + "exclusion_rationale": null + } + ], + "axes": [ + { + "axis_id": "expressive-breadth", + "label": "Expressive breadth", + "construct": "Distinct authored concerns representable within the system's declared scope, demonstrated by constructs and representative tasks rather than field counts.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No authored representation for the representative concerns.", + "1": "One narrow concern family or indirect representation.", + "2": "Several concern families with material documented omissions.", + "3": "Broad, typed coverage across the representative concerns with explicit limits." + } + }, + { + "axis_id": "semantic-precision", + "label": "Semantic precision", + "construct": "Defined meaning, reference rules, ambiguity control, and semantic invariants, independent of parser strictness.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "Meaning is informal or unavailable for the assessed constructs.", + "1": "Basic structural meaning with substantial ambiguity or implicit references.", + "2": "Documented semantic rules and validations for major constructs.", + "3": "Explicit formal or normative semantics with cross-reference and invariant treatment." + } + }, + { + "axis_id": "formal-analyzability", + "label": "Formal analyzability", + "construct": "Explicit formal models and executable analyses over authored artifacts, not ordinary schema validation.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No formal model or executable formal analysis was observed.", + "1": "Formal fragments or lightweight invariants without whole-scenario analysis.", + "2": "Substantial formalization or analysis over a bounded subset.", + "3": "Executable solver/model analysis is central to validation or generation." + } + }, + { + "axis_id": "concrete-syntax-soundness", + "label": "Concrete-syntax soundness", + "construct": "Defined source profile, safe parsing, closed shape, reference checks, diagnostics, and negative-case rejection.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No concrete authoring syntax or validation boundary applies.", + "1": "Syntax is described but negative behavior or closure is weakly evidenced.", + "2": "Implemented parser/schema checks cover common positive and negative cases.", + "3": "Bounded parser, closed structural and semantic validation, diagnostics, and negative controls are evidenced." + } + }, + { + "axis_id": "composition-versioning", + "label": "Composition and versioning", + "construct": "Reuse, imports, identity, evolution, compatibility, migration, and version-bound composition.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No reusable or versioned composition surface was observed.", + "1": "Ad hoc reuse or release versioning without composition semantics.", + "2": "Reusable units or version identity with bounded compatibility rules.", + "3": "Explicit composition plus version, compatibility, and evolution semantics." + } + }, + { + "axis_id": "experiment-design", + "label": "Experiment design", + "construct": "Tasks, factors, allocation, randomization, measures, studies, and validity limits as declared research constructs.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No experiment-design construct was observed within the declared scope.", + "1": "Objectives or evaluation hooks without a controlled design model.", + "2": "Several explicit design constructs with documented external or missing obligations.", + "3": "Versioned task/study/run design with allocation, measures, provenance, and validity boundaries." + } + }, + { + "axis_id": "participant-modeling", + "label": "Participant modeling", + "construct": "Actions, observations, visibility, episodes, outcomes, and multi-participant interaction beyond generic roles.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No participant behavior model applies.", + "1": "Roles, actors, or static knowledge without action/observation lifecycle.", + "2": "Explicit action, observation, visibility, or episode constructs with material gaps.", + "3": "Executed action/observation/reward/episode behavior with partial-observability evidence." + } + }, + { + "axis_id": "provenance-evidence", + "label": "Provenance and evidence", + "construct": "Evidence identity, lineage, integrity, redaction, disclosure, and derivation rather than the mere existence of logs.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No structured provenance or evidence surface was observed.", + "1": "Logs, timestamps, or metadata without full evidence lineage.", + "2": "Structured provenance/evidence for major outputs with bounded gaps.", + "3": "Versioned evidence identity, lineage, integrity/disclosure, and derivation surfaces." + } + }, + { + "axis_id": "interoperability", + "label": "Interoperability", + "construct": "Defined exchange or conformance boundaries between independent implementations, not format export or backend count alone.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No independent exchange or conformance boundary was observed.", + "1": "A single implementation or informal adapter boundary.", + "2": "Published interface/profile or multiple implementations without full independent conformance evidence.", + "3": "Standardized exchange/conformance boundary with independent implementation or federation evidence." + } + }, + { + "axis_id": "usability", + "label": "Usability", + "construct": "Predeclared authoring-task completion, effort, errors, diagnostics, documentation support, and author-population limits.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No authoring surface or task evidence applies.", + "1": "Examples or maintainer walkthroughs only; no independent author evidence.", + "2": "Documented workflows and task support with limited user evidence.", + "3": "Independent, disclosed author-task study with completion, effort, and error measures." + } + }, + { + "axis_id": "implementation-maturity", + "label": "Implementation maturity", + "construct": "Shipped executable behavior, releases, conformance evidence, maintenance, and independent use rather than accepted designs.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "Concept or prose only for the assessed surface.", + "1": "Prototype or materially incomplete implementation.", + "2": "Maintained implementation with releases or operational use but limited independent conformance/adoption evidence.", + "3": "Mature maintained implementation with release, operational, and independent-use or conformance evidence." + } + }, + { + "axis_id": "governance-community", + "label": "Governance and community", + "construct": "Standards/governance process, independent participation, adoption, and maintenance continuity rather than repository popularity.", + "direction": "higher-is-stronger-evidence", + "rubric": { + "0": "No durable governance or independent community evidence was observed.", + "1": "Single-project governance or maintainer-led continuity.", + "2": "Documented multi-party process or independent participation with limited standardization/adoption evidence.", + "3": "Recognized standards governance or sustained independent multi-party adoption and maintenance." + } + } + ], + "cases": [ + { + "case_id": "author-multihost-objective", + "kind": "authoring-task", + "title": "Author a multi-host scenario with an objective", + "authored_requirement": "Represent three networked hosts, service/software intent, two participant roles, and a success objective without implementation-specific credentials.", + "unit_of_observation": "One system-native authored artifact and its documented validation path.", + "applicability_rule": "Applicable when the system claims scenario, playbook, environment, or federation authoring for at least one named construct; otherwise record out of scope.", + "permitted_assistance": "Pinned official examples and reference documentation; no unstated extensions.", + "inputs": "A bounded prose brief naming topology, roles, service intent, and success condition.", + "expected_artifact": "A native artifact or source-grounded walkthrough identifying represented and unrepresented parts.", + "criteria": { + "success": "All applicable parts have native constructs and a documented validation path.", + "partial": "At least one applicable part requires prose, code, or an external plane.", + "failure": "The central authored unit cannot be represented within the system's declared scope." + } + }, + { + "case_id": "author-versioned-composition", + "kind": "authoring-task", + "title": "Compose and version reusable authored material", + "authored_requirement": "Reuse a separately identified authored unit, bind its revision, and state compatibility or replacement behavior.", + "unit_of_observation": "One composition/versioning walkthrough using the pinned source boundary.", + "applicability_rule": "Applicable to systems that accept reusable authored units; out of scope for pure runtime exchange with no authoring composition.", + "permitted_assistance": "Pinned standards, source, and official examples.", + "inputs": "A base unit, a dependent unit, and a requested revision change.", + "expected_artifact": "A native reference/import/sub-playbook/composition mechanism or an explicit unsupported result.", + "criteria": { + "success": "Identity, revision, composition, and compatibility behavior are explicit.", + "partial": "Reuse exists but revision or compatibility behavior remains implicit.", + "failure": "No native reuse/composition mechanism was found." + } + }, + { + "case_id": "author-controlled-experiment", + "kind": "authoring-task", + "title": "Declare a controlled participant experiment", + "authored_requirement": "Declare participants, a task, factor levels, allocation or randomization, measures, evidence, and a study/run boundary.", + "unit_of_observation": "One source-grounded mapping from the brief to native constructs and named external obligations.", + "applicability_rule": "Applicable when experiment, evaluation, training, or agent-study claims are in scope; otherwise out of scope.", + "permitted_assistance": "Pinned standards, source, official examples, and ACES experiment contracts.", + "inputs": "A two-condition participant study with a preserved seed, outcome measure, and evidence requirement.", + "expected_artifact": "Native study/task/run constructs or a bounded declaration of external/missing concerns.", + "criteria": { + "success": "All design elements are first-class and version/provenance bound.", + "partial": "Objectives or measures exist but allocation, study, evidence, or participant semantics are external or missing.", + "failure": "No applicable experiment-design surface was observed." + } + }, + { + "case_id": "negative-dangling-reference", + "kind": "negative-case", + "title": "Reject a dangling authored reference", + "authored_requirement": "Introduce exactly one reference to an undeclared node, step, object, participant, or federation class.", + "unit_of_observation": "The documented or executable validation result for the single injected defect.", + "applicability_rule": "Applicable when the system has cross-object authored references; otherwise out of scope.", + "permitted_assistance": "Pinned parser tests, validation rules, standards text, or formal-analysis descriptions.", + "inputs": "A valid representative artifact plus one dangling identifier.", + "expected_artifact": "A bounded rejection/diagnostic or an explicit finding that the check is absent/not evidenced.", + "criteria": { + "success": "The production or normative validation boundary requires rejection with a useful location.", + "partial": "The constraint is stated but executable rejection or diagnostics are not evidenced.", + "failure": "The dangling reference is accepted or no validation rule was found." + } + }, + { + "case_id": "negative-contradictory-constraints", + "kind": "negative-case", + "title": "Reject contradictory authored constraints", + "authored_requirement": "Inject one contradiction between two otherwise valid constraints, branches, resource bounds, or required semantic outcomes.", + "unit_of_observation": "The formal, semantic, or normative result for the single contradiction.", + "applicability_rule": "Applicable when the system claims semantic, formal, workflow, or conformance validation; otherwise out of scope.", + "permitted_assistance": "Pinned formal semantics, validator tests, standards conformance rules, and source documentation.", + "inputs": "A valid representative artifact plus one mutually unsatisfiable condition.", + "expected_artifact": "Unsatisfiable/rejected result or a bounded not-evaluated finding.", + "criteria": { + "success": "The owning analysis detects the contradiction before successful execution/exchange.", + "partial": "Only a bounded structural contradiction is detected or the rule lacks executable evidence.", + "failure": "No applicable contradiction check was observed." + } + } + ], + "analysis_rules": { + "primary_result": "Publish the evidence surface and scope-qualified Pareto frontiers; do not publish a universal total score.", + "pareto_scope": "Compute dominance only within a declared scope stratum and only over axes applicable to every member of that group.", + "missing_handling": "Keep out-of-scope, not observed, not implemented, and not evaluated distinct; never coerce them to zero.", + "weighting_policy": "Predeclare bounded integer weight profiles before reading their rankings and retain every result.", + "reversal_policy": "If reasonable profiles change the first-ranked system, disclose the reversal and prohibit a weight-independent winner headline.", + "quality_claim_policy": "A highest-quality claim requires a separate validated quality construct and is not supported by this protocol.", + "claim_evidence_policy": "Every public claim records ADR-021 evidence status, threats to validity, a falsification protocol, objective pass/fail criteria, allowed and disallowed evidence, named evidence artifacts, and a derivation recomputed from the frozen observations." + }, + "amendment_log": [] +} diff --git a/docs/research/related-work-comparison/search-log.md b/docs/research/related-work-comparison/search-log.md index 2b11a1be3..764560cca 100644 --- a/docs/research/related-work-comparison/search-log.md +++ b/docs/research/related-work-comparison/search-log.md @@ -1,380 +1,178 @@ -# Related-Work Comparison Search Log - -Issue: #508 (review LIT-1 / LIT-4). - -Purpose: record the primary sources and grounded findings behind every non-ACES -cell in [`docs/explain/sdl/related-work-comparison.md`](../../explain/sdl/related-work-comparison.md). -Each precedent's capabilities were checked against its own documentation, -standard text, source, or originating-author literature — not from memory. - -## Source Rule - -Primary sources only for competitor capability claims: the maintaining body's -standard, the originating authors' papers, official project documentation, or -the project's own source repository. Where only a secondary source was -available, it is marked secondary. Absence-of-feature claims are made only after -searching the primary documentation and source for the feature. - -## Tooling - -- Web search and direct page/document fetch for standards, official - documentation, and source repositories. -- Direct reads of project source (Rust parser, Python reference implementation, - scenario/topology YAML, TOSCA type definitions) where the source is the - authoritative encoding of the model. -- Crossref / arXiv / Semantic Scholar for DOIs and originating-author papers. - -The eight comparison dimensions are defined in the comparison page; the findings -below are organized by precedent and then by dimension. - -## Comparison Dimensions - -1. Runtime inventory depth — observed runtime node/service state as first-class - declarative inventory, distinct from authored topology. -2. Typed relationship subtypes — typed, directed relationship edges between - elements, beyond plain topology links. -3. Participant behavior / episode contracts — actions, observations, rewards, - episodes, partial observability as first-class semantics. -4. Authoring-vs-instantiation separation — a logical authored surface distinct - from concrete deployment/instantiation. -5. Backend agnosticism + conformance — the same scenario across multiple - backends, with a conformance contract. -6. Declarative objectives / workflows — objectives and workflow graphs as - authored constructs. -7. Provenance / disclosure surfaces — run provenance, evidence, and participant - information-boundary/disclosure as explicit artifacts. -8. Time semantics status — clock authority, logical/virtual time, ordering, - causality, pacing/synchronization. - ---- - -## Open Cyber Range (OCR) SDL - -YAML-based, VM-centric authoring language for cyber defense/training exercises -(infrastructure, deployable software features, scoring objectives, narrative -event timelines, participant teams), implemented as a Rust parser used across -the Open Cyber Range platform (Ranger / Handlers / Deputy). - -Primary sources: - -- SDL Reference — Open Cyber Range documentation. - and - -- SDL-parser source (Rust) — Open-Cyber-Range, GitHub. - (`node.rs`, `feature.rs`, - `infrastructure.rs`, `inject.rs`, `event.rs`, `script.rs`, `story.rs`, - `entity.rs`, `metric.rs`, `evaluation.rs`, `vulnerability.rs`) -- Ranger / VMware Handlers / Deputy — Open Cyber Range documentation. - , - -- Kaunis, K. *Hypervisor Agnostic Scenario Definition Language for Cyber - Ranges.* TalTech MSc thesis, 2022 (design intent / scope). - -Findings: - -1. Runtime inventory depth — **no.** Nodes are `Switch | VM`; Features are - `service | configuration | artifact` install/configure actions - (deployment intent), not observed runtime state. No directory, datastore, - mail, DNS, or RBAC node types (SDL Reference; SDL-parser `node.rs`, - `feature.rs`). -2. Typed relationship subtypes — **partial.** `InfraNode.links` (network) and - `InfraNode.dependencies` (deploy order) are structurally distinct, and - injects carry `from-entity`/`to-entities`; all are string references, with no - trust/integration edge type or edge-type discriminator (SDL-parser - `infrastructure.rs`, `inject.rs`). -3. Participant behavior / episode contracts — **partial.** Entities carry - exercise roles (White/Green/Red/Blue) and receive event-triggered injects; - condition-driven Metrics score trainees 0–1. No agent actions, observations, - rewards, or episode/partial-observability semantics (SDL Reference, Entities - and Metrics). -4. Authoring-vs-instantiation separation — **partial.** Node templates and - `count`/infrastructure addressing separate capability from instances - (Templater builds reusable VM templates; Machiner instantiates), but the - language is the VM-deployment spec, not a backend-neutral logical surface - (SDL Reference, Infrastructure; VMware Handlers). -5. Backend agnosticism + conformance — **partial.** Ranger exposes a - "virtualization-platform-agnostic" gRPC handler contract, but the only - shipped handler suite is VMware vSphere/NSX-T and no backend conformance - test suite was found (Ranger docs; VMware Handlers docs). -6. Declarative objectives / workflows — **yes.** Goals → TLOs → Evaluations → - Metrics → Conditions scoring chain, plus Stories → Scripts → Events → - Injects narrative timelines with parallel storylines and AND-gated condition - triggers (SDL Reference and Example). -7. Provenance / disclosure surfaces — **partial.** Inject stdout/stderr are - captured to the manager view and events are timestamped and scoped per - entity, but the SDL document has no first-class provenance/disclosure - construct (Executor / Ranger participant-guide docs). -8. Time semantics status — **partial.** Script `start-time`/`end-time`, `speed` - multipliers, and condition poll `interval` give relative/narrative pacing; - no clock authority, logical time, or causality model (SDL Reference, - Scripts/Stories; SDL-parser `script.rs`, `story.rs`). - ---- - -## CybORG (CAGE Challenge) - -Discrete-step reinforcement-learning gym for autonomous cyber operations, with a -shared OpenAI-gym interface over both a simulation backend and an AWS emulation -backend, used across the CAGE Challenge series. - -Primary sources: - -- Standen, M., Lucas, M., Bowman, D., Richer, T.J., Kim, J., Marriott, D. - "CybORG: A Gym for the Development of Autonomous Cyber Agents." arXiv:2108.09118, - 2021. -- CybORG source and CAGE Challenge scenario files — cage-challenge, GitHub. - , - CAGE Challenge 2 `Scenario1b.yaml`, CAGE Challenge 4 challenge details - (). - -Findings: - -1. Runtime inventory depth — **partial.** Host YAML declares OS, services, - processes, users, and subnets and the simulator tracks live session/process - state as a finite-state machine, but this is internal simulation state, not a - separate first-class declarative runtime-inventory surface (arXiv:2108.09118, - §2 "Scenario"). -2. Typed relationship subtypes — **partial.** Host-to-subnet membership, - process parent/child, and subnet NACL rules are implicit in the YAML and the - FSM; there is no named, directed typed-edge vocabulary (arXiv:2108.09118 §2; - `Scenario1b.yaml`). -3. Participant behavior / episode contracts — **yes.** Per-agent action spaces, - role-filtered observations, per-agent reward calculators, partial - observability, and step- or goal-bounded episodes are first-class; CAGE 4 - adds multi-agent coordination with restricted inter-agent messaging - (arXiv:2108.09118 §2; `Scenario1b.yaml`; CAGE 4 details). -4. Authoring-vs-instantiation separation — **partial.** A scenario file deploys - to sim or emulation and re-randomizes on reset; the `ScenarioGenerator` - abstraction separates generation from use, but the boundary is a Python-code - interface, not a declared logical/instance schema (arXiv:2108.09118 §3; - CybORG changelog v3.0). -5. Backend agnosticism + conformance — **partial.** A genuine dual sim+emulation - design sits behind one gym interface (each action defined for both), but - backend equivalence is asserted/empirical, not a published conformance - contract (arXiv:2108.09118 §2–3). -6. Declarative objectives / workflows — **partial.** Objectives are encoded in - reward-calculator classes and episode-termination conditions, computed in - code; there is no declarative objective expression or workflow graph - (`Scenario1b.yaml` `reward_calculator_type`; arXiv:2108.09118). -7. Provenance / disclosure surfaces — **no.** Role-based observation filtering is - an RL mechanism enforced at runtime, not a declared disclosure-boundary or - provenance artifact; evaluation emits text result files, not structured - provenance (arXiv:2108.09118 §2; CAGE 4 details). -8. Time semantics status — **partial.** Discrete steps with a fixed agent order, - and CAGE 4 variable action durations (ticks); no clock authority, - logical-time, or causality specification (arXiv:2108.09118 §2; CAGE 4 - details). - ---- - -## CACAO Security Playbooks v2.0 (OASIS) - -JSON schema and taxonomy for documenting, sharing, and orchestrating -cybersecurity response/detection/mitigation workflows across organizational and -tooling boundaries — a playbook workflow standard, not a cyber-range topology or -scenario DSL. - -Primary source: - -- CACAO Security Playbooks Version 2.0, OASIS Committee Specification 01, - 27 November 2023. - - -Findings (section numbers are CACAO v2.0): - -1. Runtime inventory depth — **no.** Agents and Targets (§7) are authoring-time - connection references for command dispatch; runtime node/service state is - explicitly out of scope. -2. Typed relationship subtypes — **partial.** Workflow steps carry typed, - directed routing edges (`on_completion`, `on_success`/`on_failure`, - `on_true`/`on_false`, `cases`, `next_steps`), but there is no general typed - inter-object relationship graph (§4). -3. Participant behavior / episode contracts — **no.** No agent observation, - reward, episode, or partial-observability concept; the standard orchestrates - command execution (§1, §7). -4. Authoring-vs-instantiation separation — **yes.** `playbook_variables` are - authored; `__variable__` substitution and step `in_args`/`out_args` resolve - values at execution time, with step scope overriding playbook scope - (§3.1, §4.1, §10.18). -5. Backend agnosticism + conformance — **partial.** Producer/consumer - conformance classes and abstract agent/target plus multi-engine command - types support exchange, but the spec notes playbooks "will require some amount - of modification" per environment, and defines no equivalence test suite (§11). -6. Declarative objectives / workflows — **yes.** A full declarative workflow - graph with eight step types (start, end, action, playbook-action, parallel, - if-condition, while-condition, switch-condition), conditional routing, and - nested playbook invocation (§4). -7. Provenance / disclosure surfaces — **yes.** First-class digital signatures - (JSON Signature Scheme, embedded/detached, quantum-safe option), TLP/IEP/ - statement data markings applied to all objects, and `created_by` provenance - (§2.4, §2.5, §9). -8. Time semantics status — **partial.** Step `delay`/`timeout` and playbook - `valid_from`/`valid_until`; no clock authority or causal-ordering semantics - (§3.1, §4.1). - ---- - -## SISO Cyber DEM and Cyber FOM - -A runtime data-exchange model (not a scenario-authoring DSL) defining a shared -ontology of cyber objects and events/effects so cyber conditions can be exchanged -bi-directionally between cyber ranges, cyber simulations, and the -Live-Virtual-Constructive environments of kinetic simulation. The Cyber FOM is -the HLA-specific federation object model derived from it. - -Primary sources: - -- SISO-STD-025-2023, Cyber Data Exchange Model (DEM) — SISO, 2023. - -- SISO-STD-025.3-2024 Cyber Federation Object Model and SIRL User's Guide — - SISO, 2024. - -- cyberdem-python reference implementation of the CyberDEM object/event model — - CMU SEI, GitHub. - (`base/__init__.py`, - `enumerations/__init__.py`) -- IEEE Std 1516 (High Level Architecture) for the inherited federation and time - management framework (secondary, for HLA mechanics). - -Findings: - -1. Runtime inventory depth — **partial.** Defines exchangeable cyber objects - (Device, System, Application, OperatingSystem, Service, Network, NetworkLink, - Data) as transmitted model-state; this is live federation exchange, not an - authored per-node declarative inventory (cyberdem-python `base`). -2. Typed relationship subtypes — **yes.** A first-class `Relationship` object - with a `RelationshipType` enumeration of ten directed subtypes - (Administers/AdministeredBy, ComponentOf/HasComponent, ContainedIn/Contains, - ProvidedBy/Provides, ResidesOn/HasResident) (cyberdem-python `enumerations`). -3. Participant behavior / episode contracts — **partial.** Typed action/effect - events (CyberAttack/CyberDefend/CyberRecon with MITRE ATT&CK references; a - Deny/Detect/Manipulate effect hierarchy) carry `actor_ids`, `target_ids`, and - a `phase`; no observation model, reward, or episode boundary - (cyberdem-python `base`). -4. Authoring-vs-instantiation separation — **out of scope.** Objects and events - are exchanged between running federates at runtime; there is no authored - scenario surface compiled to an instance (SISO-STD-025-2023 scope; - originating-author statement that CyberDEM represents cyber events/objects in - a format independent of simulation interoperability solutions). -5. Backend agnosticism + conformance — **yes.** Designed to be independent of, - but unambiguously translatable to, HLA/TENA/DIS/JSON; the Cyber FOM is an - IEEE 1516-compliant HLA FOM, inheriting formal HLA federate conformance - (SISO-STD-025-2023; SISO-STD-025.3-2024 Cyber FOM; IEEE 1516). -6. Declarative objectives / workflows — **no.** No objective, mission-goal, or - workflow construct; `CyberOrder` is a command/control directive, not a - declarative objective or workflow graph (cyberdem-python `base`; - SISO-STD-025-2023 scope). -7. Provenance / disclosure surfaces — **partial.** A `SensitivityType` - classification (17 values) plus `Data.confidentiality`, `encrypted`, and - `status` track information state, but there is no participant - information-boundary or evidence/provenance artifact (cyberdem-python - `enumerations`, `base`). -8. Time semantics status — **yes.** `_CyberEvent` carries `event_time` and - `duration`, and the Cyber FOM inherits the full HLA time-management stack - (time-advance request/grant, Time Stamp Order delivery, lookahead, GALT, - time-constrained/regulating roles) (cyberdem-python `base`; IEEE 1516 time - management; SISO-REF-072-2024). - ---- - -## Academic range DSLs: CRACK, KYPO, CyRIS - -Academic cyber-range systems for security-training exercise generation, -deployment, and (CRACK) formal verification. The comparison column leads with -**CRACK** and notes KYPO/CyRIS where they differ. - -Primary sources: - -- Russo, E., Costa, G., Armando, A. "Building next generation Cyber Ranges with - CRACK." *Computers & Security* 95:101837, 2020. DOI: - [10.1016/j.cose.2020.101837](https://doi.org/10.1016/j.cose.2020.101837). -- Russo, E., Costa, G., Armando, A. "Scenario Design and Validation for Next - Generation Cyber Ranges." IEEE NCA 2018. IEEE Xplore 8548324. -- CRACK source (TOSCA type definitions) — enricorusso/CRACK, GitHub. - -- Vykopal, J., et al. "KYPO Cyber Range: Design and Use Cases." ICSOFT 2017. - DOI: [10.5220/0006428203100321](https://doi.org/10.5220/0006428203100321). -- KYPO hands-on training behavior dataset, PMC10770710, 2024; - KYPO platform documentation, . -- Pham, C., Tang, D., Chinen, K.-I., Beuran, R. "CyRIS: A Cyber Range - Instantiation System for Facilitating Security Training." SoICT 2016. DOI: - [10.1145/3011077.3011087](https://doi.org/10.1145/3011077.3011087); - source — crond-jaist/cyris, GitHub. - -Findings (verdict leads with CRACK): - -1. Runtime inventory depth — **partial.** CRACK node types carry `runtime` - Datalog predicate maps (`isConnected`, `listeningOn`, `hostACL`, - `existsRoute`) executed as live checks to confirm authored predicates, not a - structured inventory record; KYPO/CyRIS topology is static authored YAML - (Russo et al. 2020; CRACK `types/`; Vykopal et al. 2017; Pham et al. 2016). -2. Typed relationship subtypes — **yes.** CRACK defines typed, directed TOSCA - relationships (e.g., `SetsWeakPassword`, `SetsEnumerableUsername`) and typed - capability kinds (VulnerabilityContainer, PrivilegeProvider, KnowledgeProvider, - GoalProvider, PrincipalProvider); KYPO/CyRIS encode connectivity edges only - (CRACK `types/sdl.yaml`; Russo et al. 2020). -3. Participant behavior / episode contracts — **partial.** CRACK `Principal` - nodes carry a role and a knowledge requirement, with Goals linked to - principals; these are static role/knowledge assignments, not action/ - observation/reward/episode contracts. KYPO prescribes tasks/scoring; CyRIS - models none (CRACK `types/sdl-principal.yaml`, `sdl-goal.yaml`; - PMC10770710). -4. Authoring-vs-instantiation separation — **yes.** CRACK separates the SDL - specification from instantiation via an ARIA TOSCA orchestrator that - generates OpenStack/Terraform/Packer deploy scripts; KYPO separates a sandbox - definition from pool allocation (Russo et al. 2020; Vykopal et al. 2017). -5. Backend agnosticism + conformance — **no.** CRACK and KYPO target OpenStack - only; CyRIS supports KVM and AWS but with no cross-backend conformance suite - (CRACK `types/openstack-*.yaml`; KYPO OpenStack requirements; Pham et al. - 2016). -6. Declarative objectives / workflows — **yes.** CRACK `Goal` node subtypes - (CanReach, GainPrivilege, Knows) and `Invariant` types are first-class - declarative constructs verified against the model; KYPO encodes - human-readable objectives plus flag answers (CRACK `types/sdl-goal.yaml`, - `sdl-invariant.yaml`; Russo et al. 2020; PMC10770710). -7. Provenance / disclosure surfaces — **no (KYPO partial).** CRACK `Knows` - models knowledge as a verification predicate, not a disclosure/provenance - artifact; KYPO collects observational event logs and command histories, not a - first-class authored construct (CRACK `types/`; PMC10770710 §3.3–3.4). -8. Time semantics status — **no (KYPO partial).** CRACK's Datalog encoding is - atemporal (set-theoretic reachability/privilege); KYPO records timestamps and - notes absolute time does not order events across trainees (relative - timestamps added); CyRIS has only an `attack_time` date (Russo et al. 2018; - PMC10770710 §3.5; CyRIS `examples/full.yml`). - -CRACK's distinguishing strength is machine-checkable formal verification: -SDL specifications are encoded into Datalog and checked against validation -goals, and verification traces are turned into runtime conformance test cases -(Russo et al. 2018; Russo et al. 2020). ACES's SMT/formal scenario verification -is deferred (see [`precedents.md`](../../explain/sdl/precedents.md), "VSDL SMT -verification"), so this is a genuine dimension where this class leads ACES. - ---- - -## Precedents Scoped Out Of The Matrix - -These README-lineage precedents are not comparison columns because their purpose -is not scenario authoring or runtime scenario modeling. Each is accounted for so -the matrix coverage is complete. - -- **OCSF** — a normalized security event/finding schema. ACES borrows its - observation/evidence style; it is not a scenario-authoring or runtime-modeling - language (). -- **STIX 2.1** — a cyber-threat-intelligence object and relationship model. ACES - adapts its typed-relationship pattern for scenario elements; STIX models - threat intelligence, not scenarios - (). -- **TENA** — a runtime test-range integration architecture and object-model - middleware for live test ranges, not an authoring DSL; relevant to runtime - federation maturity, captured under dimensions 5 and 8 - (). -- **IEEE HLA (1516)** — a distributed-simulation interoperability architecture - (RTI, FOM, time management), the substrate the Cyber FOM builds on, not an - authoring DSL; its time-management strength is represented through the SISO - Cyber DEM/FOM column (). -- **MITRE CALDERA** — an adversary-emulation execution platform; ACES treats it - as a behavior/execution source scenarios may bind to, not a scenario DSL - (). -- **Atomic Red Team** — a library of ATT&CK-mapped atomic test definitions; - test-execution content, not a scenario DSL - (). +# Related-Work Comparison Search And Freeze Log + +Protocol: `protocol-v1`. Snapshot: `snapshot-2026-07-13`. + +This log records how the source set was selected and frozen. The extraction +snapshot is the cell-level record; it contains the exact locator and rationale +for every system-axis and system-case observation. + +## Search Rule + +Capability claims use primary sources only: + +- a maintaining body's exact standard; +- an originating-author publication; +- official project documentation; or +- the project's own source at a full Git commit. + +Mutable official documentation requires a retrieval date and SHA-256 digest. +Git evidence requires a full 40-hex commit and a repository artifact path. +Publications and standards require an exact version, edition, DOI, or arXiv +revision. A missing capability is recorded only against the declared source +boundary and extraction procedure; it is not treated as proof of universal +absence. + +## Corpus Freeze + +The protocol includes eight independent identities: + +- **ACES**, Open Cyber Range SDL, CRACK, and VSDL in the scenario-authoring + stratum; +- **CybORG** in the agent-simulation stratum; +- **CACAO 2.0** in the playbook-orchestration stratum; and +- **Cyber DEM** and **Cyber FOM** in the federation-exchange stratum. + +Cyber DEM and Cyber FOM are separate because the former is the +solution-independent model and the latter is its HLA-specific implementation. +CRACK and VSDL are separate because their syntax, formal analysis, delivery +pipeline, and evidence are independently attributable. KYPO and CyRIS from the +earlier comparison are excluded from this fixed corpus rather than folded into +a CRACK family column. + +## Pinned External Sources + +### Open Cyber Range SDL + +- [SDL-parser commit `fe83e8281fc4b954967fbaa5a0d099007ddcb06c`](https://github.com/Open-Cyber-Range/SDL-parser/tree/fe83e8281fc4b954967fbaa5a0d099007ddcb06c), + especially `sdl-parser/src/` and its parser snapshots. +- [Official SDL Reference Guide](https://documentation.opencyberrange.ee/docs/sdl/reference/), + retrieved 2026-07-13, SHA-256 + `96d4585aec45b92548d6615de8b0db3c2a076a9d8ddec4877edc0f6098cfcbc4`. + +The source and reference cover nodes, infrastructure, features, roles, +entities, stories, scripts, injects, metrics, evaluations, TLOs, and Goals. +The parser repository is the executable evidence boundary; the mutable guide is +used for author-facing semantics and examples. + +### CybORG + +- Standen et al., [*CybORG: A Gym for the Development of Autonomous Cyber + Agents*, arXiv:2108.09118v1](https://arxiv.org/abs/2108.09118v1), SHA-256 of + the v1 PDF + `69fc40a8ca959adb3333a2716fa4bc6f52bf1e3bebf76129fcf42fe9891ed783`. +- [CybORG commit `2742b5e0ce4330c9b14006b38acd3b5ebe00d6fd`](https://github.com/cage-challenge/CybORG/tree/2742b5e0ce4330c9b14006b38acd3b5ebe00d6fd), + including `ScenarioGenerator`, `Scenario1b.yaml`, actions, observations, and + reward calculators. + +The paper supplies the common simulation/emulation and research-purpose claims. +The source supplies executable agent, scenario, observation, reward, and +episode evidence. + +### CACAO 2.0 + +- OASIS, [*CACAO Security Playbooks Version 2.0*, Committee Specification 01, + 27 November 2023](https://docs.oasis-open.org/cacao/security-playbooks/v2.0/cs01/security-playbooks-v2.0-cs01.html), + SHA-256 of the CS01 PDF + `584eae31ad4be42b6363a600d433d22d460b1f749916a0e3c4cca90ab8b2f428`. + +The exact Committee Specification, not the moving `latest` URL, is the source +for workflow objects, variables, sub-playbooks, agents/targets, data markings, +signatures, mandatory features, and producer/consumer conformance. + +### Cyber DEM + +- SISO, [SISO-STD-025-2023 Cyber Data Exchange Model](https://cdn.ymaws.com/www.sisostandards.org/resource/resmgr/standards_products/siso-std-025-2023_cyberdem.pdf), + SHA-256 + `e351e2c73ede003cea62c1ec0c22acb4430220b4d033c4de625645c6a8560e72`. +- [cyberdem-python commit `577543d53fa9ffb2b818a91e0ed61881c9bd7bfa`](https://github.com/cmu-sei/cyberdem-python/tree/577543d53fa9ffb2b818a91e0ed61881c9bd7bfa), + especially `cyberdem/base/` and `cyberdem/enumerations/`. + +The standard defines the solution-independent exchange boundary. The reference +implementation supplies object, relationship, event, effect, sensitivity, and +enumeration evidence without being treated as proof of independent adoption. + +### Cyber FOM + +- SISO, [SISO-STD-025.3-2024 Cyber Federation Object Model](https://www.sisostandards.org/page/StandardsProducts). + +The exact standard identity is the evidence boundary. The SISO product catalog +states that the Cyber FOM is the HLA-specific implementation of the Cyber DEM. +No Cyber DEM reference-implementation behavior is silently credited to the +Cyber FOM row. + +### CRACK + +- Russo, Costa, and Armando, [*Building next generation Cyber Ranges with + CRACK*, Computers & Security 95:101837](https://doi.org/10.1016/j.cose.2020.101837). +- [CRACK commit `1f22c729a379d09001f446d19e01a5561eed6ca9`](https://github.com/enricorusso/CRACK/tree/1f22c729a379d09001f446d19e01a5561eed6ca9), + especially `types/sdl*.yaml`, runtime predicates, and OpenStack plugin types. + +The paper supplies the design, Datalog verification, generation, and automated +runtime-test claims. The source supplies the TOSCA types and pinned deployment +boundary. + +### VSDL + +- Costa, Russo, and Armando, [*Automating the Generation of Cyber Range Virtual + Scenarios with VSDL*, arXiv:2001.06681v1](https://arxiv.org/abs/2001.06681v1), + SHA-256 of the v1 PDF + `d8b505602afee6962c39f02d6691fc5ab7aba77acb3ea35760bb8211e85381b4`. + +The paper is the fixed boundary for syntax, QFLIA/SMT semantics, +satisfiability, temporal guards, composition, and generated OpenStack, +Terraform, and Packer scripts. No maintained public source revision was added +to this snapshot, so implementation-maturity and negative-diagnostic claims +remain limited. + +## ACES Freeze + +ACES is pinned to pre-change `dev` commit +[`1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0`](https://github.com/Brad-Edwards/aces/tree/1b63b2a0b10dcd80e29b8bad14558ad9c6b706d0). +The snapshot records per-file hashes for the cited authoring schema, production +parser, parser tests, participant semantics, experiment study contract, +scientific-completeness assessment, composition model, lineage ledger, backend +profile, author guide, and authority ADR. + +ACES implementation claims require executable evidence where the axis concerns +shipped behavior. The scientific-completeness assessment remains the delivery +truth for partial, missing, external-contract, and deliberately-excluded +concerns. Accepted designs and formal prose do not upgrade implementation +maturity. + +## Authoring Tasks And Negative Cases + +The protocol freezes three representative tasks: + +1. author a multi-host scenario with roles and an objective; +2. compose and version reusable authored material; and +3. declare a controlled participant experiment. + +It also freezes two single-defect negative cases: a dangling reference and +contradictory constraints. The snapshot contains every system-case pair. ACES +uses existing repository execution where the cited production/tests already +exercise the boundary. External systems use source walkthroughs, which are +identified as such and do not become usability or execution claims. + +## Analysis Freeze + +Pareto dominance is computed only within a scope stratum and only over axes +applicable to every system in that group. Four scenario-authoring weight +profiles were fixed: equal evidence, breadth/composition, formal rigor, and +maturity/governance. Their first-ranked systems differ. The analysis therefore +records a sensitivity reversal and prohibits a weight-independent winner. + +`Out of scope`, `not observed`, `not implemented`, and `not evaluated` remain +distinct. The checker never coerces `out of scope` to zero. + +## Archival And Security Boundary + +The repository stores metadata, digests, precise locators, and paraphrased +findings. It does not store unrestricted copies of standards, papers, +documentation, or source repositories. Normal checks perform no network fetch, +clone, package installation, shell evaluation, or third-party execution. Source +locators must use HTTPS and cannot contain URI userinfo or secret-bearing query +parameters. + +Changing a rubric or corpus rule requires a new protocol revision. Refreshing a +source requires a new extraction snapshot. The current protocol amendment log +is empty. diff --git a/docs/specs/formal.md b/docs/specs/formal.md index 44c54edf5..81913a97f 100644 --- a/docs/specs/formal.md +++ b/docs/specs/formal.md @@ -28,6 +28,10 @@ formal artifacts are warranted. realization boundaries, backend realization support, and proposed realization-envelope membership, subsumption, witness, and negative conformance semantics +- **Behavioral Relations** (`specs/formal/behavioral-relations/`) -- Revisioned + validity, conformance, trace, simulation, refinement, bisimulation, + participant-projection, strategic, probabilistic, statistical, and empirical + claim semantics with explicit evidence and assurance boundaries ## FM Classification diff --git a/examples/scenarios/enterprise-participant-evidence-loop.README.md b/examples/scenarios/enterprise-participant-evidence-loop.README.md index 5afffd6d1..35d094990 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.README.md +++ b/examples/scenarios/enterprise-participant-evidence-loop.README.md @@ -98,7 +98,8 @@ substrate realization #601, backend manifest/capability contracts, and the experiment/evaluation contracts) into one artifact carrying scenario+compiled identity, backend manifest/capability profile and realization provenance, the realized/planned topology and network-attachment matrix, the participant action -proof, the terminal behavior-history-equivalent observation, evaluator-only +proof, the terminal participant-projected history under the named observation +boundary (explicitly not an equivalence comparison), evaluator-only Wazuh/SOC evidence, negative boundary checks, an evaluator outcome record, and redaction/provenance metadata. Embedded published-contract payloads (`BackendManifestV2Model`, `EvaluationResultStateModel`, diff --git a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml index 54ff07758..499bda883 100644 --- a/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml +++ b/examples/scenarios/enterprise-participant-evidence-loop.sdl.yaml @@ -50,10 +50,7 @@ nodes: - {name: wazuh-api, port: 55000, protocol: tcp} - {name: agent-events, port: 1514, protocol: tcp} - {name: syslog, port: 514, protocol: udp} - runtime: - health: - status: healthy - description: Runtime healthcheck must pass before evaluator evidence is complete. + conditions: {wazuh-manager-health: ""} wazuh-indexer: type: vm os: linux @@ -193,6 +190,10 @@ content: tags: [hidden-truth] conditions: + wazuh-manager-health: + command: /usr/local/bin/check-wazuh-manager + interval: 30 + description: Declarative healthcheck required before evaluator evidence is complete. participant-observation-recorded: proposition: participant-observation-recorded command: /usr/local/bin/check-participant-observation diff --git a/examples/scenarios/techvault-attacker-target.sdl.yaml b/examples/scenarios/techvault-attacker-target.sdl.yaml index 795d3237a..33c93dfbb 100644 --- a/examples/scenarios/techvault-attacker-target.sdl.yaml +++ b/examples/scenarios/techvault-attacker-target.sdl.yaml @@ -41,10 +41,7 @@ nodes: - {name: wazuh-api, port: 55000, protocol: tcp} - {name: agent-events, port: 1514, protocol: tcp} - {name: syslog, port: 514, protocol: udp} - runtime: - health: - status: healthy - description: Runtime healthcheck must pass before public startup is ready. + conditions: {wazuh-manager-health: ""} wazuh-indexer: type: vm os: linux @@ -102,3 +99,9 @@ infrastructure: aptl-grafana-otel: links: [security-net] dependencies: [aptl-tempo] + +conditions: + wazuh-manager-health: + command: /usr/local/bin/check-wazuh-manager + interval: 30 + description: Declarative healthcheck required before public startup is ready. diff --git a/examples/scenarios/techvault-defensive-min.sdl.yaml b/examples/scenarios/techvault-defensive-min.sdl.yaml index 130c546b6..7ddaabfe2 100644 --- a/examples/scenarios/techvault-defensive-min.sdl.yaml +++ b/examples/scenarios/techvault-defensive-min.sdl.yaml @@ -17,10 +17,7 @@ nodes: - {name: wazuh-api, port: 55000, protocol: tcp} - {name: agent-events, port: 1514, protocol: tcp} - {name: syslog, port: 514, protocol: udp} - runtime: - health: - status: healthy - description: Runtime healthcheck must pass before public startup is ready. + conditions: {wazuh-manager-health: ""} wazuh-indexer: type: vm os: linux @@ -82,3 +79,9 @@ features: name: aptl-soc-stack version: local description: Wazuh manager, indexer, and dashboard monitoring core. + +conditions: + wazuh-manager-health: + command: /usr/local/bin/check-wazuh-manager + interval: 30 + description: Declarative healthcheck required before public startup is ready. diff --git a/examples/scenarios/techvault-enterprise-web.sdl.yaml b/examples/scenarios/techvault-enterprise-web.sdl.yaml index bd769db01..b9d46464a 100644 --- a/examples/scenarios/techvault-enterprise-web.sdl.yaml +++ b/examples/scenarios/techvault-enterprise-web.sdl.yaml @@ -50,10 +50,7 @@ nodes: - {name: wazuh-api, port: 55000, protocol: tcp} - {name: agent-events, port: 1514, protocol: tcp} - {name: syslog, port: 514, protocol: udp} - runtime: - health: - status: healthy - description: Runtime healthcheck must pass before public startup is ready. + conditions: {wazuh-manager-health: ""} wazuh-indexer: type: vm os: linux @@ -129,3 +126,9 @@ vulnerabilities: description: Login form accepts intentionally vulnerable SQL input. technical: true class: CWE-89 + +conditions: + wazuh-manager-health: + command: /usr/local/bin/check-wazuh-manager + interval: 30 + description: Declarative healthcheck required before public startup is ready. diff --git a/examples/scenarios/techvault-operational.sdl.yaml b/examples/scenarios/techvault-operational.sdl.yaml index 6f8636f30..16970d133 100644 --- a/examples/scenarios/techvault-operational.sdl.yaml +++ b/examples/scenarios/techvault-operational.sdl.yaml @@ -27,10 +27,7 @@ nodes: - {name: wazuh-api, port: 55000, protocol: tcp} - {name: agent-events, port: 1514, protocol: tcp} - {name: syslog, port: 514, protocol: udp} - runtime: - health: - status: healthy - description: Runtime healthcheck must pass before public startup is ready. + conditions: {wazuh-manager-health: ""} wazuh-indexer: type: vm os: linux @@ -322,3 +319,9 @@ vulnerabilities: description: Login form accepts intentionally vulnerable SQL input. technical: true class: CWE-89 + +conditions: + wazuh-manager-health: + command: /usr/local/bin/check-wazuh-manager + interval: 30 + description: Declarative healthcheck required before public startup is ready. diff --git a/examples/scenarios/techvault.sdl.yaml b/examples/scenarios/techvault.sdl.yaml index 08a84777d..1d1cad285 100644 --- a/examples/scenarios/techvault.sdl.yaml +++ b/examples/scenarios/techvault.sdl.yaml @@ -390,14 +390,6 @@ nodes: - techvault.local group_add: - adm - health: - status: healthy - failing_streak: 0 - log: - - start: '2026-05-20T12:00:00Z' - end: '2026-05-20T12:00:01Z' - exit_code: 0 - output: ok local_identity: users: - username: root @@ -510,11 +502,6 @@ nodes: domainname: techvault.local endpoints: - network: aptl-dmz - network_id: 7f2c1ad4e9b30c5a8d6e4f1b2a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c - network_id_stability: stable - endpoint_id: 3a9c7e0d3f5b8a1c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b - endpoint_id_stability: ephemeral - backend_generated: true ip_address: 172.20.0.20 ip_prefix_length: 24 gateway: 172.20.0.1 @@ -525,8 +512,6 @@ nodes: dns_names: - aptl-webapp - webapp - generated_dns_names: - - 3a9c7e0d3f5b backend: driver: bridge ipam_driver: default @@ -536,11 +521,6 @@ nodes: com.docker.network.driver.mtu: '1500' description: Docker bridge network backing the TechVault DMZ. - network: aptl-internal - network_id: c4d6e9f0a2b3c5d7e9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5 - network_id_stability: stable - endpoint_id: 9f1a3b5c7d2c1ad4e9b30c5a8d6e4f1b7f2c1ad4e9b30c5a3a9c7e0d3f5b8a1c - endpoint_id_stability: ephemeral - backend_generated: true ip_address: 172.21.0.20 ip_prefix_length: 24 gateway: 172.21.0.1 @@ -551,8 +531,6 @@ nodes: dns_names: - aptl-webapp - webapp - generated_dns_names: - - 3a9c7e0d3f5b backend: driver: bridge ipam_driver: default diff --git a/implementations/python/packages/aces_cli/conformance.py b/implementations/python/packages/aces_cli/conformance.py index 7c8c22786..7101f05b3 100644 --- a/implementations/python/packages/aces_cli/conformance.py +++ b/implementations/python/packages/aces_cli/conformance.py @@ -47,6 +47,7 @@ def _report_payload(report: BackendConformanceReport) -> dict[str, object]: return { "profile": report.profile, "passed": report.passed, + "claim": report.claim.model_dump(mode="json"), "cases": [ { "name": case.name, diff --git a/implementations/python/packages/aces_conformance/conformance.py b/implementations/python/packages/aces_conformance/conformance.py index 2262adafc..0ed951dec 100644 --- a/implementations/python/packages/aces_conformance/conformance.py +++ b/implementations/python/packages/aces_conformance/conformance.py @@ -22,9 +22,14 @@ backend_profiles_root, load_backend_profile_from_path, ) +from aces_contracts.behavioral_relations import ( + BehavioralRelationCatalogModel, + validate_behavioral_claim_binding, +) from aces_contracts.contracts import ( AssociatedArtifactManifestModel, BackendManifestV2Model, + BehavioralClaimBindingModel, EvaluationHistoryEventModel, EvaluationPlanModel, EvaluationResultStateModel, @@ -74,6 +79,7 @@ from aces_processor.models import ( ParticipantActionContractRuntime, ParticipantBehaviorHistoryEvent, + ParticipantHistoryAddressScope, ParticipantObservationBoundaryRuntime, iter_participant_behavior_history_violations, iter_participant_behavior_joint_action_violations, @@ -231,6 +237,7 @@ class BackendConformanceReport: profile: str passed: bool + claim: BehavioralClaimBindingModel cases: tuple[ConformanceCaseResult, ...] = () contract_versions: dict[str, str] = field(default_factory=dict) unsupported_contract_gaps: tuple[str, ...] = () @@ -238,6 +245,43 @@ class BackendConformanceReport: diagnostics: tuple[Diagnostic, ...] = () +def _bounded_conformance_claim( + *, + profile: str, + cases: tuple[ConformanceCaseResult, ...], + left_carrier_ref: str, +) -> BehavioralClaimBindingModel: + """Describe exactly what a conformance report's finite cases establish.""" + + evidence_refs = [f"conformance-case:{case.contract_name}:{case.name}" for case in cases] + binding = BehavioralClaimBindingModel( + taxonomy_id="aces-behavioral-relations", + taxonomy_revision="rev1", + relation_id="bounded-probe-success", + subject=f"Backend conformance for profile {profile}", + left_carrier_ref=left_carrier_ref, + right_carrier_ref=f"backend-profile:{profile}", + observation_projection_ref="backend-conformance-case-report", + observation_projection_revision="rev1", + quantifier_scope="finite-cases", + evidence_scope="finite", + evidence_boundary=( + f"The {len(cases)} named fixture and target-probe cases recorded in this report; " + "no unexecuted input, trace, scheduler, strategy, or environment is quantified." + ), + assurance_status="tested", + evidence_refs=evidence_refs, + limitations=[ + "Case results are bounded by the selected profile, corpus revision, target, and execution environment." + ], + explicit_non_claims=[ + "Does not establish trace equivalence or bisimulation.", + "Does not establish strategic, epistemic, probabilistic, timed, or partial-order equivalence.", + ], + ) + return validate_behavioral_claim_binding(binding) + + _MODEL_VALIDATORS = { "backend-manifest-v2": BackendManifestV2Model.model_validate, "participant-implementation-manifest-v1": ParticipantImplementationManifestModel.model_validate, @@ -262,6 +306,7 @@ class BackendConformanceReport: _STRUCTURAL_ONLY_VALIDATORS = { "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_validate, + "behavioral-relations-v1": BehavioralRelationCatalogModel.model_validate, "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_validate, "experiment-authoring-input-v1": ExperimentSpecModel.model_validate, "experiment-study-v1": ExperimentStudyModel.model_validate, @@ -695,12 +740,14 @@ def _participant_behavior_history_diagnostics( diagnostics: list[Diagnostic] = [] for address, message in iter_participant_behavior_history_violations( payload, - action_contract_addresses=action_contract_addresses, action_contracts=action_contracts, - observation_boundary_addresses=observation_boundary_addresses, observation_boundaries=observation_boundaries, participant_episode_history=participant_episode_history, expected_participant_address=expected_participant_address, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=action_contract_addresses, + observation_boundary_addresses=observation_boundary_addresses, + ), ): if address.startswith(history_key): diagnostic_address = root_address + address.removeprefix(history_key) @@ -1114,10 +1161,17 @@ def run_fixture_suite( ) ) + profile_id = _to_profile_id(profile) + case_tuple = tuple(cases) return BackendConformanceReport( - profile=_to_profile_id(profile), + profile=profile_id, passed=not diagnostics and all(case.passed for case in cases), - cases=tuple(cases), + claim=_bounded_conformance_claim( + profile=profile_id, + cases=case_tuple, + left_carrier_ref=f"conformance-fixture-suite:{profile_id}", + ), + cases=case_tuple, contract_versions={name: str(schema.get("title", name)) for name, schema in bundle.items() if name in required}, diagnostics=tuple(diagnostics), ) @@ -1272,6 +1326,11 @@ def run_target_conformance( return BackendConformanceReport( profile=profile_id, passed=False, + claim=_bounded_conformance_claim( + profile=profile_id, + cases=fixture_report.cases, + left_carrier_ref=f"backend-target:{target.name}", + ), cases=fixture_report.cases, contract_versions=dict(fixture_report.contract_versions), diagnostics=diagnostics, @@ -1315,6 +1374,11 @@ def run_target_conformance( return BackendConformanceReport( profile=_to_profile_id(effective_profile), passed=passed, + claim=_bounded_conformance_claim( + profile=_to_profile_id(effective_profile), + cases=cases, + left_carrier_ref=f"backend-target:{target.name}", + ), cases=cases, contract_versions=dict(fixture_report.contract_versions), unsupported_contract_gaps=contract_gaps, diff --git a/implementations/python/packages/aces_contracts/behavioral_relations.py b/implementations/python/packages/aces_contracts/behavioral_relations.py new file mode 100644 index 000000000..6c4026710 --- /dev/null +++ b/implementations/python/packages/aces_contracts/behavioral_relations.py @@ -0,0 +1,244 @@ +"""Versioned behavioral-relation authority and claim-binding validation.""" + +from __future__ import annotations + +from functools import cache +from pathlib import Path +from typing import Literal + +from pydantic import Field, model_validator + +from .contracts import ( + BehavioralClaimBindingModel, + BehavioralRelationId, + BehavioralTaxonomyRevision, + ContractModel, + NonEmptyString, +) +from .corpus import CONCEPT_AUTHORITY, corpus_family_root +from .versions import BEHAVIORAL_RELATIONS_SCHEMA_VERSION + + +class ImmutablePublicationLocatorModel(ContractModel): + kind: Literal["doi", "isbn"] + value: NonEmptyString + + +class BehavioralBibliographySourceModel(ContractModel): + source_id: BehavioralRelationId + title: NonEmptyString + authors: list[NonEmptyString] = Field(min_length=1) + publication_year: int = Field(ge=1900, le=2100) + publication_venue: NonEmptyString + edition_or_version: NonEmptyString + immutable_locator: ImmutablePublicationLocatorModel + + +class TransitionSignatureModel(ContractModel): + applicability: Literal["applicable", "not-applicable"] + labels: NonEmptyString + transition_relation: NonEmptyString + observable_actions: NonEmptyString + hidden_actions: NonEmptyString + stuttering_actions: NonEmptyString + not_applicable_rationale: NonEmptyString | None = None + + @model_validator(mode="after") + def _validate_applicability(self) -> TransitionSignatureModel: + if self.applicability == "not-applicable" and self.not_applicable_rationale is None: + raise ValueError("not-applicable transition signatures require a rationale") + if self.applicability == "applicable" and self.not_applicable_rationale is not None: + raise ValueError("applicable transition signatures must not carry a not-applicable rationale") + return self + + +class ObservationProjectionModel(ContractModel): + applicability: Literal["required", "parameterized", "identity", "not-applicable"] + subject: NonEmptyString + policy_ref: NonEmptyString + policy_revision: NonEmptyString + redaction_scope: NonEmptyString + order_treatment: NonEmptyString + simultaneity_treatment: NonEmptyString + + +class RelationQuantificationModel(ContractModel): + states: NonEmptyString + traces: NonEmptyString + schedulers: NonEmptyString + strategies: NonEmptyString + environments: NonEmptyString + observations: NonEmptyString + + +class RelationDimensionTreatmentModel(ContractModel): + status: Literal["supported", "parameterized", "abstracted", "outside-scope"] + treatment: NonEmptyString + + +class RelationDimensionsModel(ContractModel): + nondeterminism: RelationDimensionTreatmentModel + concurrency: RelationDimensionTreatmentModel + probability: RelationDimensionTreatmentModel + time: RelationDimensionTreatmentModel + partial_order: RelationDimensionTreatmentModel + + +class RelationPreservationModel(ContractModel): + property: NonEmptyString + proof_obligation: NonEmptyString + + +class RelationAssuranceModel(ContractModel): + definition_status: Literal["defined", "future"] + implementation_status: Literal["implemented", "partial", "not-implemented", "not-applicable"] + test_status: Literal["tested", "bounded", "not-tested", "not-applicable"] + proof_status: Literal["proved", "model-checked", "deliberately-unproved", "future", "not-applicable"] + evidence_refs: list[NonEmptyString] = Field(default_factory=list) + + +class BehavioralRelationDefinitionModel(ContractModel): + relation_id: BehavioralRelationId + display_name: NonEmptyString + relation_class: Literal["predicate", "set-relation", "behavioral", "epistemic", "strategic", "empirical"] + definition: NonEmptyString + left_carrier: NonEmptyString + right_carrier: NonEmptyString + initial_states: NonEmptyString + transition_signature: TransitionSignatureModel + observation_projection: ObservationProjectionModel + projection_required: bool + direction: Literal["unary", "left-to-right", "right-to-left", "symmetric"] + quantification: RelationQuantificationModel + dimensions: RelationDimensionsModel + preservation: RelationPreservationModel + bounded_evidence: list[NonEmptyString] = Field(min_length=1) + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + incompatible_claim_surfaces: list[NonEmptyString] = Field(min_length=1) + assurance: RelationAssuranceModel + source_refs: list[BehavioralRelationId] = Field(min_length=1) + + +class BehavioralClaimSurfaceModel(ContractModel): + surface_id: BehavioralRelationId + intended_relation_ids: list[BehavioralRelationId] = Field(min_length=1) + evidence_boundary: NonEmptyString + prohibited_relation_ids: list[BehavioralRelationId] = Field(min_length=1) + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + + +class ExampleTransitionModel(ContractModel): + source: NonEmptyString + action: NonEmptyString + target: NonEmptyString + + +class ExampleTransitionSystemModel(ContractModel): + states: list[NonEmptyString] = Field(min_length=1) + initial_state: NonEmptyString + transitions: list[ExampleTransitionModel] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_transitions(self) -> ExampleTransitionSystemModel: + state_set = set(self.states) + if len(state_set) != len(self.states): + raise ValueError("example transition-system states must be unique") + if self.initial_state not in state_set: + raise ValueError("example initial state must be declared") + for transition in self.transitions: + if transition.source not in state_set or transition.target not in state_set: + raise ValueError("example transitions must reference declared states") + return self + + +class BehavioralWorkedExampleModel(ContractModel): + example_id: BehavioralRelationId + purpose: NonEmptyString + left_system: ExampleTransitionSystemModel + right_system: ExampleTransitionSystemModel + tested_visible_trace: list[NonEmptyString] = Field(min_length=1) + hidden_action: NonEmptyString + expected_strong_bisimulation: bool + expected_weak_matching: bool + evidence_boundary: NonEmptyString + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + + +class BehavioralRelationCatalogModel(ContractModel): + schema_version: Literal[BEHAVIORAL_RELATIONS_SCHEMA_VERSION] = BEHAVIORAL_RELATIONS_SCHEMA_VERSION + taxonomy_id: Literal["aces-behavioral-relations"] = "aces-behavioral-relations" + taxonomy_revision: BehavioralTaxonomyRevision + bibliography: list[BehavioralBibliographySourceModel] = Field(min_length=1) + relations: dict[BehavioralRelationId, BehavioralRelationDefinitionModel] = Field(min_length=1) + claim_surfaces: list[BehavioralClaimSurfaceModel] = Field(min_length=1) + worked_examples: dict[BehavioralRelationId, BehavioralWorkedExampleModel] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_catalog_references(self) -> BehavioralRelationCatalogModel: + source_ids = [source.source_id for source in self.bibliography] + if len(source_ids) != len(set(source_ids)): + raise ValueError("behavioral-relation bibliography source ids must be unique") + relation_ids = set(self.relations) + for key, relation in self.relations.items(): + if key != relation.relation_id: + raise ValueError("behavioral-relation map keys must match embedded relation ids") + missing_sources = sorted(set(relation.source_refs) - set(source_ids)) + if missing_sources: + raise ValueError(f"relation {key!r} references unknown bibliography sources: {missing_sources}") + surface_ids = [surface.surface_id for surface in self.claim_surfaces] + if len(surface_ids) != len(set(surface_ids)): + raise ValueError("behavioral claim-surface ids must be unique") + for surface in self.claim_surfaces: + missing_relations = sorted( + (set(surface.intended_relation_ids) | set(surface.prohibited_relation_ids)) - relation_ids + ) + if missing_relations: + raise ValueError( + f"claim surface {surface.surface_id!r} references unknown relations: {missing_relations}" + ) + for key, example in self.worked_examples.items(): + if key != example.example_id: + raise ValueError("worked-example map keys must match embedded example ids") + return self + + +def behavioral_relation_catalog_path() -> Path: + return corpus_family_root(CONCEPT_AUTHORITY) / "behavioral-relations-v1.json" + + +@cache +def load_behavioral_relation_catalog() -> BehavioralRelationCatalogModel: + return BehavioralRelationCatalogModel.model_validate_json( + behavioral_relation_catalog_path().read_text(encoding="utf-8") + ) + + +def validate_behavioral_claim_binding( + binding: BehavioralClaimBindingModel, + catalog: BehavioralRelationCatalogModel | None = None, +) -> BehavioralClaimBindingModel: + """Resolve a consumer binding against the canonical catalog.""" + + catalog = load_behavioral_relation_catalog() if catalog is None else catalog + if binding.taxonomy_id != catalog.taxonomy_id or binding.taxonomy_revision != catalog.taxonomy_revision: + raise ValueError("behavioral claim binding taxonomy coordinates do not match the canonical catalog") + relation = catalog.relations.get(binding.relation_id) + if relation is None: + raise ValueError(f"behavioral claim binding references unknown relation {binding.relation_id!r}") + if relation.projection_required and binding.observation_projection_ref is None: + raise ValueError(f"relation {binding.relation_id!r} requires an observation projection binding") + return binding + + +__all__ = [ + "BehavioralClaimSurfaceModel", + "BehavioralBibliographySourceModel", + "BehavioralRelationCatalogModel", + "BehavioralRelationDefinitionModel", + "BehavioralWorkedExampleModel", + "ExampleTransitionModel", + "ExampleTransitionSystemModel", + "behavioral_relation_catalog_path", + "load_behavioral_relation_catalog", + "validate_behavioral_claim_binding", +] diff --git a/implementations/python/packages/aces_contracts/contracts.py b/implementations/python/packages/aces_contracts/contracts.py index bdad0dff4..65125fd43 100644 --- a/implementations/python/packages/aces_contracts/contracts.py +++ b/implementations/python/packages/aces_contracts/contracts.py @@ -120,6 +120,64 @@ class ContractModel(BaseModel): NonEmptyString = Annotated[str, Field(min_length=1)] +BehavioralRelationId = Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]*$")] +BehavioralTaxonomyRevision = Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9.-]*$")] + + +class BehavioralClaimBindingModel(ContractModel): + """A bounded claim tied to one revisioned behavioral-relation definition. + + This is deliberately a claim *binding*, not another relation registry. The + catalog owns relation meaning; consumers supply the subject, carriers, + quantifier/evidence boundary, assurance state, and explicit limitations. + """ + + taxonomy_id: NonEmptyString + taxonomy_revision: BehavioralTaxonomyRevision + relation_id: BehavioralRelationId + subject: NonEmptyString + left_carrier_ref: NonEmptyString | None = None + right_carrier_ref: NonEmptyString | None = None + observation_projection_ref: NonEmptyString | None = None + observation_projection_revision: NonEmptyString | None = None + quantifier_scope: Literal[ + "single-artifact", + "finite-cases", + "sampled-population", + "all-admitted-inputs", + "all-traces", + "all-strategies", + ] + evidence_scope: Literal["structural", "finite", "statistical", "model-check", "proof"] + evidence_boundary: NonEmptyString + assurance_status: Literal[ + "defined", + "implemented", + "tested", + "model-checked", + "proved", + "deliberately-unproved", + "future", + ] + evidence_refs: list[NonEmptyString] = Field(default_factory=list) + limitations: list[NonEmptyString] = Field(min_length=1) + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_claim_strength(self) -> BehavioralClaimBindingModel: + universal_scopes = {"all-admitted-inputs", "all-traces", "all-strategies"} + if self.quantifier_scope in universal_scopes and self.evidence_scope not in {"model-check", "proof"}: + raise ValueError("universal quantification requires model-check or proof evidence") + if self.assurance_status == "proved" and self.evidence_scope != "proof": + raise ValueError("proved assurance requires proof evidence") + if self.assurance_status == "model-checked" and self.evidence_scope != "model-check": + raise ValueError("model-checked assurance requires model-check evidence") + projection_values = (self.observation_projection_ref, self.observation_projection_revision) + if (projection_values[0] is None) != (projection_values[1] is None): + raise ValueError("observation projection ref and revision must be supplied together") + return self + + Rfc3339DateTimeString = Annotated[ str, Field( @@ -2482,6 +2540,7 @@ class RealizationProvenanceEntryModel(ContractModel): requirement_kind: NonEmptyString explicitness: ExplicitnessClass provenance: ExplicitnessProvenance + governing_scope: NonEmptyString | None = None class RuntimeSnapshotEnvelopeModel(ContractModel): @@ -6085,6 +6144,7 @@ class ExperimentStudyModel(ContractModel): description: NonEmptyString purpose: NonEmptyString research_questions: list[NonEmptyString] = Field(default_factory=list) + behavioral_claims: list[BehavioralClaimBindingModel] = Field(default_factory=list) membership: dict[NonEmptyString, ExperimentStudyMembershipModel] = Field(min_length=1) inclusion_criteria: list[NonEmptyString] = Field(min_length=1) factors: dict[NonEmptyString, ExperimentStudyFactorModel] = Field(default_factory=dict) @@ -6096,6 +6156,14 @@ class ExperimentStudyModel(ContractModel): @model_validator(mode="after") def _validate_claim_bearing_study(self) -> ExperimentStudyModel: + from .behavioral_relations import load_behavioral_relation_catalog, validate_behavioral_claim_binding + + catalog = load_behavioral_relation_catalog() + relation_ids = [claim.relation_id for claim in self.behavioral_claims] + if len(relation_ids) != len(set(relation_ids)): + raise ValueError("study behavioral claim relation ids must be unique") + for claim in self.behavioral_claims: + validate_behavioral_claim_binding(claim, catalog) if self.run_allocation is not None: undeclared_blocking_factors = sorted( factor_id for factor_id in self.run_allocation.blocking_factors if factor_id not in self.factors @@ -6138,6 +6206,8 @@ def _validate_claim_bearing_study(self) -> ExperimentStudyModel: if self.study_kind in {"study", "benchmark"}: if not self.research_questions: raise ValueError("study and benchmark records must include at least one research question") + if not self.behavioral_claims: + raise ValueError("study and benchmark records must include at least one behavioral claim binding") if self.run_allocation is None: raise ValueError("study and benchmark records must include run_allocation") if self.analysis_plan is None: @@ -6161,9 +6231,16 @@ def __get_pydantic_json_schema__( "required": ["study_kind"], }, "then": { - "required": ["research_questions", "run_allocation", "analysis_plan", "validity_notes"], + "required": [ + "research_questions", + "behavioral_claims", + "run_allocation", + "analysis_plan", + "validity_notes", + ], "properties": { "research_questions": {"minItems": 1}, + "behavioral_claims": {"minItems": 1}, "run_allocation": {"type": "object"}, "analysis_plan": {"type": "object"}, "validity_notes": {"minItems": 1}, @@ -6174,8 +6251,8 @@ def __get_pydantic_json_schema__( _add_aces_invariant( json_schema, "claim-bearing-study-analysis-plan-required", - "Study and benchmark records must include research questions, run allocation, a substantive analysis plan, " - "and validity notes.", + "Study and benchmark records must include research questions, revisioned behavioral claim bindings, run " + "allocation, a substantive analysis plan, and validity notes.", validator="aces_contracts.contracts.ExperimentStudyModel._validate_claim_bearing_study", inputs=[{"contract_id": "experiment-study-v1", "instance_path": "#"}], ) @@ -7899,18 +7976,17 @@ def __get_pydantic_json_schema__( return json_schema -def schema_bundle() -> dict[str, dict[str, Any]]: - """Return the repo-published JSON Schemas for external contracts.""" - +def _raw_schema_bundle() -> dict[str, dict[str, Any]]: from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel + from .behavioral_relations import BehavioralRelationCatalogModel from .provenance import SDLLineageLedgerModel from .scientific_completeness import ( ScientificCompletenessAssessmentModel, ScientificCompletenessTaxonomyModel, ) - bundle = { + return { "aces-semantic-invariants-v1": _aces_semantic_invariant_profile_schema_for_bundle(), "sdl-authoring-input-v1": Scenario.model_json_schema(), "instantiated-scenario-v1": InstantiatedScenario.model_json_schema(), @@ -7922,6 +7998,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "participant-implementation-manifest-v1": ParticipantImplementationManifestModel.model_json_schema(), "participant-implementation-provenance-v1": ParticipantImplementationProvenanceModel.model_json_schema(), "concept-families-v1": ConceptFamilyCatalogModel.model_json_schema(), + "behavioral-relations-v1": BehavioralRelationCatalogModel.model_json_schema(), "reference-models-v1": ReferenceModelCatalogModel.model_json_schema(), "uco-alignment-v1": UcoAlignmentCatalogModel.model_json_schema(), "controlled-vocabularies-v1": ControlledVocabularyCatalogModel.model_json_schema(), @@ -7979,6 +8056,31 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_json_schema(), "reusable-asset-trust-policy-v1": ReusableAssetTrustPolicyModel.model_json_schema(), } + + +def schema_bundle() -> dict[str, dict[str, Any]]: + """Return the repo-published JSON Schemas for external contracts.""" + + bundle = _raw_schema_bundle() + _add_aces_invariant( + bundle["behavioral-relations-v1"], + "behavioral-relations-reference-resolution", + "Relation map keys, bibliography references, claim-surface relation references, and worked-example keys " + "must resolve exactly inside one taxonomy revision.", + validator="aces_contracts.behavioral_relations.BehavioralRelationCatalogModel", + inputs=[{"contract_id": "behavioral-relations-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + bundle["experiment-study-v1"], + "study-behavioral-claim-catalog-resolution", + "Every study behavioral claim must resolve against the canonical taxonomy revision, include a required " + "observation projection, and keep bounded evidence out of universal quantifiers.", + validator="aces_contracts.behavioral_relations.validate_behavioral_claim_binding", + inputs=[ + {"contract_id": "experiment-study-v1", "instance_path": "#/behavioral_claims"}, + {"contract_id": "behavioral-relations-v1", "instance_path": "#"}, + ], + ) _add_aces_invariant( bundle["scientific-completeness-taxonomy-v1"], "scientific-completeness-taxonomy-rectangular", @@ -7987,6 +8089,17 @@ def schema_bundle() -> dict[str, dict[str, Any]]: validator="aces_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel", inputs=[{"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"}], ) + _add_aces_invariant( + bundle["scientific-completeness-taxonomy-v1"], + "scientific-completeness-behavioral-claim-resolution", + "Each profile must carry resolved behavioral claim bindings and disjoint, catalog-resolved nonclaimed " + "relation ids.", + validator="aces_contracts.scientific_completeness.CompletenessProfileModel.validate_behavioral_claims", + inputs=[ + {"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#/profiles"}, + {"contract_id": "behavioral-relations-v1", "instance_path": "#"}, + ], + ) _add_aces_invariant( bundle["scientific-completeness-assessment-v1"], "scientific-completeness-assessment-status-evidence", @@ -8045,6 +8158,9 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "BackendCompatibilityModel", "BackendManifestV2Model", "BackendCapabilitiesV2Model", + "BehavioralClaimBindingModel", + "BehavioralRelationId", + "BehavioralTaxonomyRevision", "CONCEPT_FAMILIES_SCHEMA_VERSION", "ConceptBindingEntryModel", "ConceptFamilyCatalogModel", diff --git a/implementations/python/packages/aces_contracts/realization_envelope.py b/implementations/python/packages/aces_contracts/realization_envelope.py index 9297e1866..b852b5301 100644 --- a/implementations/python/packages/aces_contracts/realization_envelope.py +++ b/implementations/python/packages/aces_contracts/realization_envelope.py @@ -21,6 +21,7 @@ from __future__ import annotations +import re from collections.abc import Callable from enum import Enum from typing import Annotated, Literal @@ -29,6 +30,7 @@ from aces_contracts.contracts import ContractModel, NonEmptyString, RealizationEnvelopeIdentityModel from aces_contracts.versions import REALIZATION_ENVELOPE_SCHEMA_VERSION +from aces_contracts.vocabulary import Closure __all__ = [ "BackendRealizationEnvelopeModel", @@ -60,6 +62,7 @@ "scalar_matches_numeric_type", "realization_envelope_digest", "realizer_configuration_digest", + "scope_specificity", "validate_backend_realization_envelope", ] @@ -67,6 +70,14 @@ # from ``int`` here (see ``scalar_matches_numeric_type``); Pydantic preserves the # authored Python type on a closed model, so ``True`` never collapses to ``1``. DomainScalar = bool | int | float | str +_ENVELOPE_PATH_RE = re.compile(r"^[^.\[\]]+(?:\.[^.\[\]]+|\[\d+\])*$") + + +def _require_envelope_path(path: str, *, allow_root: bool) -> None: + if allow_root and path == "": + return + if _ENVELOPE_PATH_RE.fullmatch(path) is None: + raise ValueError("path must use the complete canonical SDL path grammar") class EnvelopeScope(str, Enum): @@ -79,6 +90,21 @@ class EnvelopeScope(str, Enum): SCENARIO = "scenario" +_SCOPE_SPECIFICITY = { + EnvelopeScope.SCENARIO: 0, + EnvelopeScope.TOPOLOGY: 1, + EnvelopeScope.APP: 1, + EnvelopeScope.NODE: 2, + EnvelopeScope.FIELD: 3, +} + + +def scope_specificity(scope: EnvelopeScope) -> int: + """Return semantic specificity; topology and app are sibling scopes.""" + + return _SCOPE_SPECIFICITY[scope] + + class Posture(str, Enum): """Author/backend intent for a bound value or child scope.""" @@ -87,13 +113,6 @@ class Posture(str, Enum): EXACT = "exact" -class Closure(str, Enum): - """Whether unspecified realizable dimensions under a scope are admitted.""" - - OPEN_WORLD = "open-world" - CLOSED_WORLD = "closed-world" - - class NumericType(str, Enum): """Declared numeric type for a numeric-interval domain.""" @@ -214,6 +233,7 @@ class EnvelopeBinding(ContractModel): @model_validator(mode="after") def _validate_posture_domain(self) -> EnvelopeBinding: + _require_envelope_path(self.path, allow_root=False) if self.posture is Posture.OPEN and self.domain is not None: raise ValueError("open posture binding must not name a domain") if self.posture in (Posture.CONSTRAINED, Posture.EXACT) and not self.domain: @@ -228,6 +248,11 @@ class ClosureOverlay(ContractModel): scope: EnvelopeScope closure: Closure + @model_validator(mode="after") + def _validate_path(self) -> ClosureOverlay: + _require_envelope_path(self.path, allow_root=True) + return self + class WitnessPolicy(ContractModel): """Deterministic default-selection policy for witness generation. @@ -296,23 +321,48 @@ def visit(name: str) -> None: visit(name) def _validate_bindings(self) -> None: - seen: dict[tuple[str, str], EnvelopeBinding] = {} + self._validate_binding_entries() + self._validate_closure_entries() + + def _validate_binding_entries(self) -> None: + seen: dict[tuple[str, int], EnvelopeBinding] = {} for binding in self.bindings: if binding.domain is not None and binding.domain not in self.domains: raise ValueError(f"binding path '{binding.path}' references unknown domain '{binding.domain}'") if binding.posture is Posture.EXACT and binding.domain is not None: if not _is_singleton_domain(self.domains[binding.domain]): raise ValueError(f"exact posture binding path '{binding.path}' requires a singleton domain") - key = (binding.path, binding.scope.value) + key = (binding.path, scope_specificity(binding.scope)) existing = seen.get(key) - if existing is not None and (existing.domain, existing.posture) != (binding.domain, binding.posture): + if existing is not None and ( + existing.domain, + existing.posture, + existing.overrideable, + ) != ( + binding.domain, + binding.posture, + binding.overrideable, + ): # Equal-specificity, incompatible binding is invalid, not # merge-order dependent (envelope-semantics.md R2). raise ValueError( - f"conflicting equal-specificity bindings for path '{binding.path}' at scope '{binding.scope.value}'" + f"conflicting equal-specificity bindings for path '{binding.path}' " + f"at equivalent scopes '{existing.scope.value}' and '{binding.scope.value}'" ) seen[key] = binding + def _validate_closure_entries(self) -> None: + seen_closure: dict[tuple[str, int], tuple[EnvelopeScope, Closure]] = {} + for overlay in self.closure: + key = (overlay.path, scope_specificity(overlay.scope)) + existing_closure = seen_closure.get(key) + if existing_closure is not None and existing_closure[1] is not overlay.closure: + raise ValueError( + f"conflicting equal-specificity closure overlays for path '{overlay.path}' " + f"at equivalent scopes '{existing_closure[0].value}' and '{overlay.scope.value}'" + ) + seen_closure[key] = (overlay.scope, overlay.closure) + def _validate_witness_policy(self) -> None: if self.witness_policy is None: return diff --git a/implementations/python/packages/aces_contracts/runtime_state.py b/implementations/python/packages/aces_contracts/runtime_state.py index c3e7215e4..ac64cb9e2 100644 --- a/implementations/python/packages/aces_contracts/runtime_state.py +++ b/implementations/python/packages/aces_contracts/runtime_state.py @@ -66,6 +66,7 @@ class RealizationProvenanceEntry: requirement_kind: str explicitness: ExplicitnessClass provenance: ExplicitnessProvenance + governing_scope: str | None = None @dataclass diff --git a/implementations/python/packages/aces_contracts/scientific_completeness.py b/implementations/python/packages/aces_contracts/scientific_completeness.py index 89e287d85..f2c72b19f 100644 --- a/implementations/python/packages/aces_contracts/scientific_completeness.py +++ b/implementations/python/packages/aces_contracts/scientific_completeness.py @@ -10,7 +10,12 @@ from pydantic import AfterValidator, Field, model_validator -from .contracts import ContractModel, NonEmptyString +from .contracts import ( + BehavioralClaimBindingModel, + BehavioralRelationId, + ContractModel, + NonEmptyString, +) from .corpus import PROFILES, corpus_family_root from .versions import ( SCIENTIFIC_COMPLETENESS_ASSESSMENT_SCHEMA_VERSION, @@ -77,9 +82,30 @@ class CompletenessProfileModel(ContractModel): title: NonEmptyString intended_claim: NonEmptyString explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + behavioral_claims: list[BehavioralClaimBindingModel] = Field(min_length=1) + non_claimed_relation_ids: list[BehavioralRelationId] = Field(min_length=1) dispositions: dict[ConcernId, ProfileDisposition] example_refs: list[RepoPath] = Field(default_factory=list) + @model_validator(mode="after") + def validate_behavioral_claims(self) -> CompletenessProfileModel: + from .behavioral_relations import load_behavioral_relation_catalog, validate_behavioral_claim_binding + + catalog = load_behavioral_relation_catalog() + claimed_ids = [claim.relation_id for claim in self.behavioral_claims] + if len(claimed_ids) != len(set(claimed_ids)): + raise ValueError("scientific-completeness behavioral claim relation ids must be unique") + nonclaimed_ids = set(self.non_claimed_relation_ids) + missing = sorted(nonclaimed_ids - set(catalog.relations)) + if missing: + raise ValueError(f"scientific-completeness profile references unknown relations: {missing}") + overlap = sorted(set(claimed_ids) & nonclaimed_ids) + if overlap: + raise ValueError(f"relations cannot be both claimed and explicitly non-claimed: {overlap}") + for claim in self.behavioral_claims: + validate_behavioral_claim_binding(claim, catalog) + return self + class ScientificCompletenessTaxonomyModel(ContractModel): schema_version: Literal[SCIENTIFIC_COMPLETENESS_TAXONOMY_SCHEMA_VERSION] = ( diff --git a/implementations/python/packages/aces_contracts/versions.py b/implementations/python/packages/aces_contracts/versions.py index c06613db4..e6968f1b6 100644 --- a/implementations/python/packages/aces_contracts/versions.py +++ b/implementations/python/packages/aces_contracts/versions.py @@ -7,6 +7,7 @@ PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION = "participant-implementation-manifest/v1" PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION = "participant-implementation-provenance/v1" CONCEPT_FAMILIES_SCHEMA_VERSION = "concept-families/v1" +BEHAVIORAL_RELATIONS_SCHEMA_VERSION = "behavioral-relations/v1" REFERENCE_MODELS_SCHEMA_VERSION = "reference-models/v1" UCO_ALIGNMENT_SCHEMA_VERSION = "uco-alignment/v1" CONTROLLED_VOCABULARIES_SCHEMA_VERSION = "controlled-vocabularies/v1" diff --git a/implementations/python/packages/aces_contracts/vocabulary.py b/implementations/python/packages/aces_contracts/vocabulary.py index a1654305a..d931ac4e3 100644 --- a/implementations/python/packages/aces_contracts/vocabulary.py +++ b/implementations/python/packages/aces_contracts/vocabulary.py @@ -48,6 +48,13 @@ class RealizationSupportMode(str, Enum): OPEN_REALIZATION = "open-realization" +class Closure(str, Enum): + """Whether unspecified realizable dimensions under a scope are admitted.""" + + OPEN_WORLD = "open-world" + CLOSED_WORLD = "closed-world" + + class ParticipantFeatureSupportLevel(str, Enum): """ADR-054 guarantee-strength scale for per-feature participant runtime support.""" diff --git a/implementations/python/packages/aces_mcp/tools/reference.py b/implementations/python/packages/aces_mcp/tools/reference.py index 7cce46815..cfa921a1c 100644 --- a/implementations/python/packages/aces_mcp/tools/reference.py +++ b/implementations/python/packages/aces_mcp/tools/reference.py @@ -11,9 +11,7 @@ from mcp.server.fastmcp import FastMCP -# --------------------------------------------------------------------------- # Docs / examples on disk — allowlisted filenames only -# --------------------------------------------------------------------------- def _find_repo_root(start: Path) -> Path: @@ -60,9 +58,7 @@ def _read_example(name: str) -> str: return path.read_text() -# --------------------------------------------------------------------------- # Section-level reference snippets -# --------------------------------------------------------------------------- # Maps each section name to the heading anchor used in sections.md so we # can extract just the relevant portion. diff --git a/implementations/python/packages/aces_operations/_cross_backend_corpus_backend_runs.py b/implementations/python/packages/aces_operations/_cross_backend_corpus_backend_runs.py index 5a5e858d4..2ff563181 100644 --- a/implementations/python/packages/aces_operations/_cross_backend_corpus_backend_runs.py +++ b/implementations/python/packages/aces_operations/_cross_backend_corpus_backend_runs.py @@ -94,7 +94,10 @@ def _libvirt_surface_coverage(artifact: Mapping[str, Any]) -> dict[str, str]: "participant_implementation_provenance": "recorded", "participant_episode_history": "recorded", "participant_behavior_history": "recorded", - "participant_terminal_observation": "recorded (behavior-history-equivalent)", + "participant_terminal_observation": ( + "recorded (participant-projected history under the artifact's named observation boundary; " + "no equivalence comparison)" + ), "evaluator_wazuh_evidence": f"recorded (evaluator-only; {defensive_source})", "outcome_interpretation_evidence": "recorded", } diff --git a/implementations/python/packages/aces_operations/_evidence_run_artifact.py b/implementations/python/packages/aces_operations/_evidence_run_artifact.py index 68045d005..33c5e241f 100644 --- a/implementations/python/packages/aces_operations/_evidence_run_artifact.py +++ b/implementations/python/packages/aces_operations/_evidence_run_artifact.py @@ -353,11 +353,36 @@ def _terminal_observation_section(snapshot: TerminalSnapshot) -> dict[str, Any]: addr: _redact_behavior_history(events) for addr, events in snapshot.participant_behavior_history.items() } return { - "form": "behavior-history-equivalent", + "form": "participant-projected-history", + "taxonomy": { + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "non_claimed_relation_ids": [ + "participant-projected-history-equivalence", + "epistemic-indistinguishability", + "alternating-strategic-equivalence", + ], + }, + "observation_projection": { + "subject": "the participant addressed by each behavior-history stream", + "policy_ref": "participant-observation-boundary", + "policy_revision": "participant-observation-envelope/v1", + "redaction_scope": "Only the fields retained by _redact_behavior_history are disclosed.", + "order_treatment": "Recorded participant sequence order is preserved.", + "simultaneity_treatment": "No simultaneity equivalence is inferred from the serialized order.", + }, + "evidence_boundary": ( + "The terminal snapshot's named participant behavior-history streams for this single run; " + "no second execution or universal trace set is compared." + ), "disclosure": ( "The libvirt participant runtime emits a behavior-history event stream rather than a standalone SEM-210 " - "observation envelope; the terminal participant view is reported as the behavior-history equivalent." + "observation envelope; the terminal participant view is reported as a bounded participant projection." ), + "explicit_non_claims": [ + "This single projected record does not establish participant-projected-history-equivalence.", + "It does not establish epistemic or strategic equivalence.", + ], "behavior_history": behavior_history, } diff --git a/implementations/python/packages/aces_operations/techvault_live.py b/implementations/python/packages/aces_operations/techvault_live.py index e6dd34da8..43fa6e82c 100644 --- a/implementations/python/packages/aces_operations/techvault_live.py +++ b/implementations/python/packages/aces_operations/techvault_live.py @@ -104,6 +104,7 @@ def validate_techvault_live( scenario, plan_check = _plan_scenario(target, scenario_path) del scenario checks.append(plan_check) + snapshot: Mapping[str, object] = {} if plan_check.passed: boot_check = _apply_plan(target, scenario_path, driver) checks.append(boot_check) @@ -113,20 +114,20 @@ def validate_techvault_live( checks.append(_surface_check(snapshot)) cleanup_ok, cleanup_diagnostics = cleanup_native_snapshot(driver, snapshot) checks.append(LiveCheck("verified_native_cleanup", cleanup_ok, cleanup_diagnostics)) - manifest_path = _write_manifest( - output_dir, - run_id, - scenario_path, - snapshot, - checks, - ) - checks.append( - LiveCheck( - "run_archive_manifest", - manifest_path is not None, - () if manifest_path else ("manifest write failed",), - ) + manifest_path = _write_manifest( + output_dir, + run_id, + scenario_path, + snapshot, + checks, + ) + checks.append( + LiveCheck( + "run_archive_manifest", + manifest_path is not None, + () if manifest_path else ("manifest write failed",), ) + ) return TechVaultLiveReport(str(scenario_path), str(output_dir), run_id, tuple(checks), manifest_path) diff --git a/implementations/python/packages/aces_processor/compiler.py b/implementations/python/packages/aces_processor/compiler.py index eceec8aa3..f04fc504c 100644 --- a/implementations/python/packages/aces_processor/compiler.py +++ b/implementations/python/packages/aces_processor/compiler.py @@ -12,7 +12,8 @@ from aces_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION from aces_sdl import build_declaration_index from aces_sdl.entities import flatten_entities -from aces_sdl.explicitness import ExplicitnessClass +from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_sdl.identifiers import QualifiedName from aces_sdl.instantiate import admit_instantiated_scenario, instantiate_scenario from aces_sdl.nodes import NodeType from aces_sdl.orchestration import WorkflowStepType @@ -20,6 +21,7 @@ OutcomeInterpretationSourceLayer, OutcomeInterpretationTargetLayer, ) +from aces_sdl.realization_designation import resolve_realization_designation from aces_sdl.scenario import ExpandedScenario, InstantiatedScenario, Scenario from aces_sdl.semantics.objective_semantics import ( OBJECTIVE_WINDOW_DEPENDENCY_ROLES, @@ -69,7 +71,7 @@ from .semantics.realization import ( REALIZATION_DOMAIN, CompiledRealizationRequirement, - resolve_realization_concern, + registered_realization_concerns, ) @@ -2402,15 +2404,19 @@ def _compile_workflows( } -def _realization_requirement_address(scenario: InstantiatedScenario, field_path: str) -> str: +def _realization_requirement_address( + scenario: InstantiatedScenario, + *, + section_name: str, + declaration_name: str, +) -> str: """Resolve the compiled resource address for a realization-concern path.""" - for name, node in scenario.nodes.items(): - if field_path in {f"nodes.{name}.os", f"nodes.{name}.type"}: - return _network_address(name) if node.type == NodeType.SWITCH else _node_address(name) - for name in scenario.content: - if field_path == f"content.{name}.type": - return _content_address(name) + if section_name == "nodes" and declaration_name in scenario.nodes: + node = scenario.nodes[declaration_name] + return _network_address(declaration_name) if node.type == NodeType.SWITCH else _node_address(declaration_name) + if section_name == "content" and declaration_name in scenario.content: + return _content_address(declaration_name) raise ValueError("realization concern must resolve to one compiled resource address") @@ -2420,27 +2426,60 @@ def _compile_realization_requirements( """SEM-218 typed compiler emission: lower each authored realization concern into a compiled requirement carrying its classifier explicitness class. - Open concerns are not gated and are not emitted; constrained and exact - concerns carry their concern kind for the planner realization gate. + Explicit leaves always win. Missing admitted concerns are lowered through + the typed lexical designation cascade; omitted designation preserves the + legacy closed fallback while explicit root delegation remains typed. """ requirements: list[CompiledRealizationRequirement] = [] - for field_path, record in scenario.explicitness.items(): - if record.classification is ExplicitnessClass.OPEN: - continue - concern_kind = resolve_realization_concern( - field_path, - declaration_names={"nodes": scenario.nodes, "content": scenario.content}, - ) - if concern_kind is None: - continue + explicitness = scenario.explicitness + for section_name, declaration_name, field_name, concern_kind in registered_realization_concerns( + declaration_names={"nodes": scenario.nodes, "content": scenario.content} + ): + field_path = f"{section_name}.{declaration_name}.{field_name}" + encoded_name = declaration_name.replace("~", "~0").replace("/", "~1") + field_pointer = f"/{section_name}/{encoded_name}/{field_name}" + owner_namespace = QualifiedName.parse(declaration_name).parts[:-1] + record = explicitness.get(field_path) + if record is None: + resolution = resolve_realization_designation( + scenario.instantiation_provenance.realization_designations, + field_pointer=field_pointer, + owner_namespace=owner_namespace, + ) + if resolution.source == "legacy-default" or ( + resolution.closure is not None + and resolution.closure.value == "closed-world" + and not resolution.delegated + ): + continue + requirement_explicitness = ( + ExplicitnessClass.OPEN + if resolution.closure is not None and resolution.closure.value == "open-world" + else None + ) + provenance = ExplicitnessProvenance.AUTHOR_DECLARED + governing_scope = resolution.governing_scope + delegated = resolution.delegated + else: + requirement_explicitness = record.classification + provenance = record.provenance + governing_scope = f"#{field_pointer}" + delegated = False requirements.append( CompiledRealizationRequirement( field_path=field_path, - address=_realization_requirement_address(scenario, field_path), + address=_realization_requirement_address( + scenario, + section_name=section_name, + declaration_name=declaration_name, + ), domain=REALIZATION_DOMAIN, requirement_kind=concern_kind, - explicitness=record.classification, + explicitness=requirement_explicitness, + provenance=provenance, + governing_scope=governing_scope, + delegated=delegated, ) ) return tuple(requirements) diff --git a/implementations/python/packages/aces_processor/models.py b/implementations/python/packages/aces_processor/models.py deleted file mode 100644 index 8ec313e2e..000000000 --- a/implementations/python/packages/aces_processor/models.py +++ /dev/null @@ -1,4343 +0,0 @@ -"""Runtime data models for the SDL-native execution layer. - -The runtime is split into three domains: - -- provisioning: desired deployed state -- orchestration: resolved exercise control graph -- evaluation: resolved monitoring/scoring graph - -The compiler produces a ``RuntimeModel`` with reusable templates separated -from bound runtime instances. The planner reconciles those instances against -the current ``RuntimeSnapshot`` and emits a composite ``ExecutionPlan``. -""" - -from collections.abc import Iterable, Iterator, Mapping -from dataclasses import asdict, dataclass, field -from typing import Any - -from aces_backend_protocols.capabilities import ( - BackendManifest, - WorkflowFeature, - WorkflowStatePredicateFeature, -) -from aces_contracts.addressing import require_compiled_address -from aces_contracts.diagnostics import Diagnostic as Diagnostic -from aces_contracts.diagnostics import Severity as Severity -from aces_contracts.evaluation import ( - EvaluationExecutionContract as EvaluationExecutionContract, -) -from aces_contracts.evaluation import ( - EvaluationExecutionState as EvaluationExecutionState, -) -from aces_contracts.evaluation import ( - EvaluationHistoryEvent as EvaluationHistoryEvent, -) -from aces_contracts.evaluation import ( - EvaluationHistoryEventType as EvaluationHistoryEventType, -) -from aces_contracts.evaluation import ( - EvaluationResultContract as EvaluationResultContract, -) -from aces_contracts.evaluation import ( - EvaluationResultStatus as EvaluationResultStatus, -) -from aces_contracts.evaluation import ( - validate_evaluation_result as validate_evaluation_result, -) -from aces_contracts.participant_behavior import ( - ParticipantActionPreconditionStatus as ParticipantActionPreconditionStatus, -) -from aces_contracts.participant_behavior import ( - ParticipantActionResultStatus as ParticipantActionResultStatus, -) -from aces_contracts.participant_behavior import ( - ParticipantAdmissionDisposition as ParticipantAdmissionDisposition, -) -from aces_contracts.participant_behavior import ( - ParticipantBehaviorHistoryEventType as ParticipantBehaviorHistoryEventType, -) -from aces_contracts.participant_behavior import ( - ParticipantLifecycleOperationState as ParticipantLifecycleOperationState, -) -from aces_contracts.participant_behavior import ( - ParticipantObservationStatus as ParticipantObservationStatus, -) -from aces_contracts.participant_behavior import ( - ParticipantPhaseRealization as ParticipantPhaseRealization, -) -from aces_contracts.participant_behavior import ( - ParticipantRuntimeLifecyclePhase as ParticipantRuntimeLifecyclePhase, -) -from aces_contracts.participant_behavior import ( - participant_lifecycle_field_violation_messages as participant_lifecycle_field_violation_messages, -) -from aces_contracts.participant_episode import ( - PARTICIPANT_EPISODE_CONTROL_EVENTS, - PARTICIPANT_EPISODE_TERMINAL_EVENTS, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeControlAction as ParticipantEpisodeControlAction, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeExecutionState as ParticipantEpisodeExecutionState, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeHistoryEvent as ParticipantEpisodeHistoryEvent, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeHistoryEventType as ParticipantEpisodeHistoryEventType, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeInitializeRequest as ParticipantEpisodeInitializeRequest, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeResetRequest as ParticipantEpisodeResetRequest, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeRestartRequest as ParticipantEpisodeRestartRequest, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeStatus as ParticipantEpisodeStatus, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeTerminalReason as ParticipantEpisodeTerminalReason, -) -from aces_contracts.participant_episode import ( - ParticipantEpisodeTerminateRequest as ParticipantEpisodeTerminateRequest, -) -from aces_contracts.participant_episode import ( - iter_participant_episode_snapshot_violations as iter_participant_episode_snapshot_violations, -) -from aces_contracts.planning import ( - ChangeAction as ChangeAction, -) -from aces_contracts.planning import ( - EvaluationOp as EvaluationOp, -) -from aces_contracts.planning import ( - EvaluationPlan as EvaluationPlan, -) -from aces_contracts.planning import ( - OrchestrationOp as OrchestrationOp, -) -from aces_contracts.planning import ( - OrchestrationPlan as OrchestrationPlan, -) -from aces_contracts.planning import ( - PlannedResource as PlannedResource, -) -from aces_contracts.planning import ( - PlanOperation as PlanOperation, -) -from aces_contracts.planning import ( - ProvisioningPlan as ProvisioningPlan, -) -from aces_contracts.planning import ( - ProvisionOp as ProvisionOp, -) -from aces_contracts.planning import ( - RuntimeDomain as RuntimeDomain, -) -from aces_contracts.runtime_state import ( - ApplyResult as ApplyResult, -) -from aces_contracts.runtime_state import ( - OperationReceipt as OperationReceipt, -) -from aces_contracts.runtime_state import ( - OperationState as OperationState, -) -from aces_contracts.runtime_state import ( - OperationStatus as OperationStatus, -) -from aces_contracts.runtime_state import ( - RealizationProvenanceEntry as RealizationProvenanceEntry, -) -from aces_contracts.runtime_state import ( - RuntimeSnapshot as RuntimeSnapshot, -) -from aces_contracts.runtime_state import ( - RuntimeSnapshotEnvelope as RuntimeSnapshotEnvelope, -) -from aces_contracts.runtime_state import ( - SnapshotEntry as SnapshotEntry, -) -from aces_contracts.versions import ( - EVALUATION_STATE_SCHEMA_VERSION as EVALUATION_STATE_SCHEMA_VERSION, -) -from aces_contracts.versions import ( - OPERATION_SCHEMA_VERSION as OPERATION_SCHEMA_VERSION, -) -from aces_contracts.versions import ( - PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION as PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION, -) -from aces_contracts.versions import ( - RUNTIME_SNAPSHOT_SCHEMA_VERSION as RUNTIME_SNAPSHOT_SCHEMA_VERSION, -) -from aces_contracts.versions import ( - WORKFLOW_STATE_SCHEMA_VERSION as WORKFLOW_STATE_SCHEMA_VERSION, -) -from aces_contracts.workflow import ( - WorkflowCancellationRequest as WorkflowCancellationRequest, -) -from aces_contracts.workflow import ( - WorkflowCompensationStatus as WorkflowCompensationStatus, -) -from aces_contracts.workflow import ( - WorkflowExecutionContract as WorkflowExecutionContract, -) -from aces_contracts.workflow import ( - WorkflowExecutionState as WorkflowExecutionState, -) -from aces_contracts.workflow import ( - WorkflowHistoryEvent as WorkflowHistoryEvent, -) -from aces_contracts.workflow import ( - WorkflowHistoryEventType as WorkflowHistoryEventType, -) -from aces_contracts.workflow import ( - WorkflowResultContract as WorkflowResultContract, -) -from aces_contracts.workflow import ( - WorkflowStatus as WorkflowStatus, -) -from aces_contracts.workflow import ( - WorkflowStepExecutionState as WorkflowStepExecutionState, -) -from aces_contracts.workflow import ( - WorkflowStepLifecycle as WorkflowStepLifecycle, -) -from aces_contracts.workflow import ( - WorkflowStepOutcome as WorkflowStepOutcome, -) -from aces_contracts.workflow import ( - validate_workflow_step_result_contract as validate_workflow_step_result_contract, -) -from aces_sdl.participant_attribution_semantics import ( - OUTCOME_ATTRIBUTION_CANDIDATE_KINDS, - STRONG_ATTRIBUTION_SUPPORT_CLASSES, - ParticipantAttributionCandidateKind, - ParticipantAttributionOrderingBasisKind, - ParticipantAttributionSupportClass, -) -from aces_sdl.participant_behavior import ( - ParticipantEffectClass, - ParticipantFailureClass, - ParticipantInteractionClass, - ParticipantPreconditionClass, -) -from aces_sdl.participant_outcome_semantics import ( - PROVENANCE_REQUIRED_OUTCOME_SOURCE_LAYERS, - OutcomeInterpretationSourceLayer, - OutcomeInterpretationTargetLayer, -) -from aces_sdl.participant_temporal_semantics import ( - ParticipantTemporalEventPoint, - ParticipantTemporalState, - ParticipantTimeDomain, -) -from aces_sdl.scenario import InstantiatedScenario -from aces_sdl.semantics.workflow import ( - WorkflowStepSemanticContract, -) - -from aces_processor.semantics.realization import ( - CompiledRealizationRequirement as CompiledRealizationRequirement, -) - -_PARTICIPANT_ACTION_CONTRACT_PREFIX = "participant.action-contract." -_PARTICIPANT_OBSERVATION_BOUNDARY_PREFIX = "participant.observation-boundary." -_PARTICIPANT_OUTCOME_RULE_PREFIX = "participant.outcome-interpretation-rule." -_PARTICIPANT_BEHAVIOR_HISTORY_KEY = "runtime.snapshot.participant-behavior-history" -_PARTICIPANT_EPISODE_CONTROL_EVENTS = PARTICIPANT_EPISODE_CONTROL_EVENTS -_PARTICIPANT_EPISODE_TERMINAL_EVENTS = PARTICIPANT_EPISODE_TERMINAL_EVENTS - - -@dataclass(frozen=True) -class RuntimeTemplate: - """Reusable SDL definition preserved in compiled form.""" - - address: str - name: str - spec: dict[str, Any] - - -@dataclass(frozen=True) -class ResolvedResource: - """Base class for bound runtime resources.""" - - address: str - name: str - spec: dict[str, Any] - ordering_dependencies: tuple[str, ...] = () - refresh_dependencies: tuple[str, ...] = () - - -@dataclass(frozen=True) -class NetworkRuntime(ResolvedResource): - """Compiled switch/network deployment.""" - - node_name: str = "" - - -@dataclass(frozen=True) -class NodeRuntime(ResolvedResource): - """Compiled VM deployment.""" - - node_name: str = "" - node_type: str = "" - os_family: str = "" - count: int | str | None = None - - -@dataclass(frozen=True) -class FeatureBinding(ResolvedResource): - """Feature template bound to a specific node role.""" - - node_name: str = "" - node_address: str = "" - feature_name: str = "" - template_address: str = "" - role_name: str = "" - - -@dataclass(frozen=True) -class PropositionRuntime(ResolvedResource): - """Compiled backend-neutral proposition with resolved finite subjects.""" - - subject_addresses: tuple[str, ...] = () - predicate_kind: str = "" - evaluation_basis: str = "" - evidence_requirement_refs: tuple[str, ...] = () - - -@dataclass(frozen=True) -class AssertionRuntime(ResolvedResource): - """Compiled assertion use over one proposition.""" - - proposition_address: str = "" - role: str = "" - polarity: str = "" - - -@dataclass(frozen=True) -class ConditionBinding(ResolvedResource): - """Condition template bound to a specific node role.""" - - node_name: str = "" - node_address: str = "" - condition_name: str = "" - template_address: str = "" - role_name: str = "" - proposition_address: str = "" - result_contract: "EvaluationResultContract" = field( - default_factory=lambda: EvaluationResultContract(resource_type="condition-binding") - ) - execution_contract: "EvaluationExecutionContract" = field( - default_factory=lambda: EvaluationExecutionContract(resource_type="condition-binding") - ) - - -@dataclass(frozen=True) -class InjectBinding(ResolvedResource): - """Inject template bound to a specific node role.""" - - node_name: str = "" - node_address: str = "" - inject_name: str = "" - template_address: str = "" - role_name: str = "" - - -@dataclass(frozen=True) -class InjectRuntime(ResolvedResource): - """Resolved top-level inject resource.""" - - -@dataclass(frozen=True) -class ContentPlacement(ResolvedResource): - """Content entry resolved to a concrete target node.""" - - content_name: str = "" - target_node: str = "" - target_address: str = "" - - -@dataclass(frozen=True) -class AccountPlacement(ResolvedResource): - """Account entry resolved to a concrete target node.""" - - account_name: str = "" - node_name: str = "" - target_address: str = "" - - -@dataclass(frozen=True) -class ParticipantActionContractRuntime(ResolvedResource): - """Compiled participant action contract.""" - - action_name: str = "" - semantic_version: str = "" - lifecycle_state: str = "" - behavioral_granularity: str = "" - precondition_classes: tuple[str, ...] = () - effect_classes: tuple[str, ...] = () - failure_classes: tuple[str, ...] = () - backend_failure_mappings: tuple[dict[str, str], ...] = () - interaction_classes: tuple[str, ...] = () - shared_state_refs: tuple[str, ...] = () - temporal_contract_ids: tuple[str, ...] = () - temporal_kinds: tuple[str, ...] = () - time_domains: tuple[str, ...] = () - clock_authorities: tuple[str, ...] = () - backend_timing_disclosures: tuple[dict[str, Any], ...] = () - - -def map_backend_diagnostic_to_participant_failure( - diagnostic: Diagnostic | Mapping[str, Any] | str, - contract: ParticipantActionContractRuntime, -) -> ParticipantFailureClass: - """Map a backend diagnostic to a portable SEM-211 failure class.""" - - if isinstance(diagnostic, Diagnostic): - code = diagnostic.code - elif isinstance(diagnostic, Mapping): - code = str(diagnostic.get("code", "")) - else: - code = str(diagnostic) - - for mapping in contract.backend_failure_mappings: - if mapping.get("backend_error_code") == code: - return ParticipantFailureClass(str(mapping.get("failure_class", ParticipantFailureClass.UNKNOWN.value))) - return ParticipantFailureClass.BACKEND_ERROR if code else ParticipantFailureClass.UNKNOWN - - -def _as_string_set(value: Any) -> set[str]: - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - return set() - return {str(item) for item in value if isinstance(item, str) and item} - - -def _contract_sem211_precondition_refs( - contract: ParticipantActionContractRuntime, -) -> dict[tuple[str, str], dict[str, set[str]]]: - preconditions = contract.spec.get("preconditions", ()) - if isinstance(preconditions, (str, bytes, Mapping)) or not isinstance(preconditions, Iterable): - return {} - refs: dict[tuple[str, str], dict[str, set[str]]] = {} - for item in preconditions: - if not isinstance(item, Mapping) or not item.get("precondition_id") or not item.get("precondition_class"): - continue - key = (str(item.get("precondition_id", "")), str(item.get("precondition_class", ""))) - refs[key] = { - "support_refs": _as_string_set(item.get("support_refs", ())), - "evidence_refs": _as_string_set(item.get("evidence_refs", ())), - } - return refs - - -def _contract_sem211_effect_refs( - contract: ParticipantActionContractRuntime, -) -> dict[tuple[str, str], dict[str, set[str]]]: - effects = contract.spec.get("effects", ()) - if isinstance(effects, (str, bytes, Mapping)) or not isinstance(effects, Iterable): - return {} - refs: dict[tuple[str, str], dict[str, set[str]]] = {} - for item in effects: - if not isinstance(item, Mapping) or not item.get("effect_id") or not item.get("effect_class"): - continue - key = (str(item.get("effect_id", "")), str(item.get("effect_class", ""))) - refs[key] = { - "target_refs": _as_string_set(item.get("target_refs", ())), - "evidence_refs": _as_string_set(item.get("evidence_refs", ())), - } - return refs - - -def _contract_uses_sem211_action_results(contract: ParticipantActionContractRuntime) -> bool: - return bool(contract.precondition_classes or contract.effect_classes or contract.failure_classes) - - -def validate_participant_action_result_contract( - result: "ParticipantActionResult", - contract: ParticipantActionContractRuntime, -) -> list[str]: - """Return SEM-211 contract violations for one typed action result.""" - - violations: list[str] = [] - if result.action_contract_address != contract.address: - violations.append( - "action_result action_contract_address " - f"{result.action_contract_address!r} does not match compiled action contract {contract.address!r}" - ) - return violations - - declared_precondition_classes = set(contract.precondition_classes) - declared_effect_classes = set(contract.effect_classes) - declared_failure_classes = set(contract.failure_classes) - declared_precondition_refs = _contract_sem211_precondition_refs(contract) - declared_effect_refs = _contract_sem211_effect_refs(contract) - declared_preconditions = set(declared_precondition_refs) - declared_effects = set(declared_effect_refs) - reported_preconditions: set[tuple[str, str]] = set() - - for precondition in result.preconditions: - precondition_key = (precondition.precondition_id, precondition.precondition_class.value) - reported_preconditions.add(precondition_key) - if precondition.precondition_class.value not in declared_precondition_classes: - violations.append( - f"action_result precondition {precondition.precondition_id!r} uses undeclared " - f"precondition_class {precondition.precondition_class.value!r}" - ) - if declared_preconditions and precondition_key not in declared_preconditions: - violations.append( - f"action_result precondition {precondition.precondition_id!r}/" - f"{precondition.precondition_class.value!r} is not declared by {contract.address}" - ) - declared_refs = declared_precondition_refs.get(precondition_key) - if declared_refs is not None: - undeclared_support_refs = set(precondition.support_refs) - declared_refs["support_refs"] - undeclared_evidence_refs = set(precondition.evidence_refs) - declared_refs["evidence_refs"] - for ref in sorted(undeclared_support_refs): - violations.append( - f"action_result precondition {precondition.precondition_id!r} reports undeclared " - f"support_ref {ref!r}" - ) - for ref in sorted(undeclared_evidence_refs): - violations.append( - f"action_result precondition {precondition.precondition_id!r} reports undeclared " - f"evidence_ref {ref!r}" - ) - for precondition_id, precondition_class in sorted(declared_preconditions - reported_preconditions): - violations.append( - f"action_result is missing declared precondition {precondition_id!r}/" - f"{precondition_class!r} for {contract.address}" - ) - - for effect in result.effects: - effect_key = (effect.effect_id, effect.effect_class.value) - if effect.effect_class.value not in declared_effect_classes: - violations.append( - f"action_result effect {effect.effect_id!r} uses undeclared effect_class {effect.effect_class.value!r}" - ) - if declared_effects and effect_key not in declared_effects: - violations.append( - f"action_result effect {effect.effect_id!r}/" - f"{effect.effect_class.value!r} is not declared by {contract.address}" - ) - declared_refs = declared_effect_refs.get(effect_key) - if declared_refs is not None: - undeclared_target_refs = set(effect.target_refs) - declared_refs["target_refs"] - undeclared_evidence_refs = set(effect.evidence_refs) - declared_refs["evidence_refs"] - for ref in sorted(undeclared_target_refs): - violations.append(f"action_result effect {effect.effect_id!r} reports undeclared target_ref {ref!r}") - for ref in sorted(undeclared_evidence_refs): - violations.append(f"action_result effect {effect.effect_id!r} reports undeclared evidence_ref {ref!r}") - - declared_result_evidence_refs = { - ref for declared_refs in declared_precondition_refs.values() for ref in declared_refs["evidence_refs"] - } | {ref for declared_refs in declared_effect_refs.values() for ref in declared_refs["evidence_refs"]} - reported_result_evidence_refs = { - ref for precondition in result.preconditions for ref in precondition.evidence_refs - } | {ref for effect in result.effects for ref in effect.evidence_refs} - if declared_precondition_refs or declared_effect_refs: - for ref in sorted(set(result.evidence_refs) - declared_result_evidence_refs): - violations.append(f"action_result reports undeclared evidence_ref {ref!r}") - for ref in sorted(set(result.evidence_refs) & declared_result_evidence_refs - reported_result_evidence_refs): - violations.append( - f"action_result evidence_ref {ref!r} is not grounded in reported precondition or effect evidence_refs" - ) - - if result.failure_class is not None and result.failure_class.value not in declared_failure_classes: - violations.append( - f"action_result failure_class {result.failure_class.value!r} is not declared by {contract.address}" - ) - return violations - - -@dataclass(frozen=True) -class ParticipantObservationBoundaryRuntime(ResolvedResource): - """Compiled participant observation projection boundary.""" - - boundary_name: str = "" - projection_basis: str = "" - hidden_refs: tuple[str, ...] = () - observable_refs: tuple[str, ...] = () - evidence_refs: tuple[str, ...] = () - disclosed_refs: tuple[str, ...] = () - evidence_only_refs: tuple[str, ...] = () - discovered_refs: tuple[str, ...] = () - inferred_refs: tuple[str, ...] = () - concealed_refs: tuple[str, ...] = () - deceptive_refs: tuple[str, ...] = () - view_transitions: tuple[dict[str, Any], ...] = () - view_relation_timeline: tuple[dict[str, Any], ...] = () - realized_view_disclosure: str = "" - - -@dataclass(frozen=True) -class ParticipantOutcomeInterpretationRuleRuntime(ResolvedResource): - """Compiled SEM-215 participant outcome interpretation rule.""" - - rule_name: str = "" - semantic_version: str = "" - participant_scope: str = "" - observation_point_basis: str = "" - interpretation_basis: str = "" - source_layers: tuple[str, ...] = () - source_refs: tuple[str, ...] = () - target_layers: tuple[str, ...] = () - target_refs: tuple[str, ...] = () - evidence_refs: tuple[str, ...] = () - limitations: tuple[str, ...] = () - - -@dataclass(frozen=True) -class ParticipantBehaviorRuntime(ResolvedResource): - """Compiled role-neutral participant behavior binding.""" - - participant_name: str = "" - entity_name: str = "" - starting_account_refs: tuple[str, ...] = () - starting_account_addresses: tuple[str, ...] = () - initial_knowledge_addresses: tuple[str, ...] = () - starting_assertion_refs: tuple[str, ...] = () - starting_assertion_addresses: tuple[str, ...] = () - authority_anchor_refs: tuple[str, ...] = () - authority_anchor_addresses: tuple[str, ...] = () - operating_scope_refs: tuple[str, ...] = () - operating_scope_addresses: tuple[str, ...] = () - action_contract_addresses: tuple[str, ...] = () - observation_boundary_addresses: tuple[str, ...] = () - interpretation_mode: str = "role-neutral-projection" - - -@dataclass(frozen=True) -class ParticipantBehaviorSpecificationRuntime(ResolvedResource): - """Compiled first-class participant behavior specification aggregate.""" - - spec_name: str = "" - semantic_version: str = "" - lifecycle_state: str = "" - participant_addresses: tuple[str, ...] = () - participant_role_refs: tuple[str, ...] = () - action_contract_addresses: tuple[str, ...] = () - observation_boundary_addresses: tuple[str, ...] = () - outcome_interpretation_rule_addresses: tuple[str, ...] = () - authority_scope_refs: tuple[str, ...] = () - authority_scope_addresses: tuple[str, ...] = () - behavior_mode: str = "" - ai_offensive_behavior_refs: tuple[str, ...] = () - offensive_behavior_refs: tuple[str, ...] = () - realization_profile_ref: str = "" - backend_feature_support_refs: tuple[str, ...] = () - evidence_contract_refs: tuple[str, ...] = () - extension_policy: str = "" - extension_keys: tuple[str, ...] = () - - -@dataclass(frozen=True) -class EventRuntime(ResolvedResource): - """Resolved orchestration event.""" - - assertion_names: tuple[str, ...] = () - assertion_addresses: tuple[str, ...] = () - inject_names: tuple[str, ...] = () - inject_addresses: tuple[str, ...] = () - - -@dataclass(frozen=True) -class ScriptRuntime(ResolvedResource): - """Resolved script with event dependencies.""" - - event_addresses: tuple[str, ...] = () - - -@dataclass(frozen=True) -class StoryRuntime(ResolvedResource): - """Resolved story with script dependencies.""" - - script_addresses: tuple[str, ...] = () - - -@dataclass(frozen=True) -class ObjectiveWindowReferenceRuntime: - """Normalized resolved objective/window reference.""" - - raw: str - canonical_name: str - reference_kind: str - dependency_roles: tuple[str, ...] = () - workflow_name: str = "" - step_name: str = "" - namespace_path: tuple[str, ...] = () - - -@dataclass(frozen=True) -class WorkflowStepStatePredicateRuntime: - """Resolved predicate clause over prior workflow step state.""" - - step_name: str - outcomes: tuple[WorkflowStepOutcome, ...] = () - min_attempts: int | str | None = None - - -@dataclass(frozen=True) -class WorkflowPredicateRuntime: - """Resolved workflow predicate semantics.""" - - assertion_addresses: tuple[str, ...] = () - objective_addresses: tuple[str, ...] = () - step_state_predicates: tuple[WorkflowStepStatePredicateRuntime, ...] = () - - @property - def external_addresses(self) -> tuple[str, ...]: - seen: set[str] = set() - ordered: list[str] = [] - for address in ( - *self.assertion_addresses, - *self.objective_addresses, - ): - if address in seen: - continue - seen.add(address) - ordered.append(address) - return tuple(ordered) - - -@dataclass(frozen=True) -class WorkflowSwitchCaseRuntime: - """Resolved ordered switch-case branch semantics.""" - - case_index: int - predicate: WorkflowPredicateRuntime - next_step: str - - -@dataclass(frozen=True) -class WorkflowStepRuntime: - """Resolved workflow step semantics.""" - - name: str - step_type: str - objective_address: str = "" - predicate: WorkflowPredicateRuntime | None = None - next_step: str = "" - on_success: str = "" - on_failure: str = "" - on_exhausted: str = "" - then_step: str = "" - else_step: str = "" - switch_cases: tuple[WorkflowSwitchCaseRuntime, ...] = () - default_step: str = "" - branches: tuple[str, ...] = () - join_step: str = "" - owning_parallel_step: str = "" - called_workflow_address: str = "" - compensation_workflow_address: str = "" - max_attempts: int | str | None = None - state_contract: WorkflowStepSemanticContract = field( - default_factory=lambda: WorkflowStepSemanticContract(step_type="") - ) - - -@dataclass(frozen=True) -class WorkflowRuntime(ResolvedResource): - """Resolved workflow control program.""" - - start_step: str = "" - referenced_objective_addresses: tuple[str, ...] = () - control_steps: dict[str, WorkflowStepRuntime] = field(default_factory=dict) - control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) - join_owners: dict[str, str] = field(default_factory=dict) - step_assertion_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) - step_predicate_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) - required_features: tuple[WorkflowFeature, ...] = () - required_state_predicate_features: tuple[WorkflowStatePredicateFeature, ...] = () - result_contract: "WorkflowResultContract" = field(default_factory=lambda: WorkflowResultContract()) - execution_contract: "WorkflowExecutionContract" = field(default_factory=lambda: WorkflowExecutionContract()) - state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION - - -def _participant_observation_status_from_payload(value: Any) -> ParticipantObservationStatus | None: - if isinstance(value, ParticipantObservationStatus): - return value - if value is None: - return None - return ParticipantObservationStatus(str(value)) - - -def _validate_required_string(value: Any, message: str) -> None: - if not isinstance(value, str) or not value: - raise TypeError(message) - - -def _validate_optional_string(value: Any, message: str) -> None: - if value is not None and (not isinstance(value, str) or not value): - raise TypeError(message) - - -def _validate_optional_address(value: str | None, *, prefix: str, message: str) -> None: - if value is not None and (not isinstance(value, str) or not value.startswith(prefix)): - raise ValueError(message) - - -def _validate_required_address(value: str, *, prefix: str, message: str) -> None: - if not isinstance(value, str) or not value.startswith(prefix): - raise ValueError(message) - - -def _tuple_of_non_empty_strings(value: Any, *, field_name: str) -> tuple[str, ...]: - if value is None: - return () - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError(f"{field_name} must be a list of strings") - values = tuple(value) - refs = tuple(str(item) for item in values if isinstance(item, str) and item) - if len(refs) != len(values): - raise TypeError(f"{field_name} entries must be non-empty strings") - if len(set(refs)) != len(refs): - raise ValueError(f"{field_name} entries must be unique") - return refs - - -def _observation_point_matches_action_instance(observation_point: str, action_instance_id: str) -> bool: - return action_instance_id in observation_point.split(":") - - -@dataclass(frozen=True) -class ParticipantActionPreconditionResult: - """Resolved applicability state for one typed SEM-211 precondition.""" - - precondition_id: str - precondition_class: ParticipantPreconditionClass - status: ParticipantActionPreconditionStatus - participant_address: str - episode_id: str - action_contract_address: str - observation_point: str - support_refs: tuple[str, ...] = () - evidence_refs: tuple[str, ...] = () - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionPreconditionResult": - if not isinstance(payload, Mapping): - raise TypeError("participant action precondition result must be a mapping") - missing = [ - key - for key in ( - "precondition_id", - "precondition_class", - "status", - "participant_address", - "episode_id", - "action_contract_address", - "observation_point", - ) - if key not in payload - ] - if missing: - raise ValueError("participant action precondition result is missing required fields: " + ", ".join(missing)) - precondition_class_raw = payload.get("precondition_class") - status_raw = payload.get("status") - return cls( - precondition_id=str(payload.get("precondition_id")), - precondition_class=( - precondition_class_raw - if isinstance(precondition_class_raw, ParticipantPreconditionClass) - else ParticipantPreconditionClass(str(precondition_class_raw)) - ), - status=( - status_raw - if isinstance(status_raw, ParticipantActionPreconditionStatus) - else ParticipantActionPreconditionStatus(str(status_raw)) - ), - participant_address=str(payload.get("participant_address")), - episode_id=str(payload.get("episode_id")), - action_contract_address=str(payload.get("action_contract_address")), - observation_point=str(payload.get("observation_point")), - support_refs=_tuple_of_non_empty_strings(payload.get("support_refs", ()), field_name="support_refs"), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "precondition_id": self.precondition_id, - "precondition_class": self.precondition_class.value, - "status": self.status.value, - "participant_address": self.participant_address, - "episode_id": self.episode_id, - "action_contract_address": self.action_contract_address, - "observation_point": self.observation_point, - "support_refs": list(self.support_refs), - "evidence_refs": list(self.evidence_refs), - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - _validate_required_string( - self.precondition_id, - "precondition_id must be a non-empty string", - ) - if not isinstance(self.precondition_class, ParticipantPreconditionClass): - raise TypeError("precondition_class must be a ParticipantPreconditionClass") - if not isinstance(self.status, ParticipantActionPreconditionStatus): - raise TypeError("status must be a ParticipantActionPreconditionStatus") - _validate_required_string( - self.participant_address, - "participant action precondition participant_address must be a non-empty string", - ) - _validate_required_string( - self.episode_id, - "participant action precondition episode_id must be a non-empty string", - ) - _validate_required_address( - self.action_contract_address, - prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, - message="action_contract_address must be a compiled participant action contract address", - ) - _validate_required_string( - self.observation_point, - "observation_point must be a non-empty string", - ) - _tuple_of_non_empty_strings(self.support_refs, field_name="support_refs") - _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - - -@dataclass(frozen=True) -class ParticipantActionEffectResult: - """Realized effect entry for a SEM-211 participant action result.""" - - effect_id: str - effect_class: ParticipantEffectClass - description: str - target_refs: tuple[str, ...] = () - evidence_refs: tuple[str, ...] = () - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionEffectResult": - if not isinstance(payload, Mapping): - raise TypeError("participant action effect result must be a mapping") - missing = [key for key in ("effect_id", "effect_class", "description") if key not in payload] - if missing: - raise ValueError("participant action effect result is missing required fields: " + ", ".join(missing)) - effect_class_raw = payload.get("effect_class") - return cls( - effect_id=str(payload.get("effect_id")), - effect_class=( - effect_class_raw - if isinstance(effect_class_raw, ParticipantEffectClass) - else ParticipantEffectClass(str(effect_class_raw)) - ), - description=str(payload.get("description")), - target_refs=_tuple_of_non_empty_strings(payload.get("target_refs", ()), field_name="target_refs"), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "effect_id": self.effect_id, - "effect_class": self.effect_class.value, - "description": self.description, - "target_refs": list(self.target_refs), - "evidence_refs": list(self.evidence_refs), - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - _validate_required_string(self.effect_id, "effect_id must be a non-empty string") - if not isinstance(self.effect_class, ParticipantEffectClass): - raise TypeError("effect_class must be a ParticipantEffectClass") - _validate_required_string( - self.description, - "participant action effect description must be a non-empty string", - ) - _tuple_of_non_empty_strings(self.target_refs, field_name="target_refs") - _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - if self.effect_class not in {ParticipantEffectClass.NO_EFFECT, ParticipantEffectClass.UNKNOWN_EFFECT}: - if not self.target_refs and not self.evidence_refs: - raise ValueError(f"{self.effect_class.value} effects require target_refs or evidence_refs") - - -_PARTICIPANT_ACTION_FAILURE_STATUSES = frozenset( - { - ParticipantActionResultStatus.REJECTED, - ParticipantActionResultStatus.WITHHELD, - ParticipantActionResultStatus.FAILED, - ParticipantActionResultStatus.PARTIAL_SUCCESS, - ParticipantActionResultStatus.UNKNOWN, - } -) -_PARTICIPANT_ACTION_SUCCESS_STATUSES = frozenset( - { - ParticipantActionResultStatus.ACCEPTED, - ParticipantActionResultStatus.SUCCEEDED, - ParticipantActionResultStatus.PARTIAL_SUCCESS, - } -) -_PARTICIPANT_ACTION_TERMINAL_EFFECT_STATUSES = frozenset( - { - ParticipantActionResultStatus.SUCCEEDED, - ParticipantActionResultStatus.PARTIAL_SUCCESS, - } -) - - -@dataclass(frozen=True) -class ParticipantActionResult: - """Typed SEM-211 local result for a participant action attempt.""" - - status: ParticipantActionResultStatus - participant_address: str - episode_id: str - action_instance_id: str - action_contract_address: str - observation_point: str - preconditions: tuple[ParticipantActionPreconditionResult, ...] = () - effects: tuple[ParticipantActionEffectResult, ...] = () - failure_class: ParticipantFailureClass | None = None - observations: tuple[str, ...] = () - evidence_refs: tuple[str, ...] = () - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionResult": - if not isinstance(payload, Mapping): - raise TypeError("participant action result must be a mapping") - missing = [ - key - for key in ( - "status", - "participant_address", - "episode_id", - "action_instance_id", - "action_contract_address", - "observation_point", - ) - if key not in payload - ] - if missing: - raise ValueError("participant action result is missing required fields: " + ", ".join(missing)) - status_raw = payload.get("status") - failure_raw = payload.get("failure_class") - preconditions_raw = payload.get("preconditions", ()) - effects_raw = payload.get("effects", ()) - if isinstance(preconditions_raw, (str, bytes, Mapping)) or not isinstance(preconditions_raw, Iterable): - raise TypeError("preconditions must be a list of participant action precondition results") - if isinstance(effects_raw, (str, bytes, Mapping)) or not isinstance(effects_raw, Iterable): - raise TypeError("effects must be a list of participant action effect results") - return cls( - status=( - status_raw - if isinstance(status_raw, ParticipantActionResultStatus) - else ParticipantActionResultStatus(str(status_raw)) - ), - participant_address=str(payload.get("participant_address")), - episode_id=str(payload.get("episode_id")), - action_instance_id=str(payload.get("action_instance_id")), - action_contract_address=str(payload.get("action_contract_address")), - observation_point=str(payload.get("observation_point")), - preconditions=tuple(ParticipantActionPreconditionResult.from_payload(item) for item in preconditions_raw), - effects=tuple(ParticipantActionEffectResult.from_payload(item) for item in effects_raw), - failure_class=( - None - if failure_raw is None - else ( - failure_raw - if isinstance(failure_raw, ParticipantFailureClass) - else ParticipantFailureClass(str(failure_raw)) - ) - ), - observations=_tuple_of_non_empty_strings(payload.get("observations", ()), field_name="observations"), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "status": self.status.value, - "participant_address": self.participant_address, - "episode_id": self.episode_id, - "action_instance_id": self.action_instance_id, - "action_contract_address": self.action_contract_address, - "observation_point": self.observation_point, - "preconditions": [item.to_payload() for item in self.preconditions], - "effects": [item.to_payload() for item in self.effects], - "failure_class": self.failure_class.value if self.failure_class is not None else None, - "observations": list(self.observations), - "evidence_refs": list(self.evidence_refs), - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - if not isinstance(self.status, ParticipantActionResultStatus): - raise TypeError("status must be a ParticipantActionResultStatus") - _validate_required_string( - self.participant_address, - "participant action result participant_address must be a non-empty string", - ) - _validate_required_string( - self.episode_id, - "participant action result episode_id must be a non-empty string", - ) - _validate_required_string( - self.action_instance_id, - "participant action result action_instance_id must be a non-empty string", - ) - _validate_required_address( - self.action_contract_address, - prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, - message="action_contract_address must be a compiled participant action contract address", - ) - _validate_required_string( - self.observation_point, - "observation_point must be a non-empty string", - ) - if not _observation_point_matches_action_instance(self.observation_point, self.action_instance_id): - raise ValueError("action result observation_point must be anchored to action_instance_id") - if not isinstance(self.preconditions, tuple): - raise TypeError("preconditions must be a tuple") - if not self.preconditions: - raise ValueError("participant action results require precondition results") - if any(not isinstance(item, ParticipantActionPreconditionResult) for item in self.preconditions): - raise TypeError("preconditions must contain ParticipantActionPreconditionResult values") - if len({item.precondition_id for item in self.preconditions}) != len(self.preconditions): - raise ValueError("precondition result ids must be unique") - if not isinstance(self.effects, tuple): - raise TypeError("effects must be a tuple") - if any(not isinstance(item, ParticipantActionEffectResult) for item in self.effects): - raise TypeError("effects must contain ParticipantActionEffectResult values") - if len({item.effect_id for item in self.effects}) != len(self.effects): - raise ValueError("effect result ids must be unique") - if self.failure_class is not None and not isinstance(self.failure_class, ParticipantFailureClass): - raise TypeError("failure_class must be a ParticipantFailureClass or None") - _tuple_of_non_empty_strings(self.observations, field_name="observations") - _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - self._validate_scope() - self._validate_fail_closed() - - def _validate_scope(self) -> None: - for precondition in self.preconditions: - if precondition.participant_address != self.participant_address: - raise ValueError("precondition participant_address must match action result participant_address") - if precondition.episode_id != self.episode_id: - raise ValueError("precondition episode_id must match action result episode_id") - if precondition.action_contract_address != self.action_contract_address: - raise ValueError( - "precondition action_contract_address must match action result action_contract_address" - ) - if not _observation_point_matches_action_instance(precondition.observation_point, self.action_instance_id): - raise ValueError("precondition observation_point must be anchored to action result action_instance_id") - - def _validate_fail_closed(self) -> None: - blocked = [ - item - for item in self.preconditions - if item.status - in { - ParticipantActionPreconditionStatus.UNSATISFIED, - ParticipantActionPreconditionStatus.UNRESOLVED, - } - ] - if blocked and self.status in _PARTICIPANT_ACTION_SUCCESS_STATUSES: - raise ValueError("unsatisfied or unresolved preconditions fail closed") - if blocked and self.failure_class is None: - raise ValueError("unsatisfied or unresolved preconditions require a portable failure_class") - if self.status == ParticipantActionResultStatus.SUCCEEDED: - if self.failure_class is not None: - raise ValueError("succeeded action results may not report failure_class") - if self.status == ParticipantActionResultStatus.ACCEPTED and self.failure_class is not None: - raise ValueError("accepted action results may not report failure_class") - if self.status in _PARTICIPANT_ACTION_TERMINAL_EFFECT_STATUSES: - if not self.effects: - raise ValueError(f"{self.status.value} action results require declared effects") - if self.status in _PARTICIPANT_ACTION_FAILURE_STATUSES and self.failure_class is None: - raise ValueError(f"{self.status.value} action results require a portable failure_class") - - -@dataclass(frozen=True) -class ParticipantAttributionCandidate: - """Candidate endpoint for a SEM-212 attribution edge.""" - - candidate_kind: ParticipantAttributionCandidateKind - ref: str - description: str - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionCandidate": - if not isinstance(payload, Mapping): - raise TypeError("participant attribution candidate must be a mapping") - missing = [key for key in ("candidate_kind", "ref", "description") if key not in payload] - if missing: - raise ValueError("participant attribution candidate is missing required fields: " + ", ".join(missing)) - candidate_kind_raw = payload.get("candidate_kind") - return cls( - candidate_kind=( - candidate_kind_raw - if isinstance(candidate_kind_raw, ParticipantAttributionCandidateKind) - else ParticipantAttributionCandidateKind(str(candidate_kind_raw)) - ), - ref=str(payload.get("ref")), - description=str(payload.get("description")), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "candidate_kind": self.candidate_kind.value, - "ref": self.ref, - "description": self.description, - } - - def __post_init__(self) -> None: - if not isinstance(self.candidate_kind, ParticipantAttributionCandidateKind): - raise TypeError("candidate_kind must be a ParticipantAttributionCandidateKind") - _validate_required_string(self.ref, "participant attribution candidate ref must be a non-empty string") - _validate_required_string( - self.description, - "participant attribution candidate description must be a non-empty string", - ) - - -@dataclass(frozen=True) -class ParticipantAttributionOrderingBasis: - """Explicit ordering basis for a SEM-212 attribution edge.""" - - basis_kind: ParticipantAttributionOrderingBasisKind - relation_ref: str - description: str - ordered_event_refs: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionOrderingBasis": - if not isinstance(payload, Mapping): - raise TypeError("participant attribution ordering_basis must be a mapping") - missing = [key for key in ("basis_kind", "relation_ref", "description") if key not in payload] - if missing: - raise ValueError("participant attribution ordering_basis is missing required fields: " + ", ".join(missing)) - basis_kind_raw = payload.get("basis_kind") - return cls( - basis_kind=( - basis_kind_raw - if isinstance(basis_kind_raw, ParticipantAttributionOrderingBasisKind) - else ParticipantAttributionOrderingBasisKind(str(basis_kind_raw)) - ), - relation_ref=str(payload.get("relation_ref")), - description=str(payload.get("description")), - ordered_event_refs=_tuple_of_non_empty_strings( - payload.get("ordered_event_refs", ()), - field_name="ordered_event_refs", - ), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "basis_kind": self.basis_kind.value, - "relation_ref": self.relation_ref, - "description": self.description, - "ordered_event_refs": list(self.ordered_event_refs), - } - - def __post_init__(self) -> None: - if not isinstance(self.basis_kind, ParticipantAttributionOrderingBasisKind): - raise TypeError("basis_kind must be a ParticipantAttributionOrderingBasisKind") - _validate_required_string(self.relation_ref, "ordering_basis relation_ref must be a non-empty string") - _validate_required_string(self.description, "ordering_basis description must be a non-empty string") - _tuple_of_non_empty_strings(self.ordered_event_refs, field_name="ordered_event_refs") - - -@dataclass(frozen=True) -class ParticipantAttributionEvidenceBasis: - """Evidence-disclosure basis for a SEM-212 attribution edge.""" - - capture_apparatus: str - granularity: str - loss_model: str - redaction_policy: str - observer_effects: tuple[str, ...] - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionEvidenceBasis": - if not isinstance(payload, Mapping): - raise TypeError("participant attribution evidence_basis must be a mapping") - missing = [ - key - for key in ( - "capture_apparatus", - "granularity", - "loss_model", - "redaction_policy", - "observer_effects", - ) - if key not in payload - ] - if missing: - raise ValueError("participant attribution evidence_basis is missing required fields: " + ", ".join(missing)) - return cls( - capture_apparatus=str(payload.get("capture_apparatus")), - granularity=str(payload.get("granularity")), - loss_model=str(payload.get("loss_model")), - redaction_policy=str(payload.get("redaction_policy")), - observer_effects=_tuple_of_non_empty_strings( - payload.get("observer_effects", ()), - field_name="observer_effects", - ), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "capture_apparatus": self.capture_apparatus, - "granularity": self.granularity, - "loss_model": self.loss_model, - "redaction_policy": self.redaction_policy, - "observer_effects": list(self.observer_effects), - } - - def __post_init__(self) -> None: - _validate_required_string( - self.capture_apparatus, - "evidence_basis capture_apparatus must be a non-empty string", - ) - _validate_required_string(self.granularity, "evidence_basis granularity must be a non-empty string") - _validate_required_string(self.loss_model, "evidence_basis loss_model must be a non-empty string") - _validate_required_string(self.redaction_policy, "evidence_basis redaction_policy must be a non-empty string") - observer_effects = _tuple_of_non_empty_strings(self.observer_effects, field_name="observer_effects") - if not observer_effects: - raise ValueError("evidence_basis observer_effects must disclose at least one observer effect") - - -@dataclass(frozen=True) -class ParticipantAttributionEdge: - """Evidence-labeled SEM-212 attribution edge.""" - - edge_id: str - participant_address: str - episode_id: str - observation_point: str - cause_candidate: ParticipantAttributionCandidate - effect_candidate: ParticipantAttributionCandidate - ordering_basis: ParticipantAttributionOrderingBasis - evidence_basis: ParticipantAttributionEvidenceBasis - support_class: ParticipantAttributionSupportClass - confidence: str - strength: str - limitations: tuple[str, ...] - evidence_refs: tuple[str, ...] - interpretation_rule_ref: str | None = None - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionEdge": - if not isinstance(payload, Mapping): - raise TypeError("participant attribution edge must be a mapping") - missing = [ - key - for key in ( - "edge_id", - "participant_address", - "episode_id", - "observation_point", - "cause_candidate", - "effect_candidate", - "ordering_basis", - "evidence_basis", - "support_class", - "confidence", - "strength", - "limitations", - "evidence_refs", - ) - if key not in payload - ] - if missing: - raise ValueError("participant attribution edge is missing required fields: " + ", ".join(missing)) - support_class_raw = payload.get("support_class") - return cls( - edge_id=str(payload.get("edge_id")), - participant_address=str(payload.get("participant_address")), - episode_id=str(payload.get("episode_id")), - observation_point=str(payload.get("observation_point")), - cause_candidate=ParticipantAttributionCandidate.from_payload(payload.get("cause_candidate")), - effect_candidate=ParticipantAttributionCandidate.from_payload(payload.get("effect_candidate")), - ordering_basis=ParticipantAttributionOrderingBasis.from_payload(payload.get("ordering_basis")), - evidence_basis=ParticipantAttributionEvidenceBasis.from_payload(payload.get("evidence_basis")), - support_class=( - support_class_raw - if isinstance(support_class_raw, ParticipantAttributionSupportClass) - else ParticipantAttributionSupportClass(str(support_class_raw)) - ), - confidence=str(payload.get("confidence")), - strength=str(payload.get("strength")), - limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), - interpretation_rule_ref=( - str(payload["interpretation_rule_ref"]) if payload.get("interpretation_rule_ref") is not None else None - ), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "edge_id": self.edge_id, - "participant_address": self.participant_address, - "episode_id": self.episode_id, - "observation_point": self.observation_point, - "cause_candidate": self.cause_candidate.to_payload(), - "effect_candidate": self.effect_candidate.to_payload(), - "ordering_basis": self.ordering_basis.to_payload(), - "evidence_basis": self.evidence_basis.to_payload(), - "support_class": self.support_class.value, - "confidence": self.confidence, - "strength": self.strength, - "limitations": list(self.limitations), - "evidence_refs": list(self.evidence_refs), - "interpretation_rule_ref": self.interpretation_rule_ref, - } - - def __post_init__(self) -> None: - _validate_required_string(self.edge_id, "participant attribution edge_id must be a non-empty string") - _validate_required_string( - self.participant_address, - "participant attribution participant_address must be a non-empty string", - ) - _validate_required_string(self.episode_id, "participant attribution episode_id must be a non-empty string") - _validate_required_string( - self.observation_point, - "participant attribution observation_point must be a non-empty string", - ) - if not isinstance(self.cause_candidate, ParticipantAttributionCandidate): - raise TypeError("cause_candidate must be a ParticipantAttributionCandidate") - if not isinstance(self.effect_candidate, ParticipantAttributionCandidate): - raise TypeError("effect_candidate must be a ParticipantAttributionCandidate") - if not isinstance(self.ordering_basis, ParticipantAttributionOrderingBasis): - raise TypeError("ordering_basis must be a ParticipantAttributionOrderingBasis") - if not isinstance(self.evidence_basis, ParticipantAttributionEvidenceBasis): - raise TypeError("evidence_basis must be a ParticipantAttributionEvidenceBasis") - if not isinstance(self.support_class, ParticipantAttributionSupportClass): - raise TypeError("support_class must be a ParticipantAttributionSupportClass") - _validate_required_string(self.confidence, "participant attribution confidence must be a non-empty string") - _validate_required_string(self.strength, "participant attribution strength must be a non-empty string") - limitations = _tuple_of_non_empty_strings(self.limitations, field_name="limitations") - evidence_refs = _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - if not limitations: - raise ValueError("participant attribution edges require limitations") - if not evidence_refs: - raise ValueError("participant attribution edges require evidence_refs") - _validate_optional_string( - self.interpretation_rule_ref, - "interpretation_rule_ref must be a non-empty string or None", - ) - if ( - self.support_class in STRONG_ATTRIBUTION_SUPPORT_CLASSES - and self.ordering_basis.basis_kind == ParticipantAttributionOrderingBasisKind.TIMESTAMP_ADJACENCY - ): - raise ValueError("timestamp_adjacency ordering_basis cannot support strong causal attribution claims") - if ( - self.effect_candidate.candidate_kind in OUTCOME_ATTRIBUTION_CANDIDATE_KINDS - and self.interpretation_rule_ref is None - ): - raise ValueError("downstream outcome attribution requires interpretation_rule_ref") - - -def _outcome_source_layer_from_payload(value: Any) -> OutcomeInterpretationSourceLayer: - if isinstance(value, OutcomeInterpretationSourceLayer): - return value - return OutcomeInterpretationSourceLayer(str(value)) - - -def _outcome_target_layer_from_payload(value: Any) -> OutcomeInterpretationTargetLayer: - if isinstance(value, OutcomeInterpretationTargetLayer): - return value - return OutcomeInterpretationTargetLayer(str(value)) - - -@dataclass(frozen=True) -class ParticipantOutcomeSourceRecord: - """Runtime source observed for a SEM-215 outcome interpretation.""" - - source_id: str - source_layer: OutcomeInterpretationSourceLayer - ref: str - observed_value: str - evidence_refs: tuple[str, ...] = () - provenance_refs: tuple[str, ...] = () - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeSourceRecord": - if not isinstance(payload, Mapping): - raise TypeError("participant outcome source record must be a mapping") - missing = [key for key in ("source_id", "source_layer", "ref", "observed_value") if key not in payload] - if missing: - raise ValueError("participant outcome source record is missing required fields: " + ", ".join(missing)) - return cls( - source_id=str(payload.get("source_id")), - source_layer=_outcome_source_layer_from_payload(payload.get("source_layer")), - ref=str(payload.get("ref")), - observed_value=str(payload.get("observed_value")), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), - provenance_refs=_tuple_of_non_empty_strings( - payload.get("provenance_refs", ()), - field_name="provenance_refs", - ), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "source_id": self.source_id, - "source_layer": self.source_layer.value, - "ref": self.ref, - "observed_value": self.observed_value, - "evidence_refs": list(self.evidence_refs), - "provenance_refs": list(self.provenance_refs), - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - _validate_required_string(self.source_id, "participant outcome source_id must be a non-empty string") - if not isinstance(self.source_layer, OutcomeInterpretationSourceLayer): - raise TypeError("source_layer must be an OutcomeInterpretationSourceLayer") - _validate_required_string(self.ref, "participant outcome source ref must be a non-empty string") - _validate_required_string( - self.observed_value, - "participant outcome source observed_value must be a non-empty string", - ) - _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - _tuple_of_non_empty_strings(self.provenance_refs, field_name="provenance_refs") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - - -@dataclass(frozen=True) -class ParticipantOutcomeTargetRecord: - """Runtime target interpretation produced by a SEM-215 rule.""" - - target_id: str - target_layer: OutcomeInterpretationTargetLayer - ref: str - interpreted_value: str - evidence_refs: tuple[str, ...] - limitations: tuple[str, ...] - governance_ref: str | None = None - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeTargetRecord": - if not isinstance(payload, Mapping): - raise TypeError("participant outcome target record must be a mapping") - missing = [ - key - for key in ( - "target_id", - "target_layer", - "ref", - "interpreted_value", - "evidence_refs", - "limitations", - ) - if key not in payload - ] - if missing: - raise ValueError("participant outcome target record is missing required fields: " + ", ".join(missing)) - return cls( - target_id=str(payload.get("target_id")), - target_layer=_outcome_target_layer_from_payload(payload.get("target_layer")), - ref=str(payload.get("ref")), - interpreted_value=str(payload.get("interpreted_value")), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), - limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), - governance_ref=_optional_payload_string(payload, "governance_ref"), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "target_id": self.target_id, - "target_layer": self.target_layer.value, - "ref": self.ref, - "interpreted_value": self.interpreted_value, - "evidence_refs": list(self.evidence_refs), - "limitations": list(self.limitations), - "governance_ref": self.governance_ref, - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - _validate_required_string(self.target_id, "participant outcome target_id must be a non-empty string") - if not isinstance(self.target_layer, OutcomeInterpretationTargetLayer): - raise TypeError("target_layer must be an OutcomeInterpretationTargetLayer") - _validate_required_string(self.ref, "participant outcome target ref must be a non-empty string") - _validate_required_string( - self.interpreted_value, - "participant outcome target interpreted_value must be a non-empty string", - ) - if not _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs"): - raise ValueError("participant outcome targets require evidence_refs") - if not _tuple_of_non_empty_strings(self.limitations, field_name="limitations"): - raise ValueError("participant outcome targets require limitations") - _validate_optional_string(self.governance_ref, "governance_ref must be a non-empty string or None") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - if self.target_layer == OutcomeInterpretationTargetLayer.REWARD_SIGNAL and self.governance_ref is None: - raise ValueError("reward_signal outcome targets require governance_ref") - - -def _participant_outcome_source_records_from_payload(value: Any) -> tuple[ParticipantOutcomeSourceRecord, ...]: - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("outcome source_bindings must be a list of source records") - return tuple(ParticipantOutcomeSourceRecord.from_payload(item) for item in value) - - -def _participant_outcome_target_records_from_payload(value: Any) -> tuple[ParticipantOutcomeTargetRecord, ...]: - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("outcome target_bindings must be a list of target records") - return tuple(ParticipantOutcomeTargetRecord.from_payload(item) for item in value) - - -@dataclass(frozen=True) -class ParticipantOutcomeInterpretationRecord: - """Provenance-bearing SEM-215 interpretation of participant-local outcomes.""" - - interpretation_id: str - rule_address: str - participant_address: str - episode_id: str - observation_point: str - source_bindings: tuple[ParticipantOutcomeSourceRecord, ...] - target_bindings: tuple[ParticipantOutcomeTargetRecord, ...] - evidence_refs: tuple[str, ...] - limitations: tuple[str, ...] - diagnostics: tuple[str, ...] = () - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeInterpretationRecord": - if not isinstance(payload, Mapping): - raise TypeError("participant outcome interpretation record must be a mapping") - missing = [ - key - for key in ( - "interpretation_id", - "rule_address", - "participant_address", - "episode_id", - "observation_point", - "source_bindings", - "target_bindings", - "evidence_refs", - "limitations", - ) - if key not in payload - ] - if missing: - raise ValueError( - "participant outcome interpretation record is missing required fields: " + ", ".join(missing) - ) - return cls( - interpretation_id=str(payload.get("interpretation_id")), - rule_address=str(payload.get("rule_address")), - participant_address=str(payload.get("participant_address")), - episode_id=str(payload.get("episode_id")), - observation_point=str(payload.get("observation_point")), - source_bindings=_participant_outcome_source_records_from_payload(payload.get("source_bindings")), - target_bindings=_participant_outcome_target_records_from_payload(payload.get("target_bindings")), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), - limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), - diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "interpretation_id": self.interpretation_id, - "rule_address": self.rule_address, - "participant_address": self.participant_address, - "episode_id": self.episode_id, - "observation_point": self.observation_point, - "source_bindings": [source.to_payload() for source in self.source_bindings], - "target_bindings": [target.to_payload() for target in self.target_bindings], - "evidence_refs": list(self.evidence_refs), - "limitations": list(self.limitations), - "diagnostics": list(self.diagnostics), - } - - def __post_init__(self) -> None: - _validate_required_string( - self.interpretation_id, - "participant outcome interpretation_id must be a non-empty string", - ) - _validate_required_address( - self.rule_address, - prefix=_PARTICIPANT_OUTCOME_RULE_PREFIX, - message="rule_address must be a compiled participant outcome interpretation rule address", - ) - _validate_required_string( - self.participant_address, - "participant outcome participant_address must be a non-empty string", - ) - _validate_required_string(self.episode_id, "participant outcome episode_id must be a non-empty string") - _validate_required_string( - self.observation_point, - "participant outcome observation_point must be a non-empty string", - ) - if not isinstance(self.source_bindings, tuple): - raise TypeError("source_bindings must be a tuple") - if not self.source_bindings: - raise ValueError("participant outcome interpretations require source_bindings") - if any(not isinstance(source, ParticipantOutcomeSourceRecord) for source in self.source_bindings): - raise TypeError("source_bindings must contain ParticipantOutcomeSourceRecord values") - if len({source.source_id for source in self.source_bindings}) != len(self.source_bindings): - raise ValueError("participant outcome source_id values must be unique") - if not isinstance(self.target_bindings, tuple): - raise TypeError("target_bindings must be a tuple") - if not self.target_bindings: - raise ValueError("participant outcome interpretations require target_bindings") - if any(not isinstance(target, ParticipantOutcomeTargetRecord) for target in self.target_bindings): - raise TypeError("target_bindings must contain ParticipantOutcomeTargetRecord values") - if len({target.target_id for target in self.target_bindings}) != len(self.target_bindings): - raise ValueError("participant outcome target_id values must be unique") - if not _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs"): - raise ValueError("participant outcome interpretations require evidence_refs") - if not _tuple_of_non_empty_strings(self.limitations, field_name="limitations"): - raise ValueError("participant outcome interpretations require limitations") - _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") - - -def _participant_outcome_interpretation_records_from_payload( - value: Any, -) -> tuple[ParticipantOutcomeInterpretationRecord, ...]: - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("outcome_interpretations must be a list of interpretation records") - return tuple(ParticipantOutcomeInterpretationRecord.from_payload(item) for item in value) - - -def _participant_time_domain_from_payload(value: Any) -> ParticipantTimeDomain: - if isinstance(value, ParticipantTimeDomain): - return value - return ParticipantTimeDomain(str(value)) - - -def _participant_temporal_event_point_from_payload(value: Any) -> ParticipantTemporalEventPoint: - if isinstance(value, ParticipantTemporalEventPoint): - return value - return ParticipantTemporalEventPoint(str(value)) - - -def _participant_temporal_state_from_payload(value: Any) -> ParticipantTemporalState: - if isinstance(value, ParticipantTemporalState): - return value - return ParticipantTemporalState(str(value)) - - -def _participant_temporal_event_points_from_payload(value: Any) -> tuple[ParticipantTemporalEventPoint, ...]: - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("temporal event_points must be a list of event-point strings") - points = tuple(_participant_temporal_event_point_from_payload(item) for item in value) - if not points: - raise ValueError("temporal event_points must be non-empty") - if len(set(points)) != len(points): - raise ValueError("temporal event_points must be unique") - return points - - -@dataclass(frozen=True) -class ParticipantTemporalRuntimeContext: - """Realized SEM-213 temporal context on a participant behavior event.""" - - temporal_contract_id: str - time_domain: ParticipantTimeDomain - clock_authority: str - event_points: tuple[ParticipantTemporalEventPoint, ...] - observation_point: str - backend_disclosure_refs: tuple[str, ...] = () - reset_boundary: str | None = None - replay_boundary: str | None = None - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantTemporalRuntimeContext": - if not isinstance(payload, Mapping): - raise TypeError("participant temporal runtime context must be a mapping") - missing = [ - key - for key in ( - "temporal_contract_id", - "time_domain", - "clock_authority", - "event_points", - "observation_point", - ) - if key not in payload - ] - if missing: - raise ValueError("participant temporal runtime context is missing required fields: " + ", ".join(missing)) - return cls( - temporal_contract_id=str(payload.get("temporal_contract_id")), - time_domain=_participant_time_domain_from_payload(payload.get("time_domain")), - clock_authority=str(payload.get("clock_authority")), - event_points=_participant_temporal_event_points_from_payload(payload.get("event_points")), - observation_point=str(payload.get("observation_point")), - backend_disclosure_refs=_tuple_of_non_empty_strings( - payload.get("backend_disclosure_refs", ()), - field_name="backend_disclosure_refs", - ), - reset_boundary=_optional_payload_string(payload, "reset_boundary"), - replay_boundary=_optional_payload_string(payload, "replay_boundary"), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "temporal_contract_id": self.temporal_contract_id, - "time_domain": self.time_domain.value, - "clock_authority": self.clock_authority, - "event_points": [point.value for point in self.event_points], - "observation_point": self.observation_point, - "backend_disclosure_refs": list(self.backend_disclosure_refs), - "reset_boundary": self.reset_boundary, - "replay_boundary": self.replay_boundary, - } - - def __post_init__(self) -> None: - _validate_required_string( - self.temporal_contract_id, - "participant temporal temporal_contract_id must be a non-empty string", - ) - if not isinstance(self.time_domain, ParticipantTimeDomain): - raise TypeError("time_domain must be a ParticipantTimeDomain") - _validate_required_string(self.clock_authority, "participant temporal clock_authority must be non-empty") - if not isinstance(self.event_points, tuple): - raise TypeError("event_points must be a tuple") - if not self.event_points: - raise ValueError("participant temporal event_points must be non-empty") - if any(not isinstance(point, ParticipantTemporalEventPoint) for point in self.event_points): - raise TypeError("event_points must contain ParticipantTemporalEventPoint values") - if len(set(self.event_points)) != len(self.event_points): - raise ValueError("participant temporal event_points must be unique") - _validate_required_string(self.observation_point, "participant temporal observation_point must be non-empty") - _tuple_of_non_empty_strings(self.backend_disclosure_refs, field_name="backend_disclosure_refs") - _validate_optional_string(self.reset_boundary, "reset_boundary must be a non-empty string or None") - _validate_optional_string(self.replay_boundary, "replay_boundary must be a non-empty string or None") - - -@dataclass(frozen=True) -class ParticipantTemporalStateTransition: - """Abstract SEM-213 deadline / dwell / timeout state transition.""" - - temporal_contract_id: str - from_state: ParticipantTemporalState - to_state: ParticipantTemporalState - event_point: ParticipantTemporalEventPoint - time_domain: ParticipantTimeDomain - clock_authority: str - boundary_ref: str - evidence_refs: tuple[str, ...] - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantTemporalStateTransition": - if not isinstance(payload, Mapping): - raise TypeError("participant temporal state transition must be a mapping") - missing = [ - key - for key in ( - "temporal_contract_id", - "from_state", - "to_state", - "event_point", - "time_domain", - "clock_authority", - "boundary_ref", - "evidence_refs", - ) - if key not in payload - ] - if missing: - raise ValueError("participant temporal state transition is missing required fields: " + ", ".join(missing)) - return cls( - temporal_contract_id=str(payload.get("temporal_contract_id")), - from_state=_participant_temporal_state_from_payload(payload.get("from_state")), - to_state=_participant_temporal_state_from_payload(payload.get("to_state")), - event_point=_participant_temporal_event_point_from_payload(payload.get("event_point")), - time_domain=_participant_time_domain_from_payload(payload.get("time_domain")), - clock_authority=str(payload.get("clock_authority")), - boundary_ref=str(payload.get("boundary_ref")), - evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), - ) - - def __post_init__(self) -> None: - _validate_required_string( - self.temporal_contract_id, - "participant temporal temporal_contract_id must be a non-empty string", - ) - if not isinstance(self.from_state, ParticipantTemporalState): - raise TypeError("from_state must be a ParticipantTemporalState") - if not isinstance(self.to_state, ParticipantTemporalState): - raise TypeError("to_state must be a ParticipantTemporalState") - if not isinstance(self.event_point, ParticipantTemporalEventPoint): - raise TypeError("event_point must be a ParticipantTemporalEventPoint") - if not isinstance(self.time_domain, ParticipantTimeDomain): - raise TypeError("time_domain must be a ParticipantTimeDomain") - _validate_required_string(self.clock_authority, "participant temporal clock_authority must be non-empty") - _validate_required_string(self.boundary_ref, "participant temporal boundary_ref must be non-empty") - evidence_refs = _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") - if not evidence_refs: - raise ValueError("participant temporal state transitions require evidence_refs") - - -def _participant_temporal_state_transition_from_payload( - value: ParticipantTemporalStateTransition | Mapping[str, Any], -) -> ParticipantTemporalStateTransition: - if isinstance(value, ParticipantTemporalStateTransition): - return value - return ParticipantTemporalStateTransition.from_payload(value) - - -def iter_participant_temporal_state_machine_violations( - transitions: Iterable[ParticipantTemporalStateTransition | Mapping[str, Any]], -) -> Iterator[tuple[str, str]]: - """Yield SEM-213 abstract state-machine violations.""" - - prior_state: dict[str, ParticipantTemporalState] = {} - domain_authority: dict[str, tuple[ParticipantTimeDomain, str]] = {} - terminal_states = {ParticipantTemporalState.DEADLINE_MISSED, ParticipantTemporalState.TIMEOUT} - boundary_events = {ParticipantTemporalEventPoint.RESET, ParticipantTemporalEventPoint.REPLAY} - boundary_states = {ParticipantTemporalState.RESET, ParticipantTemporalState.REPLAY_BOUNDARY} - cadence_guard_events = { - ParticipantTemporalEventPoint.SUBMIT, - ParticipantTemporalEventPoint.START, - ParticipantTemporalEventPoint.OBSERVED, - ParticipantTemporalEventPoint.EFFECTIVE, - } - cadence_ready_states = { - ParticipantTemporalState.CADENCE_READY, - ParticipantTemporalState.ELIGIBLE, - ParticipantTemporalState.RESET, - ParticipantTemporalState.REPLAY_BOUNDARY, - } - - for index, raw_transition in enumerate(transitions): - locator = f"participant temporal state transition[{index}]" - try: - transition = _participant_temporal_state_transition_from_payload(raw_transition) - except (TypeError, ValueError) as exc: - yield (locator, f"participant temporal state transition is invalid: {exc}") - continue - - key = transition.temporal_contract_id - observed_domain_authority = (transition.time_domain, transition.clock_authority) - if key in domain_authority and domain_authority[key] != observed_domain_authority: - expected_domain, expected_authority = domain_authority[key] - yield ( - locator, - f"temporal contract {key!r} changed time domain or clock authority from " - f"{expected_domain.value}/{expected_authority!r} to " - f"{transition.time_domain.value}/{transition.clock_authority!r}", - ) - else: - domain_authority[key] = observed_domain_authority - - crosses_boundary = transition.event_point in boundary_events or transition.to_state in boundary_states - if ( - key in prior_state - and transition.from_state != prior_state[key] - and prior_state[key] not in boundary_states - and not crosses_boundary - ): - yield ( - locator, - f"temporal contract {key!r} transition from_state {transition.from_state.value!r} " - f"does not match prior to_state {prior_state[key].value!r}", - ) - - if ( - transition.from_state == ParticipantTemporalState.CADENCE_WAITING - and transition.event_point in cadence_guard_events - and not crosses_boundary - ): - yield (locator, "cadence repeated event requires cadence_ready or reset/replay boundary before reuse") - elif ( - transition.to_state == ParticipantTemporalState.CADENCE_WAITING - and transition.from_state not in cadence_ready_states - and prior_state.get(key) not in cadence_ready_states - and not crosses_boundary - ): - yield (locator, "cadence_waiting requires prior cadence_ready or eligible state in the same segment") - - if ( - transition.to_state == ParticipantTemporalState.DWELL_SATISFIED - and transition.from_state != ParticipantTemporalState.DWELL_ACTIVE - and prior_state.get(key) != ParticipantTemporalState.DWELL_ACTIVE - ): - yield (locator, "dwell_satisfied requires prior dwell_active state in the same temporal segment") - - if ( - transition.from_state in terminal_states - and transition.event_point not in boundary_events - and transition.to_state not in boundary_states - ): - yield (locator, "terminal temporal state requires reset or replay boundary before reuse") - - prior_state[key] = transition.to_state - - -def _optional_payload_string(payload: Mapping[str, Any], key: str) -> str | None: - value = payload.get(key) - return str(value) if value is not None else None - - -def _participant_behavior_event_type_from_payload(value: Any) -> ParticipantBehaviorHistoryEventType: - if isinstance(value, ParticipantBehaviorHistoryEventType): - return value - return ParticipantBehaviorHistoryEventType(str(value)) - - -def _participant_interaction_class_from_payload(value: Any) -> ParticipantInteractionClass | None: - if value is None: - return None - if isinstance(value, ParticipantInteractionClass): - return value - return ParticipantInteractionClass(str(value)) - - -def _participant_lifecycle_phase_from_payload( - value: str | ParticipantRuntimeLifecyclePhase | None, -) -> ParticipantRuntimeLifecyclePhase | None: - if value is None: - return None - if isinstance(value, ParticipantRuntimeLifecyclePhase): - return value - return ParticipantRuntimeLifecyclePhase(str(value)) - - -def _participant_phase_realization_from_payload( - value: str | ParticipantPhaseRealization | None, -) -> ParticipantPhaseRealization | None: - if value is None: - return None - if isinstance(value, ParticipantPhaseRealization): - return value - return ParticipantPhaseRealization(str(value)) - - -def _participant_admission_disposition_from_payload( - value: str | ParticipantAdmissionDisposition | None, -) -> ParticipantAdmissionDisposition | None: - if value is None: - return None - if isinstance(value, ParticipantAdmissionDisposition): - return value - return ParticipantAdmissionDisposition(str(value)) - - -def _participant_lifecycle_operation_state_from_payload( - value: str | ParticipantLifecycleOperationState | None, -) -> ParticipantLifecycleOperationState | None: - if value is None: - return None - if isinstance(value, ParticipantLifecycleOperationState): - return value - return ParticipantLifecycleOperationState(str(value)) - - -def _participant_behavior_shared_state_refs_from_payload(value: Any) -> tuple[str, ...]: - if value is None: - return () - if not isinstance(value, (list, tuple)): - raise TypeError("shared_state_refs must be a list of strings") - return tuple(str(ref) for ref in value) - - -def _participant_action_result_from_payload(value: Any) -> ParticipantActionResult | None: - if value is None: - return None - if isinstance(value, ParticipantActionResult): - return value - return ParticipantActionResult.from_payload(value) - - -def _participant_attribution_edges_from_payload(value: Any) -> tuple[ParticipantAttributionEdge, ...]: - if value is None: - return () - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("attribution_edges must be a list of participant attribution edges") - return tuple( - edge if isinstance(edge, ParticipantAttributionEdge) else ParticipantAttributionEdge.from_payload(edge) - for edge in value - ) - - -def _participant_temporal_contexts_from_payload(value: Any) -> tuple[ParticipantTemporalRuntimeContext, ...]: - if value is None: - return () - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - raise TypeError("temporal_contexts must be a list of participant temporal runtime contexts") - return tuple( - context - if isinstance(context, ParticipantTemporalRuntimeContext) - else ParticipantTemporalRuntimeContext.from_payload(context) - for context in value - ) - - -def _participant_behavior_details_from_payload(value: Any) -> dict[str, Any]: - if value is None: - value = {} - if not isinstance(value, Mapping): - raise TypeError("participant behavior details must be a mapping") - details = dict(value) - for empty_ref_key in ("visible_refs", "disclosed_refs", "evidence_refs"): - refs = details.get(empty_ref_key) - if isinstance(refs, (list, tuple)) and not refs: - details.pop(empty_ref_key) - return details - - -@dataclass(frozen=True) -class ParticipantBehaviorHistoryEvent: - """Internal normalized participant behavior history event. - - The canonical record keeps actor provenance and compiled behavior-contract - addresses. Role-neutral interpretation is a projection over those records, - not a reason to treat raw action names or backend-native logs as behavior - semantics. - """ - - event_type: ParticipantBehaviorHistoryEventType - timestamp: str - participant_address: str - episode_id: str - action_instance_id: str - action_contract_address: str | None = None - observation_boundary_address: str | None = None - observation_status: ParticipantObservationStatus | None = None - actor_provenance: str | None = None - lifecycle_phase: ParticipantRuntimeLifecyclePhase | None = None - phase_realization: ParticipantPhaseRealization | None = None - admission_disposition: ParticipantAdmissionDisposition | None = None - operation_ref: str | None = None - operation_state: ParticipantLifecycleOperationState | None = None - state_transition_kind: str | None = None - post_state_digest: str | None = None - joint_action_set_id: str | None = None - realized_order: int | None = None - interaction_class: ParticipantInteractionClass | None = None - interaction_ref: str | None = None - shared_state_refs: tuple[str, ...] = () - action_result: ParticipantActionResult | None = None - attribution_edges: tuple[ParticipantAttributionEdge, ...] = () - outcome_interpretations: tuple[ParticipantOutcomeInterpretationRecord, ...] = () - temporal_contexts: tuple[ParticipantTemporalRuntimeContext, ...] = () - details: dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_payload( - cls, - payload: Mapping[str, Any], - ) -> "ParticipantBehaviorHistoryEvent": - if not isinstance(payload, Mapping): - raise TypeError("participant behavior history event must be a mapping") - missing_keys = [ - key - for key in ( - "event_type", - "timestamp", - "participant_address", - "episode_id", - "action_instance_id", - ) - if key not in payload - ] - if missing_keys: - raise ValueError( - "participant behavior history event is missing required fields: " + ", ".join(missing_keys) - ) - return cls( - event_type=_participant_behavior_event_type_from_payload(payload.get("event_type")), - timestamp=str(payload.get("timestamp")), - participant_address=str(payload.get("participant_address")), - episode_id=str(payload.get("episode_id")), - action_instance_id=str(payload.get("action_instance_id")), - action_contract_address=_optional_payload_string(payload, "action_contract_address"), - observation_boundary_address=_optional_payload_string(payload, "observation_boundary_address"), - observation_status=_participant_observation_status_from_payload(payload.get("observation_status")), - actor_provenance=_optional_payload_string(payload, "actor_provenance"), - lifecycle_phase=_participant_lifecycle_phase_from_payload(payload.get("lifecycle_phase")), - phase_realization=_participant_phase_realization_from_payload(payload.get("phase_realization")), - admission_disposition=_participant_admission_disposition_from_payload(payload.get("admission_disposition")), - operation_ref=_optional_payload_string(payload, "operation_ref"), - operation_state=_participant_lifecycle_operation_state_from_payload(payload.get("operation_state")), - state_transition_kind=_optional_payload_string(payload, "state_transition_kind"), - post_state_digest=_optional_payload_string(payload, "post_state_digest"), - joint_action_set_id=_optional_payload_string(payload, "joint_action_set_id"), - realized_order=payload.get("realized_order"), - interaction_class=_participant_interaction_class_from_payload(payload.get("interaction_class")), - interaction_ref=_optional_payload_string(payload, "interaction_ref"), - shared_state_refs=_participant_behavior_shared_state_refs_from_payload( - payload.get("shared_state_refs", ()) - ), - action_result=_participant_action_result_from_payload(payload.get("action_result")), - attribution_edges=_participant_attribution_edges_from_payload(payload.get("attribution_edges", ())), - outcome_interpretations=_participant_outcome_interpretation_records_from_payload( - payload.get("outcome_interpretations", ()) - ), - temporal_contexts=_participant_temporal_contexts_from_payload(payload.get("temporal_contexts", ())), - details=_participant_behavior_details_from_payload(payload.get("details", {})), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "event_type": self.event_type.value, - "timestamp": self.timestamp, - "participant_address": self.participant_address, - "episode_id": self.episode_id, - "action_instance_id": self.action_instance_id, - "action_contract_address": self.action_contract_address, - "observation_boundary_address": self.observation_boundary_address, - "observation_status": self.observation_status.value if self.observation_status is not None else None, - "actor_provenance": self.actor_provenance, - "lifecycle_phase": self.lifecycle_phase.value if self.lifecycle_phase is not None else None, - "phase_realization": self.phase_realization.value if self.phase_realization is not None else None, - "admission_disposition": ( - self.admission_disposition.value if self.admission_disposition is not None else None - ), - "operation_ref": self.operation_ref, - "operation_state": self.operation_state.value if self.operation_state is not None else None, - "state_transition_kind": self.state_transition_kind, - "post_state_digest": self.post_state_digest, - "joint_action_set_id": self.joint_action_set_id, - "realized_order": self.realized_order, - "interaction_class": self.interaction_class.value if self.interaction_class is not None else None, - "interaction_ref": self.interaction_ref, - "shared_state_refs": list(self.shared_state_refs), - "action_result": self.action_result.to_payload() if self.action_result is not None else None, - "attribution_edges": [edge.to_payload() for edge in self.attribution_edges], - "outcome_interpretations": [record.to_payload() for record in self.outcome_interpretations], - "temporal_contexts": [context.to_payload() for context in self.temporal_contexts], - "details": dict(self.details), - } - - def __post_init__(self) -> None: - self._validate_common_fields() - self._validate_event_type_fields() - - def _validate_common_fields(self) -> None: - if not isinstance(self.event_type, ParticipantBehaviorHistoryEventType): - raise TypeError("event_type must be a ParticipantBehaviorHistoryEventType") - self._validate_required_string(self.timestamp, "participant behavior timestamp must be a non-empty string") - self._validate_required_string( - self.participant_address, - "participant behavior participant_address must be a non-empty string", - ) - self._validate_required_string(self.episode_id, "participant behavior episode_id must be a non-empty string") - self._validate_required_string(self.action_instance_id, "action_instance_id must be a non-empty string") - self._validate_optional_address( - self.action_contract_address, - prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, - message="action_contract_address must be a compiled participant action contract address", - ) - self._validate_optional_address( - self.observation_boundary_address, - prefix=_PARTICIPANT_OBSERVATION_BOUNDARY_PREFIX, - message="observation_boundary_address must be a compiled participant observation boundary address", - ) - if self.observation_status is not None and not isinstance( - self.observation_status, - ParticipantObservationStatus, - ): - raise TypeError("observation_status must be a ParticipantObservationStatus or None") - self._validate_optional_string(self.actor_provenance, "actor_provenance must be a non-empty string or None") - self._validate_lifecycle_fields() - self._validate_optional_state_fields() - self._validate_realized_order() - self._validate_interaction_type() - if not isinstance(self.shared_state_refs, tuple): - raise TypeError("shared_state_refs must be a tuple") - for ref in self.shared_state_refs: - self._validate_required_string(ref, "shared_state_refs entries must be non-empty strings") - if len(set(self.shared_state_refs)) != len(self.shared_state_refs): - raise ValueError("shared_state_refs entries must be unique") - self._validate_interaction_fields() - self._validate_action_result_type() - self._validate_attribution_edge_types() - self._validate_outcome_interpretation_types() - self._validate_temporal_context_types() - if not isinstance(self.details, dict): - raise TypeError("participant behavior details must be a dict") - - def _validate_optional_state_fields(self) -> None: - self._validate_optional_string( - self.state_transition_kind, - "state_transition_kind must be a non-empty string or None", - ) - self._validate_optional_string(self.post_state_digest, "post_state_digest must be a non-empty string or None") - self._validate_optional_string( - self.joint_action_set_id, - "joint_action_set_id must be a non-empty string or None", - ) - - def _validate_lifecycle_fields(self) -> None: - self._validate_lifecycle_enum_types() - self._validate_optional_string(self.operation_ref, "operation_ref must be a non-empty string or None") - messages = participant_lifecycle_field_violation_messages( - event_type=self.event_type, - lifecycle_phase=self.lifecycle_phase, - phase_realization=self.phase_realization, - admission_disposition=self.admission_disposition, - operation_ref=self.operation_ref, - operation_state=self.operation_state, - ) - if messages: - raise ValueError(messages[0]) - - def _validate_lifecycle_enum_types(self) -> None: - expectations = ( - ( - self.lifecycle_phase, - ParticipantRuntimeLifecyclePhase, - "lifecycle_phase must be a ParticipantRuntimeLifecyclePhase or None", - ), - ( - self.phase_realization, - ParticipantPhaseRealization, - "phase_realization must be a ParticipantPhaseRealization or None", - ), - ( - self.admission_disposition, - ParticipantAdmissionDisposition, - "admission_disposition must be a ParticipantAdmissionDisposition or None", - ), - ( - self.operation_state, - ParticipantLifecycleOperationState, - "operation_state must be a ParticipantLifecycleOperationState or None", - ), - ) - for value, enum_type, message in expectations: - if value is not None and not isinstance(value, enum_type): - raise TypeError(message) - - def _validate_realized_order(self) -> None: - if self.realized_order is not None and ( - not isinstance(self.realized_order, int) or isinstance(self.realized_order, bool) or self.realized_order < 0 - ): - raise TypeError("realized_order must be a non-negative integer or None") - - def _validate_interaction_type(self) -> None: - if self.interaction_class is not None and not isinstance(self.interaction_class, ParticipantInteractionClass): - raise TypeError("interaction_class must be a ParticipantInteractionClass or None") - self._validate_optional_string(self.interaction_ref, "interaction_ref must be a non-empty string or None") - - def _validate_action_result_type(self) -> None: - if self.action_result is not None and not isinstance(self.action_result, ParticipantActionResult): - raise TypeError("action_result must be a ParticipantActionResult or None") - - def _validate_attribution_edge_types(self) -> None: - if not isinstance(self.attribution_edges, tuple): - raise TypeError("attribution_edges must be a tuple") - if any(not isinstance(edge, ParticipantAttributionEdge) for edge in self.attribution_edges): - raise TypeError("attribution_edges must contain ParticipantAttributionEdge values") - if len({edge.edge_id for edge in self.attribution_edges}) != len(self.attribution_edges): - raise ValueError("participant attribution edge_id values must be unique per event") - if self.attribution_edges and self.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - raise ValueError("participant attribution edges are only allowed on observation_emitted events") - - def _validate_outcome_interpretation_types(self) -> None: - if not isinstance(self.outcome_interpretations, tuple): - raise TypeError("outcome_interpretations must be a tuple") - if any( - not isinstance(record, ParticipantOutcomeInterpretationRecord) for record in self.outcome_interpretations - ): - raise TypeError("outcome_interpretations must contain ParticipantOutcomeInterpretationRecord values") - if len({record.interpretation_id for record in self.outcome_interpretations}) != len( - self.outcome_interpretations - ): - raise ValueError("participant outcome interpretation_id values must be unique per event") - if self.outcome_interpretations and self.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - raise ValueError("participant outcome interpretations are only allowed on observation_emitted events") - - def _validate_temporal_context_types(self) -> None: - if not isinstance(self.temporal_contexts, tuple): - raise TypeError("temporal_contexts must be a tuple") - if any(not isinstance(context, ParticipantTemporalRuntimeContext) for context in self.temporal_contexts): - raise TypeError("temporal_contexts must contain ParticipantTemporalRuntimeContext values") - if len({context.temporal_contract_id for context in self.temporal_contexts}) != len(self.temporal_contexts): - raise ValueError("participant temporal_contract_id values must be unique per event") - - @staticmethod - def _validate_required_string(value: Any, message: str) -> None: - if not isinstance(value, str) or not value: - raise TypeError(message) - - @staticmethod - def _validate_optional_string(value: Any, message: str) -> None: - if value is not None and (not isinstance(value, str) or not value): - raise TypeError(message) - - @staticmethod - def _validate_optional_address(value: str | None, *, prefix: str, message: str) -> None: - if value is not None and (not isinstance(value, str) or not value.startswith(prefix)): - raise ValueError(message) - - def _validate_event_type_fields(self) -> None: - validators = { - ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED: self._validate_action_attempted_fields, - ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED: self._validate_state_transition_fields, - ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: self._validate_observation_emitted_fields, - } - validators[self.event_type]() - - def _validate_interaction_fields(self) -> None: - if self.joint_action_set_id is None and self.realized_order is not None: - raise ValueError("realized_order requires joint_action_set_id") - if self.joint_action_set_id is not None and self.realized_order is None: - raise ValueError("joint_action_set_id requires realized_order") - if self.interaction_class is None: - if self.interaction_ref is not None: - raise ValueError("interaction_ref requires interaction_class") - return - if self.joint_action_set_id is None: - raise ValueError("interaction_class requires joint_action_set_id and realized_order") - if ( - self.interaction_class - in { - ParticipantInteractionClass.COORDINATION, - ParticipantInteractionClass.INTERFERENCE, - } - and self.interaction_ref is None - ): - raise ValueError(f"{self.interaction_class.value} events require interaction_ref") - if ( - self.interaction_class - in { - ParticipantInteractionClass.CONTENTION, - ParticipantInteractionClass.SHARED_STATE_CHANGE, - } - and not self.shared_state_refs - ): - raise ValueError(f"{self.interaction_class.value} events require shared_state_refs") - - def _validate_action_attempted_fields(self) -> None: - if self.action_contract_address is None: - raise ValueError("action_attempted events require action_contract_address") - if self.actor_provenance is None: - raise ValueError("action_attempted events require actor_provenance") - if self.observation_boundary_address is not None or self.observation_status is not None: - raise ValueError("action_attempted events may not report observation fields") - if self.state_transition_kind is not None or self.post_state_digest is not None: - raise ValueError("action_attempted events may not report state-transition fields") - if self.action_result is not None: - raise ValueError("action_attempted events may not report action_result") - - def _validate_state_transition_fields(self) -> None: - if self.action_contract_address is None: - raise ValueError("state_transition_recorded events require action_contract_address") - if self.state_transition_kind is None: - raise ValueError("state_transition_recorded events require state_transition_kind") - if self.post_state_digest is None: - raise ValueError("state_transition_recorded events require post_state_digest") - if self.observation_boundary_address is not None or self.observation_status is not None: - raise ValueError("state_transition_recorded events may not report observation fields") - if self.action_result is not None: - raise ValueError("state_transition_recorded events may not report action_result") - - def _validate_observation_emitted_fields(self) -> None: - if self.action_contract_address is None: - raise ValueError("observation_emitted events require action_contract_address") - if self.observation_boundary_address is None: - raise ValueError("observation_emitted events require observation_boundary_address") - if self.observation_status is None: - raise ValueError("observation_emitted events require observation_status") - if self.observation_status == ParticipantObservationStatus.TERMINAL and self.post_state_digest is None: - raise ValueError("terminal observation_emitted events require post_state_digest") - if self.state_transition_kind is not None: - raise ValueError("observation_emitted events may not report state_transition_kind") - if self.action_result is not None: - self._validate_action_result_scope() - self._validate_attribution_edges() - self._validate_outcome_interpretations() - - def _validate_action_result_scope(self) -> None: - if self.action_result is None: - return - if self.action_result.participant_address != self.participant_address: - raise ValueError("action_result participant_address must match event participant_address") - if self.action_result.episode_id != self.episode_id: - raise ValueError("action_result episode_id must match event episode_id") - if self.action_result.action_instance_id != self.action_instance_id: - raise ValueError("action_result action_instance_id must match event action_instance_id") - if self.action_result.action_contract_address != self.action_contract_address: - raise ValueError("action_result action_contract_address must match event action_contract_address") - if ( - self.observation_status in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES - and self.action_result.status == ParticipantActionResultStatus.ACCEPTED - ): - raise ValueError("terminal observation action_result must report a terminal status") - - def _validate_attribution_edges(self) -> None: - for edge in self.attribution_edges: - if edge.participant_address != self.participant_address: - raise ValueError("attribution edge participant_address must match event participant_address") - if edge.episode_id != self.episode_id: - raise ValueError("attribution edge episode_id must match event episode_id") - if self.action_result is not None: - if edge.observation_point != self.action_result.observation_point: - raise ValueError("attribution edge observation_point must match action_result observation_point") - elif not _observation_point_matches_action_instance(edge.observation_point, self.action_instance_id): - raise ValueError("attribution edge observation_point must be anchored to action_instance_id") - self._validate_attribution_candidate_grounding(edge) - - def _validate_outcome_interpretations(self) -> None: - for record in self.outcome_interpretations: - if record.participant_address != self.participant_address: - raise ValueError("outcome interpretation participant_address must match event participant_address") - if record.episode_id != self.episode_id: - raise ValueError("outcome interpretation episode_id must match event episode_id") - if self.action_result is not None: - if record.observation_point != self.action_result.observation_point: - raise ValueError( - "outcome interpretation observation_point must match action_result observation_point" - ) - elif not _observation_point_matches_action_instance(record.observation_point, self.action_instance_id): - raise ValueError("outcome interpretation observation_point must be anchored to action_instance_id") - - def _validate_attribution_candidate_grounding(self, edge: ParticipantAttributionEdge) -> None: - allowed_action_refs = {self.action_instance_id} - if self.action_contract_address is not None: - allowed_action_refs.add(self.action_contract_address) - if edge.cause_candidate.candidate_kind == ParticipantAttributionCandidateKind.ACTION: - if edge.cause_candidate.ref not in allowed_action_refs: - raise ValueError("attribution edge action cause_candidate must match the event action") - elif edge.cause_candidate.ref not in self._attribution_grounded_refs(): - raise ValueError(f"attribution edge cause_candidate {edge.cause_candidate.ref!r} is not grounded") - - if edge.effect_candidate.candidate_kind in OUTCOME_ATTRIBUTION_CANDIDATE_KINDS: - return - if edge.effect_candidate.ref not in self._attribution_grounded_refs(): - raise ValueError(f"attribution edge effect_candidate {edge.effect_candidate.ref!r} is not grounded") - - def _attribution_grounded_refs(self) -> set[str]: - refs: set[str] = {self.action_instance_id} - if self.action_contract_address is not None: - refs.add(self.action_contract_address) - if self.observation_boundary_address is not None: - refs.add(self.observation_boundary_address) - if self.post_state_digest is not None: - refs.add(self.post_state_digest) - for key in _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS: - value = self.details.get(key) - if isinstance(value, (list, tuple)): - refs.update(str(item) for item in value if isinstance(item, str) and item) - if self.action_result is None: - return refs - refs.update(self.action_result.observations) - refs.update(self.action_result.evidence_refs) - for precondition in self.action_result.preconditions: - refs.update(precondition.support_refs) - refs.update(precondition.evidence_refs) - for effect in self.action_result.effects: - refs.update(effect.target_refs) - refs.update(effect.evidence_refs) - return refs - - -_PARTICIPANT_TERMINAL_OBSERVATION_STATUSES = frozenset( - { - ParticipantObservationStatus.TERMINAL, - ParticipantObservationStatus.ORPHANED_ACTION, - } -) -_PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS = frozenset({"observable", "discovered", "inferred", "disclosed", "deceptive"}) -_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS = ("visible_refs", "disclosed_refs", "evidence_refs") -_PARTICIPANT_OBSERVATION_DETAIL_KEYS = frozenset(_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS) - - -def _participant_behavior_detail_refs( - event: ParticipantBehaviorHistoryEvent, - *, - key: str, - locator: str, -) -> tuple[tuple[str, ...], list[tuple[str, str]]]: - if key not in event.details: - return (), [] - value = event.details[key] - if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): - return (), [(locator, f"observation details field {key!r} must be a list of strings")] - items = tuple(value) - refs = tuple(str(ref) for ref in items if isinstance(ref, str) and ref) - if len(refs) != len(items): - return (), [(locator, f"observation details field {key!r} must contain only non-empty strings")] - if len(set(refs)) != len(refs): - return (), [(locator, f"observation details field {key!r} must not contain duplicate refs")] - return refs, [] - - -def _participant_behavior_detail_shape_violations( - event: ParticipantBehaviorHistoryEvent, - *, - locator: str, -) -> list[tuple[str, str]]: - if not event.details: - return [] - violations: list[tuple[str, str]] = [] - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - violations.append((locator, "participant behavior details are only allowed on observation_emitted events")) - unsupported_keys = sorted(str(key) for key in event.details if key not in _PARTICIPANT_OBSERVATION_DETAIL_KEYS) - if unsupported_keys: - allowed = ", ".join(_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS) - unsupported = ", ".join(unsupported_keys) - violations.append( - ( - locator, - f"observation details may only contain {allowed}; unsupported fields: {unsupported}", - ) - ) - return violations - - -def _participant_behavior_timeline_relations( - boundary: ParticipantObservationBoundaryRuntime, -) -> tuple[tuple[int, dict[str, str]], ...]: - relations: list[tuple[int, dict[str, str]]] = [] - for snapshot in boundary.view_relation_timeline: - order = snapshot.get("effective_order") - raw_relation = snapshot.get("view_relation", {}) - if not isinstance(order, int) or isinstance(order, bool) or not isinstance(raw_relation, Mapping): - continue - relations.append((order, {str(ref): str(disposition) for ref, disposition in raw_relation.items()})) - return tuple(sorted(relations, key=lambda item: item[0])) - - -def _participant_behavior_initial_view_relation( - boundary: ParticipantObservationBoundaryRuntime, -) -> dict[str, str]: - initial_relation: dict[str, str] = {} - for order, relation in _participant_behavior_timeline_relations(boundary): - if order > -1: - break - initial_relation = dict(relation) - return initial_relation - - -def _participant_behavior_view_relation_deltas_by_order( - boundary: ParticipantObservationBoundaryRuntime, -) -> dict[int, dict[str, str]]: - deltas: dict[int, dict[str, str]] = {} - previous_relation: dict[str, str] | None = None - for order, relation in _participant_behavior_timeline_relations(boundary): - if previous_relation is None: - previous_relation = relation - continue - deltas[order] = { - ref: disposition for ref, disposition in relation.items() if previous_relation.get(ref) != disposition - } - previous_relation = relation - return deltas - - -def _participant_behavior_transition_effective_order(transition: Mapping[str, Any]) -> int | None: - order = transition.get("effective_order") - if not isinstance(order, int) or isinstance(order, bool): - return None - return order - - -def _participant_behavior_transition_delta( - transition: Mapping[str, Any], - *, - deltas_by_order: Mapping[int, Mapping[str, str]], -) -> dict[str, str]: - information_ref = transition.get("information_ref") - to_disposition = transition.get("to_disposition") - if isinstance(information_ref, str) and information_ref and isinstance(to_disposition, str) and to_disposition: - return {information_ref: to_disposition} - order = _participant_behavior_transition_effective_order(transition) - if order is None: - return {} - return dict(deltas_by_order.get(order, {})) - - -def _participant_behavior_transition_matches_relation( - transition: Mapping[str, Any], - *, - relation: Mapping[str, str], -) -> bool: - information_ref = transition.get("information_ref") - from_disposition = transition.get("from_disposition") - if not isinstance(information_ref, str) or not information_ref: - return True - if not isinstance(from_disposition, str) or not from_disposition: - return True - return relation.get(information_ref) == from_disposition - - -def _participant_behavior_observation_detail_refs( - event: ParticipantBehaviorHistoryEvent, - *, - locator: str, -) -> tuple[dict[str, tuple[str, ...]], list[tuple[str, str]]]: - detail_refs: dict[str, tuple[str, ...]] = {} - violations: list[tuple[str, str]] = [] - for key in _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS: - refs, ref_violations = _participant_behavior_detail_refs(event, key=key, locator=locator) - detail_refs[key] = refs - violations.extend(ref_violations) - return detail_refs, violations - - -def _participant_behavior_disposition_ref_violations( - *, - locator: str, - refs: tuple[str, ...], - relation: Mapping[str, str], - allowed_dispositions: frozenset[str], - effective_order: int, - detail_key: str, - allowed_label: str, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for ref in refs: - disposition = relation.get(ref) - if disposition in allowed_dispositions: - continue - violations.append( - ( - locator, - ( - f"observation {detail_key} may only contain {allowed_label} refs at " - f"effective_order {effective_order}: " - f"{ref!r} has disposition {disposition!r}" - ), - ) - ) - return violations - - -def _participant_behavior_evidence_ref_violations( - *, - locator: str, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for ref in refs: - if ref in boundary.evidence_refs or relation.get(ref) == "evidence_only": - continue - violations.append( - ( - locator, - ( - "observation evidence_refs may only contain boundary evidence refs at " - f"effective_order {effective_order}: {ref!r}" - ), - ) - ) - return violations - - -def _participant_behavior_visibility_detail_violations( - *, - locator: str, - detail_refs: Mapping[str, tuple[str, ...]], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - return [ - *_participant_behavior_disposition_ref_violations( - locator=locator, - refs=detail_refs["visible_refs"], - relation=relation, - allowed_dispositions=_PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS, - effective_order=effective_order, - detail_key="visible_refs", - allowed_label="participant-visible", - ), - *_participant_behavior_disposition_ref_violations( - locator=locator, - refs=detail_refs["disclosed_refs"], - relation=relation, - allowed_dispositions=frozenset({"disclosed"}), - effective_order=effective_order, - detail_key="disclosed_refs", - allowed_label="disclosed", - ), - *_participant_behavior_evidence_ref_violations( - locator=locator, - refs=detail_refs["evidence_refs"], - boundary=boundary, - relation=relation, - effective_order=effective_order, - ), - ] - - -def _participant_behavior_action_result_visible_ref_violations( - *, - locator: str, - owner_label: str, - field_name: str, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - owner_prefix = f" {owner_label}" if owner_label else "" - for ref in refs: - disposition = relation.get(ref) - if disposition is None and ref in boundary.hidden_refs: - disposition = "hidden" - if disposition is None: - continue - if disposition in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: - continue - violations.append( - ( - locator, - ( - f"action_result{owner_prefix} {field_name} {ref!r} is not participant-visible " - f"at effective_order {effective_order}: disposition {disposition!r}" - ), - ) - ) - return violations - - -def _participant_behavior_action_result_evidence_ref_violations( - *, - locator: str, - owner_label: str, - field_name: str, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - owner_prefix = f" {owner_label}" if owner_label else "" - for ref in refs: - disposition = relation.get(ref) - if disposition is None and ref in boundary.hidden_refs: - disposition = "hidden" - if ref in boundary.evidence_refs or disposition == "evidence_only": - continue - suffix = f": disposition {disposition!r}" if disposition is not None else "" - violations.append( - ( - locator, - ( - f"action_result{owner_prefix} {field_name} {ref!r} is not authorized evidence " - f"at effective_order {effective_order}{suffix}" - ), - ) - ) - return violations - - -def _participant_behavior_action_result_ref_authorization_violations( - *, - event: ParticipantBehaviorHistoryEvent, - locator: str, - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - if event.action_result is None: - return [] - violations: list[tuple[str, str]] = [] - for precondition in event.action_result.preconditions: - owner_label = f"precondition {precondition.precondition_id!r}" - violations.extend( - _participant_behavior_action_result_visible_ref_violations( - locator=locator, - owner_label=owner_label, - field_name="support_ref", - refs=precondition.support_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_action_result_evidence_ref_violations( - locator=locator, - owner_label=owner_label, - field_name="evidence_ref", - refs=precondition.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - for effect in event.action_result.effects: - owner_label = f"effect {effect.effect_id!r}" - violations.extend( - _participant_behavior_action_result_visible_ref_violations( - locator=locator, - owner_label=owner_label, - field_name="target_ref", - refs=effect.target_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_action_result_evidence_ref_violations( - locator=locator, - owner_label=owner_label, - field_name="evidence_ref", - refs=effect.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_action_result_evidence_ref_violations( - locator=locator, - owner_label="", - field_name="evidence_ref", - refs=event.action_result.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - return violations - - -def _participant_behavior_attribution_evidence_ref_violations( - *, - locator: str, - edge: ParticipantAttributionEdge, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for ref in refs: - disposition = relation.get(ref) - if disposition is None and ref in boundary.hidden_refs: - disposition = "hidden" - if ref in boundary.evidence_refs or disposition == "evidence_only": - continue - suffix = f": disposition {disposition!r}" if disposition is not None else "" - violations.append( - ( - locator, - ( - f"attribution edge {edge.edge_id!r} evidence_ref {ref!r} is not authorized evidence " - f"at effective_order {effective_order}{suffix}" - ), - ) - ) - return violations - - -def _participant_behavior_attribution_candidate_ref_violations( - *, - locator: str, - edge: ParticipantAttributionEdge, - candidate: ParticipantAttributionCandidate, - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - if candidate.candidate_kind == ParticipantAttributionCandidateKind.ACTION: - return [] - if candidate.candidate_kind == ParticipantAttributionCandidateKind.EVIDENCE: - return _participant_behavior_attribution_evidence_ref_violations( - locator=locator, - edge=edge, - refs=(candidate.ref,), - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - disposition = relation.get(candidate.ref) - if disposition is None and candidate.ref in boundary.hidden_refs: - disposition = "hidden" - if disposition is None or disposition in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: - return [] - return [ - ( - locator, - ( - f"attribution edge {edge.edge_id!r} {candidate.candidate_kind.value} candidate " - f"{candidate.ref!r} is not participant-visible at effective_order {effective_order}: " - f"disposition {disposition!r}" - ), - ) - ] - - -def _participant_behavior_attribution_ref_authorization_violations( - *, - event: ParticipantBehaviorHistoryEvent, - locator: str, - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for edge in event.attribution_edges: - violations.extend( - _participant_behavior_attribution_evidence_ref_violations( - locator=locator, - edge=edge, - refs=edge.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_attribution_candidate_ref_violations( - locator=locator, - edge=edge, - candidate=edge.cause_candidate, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_attribution_candidate_ref_violations( - locator=locator, - edge=edge, - candidate=edge.effect_candidate, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - return violations - - -def _participant_behavior_outcome_evidence_ref_violations( - *, - locator: str, - record: ParticipantOutcomeInterpretationRecord, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for ref in refs: - disposition = relation.get(ref) - if disposition is None and ref in boundary.hidden_refs: - disposition = "hidden" - if ref in boundary.evidence_refs or disposition == "evidence_only": - continue - suffix = f": disposition {disposition!r}" if disposition is not None else "" - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} evidence_ref {ref!r} " - f"is not authorized evidence at effective_order {effective_order}{suffix}" - ), - ) - ) - return violations - - -def _participant_behavior_outcome_provenance_ref_violations( - *, - locator: str, - record: ParticipantOutcomeInterpretationRecord, - refs: tuple[str, ...], - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for ref in refs: - disposition = relation.get(ref) - if disposition is None and ref in boundary.hidden_refs: - disposition = "hidden" - if ref in boundary.evidence_refs or disposition in {"evidence_only", "disclosed", "observable", "discovered"}: - continue - if ref not in boundary.hidden_refs and disposition not in {"hidden", "concealed", "deceptive"}: - continue - suffix = f": disposition {disposition!r}" if disposition is not None else "" - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} provenance_ref {ref!r} " - f"exposes a hidden participant-boundary ref at effective_order {effective_order}{suffix}" - ), - ) - ) - return violations - - -def _participant_behavior_outcome_ref_authorization_violations( - *, - event: ParticipantBehaviorHistoryEvent, - locator: str, - boundary: ParticipantObservationBoundaryRuntime, - relation: Mapping[str, str], - effective_order: int, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - for record in event.outcome_interpretations: - violations.extend( - _participant_behavior_outcome_evidence_ref_violations( - locator=locator, - record=record, - refs=record.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - for source in record.source_bindings: - violations.extend( - _participant_behavior_outcome_evidence_ref_violations( - locator=locator, - record=record, - refs=source.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - violations.extend( - _participant_behavior_outcome_provenance_ref_violations( - locator=locator, - record=record, - refs=source.provenance_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - for target in record.target_bindings: - violations.extend( - _participant_behavior_outcome_evidence_ref_violations( - locator=locator, - record=record, - refs=target.evidence_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - ) - return violations - - -def _participant_behavior_event_evidence_refs(event: ParticipantBehaviorHistoryEvent) -> set[str]: - evidence_refs: set[str] = set() - detail_refs = event.details.get("evidence_refs") - if not isinstance(detail_refs, (str, bytes, Mapping)) and isinstance(detail_refs, Iterable): - evidence_refs.update(str(ref) for ref in detail_refs if isinstance(ref, str) and ref) - if event.action_result is not None: - evidence_refs.update(event.action_result.evidence_refs) - for precondition in event.action_result.preconditions: - evidence_refs.update(precondition.evidence_refs) - for effect in event.action_result.effects: - evidence_refs.update(effect.evidence_refs) - for edge in event.attribution_edges: - evidence_refs.update(edge.evidence_refs) - return evidence_refs - - -def _participant_episode_terminal_statuses( - participant_episode_history: Any, -) -> dict[tuple[str, str], set[str]]: - terminal_statuses: dict[tuple[str, str], set[str]] = {} - if isinstance(participant_episode_history, Mapping): - histories = participant_episode_history.values() - elif isinstance(participant_episode_history, list): - histories = (participant_episode_history,) - else: - histories = () - for history in histories: - if isinstance(history, (str, bytes, Mapping)) or not isinstance(history, Iterable): - continue - for event in history: - if not isinstance(event, Mapping): - continue - try: - normalized = ParticipantEpisodeHistoryEvent.from_payload(event) - except (TypeError, ValueError): - continue - terminal_reason = _PARTICIPANT_EPISODE_TERMINAL_EVENTS.get(normalized.event_type) - if terminal_reason is None: - continue - key = (normalized.participant_address, normalized.episode_id) - terminal_statuses.setdefault(key, set()).add(terminal_reason.value) - return terminal_statuses - - -def _participant_behavior_outcome_evidence_grounding_violations( - *, - locator: str, - record: ParticipantOutcomeInterpretationRecord, - owner_label: str, - refs: tuple[str, ...], - grounded_evidence_refs: set[str], -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - owner = f" {owner_label}" if owner_label else "" - for ref in refs: - if ref in grounded_evidence_refs: - continue - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r}{owner} evidence_ref {ref!r} " - "is not grounded in event evidence" - ), - ) - ) - return violations - - -def _participant_behavior_outcome_action_source_grounding_violations( - *, - locator: str, - event: ParticipantBehaviorHistoryEvent, - record: ParticipantOutcomeInterpretationRecord, - source: ParticipantOutcomeSourceRecord, -) -> list[tuple[str, str]]: - if source.source_layer != OutcomeInterpretationSourceLayer.PARTICIPANT_ACTION_OUTCOME: - return [] - if event.action_result is None: - return [ - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - "uses participant_action_outcome without an action_result" - ), - ) - ] - violations: list[tuple[str, str]] = [] - if source.ref != event.action_result.action_contract_address: - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"ref {source.ref!r} does not match action_result action_contract_address " - f"{event.action_result.action_contract_address!r}" - ), - ) - ) - expected_status = event.action_result.status.value - if source.observed_value != expected_status: - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"observed_value {source.observed_value!r} does not match action_result status " - f"{expected_status!r}" - ), - ) - ) - return violations - - -def _participant_behavior_outcome_episode_status_grounding_violations( - *, - locator: str, - record: ParticipantOutcomeInterpretationRecord, - source: ParticipantOutcomeSourceRecord, - terminal_statuses: Mapping[tuple[str, str], set[str]], -) -> list[tuple[str, str]]: - if source.source_layer != OutcomeInterpretationSourceLayer.PARTICIPANT_EPISODE_STATUS: - return [] - key = (record.participant_address, record.episode_id) - statuses = terminal_statuses.get(key, set()) - if not statuses: - return [ - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - "participant_episode_status is not grounded by a terminal participant_episode_history event" - ), - ) - ] - if source.observed_value in statuses: - return [] - expected = ", ".join(repr(status) for status in sorted(statuses)) - return [ - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"observed_value {source.observed_value!r} does not match participant_episode_history terminal status " - f"{expected}" - ), - ) - ] - - -def _participant_behavior_outcome_source_grounding_violations( - *, - locator: str, - event: ParticipantBehaviorHistoryEvent, - record: ParticipantOutcomeInterpretationRecord, - source: ParticipantOutcomeSourceRecord, - grounded_evidence_refs: set[str], - terminal_statuses: Mapping[tuple[str, str], set[str]], -) -> list[tuple[str, str]]: - violations = _participant_behavior_outcome_action_source_grounding_violations( - locator=locator, - event=event, - record=record, - source=source, - ) - violations.extend( - _participant_behavior_outcome_episode_status_grounding_violations( - locator=locator, - record=record, - source=source, - terminal_statuses=terminal_statuses, - ) - ) - if ( - source.source_layer == OutcomeInterpretationSourceLayer.EVIDENCE_CLAIM - and source.ref not in grounded_evidence_refs - ): - violations.append( - ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} evidence_claim source " - f"{source.source_id!r} ref {source.ref!r} is not grounded in event evidence" - ), - ) - ) - violations.extend( - _participant_behavior_outcome_evidence_grounding_violations( - locator=locator, - record=record, - owner_label=f"source {source.source_id!r}", - refs=source.evidence_refs, - grounded_evidence_refs=grounded_evidence_refs, - ) - ) - return violations - - -def _participant_behavior_outcome_event_grounding_violations( - events: Iterable[ParticipantBehaviorHistoryEvent], - *, - participant_episode_history: Any = None, -) -> Iterator[tuple[str, str]]: - terminal_statuses = _participant_episode_terminal_statuses(participant_episode_history) - for index, event in enumerate(events): - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - continue - if not event.outcome_interpretations: - continue - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - grounded_evidence_refs = _participant_behavior_event_evidence_refs(event) - for record in event.outcome_interpretations: - yield from _participant_behavior_outcome_evidence_grounding_violations( - locator=locator, - record=record, - owner_label="", - refs=record.evidence_refs, - grounded_evidence_refs=grounded_evidence_refs, - ) - for source in record.source_bindings: - yield from _participant_behavior_outcome_source_grounding_violations( - locator=locator, - event=event, - record=record, - source=source, - grounded_evidence_refs=grounded_evidence_refs, - terminal_statuses=terminal_statuses, - ) - for target in record.target_bindings: - yield from _participant_behavior_outcome_evidence_grounding_violations( - locator=locator, - record=record, - owner_label=f"target {target.target_id!r}", - refs=target.evidence_refs, - grounded_evidence_refs=grounded_evidence_refs, - ) - - -def _participant_behavior_history_anchor_indexes( - events: Iterable[ParticipantBehaviorHistoryEvent], -) -> tuple[dict[str, int], dict[str, int], dict[tuple[str, str | None], int]]: - action_attempts: dict[str, int] = {} - state_transitions: dict[str, int] = {} - observations: dict[tuple[str, str | None], int] = {} - for index, event in enumerate(events): - if event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED: - action_attempts.setdefault(event.action_instance_id, index) - elif event.event_type == ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED: - state_transitions.setdefault(event.action_instance_id, index) - elif event.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - observations.setdefault((event.action_instance_id, event.observation_boundary_address), index) - return action_attempts, state_transitions, observations - - -def _participant_behavior_transition_anchor_index( - *, - transition: Mapping[str, Any], - boundary_address: str, - action_attempts: Mapping[str, int], - state_transitions: Mapping[str, int], - observations: Mapping[tuple[str, str | None], int], -) -> int | None: - event_type = str(transition.get("history_event_type", "")) - if event_type == "episode_close": - return None - action_instance_id = transition.get("action_instance_id") - if not isinstance(action_instance_id, str) or not action_instance_id: - return None - if event_type == "action_attempted": - return action_attempts.get(action_instance_id) - if event_type == "state_transition_recorded": - return state_transitions.get(action_instance_id) - if event_type == "observation_emitted": - return observations.get((action_instance_id, boundary_address)) - return None - - -def _participant_behavior_transition_anchor_violation( - *, - transition: Mapping[str, Any], - boundary_address: str, - action_attempts: Mapping[str, int], - state_transitions: Mapping[str, int], - observations: Mapping[tuple[str, str | None], int], - episode_close_resolved: bool, -) -> tuple[str, str] | None: - event_type = str(transition.get("history_event_type", "")) - action_instance_id = transition.get("action_instance_id") - transition_id = str(transition.get("transition_id", "")) - locator = f"{boundary_address}.view_transitions.{transition_id}" - if event_type == "episode_close": - if episode_close_resolved: - return None - return ( - locator, - "visibility transition anchor does not resolve to a terminal participant episode history event", - ) - if not isinstance(action_instance_id, str) or not action_instance_id: - return (locator, "visibility transition anchors require action_instance_id") - event_indexes = { - "action_attempted": action_instance_id in action_attempts, - "state_transition_recorded": action_instance_id in state_transitions, - "observation_emitted": (action_instance_id, boundary_address) in observations, - } - if event_type not in event_indexes: - return (locator, f"visibility transition anchor has unknown history_event_type {event_type!r}") - if event_indexes[event_type]: - return None - article = "an" if event_type == "observation_emitted" else "a" - return (locator, f"visibility transition anchor does not resolve to {article} {event_type} event") - - -def _participant_behavior_transition_anchor_violations( - events: list[ParticipantBehaviorHistoryEvent], - *, - observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], - participant_episode_history: Any = None, -) -> Iterator[tuple[str, str]]: - action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) - episode_close_resolved = _participant_behavior_episode_close_resolved( - events, - participant_episode_history=participant_episode_history, - ) - for boundary_address, boundary in observation_boundaries.items(): - for transition in boundary.view_transitions: - violation = _participant_behavior_transition_anchor_violation( - transition=transition, - boundary_address=boundary_address, - action_attempts=action_attempts, - state_transitions=state_transitions, - observations=observations, - episode_close_resolved=episode_close_resolved, - ) - if violation is not None: - yield violation - - -def _participant_behavior_episode_close_resolved( - events: list[ParticipantBehaviorHistoryEvent], - *, - participant_episode_history: Any, -) -> bool: - if not isinstance(participant_episode_history, list): - return False - participant_addresses = {event.participant_address for event in events} - episode_ids = {event.episode_id for event in events} - if not participant_addresses or not episode_ids: - return False - closed_episode_ids: set[str] = set() - for event in participant_episode_history: - if not isinstance(event, Mapping): - continue - try: - normalized = ParticipantEpisodeHistoryEvent.from_payload(event) - except (TypeError, ValueError): - continue - if normalized.participant_address not in participant_addresses: - continue - if normalized.episode_id not in episode_ids: - continue - if normalized.event_type in _PARTICIPANT_EPISODE_TERMINAL_EVENTS: - closed_episode_ids.add(normalized.episode_id) - return episode_ids <= closed_episode_ids - - -def _participant_behavior_observation_effective_relation( - *, - observation_index: int, - boundary_address: str, - boundary: ParticipantObservationBoundaryRuntime, - action_attempts: Mapping[str, int], - state_transitions: Mapping[str, int], - observations: Mapping[tuple[str, str | None], int], -) -> tuple[dict[str, str], int]: - relation = _participant_behavior_initial_view_relation(boundary) - deltas_by_order = _participant_behavior_view_relation_deltas_by_order(boundary) - effective_order = -1 - for transition in sorted( - boundary.view_transitions, - key=lambda item: ( - _participant_behavior_transition_effective_order(item) - if _participant_behavior_transition_effective_order(item) is not None - else -1 - ), - ): - order = _participant_behavior_transition_effective_order(transition) - if order is None: - continue - anchor_index = _participant_behavior_transition_anchor_index( - transition=transition, - boundary_address=boundary_address, - action_attempts=action_attempts, - state_transitions=state_transitions, - observations=observations, - ) - if anchor_index is None or anchor_index > observation_index: - continue - if not _participant_behavior_transition_matches_relation(transition, relation=relation): - continue - relation.update(_participant_behavior_transition_delta(transition, deltas_by_order=deltas_by_order)) - effective_order = max(effective_order, order) - return relation, effective_order - - -def _participant_behavior_detail_shape_violations_for_events( - events: list[ParticipantBehaviorHistoryEvent], -) -> Iterator[tuple[str, str]]: - for index, event in enumerate(events): - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - yield from _participant_behavior_detail_shape_violations(event, locator=locator) - - -def _participant_behavior_observation_visibility_violations( - events: list[ParticipantBehaviorHistoryEvent], - *, - observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], -) -> Iterator[tuple[str, str]]: - action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) - for index, event in enumerate(events): - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - continue - boundary_address = event.observation_boundary_address or "" - boundary = observation_boundaries.get(boundary_address) - if boundary is None: - continue - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - detail_refs, violations = _participant_behavior_observation_detail_refs(event, locator=locator) - if violations: - yield from violations - continue - if not any(detail_refs.values()): - continue - relation, effective_order = _participant_behavior_observation_effective_relation( - observation_index=index, - boundary_address=boundary_address, - boundary=boundary, - action_attempts=action_attempts, - state_transitions=state_transitions, - observations=observations, - ) - yield from _participant_behavior_visibility_detail_violations( - locator=locator, - detail_refs=detail_refs, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - - -def _participant_behavior_action_result_ref_authorization_violations_for_events( - events: list[ParticipantBehaviorHistoryEvent], - *, - observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], -) -> Iterator[tuple[str, str]]: - action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) - for index, event in enumerate(events): - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - continue - if event.action_result is None and not event.attribution_edges and not event.outcome_interpretations: - continue - boundary_address = event.observation_boundary_address or "" - boundary = observation_boundaries.get(boundary_address) - if boundary is None: - continue - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - relation, effective_order = _participant_behavior_observation_effective_relation( - observation_index=index, - boundary_address=boundary_address, - boundary=boundary, - action_attempts=action_attempts, - state_transitions=state_transitions, - observations=observations, - ) - if event.action_result is not None: - yield from _participant_behavior_action_result_ref_authorization_violations( - event=event, - locator=locator, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - yield from _participant_behavior_attribution_ref_authorization_violations( - event=event, - locator=locator, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - yield from _participant_behavior_outcome_ref_authorization_violations( - event=event, - locator=locator, - boundary=boundary, - relation=relation, - effective_order=effective_order, - ) - - -def _participant_behavior_address_violations( - event: ParticipantBehaviorHistoryEvent, - *, - locator: str, - action_contract_addresses: set[str] | frozenset[str] | None, - observation_boundary_addresses: set[str] | frozenset[str] | None, -) -> list[tuple[str, str]]: - violations: list[tuple[str, str]] = [] - if action_contract_addresses is not None and event.action_contract_address not in action_contract_addresses: - violations.append( - ( - locator, - ( - "participant behavior event references unknown action_contract_address " - f"{event.action_contract_address!r}" - ), - ) - ) - if ( - observation_boundary_addresses is not None - and event.observation_boundary_address is not None - and event.observation_boundary_address not in observation_boundary_addresses - ): - violations.append( - ( - locator, - ( - "participant behavior event references unknown observation_boundary_address " - f"{event.observation_boundary_address!r}" - ), - ) - ) - return violations - - -def _contract_sem213_temporal_contracts( - contract: ParticipantActionContractRuntime, -) -> dict[str, Mapping[str, Any]]: - temporal_contracts = contract.spec.get("temporal_contracts", ()) - if isinstance(temporal_contracts, (str, bytes, Mapping)) or not isinstance(temporal_contracts, Iterable): - return {} - return { - str(temporal_contract.get("temporal_id")): temporal_contract - for temporal_contract in temporal_contracts - if isinstance(temporal_contract, Mapping) and temporal_contract.get("temporal_id") - } - - -def _contract_sem213_backend_disclosure_ids(contract: ParticipantActionContractRuntime) -> set[str]: - disclosures = contract.spec.get("backend_timing_disclosures", ()) - if isinstance(disclosures, (str, bytes, Mapping)) or not isinstance(disclosures, Iterable): - return set() - return { - str(disclosure.get("disclosure_id")) - for disclosure in disclosures - if isinstance(disclosure, Mapping) and disclosure.get("disclosure_id") - } - - -def _participant_temporal_context_contract_violations( - context: ParticipantTemporalRuntimeContext, - *, - contract: ParticipantActionContractRuntime, -) -> list[str]: - violations: list[str] = [] - temporal_contracts = _contract_sem213_temporal_contracts(contract) - temporal_contract = temporal_contracts.get(context.temporal_contract_id) - if temporal_contract is None: - return [f"temporal context references undeclared temporal_contract_id {context.temporal_contract_id!r}"] - - declared_time_domain = str(temporal_contract.get("time_domain", "")) - if context.time_domain.value != declared_time_domain: - violations.append( - f"temporal context {context.temporal_contract_id!r} time_domain {context.time_domain.value!r} " - f"does not match compiled contract {declared_time_domain!r}" - ) - - declared_clock_authority = str(temporal_contract.get("clock_authority", "")) - if context.clock_authority != declared_clock_authority: - violations.append( - f"temporal context {context.temporal_contract_id!r} clock_authority {context.clock_authority!r} " - f"does not match compiled contract {declared_clock_authority!r}" - ) - - declared_event_points = tuple(str(point) for point in temporal_contract.get("event_points", ())) - observed_event_points = tuple(point.value for point in context.event_points) - if observed_event_points != declared_event_points: - violations.append( - f"temporal context {context.temporal_contract_id!r} event_points {observed_event_points!r} " - f"do not match compiled contract {declared_event_points!r}" - ) - - declared_contract_disclosures = set(str(ref) for ref in temporal_contract.get("backend_disclosure_refs", ())) - declared_disclosures = _contract_sem213_backend_disclosure_ids(contract) - for ref in sorted(set(context.backend_disclosure_refs) - declared_contract_disclosures): - violations.append( - f"temporal context {context.temporal_contract_id!r} reports backend_disclosure_ref {ref!r} " - "not declared by the temporal contract" - ) - for ref in sorted(set(context.backend_disclosure_refs) - declared_disclosures): - violations.append( - f"temporal context {context.temporal_contract_id!r} reports unknown backend_disclosure_ref {ref!r}" - ) - - declared_reset_boundary = temporal_contract.get("reset_boundary") - if declared_reset_boundary is not None and context.reset_boundary != str(declared_reset_boundary): - violations.append( - f"temporal context {context.temporal_contract_id!r} reset_boundary {context.reset_boundary!r} " - f"does not match compiled contract {str(declared_reset_boundary)!r}" - ) - declared_replay_boundary = temporal_contract.get("replay_boundary") - if declared_replay_boundary is not None and context.replay_boundary != str(declared_replay_boundary): - violations.append( - f"temporal context {context.temporal_contract_id!r} replay_boundary {context.replay_boundary!r} " - f"does not match compiled contract {str(declared_replay_boundary)!r}" - ) - - return violations - - -def _participant_behavior_temporal_contract_violations( - events: Iterable[ParticipantBehaviorHistoryEvent], - *, - action_contracts: Mapping[str, ParticipantActionContractRuntime], -) -> Iterator[tuple[str, str]]: - for index, event in enumerate(events): - if not event.temporal_contexts: - continue - action_contract_address = event.action_contract_address or "" - contract = action_contracts.get(action_contract_address) - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - if contract is None: - yield (locator, f"temporal context cannot resolve action contract {action_contract_address!r}") - continue - for context in event.temporal_contexts: - for violation in _participant_temporal_context_contract_violations(context, contract=contract): - yield (locator, violation) - - -def _contract_sem215_source_bindings( - rule: ParticipantOutcomeInterpretationRuleRuntime, -) -> dict[tuple[str, str], dict[str, str | set[str]]]: - bindings = rule.spec.get("source_bindings", ()) - if isinstance(bindings, (str, bytes, Mapping)) or not isinstance(bindings, Iterable): - return {} - declarations: dict[tuple[str, str], dict[str, str | set[str]]] = {} - for index, binding in enumerate(bindings): - if not isinstance(binding, Mapping) or not binding.get("source_id") or not binding.get("source_layer"): - continue - declarations[(str(binding.get("source_id")), str(binding.get("source_layer")))] = { - "ref": rule.source_refs[index] if index < len(rule.source_refs) else str(binding.get("ref", "")), - "evidence_refs": _as_string_set(binding.get("evidence_refs", ())), - "provenance_refs": _as_string_set(binding.get("provenance_refs", ())), - } - return declarations - - -def _contract_sem215_target_bindings( - rule: ParticipantOutcomeInterpretationRuleRuntime, -) -> dict[tuple[str, str], dict[str, str | set[str] | None]]: - bindings = rule.spec.get("target_bindings", ()) - if isinstance(bindings, (str, bytes, Mapping)) or not isinstance(bindings, Iterable): - return {} - declarations: dict[tuple[str, str], dict[str, str | set[str] | None]] = {} - for index, binding in enumerate(bindings): - if not isinstance(binding, Mapping) or not binding.get("target_id") or not binding.get("target_layer"): - continue - governance_ref = binding.get("governance_ref") - declarations[(str(binding.get("target_id")), str(binding.get("target_layer")))] = { - "ref": rule.target_refs[index] if index < len(rule.target_refs) else str(binding.get("ref", "")), - "governance_ref": str(governance_ref) if governance_ref is not None else None, - "evidence_refs": _as_string_set(binding.get("evidence_refs", ())), - "limitations": _as_string_set(binding.get("limitations", ())), - } - return declarations - - -def _outcome_source_layer_requires_provenance(layer: str) -> bool: - try: - source_layer = OutcomeInterpretationSourceLayer(layer) - except ValueError: - return False - return source_layer in PROVENANCE_REQUIRED_OUTCOME_SOURCE_LAYERS - - -def validate_participant_outcome_interpretation_record( - record: ParticipantOutcomeInterpretationRecord, - rule: ParticipantOutcomeInterpretationRuleRuntime, -) -> list[str]: - """Return SEM-215 rule-conformance violations for a runtime interpretation.""" - - violations: list[str] = [] - declared_sources = _contract_sem215_source_bindings(rule) - declared_targets = _contract_sem215_target_bindings(rule) - reported_sources: set[tuple[str, str]] = set() - for source in record.source_bindings: - source_key = (source.source_id, source.source_layer.value) - reported_sources.add(source_key) - if source_key not in declared_sources: - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"is not declared by {rule.address}" - ) - continue - declared_refs = declared_sources[source_key] - if source.ref != declared_refs["ref"]: - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"ref {source.ref!r} does not match declared ref {declared_refs['ref']!r}" - ) - for ref in sorted(set(source.evidence_refs) - declared_refs["evidence_refs"]): - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"reports undeclared evidence_ref {ref!r}" - ) - for ref in sorted(set(source.provenance_refs) - declared_refs["provenance_refs"]): - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"reports undeclared provenance_ref {ref!r}" - ) - for ref in sorted(declared_refs["provenance_refs"] - set(source.provenance_refs)): - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " - f"omits declared provenance_ref {ref!r}" - ) - for source_id, source_layer in sorted(declared_sources): - if not _outcome_source_layer_requires_provenance(source_layer): - continue - if (source_id, source_layer) not in reported_sources: - violations.append( - f"outcome interpretation {record.interpretation_id!r} source {source_id!r} " - f"with provenance-required layer {source_layer!r} is not reported" - ) - for target in record.target_bindings: - target_key = (target.target_id, target.target_layer.value) - if target_key not in declared_targets: - violations.append( - f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " - f"is not declared by {rule.address}" - ) - continue - declared_refs = declared_targets[target_key] - if target.ref != declared_refs["ref"]: - violations.append( - f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " - f"ref {target.ref!r} does not match declared ref {declared_refs['ref']!r}" - ) - if target.governance_ref != declared_refs["governance_ref"]: - violations.append( - f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " - f"governance_ref {target.governance_ref!r} does not match declared governance_ref " - f"{declared_refs['governance_ref']!r}" - ) - for ref in sorted(set(target.evidence_refs) - declared_refs["evidence_refs"]): - violations.append( - f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " - f"reports undeclared evidence_ref {ref!r}" - ) - if declared_refs["limitations"] and not set(target.limitations) <= declared_refs["limitations"]: - violations.append( - f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " - "reports limitations outside the declared rule" - ) - declared_rule_evidence_refs = _as_string_set(rule.spec.get("evidence_refs", ())) - for ref in sorted(set(record.evidence_refs) - declared_rule_evidence_refs): - violations.append( - f"outcome interpretation {record.interpretation_id!r} reports undeclared evidence_ref {ref!r}" - ) - return violations - - -def _participant_behavior_outcome_interpretation_rule_violations( - events: Iterable[ParticipantBehaviorHistoryEvent], - *, - outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime], -) -> Iterator[tuple[str, str]]: - for index, event in enumerate(events): - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - continue - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - for record in event.outcome_interpretations: - rule = outcome_interpretation_rules.get(record.rule_address) - if rule is None: - yield ( - locator, - ( - f"outcome interpretation {record.interpretation_id!r} references unknown " - f"rule_address {record.rule_address!r}" - ), - ) - continue - for violation in validate_participant_outcome_interpretation_record(record, rule): - yield (locator, violation) - - -def _participant_behavior_action_result_contract_violations( - events: Iterable[ParticipantBehaviorHistoryEvent], - *, - action_contracts: Mapping[str, ParticipantActionContractRuntime], -) -> Iterator[tuple[str, str]]: - for index, event in enumerate(events): - if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: - continue - if event.observation_status not in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES: - continue - action_contract_address = event.action_contract_address or "" - contract = action_contracts.get(action_contract_address) - if contract is None or not _contract_uses_sem211_action_results(contract): - continue - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - if event.action_result is None: - yield ( - locator, - f"terminal observation must carry SEM-211 action_result for {action_contract_address}", - ) - continue - for violation in validate_participant_action_result_contract(event.action_result, contract): - yield (locator, violation) - - -def _normalize_participant_behavior_events( - participant_behavior_history: list[Any], - *, - action_contract_addresses: set[str] | frozenset[str] | None, - observation_boundary_addresses: set[str] | frozenset[str] | None, - expected_participant_address: str | None = None, -) -> tuple[list[ParticipantBehaviorHistoryEvent], list[tuple[str, str]]]: - normalized_events: list[ParticipantBehaviorHistoryEvent] = [] - violations: list[tuple[str, str]] = [] - for index, event in enumerate(participant_behavior_history): - locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" - if not isinstance(event, Mapping): - violations.append((locator, "participant behavior history event must be a mapping")) - continue - try: - normalized = ParticipantBehaviorHistoryEvent.from_payload(event) - except (TypeError, ValueError) as exc: - violations.append((locator, f"participant behavior history event is invalid: {exc}")) - continue - if expected_participant_address is not None and normalized.participant_address != expected_participant_address: - violations.append( - ( - locator, - ( - f"participant behavior history event outer key {expected_participant_address!r} " - f"does not match inner participant_address {normalized.participant_address!r}" - ), - ) - ) - continue - violations.extend( - _participant_behavior_address_violations( - normalized, - locator=locator, - action_contract_addresses=action_contract_addresses, - observation_boundary_addresses=observation_boundary_addresses, - ) - ) - normalized_events.append(normalized) - return normalized_events, violations - - -def _participant_behavior_events_by_action_instance( - events: list[ParticipantBehaviorHistoryEvent], -) -> dict[str, list[ParticipantBehaviorHistoryEvent]]: - events_by_action_instance: dict[str, list[ParticipantBehaviorHistoryEvent]] = {} - for event in events: - events_by_action_instance.setdefault(event.action_instance_id, []).append(event) - return events_by_action_instance - - -def _participant_behavior_action_instance_violation( - action_instance_id: str, - events: list[ParticipantBehaviorHistoryEvent], -) -> tuple[str, str] | None: - attempts = [event for event in events if event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED] - observations = [ - event - for event in events - if event.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED - and event.observation_status in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES - ] - transitions = [ - event for event in events if event.event_type == ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED - ] - - if len(attempts) > 1: - return (action_instance_id, "participant action instance may only have one action_attempted event") - if len(attempts) == 0: - return (action_instance_id, "participant behavior events require a matching action_attempted event") - if len(observations) != 1: - return ( - action_instance_id, - "participant action instance requires exactly one terminal observation or orphaned-action observation", - ) - observation = observations[0] - if observation.observation_status == ParticipantObservationStatus.ORPHANED_ACTION: - return None - if len(transitions) != 1: - return (action_instance_id, "participant action instance requires exactly one state transition") - if observation.post_state_digest != transitions[0].post_state_digest: - return ( - action_instance_id, - "terminal observation post_state_digest must match the state transition post_state_digest", - ) - return None - - -def _participant_behavior_action_instance_violations( - events: list[ParticipantBehaviorHistoryEvent], -) -> Iterator[tuple[str, str]]: - for action_instance_id, grouped_events in _participant_behavior_events_by_action_instance(events).items(): - violation = _participant_behavior_action_instance_violation(action_instance_id, grouped_events) - if violation is not None: - yield violation - - -def _participant_behavior_joint_action_order_violations( - events: Iterable[ParticipantBehaviorHistoryEvent], -) -> Iterator[tuple[str, str]]: - attempts_by_joint_set: dict[str, list[ParticipantBehaviorHistoryEvent]] = {} - for event in events: - if ( - event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED - and event.joint_action_set_id is not None - ): - attempts_by_joint_set.setdefault(event.joint_action_set_id, []).append(event) - - for joint_action_set_id, attempts in sorted(attempts_by_joint_set.items()): - attempts_by_order: dict[int, list[ParticipantBehaviorHistoryEvent]] = {} - for event in attempts: - if event.realized_order is None: - continue - attempts_by_order.setdefault(event.realized_order, []).append(event) - for realized_order, duplicate_attempts in sorted(attempts_by_order.items()): - if len(duplicate_attempts) <= 1: - continue - instances = ", ".join( - sorted(f"{event.participant_address}/{event.action_instance_id}" for event in duplicate_attempts) - ) - yield ( - f"joint-action-set.{joint_action_set_id}", - ( - f"joint action set realized_order {realized_order} is assigned to " - f"multiple action_attempted events: {instances}" - ), - ) - - -def iter_participant_behavior_history_violations( - participant_behavior_history: Any, - *, - action_contract_addresses: set[str] | frozenset[str] | None = None, - action_contracts: Mapping[str, ParticipantActionContractRuntime] | None = None, - outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime] | None = None, - observation_boundary_addresses: set[str] | frozenset[str] | None = None, - observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime] | None = None, - participant_episode_history: Any = None, - expected_participant_address: str | None = None, -) -> Iterator[tuple[str, str]]: - """Yield every SEM-208 behavior-history invariant violation. - - The helper checks that each action instance has one terminal observation - paired with the state transition digest it reports. When compiled address - sets are provided, it also rejects references outside those sets. When - compiled observation boundaries are provided, SEM-210 observation details - and SEM-211 action-result references are checked against the time-indexed - participant view relation. - """ - - if not isinstance(participant_behavior_history, list): - yield (_PARTICIPANT_BEHAVIOR_HISTORY_KEY, "participant behavior history must be a list of events") - return - if action_contracts is not None and action_contract_addresses is None: - action_contract_addresses = frozenset(action_contracts.keys()) - if observation_boundaries is not None and observation_boundary_addresses is None: - observation_boundary_addresses = frozenset(observation_boundaries.keys()) - - normalized_events, entry_violations = _normalize_participant_behavior_events( - participant_behavior_history, - action_contract_addresses=action_contract_addresses, - observation_boundary_addresses=observation_boundary_addresses, - expected_participant_address=expected_participant_address, - ) - if entry_violations: - yield from entry_violations - return - - yield from _participant_behavior_detail_shape_violations_for_events(normalized_events) - yield from _participant_behavior_action_instance_violations(normalized_events) - yield from _participant_behavior_joint_action_order_violations(normalized_events) - yield from _participant_behavior_outcome_event_grounding_violations( - normalized_events, - participant_episode_history=participant_episode_history, - ) - if action_contracts is not None: - yield from _participant_behavior_action_result_contract_violations( - normalized_events, - action_contracts=action_contracts, - ) - yield from _participant_behavior_temporal_contract_violations( - normalized_events, - action_contracts=action_contracts, - ) - if outcome_interpretation_rules is not None: - yield from _participant_behavior_outcome_interpretation_rule_violations( - normalized_events, - outcome_interpretation_rules=outcome_interpretation_rules, - ) - if observation_boundaries is not None: - yield from _participant_behavior_transition_anchor_violations( - normalized_events, - observation_boundaries=observation_boundaries, - participant_episode_history=participant_episode_history, - ) - yield from _participant_behavior_observation_visibility_violations( - normalized_events, - observation_boundaries=observation_boundaries, - ) - yield from _participant_behavior_action_result_ref_authorization_violations_for_events( - normalized_events, - observation_boundaries=observation_boundaries, - ) - - -def iter_participant_behavior_joint_action_violations( - participant_behavior_history_by_participant: Any, -) -> Iterator[tuple[str, str]]: - """Yield SEM-209 joint-action ordering violations across participant histories.""" - - if not isinstance(participant_behavior_history_by_participant, Mapping): - yield (_PARTICIPANT_BEHAVIOR_HISTORY_KEY, "participant behavior histories must be a mapping") - return - - normalized_events: list[ParticipantBehaviorHistoryEvent] = [] - for history in participant_behavior_history_by_participant.values(): - if not isinstance(history, list): - continue - participant_events, entry_violations = _normalize_participant_behavior_events( - history, - action_contract_addresses=None, - observation_boundary_addresses=None, - ) - if entry_violations: - continue - normalized_events.extend(participant_events) - - yield from _participant_behavior_joint_action_order_violations(normalized_events) - - -@dataclass(frozen=True) -class ObjectiveRuntime(ResolvedResource): - """Resolved objective node.""" - - actor_type: str = "" - actor_name: str = "" - success_addresses: tuple[str, ...] = () - objective_dependencies: tuple[str, ...] = () - window_story_addresses: tuple[str, ...] = () - window_script_addresses: tuple[str, ...] = () - window_event_addresses: tuple[str, ...] = () - window_workflow_addresses: tuple[str, ...] = () - window_step_refs: tuple[str, ...] = () - window_step_workflow_addresses: tuple[str, ...] = () - window_references: tuple[ObjectiveWindowReferenceRuntime, ...] = () - result_contract: "EvaluationResultContract" = field( - default_factory=lambda: EvaluationResultContract(resource_type="objective") - ) - execution_contract: "EvaluationExecutionContract" = field( - default_factory=lambda: EvaluationExecutionContract(resource_type="objective") - ) - - -@dataclass(frozen=True) -class CompiledCapabilityConstraint: - """One finite SDL capability domain lowered onto a compiled resource.""" - - address: str - concern: str - parameter: tuple[str, ...] - allowed_values: tuple[str | int | float | bool, ...] - - def __post_init__(self) -> None: - require_compiled_address(self.address, field_name="capability constraint address") - if self.concern not in {"nodes.os", "infrastructure.count"}: - raise ValueError("compiled capability constraint has an unsupported concern") - if not self.parameter or any(not segment for segment in self.parameter): - raise ValueError("compiled capability constraint requires a parameter identity") - if not self.allowed_values: - raise ValueError("compiled capability constraint requires a non-empty domain") - - -@dataclass(frozen=True) -class RuntimeModel: - """Compiled SDL runtime model. - - Reusable definitions stay as templates or metadata. Only bound runtime - instances become planned resources. - """ - - scenario_name: str - feature_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) - condition_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) - inject_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) - vulnerability_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) - entity_specs: dict[str, dict[str, Any]] = field(default_factory=dict) - agent_specs: dict[str, dict[str, Any]] = field(default_factory=dict) - relationship_specs: dict[str, dict[str, Any]] = field(default_factory=dict) - # Typed compiler metadata for finite pre-instantiation domains. It is - # consumed by planner capability checks and never enters backend resource - # payloads. - capability_constraints: tuple[CompiledCapabilityConstraint, ...] = () - networks: dict[str, NetworkRuntime] = field(default_factory=dict) - node_deployments: dict[str, NodeRuntime] = field(default_factory=dict) - feature_bindings: dict[str, FeatureBinding] = field(default_factory=dict) - propositions: dict[str, PropositionRuntime] = field(default_factory=dict) - assertions: dict[str, AssertionRuntime] = field(default_factory=dict) - condition_bindings: dict[str, ConditionBinding] = field(default_factory=dict) - injects: dict[str, InjectRuntime] = field(default_factory=dict) - inject_bindings: dict[str, InjectBinding] = field(default_factory=dict) - content_placements: dict[str, ContentPlacement] = field(default_factory=dict) - account_placements: dict[str, AccountPlacement] = field(default_factory=dict) - action_contracts: dict[str, ParticipantActionContractRuntime] = field(default_factory=dict) - observation_boundaries: dict[str, ParticipantObservationBoundaryRuntime] = field(default_factory=dict) - outcome_interpretation_rules: dict[str, ParticipantOutcomeInterpretationRuleRuntime] = field(default_factory=dict) - participant_behaviors: dict[str, ParticipantBehaviorRuntime] = field(default_factory=dict) - behavior_specifications: dict[str, ParticipantBehaviorSpecificationRuntime] = field(default_factory=dict) - events: dict[str, EventRuntime] = field(default_factory=dict) - scripts: dict[str, ScriptRuntime] = field(default_factory=dict) - stories: dict[str, StoryRuntime] = field(default_factory=dict) - workflows: dict[str, WorkflowRuntime] = field(default_factory=dict) - objectives: dict[str, ObjectiveRuntime] = field(default_factory=dict) - diagnostics: list[Diagnostic] = field(default_factory=list) - # SEM-218 typed compiler emission: each authored realization concern with - # its preserved explicitness class. Model-side metadata; it never enters the backend-facing - # `resource_payload()` envelope. Consumed by the planner realization gate. - realization_requirements: tuple[CompiledRealizationRequirement, ...] = () - realization_instance: InstantiatedScenario | None = None - - def __post_init__(self) -> None: - owners: dict[str, str] = {} - address_map_fields = ( - "networks", - "node_deployments", - "feature_bindings", - "propositions", - "assertions", - "condition_bindings", - "injects", - "inject_bindings", - "content_placements", - "account_placements", - "action_contracts", - "observation_boundaries", - "outcome_interpretation_rules", - "participant_behaviors", - "behavior_specifications", - "events", - "scripts", - "stories", - "workflows", - "objectives", - ) - for field_name in address_map_fields: - value = getattr(self, field_name) - for map_key, item in value.items(): - address = getattr(item, "address", None) - if not isinstance(address, str): - raise TypeError(f"RuntimeModel {field_name} entries must carry an address") - require_compiled_address(address) - require_compiled_address(map_key, field_name="runtime model map key") - if map_key != address: - raise ValueError(f"RuntimeModel {field_name} map key must equal embedded address") - previous_owner = owners.get(address) - if previous_owner is not None and previous_owner != field_name: - raise ValueError( - f"RuntimeModel duplicate compiled address across {previous_owner} and {field_name}" - ) - owners[address] = field_name - capability_keys = [(constraint.address, constraint.concern) for constraint in self.capability_constraints] - if len(capability_keys) != len(set(capability_keys)): - raise ValueError("RuntimeModel capability constraints must address unique fields") - - -@dataclass(frozen=True) -class ExecutionPlan: - """Composite runtime execution plan.""" - - target_name: str | None - manifest: BackendManifest - base_snapshot: "RuntimeSnapshot" - scenario_name: str - model: RuntimeModel - provisioning: ProvisioningPlan - orchestration: OrchestrationPlan - evaluation: EvaluationPlan - diagnostics: list[Diagnostic] = field(default_factory=list) - - @property - def is_valid(self) -> bool: - return not any(diag.is_error for diag in self.diagnostics) - - -def resource_payload(resource: ResolvedResource) -> dict[str, Any]: - """Convert a compiled resource to a stable planner payload.""" - - payload = asdict(resource) - payload.pop("address", None) - payload.pop("ordering_dependencies", None) - payload.pop("refresh_dependencies", None) - return payload diff --git a/implementations/python/packages/aces_processor/models/__init__.py b/implementations/python/packages/aces_processor/models/__init__.py new file mode 100644 index 000000000..414dbd75d --- /dev/null +++ b/implementations/python/packages/aces_processor/models/__init__.py @@ -0,0 +1,280 @@ +"""Runtime data models for the SDL-native execution layer. + +The runtime is split into three domains: + +- provisioning: desired deployed state +- orchestration: resolved exercise control graph +- evaluation: resolved monitoring/scoring graph + +The compiler produces a ``RuntimeModel`` with reusable templates separated +from bound runtime instances. The planner reconciles those instances against +the current ``RuntimeSnapshot`` and emits a composite ``ExecutionPlan``. +""" + +from aces_contracts.diagnostics import Diagnostic as Diagnostic +from aces_contracts.diagnostics import Severity as Severity +from aces_contracts.evaluation import EvaluationExecutionContract as EvaluationExecutionContract +from aces_contracts.evaluation import EvaluationExecutionState as EvaluationExecutionState +from aces_contracts.evaluation import EvaluationHistoryEvent as EvaluationHistoryEvent +from aces_contracts.evaluation import EvaluationHistoryEventType as EvaluationHistoryEventType +from aces_contracts.evaluation import EvaluationResultContract as EvaluationResultContract +from aces_contracts.evaluation import EvaluationResultStatus as EvaluationResultStatus +from aces_contracts.evaluation import validate_evaluation_result as validate_evaluation_result +from aces_contracts.participant_behavior import ( + ParticipantActionPreconditionStatus as ParticipantActionPreconditionStatus, +) +from aces_contracts.participant_behavior import ParticipantActionResultStatus as ParticipantActionResultStatus +from aces_contracts.participant_behavior import ParticipantAdmissionDisposition as ParticipantAdmissionDisposition +from aces_contracts.participant_behavior import ( + ParticipantBehaviorHistoryEventType as ParticipantBehaviorHistoryEventType, +) +from aces_contracts.participant_behavior import ParticipantLifecycleOperationState as ParticipantLifecycleOperationState +from aces_contracts.participant_behavior import ParticipantObservationStatus as ParticipantObservationStatus +from aces_contracts.participant_behavior import ParticipantPhaseRealization as ParticipantPhaseRealization +from aces_contracts.participant_behavior import ParticipantRuntimeLifecyclePhase as ParticipantRuntimeLifecyclePhase +from aces_contracts.participant_behavior import ( + participant_lifecycle_field_violation_messages as participant_lifecycle_field_violation_messages, +) +from aces_contracts.participant_episode import ParticipantEpisodeControlAction as ParticipantEpisodeControlAction +from aces_contracts.participant_episode import ParticipantEpisodeExecutionState as ParticipantEpisodeExecutionState +from aces_contracts.participant_episode import ParticipantEpisodeHistoryEvent as ParticipantEpisodeHistoryEvent +from aces_contracts.participant_episode import ParticipantEpisodeHistoryEventType as ParticipantEpisodeHistoryEventType +from aces_contracts.participant_episode import ( + ParticipantEpisodeInitializeRequest as ParticipantEpisodeInitializeRequest, +) +from aces_contracts.participant_episode import ParticipantEpisodeResetRequest as ParticipantEpisodeResetRequest +from aces_contracts.participant_episode import ParticipantEpisodeRestartRequest as ParticipantEpisodeRestartRequest +from aces_contracts.participant_episode import ParticipantEpisodeStatus as ParticipantEpisodeStatus +from aces_contracts.participant_episode import ParticipantEpisodeTerminalReason as ParticipantEpisodeTerminalReason +from aces_contracts.participant_episode import ParticipantEpisodeTerminateRequest as ParticipantEpisodeTerminateRequest +from aces_contracts.participant_episode import ( + iter_participant_episode_snapshot_violations as iter_participant_episode_snapshot_violations, +) +from aces_contracts.planning import ChangeAction as ChangeAction +from aces_contracts.planning import EvaluationOp as EvaluationOp +from aces_contracts.planning import EvaluationPlan as EvaluationPlan +from aces_contracts.planning import OrchestrationOp as OrchestrationOp +from aces_contracts.planning import OrchestrationPlan as OrchestrationPlan +from aces_contracts.planning import PlannedResource as PlannedResource +from aces_contracts.planning import PlanOperation as PlanOperation +from aces_contracts.planning import ProvisioningPlan as ProvisioningPlan +from aces_contracts.planning import ProvisionOp as ProvisionOp +from aces_contracts.planning import RuntimeDomain as RuntimeDomain +from aces_contracts.runtime_state import ApplyResult as ApplyResult +from aces_contracts.runtime_state import OperationReceipt as OperationReceipt +from aces_contracts.runtime_state import OperationState as OperationState +from aces_contracts.runtime_state import OperationStatus as OperationStatus +from aces_contracts.runtime_state import RealizationProvenanceEntry as RealizationProvenanceEntry +from aces_contracts.runtime_state import RuntimeSnapshot as RuntimeSnapshot +from aces_contracts.runtime_state import RuntimeSnapshotEnvelope as RuntimeSnapshotEnvelope +from aces_contracts.runtime_state import SnapshotEntry as SnapshotEntry +from aces_contracts.versions import EVALUATION_STATE_SCHEMA_VERSION as EVALUATION_STATE_SCHEMA_VERSION +from aces_contracts.versions import OPERATION_SCHEMA_VERSION as OPERATION_SCHEMA_VERSION +from aces_contracts.versions import PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION as PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION +from aces_contracts.versions import RUNTIME_SNAPSHOT_SCHEMA_VERSION as RUNTIME_SNAPSHOT_SCHEMA_VERSION +from aces_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION as WORKFLOW_STATE_SCHEMA_VERSION +from aces_contracts.workflow import WorkflowCancellationRequest as WorkflowCancellationRequest +from aces_contracts.workflow import WorkflowCompensationStatus as WorkflowCompensationStatus +from aces_contracts.workflow import WorkflowExecutionContract as WorkflowExecutionContract +from aces_contracts.workflow import WorkflowExecutionState as WorkflowExecutionState +from aces_contracts.workflow import WorkflowHistoryEvent as WorkflowHistoryEvent +from aces_contracts.workflow import WorkflowHistoryEventType as WorkflowHistoryEventType +from aces_contracts.workflow import WorkflowResultContract as WorkflowResultContract +from aces_contracts.workflow import WorkflowStatus as WorkflowStatus +from aces_contracts.workflow import WorkflowStepExecutionState as WorkflowStepExecutionState +from aces_contracts.workflow import WorkflowStepLifecycle as WorkflowStepLifecycle +from aces_contracts.workflow import WorkflowStepOutcome as WorkflowStepOutcome +from aces_contracts.workflow import validate_workflow_step_result_contract as validate_workflow_step_result_contract +from aces_sdl.participant_temporal_semantics import ParticipantTemporalState as ParticipantTemporalState + +from aces_processor.semantics.realization import CompiledRealizationRequirement as CompiledRealizationRequirement + +from .action_results import ( + ParticipantActionEffectResult, + ParticipantActionPreconditionResult, + ParticipantActionResult, +) +from .attribution import ( + ParticipantAttributionCandidate, + ParticipantAttributionEdge, + ParticipantAttributionEvidenceBasis, + ParticipantAttributionOrderingBasis, +) +from .behavior_history_violations import ( + ParticipantHistoryAddressScope, + iter_participant_behavior_history_violations, + iter_participant_behavior_joint_action_violations, +) +from .behavior_resources import ( + EventRuntime, + ObjectiveWindowReferenceRuntime, + ParticipantBehaviorRuntime, + ParticipantBehaviorSpecificationRuntime, + ParticipantObservationBoundaryRuntime, + ParticipantOutcomeInterpretationRuleRuntime, + ScriptRuntime, + StoryRuntime, + WorkflowPredicateRuntime, + WorkflowRuntime, + WorkflowStepRuntime, + WorkflowStepStatePredicateRuntime, + WorkflowSwitchCaseRuntime, +) +from .history_event import ( + ParticipantBehaviorHistoryEvent, +) +from .outcome import ( + ParticipantOutcomeInterpretationRecord, + ParticipantOutcomeSourceRecord, + ParticipantOutcomeTargetRecord, +) +from .outcome_interpretation_validation import validate_participant_outcome_interpretation_record +from .resources import ( + AccountPlacement, + AssertionRuntime, + ConditionBinding, + ContentPlacement, + FeatureBinding, + InjectBinding, + InjectRuntime, + NetworkRuntime, + NodeRuntime, + ParticipantActionContractRuntime, + PropositionRuntime, + ResolvedResource, + RuntimeTemplate, + map_backend_diagnostic_to_participant_failure, + validate_participant_action_result_contract, +) +from .runtime_model import ( + CompiledCapabilityConstraint, + ExecutionPlan, + ObjectiveRuntime, + RuntimeModel, + resource_payload, +) +from .temporal import ( + ParticipantTemporalRuntimeContext, + ParticipantTemporalStateTransition, + iter_participant_temporal_state_machine_violations, +) + +__all__ = [ + "AccountPlacement", + "ApplyResult", + "AssertionRuntime", + "ChangeAction", + "CompiledCapabilityConstraint", + "CompiledRealizationRequirement", + "ConditionBinding", + "ContentPlacement", + "Diagnostic", + "EVALUATION_STATE_SCHEMA_VERSION", + "EvaluationExecutionContract", + "EvaluationExecutionState", + "EvaluationHistoryEvent", + "EvaluationHistoryEventType", + "EvaluationOp", + "EvaluationPlan", + "EvaluationResultContract", + "EvaluationResultStatus", + "EventRuntime", + "ExecutionPlan", + "FeatureBinding", + "InjectBinding", + "InjectRuntime", + "NetworkRuntime", + "NodeRuntime", + "OPERATION_SCHEMA_VERSION", + "ObjectiveRuntime", + "ObjectiveWindowReferenceRuntime", + "OperationReceipt", + "OperationState", + "OperationStatus", + "OrchestrationOp", + "OrchestrationPlan", + "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION", + "ParticipantActionContractRuntime", + "ParticipantActionEffectResult", + "ParticipantActionPreconditionResult", + "ParticipantActionPreconditionStatus", + "ParticipantActionResult", + "ParticipantActionResultStatus", + "ParticipantAdmissionDisposition", + "ParticipantAttributionCandidate", + "ParticipantAttributionEdge", + "ParticipantAttributionEvidenceBasis", + "ParticipantAttributionOrderingBasis", + "ParticipantBehaviorHistoryEvent", + "ParticipantBehaviorHistoryEventType", + "ParticipantBehaviorRuntime", + "ParticipantBehaviorSpecificationRuntime", + "ParticipantEpisodeControlAction", + "ParticipantEpisodeExecutionState", + "ParticipantEpisodeHistoryEvent", + "ParticipantEpisodeHistoryEventType", + "ParticipantEpisodeInitializeRequest", + "ParticipantEpisodeResetRequest", + "ParticipantEpisodeRestartRequest", + "ParticipantEpisodeStatus", + "ParticipantEpisodeTerminalReason", + "ParticipantEpisodeTerminateRequest", + "ParticipantHistoryAddressScope", + "ParticipantLifecycleOperationState", + "ParticipantObservationBoundaryRuntime", + "ParticipantObservationStatus", + "ParticipantOutcomeInterpretationRecord", + "ParticipantOutcomeInterpretationRuleRuntime", + "ParticipantOutcomeSourceRecord", + "ParticipantOutcomeTargetRecord", + "ParticipantPhaseRealization", + "ParticipantRuntimeLifecyclePhase", + "ParticipantTemporalRuntimeContext", + "ParticipantTemporalState", + "ParticipantTemporalStateTransition", + "PlanOperation", + "PlannedResource", + "PropositionRuntime", + "ProvisionOp", + "ProvisioningPlan", + "RUNTIME_SNAPSHOT_SCHEMA_VERSION", + "RealizationProvenanceEntry", + "ResolvedResource", + "RuntimeDomain", + "RuntimeModel", + "RuntimeSnapshot", + "RuntimeSnapshotEnvelope", + "RuntimeTemplate", + "ScriptRuntime", + "Severity", + "SnapshotEntry", + "StoryRuntime", + "WORKFLOW_STATE_SCHEMA_VERSION", + "WorkflowCancellationRequest", + "WorkflowCompensationStatus", + "WorkflowExecutionContract", + "WorkflowExecutionState", + "WorkflowHistoryEvent", + "WorkflowHistoryEventType", + "WorkflowPredicateRuntime", + "WorkflowResultContract", + "WorkflowRuntime", + "WorkflowStatus", + "WorkflowStepExecutionState", + "WorkflowStepLifecycle", + "WorkflowStepOutcome", + "WorkflowStepRuntime", + "WorkflowStepStatePredicateRuntime", + "WorkflowSwitchCaseRuntime", + "iter_participant_behavior_history_violations", + "iter_participant_behavior_joint_action_violations", + "iter_participant_episode_snapshot_violations", + "iter_participant_temporal_state_machine_violations", + "map_backend_diagnostic_to_participant_failure", + "participant_lifecycle_field_violation_messages", + "resource_payload", + "validate_evaluation_result", + "validate_participant_action_result_contract", + "validate_participant_outcome_interpretation_record", + "validate_workflow_step_result_contract", +] diff --git a/implementations/python/packages/aces_processor/models/action_results.py b/implementations/python/packages/aces_processor/models/action_results.py new file mode 100644 index 000000000..b40865ac7 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/action_results.py @@ -0,0 +1,408 @@ +"""Resolved participant action precondition/effect/result records.""" + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from typing import Any + +from aces_contracts.participant_behavior import ParticipantActionPreconditionStatus, ParticipantActionResultStatus +from aces_sdl.participant_behavior import ParticipantEffectClass, ParticipantFailureClass, ParticipantPreconditionClass + +from .behavior_resources import ( + _observation_point_matches_action_instance, + _tuple_of_non_empty_strings, + _validate_required_address, + _validate_required_string, +) +from .resources import _PARTICIPANT_ACTION_CONTRACT_PREFIX + + +@dataclass(frozen=True) +class ParticipantActionPreconditionResult: + """Resolved applicability state for one typed SEM-211 precondition.""" + + precondition_id: str + precondition_class: ParticipantPreconditionClass + status: ParticipantActionPreconditionStatus + participant_address: str + episode_id: str + action_contract_address: str + observation_point: str + support_refs: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionPreconditionResult": + if not isinstance(payload, Mapping): + raise TypeError("participant action precondition result must be a mapping") + missing = [ + key + for key in ( + "precondition_id", + "precondition_class", + "status", + "participant_address", + "episode_id", + "action_contract_address", + "observation_point", + ) + if key not in payload + ] + if missing: + raise ValueError("participant action precondition result is missing required fields: " + ", ".join(missing)) + precondition_class_raw = payload.get("precondition_class") + status_raw = payload.get("status") + return cls( + precondition_id=str(payload.get("precondition_id")), + precondition_class=( + precondition_class_raw + if isinstance(precondition_class_raw, ParticipantPreconditionClass) + else ParticipantPreconditionClass(str(precondition_class_raw)) + ), + status=( + status_raw + if isinstance(status_raw, ParticipantActionPreconditionStatus) + else ParticipantActionPreconditionStatus(str(status_raw)) + ), + participant_address=str(payload.get("participant_address")), + episode_id=str(payload.get("episode_id")), + action_contract_address=str(payload.get("action_contract_address")), + observation_point=str(payload.get("observation_point")), + support_refs=_tuple_of_non_empty_strings(payload.get("support_refs", ()), field_name="support_refs"), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "precondition_id": self.precondition_id, + "precondition_class": self.precondition_class.value, + "status": self.status.value, + "participant_address": self.participant_address, + "episode_id": self.episode_id, + "action_contract_address": self.action_contract_address, + "observation_point": self.observation_point, + "support_refs": list(self.support_refs), + "evidence_refs": list(self.evidence_refs), + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + _validate_required_string( + self.precondition_id, + "precondition_id must be a non-empty string", + ) + if not isinstance(self.precondition_class, ParticipantPreconditionClass): + raise TypeError("precondition_class must be a ParticipantPreconditionClass") + if not isinstance(self.status, ParticipantActionPreconditionStatus): + raise TypeError("status must be a ParticipantActionPreconditionStatus") + _validate_required_string( + self.participant_address, + "participant action precondition participant_address must be a non-empty string", + ) + _validate_required_string( + self.episode_id, + "participant action precondition episode_id must be a non-empty string", + ) + _validate_required_address( + self.action_contract_address, + prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, + message="action_contract_address must be a compiled participant action contract address", + ) + _validate_required_string( + self.observation_point, + "observation_point must be a non-empty string", + ) + _tuple_of_non_empty_strings(self.support_refs, field_name="support_refs") + _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + + +@dataclass(frozen=True) +class ParticipantActionEffectResult: + """Realized effect entry for a SEM-211 participant action result.""" + + effect_id: str + effect_class: ParticipantEffectClass + description: str + target_refs: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionEffectResult": + if not isinstance(payload, Mapping): + raise TypeError("participant action effect result must be a mapping") + missing = [key for key in ("effect_id", "effect_class", "description") if key not in payload] + if missing: + raise ValueError("participant action effect result is missing required fields: " + ", ".join(missing)) + effect_class_raw = payload.get("effect_class") + return cls( + effect_id=str(payload.get("effect_id")), + effect_class=( + effect_class_raw + if isinstance(effect_class_raw, ParticipantEffectClass) + else ParticipantEffectClass(str(effect_class_raw)) + ), + description=str(payload.get("description")), + target_refs=_tuple_of_non_empty_strings(payload.get("target_refs", ()), field_name="target_refs"), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "effect_id": self.effect_id, + "effect_class": self.effect_class.value, + "description": self.description, + "target_refs": list(self.target_refs), + "evidence_refs": list(self.evidence_refs), + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + _validate_required_string(self.effect_id, "effect_id must be a non-empty string") + if not isinstance(self.effect_class, ParticipantEffectClass): + raise TypeError("effect_class must be a ParticipantEffectClass") + _validate_required_string( + self.description, + "participant action effect description must be a non-empty string", + ) + _tuple_of_non_empty_strings(self.target_refs, field_name="target_refs") + _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + if self.effect_class not in {ParticipantEffectClass.NO_EFFECT, ParticipantEffectClass.UNKNOWN_EFFECT}: + if not self.target_refs and not self.evidence_refs: + raise ValueError(f"{self.effect_class.value} effects require target_refs or evidence_refs") + + +_PARTICIPANT_ACTION_FAILURE_STATUSES = frozenset( + { + ParticipantActionResultStatus.REJECTED, + ParticipantActionResultStatus.WITHHELD, + ParticipantActionResultStatus.FAILED, + ParticipantActionResultStatus.PARTIAL_SUCCESS, + ParticipantActionResultStatus.UNKNOWN, + } +) +_PARTICIPANT_ACTION_SUCCESS_STATUSES = frozenset( + { + ParticipantActionResultStatus.ACCEPTED, + ParticipantActionResultStatus.SUCCEEDED, + ParticipantActionResultStatus.PARTIAL_SUCCESS, + } +) +_PARTICIPANT_ACTION_TERMINAL_EFFECT_STATUSES = frozenset( + { + ParticipantActionResultStatus.SUCCEEDED, + ParticipantActionResultStatus.PARTIAL_SUCCESS, + } +) + + +def _validate_action_result_required_fields(payload: Mapping[str, Any]) -> None: + if not isinstance(payload, Mapping): + raise TypeError("participant action result must be a mapping") + missing = [ + key + for key in ( + "status", + "participant_address", + "episode_id", + "action_instance_id", + "action_contract_address", + "observation_point", + ) + if key not in payload + ] + if missing: + raise ValueError("participant action result is missing required fields: " + ", ".join(missing)) + + +def _ensure_iterable_of_item_payloads(value: object, message: str) -> None: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError(message) + + +def _coerce_action_result_status(status_raw: object) -> ParticipantActionResultStatus: + if isinstance(status_raw, ParticipantActionResultStatus): + return status_raw + return ParticipantActionResultStatus(str(status_raw)) + + +def _coerce_action_result_failure_class(failure_raw: object) -> ParticipantFailureClass | None: + if failure_raw is None: + return None + if isinstance(failure_raw, ParticipantFailureClass): + return failure_raw + return ParticipantFailureClass(str(failure_raw)) + + +@dataclass(frozen=True) +class ParticipantActionResult: + """Typed SEM-211 local result for a participant action attempt.""" + + status: ParticipantActionResultStatus + participant_address: str + episode_id: str + action_instance_id: str + action_contract_address: str + observation_point: str + preconditions: tuple[ParticipantActionPreconditionResult, ...] = () + effects: tuple[ParticipantActionEffectResult, ...] = () + failure_class: ParticipantFailureClass | None = None + observations: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantActionResult": + _validate_action_result_required_fields(payload) + status_raw = payload.get("status") + failure_raw = payload.get("failure_class") + preconditions_raw = payload.get("preconditions", ()) + effects_raw = payload.get("effects", ()) + _ensure_iterable_of_item_payloads( + preconditions_raw, "preconditions must be a list of participant action precondition results" + ) + _ensure_iterable_of_item_payloads(effects_raw, "effects must be a list of participant action effect results") + return cls( + status=_coerce_action_result_status(status_raw), + participant_address=str(payload.get("participant_address")), + episode_id=str(payload.get("episode_id")), + action_instance_id=str(payload.get("action_instance_id")), + action_contract_address=str(payload.get("action_contract_address")), + observation_point=str(payload.get("observation_point")), + preconditions=tuple(ParticipantActionPreconditionResult.from_payload(item) for item in preconditions_raw), + effects=tuple(ParticipantActionEffectResult.from_payload(item) for item in effects_raw), + failure_class=_coerce_action_result_failure_class(failure_raw), + observations=_tuple_of_non_empty_strings(payload.get("observations", ()), field_name="observations"), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "status": self.status.value, + "participant_address": self.participant_address, + "episode_id": self.episode_id, + "action_instance_id": self.action_instance_id, + "action_contract_address": self.action_contract_address, + "observation_point": self.observation_point, + "preconditions": [item.to_payload() for item in self.preconditions], + "effects": [item.to_payload() for item in self.effects], + "failure_class": self.failure_class.value if self.failure_class is not None else None, + "observations": list(self.observations), + "evidence_refs": list(self.evidence_refs), + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + self._validate_identity_fields() + self._validate_preconditions_collection() + self._validate_effects_collection() + self._validate_failure_class_and_strings() + self._validate_scope() + self._validate_fail_closed() + + def _validate_identity_fields(self) -> None: + if not isinstance(self.status, ParticipantActionResultStatus): + raise TypeError("status must be a ParticipantActionResultStatus") + _validate_required_string( + self.participant_address, + "participant action result participant_address must be a non-empty string", + ) + _validate_required_string( + self.episode_id, + "participant action result episode_id must be a non-empty string", + ) + _validate_required_string( + self.action_instance_id, + "participant action result action_instance_id must be a non-empty string", + ) + _validate_required_address( + self.action_contract_address, + prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, + message="action_contract_address must be a compiled participant action contract address", + ) + _validate_required_string( + self.observation_point, + "observation_point must be a non-empty string", + ) + if not _observation_point_matches_action_instance(self.observation_point, self.action_instance_id): + raise ValueError("action result observation_point must be anchored to action_instance_id") + + def _validate_preconditions_collection(self) -> None: + if not isinstance(self.preconditions, tuple): + raise TypeError("preconditions must be a tuple") + if not self.preconditions: + raise ValueError("participant action results require precondition results") + if any(not isinstance(item, ParticipantActionPreconditionResult) for item in self.preconditions): + raise TypeError("preconditions must contain ParticipantActionPreconditionResult values") + if len({item.precondition_id for item in self.preconditions}) != len(self.preconditions): + raise ValueError("precondition result ids must be unique") + + def _validate_effects_collection(self) -> None: + if not isinstance(self.effects, tuple): + raise TypeError("effects must be a tuple") + if any(not isinstance(item, ParticipantActionEffectResult) for item in self.effects): + raise TypeError("effects must contain ParticipantActionEffectResult values") + if len({item.effect_id for item in self.effects}) != len(self.effects): + raise ValueError("effect result ids must be unique") + + def _validate_failure_class_and_strings(self) -> None: + if self.failure_class is not None and not isinstance(self.failure_class, ParticipantFailureClass): + raise TypeError("failure_class must be a ParticipantFailureClass or None") + _tuple_of_non_empty_strings(self.observations, field_name="observations") + _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + + def _validate_scope(self) -> None: + for precondition in self.preconditions: + if precondition.participant_address != self.participant_address: + raise ValueError("precondition participant_address must match action result participant_address") + if precondition.episode_id != self.episode_id: + raise ValueError("precondition episode_id must match action result episode_id") + if precondition.action_contract_address != self.action_contract_address: + raise ValueError( + "precondition action_contract_address must match action result action_contract_address" + ) + if not _observation_point_matches_action_instance(precondition.observation_point, self.action_instance_id): + raise ValueError("precondition observation_point must be anchored to action result action_instance_id") + + def _validate_fail_closed(self) -> None: + blocked = [ + item + for item in self.preconditions + if item.status + in { + ParticipantActionPreconditionStatus.UNSATISFIED, + ParticipantActionPreconditionStatus.UNRESOLVED, + } + ] + self._validate_blocked_preconditions(blocked) + self._validate_no_failure_class_for_success() + self._validate_terminal_effects_present() + self._validate_failure_status_requires_failure_class() + + def _validate_blocked_preconditions(self, blocked: list[ParticipantActionPreconditionResult]) -> None: + if blocked and self.status in _PARTICIPANT_ACTION_SUCCESS_STATUSES: + raise ValueError("unsatisfied or unresolved preconditions fail closed") + if blocked and self.failure_class is None: + raise ValueError("unsatisfied or unresolved preconditions require a portable failure_class") + + def _validate_no_failure_class_for_success(self) -> None: + if self.status == ParticipantActionResultStatus.SUCCEEDED: + if self.failure_class is not None: + raise ValueError("succeeded action results may not report failure_class") + if self.status == ParticipantActionResultStatus.ACCEPTED and self.failure_class is not None: + raise ValueError("accepted action results may not report failure_class") + + def _validate_terminal_effects_present(self) -> None: + if self.status in _PARTICIPANT_ACTION_TERMINAL_EFFECT_STATUSES: + if not self.effects: + raise ValueError(f"{self.status.value} action results require declared effects") + + def _validate_failure_status_requires_failure_class(self) -> None: + if self.status in _PARTICIPANT_ACTION_FAILURE_STATUSES and self.failure_class is None: + raise ValueError(f"{self.status.value} action results require a portable failure_class") diff --git a/implementations/python/packages/aces_processor/models/attribution.py b/implementations/python/packages/aces_processor/models/attribution.py new file mode 100644 index 000000000..dac79bb45 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/attribution.py @@ -0,0 +1,323 @@ +"""Participant attribution candidate, ordering, evidence, and edge records.""" + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +from aces_sdl.participant_attribution_semantics import ( + OUTCOME_ATTRIBUTION_CANDIDATE_KINDS, + STRONG_ATTRIBUTION_SUPPORT_CLASSES, + ParticipantAttributionCandidateKind, + ParticipantAttributionOrderingBasisKind, + ParticipantAttributionSupportClass, +) + +from .behavior_resources import _tuple_of_non_empty_strings, _validate_optional_string, _validate_required_string + + +@dataclass(frozen=True) +class ParticipantAttributionCandidate: + """Candidate endpoint for a SEM-212 attribution edge.""" + + candidate_kind: ParticipantAttributionCandidateKind + ref: str + description: str + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionCandidate": + if not isinstance(payload, Mapping): + raise TypeError("participant attribution candidate must be a mapping") + missing = [key for key in ("candidate_kind", "ref", "description") if key not in payload] + if missing: + raise ValueError("participant attribution candidate is missing required fields: " + ", ".join(missing)) + candidate_kind_raw = payload.get("candidate_kind") + return cls( + candidate_kind=( + candidate_kind_raw + if isinstance(candidate_kind_raw, ParticipantAttributionCandidateKind) + else ParticipantAttributionCandidateKind(str(candidate_kind_raw)) + ), + ref=str(payload.get("ref")), + description=str(payload.get("description")), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "candidate_kind": self.candidate_kind.value, + "ref": self.ref, + "description": self.description, + } + + def __post_init__(self) -> None: + if not isinstance(self.candidate_kind, ParticipantAttributionCandidateKind): + raise TypeError("candidate_kind must be a ParticipantAttributionCandidateKind") + _validate_required_string(self.ref, "participant attribution candidate ref must be a non-empty string") + _validate_required_string( + self.description, + "participant attribution candidate description must be a non-empty string", + ) + + +@dataclass(frozen=True) +class ParticipantAttributionOrderingBasis: + """Explicit ordering basis for a SEM-212 attribution edge.""" + + basis_kind: ParticipantAttributionOrderingBasisKind + relation_ref: str + description: str + ordered_event_refs: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionOrderingBasis": + if not isinstance(payload, Mapping): + raise TypeError("participant attribution ordering_basis must be a mapping") + missing = [key for key in ("basis_kind", "relation_ref", "description") if key not in payload] + if missing: + raise ValueError("participant attribution ordering_basis is missing required fields: " + ", ".join(missing)) + basis_kind_raw = payload.get("basis_kind") + return cls( + basis_kind=( + basis_kind_raw + if isinstance(basis_kind_raw, ParticipantAttributionOrderingBasisKind) + else ParticipantAttributionOrderingBasisKind(str(basis_kind_raw)) + ), + relation_ref=str(payload.get("relation_ref")), + description=str(payload.get("description")), + ordered_event_refs=_tuple_of_non_empty_strings( + payload.get("ordered_event_refs", ()), + field_name="ordered_event_refs", + ), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "basis_kind": self.basis_kind.value, + "relation_ref": self.relation_ref, + "description": self.description, + "ordered_event_refs": list(self.ordered_event_refs), + } + + def __post_init__(self) -> None: + if not isinstance(self.basis_kind, ParticipantAttributionOrderingBasisKind): + raise TypeError("basis_kind must be a ParticipantAttributionOrderingBasisKind") + _validate_required_string(self.relation_ref, "ordering_basis relation_ref must be a non-empty string") + _validate_required_string(self.description, "ordering_basis description must be a non-empty string") + _tuple_of_non_empty_strings(self.ordered_event_refs, field_name="ordered_event_refs") + + +@dataclass(frozen=True) +class ParticipantAttributionEvidenceBasis: + """Evidence-disclosure basis for a SEM-212 attribution edge.""" + + capture_apparatus: str + granularity: str + loss_model: str + redaction_policy: str + observer_effects: tuple[str, ...] + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionEvidenceBasis": + if not isinstance(payload, Mapping): + raise TypeError("participant attribution evidence_basis must be a mapping") + missing = [ + key + for key in ( + "capture_apparatus", + "granularity", + "loss_model", + "redaction_policy", + "observer_effects", + ) + if key not in payload + ] + if missing: + raise ValueError("participant attribution evidence_basis is missing required fields: " + ", ".join(missing)) + return cls( + capture_apparatus=str(payload.get("capture_apparatus")), + granularity=str(payload.get("granularity")), + loss_model=str(payload.get("loss_model")), + redaction_policy=str(payload.get("redaction_policy")), + observer_effects=_tuple_of_non_empty_strings( + payload.get("observer_effects", ()), + field_name="observer_effects", + ), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "capture_apparatus": self.capture_apparatus, + "granularity": self.granularity, + "loss_model": self.loss_model, + "redaction_policy": self.redaction_policy, + "observer_effects": list(self.observer_effects), + } + + def __post_init__(self) -> None: + _validate_required_string( + self.capture_apparatus, + "evidence_basis capture_apparatus must be a non-empty string", + ) + _validate_required_string(self.granularity, "evidence_basis granularity must be a non-empty string") + _validate_required_string(self.loss_model, "evidence_basis loss_model must be a non-empty string") + _validate_required_string(self.redaction_policy, "evidence_basis redaction_policy must be a non-empty string") + observer_effects = _tuple_of_non_empty_strings(self.observer_effects, field_name="observer_effects") + if not observer_effects: + raise ValueError("evidence_basis observer_effects must disclose at least one observer effect") + + +def _validate_participant_attribution_edge_types( + cause_candidate: ParticipantAttributionCandidate, + effect_candidate: ParticipantAttributionCandidate, + ordering_basis: ParticipantAttributionOrderingBasis, + evidence_basis: ParticipantAttributionEvidenceBasis, + support_class: ParticipantAttributionSupportClass, +) -> None: + if not isinstance(cause_candidate, ParticipantAttributionCandidate): + raise TypeError("cause_candidate must be a ParticipantAttributionCandidate") + if not isinstance(effect_candidate, ParticipantAttributionCandidate): + raise TypeError("effect_candidate must be a ParticipantAttributionCandidate") + if not isinstance(ordering_basis, ParticipantAttributionOrderingBasis): + raise TypeError("ordering_basis must be a ParticipantAttributionOrderingBasis") + if not isinstance(evidence_basis, ParticipantAttributionEvidenceBasis): + raise TypeError("evidence_basis must be a ParticipantAttributionEvidenceBasis") + if not isinstance(support_class, ParticipantAttributionSupportClass): + raise TypeError("support_class must be a ParticipantAttributionSupportClass") + + +def _validate_participant_attribution_edge_causal_rules( + support_class: ParticipantAttributionSupportClass, + ordering_basis: ParticipantAttributionOrderingBasis, + effect_candidate: ParticipantAttributionCandidate, + interpretation_rule_ref: str | None, +) -> None: + if ( + support_class in STRONG_ATTRIBUTION_SUPPORT_CLASSES + and ordering_basis.basis_kind == ParticipantAttributionOrderingBasisKind.TIMESTAMP_ADJACENCY + ): + raise ValueError("timestamp_adjacency ordering_basis cannot support strong causal attribution claims") + if effect_candidate.candidate_kind in OUTCOME_ATTRIBUTION_CANDIDATE_KINDS and interpretation_rule_ref is None: + raise ValueError("downstream outcome attribution requires interpretation_rule_ref") + + +@dataclass(frozen=True) +class ParticipantAttributionEdge: + """Evidence-labeled SEM-212 attribution edge.""" + + edge_id: str + participant_address: str + episode_id: str + observation_point: str + cause_candidate: ParticipantAttributionCandidate + effect_candidate: ParticipantAttributionCandidate + ordering_basis: ParticipantAttributionOrderingBasis + evidence_basis: ParticipantAttributionEvidenceBasis + support_class: ParticipantAttributionSupportClass + confidence: str + strength: str + limitations: tuple[str, ...] + evidence_refs: tuple[str, ...] + interpretation_rule_ref: str | None = None + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantAttributionEdge": + if not isinstance(payload, Mapping): + raise TypeError("participant attribution edge must be a mapping") + missing = [ + key + for key in ( + "edge_id", + "participant_address", + "episode_id", + "observation_point", + "cause_candidate", + "effect_candidate", + "ordering_basis", + "evidence_basis", + "support_class", + "confidence", + "strength", + "limitations", + "evidence_refs", + ) + if key not in payload + ] + if missing: + raise ValueError("participant attribution edge is missing required fields: " + ", ".join(missing)) + support_class_raw = payload.get("support_class") + return cls( + edge_id=str(payload.get("edge_id")), + participant_address=str(payload.get("participant_address")), + episode_id=str(payload.get("episode_id")), + observation_point=str(payload.get("observation_point")), + cause_candidate=ParticipantAttributionCandidate.from_payload(payload.get("cause_candidate")), + effect_candidate=ParticipantAttributionCandidate.from_payload(payload.get("effect_candidate")), + ordering_basis=ParticipantAttributionOrderingBasis.from_payload(payload.get("ordering_basis")), + evidence_basis=ParticipantAttributionEvidenceBasis.from_payload(payload.get("evidence_basis")), + support_class=( + support_class_raw + if isinstance(support_class_raw, ParticipantAttributionSupportClass) + else ParticipantAttributionSupportClass(str(support_class_raw)) + ), + confidence=str(payload.get("confidence")), + strength=str(payload.get("strength")), + limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), + interpretation_rule_ref=( + str(payload["interpretation_rule_ref"]) if payload.get("interpretation_rule_ref") is not None else None + ), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "edge_id": self.edge_id, + "participant_address": self.participant_address, + "episode_id": self.episode_id, + "observation_point": self.observation_point, + "cause_candidate": self.cause_candidate.to_payload(), + "effect_candidate": self.effect_candidate.to_payload(), + "ordering_basis": self.ordering_basis.to_payload(), + "evidence_basis": self.evidence_basis.to_payload(), + "support_class": self.support_class.value, + "confidence": self.confidence, + "strength": self.strength, + "limitations": list(self.limitations), + "evidence_refs": list(self.evidence_refs), + "interpretation_rule_ref": self.interpretation_rule_ref, + } + + def __post_init__(self) -> None: + _validate_required_string(self.edge_id, "participant attribution edge_id must be a non-empty string") + _validate_required_string( + self.participant_address, + "participant attribution participant_address must be a non-empty string", + ) + _validate_required_string(self.episode_id, "participant attribution episode_id must be a non-empty string") + _validate_required_string( + self.observation_point, + "participant attribution observation_point must be a non-empty string", + ) + _validate_participant_attribution_edge_types( + self.cause_candidate, + self.effect_candidate, + self.ordering_basis, + self.evidence_basis, + self.support_class, + ) + _validate_required_string(self.confidence, "participant attribution confidence must be a non-empty string") + _validate_required_string(self.strength, "participant attribution strength must be a non-empty string") + limitations = _tuple_of_non_empty_strings(self.limitations, field_name="limitations") + evidence_refs = _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + if not limitations: + raise ValueError("participant attribution edges require limitations") + if not evidence_refs: + raise ValueError("participant attribution edges require evidence_refs") + _validate_optional_string( + self.interpretation_rule_ref, + "interpretation_rule_ref must be a non-empty string or None", + ) + _validate_participant_attribution_edge_causal_rules( + self.support_class, + self.ordering_basis, + self.effect_candidate, + self.interpretation_rule_ref, + ) diff --git a/implementations/python/packages/aces_processor/models/behavior_anchor_checks.py b/implementations/python/packages/aces_processor/models/behavior_anchor_checks.py new file mode 100644 index 000000000..187aa81fa --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_anchor_checks.py @@ -0,0 +1,476 @@ +"""Participant-behavior anchor, visibility, and SEM-213/215 contract violation checks.""" + +from collections.abc import Iterable, Iterator, Mapping +from typing import Any + +from aces_contracts.participant_behavior import ParticipantBehaviorHistoryEventType +from aces_contracts.participant_episode import ParticipantEpisodeHistoryEvent + +from .behavior_anchor_index import ( + _participant_behavior_history_anchor_indexes, + _participant_behavior_transition_anchor_index, +) +from .behavior_grounding_checks import ( + _participant_behavior_attribution_ref_authorization_violations, + _participant_behavior_outcome_ref_authorization_violations, +) +from .behavior_ref_checks import ( + _participant_behavior_action_result_ref_authorization_violations, + _participant_behavior_detail_shape_violations, + _participant_behavior_initial_view_relation, + _participant_behavior_observation_detail_refs, + _participant_behavior_transition_delta, + _participant_behavior_transition_effective_order, + _participant_behavior_transition_matches_relation, + _participant_behavior_view_relation_deltas_by_order, + _participant_behavior_visibility_detail_violations, +) +from .behavior_resources import ParticipantObservationBoundaryRuntime +from .history_event import ParticipantBehaviorHistoryEvent +from .resources import ( + _PARTICIPANT_BEHAVIOR_HISTORY_KEY, + _PARTICIPANT_EPISODE_TERMINAL_EVENTS, + ParticipantActionContractRuntime, +) +from .temporal import ParticipantTemporalRuntimeContext + + +def _participant_behavior_transition_anchor_violation( + *, + transition: Mapping[str, Any], + boundary_address: str, + action_attempts: Mapping[str, int], + state_transitions: Mapping[str, int], + observations: Mapping[tuple[str, str | None], int], + episode_close_resolved: bool, +) -> tuple[str, str] | None: + event_type = str(transition.get("history_event_type", "")) + action_instance_id = transition.get("action_instance_id") + transition_id = str(transition.get("transition_id", "")) + locator = f"{boundary_address}.view_transitions.{transition_id}" + if event_type == "episode_close": + return _participant_behavior_episode_close_transition_violation( + locator=locator, + episode_close_resolved=episode_close_resolved, + ) + return _participant_behavior_action_transition_anchor_violation( + event_type=event_type, + action_instance_id=action_instance_id, + locator=locator, + boundary_address=boundary_address, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) + + +def _participant_behavior_episode_close_transition_violation( + *, + locator: str, + episode_close_resolved: bool, +) -> tuple[str, str] | None: + if episode_close_resolved: + return None + return ( + locator, + "visibility transition anchor does not resolve to a terminal participant episode history event", + ) + + +def _participant_behavior_action_transition_anchor_violation( + *, + event_type: str, + action_instance_id: object, + locator: str, + boundary_address: str, + action_attempts: Mapping[str, int], + state_transitions: Mapping[str, int], + observations: Mapping[tuple[str, str | None], int], +) -> tuple[str, str] | None: + if not isinstance(action_instance_id, str) or not action_instance_id: + return (locator, "visibility transition anchors require action_instance_id") + event_indexes = { + "action_attempted": action_instance_id in action_attempts, + "state_transition_recorded": action_instance_id in state_transitions, + "observation_emitted": (action_instance_id, boundary_address) in observations, + } + if event_type not in event_indexes: + return (locator, f"visibility transition anchor has unknown history_event_type {event_type!r}") + if event_indexes[event_type]: + result = None + else: + article = "an" if event_type == "observation_emitted" else "a" + result = (locator, f"visibility transition anchor does not resolve to {article} {event_type} event") + return result + + +def _participant_behavior_transition_anchor_violations( + events: list[ParticipantBehaviorHistoryEvent], + *, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], + participant_episode_history: object = None, +) -> Iterator[tuple[str, str]]: + action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) + episode_close_resolved = _participant_behavior_episode_close_resolved( + events, + participant_episode_history=participant_episode_history, + ) + for boundary_address, boundary in observation_boundaries.items(): + for transition in boundary.view_transitions: + violation = _participant_behavior_transition_anchor_violation( + transition=transition, + boundary_address=boundary_address, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + episode_close_resolved=episode_close_resolved, + ) + if violation is not None: + yield violation + + +def _participant_behavior_episode_close_resolved( + events: list[ParticipantBehaviorHistoryEvent], + *, + participant_episode_history: object, +) -> bool: + if not isinstance(participant_episode_history, list): + return False + participant_addresses = {event.participant_address for event in events} + episode_ids = {event.episode_id for event in events} + if not participant_addresses or not episode_ids: + return False + closed_episode_ids = _participant_behavior_episode_closed_ids( + participant_episode_history, + participant_addresses=participant_addresses, + episode_ids=episode_ids, + ) + return episode_ids <= closed_episode_ids + + +def _participant_behavior_episode_closed_ids( + participant_episode_history: list[object], + *, + participant_addresses: set[str], + episode_ids: set[str], +) -> set[str]: + closed_episode_ids: set[str] = set() + for event in participant_episode_history: + if not isinstance(event, Mapping): + continue + try: + normalized = ParticipantEpisodeHistoryEvent.from_payload(event) + except (TypeError, ValueError): + continue + if normalized.participant_address not in participant_addresses: + continue + if normalized.episode_id not in episode_ids: + continue + if normalized.event_type in _PARTICIPANT_EPISODE_TERMINAL_EVENTS: + closed_episode_ids.add(normalized.episode_id) + return closed_episode_ids + + +def _participant_behavior_observation_effective_relation( + *, + observation_index: int, + boundary_address: str, + boundary: ParticipantObservationBoundaryRuntime, + action_attempts: Mapping[str, int], + state_transitions: Mapping[str, int], + observations: Mapping[tuple[str, str | None], int], +) -> tuple[dict[str, str], int]: + relation = _participant_behavior_initial_view_relation(boundary) + deltas_by_order = _participant_behavior_view_relation_deltas_by_order(boundary) + effective_order = -1 + for transition in sorted( + boundary.view_transitions, + key=lambda item: ( + _participant_behavior_transition_effective_order(item) + if _participant_behavior_transition_effective_order(item) is not None + else -1 + ), + ): + order = _participant_behavior_transition_effective_order(transition) + if order is None: + continue + anchor_index = _participant_behavior_transition_anchor_index( + transition=transition, + boundary_address=boundary_address, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) + if anchor_index is None or anchor_index > observation_index: + continue + if not _participant_behavior_transition_matches_relation(transition, relation=relation): + continue + relation.update(_participant_behavior_transition_delta(transition, deltas_by_order=deltas_by_order)) + effective_order = max(effective_order, order) + return relation, effective_order + + +def _participant_behavior_detail_shape_violations_for_events( + events: list[ParticipantBehaviorHistoryEvent], +) -> Iterator[tuple[str, str]]: + for index, event in enumerate(events): + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + yield from _participant_behavior_detail_shape_violations(event, locator=locator) + + +def _participant_behavior_observation_visibility_violations( + events: list[ParticipantBehaviorHistoryEvent], + *, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], +) -> Iterator[tuple[str, str]]: + action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) + for index, event in enumerate(events): + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + continue + boundary_address = event.observation_boundary_address or "" + boundary = observation_boundaries.get(boundary_address) + if boundary is None: + continue + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + detail_refs, violations = _participant_behavior_observation_detail_refs(event, locator=locator) + if violations: + yield from violations + continue + if not any(detail_refs.values()): + continue + relation, effective_order = _participant_behavior_observation_effective_relation( + observation_index=index, + boundary_address=boundary_address, + boundary=boundary, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) + yield from _participant_behavior_visibility_detail_violations( + locator=locator, + detail_refs=detail_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + + +def _participant_behavior_action_result_ref_authorization_violations_for_events( + events: list[ParticipantBehaviorHistoryEvent], + *, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime], +) -> Iterator[tuple[str, str]]: + action_attempts, state_transitions, observations = _participant_behavior_history_anchor_indexes(events) + for index, event in enumerate(events): + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + continue + if event.action_result is None and not event.attribution_edges and not event.outcome_interpretations: + continue + boundary_address = event.observation_boundary_address or "" + boundary = observation_boundaries.get(boundary_address) + if boundary is None: + continue + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + relation, effective_order = _participant_behavior_observation_effective_relation( + observation_index=index, + boundary_address=boundary_address, + boundary=boundary, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) + if event.action_result is not None: + yield from _participant_behavior_action_result_ref_authorization_violations( + event=event, + locator=locator, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + yield from _participant_behavior_attribution_ref_authorization_violations( + event=event, + locator=locator, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + yield from _participant_behavior_outcome_ref_authorization_violations( + event=event, + locator=locator, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + + +def _participant_behavior_address_violations( + event: ParticipantBehaviorHistoryEvent, + *, + locator: str, + action_contract_addresses: set[str] | frozenset[str] | None, + observation_boundary_addresses: set[str] | frozenset[str] | None, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + if action_contract_addresses is not None and event.action_contract_address not in action_contract_addresses: + violations.append( + ( + locator, + ( + "participant behavior event references unknown action_contract_address " + f"{event.action_contract_address!r}" + ), + ) + ) + if ( + observation_boundary_addresses is not None + and event.observation_boundary_address is not None + and event.observation_boundary_address not in observation_boundary_addresses + ): + violations.append( + ( + locator, + ( + "participant behavior event references unknown observation_boundary_address " + f"{event.observation_boundary_address!r}" + ), + ) + ) + return violations + + +def _contract_sem213_temporal_contracts( + contract: ParticipantActionContractRuntime, +) -> dict[str, Mapping[str, Any]]: + temporal_contracts = contract.spec.get("temporal_contracts", ()) + if isinstance(temporal_contracts, (str, bytes, Mapping)) or not isinstance(temporal_contracts, Iterable): + return {} + return { + str(temporal_contract.get("temporal_id")): temporal_contract + for temporal_contract in temporal_contracts + if isinstance(temporal_contract, Mapping) and temporal_contract.get("temporal_id") + } + + +def _contract_sem213_backend_disclosure_ids(contract: ParticipantActionContractRuntime) -> set[str]: + disclosures = contract.spec.get("backend_timing_disclosures", ()) + if isinstance(disclosures, (str, bytes, Mapping)) or not isinstance(disclosures, Iterable): + return set() + return { + str(disclosure.get("disclosure_id")) + for disclosure in disclosures + if isinstance(disclosure, Mapping) and disclosure.get("disclosure_id") + } + + +def _participant_temporal_context_contract_violations( + context: ParticipantTemporalRuntimeContext, + *, + contract: ParticipantActionContractRuntime, +) -> list[str]: + temporal_contracts = _contract_sem213_temporal_contracts(contract) + temporal_contract = temporal_contracts.get(context.temporal_contract_id) + if temporal_contract is None: + return [f"temporal context references undeclared temporal_contract_id {context.temporal_contract_id!r}"] + + violations: list[str] = [] + violations.extend(_participant_temporal_context_scalar_violations(context, temporal_contract=temporal_contract)) + violations.extend( + _participant_temporal_context_disclosure_violations( + context, + temporal_contract=temporal_contract, + contract=contract, + ) + ) + violations.extend(_participant_temporal_context_boundary_violations(context, temporal_contract=temporal_contract)) + return violations + + +def _participant_temporal_context_scalar_violations( + context: ParticipantTemporalRuntimeContext, + *, + temporal_contract: Mapping[str, Any], +) -> list[str]: + violations: list[str] = [] + declared_time_domain = str(temporal_contract.get("time_domain", "")) + if context.time_domain.value != declared_time_domain: + violations.append( + f"temporal context {context.temporal_contract_id!r} time_domain {context.time_domain.value!r} " + f"does not match compiled contract {declared_time_domain!r}" + ) + + declared_clock_authority = str(temporal_contract.get("clock_authority", "")) + if context.clock_authority != declared_clock_authority: + violations.append( + f"temporal context {context.temporal_contract_id!r} clock_authority {context.clock_authority!r} " + f"does not match compiled contract {declared_clock_authority!r}" + ) + + declared_event_points = tuple(str(point) for point in temporal_contract.get("event_points", ())) + observed_event_points = tuple(point.value for point in context.event_points) + if observed_event_points != declared_event_points: + violations.append( + f"temporal context {context.temporal_contract_id!r} event_points {observed_event_points!r} " + f"do not match compiled contract {declared_event_points!r}" + ) + return violations + + +def _participant_temporal_context_disclosure_violations( + context: ParticipantTemporalRuntimeContext, + *, + temporal_contract: Mapping[str, Any], + contract: ParticipantActionContractRuntime, +) -> list[str]: + violations: list[str] = [] + declared_contract_disclosures = {str(ref) for ref in temporal_contract.get("backend_disclosure_refs", ())} + declared_disclosures = _contract_sem213_backend_disclosure_ids(contract) + for ref in sorted(set(context.backend_disclosure_refs) - declared_contract_disclosures): + violations.append( + f"temporal context {context.temporal_contract_id!r} reports backend_disclosure_ref {ref!r} " + "not declared by the temporal contract" + ) + for ref in sorted(set(context.backend_disclosure_refs) - declared_disclosures): + violations.append( + f"temporal context {context.temporal_contract_id!r} reports unknown backend_disclosure_ref {ref!r}" + ) + return violations + + +def _participant_temporal_context_boundary_violations( + context: ParticipantTemporalRuntimeContext, + *, + temporal_contract: Mapping[str, Any], +) -> list[str]: + violations: list[str] = [] + declared_reset_boundary = temporal_contract.get("reset_boundary") + if declared_reset_boundary is not None and context.reset_boundary != str(declared_reset_boundary): + violations.append( + f"temporal context {context.temporal_contract_id!r} reset_boundary {context.reset_boundary!r} " + f"does not match compiled contract {str(declared_reset_boundary)!r}" + ) + declared_replay_boundary = temporal_contract.get("replay_boundary") + if declared_replay_boundary is not None and context.replay_boundary != str(declared_replay_boundary): + violations.append( + f"temporal context {context.temporal_contract_id!r} replay_boundary {context.replay_boundary!r} " + f"does not match compiled contract {str(declared_replay_boundary)!r}" + ) + return violations + + +def _participant_behavior_temporal_contract_violations( + events: Iterable[ParticipantBehaviorHistoryEvent], + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime], +) -> Iterator[tuple[str, str]]: + for index, event in enumerate(events): + if not event.temporal_contexts: + continue + action_contract_address = event.action_contract_address or "" + contract = action_contracts.get(action_contract_address) + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + if contract is None: + yield (locator, f"temporal context cannot resolve action contract {action_contract_address!r}") + continue + for context in event.temporal_contexts: + for violation in _participant_temporal_context_contract_violations(context, contract=contract): + yield (locator, violation) diff --git a/implementations/python/packages/aces_processor/models/behavior_anchor_index.py b/implementations/python/packages/aces_processor/models/behavior_anchor_index.py new file mode 100644 index 000000000..b511f0652 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_anchor_index.py @@ -0,0 +1,64 @@ +"""Time-indexed anchor lookups over participant-behavior history events.""" + +from collections.abc import Iterable, Mapping +from typing import Any + +from aces_contracts.participant_behavior import ParticipantBehaviorHistoryEventType + +from .history_event import ParticipantBehaviorHistoryEvent + + +def _participant_behavior_history_anchor_indexes( + events: Iterable[ParticipantBehaviorHistoryEvent], +) -> tuple[dict[str, int], dict[str, int], dict[tuple[str, str | None], int]]: + action_attempts: dict[str, int] = {} + state_transitions: dict[str, int] = {} + observations: dict[tuple[str, str | None], int] = {} + for index, event in enumerate(events): + if event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED: + action_attempts.setdefault(event.action_instance_id, index) + elif event.event_type == ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED: + state_transitions.setdefault(event.action_instance_id, index) + elif event.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + observations.setdefault((event.action_instance_id, event.observation_boundary_address), index) + return action_attempts, state_transitions, observations + + +def _participant_behavior_transition_anchor_index_by_event_type( + *, + event_type: str, + action_instance_id: str, + boundary_address: str, + action_attempts: Mapping[str, int], + state_transitions: Mapping[str, int], + observations: Mapping[tuple[str, str | None], int], +) -> int | None: + if event_type == "action_attempted": + return action_attempts.get(action_instance_id) + if event_type == "state_transition_recorded": + return state_transitions.get(action_instance_id) + return observations.get((action_instance_id, boundary_address)) if event_type == "observation_emitted" else None + + +def _participant_behavior_transition_anchor_index( + *, + transition: Mapping[str, Any], + boundary_address: str, + action_attempts: Mapping[str, int], + state_transitions: Mapping[str, int], + observations: Mapping[tuple[str, str | None], int], +) -> int | None: + event_type = str(transition.get("history_event_type", "")) + if event_type == "episode_close": + return None + action_instance_id = transition.get("action_instance_id") + if not isinstance(action_instance_id, str) or not action_instance_id: + return None + return _participant_behavior_transition_anchor_index_by_event_type( + event_type=event_type, + action_instance_id=action_instance_id, + boundary_address=boundary_address, + action_attempts=action_attempts, + state_transitions=state_transitions, + observations=observations, + ) diff --git a/implementations/python/packages/aces_processor/models/behavior_grounding_checks.py b/implementations/python/packages/aces_processor/models/behavior_grounding_checks.py new file mode 100644 index 000000000..5179db17d --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_grounding_checks.py @@ -0,0 +1,449 @@ +"""Participant-behavior outcome-grounding and anchor-index violation checks.""" + +from collections.abc import Iterable, Iterator, Mapping + +from aces_contracts.participant_behavior import ParticipantBehaviorHistoryEventType +from aces_contracts.participant_episode import ParticipantEpisodeHistoryEvent +from aces_sdl.participant_outcome_semantics import OutcomeInterpretationSourceLayer + +from .behavior_ref_checks import ( + _participant_behavior_attribution_candidate_ref_violations, + _participant_behavior_attribution_evidence_ref_violations, +) +from .behavior_resources import ParticipantObservationBoundaryRuntime +from .history_event import ParticipantBehaviorHistoryEvent +from .outcome import ParticipantOutcomeInterpretationRecord, ParticipantOutcomeSourceRecord +from .resources import _PARTICIPANT_BEHAVIOR_HISTORY_KEY, _PARTICIPANT_EPISODE_TERMINAL_EVENTS + + +def _participant_behavior_attribution_ref_authorization_violations( + *, + event: ParticipantBehaviorHistoryEvent, + locator: str, + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for edge in event.attribution_edges: + violations.extend( + _participant_behavior_attribution_evidence_ref_violations( + locator=locator, + edge=edge, + refs=edge.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_attribution_candidate_ref_violations( + locator=locator, + edge=edge, + candidate=edge.cause_candidate, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_attribution_candidate_ref_violations( + locator=locator, + edge=edge, + candidate=edge.effect_candidate, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + return violations + + +def _participant_behavior_outcome_evidence_ref_violations( + *, + locator: str, + record: ParticipantOutcomeInterpretationRecord, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for ref in refs: + disposition = relation.get(ref) + if disposition is None and ref in boundary.hidden_refs: + disposition = "hidden" + if ref in boundary.evidence_refs or disposition == "evidence_only": + continue + suffix = f": disposition {disposition!r}" if disposition is not None else "" + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} evidence_ref {ref!r} " + f"is not authorized evidence at effective_order {effective_order}{suffix}" + ), + ) + ) + return violations + + +def _participant_behavior_outcome_provenance_ref_violations( + *, + locator: str, + record: ParticipantOutcomeInterpretationRecord, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for ref in refs: + disposition = relation.get(ref) + if disposition is None and ref in boundary.hidden_refs: + disposition = "hidden" + if ref in boundary.evidence_refs or disposition in {"evidence_only", "disclosed", "observable", "discovered"}: + continue + if ref not in boundary.hidden_refs and disposition not in {"hidden", "concealed", "deceptive"}: + continue + suffix = f": disposition {disposition!r}" if disposition is not None else "" + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} provenance_ref {ref!r} " + f"exposes a hidden participant-boundary ref at effective_order {effective_order}{suffix}" + ), + ) + ) + return violations + + +def _participant_behavior_outcome_ref_authorization_violations( + *, + event: ParticipantBehaviorHistoryEvent, + locator: str, + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for record in event.outcome_interpretations: + violations.extend( + _participant_behavior_outcome_evidence_ref_violations( + locator=locator, + record=record, + refs=record.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + for source in record.source_bindings: + violations.extend( + _participant_behavior_outcome_evidence_ref_violations( + locator=locator, + record=record, + refs=source.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_outcome_provenance_ref_violations( + locator=locator, + record=record, + refs=source.provenance_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + for target in record.target_bindings: + violations.extend( + _participant_behavior_outcome_evidence_ref_violations( + locator=locator, + record=record, + refs=target.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + return violations + + +def _participant_behavior_event_evidence_refs(event: ParticipantBehaviorHistoryEvent) -> set[str]: + evidence_refs: set[str] = set() + detail_refs = event.details.get("evidence_refs") + if not isinstance(detail_refs, (str, bytes, Mapping)) and isinstance(detail_refs, Iterable): + evidence_refs.update(str(ref) for ref in detail_refs if isinstance(ref, str) and ref) + if event.action_result is not None: + evidence_refs.update(event.action_result.evidence_refs) + for precondition in event.action_result.preconditions: + evidence_refs.update(precondition.evidence_refs) + for effect in event.action_result.effects: + evidence_refs.update(effect.evidence_refs) + for edge in event.attribution_edges: + evidence_refs.update(edge.evidence_refs) + return evidence_refs + + +def _participant_episode_histories_from_source( + participant_episode_history: object, +) -> Iterable[object]: + if isinstance(participant_episode_history, Mapping): + return participant_episode_history.values() + if isinstance(participant_episode_history, list): + return [participant_episode_history] + return [] + + +def _participant_episode_normalized_history_event( + event: object, +) -> ParticipantEpisodeHistoryEvent | None: + if not isinstance(event, Mapping): + return None + try: + return ParticipantEpisodeHistoryEvent.from_payload(event) + except (TypeError, ValueError): + return None + + +def _participant_episode_terminal_status_entry( + event: object, +) -> tuple[tuple[str, str], str] | None: + normalized = _participant_episode_normalized_history_event(event) + if normalized is None: + return None + terminal_reason = _PARTICIPANT_EPISODE_TERMINAL_EVENTS.get(normalized.event_type) + if terminal_reason is None: + return None + return (normalized.participant_address, normalized.episode_id), terminal_reason.value + + +def _participant_episode_terminal_statuses( + participant_episode_history: object, +) -> dict[tuple[str, str], set[str]]: + terminal_statuses: dict[tuple[str, str], set[str]] = {} + for history in _participant_episode_histories_from_source(participant_episode_history): + if isinstance(history, (str, bytes, Mapping)) or not isinstance(history, Iterable): + continue + for event in history: + entry = _participant_episode_terminal_status_entry(event) + if entry is None: + continue + key, value = entry + terminal_statuses.setdefault(key, set()).add(value) + return terminal_statuses + + +def _participant_behavior_outcome_evidence_grounding_violations( + *, + locator: str, + record: ParticipantOutcomeInterpretationRecord, + owner_label: str, + refs: tuple[str, ...], + grounded_evidence_refs: set[str], +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + owner = f" {owner_label}" if owner_label else "" + for ref in refs: + if ref in grounded_evidence_refs: + continue + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r}{owner} evidence_ref {ref!r} " + "is not grounded in event evidence" + ), + ) + ) + return violations + + +def _participant_behavior_outcome_action_source_grounding_violations( + *, + locator: str, + event: ParticipantBehaviorHistoryEvent, + record: ParticipantOutcomeInterpretationRecord, + source: ParticipantOutcomeSourceRecord, +) -> list[tuple[str, str]]: + if source.source_layer != OutcomeInterpretationSourceLayer.PARTICIPANT_ACTION_OUTCOME: + return [] + if event.action_result is None: + return [ + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + "uses participant_action_outcome without an action_result" + ), + ) + ] + violations: list[tuple[str, str]] = [] + if source.ref != event.action_result.action_contract_address: + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"ref {source.ref!r} does not match action_result action_contract_address " + f"{event.action_result.action_contract_address!r}" + ), + ) + ) + expected_status = event.action_result.status.value + if source.observed_value != expected_status: + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"observed_value {source.observed_value!r} does not match action_result status " + f"{expected_status!r}" + ), + ) + ) + return violations + + +def _participant_behavior_outcome_episode_status_mismatch_violations( + *, + locator: str, + record: ParticipantOutcomeInterpretationRecord, + source: ParticipantOutcomeSourceRecord, + statuses: set[str], +) -> list[tuple[str, str]]: + if source.observed_value in statuses: + return [] + expected = ", ".join(repr(status) for status in sorted(statuses)) + return [ + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"observed_value {source.observed_value!r} does not match participant_episode_history terminal status " + f"{expected}" + ), + ) + ] + + +def _participant_behavior_outcome_episode_status_grounding_violations( + *, + locator: str, + record: ParticipantOutcomeInterpretationRecord, + source: ParticipantOutcomeSourceRecord, + terminal_statuses: Mapping[tuple[str, str], set[str]], +) -> list[tuple[str, str]]: + if source.source_layer != OutcomeInterpretationSourceLayer.PARTICIPANT_EPISODE_STATUS: + return [] + key = (record.participant_address, record.episode_id) + statuses = terminal_statuses.get(key, set()) + if not statuses: + return [ + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + "participant_episode_status is not grounded by a terminal participant_episode_history event" + ), + ) + ] + return _participant_behavior_outcome_episode_status_mismatch_violations( + locator=locator, + record=record, + source=source, + statuses=statuses, + ) + + +def _participant_behavior_outcome_source_grounding_violations( + *, + locator: str, + event: ParticipantBehaviorHistoryEvent, + record: ParticipantOutcomeInterpretationRecord, + source: ParticipantOutcomeSourceRecord, + grounded_evidence_refs: set[str], + terminal_statuses: Mapping[tuple[str, str], set[str]], +) -> list[tuple[str, str]]: + violations = _participant_behavior_outcome_action_source_grounding_violations( + locator=locator, + event=event, + record=record, + source=source, + ) + violations.extend( + _participant_behavior_outcome_episode_status_grounding_violations( + locator=locator, + record=record, + source=source, + terminal_statuses=terminal_statuses, + ) + ) + if ( + source.source_layer == OutcomeInterpretationSourceLayer.EVIDENCE_CLAIM + and source.ref not in grounded_evidence_refs + ): + violations.append( + ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} evidence_claim source " + f"{source.source_id!r} ref {source.ref!r} is not grounded in event evidence" + ), + ) + ) + violations.extend( + _participant_behavior_outcome_evidence_grounding_violations( + locator=locator, + record=record, + owner_label=f"source {source.source_id!r}", + refs=source.evidence_refs, + grounded_evidence_refs=grounded_evidence_refs, + ) + ) + return violations + + +def _participant_behavior_outcome_event_grounding_violations( + events: Iterable[ParticipantBehaviorHistoryEvent], + *, + participant_episode_history: object = None, +) -> Iterator[tuple[str, str]]: + terminal_statuses = _participant_episode_terminal_statuses(participant_episode_history) + for index, event in enumerate(events): + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + continue + if not event.outcome_interpretations: + continue + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + grounded_evidence_refs = _participant_behavior_event_evidence_refs(event) + for record in event.outcome_interpretations: + yield from _participant_behavior_outcome_evidence_grounding_violations( + locator=locator, + record=record, + owner_label="", + refs=record.evidence_refs, + grounded_evidence_refs=grounded_evidence_refs, + ) + for source in record.source_bindings: + yield from _participant_behavior_outcome_source_grounding_violations( + locator=locator, + event=event, + record=record, + source=source, + grounded_evidence_refs=grounded_evidence_refs, + terminal_statuses=terminal_statuses, + ) + for target in record.target_bindings: + yield from _participant_behavior_outcome_evidence_grounding_violations( + locator=locator, + record=record, + owner_label=f"target {target.target_id!r}", + refs=target.evidence_refs, + grounded_evidence_refs=grounded_evidence_refs, + ) diff --git a/implementations/python/packages/aces_processor/models/behavior_history_violations.py b/implementations/python/packages/aces_processor/models/behavior_history_violations.py new file mode 100644 index 000000000..142be418c --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_history_violations.py @@ -0,0 +1,437 @@ +"""Outcome-interpretation validation and the public participant-behavior violation iterators.""" + +from collections.abc import Iterable, Iterator, Mapping +from dataclasses import dataclass +from typing import Any + +from aces_contracts.participant_behavior import ParticipantBehaviorHistoryEventType, ParticipantObservationStatus + +from .behavior_anchor_checks import ( + _participant_behavior_action_result_ref_authorization_violations_for_events, + _participant_behavior_address_violations, + _participant_behavior_detail_shape_violations_for_events, + _participant_behavior_observation_visibility_violations, + _participant_behavior_temporal_contract_violations, + _participant_behavior_transition_anchor_violations, +) +from .behavior_grounding_checks import _participant_behavior_outcome_event_grounding_violations +from .behavior_resources import ( + _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES, + ParticipantObservationBoundaryRuntime, + ParticipantOutcomeInterpretationRuleRuntime, +) +from .history_event import ParticipantBehaviorHistoryEvent +from .outcome_interpretation_validation import validate_participant_outcome_interpretation_record +from .resources import ( + _PARTICIPANT_BEHAVIOR_HISTORY_KEY, + ParticipantActionContractRuntime, + _contract_uses_sem211_action_results, + validate_participant_action_result_contract, +) + + +def _participant_behavior_outcome_interpretation_rule_violations( + events: Iterable[ParticipantBehaviorHistoryEvent], + *, + outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime], +) -> Iterator[tuple[str, str]]: + for index, event in enumerate(events): + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + continue + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + for record in event.outcome_interpretations: + rule = outcome_interpretation_rules.get(record.rule_address) + if rule is None: + yield ( + locator, + ( + f"outcome interpretation {record.interpretation_id!r} references unknown " + f"rule_address {record.rule_address!r}" + ), + ) + continue + for violation in validate_participant_outcome_interpretation_record(record, rule): + yield (locator, violation) + + +def _participant_behavior_action_result_contract_violations( + events: Iterable[ParticipantBehaviorHistoryEvent], + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime], +) -> Iterator[tuple[str, str]]: + for index, event in enumerate(events): + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + continue + if event.observation_status not in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES: + continue + action_contract_address = event.action_contract_address or "" + contract = action_contracts.get(action_contract_address) + if contract is None or not _contract_uses_sem211_action_results(contract): + continue + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + if event.action_result is None: + yield ( + locator, + f"terminal observation must carry SEM-211 action_result for {action_contract_address}", + ) + continue + for violation in validate_participant_action_result_contract(event.action_result, contract): + yield (locator, violation) + + +def _normalize_participant_behavior_events( + participant_behavior_history: list[Any], + *, + action_contract_addresses: set[str] | frozenset[str] | None, + observation_boundary_addresses: set[str] | frozenset[str] | None, + expected_participant_address: str | None = None, +) -> tuple[list[ParticipantBehaviorHistoryEvent], list[tuple[str, str]]]: + normalized_events: list[ParticipantBehaviorHistoryEvent] = [] + violations: list[tuple[str, str]] = [] + for index, event in enumerate(participant_behavior_history): + locator = f"{_PARTICIPANT_BEHAVIOR_HISTORY_KEY}[{index}]" + if not isinstance(event, Mapping): + violations.append((locator, "participant behavior history event must be a mapping")) + continue + try: + normalized = ParticipantBehaviorHistoryEvent.from_payload(event) + except (TypeError, ValueError) as exc: + violations.append((locator, f"participant behavior history event is invalid: {exc}")) + continue + if expected_participant_address is not None and normalized.participant_address != expected_participant_address: + violations.append( + ( + locator, + ( + f"participant behavior history event outer key {expected_participant_address!r} " + f"does not match inner participant_address {normalized.participant_address!r}" + ), + ) + ) + continue + violations.extend( + _participant_behavior_address_violations( + normalized, + locator=locator, + action_contract_addresses=action_contract_addresses, + observation_boundary_addresses=observation_boundary_addresses, + ) + ) + normalized_events.append(normalized) + return normalized_events, violations + + +def _participant_behavior_events_by_action_instance( + events: list[ParticipantBehaviorHistoryEvent], +) -> dict[str, list[ParticipantBehaviorHistoryEvent]]: + events_by_action_instance: dict[str, list[ParticipantBehaviorHistoryEvent]] = {} + for event in events: + events_by_action_instance.setdefault(event.action_instance_id, []).append(event) + return events_by_action_instance + + +def _participant_action_instance_event_groups( + events: list[ParticipantBehaviorHistoryEvent], +) -> tuple[ + list[ParticipantBehaviorHistoryEvent], + list[ParticipantBehaviorHistoryEvent], + list[ParticipantBehaviorHistoryEvent], +]: + attempts = [event for event in events if event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED] + observations = [ + event + for event in events + if event.event_type == ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED + and event.observation_status in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES + ] + transitions = [ + event for event in events if event.event_type == ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED + ] + return attempts, observations, transitions + + +def _participant_action_instance_count_violation( + action_instance_id: str, + attempts: list[ParticipantBehaviorHistoryEvent], + observations: list[ParticipantBehaviorHistoryEvent], +) -> tuple[str, str] | None: + result: tuple[str, str] | None = None + if len(attempts) > 1: + result = (action_instance_id, "participant action instance may only have one action_attempted event") + elif len(attempts) == 0: + result = (action_instance_id, "participant behavior events require a matching action_attempted event") + elif len(observations) != 1: + result = ( + action_instance_id, + "participant action instance requires exactly one terminal observation or orphaned-action observation", + ) + return result + + +def _participant_action_instance_transition_violation( + action_instance_id: str, + observation: ParticipantBehaviorHistoryEvent, + transitions: list[ParticipantBehaviorHistoryEvent], +) -> tuple[str, str] | None: + if len(transitions) != 1: + return (action_instance_id, "participant action instance requires exactly one state transition") + if observation.post_state_digest != transitions[0].post_state_digest: + return ( + action_instance_id, + "terminal observation post_state_digest must match the state transition post_state_digest", + ) + return None + + +def _participant_behavior_action_instance_violation( + action_instance_id: str, + events: list[ParticipantBehaviorHistoryEvent], +) -> tuple[str, str] | None: + attempts, observations, transitions = _participant_action_instance_event_groups(events) + count_violation = _participant_action_instance_count_violation(action_instance_id, attempts, observations) + if count_violation is not None: + return count_violation + observation = observations[0] + if observation.observation_status == ParticipantObservationStatus.ORPHANED_ACTION: + return None + return _participant_action_instance_transition_violation(action_instance_id, observation, transitions) + + +def _participant_behavior_action_instance_violations( + events: list[ParticipantBehaviorHistoryEvent], +) -> Iterator[tuple[str, str]]: + for action_instance_id, grouped_events in _participant_behavior_events_by_action_instance(events).items(): + violation = _participant_behavior_action_instance_violation(action_instance_id, grouped_events) + if violation is not None: + yield violation + + +def _participant_behavior_joint_action_order_violations( + events: Iterable[ParticipantBehaviorHistoryEvent], +) -> Iterator[tuple[str, str]]: + attempts_by_joint_set: dict[str, list[ParticipantBehaviorHistoryEvent]] = {} + for event in events: + if ( + event.event_type == ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED + and event.joint_action_set_id is not None + ): + attempts_by_joint_set.setdefault(event.joint_action_set_id, []).append(event) + + for joint_action_set_id, attempts in sorted(attempts_by_joint_set.items()): + attempts_by_order: dict[int, list[ParticipantBehaviorHistoryEvent]] = {} + for event in attempts: + if event.realized_order is None: + continue + attempts_by_order.setdefault(event.realized_order, []).append(event) + for realized_order, duplicate_attempts in sorted(attempts_by_order.items()): + if len(duplicate_attempts) <= 1: + continue + instances = ", ".join( + sorted(f"{event.participant_address}/{event.action_instance_id}" for event in duplicate_attempts) + ) + yield ( + f"joint-action-set.{joint_action_set_id}", + ( + f"joint action set realized_order {realized_order} is assigned to " + f"multiple action_attempted events: {instances}" + ), + ) + + +@dataclass(frozen=True) +class ParticipantHistoryAddressScope: + """Optional compiled address sets for participant-behavior history checks. + + Bundles the compiled action-contract and observation-boundary address sets + so a caller can restrict address validation to a participant-scoped subset + independently of the contract/boundary mappings. When a field is ``None`` + and the matching mapping is supplied, the address set defaults to that + mapping's keys. + """ + + action_contract_addresses: set[str] | frozenset[str] | None = None + observation_boundary_addresses: set[str] | frozenset[str] | None = None + + +def iter_participant_behavior_history_violations( + participant_behavior_history: object, + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime] | None = None, + outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime] | None = None, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime] | None = None, + participant_episode_history: object = None, + expected_participant_address: str | None = None, + address_scope: ParticipantHistoryAddressScope | None = None, +) -> Iterator[tuple[str, str]]: + """Yield every SEM-208 behavior-history invariant violation. + + The helper checks that each action instance has one terminal observation + paired with the state transition digest it reports. When compiled address + sets are provided (via ``address_scope``), it also rejects references + outside those sets. When compiled observation boundaries are provided, + SEM-210 observation details and SEM-211 action-result references are checked + against the time-indexed participant view relation. + """ + + if not isinstance(participant_behavior_history, list): + yield (_PARTICIPANT_BEHAVIOR_HISTORY_KEY, "participant behavior history must be a list of events") + return + yield from _iter_validated_participant_behavior_violations( + participant_behavior_history, + action_contracts=action_contracts, + outcome_interpretation_rules=outcome_interpretation_rules, + observation_boundaries=observation_boundaries, + participant_episode_history=participant_episode_history, + expected_participant_address=expected_participant_address, + address_scope=address_scope, + ) + + +def _iter_validated_participant_behavior_violations( + participant_behavior_history: list[Any], + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime] | None, + outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime] | None, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime] | None, + participant_episode_history: object, + expected_participant_address: str | None, + address_scope: ParticipantHistoryAddressScope | None, +) -> Iterator[tuple[str, str]]: + if address_scope is None: + address_scope = ParticipantHistoryAddressScope() + action_contract_addresses = address_scope.action_contract_addresses + observation_boundary_addresses = address_scope.observation_boundary_addresses + if action_contracts is not None and action_contract_addresses is None: + action_contract_addresses = frozenset(action_contracts.keys()) + if observation_boundaries is not None and observation_boundary_addresses is None: + observation_boundary_addresses = frozenset(observation_boundaries.keys()) + + normalized_events, entry_violations = _normalize_participant_behavior_events( + participant_behavior_history, + action_contract_addresses=action_contract_addresses, + observation_boundary_addresses=observation_boundary_addresses, + expected_participant_address=expected_participant_address, + ) + if entry_violations: + yield from entry_violations + return + yield from _iter_normalized_participant_behavior_violations( + normalized_events, + action_contracts=action_contracts, + outcome_interpretation_rules=outcome_interpretation_rules, + observation_boundaries=observation_boundaries, + participant_episode_history=participant_episode_history, + ) + + +def _iter_normalized_participant_behavior_violations( + normalized_events: list[ParticipantBehaviorHistoryEvent], + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime] | None, + outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime] | None, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime] | None, + participant_episode_history: object, +) -> Iterator[tuple[str, str]]: + yield from _iter_participant_behavior_structure_violations( + normalized_events, + participant_episode_history=participant_episode_history, + ) + yield from _iter_participant_behavior_contract_violations( + normalized_events, + action_contracts=action_contracts, + outcome_interpretation_rules=outcome_interpretation_rules, + ) + yield from _iter_participant_behavior_observation_violations( + normalized_events, + observation_boundaries=observation_boundaries, + participant_episode_history=participant_episode_history, + ) + + +def _iter_participant_behavior_structure_violations( + normalized_events: list[ParticipantBehaviorHistoryEvent], + *, + participant_episode_history: object, +) -> Iterator[tuple[str, str]]: + yield from _iter_participant_behavior_event_shape_violations(normalized_events) + yield from _participant_behavior_outcome_event_grounding_violations( + normalized_events, + participant_episode_history=participant_episode_history, + ) + + +def _iter_participant_behavior_event_shape_violations( + normalized_events: list[ParticipantBehaviorHistoryEvent], +) -> Iterator[tuple[str, str]]: + yield from _participant_behavior_detail_shape_violations_for_events(normalized_events) + yield from _participant_behavior_action_instance_violations(normalized_events) + yield from _participant_behavior_joint_action_order_violations(normalized_events) + + +def _iter_participant_behavior_contract_violations( + normalized_events: list[ParticipantBehaviorHistoryEvent], + *, + action_contracts: Mapping[str, ParticipantActionContractRuntime] | None, + outcome_interpretation_rules: Mapping[str, ParticipantOutcomeInterpretationRuleRuntime] | None, +) -> Iterator[tuple[str, str]]: + if action_contracts is not None: + yield from _participant_behavior_action_result_contract_violations( + normalized_events, + action_contracts=action_contracts, + ) + yield from _participant_behavior_temporal_contract_violations( + normalized_events, + action_contracts=action_contracts, + ) + if outcome_interpretation_rules is not None: + yield from _participant_behavior_outcome_interpretation_rule_violations( + normalized_events, + outcome_interpretation_rules=outcome_interpretation_rules, + ) + + +def _iter_participant_behavior_observation_violations( + normalized_events: list[ParticipantBehaviorHistoryEvent], + *, + observation_boundaries: Mapping[str, ParticipantObservationBoundaryRuntime] | None, + participant_episode_history: object, +) -> Iterator[tuple[str, str]]: + if observation_boundaries is not None: + yield from _participant_behavior_transition_anchor_violations( + normalized_events, + observation_boundaries=observation_boundaries, + participant_episode_history=participant_episode_history, + ) + yield from _participant_behavior_observation_visibility_violations( + normalized_events, + observation_boundaries=observation_boundaries, + ) + yield from _participant_behavior_action_result_ref_authorization_violations_for_events( + normalized_events, + observation_boundaries=observation_boundaries, + ) + + +def iter_participant_behavior_joint_action_violations( + participant_behavior_history_by_participant: object, +) -> Iterator[tuple[str, str]]: + """Yield SEM-209 joint-action ordering violations across participant histories.""" + + if not isinstance(participant_behavior_history_by_participant, Mapping): + yield (_PARTICIPANT_BEHAVIOR_HISTORY_KEY, "participant behavior histories must be a mapping") + return + + normalized_events: list[ParticipantBehaviorHistoryEvent] = [] + for history in participant_behavior_history_by_participant.values(): + if not isinstance(history, list): + continue + participant_events, entry_violations = _normalize_participant_behavior_events( + history, + action_contract_addresses=None, + observation_boundary_addresses=None, + ) + if entry_violations: + continue + normalized_events.extend(participant_events) + + yield from _participant_behavior_joint_action_order_violations(normalized_events) diff --git a/implementations/python/packages/aces_processor/models/behavior_ref_checks.py b/implementations/python/packages/aces_processor/models/behavior_ref_checks.py new file mode 100644 index 000000000..3e3bb94a0 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_ref_checks.py @@ -0,0 +1,471 @@ +"""Participant-behavior history reference and ref-authorization violation checks.""" + +from collections.abc import Iterable, Mapping +from typing import Any + +from aces_contracts.participant_behavior import ParticipantBehaviorHistoryEventType +from aces_sdl.participant_attribution_semantics import ParticipantAttributionCandidateKind + +from .attribution import ParticipantAttributionCandidate, ParticipantAttributionEdge +from .behavior_resources import ( + _PARTICIPANT_OBSERVATION_DETAIL_KEYS, + _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS, + _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS, + ParticipantObservationBoundaryRuntime, +) +from .history_event import ParticipantBehaviorHistoryEvent + + +def _participant_behavior_detail_refs_result( + value: object, + *, + key: str, +) -> tuple[tuple[str, ...], str | None]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + return (), f"observation details field {key!r} must be a list of strings" + items = tuple(value) + refs = tuple(str(ref) for ref in items if isinstance(ref, str) and ref) + if len(refs) != len(items): + message: str | None = f"observation details field {key!r} must contain only non-empty strings" + elif len(set(refs)) != len(refs): + message = f"observation details field {key!r} must not contain duplicate refs" + else: + message = None + return refs, message + + +def _participant_behavior_detail_refs( + event: ParticipantBehaviorHistoryEvent, + *, + key: str, + locator: str, +) -> tuple[tuple[str, ...], list[tuple[str, str]]]: + if key not in event.details: + return (), [] + refs, message = _participant_behavior_detail_refs_result(event.details[key], key=key) + if message is None: + return refs, [] + return (), [(locator, message)] + + +def _participant_behavior_detail_shape_violations( + event: ParticipantBehaviorHistoryEvent, + *, + locator: str, +) -> list[tuple[str, str]]: + if not event.details: + return [] + violations: list[tuple[str, str]] = [] + if event.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + violations.append((locator, "participant behavior details are only allowed on observation_emitted events")) + unsupported_keys = sorted(str(key) for key in event.details if key not in _PARTICIPANT_OBSERVATION_DETAIL_KEYS) + if unsupported_keys: + allowed = ", ".join(_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS) + unsupported = ", ".join(unsupported_keys) + violations.append( + ( + locator, + f"observation details may only contain {allowed}; unsupported fields: {unsupported}", + ) + ) + return violations + + +def _participant_behavior_timeline_relations( + boundary: ParticipantObservationBoundaryRuntime, +) -> tuple[tuple[int, dict[str, str]], ...]: + relations: list[tuple[int, dict[str, str]]] = [] + for snapshot in boundary.view_relation_timeline: + order = snapshot.get("effective_order") + raw_relation = snapshot.get("view_relation", {}) + if not isinstance(order, int) or isinstance(order, bool) or not isinstance(raw_relation, Mapping): + continue + relations.append((order, {str(ref): str(disposition) for ref, disposition in raw_relation.items()})) + return tuple(sorted(relations, key=lambda item: item[0])) + + +def _participant_behavior_initial_view_relation( + boundary: ParticipantObservationBoundaryRuntime, +) -> dict[str, str]: + initial_relation: dict[str, str] = {} + for order, relation in _participant_behavior_timeline_relations(boundary): + if order > -1: + break + initial_relation = dict(relation) + return initial_relation + + +def _participant_behavior_view_relation_deltas_by_order( + boundary: ParticipantObservationBoundaryRuntime, +) -> dict[int, dict[str, str]]: + deltas: dict[int, dict[str, str]] = {} + previous_relation: dict[str, str] | None = None + for order, relation in _participant_behavior_timeline_relations(boundary): + if previous_relation is None: + previous_relation = relation + continue + deltas[order] = { + ref: disposition for ref, disposition in relation.items() if previous_relation.get(ref) != disposition + } + previous_relation = relation + return deltas + + +def _participant_behavior_transition_effective_order(transition: Mapping[str, Any]) -> int | None: + order = transition.get("effective_order") + if not isinstance(order, int) or isinstance(order, bool): + return None + return order + + +def _participant_behavior_transition_delta( + transition: Mapping[str, Any], + *, + deltas_by_order: Mapping[int, Mapping[str, str]], +) -> dict[str, str]: + information_ref = transition.get("information_ref") + to_disposition = transition.get("to_disposition") + if isinstance(information_ref, str) and information_ref and isinstance(to_disposition, str) and to_disposition: + return {information_ref: to_disposition} + order = _participant_behavior_transition_effective_order(transition) + if order is None: + return {} + return dict(deltas_by_order.get(order, {})) + + +def _participant_behavior_transition_matches_relation( + transition: Mapping[str, Any], + *, + relation: Mapping[str, str], +) -> bool: + information_ref = transition.get("information_ref") + from_disposition = transition.get("from_disposition") + if not isinstance(information_ref, str) or not information_ref: + return True + if not isinstance(from_disposition, str) or not from_disposition: + return True + return relation.get(information_ref) == from_disposition + + +def _participant_behavior_observation_detail_refs( + event: ParticipantBehaviorHistoryEvent, + *, + locator: str, +) -> tuple[dict[str, tuple[str, ...]], list[tuple[str, str]]]: + detail_refs: dict[str, tuple[str, ...]] = {} + violations: list[tuple[str, str]] = [] + for key in _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS: + refs, ref_violations = _participant_behavior_detail_refs(event, key=key, locator=locator) + detail_refs[key] = refs + violations.extend(ref_violations) + return detail_refs, violations + + +def _participant_behavior_disposition_ref_violations( + *, + locator: str, + refs: tuple[str, ...], + relation: Mapping[str, str], + allowed_dispositions: frozenset[str], + effective_order: int, + detail_key: str, + allowed_label: str, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for ref in refs: + disposition = relation.get(ref) + if disposition in allowed_dispositions: + continue + violations.append( + ( + locator, + ( + f"observation {detail_key} may only contain {allowed_label} refs at " + f"effective_order {effective_order}: " + f"{ref!r} has disposition {disposition!r}" + ), + ) + ) + return violations + + +def _participant_behavior_evidence_ref_violations( + *, + locator: str, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for ref in refs: + if ref in boundary.evidence_refs or relation.get(ref) == "evidence_only": + continue + violations.append( + ( + locator, + ( + "observation evidence_refs may only contain boundary evidence refs at " + f"effective_order {effective_order}: {ref!r}" + ), + ) + ) + return violations + + +def _participant_behavior_visibility_detail_violations( + *, + locator: str, + detail_refs: Mapping[str, tuple[str, ...]], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + return [ + *_participant_behavior_disposition_ref_violations( + locator=locator, + refs=detail_refs["visible_refs"], + relation=relation, + allowed_dispositions=_PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS, + effective_order=effective_order, + detail_key="visible_refs", + allowed_label="participant-visible", + ), + *_participant_behavior_disposition_ref_violations( + locator=locator, + refs=detail_refs["disclosed_refs"], + relation=relation, + allowed_dispositions=frozenset({"disclosed"}), + effective_order=effective_order, + detail_key="disclosed_refs", + allowed_label="disclosed", + ), + *_participant_behavior_evidence_ref_violations( + locator=locator, + refs=detail_refs["evidence_refs"], + boundary=boundary, + relation=relation, + effective_order=effective_order, + ), + ] + + +def _participant_behavior_action_result_visible_ref_violations( + *, + locator: str, + owner_label: str, + field_name: str, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + owner_prefix = f" {owner_label}" if owner_label else "" + for ref in refs: + disposition = relation.get(ref) + if disposition is None and ref in boundary.hidden_refs: + disposition = "hidden" + if disposition is None: + continue + if disposition in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: + continue + violations.append( + ( + locator, + ( + f"action_result{owner_prefix} {field_name} {ref!r} is not participant-visible " + f"at effective_order {effective_order}: disposition {disposition!r}" + ), + ) + ) + return violations + + +def _participant_behavior_action_result_evidence_ref_violations( + *, + locator: str, + owner_label: str, + field_name: str, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + owner_prefix = f" {owner_label}" if owner_label else "" + for ref in refs: + disposition = relation.get(ref) + if disposition is None and ref in boundary.hidden_refs: + disposition = "hidden" + if ref in boundary.evidence_refs or disposition == "evidence_only": + continue + suffix = f": disposition {disposition!r}" if disposition is not None else "" + violations.append( + ( + locator, + ( + f"action_result{owner_prefix} {field_name} {ref!r} is not authorized evidence " + f"at effective_order {effective_order}{suffix}" + ), + ) + ) + return violations + + +def _participant_behavior_action_result_ref_authorization_violations( + *, + event: ParticipantBehaviorHistoryEvent, + locator: str, + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + if event.action_result is None: + return [] + violations: list[tuple[str, str]] = [] + for precondition in event.action_result.preconditions: + owner_label = f"precondition {precondition.precondition_id!r}" + violations.extend( + _participant_behavior_action_result_visible_ref_violations( + locator=locator, + owner_label=owner_label, + field_name="support_ref", + refs=precondition.support_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_action_result_evidence_ref_violations( + locator=locator, + owner_label=owner_label, + field_name="evidence_ref", + refs=precondition.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + for effect in event.action_result.effects: + owner_label = f"effect {effect.effect_id!r}" + violations.extend( + _participant_behavior_action_result_visible_ref_violations( + locator=locator, + owner_label=owner_label, + field_name="target_ref", + refs=effect.target_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_action_result_evidence_ref_violations( + locator=locator, + owner_label=owner_label, + field_name="evidence_ref", + refs=effect.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + violations.extend( + _participant_behavior_action_result_evidence_ref_violations( + locator=locator, + owner_label="", + field_name="evidence_ref", + refs=event.action_result.evidence_refs, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + ) + return violations + + +def _participant_behavior_attribution_evidence_ref_violations( + *, + locator: str, + edge: ParticipantAttributionEdge, + refs: tuple[str, ...], + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + violations: list[tuple[str, str]] = [] + for ref in refs: + disposition = relation.get(ref) + if disposition is None and ref in boundary.hidden_refs: + disposition = "hidden" + if ref in boundary.evidence_refs or disposition == "evidence_only": + continue + suffix = f": disposition {disposition!r}" if disposition is not None else "" + violations.append( + ( + locator, + ( + f"attribution edge {edge.edge_id!r} evidence_ref {ref!r} is not authorized evidence " + f"at effective_order {effective_order}{suffix}" + ), + ) + ) + return violations + + +def _participant_behavior_attribution_visibility_candidate_violations( + *, + locator: str, + edge: ParticipantAttributionEdge, + candidate: ParticipantAttributionCandidate, + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + disposition = relation.get(candidate.ref) + if disposition is None and candidate.ref in boundary.hidden_refs: + disposition = "hidden" + if disposition is None or disposition in _PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS: + return [] + return [ + ( + locator, + ( + f"attribution edge {edge.edge_id!r} {candidate.candidate_kind.value} candidate " + f"{candidate.ref!r} is not participant-visible at effective_order {effective_order}: " + f"disposition {disposition!r}" + ), + ) + ] + + +def _participant_behavior_attribution_candidate_ref_violations( + *, + locator: str, + edge: ParticipantAttributionEdge, + candidate: ParticipantAttributionCandidate, + boundary: ParticipantObservationBoundaryRuntime, + relation: Mapping[str, str], + effective_order: int, +) -> list[tuple[str, str]]: + if candidate.candidate_kind == ParticipantAttributionCandidateKind.ACTION: + return [] + if candidate.candidate_kind == ParticipantAttributionCandidateKind.EVIDENCE: + return _participant_behavior_attribution_evidence_ref_violations( + locator=locator, + edge=edge, + refs=(candidate.ref,), + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) + return _participant_behavior_attribution_visibility_candidate_violations( + locator=locator, + edge=edge, + candidate=candidate, + boundary=boundary, + relation=relation, + effective_order=effective_order, + ) diff --git a/implementations/python/packages/aces_processor/models/behavior_resources.py b/implementations/python/packages/aces_processor/models/behavior_resources.py new file mode 100644 index 000000000..acf02209c --- /dev/null +++ b/implementations/python/packages/aces_processor/models/behavior_resources.py @@ -0,0 +1,279 @@ +"""Participant-behavior and workflow resource records plus shared validation helpers.""" + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any + +from aces_backend_protocols.capabilities import WorkflowFeature, WorkflowStatePredicateFeature +from aces_contracts.participant_behavior import ParticipantObservationStatus +from aces_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION +from aces_contracts.workflow import WorkflowExecutionContract, WorkflowResultContract, WorkflowStepOutcome +from aces_sdl.semantics.workflow import WorkflowStepSemanticContract + +from .resources import ResolvedResource + + +@dataclass(frozen=True) +class ParticipantObservationBoundaryRuntime(ResolvedResource): + """Compiled participant observation projection boundary.""" + + boundary_name: str = "" + projection_basis: str = "" + hidden_refs: tuple[str, ...] = () + observable_refs: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + disclosed_refs: tuple[str, ...] = () + evidence_only_refs: tuple[str, ...] = () + discovered_refs: tuple[str, ...] = () + inferred_refs: tuple[str, ...] = () + concealed_refs: tuple[str, ...] = () + deceptive_refs: tuple[str, ...] = () + view_transitions: tuple[dict[str, Any], ...] = () + view_relation_timeline: tuple[dict[str, Any], ...] = () + realized_view_disclosure: str = "" + + +@dataclass(frozen=True) +class ParticipantOutcomeInterpretationRuleRuntime(ResolvedResource): + """Compiled SEM-215 participant outcome interpretation rule.""" + + rule_name: str = "" + semantic_version: str = "" + participant_scope: str = "" + observation_point_basis: str = "" + interpretation_basis: str = "" + source_layers: tuple[str, ...] = () + source_refs: tuple[str, ...] = () + target_layers: tuple[str, ...] = () + target_refs: tuple[str, ...] = () + evidence_refs: tuple[str, ...] = () + limitations: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ParticipantBehaviorRuntime(ResolvedResource): + """Compiled role-neutral participant behavior binding.""" + + participant_name: str = "" + entity_name: str = "" + starting_account_refs: tuple[str, ...] = () + starting_account_addresses: tuple[str, ...] = () + initial_knowledge_addresses: tuple[str, ...] = () + starting_assertion_refs: tuple[str, ...] = () + starting_assertion_addresses: tuple[str, ...] = () + authority_anchor_refs: tuple[str, ...] = () + authority_anchor_addresses: tuple[str, ...] = () + operating_scope_refs: tuple[str, ...] = () + operating_scope_addresses: tuple[str, ...] = () + action_contract_addresses: tuple[str, ...] = () + observation_boundary_addresses: tuple[str, ...] = () + interpretation_mode: str = "role-neutral-projection" + + +@dataclass(frozen=True) +class ParticipantBehaviorSpecificationRuntime(ResolvedResource): + """Compiled first-class participant behavior specification aggregate.""" + + spec_name: str = "" + semantic_version: str = "" + lifecycle_state: str = "" + participant_addresses: tuple[str, ...] = () + participant_role_refs: tuple[str, ...] = () + action_contract_addresses: tuple[str, ...] = () + observation_boundary_addresses: tuple[str, ...] = () + outcome_interpretation_rule_addresses: tuple[str, ...] = () + authority_scope_refs: tuple[str, ...] = () + authority_scope_addresses: tuple[str, ...] = () + behavior_mode: str = "" + ai_offensive_behavior_refs: tuple[str, ...] = () + offensive_behavior_refs: tuple[str, ...] = () + realization_profile_ref: str = "" + backend_feature_support_refs: tuple[str, ...] = () + evidence_contract_refs: tuple[str, ...] = () + extension_policy: str = "" + extension_keys: tuple[str, ...] = () + + +@dataclass(frozen=True) +class EventRuntime(ResolvedResource): + """Resolved orchestration event.""" + + assertion_names: tuple[str, ...] = () + assertion_addresses: tuple[str, ...] = () + inject_names: tuple[str, ...] = () + inject_addresses: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ScriptRuntime(ResolvedResource): + """Resolved script with event dependencies.""" + + event_addresses: tuple[str, ...] = () + + +@dataclass(frozen=True) +class StoryRuntime(ResolvedResource): + """Resolved story with script dependencies.""" + + script_addresses: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ObjectiveWindowReferenceRuntime: + """Normalized resolved objective/window reference.""" + + raw: str + canonical_name: str + reference_kind: str + dependency_roles: tuple[str, ...] = () + workflow_name: str = "" + step_name: str = "" + namespace_path: tuple[str, ...] = () + + +@dataclass(frozen=True) +class WorkflowStepStatePredicateRuntime: + """Resolved predicate clause over prior workflow step state.""" + + step_name: str + outcomes: tuple[WorkflowStepOutcome, ...] = () + min_attempts: int | str | None = None + + +@dataclass(frozen=True) +class WorkflowPredicateRuntime: + """Resolved workflow predicate semantics.""" + + assertion_addresses: tuple[str, ...] = () + objective_addresses: tuple[str, ...] = () + step_state_predicates: tuple[WorkflowStepStatePredicateRuntime, ...] = () + + @property + def external_addresses(self) -> tuple[str, ...]: + seen: set[str] = set() + ordered: list[str] = [] + for address in ( + *self.assertion_addresses, + *self.objective_addresses, + ): + if address in seen: + continue + seen.add(address) + ordered.append(address) + return tuple(ordered) + + +@dataclass(frozen=True) +class WorkflowSwitchCaseRuntime: + """Resolved ordered switch-case branch semantics.""" + + case_index: int + predicate: WorkflowPredicateRuntime + next_step: str + + +@dataclass(frozen=True) +class WorkflowStepRuntime: + """Resolved workflow step semantics.""" + + name: str + step_type: str + objective_address: str = "" + predicate: WorkflowPredicateRuntime | None = None + next_step: str = "" + on_success: str = "" + on_failure: str = "" + on_exhausted: str = "" + then_step: str = "" + else_step: str = "" + switch_cases: tuple[WorkflowSwitchCaseRuntime, ...] = () + default_step: str = "" + branches: tuple[str, ...] = () + join_step: str = "" + owning_parallel_step: str = "" + called_workflow_address: str = "" + compensation_workflow_address: str = "" + max_attempts: int | str | None = None + state_contract: WorkflowStepSemanticContract = field( + default_factory=lambda: WorkflowStepSemanticContract(step_type="") + ) + + +@dataclass(frozen=True) +class WorkflowRuntime(ResolvedResource): + """Resolved workflow control program.""" + + start_step: str = "" + referenced_objective_addresses: tuple[str, ...] = () + control_steps: dict[str, WorkflowStepRuntime] = field(default_factory=dict) + control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) + join_owners: dict[str, str] = field(default_factory=dict) + step_assertion_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) + step_predicate_addresses: dict[str, tuple[str, ...]] = field(default_factory=dict) + required_features: tuple[WorkflowFeature, ...] = () + required_state_predicate_features: tuple[WorkflowStatePredicateFeature, ...] = () + result_contract: "WorkflowResultContract" = field(default_factory=lambda: WorkflowResultContract()) + execution_contract: "WorkflowExecutionContract" = field(default_factory=lambda: WorkflowExecutionContract()) + state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION + + +def _participant_observation_status_from_payload(value: object) -> ParticipantObservationStatus | None: + if isinstance(value, ParticipantObservationStatus): + return value + if value is None: + return None + return ParticipantObservationStatus(str(value)) + + +def _validate_required_string(value: object, message: str) -> None: + if not isinstance(value, str) or not value: + raise TypeError(message) + + +def _validate_optional_string(value: object, message: str) -> None: + if value is not None and (not isinstance(value, str) or not value): + raise TypeError(message) + + +def _validate_optional_address(value: str | None, *, prefix: str, message: str) -> None: + if value is not None and (not isinstance(value, str) or not value.startswith(prefix)): + raise ValueError(message) + + +def _validate_required_address(value: str, *, prefix: str, message: str) -> None: + if not isinstance(value, str) or not value.startswith(prefix): + raise ValueError(message) + + +def _tuple_of_non_empty_strings(value: object, *, field_name: str) -> tuple[str, ...]: + if value is None: + return () + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError(f"{field_name} must be a list of strings") + values = tuple(value) + refs = tuple(str(item) for item in values if isinstance(item, str) and item) + if len(refs) != len(values): + raise TypeError(f"{field_name} entries must be non-empty strings") + if len(set(refs)) != len(refs): + raise ValueError(f"{field_name} entries must be unique") + return refs + + +def _observation_point_matches_action_instance(observation_point: str, action_instance_id: str) -> bool: + return action_instance_id in observation_point.split(":") + + +def _optional_payload_string(payload: Mapping[str, Any], key: str) -> str | None: + value = payload.get(key) + return str(value) if value is not None else None + + +_PARTICIPANT_TERMINAL_OBSERVATION_STATUSES = frozenset( + { + ParticipantObservationStatus.TERMINAL, + ParticipantObservationStatus.ORPHANED_ACTION, + } +) +_PARTICIPANT_VISIBLE_VIEW_DISPOSITIONS = frozenset({"observable", "discovered", "inferred", "disclosed", "deceptive"}) +_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS = ("visible_refs", "disclosed_refs", "evidence_refs") +_PARTICIPANT_OBSERVATION_DETAIL_KEYS = frozenset(_PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS) diff --git a/implementations/python/packages/aces_processor/models/history_event.py b/implementations/python/packages/aces_processor/models/history_event.py new file mode 100644 index 000000000..20349f97e --- /dev/null +++ b/implementations/python/packages/aces_processor/models/history_event.py @@ -0,0 +1,490 @@ +"""The resolved participant-behavior history event record.""" + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + +from aces_contracts.participant_behavior import ( + ParticipantActionResultStatus, + ParticipantAdmissionDisposition, + ParticipantBehaviorHistoryEventType, + ParticipantLifecycleOperationState, + ParticipantObservationStatus, + ParticipantPhaseRealization, + ParticipantRuntimeLifecyclePhase, + participant_lifecycle_field_violation_messages, +) +from aces_sdl.participant_attribution_semantics import ( + OUTCOME_ATTRIBUTION_CANDIDATE_KINDS, + ParticipantAttributionCandidateKind, +) +from aces_sdl.participant_behavior import ParticipantInteractionClass + +from .action_results import ParticipantActionResult +from .attribution import ParticipantAttributionEdge +from .behavior_resources import ( + _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES, + _observation_point_matches_action_instance, + _optional_payload_string, + _participant_observation_status_from_payload, +) +from .history_event_grounding import _event_attribution_grounded_refs, _optional_enum_value +from .history_event_payloads import ( + _participant_action_result_from_payload, + _participant_admission_disposition_from_payload, + _participant_attribution_edges_from_payload, + _participant_behavior_details_from_payload, + _participant_behavior_event_type_from_payload, + _participant_behavior_shared_state_refs_from_payload, + _participant_interaction_class_from_payload, + _participant_lifecycle_operation_state_from_payload, + _participant_lifecycle_phase_from_payload, + _participant_phase_realization_from_payload, + _participant_temporal_contexts_from_payload, +) +from .outcome import ParticipantOutcomeInterpretationRecord, _participant_outcome_interpretation_records_from_payload +from .resources import _PARTICIPANT_ACTION_CONTRACT_PREFIX, _PARTICIPANT_OBSERVATION_BOUNDARY_PREFIX +from .temporal import ParticipantTemporalRuntimeContext + + +@dataclass(frozen=True) +class ParticipantBehaviorHistoryEvent: + """Internal normalized participant behavior history event. + + The canonical record keeps actor provenance and compiled behavior-contract + addresses. Role-neutral interpretation is a projection over those records, + not a reason to treat raw action names or backend-native logs as behavior + semantics. + """ + + event_type: ParticipantBehaviorHistoryEventType + timestamp: str + participant_address: str + episode_id: str + action_instance_id: str + action_contract_address: str | None = None + observation_boundary_address: str | None = None + observation_status: ParticipantObservationStatus | None = None + actor_provenance: str | None = None + lifecycle_phase: ParticipantRuntimeLifecyclePhase | None = None + phase_realization: ParticipantPhaseRealization | None = None + admission_disposition: ParticipantAdmissionDisposition | None = None + operation_ref: str | None = None + operation_state: ParticipantLifecycleOperationState | None = None + state_transition_kind: str | None = None + post_state_digest: str | None = None + joint_action_set_id: str | None = None + realized_order: int | None = None + interaction_class: ParticipantInteractionClass | None = None + interaction_ref: str | None = None + shared_state_refs: tuple[str, ...] = () + action_result: ParticipantActionResult | None = None + attribution_edges: tuple[ParticipantAttributionEdge, ...] = () + outcome_interpretations: tuple[ParticipantOutcomeInterpretationRecord, ...] = () + temporal_contexts: tuple[ParticipantTemporalRuntimeContext, ...] = () + details: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_payload( + cls, + payload: Mapping[str, Any], + ) -> "ParticipantBehaviorHistoryEvent": + if not isinstance(payload, Mapping): + raise TypeError("participant behavior history event must be a mapping") + missing_keys = [ + key + for key in ( + "event_type", + "timestamp", + "participant_address", + "episode_id", + "action_instance_id", + ) + if key not in payload + ] + if missing_keys: + raise ValueError( + "participant behavior history event is missing required fields: " + ", ".join(missing_keys) + ) + return cls( + event_type=_participant_behavior_event_type_from_payload(payload.get("event_type")), + timestamp=str(payload.get("timestamp")), + participant_address=str(payload.get("participant_address")), + episode_id=str(payload.get("episode_id")), + action_instance_id=str(payload.get("action_instance_id")), + action_contract_address=_optional_payload_string(payload, "action_contract_address"), + observation_boundary_address=_optional_payload_string(payload, "observation_boundary_address"), + observation_status=_participant_observation_status_from_payload(payload.get("observation_status")), + actor_provenance=_optional_payload_string(payload, "actor_provenance"), + lifecycle_phase=_participant_lifecycle_phase_from_payload(payload.get("lifecycle_phase")), + phase_realization=_participant_phase_realization_from_payload(payload.get("phase_realization")), + admission_disposition=_participant_admission_disposition_from_payload(payload.get("admission_disposition")), + operation_ref=_optional_payload_string(payload, "operation_ref"), + operation_state=_participant_lifecycle_operation_state_from_payload(payload.get("operation_state")), + state_transition_kind=_optional_payload_string(payload, "state_transition_kind"), + post_state_digest=_optional_payload_string(payload, "post_state_digest"), + joint_action_set_id=_optional_payload_string(payload, "joint_action_set_id"), + realized_order=payload.get("realized_order"), + interaction_class=_participant_interaction_class_from_payload(payload.get("interaction_class")), + interaction_ref=_optional_payload_string(payload, "interaction_ref"), + shared_state_refs=_participant_behavior_shared_state_refs_from_payload( + payload.get("shared_state_refs", ()) + ), + action_result=_participant_action_result_from_payload(payload.get("action_result")), + attribution_edges=_participant_attribution_edges_from_payload(payload.get("attribution_edges", ())), + outcome_interpretations=_participant_outcome_interpretation_records_from_payload( + payload.get("outcome_interpretations", ()) + ), + temporal_contexts=_participant_temporal_contexts_from_payload(payload.get("temporal_contexts", ())), + details=_participant_behavior_details_from_payload(payload.get("details", {})), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "event_type": self.event_type.value, + "timestamp": self.timestamp, + "participant_address": self.participant_address, + "episode_id": self.episode_id, + "action_instance_id": self.action_instance_id, + "action_contract_address": self.action_contract_address, + "observation_boundary_address": self.observation_boundary_address, + "observation_status": _optional_enum_value(self.observation_status), + "actor_provenance": self.actor_provenance, + "lifecycle_phase": _optional_enum_value(self.lifecycle_phase), + "phase_realization": _optional_enum_value(self.phase_realization), + "admission_disposition": _optional_enum_value(self.admission_disposition), + "operation_ref": self.operation_ref, + "operation_state": _optional_enum_value(self.operation_state), + "state_transition_kind": self.state_transition_kind, + "post_state_digest": self.post_state_digest, + "joint_action_set_id": self.joint_action_set_id, + "realized_order": self.realized_order, + "interaction_class": _optional_enum_value(self.interaction_class), + "interaction_ref": self.interaction_ref, + "shared_state_refs": list(self.shared_state_refs), + "action_result": self.action_result.to_payload() if self.action_result is not None else None, + "attribution_edges": [edge.to_payload() for edge in self.attribution_edges], + "outcome_interpretations": [record.to_payload() for record in self.outcome_interpretations], + "temporal_contexts": [context.to_payload() for context in self.temporal_contexts], + "details": dict(self.details), + } + + def __post_init__(self) -> None: + self._validate_common_fields() + self._validate_event_type_fields() + + def _validate_common_fields(self) -> None: + if not isinstance(self.event_type, ParticipantBehaviorHistoryEventType): + raise TypeError("event_type must be a ParticipantBehaviorHistoryEventType") + self._validate_required_string(self.timestamp, "participant behavior timestamp must be a non-empty string") + self._validate_required_string( + self.participant_address, + "participant behavior participant_address must be a non-empty string", + ) + self._validate_required_string(self.episode_id, "participant behavior episode_id must be a non-empty string") + self._validate_required_string(self.action_instance_id, "action_instance_id must be a non-empty string") + self._validate_optional_address( + self.action_contract_address, + prefix=_PARTICIPANT_ACTION_CONTRACT_PREFIX, + message="action_contract_address must be a compiled participant action contract address", + ) + self._validate_optional_address( + self.observation_boundary_address, + prefix=_PARTICIPANT_OBSERVATION_BOUNDARY_PREFIX, + message="observation_boundary_address must be a compiled participant observation boundary address", + ) + if self.observation_status is not None and not isinstance( + self.observation_status, + ParticipantObservationStatus, + ): + raise TypeError("observation_status must be a ParticipantObservationStatus or None") + self._validate_optional_string(self.actor_provenance, "actor_provenance must be a non-empty string or None") + self._validate_lifecycle_fields() + self._validate_optional_state_fields() + self._validate_realized_order() + self._validate_interaction_type() + if not isinstance(self.shared_state_refs, tuple): + raise TypeError("shared_state_refs must be a tuple") + for ref in self.shared_state_refs: + self._validate_required_string(ref, "shared_state_refs entries must be non-empty strings") + if len(set(self.shared_state_refs)) != len(self.shared_state_refs): + raise ValueError("shared_state_refs entries must be unique") + self._validate_interaction_fields() + self._validate_action_result_type() + self._validate_attribution_edge_types() + self._validate_outcome_interpretation_types() + self._validate_temporal_context_types() + if not isinstance(self.details, dict): + raise TypeError("participant behavior details must be a dict") + + def _validate_optional_state_fields(self) -> None: + self._validate_optional_string( + self.state_transition_kind, + "state_transition_kind must be a non-empty string or None", + ) + self._validate_optional_string(self.post_state_digest, "post_state_digest must be a non-empty string or None") + self._validate_optional_string( + self.joint_action_set_id, + "joint_action_set_id must be a non-empty string or None", + ) + + def _validate_lifecycle_fields(self) -> None: + self._validate_lifecycle_enum_types() + self._validate_optional_string(self.operation_ref, "operation_ref must be a non-empty string or None") + messages = participant_lifecycle_field_violation_messages( + event_type=self.event_type, + lifecycle_phase=self.lifecycle_phase, + phase_realization=self.phase_realization, + admission_disposition=self.admission_disposition, + operation_ref=self.operation_ref, + operation_state=self.operation_state, + ) + if messages: + raise ValueError(messages[0]) + + def _validate_lifecycle_enum_types(self) -> None: + expectations = ( + ( + self.lifecycle_phase, + ParticipantRuntimeLifecyclePhase, + "lifecycle_phase must be a ParticipantRuntimeLifecyclePhase or None", + ), + ( + self.phase_realization, + ParticipantPhaseRealization, + "phase_realization must be a ParticipantPhaseRealization or None", + ), + ( + self.admission_disposition, + ParticipantAdmissionDisposition, + "admission_disposition must be a ParticipantAdmissionDisposition or None", + ), + ( + self.operation_state, + ParticipantLifecycleOperationState, + "operation_state must be a ParticipantLifecycleOperationState or None", + ), + ) + for value, enum_type, message in expectations: + if value is not None and not isinstance(value, enum_type): + raise TypeError(message) + + def _validate_realized_order(self) -> None: + if self.realized_order is not None and ( + not isinstance(self.realized_order, int) or isinstance(self.realized_order, bool) or self.realized_order < 0 + ): + raise TypeError("realized_order must be a non-negative integer or None") + + def _validate_interaction_type(self) -> None: + if self.interaction_class is not None and not isinstance(self.interaction_class, ParticipantInteractionClass): + raise TypeError("interaction_class must be a ParticipantInteractionClass or None") + self._validate_optional_string(self.interaction_ref, "interaction_ref must be a non-empty string or None") + + def _validate_action_result_type(self) -> None: + if self.action_result is not None and not isinstance(self.action_result, ParticipantActionResult): + raise TypeError("action_result must be a ParticipantActionResult or None") + + def _validate_attribution_edge_types(self) -> None: + if not isinstance(self.attribution_edges, tuple): + raise TypeError("attribution_edges must be a tuple") + if any(not isinstance(edge, ParticipantAttributionEdge) for edge in self.attribution_edges): + raise TypeError("attribution_edges must contain ParticipantAttributionEdge values") + if len({edge.edge_id for edge in self.attribution_edges}) != len(self.attribution_edges): + raise ValueError("participant attribution edge_id values must be unique per event") + if self.attribution_edges and self.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("participant attribution edges are only allowed on observation_emitted events") + + def _validate_outcome_interpretation_types(self) -> None: + if not isinstance(self.outcome_interpretations, tuple): + raise TypeError("outcome_interpretations must be a tuple") + if any( + not isinstance(record, ParticipantOutcomeInterpretationRecord) for record in self.outcome_interpretations + ): + raise TypeError("outcome_interpretations must contain ParticipantOutcomeInterpretationRecord values") + if len({record.interpretation_id for record in self.outcome_interpretations}) != len( + self.outcome_interpretations + ): + raise ValueError("participant outcome interpretation_id values must be unique per event") + if self.outcome_interpretations and self.event_type != ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: + raise ValueError("participant outcome interpretations are only allowed on observation_emitted events") + + def _validate_temporal_context_types(self) -> None: + if not isinstance(self.temporal_contexts, tuple): + raise TypeError("temporal_contexts must be a tuple") + if any(not isinstance(context, ParticipantTemporalRuntimeContext) for context in self.temporal_contexts): + raise TypeError("temporal_contexts must contain ParticipantTemporalRuntimeContext values") + if len({context.temporal_contract_id for context in self.temporal_contexts}) != len(self.temporal_contexts): + raise ValueError("participant temporal_contract_id values must be unique per event") + + @staticmethod + def _validate_required_string(value: object, message: str) -> None: + if not isinstance(value, str) or not value: + raise TypeError(message) + + @staticmethod + def _validate_optional_string(value: object, message: str) -> None: + if value is not None and (not isinstance(value, str) or not value): + raise TypeError(message) + + @staticmethod + def _validate_optional_address(value: str | None, *, prefix: str, message: str) -> None: + if value is not None and (not isinstance(value, str) or not value.startswith(prefix)): + raise ValueError(message) + + def _validate_event_type_fields(self) -> None: + validators = { + ParticipantBehaviorHistoryEventType.ACTION_ATTEMPTED: self._validate_action_attempted_fields, + ParticipantBehaviorHistoryEventType.STATE_TRANSITION_RECORDED: self._validate_state_transition_fields, + ParticipantBehaviorHistoryEventType.OBSERVATION_EMITTED: self._validate_observation_emitted_fields, + } + validators[self.event_type]() + + def _validate_interaction_fields(self) -> None: + self._validate_joint_action_pairing(self.joint_action_set_id, self.realized_order) + self._validate_interaction_class_consistency( + self.interaction_class, + self.interaction_ref, + self.joint_action_set_id, + self.shared_state_refs, + ) + + @staticmethod + def _validate_joint_action_pairing(joint_action_set_id: str | None, realized_order: int | None) -> None: + if joint_action_set_id is None and realized_order is not None: + raise ValueError("realized_order requires joint_action_set_id") + if joint_action_set_id is not None and realized_order is None: + raise ValueError("joint_action_set_id requires realized_order") + + @staticmethod + def _validate_interaction_class_consistency( + interaction_class: ParticipantInteractionClass | None, + interaction_ref: str | None, + joint_action_set_id: str | None, + shared_state_refs: tuple[str, ...], + ) -> None: + if interaction_class is None: + if interaction_ref is not None: + raise ValueError("interaction_ref requires interaction_class") + return + if joint_action_set_id is None: + raise ValueError("interaction_class requires joint_action_set_id and realized_order") + if ( + interaction_class + in { + ParticipantInteractionClass.COORDINATION, + ParticipantInteractionClass.INTERFERENCE, + } + and interaction_ref is None + ): + raise ValueError(f"{interaction_class.value} events require interaction_ref") + if ( + interaction_class + in { + ParticipantInteractionClass.CONTENTION, + ParticipantInteractionClass.SHARED_STATE_CHANGE, + } + and not shared_state_refs + ): + raise ValueError(f"{interaction_class.value} events require shared_state_refs") + + def _validate_action_attempted_fields(self) -> None: + if self.action_contract_address is None: + raise ValueError("action_attempted events require action_contract_address") + if self.actor_provenance is None: + raise ValueError("action_attempted events require actor_provenance") + if self.observation_boundary_address is not None or self.observation_status is not None: + raise ValueError("action_attempted events may not report observation fields") + if self.state_transition_kind is not None or self.post_state_digest is not None: + raise ValueError("action_attempted events may not report state-transition fields") + if self.action_result is not None: + raise ValueError("action_attempted events may not report action_result") + + def _validate_state_transition_fields(self) -> None: + if self.action_contract_address is None: + raise ValueError("state_transition_recorded events require action_contract_address") + if self.state_transition_kind is None: + raise ValueError("state_transition_recorded events require state_transition_kind") + if self.post_state_digest is None: + raise ValueError("state_transition_recorded events require post_state_digest") + if self.observation_boundary_address is not None or self.observation_status is not None: + raise ValueError("state_transition_recorded events may not report observation fields") + if self.action_result is not None: + raise ValueError("state_transition_recorded events may not report action_result") + + def _validate_observation_emitted_fields(self) -> None: + if self.action_contract_address is None: + raise ValueError("observation_emitted events require action_contract_address") + if self.observation_boundary_address is None: + raise ValueError("observation_emitted events require observation_boundary_address") + if self.observation_status is None: + raise ValueError("observation_emitted events require observation_status") + if self.observation_status == ParticipantObservationStatus.TERMINAL and self.post_state_digest is None: + raise ValueError("terminal observation_emitted events require post_state_digest") + if self.state_transition_kind is not None: + raise ValueError("observation_emitted events may not report state_transition_kind") + if self.action_result is not None: + self._validate_action_result_scope() + self._validate_attribution_edges() + self._validate_outcome_interpretations() + + def _validate_action_result_scope(self) -> None: + if self.action_result is None: + return + if self.action_result.participant_address != self.participant_address: + raise ValueError("action_result participant_address must match event participant_address") + if self.action_result.episode_id != self.episode_id: + raise ValueError("action_result episode_id must match event episode_id") + if self.action_result.action_instance_id != self.action_instance_id: + raise ValueError("action_result action_instance_id must match event action_instance_id") + if self.action_result.action_contract_address != self.action_contract_address: + raise ValueError("action_result action_contract_address must match event action_contract_address") + if ( + self.observation_status in _PARTICIPANT_TERMINAL_OBSERVATION_STATUSES + and self.action_result.status == ParticipantActionResultStatus.ACCEPTED + ): + raise ValueError("terminal observation action_result must report a terminal status") + + def _validate_attribution_edges(self) -> None: + for edge in self.attribution_edges: + if edge.participant_address != self.participant_address: + raise ValueError("attribution edge participant_address must match event participant_address") + if edge.episode_id != self.episode_id: + raise ValueError("attribution edge episode_id must match event episode_id") + if self.action_result is not None: + if edge.observation_point != self.action_result.observation_point: + raise ValueError("attribution edge observation_point must match action_result observation_point") + elif not _observation_point_matches_action_instance(edge.observation_point, self.action_instance_id): + raise ValueError("attribution edge observation_point must be anchored to action_instance_id") + self._validate_attribution_candidate_grounding(edge) + + def _validate_outcome_interpretations(self) -> None: + for record in self.outcome_interpretations: + if record.participant_address != self.participant_address: + raise ValueError("outcome interpretation participant_address must match event participant_address") + if record.episode_id != self.episode_id: + raise ValueError("outcome interpretation episode_id must match event episode_id") + if self.action_result is not None: + if record.observation_point != self.action_result.observation_point: + raise ValueError( + "outcome interpretation observation_point must match action_result observation_point" + ) + elif not _observation_point_matches_action_instance(record.observation_point, self.action_instance_id): + raise ValueError("outcome interpretation observation_point must be anchored to action_instance_id") + + def _validate_attribution_candidate_grounding(self, edge: ParticipantAttributionEdge) -> None: + allowed_action_refs = {self.action_instance_id} + if self.action_contract_address is not None: + allowed_action_refs.add(self.action_contract_address) + if edge.cause_candidate.candidate_kind == ParticipantAttributionCandidateKind.ACTION: + if edge.cause_candidate.ref not in allowed_action_refs: + raise ValueError("attribution edge action cause_candidate must match the event action") + elif edge.cause_candidate.ref not in self._attribution_grounded_refs(): + raise ValueError(f"attribution edge cause_candidate {edge.cause_candidate.ref!r} is not grounded") + + if edge.effect_candidate.candidate_kind in OUTCOME_ATTRIBUTION_CANDIDATE_KINDS: + return + if edge.effect_candidate.ref not in self._attribution_grounded_refs(): + raise ValueError(f"attribution edge effect_candidate {edge.effect_candidate.ref!r} is not grounded") + + def _attribution_grounded_refs(self) -> set[str]: + return _event_attribution_grounded_refs(self) diff --git a/implementations/python/packages/aces_processor/models/history_event_grounding.py b/implementations/python/packages/aces_processor/models/history_event_grounding.py new file mode 100644 index 000000000..3e8ba4e43 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/history_event_grounding.py @@ -0,0 +1,50 @@ +"""Payload projection and grounding-ref helpers for participant-behavior history events.""" + +from typing import TYPE_CHECKING, Any + +from .action_results import ParticipantActionResult +from .behavior_resources import _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS + +if TYPE_CHECKING: + from .history_event import ParticipantBehaviorHistoryEvent + + +def _optional_enum_value(value: object) -> str | None: + return value.value if value is not None else None + + +def _participant_observation_detail_grounded_refs(details: dict[str, Any]) -> set[str]: + refs: set[str] = set() + for key in _PARTICIPANT_OBSERVATION_DETAIL_REF_KEYS: + value = details.get(key) + if isinstance(value, (list, tuple)): + refs.update(str(item) for item in value if isinstance(item, str) and item) + return refs + + +def _participant_action_result_grounded_refs(action_result: ParticipantActionResult) -> set[str]: + refs: set[str] = set() + refs.update(action_result.observations) + refs.update(action_result.evidence_refs) + for precondition in action_result.preconditions: + refs.update(precondition.support_refs) + refs.update(precondition.evidence_refs) + for effect in action_result.effects: + refs.update(effect.target_refs) + refs.update(effect.evidence_refs) + return refs + + +def _event_attribution_grounded_refs(event: "ParticipantBehaviorHistoryEvent") -> set[str]: + refs: set[str] = {event.action_instance_id} + if event.action_contract_address is not None: + refs.add(event.action_contract_address) + if event.observation_boundary_address is not None: + refs.add(event.observation_boundary_address) + if event.post_state_digest is not None: + refs.add(event.post_state_digest) + refs.update(_participant_observation_detail_grounded_refs(event.details)) + if event.action_result is None: + return refs + refs.update(_participant_action_result_grounded_refs(event.action_result)) + return refs diff --git a/implementations/python/packages/aces_processor/models/history_event_payloads.py b/implementations/python/packages/aces_processor/models/history_event_payloads.py new file mode 100644 index 000000000..fec1fd34f --- /dev/null +++ b/implementations/python/packages/aces_processor/models/history_event_payloads.py @@ -0,0 +1,124 @@ +"""Payload decoding helpers for participant-behavior history events.""" + +from collections.abc import Iterable, Mapping +from typing import Any + +from aces_contracts.participant_behavior import ( + ParticipantAdmissionDisposition, + ParticipantBehaviorHistoryEventType, + ParticipantLifecycleOperationState, + ParticipantPhaseRealization, + ParticipantRuntimeLifecyclePhase, +) +from aces_sdl.participant_behavior import ParticipantInteractionClass + +from .action_results import ParticipantActionResult +from .attribution import ParticipantAttributionEdge +from .temporal import ParticipantTemporalRuntimeContext + + +def _participant_behavior_event_type_from_payload(value: object) -> ParticipantBehaviorHistoryEventType: + if isinstance(value, ParticipantBehaviorHistoryEventType): + return value + return ParticipantBehaviorHistoryEventType(str(value)) + + +def _participant_interaction_class_from_payload(value: object) -> ParticipantInteractionClass | None: + if value is None: + return None + if isinstance(value, ParticipantInteractionClass): + return value + return ParticipantInteractionClass(str(value)) + + +def _participant_lifecycle_phase_from_payload( + value: str | ParticipantRuntimeLifecyclePhase | None, +) -> ParticipantRuntimeLifecyclePhase | None: + if value is None: + return None + if isinstance(value, ParticipantRuntimeLifecyclePhase): + return value + return ParticipantRuntimeLifecyclePhase(str(value)) + + +def _participant_phase_realization_from_payload( + value: str | ParticipantPhaseRealization | None, +) -> ParticipantPhaseRealization | None: + if value is None: + return None + if isinstance(value, ParticipantPhaseRealization): + return value + return ParticipantPhaseRealization(str(value)) + + +def _participant_admission_disposition_from_payload( + value: str | ParticipantAdmissionDisposition | None, +) -> ParticipantAdmissionDisposition | None: + if value is None: + return None + if isinstance(value, ParticipantAdmissionDisposition): + return value + return ParticipantAdmissionDisposition(str(value)) + + +def _participant_lifecycle_operation_state_from_payload( + value: str | ParticipantLifecycleOperationState | None, +) -> ParticipantLifecycleOperationState | None: + if value is None: + return None + if isinstance(value, ParticipantLifecycleOperationState): + return value + return ParticipantLifecycleOperationState(str(value)) + + +def _participant_behavior_shared_state_refs_from_payload(value: object) -> tuple[str, ...]: + if value is None: + return () + if not isinstance(value, (list, tuple)): + raise TypeError("shared_state_refs must be a list of strings") + return tuple(str(ref) for ref in value) + + +def _participant_action_result_from_payload(value: object) -> ParticipantActionResult | None: + if value is None: + return None + if isinstance(value, ParticipantActionResult): + return value + return ParticipantActionResult.from_payload(value) + + +def _participant_attribution_edges_from_payload(value: object) -> tuple[ParticipantAttributionEdge, ...]: + if value is None: + return () + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("attribution_edges must be a list of participant attribution edges") + return tuple( + edge if isinstance(edge, ParticipantAttributionEdge) else ParticipantAttributionEdge.from_payload(edge) + for edge in value + ) + + +def _participant_temporal_contexts_from_payload(value: object) -> tuple[ParticipantTemporalRuntimeContext, ...]: + if value is None: + return () + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("temporal_contexts must be a list of participant temporal runtime contexts") + return tuple( + context + if isinstance(context, ParticipantTemporalRuntimeContext) + else ParticipantTemporalRuntimeContext.from_payload(context) + for context in value + ) + + +def _participant_behavior_details_from_payload(value: object) -> dict[str, Any]: + if value is None: + value = {} + if not isinstance(value, Mapping): + raise TypeError("participant behavior details must be a mapping") + details = dict(value) + for empty_ref_key in ("visible_refs", "disclosed_refs", "evidence_refs"): + refs = details.get(empty_ref_key) + if isinstance(refs, (list, tuple)) and not refs: + details.pop(empty_ref_key) + return details diff --git a/implementations/python/packages/aces_processor/models/outcome.py b/implementations/python/packages/aces_processor/models/outcome.py new file mode 100644 index 000000000..ac79b1140 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/outcome.py @@ -0,0 +1,293 @@ +"""Participant outcome source/target/interpretation records.""" + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from typing import Any + +from aces_sdl.participant_outcome_semantics import OutcomeInterpretationSourceLayer, OutcomeInterpretationTargetLayer + +from .behavior_resources import ( + _optional_payload_string, + _tuple_of_non_empty_strings, + _validate_optional_string, + _validate_required_address, + _validate_required_string, +) +from .resources import _PARTICIPANT_OUTCOME_RULE_PREFIX + + +def _outcome_source_layer_from_payload(value: object) -> OutcomeInterpretationSourceLayer: + if isinstance(value, OutcomeInterpretationSourceLayer): + return value + return OutcomeInterpretationSourceLayer(str(value)) + + +def _outcome_target_layer_from_payload(value: object) -> OutcomeInterpretationTargetLayer: + if isinstance(value, OutcomeInterpretationTargetLayer): + return value + return OutcomeInterpretationTargetLayer(str(value)) + + +@dataclass(frozen=True) +class ParticipantOutcomeSourceRecord: + """Runtime source observed for a SEM-215 outcome interpretation.""" + + source_id: str + source_layer: OutcomeInterpretationSourceLayer + ref: str + observed_value: str + evidence_refs: tuple[str, ...] = () + provenance_refs: tuple[str, ...] = () + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeSourceRecord": + if not isinstance(payload, Mapping): + raise TypeError("participant outcome source record must be a mapping") + missing = [key for key in ("source_id", "source_layer", "ref", "observed_value") if key not in payload] + if missing: + raise ValueError("participant outcome source record is missing required fields: " + ", ".join(missing)) + return cls( + source_id=str(payload.get("source_id")), + source_layer=_outcome_source_layer_from_payload(payload.get("source_layer")), + ref=str(payload.get("ref")), + observed_value=str(payload.get("observed_value")), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs", ()), field_name="evidence_refs"), + provenance_refs=_tuple_of_non_empty_strings( + payload.get("provenance_refs", ()), + field_name="provenance_refs", + ), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "source_id": self.source_id, + "source_layer": self.source_layer.value, + "ref": self.ref, + "observed_value": self.observed_value, + "evidence_refs": list(self.evidence_refs), + "provenance_refs": list(self.provenance_refs), + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + _validate_required_string(self.source_id, "participant outcome source_id must be a non-empty string") + if not isinstance(self.source_layer, OutcomeInterpretationSourceLayer): + raise TypeError("source_layer must be an OutcomeInterpretationSourceLayer") + _validate_required_string(self.ref, "participant outcome source ref must be a non-empty string") + _validate_required_string( + self.observed_value, + "participant outcome source observed_value must be a non-empty string", + ) + _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + _tuple_of_non_empty_strings(self.provenance_refs, field_name="provenance_refs") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + + +@dataclass(frozen=True) +class ParticipantOutcomeTargetRecord: + """Runtime target interpretation produced by a SEM-215 rule.""" + + target_id: str + target_layer: OutcomeInterpretationTargetLayer + ref: str + interpreted_value: str + evidence_refs: tuple[str, ...] + limitations: tuple[str, ...] + governance_ref: str | None = None + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeTargetRecord": + if not isinstance(payload, Mapping): + raise TypeError("participant outcome target record must be a mapping") + missing = [ + key + for key in ( + "target_id", + "target_layer", + "ref", + "interpreted_value", + "evidence_refs", + "limitations", + ) + if key not in payload + ] + if missing: + raise ValueError("participant outcome target record is missing required fields: " + ", ".join(missing)) + return cls( + target_id=str(payload.get("target_id")), + target_layer=_outcome_target_layer_from_payload(payload.get("target_layer")), + ref=str(payload.get("ref")), + interpreted_value=str(payload.get("interpreted_value")), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), + limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), + governance_ref=_optional_payload_string(payload, "governance_ref"), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "target_id": self.target_id, + "target_layer": self.target_layer.value, + "ref": self.ref, + "interpreted_value": self.interpreted_value, + "evidence_refs": list(self.evidence_refs), + "limitations": list(self.limitations), + "governance_ref": self.governance_ref, + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + _validate_required_string(self.target_id, "participant outcome target_id must be a non-empty string") + if not isinstance(self.target_layer, OutcomeInterpretationTargetLayer): + raise TypeError("target_layer must be an OutcomeInterpretationTargetLayer") + _validate_required_string(self.ref, "participant outcome target ref must be a non-empty string") + _validate_required_string( + self.interpreted_value, + "participant outcome target interpreted_value must be a non-empty string", + ) + if not _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs"): + raise ValueError("participant outcome targets require evidence_refs") + if not _tuple_of_non_empty_strings(self.limitations, field_name="limitations"): + raise ValueError("participant outcome targets require limitations") + _validate_optional_string(self.governance_ref, "governance_ref must be a non-empty string or None") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + if self.target_layer == OutcomeInterpretationTargetLayer.REWARD_SIGNAL and self.governance_ref is None: + raise ValueError("reward_signal outcome targets require governance_ref") + + +def _participant_outcome_source_records_from_payload(value: object) -> tuple[ParticipantOutcomeSourceRecord, ...]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("outcome source_bindings must be a list of source records") + return tuple(ParticipantOutcomeSourceRecord.from_payload(item) for item in value) + + +def _participant_outcome_target_records_from_payload(value: object) -> tuple[ParticipantOutcomeTargetRecord, ...]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("outcome target_bindings must be a list of target records") + return tuple(ParticipantOutcomeTargetRecord.from_payload(item) for item in value) + + +def _validate_outcome_source_bindings(source_bindings: tuple[ParticipantOutcomeSourceRecord, ...]) -> None: + if not isinstance(source_bindings, tuple): + raise TypeError("source_bindings must be a tuple") + if not source_bindings: + raise ValueError("participant outcome interpretations require source_bindings") + if any(not isinstance(source, ParticipantOutcomeSourceRecord) for source in source_bindings): + raise TypeError("source_bindings must contain ParticipantOutcomeSourceRecord values") + if len({source.source_id for source in source_bindings}) != len(source_bindings): + raise ValueError("participant outcome source_id values must be unique") + + +def _validate_outcome_target_bindings(target_bindings: tuple[ParticipantOutcomeTargetRecord, ...]) -> None: + if not isinstance(target_bindings, tuple): + raise TypeError("target_bindings must be a tuple") + if not target_bindings: + raise ValueError("participant outcome interpretations require target_bindings") + if any(not isinstance(target, ParticipantOutcomeTargetRecord) for target in target_bindings): + raise TypeError("target_bindings must contain ParticipantOutcomeTargetRecord values") + if len({target.target_id for target in target_bindings}) != len(target_bindings): + raise ValueError("participant outcome target_id values must be unique") + + +@dataclass(frozen=True) +class ParticipantOutcomeInterpretationRecord: + """Provenance-bearing SEM-215 interpretation of participant-local outcomes.""" + + interpretation_id: str + rule_address: str + participant_address: str + episode_id: str + observation_point: str + source_bindings: tuple[ParticipantOutcomeSourceRecord, ...] + target_bindings: tuple[ParticipantOutcomeTargetRecord, ...] + evidence_refs: tuple[str, ...] + limitations: tuple[str, ...] + diagnostics: tuple[str, ...] = () + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantOutcomeInterpretationRecord": + if not isinstance(payload, Mapping): + raise TypeError("participant outcome interpretation record must be a mapping") + missing = [ + key + for key in ( + "interpretation_id", + "rule_address", + "participant_address", + "episode_id", + "observation_point", + "source_bindings", + "target_bindings", + "evidence_refs", + "limitations", + ) + if key not in payload + ] + if missing: + raise ValueError( + "participant outcome interpretation record is missing required fields: " + ", ".join(missing) + ) + return cls( + interpretation_id=str(payload.get("interpretation_id")), + rule_address=str(payload.get("rule_address")), + participant_address=str(payload.get("participant_address")), + episode_id=str(payload.get("episode_id")), + observation_point=str(payload.get("observation_point")), + source_bindings=_participant_outcome_source_records_from_payload(payload.get("source_bindings")), + target_bindings=_participant_outcome_target_records_from_payload(payload.get("target_bindings")), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), + limitations=_tuple_of_non_empty_strings(payload.get("limitations"), field_name="limitations"), + diagnostics=_tuple_of_non_empty_strings(payload.get("diagnostics", ()), field_name="diagnostics"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "interpretation_id": self.interpretation_id, + "rule_address": self.rule_address, + "participant_address": self.participant_address, + "episode_id": self.episode_id, + "observation_point": self.observation_point, + "source_bindings": [source.to_payload() for source in self.source_bindings], + "target_bindings": [target.to_payload() for target in self.target_bindings], + "evidence_refs": list(self.evidence_refs), + "limitations": list(self.limitations), + "diagnostics": list(self.diagnostics), + } + + def __post_init__(self) -> None: + _validate_required_string( + self.interpretation_id, + "participant outcome interpretation_id must be a non-empty string", + ) + _validate_required_address( + self.rule_address, + prefix=_PARTICIPANT_OUTCOME_RULE_PREFIX, + message="rule_address must be a compiled participant outcome interpretation rule address", + ) + _validate_required_string( + self.participant_address, + "participant outcome participant_address must be a non-empty string", + ) + _validate_required_string(self.episode_id, "participant outcome episode_id must be a non-empty string") + _validate_required_string( + self.observation_point, + "participant outcome observation_point must be a non-empty string", + ) + _validate_outcome_source_bindings(self.source_bindings) + _validate_outcome_target_bindings(self.target_bindings) + if not _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs"): + raise ValueError("participant outcome interpretations require evidence_refs") + if not _tuple_of_non_empty_strings(self.limitations, field_name="limitations"): + raise ValueError("participant outcome interpretations require limitations") + _tuple_of_non_empty_strings(self.diagnostics, field_name="diagnostics") + + +def _participant_outcome_interpretation_records_from_payload( + value: object, +) -> tuple[ParticipantOutcomeInterpretationRecord, ...]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("outcome_interpretations must be a list of interpretation records") + return tuple(ParticipantOutcomeInterpretationRecord.from_payload(item) for item in value) diff --git a/implementations/python/packages/aces_processor/models/outcome_interpretation_validation.py b/implementations/python/packages/aces_processor/models/outcome_interpretation_validation.py new file mode 100644 index 000000000..456fd3643 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/outcome_interpretation_validation.py @@ -0,0 +1,172 @@ +"""SEM-215 outcome-interpretation rule-conformance validation.""" + +from collections.abc import Iterable, Mapping + +from aces_sdl.participant_outcome_semantics import ( + PROVENANCE_REQUIRED_OUTCOME_SOURCE_LAYERS, + OutcomeInterpretationSourceLayer, +) + +from .behavior_resources import ParticipantOutcomeInterpretationRuleRuntime +from .outcome import ParticipantOutcomeInterpretationRecord +from .resources import _as_string_set + + +def _contract_sem215_source_bindings( + rule: ParticipantOutcomeInterpretationRuleRuntime, +) -> dict[tuple[str, str], dict[str, str | set[str]]]: + bindings = rule.spec.get("source_bindings", ()) + if isinstance(bindings, (str, bytes, Mapping)) or not isinstance(bindings, Iterable): + return {} + declarations: dict[tuple[str, str], dict[str, str | set[str]]] = {} + for index, binding in enumerate(bindings): + if not isinstance(binding, Mapping) or not binding.get("source_id") or not binding.get("source_layer"): + continue + declarations[(str(binding.get("source_id")), str(binding.get("source_layer")))] = { + "ref": rule.source_refs[index] if index < len(rule.source_refs) else str(binding.get("ref", "")), + "evidence_refs": _as_string_set(binding.get("evidence_refs", ())), + "provenance_refs": _as_string_set(binding.get("provenance_refs", ())), + } + return declarations + + +def _contract_sem215_target_bindings( + rule: ParticipantOutcomeInterpretationRuleRuntime, +) -> dict[tuple[str, str], dict[str, str | set[str] | None]]: + bindings = rule.spec.get("target_bindings", ()) + if isinstance(bindings, (str, bytes, Mapping)) or not isinstance(bindings, Iterable): + return {} + declarations: dict[tuple[str, str], dict[str, str | set[str] | None]] = {} + for index, binding in enumerate(bindings): + if not isinstance(binding, Mapping) or not binding.get("target_id") or not binding.get("target_layer"): + continue + governance_ref = binding.get("governance_ref") + declarations[(str(binding.get("target_id")), str(binding.get("target_layer")))] = { + "ref": rule.target_refs[index] if index < len(rule.target_refs) else str(binding.get("ref", "")), + "governance_ref": str(governance_ref) if governance_ref is not None else None, + "evidence_refs": _as_string_set(binding.get("evidence_refs", ())), + "limitations": _as_string_set(binding.get("limitations", ())), + } + return declarations + + +def _outcome_source_layer_requires_provenance(layer: str) -> bool: + try: + source_layer = OutcomeInterpretationSourceLayer(layer) + except ValueError: + return False + return source_layer in PROVENANCE_REQUIRED_OUTCOME_SOURCE_LAYERS + + +def _sem215_unreported_source_violations( + record: ParticipantOutcomeInterpretationRecord, + declared_sources: Iterable[tuple[str, str]], + reported_sources: set[tuple[str, str]], +) -> list[str]: + violations: list[str] = [] + for source_id, source_layer in sorted(declared_sources): + if not _outcome_source_layer_requires_provenance(source_layer): + continue + if (source_id, source_layer) not in reported_sources: + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source_id!r} " + f"with provenance-required layer {source_layer!r} is not reported" + ) + return violations + + +def _sem215_source_binding_violations( + record: ParticipantOutcomeInterpretationRecord, + rule: ParticipantOutcomeInterpretationRuleRuntime, +) -> list[str]: + violations: list[str] = [] + declared_sources = _contract_sem215_source_bindings(rule) + reported_sources: set[tuple[str, str]] = set() + for source in record.source_bindings: + source_key = (source.source_id, source.source_layer.value) + reported_sources.add(source_key) + if source_key not in declared_sources: + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"is not declared by {rule.address}" + ) + continue + declared_refs = declared_sources[source_key] + if source.ref != declared_refs["ref"]: + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"ref {source.ref!r} does not match declared ref {declared_refs['ref']!r}" + ) + for ref in sorted(set(source.evidence_refs) - declared_refs["evidence_refs"]): + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"reports undeclared evidence_ref {ref!r}" + ) + for ref in sorted(set(source.provenance_refs) - declared_refs["provenance_refs"]): + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"reports undeclared provenance_ref {ref!r}" + ) + for ref in sorted(declared_refs["provenance_refs"] - set(source.provenance_refs)): + violations.append( + f"outcome interpretation {record.interpretation_id!r} source {source.source_id!r} " + f"omits declared provenance_ref {ref!r}" + ) + violations.extend(_sem215_unreported_source_violations(record, declared_sources, reported_sources)) + return violations + + +def _sem215_target_binding_violations( + record: ParticipantOutcomeInterpretationRecord, + rule: ParticipantOutcomeInterpretationRuleRuntime, +) -> list[str]: + violations: list[str] = [] + declared_targets = _contract_sem215_target_bindings(rule) + for target in record.target_bindings: + target_key = (target.target_id, target.target_layer.value) + if target_key not in declared_targets: + violations.append( + f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " + f"is not declared by {rule.address}" + ) + continue + declared_refs = declared_targets[target_key] + if target.ref != declared_refs["ref"]: + violations.append( + f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " + f"ref {target.ref!r} does not match declared ref {declared_refs['ref']!r}" + ) + if target.governance_ref != declared_refs["governance_ref"]: + violations.append( + f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " + f"governance_ref {target.governance_ref!r} does not match declared governance_ref " + f"{declared_refs['governance_ref']!r}" + ) + for ref in sorted(set(target.evidence_refs) - declared_refs["evidence_refs"]): + violations.append( + f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " + f"reports undeclared evidence_ref {ref!r}" + ) + if declared_refs["limitations"] and not set(target.limitations) <= declared_refs["limitations"]: + violations.append( + f"outcome interpretation {record.interpretation_id!r} target {target.target_id!r} " + "reports limitations outside the declared rule" + ) + return violations + + +def validate_participant_outcome_interpretation_record( + record: ParticipantOutcomeInterpretationRecord, + rule: ParticipantOutcomeInterpretationRuleRuntime, +) -> list[str]: + """Return SEM-215 rule-conformance violations for a runtime interpretation.""" + + violations: list[str] = [] + violations.extend(_sem215_source_binding_violations(record, rule)) + violations.extend(_sem215_target_binding_violations(record, rule)) + declared_rule_evidence_refs = _as_string_set(rule.spec.get("evidence_refs", ())) + for ref in sorted(set(record.evidence_refs) - declared_rule_evidence_refs): + violations.append( + f"outcome interpretation {record.interpretation_id!r} reports undeclared evidence_ref {ref!r}" + ) + return violations diff --git a/implementations/python/packages/aces_processor/models/resources.py b/implementations/python/packages/aces_processor/models/resources.py new file mode 100644 index 000000000..3a9d635bd --- /dev/null +++ b/implementations/python/packages/aces_processor/models/resources.py @@ -0,0 +1,390 @@ +"""Compiled provisioning/resolved-resource records and SEM-211 action-contract validation.""" + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.evaluation import EvaluationExecutionContract, EvaluationResultContract +from aces_contracts.participant_episode import PARTICIPANT_EPISODE_CONTROL_EVENTS, PARTICIPANT_EPISODE_TERMINAL_EVENTS +from aces_sdl.participant_behavior import ParticipantFailureClass + +if TYPE_CHECKING: + # Forward reference only: ParticipantActionResult is defined in the later + # action_results module. The annotation on validate_participant_action_result_contract + # is a string, so no runtime import (and no import cycle) is created. + from .action_results import ParticipantActionResult + +_PARTICIPANT_ACTION_CONTRACT_PREFIX = "participant.action-contract." +_PARTICIPANT_OBSERVATION_BOUNDARY_PREFIX = "participant.observation-boundary." +_PARTICIPANT_OUTCOME_RULE_PREFIX = "participant.outcome-interpretation-rule." +_PARTICIPANT_BEHAVIOR_HISTORY_KEY = "runtime.snapshot.participant-behavior-history" +_PARTICIPANT_EPISODE_CONTROL_EVENTS = PARTICIPANT_EPISODE_CONTROL_EVENTS +_PARTICIPANT_EPISODE_TERMINAL_EVENTS = PARTICIPANT_EPISODE_TERMINAL_EVENTS + + +@dataclass(frozen=True) +class RuntimeTemplate: + """Reusable SDL definition preserved in compiled form.""" + + address: str + name: str + spec: dict[str, Any] + + +@dataclass(frozen=True) +class ResolvedResource: + """Base class for bound runtime resources.""" + + address: str + name: str + spec: dict[str, Any] + ordering_dependencies: tuple[str, ...] = () + refresh_dependencies: tuple[str, ...] = () + + +@dataclass(frozen=True) +class NetworkRuntime(ResolvedResource): + """Compiled switch/network deployment.""" + + node_name: str = "" + + +@dataclass(frozen=True) +class NodeRuntime(ResolvedResource): + """Compiled VM deployment.""" + + node_name: str = "" + node_type: str = "" + os_family: str = "" + count: int | str | None = None + + +@dataclass(frozen=True) +class FeatureBinding(ResolvedResource): + """Feature template bound to a specific node role.""" + + node_name: str = "" + node_address: str = "" + feature_name: str = "" + template_address: str = "" + role_name: str = "" + + +@dataclass(frozen=True) +class PropositionRuntime(ResolvedResource): + """Compiled backend-neutral proposition with resolved finite subjects.""" + + subject_addresses: tuple[str, ...] = () + predicate_kind: str = "" + evaluation_basis: str = "" + evidence_requirement_refs: tuple[str, ...] = () + + +@dataclass(frozen=True) +class AssertionRuntime(ResolvedResource): + """Compiled assertion use over one proposition.""" + + proposition_address: str = "" + role: str = "" + polarity: str = "" + + +@dataclass(frozen=True) +class ConditionBinding(ResolvedResource): + """Condition template bound to a specific node role.""" + + node_name: str = "" + node_address: str = "" + condition_name: str = "" + template_address: str = "" + role_name: str = "" + proposition_address: str = "" + result_contract: "EvaluationResultContract" = field( + default_factory=lambda: EvaluationResultContract(resource_type="condition-binding") + ) + execution_contract: "EvaluationExecutionContract" = field( + default_factory=lambda: EvaluationExecutionContract(resource_type="condition-binding") + ) + + +@dataclass(frozen=True) +class InjectBinding(ResolvedResource): + """Inject template bound to a specific node role.""" + + node_name: str = "" + node_address: str = "" + inject_name: str = "" + template_address: str = "" + role_name: str = "" + + +@dataclass(frozen=True) +class InjectRuntime(ResolvedResource): + """Resolved top-level inject resource.""" + + +@dataclass(frozen=True) +class ContentPlacement(ResolvedResource): + """Content entry resolved to a concrete target node.""" + + content_name: str = "" + target_node: str = "" + target_address: str = "" + + +@dataclass(frozen=True) +class AccountPlacement(ResolvedResource): + """Account entry resolved to a concrete target node.""" + + account_name: str = "" + node_name: str = "" + target_address: str = "" + + +@dataclass(frozen=True) +class ParticipantActionContractRuntime(ResolvedResource): + """Compiled participant action contract.""" + + action_name: str = "" + semantic_version: str = "" + lifecycle_state: str = "" + behavioral_granularity: str = "" + precondition_classes: tuple[str, ...] = () + effect_classes: tuple[str, ...] = () + failure_classes: tuple[str, ...] = () + backend_failure_mappings: tuple[dict[str, str], ...] = () + interaction_classes: tuple[str, ...] = () + shared_state_refs: tuple[str, ...] = () + temporal_contract_ids: tuple[str, ...] = () + temporal_kinds: tuple[str, ...] = () + time_domains: tuple[str, ...] = () + clock_authorities: tuple[str, ...] = () + backend_timing_disclosures: tuple[dict[str, Any], ...] = () + + +def map_backend_diagnostic_to_participant_failure( + diagnostic: Diagnostic | Mapping[str, Any] | str, + contract: ParticipantActionContractRuntime, +) -> ParticipantFailureClass: + """Map a backend diagnostic to a portable SEM-211 failure class.""" + + if isinstance(diagnostic, Diagnostic): + code = diagnostic.code + elif isinstance(diagnostic, Mapping): + code = str(diagnostic.get("code", "")) + else: + code = str(diagnostic) + + for mapping in contract.backend_failure_mappings: + if mapping.get("backend_error_code") == code: + return ParticipantFailureClass(str(mapping.get("failure_class", ParticipantFailureClass.UNKNOWN.value))) + return ParticipantFailureClass.BACKEND_ERROR if code else ParticipantFailureClass.UNKNOWN + + +def _as_string_set(value: object) -> set[str]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + return set() + return {str(item) for item in value if isinstance(item, str) and item} + + +def _contract_sem211_precondition_refs( + contract: ParticipantActionContractRuntime, +) -> dict[tuple[str, str], dict[str, set[str]]]: + preconditions = contract.spec.get("preconditions", ()) + if isinstance(preconditions, (str, bytes, Mapping)) or not isinstance(preconditions, Iterable): + return {} + refs: dict[tuple[str, str], dict[str, set[str]]] = {} + for item in preconditions: + if not isinstance(item, Mapping) or not item.get("precondition_id") or not item.get("precondition_class"): + continue + key = (str(item.get("precondition_id", "")), str(item.get("precondition_class", ""))) + refs[key] = { + "support_refs": _as_string_set(item.get("support_refs", ())), + "evidence_refs": _as_string_set(item.get("evidence_refs", ())), + } + return refs + + +def _contract_sem211_effect_refs( + contract: ParticipantActionContractRuntime, +) -> dict[tuple[str, str], dict[str, set[str]]]: + effects = contract.spec.get("effects", ()) + if isinstance(effects, (str, bytes, Mapping)) or not isinstance(effects, Iterable): + return {} + refs: dict[tuple[str, str], dict[str, set[str]]] = {} + for item in effects: + if not isinstance(item, Mapping) or not item.get("effect_id") or not item.get("effect_class"): + continue + key = (str(item.get("effect_id", "")), str(item.get("effect_class", ""))) + refs[key] = { + "target_refs": _as_string_set(item.get("target_refs", ())), + "evidence_refs": _as_string_set(item.get("evidence_refs", ())), + } + return refs + + +def _contract_uses_sem211_action_results(contract: ParticipantActionContractRuntime) -> bool: + return bool(contract.precondition_classes or contract.effect_classes or contract.failure_classes) + + +_ContractRefMap = dict[tuple[str, str], dict[str, set[str]]] + + +def _sem211_precondition_violations( + result: "ParticipantActionResult", + contract: ParticipantActionContractRuntime, + declared_precondition_classes: set[str], + declared_preconditions: set[tuple[str, str]], + declared_precondition_refs: _ContractRefMap, +) -> list[str]: + violations: list[str] = [] + reported_preconditions: set[tuple[str, str]] = set() + for precondition in result.preconditions: + precondition_key = (precondition.precondition_id, precondition.precondition_class.value) + reported_preconditions.add(precondition_key) + if precondition.precondition_class.value not in declared_precondition_classes: + violations.append( + f"action_result precondition {precondition.precondition_id!r} uses undeclared " + f"precondition_class {precondition.precondition_class.value!r}" + ) + if declared_preconditions and precondition_key not in declared_preconditions: + violations.append( + f"action_result precondition {precondition.precondition_id!r}/" + f"{precondition.precondition_class.value!r} is not declared by {contract.address}" + ) + declared_refs = declared_precondition_refs.get(precondition_key) + if declared_refs is not None: + undeclared_support_refs = set(precondition.support_refs) - declared_refs["support_refs"] + undeclared_evidence_refs = set(precondition.evidence_refs) - declared_refs["evidence_refs"] + for ref in sorted(undeclared_support_refs): + violations.append( + f"action_result precondition {precondition.precondition_id!r} reports undeclared " + f"support_ref {ref!r}" + ) + for ref in sorted(undeclared_evidence_refs): + violations.append( + f"action_result precondition {precondition.precondition_id!r} reports undeclared " + f"evidence_ref {ref!r}" + ) + for precondition_id, precondition_class in sorted(declared_preconditions - reported_preconditions): + violations.append( + f"action_result is missing declared precondition {precondition_id!r}/" + f"{precondition_class!r} for {contract.address}" + ) + return violations + + +def _sem211_effect_violations( + result: "ParticipantActionResult", + contract: ParticipantActionContractRuntime, + declared_effect_classes: set[str], + declared_effects: set[tuple[str, str]], + declared_effect_refs: _ContractRefMap, +) -> list[str]: + violations: list[str] = [] + for effect in result.effects: + effect_key = (effect.effect_id, effect.effect_class.value) + if effect.effect_class.value not in declared_effect_classes: + violations.append( + f"action_result effect {effect.effect_id!r} uses undeclared effect_class {effect.effect_class.value!r}" + ) + if declared_effects and effect_key not in declared_effects: + violations.append( + f"action_result effect {effect.effect_id!r}/" + f"{effect.effect_class.value!r} is not declared by {contract.address}" + ) + declared_refs = declared_effect_refs.get(effect_key) + if declared_refs is not None: + undeclared_target_refs = set(effect.target_refs) - declared_refs["target_refs"] + undeclared_evidence_refs = set(effect.evidence_refs) - declared_refs["evidence_refs"] + for ref in sorted(undeclared_target_refs): + violations.append(f"action_result effect {effect.effect_id!r} reports undeclared target_ref {ref!r}") + for ref in sorted(undeclared_evidence_refs): + violations.append(f"action_result effect {effect.effect_id!r} reports undeclared evidence_ref {ref!r}") + return violations + + +def _sem211_declared_evidence_refs( + declared_precondition_refs: _ContractRefMap, + declared_effect_refs: _ContractRefMap, +) -> set[str]: + refs: set[str] = set() + for declared_refs in declared_precondition_refs.values(): + refs.update(declared_refs["evidence_refs"]) + for declared_refs in declared_effect_refs.values(): + refs.update(declared_refs["evidence_refs"]) + return refs + + +def _sem211_reported_evidence_refs(result: "ParticipantActionResult") -> set[str]: + refs: set[str] = set() + for precondition in result.preconditions: + refs.update(precondition.evidence_refs) + for effect in result.effects: + refs.update(effect.evidence_refs) + return refs + + +def _sem211_result_evidence_violations( + result: "ParticipantActionResult", + declared_precondition_refs: _ContractRefMap, + declared_effect_refs: _ContractRefMap, +) -> list[str]: + if not (declared_precondition_refs or declared_effect_refs): + return [] + declared_result_evidence_refs = _sem211_declared_evidence_refs(declared_precondition_refs, declared_effect_refs) + reported_result_evidence_refs = _sem211_reported_evidence_refs(result) + violations: list[str] = [] + for ref in sorted(set(result.evidence_refs) - declared_result_evidence_refs): + violations.append(f"action_result reports undeclared evidence_ref {ref!r}") + for ref in sorted(set(result.evidence_refs) & declared_result_evidence_refs - reported_result_evidence_refs): + violations.append( + f"action_result evidence_ref {ref!r} is not grounded in reported precondition or effect evidence_refs" + ) + return violations + + +def validate_participant_action_result_contract( + result: "ParticipantActionResult", + contract: ParticipantActionContractRuntime, +) -> list[str]: + """Return SEM-211 contract violations for one typed action result.""" + + if result.action_contract_address != contract.address: + return [ + "action_result action_contract_address " + f"{result.action_contract_address!r} does not match compiled action contract {contract.address!r}" + ] + + declared_precondition_refs = _contract_sem211_precondition_refs(contract) + declared_effect_refs = _contract_sem211_effect_refs(contract) + declared_preconditions = set(declared_precondition_refs) + declared_effects = set(declared_effect_refs) + + violations: list[str] = [] + violations.extend( + _sem211_precondition_violations( + result, + contract, + set(contract.precondition_classes), + declared_preconditions, + declared_precondition_refs, + ) + ) + violations.extend( + _sem211_effect_violations( + result, + contract, + set(contract.effect_classes), + declared_effects, + declared_effect_refs, + ) + ) + violations.extend(_sem211_result_evidence_violations(result, declared_precondition_refs, declared_effect_refs)) + + if result.failure_class is not None and result.failure_class.value not in set(contract.failure_classes): + violations.append( + f"action_result failure_class {result.failure_class.value!r} is not declared by {contract.address}" + ) + return violations diff --git a/implementations/python/packages/aces_processor/models/runtime_model.py b/implementations/python/packages/aces_processor/models/runtime_model.py new file mode 100644 index 000000000..7a06a5c56 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/runtime_model.py @@ -0,0 +1,204 @@ +"""Top-level compiled RuntimeModel, ExecutionPlan, and resource payload projection.""" + +from dataclasses import asdict, dataclass, field +from typing import Any + +from aces_backend_protocols.capabilities import BackendManifest +from aces_contracts.addressing import require_compiled_address +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.evaluation import EvaluationExecutionContract, EvaluationResultContract +from aces_contracts.planning import EvaluationPlan, OrchestrationPlan, ProvisioningPlan +from aces_contracts.runtime_state import RuntimeSnapshot +from aces_sdl.scenario import InstantiatedScenario + +from aces_processor.semantics.realization import CompiledRealizationRequirement + +from .behavior_resources import ( + EventRuntime, + ObjectiveWindowReferenceRuntime, + ParticipantBehaviorRuntime, + ParticipantBehaviorSpecificationRuntime, + ParticipantObservationBoundaryRuntime, + ParticipantOutcomeInterpretationRuleRuntime, + ScriptRuntime, + StoryRuntime, + WorkflowRuntime, +) +from .resources import ( + AccountPlacement, + AssertionRuntime, + ConditionBinding, + ContentPlacement, + FeatureBinding, + InjectBinding, + InjectRuntime, + NetworkRuntime, + NodeRuntime, + ParticipantActionContractRuntime, + PropositionRuntime, + ResolvedResource, + RuntimeTemplate, +) + + +@dataclass(frozen=True) +class ObjectiveRuntime(ResolvedResource): + """Resolved objective node.""" + + actor_type: str = "" + actor_name: str = "" + success_addresses: tuple[str, ...] = () + objective_dependencies: tuple[str, ...] = () + window_story_addresses: tuple[str, ...] = () + window_script_addresses: tuple[str, ...] = () + window_event_addresses: tuple[str, ...] = () + window_workflow_addresses: tuple[str, ...] = () + window_step_refs: tuple[str, ...] = () + window_step_workflow_addresses: tuple[str, ...] = () + window_references: tuple[ObjectiveWindowReferenceRuntime, ...] = () + result_contract: "EvaluationResultContract" = field( + default_factory=lambda: EvaluationResultContract(resource_type="objective") + ) + execution_contract: "EvaluationExecutionContract" = field( + default_factory=lambda: EvaluationExecutionContract(resource_type="objective") + ) + + +@dataclass(frozen=True) +class CompiledCapabilityConstraint: + """One finite SDL capability domain lowered onto a compiled resource.""" + + address: str + concern: str + parameter: tuple[str, ...] + allowed_values: tuple[str | int | float | bool, ...] + + def __post_init__(self) -> None: + require_compiled_address(self.address, field_name="capability constraint address") + if self.concern not in {"nodes.os", "infrastructure.count"}: + raise ValueError("compiled capability constraint has an unsupported concern") + if not self.parameter or any(not segment for segment in self.parameter): + raise ValueError("compiled capability constraint requires a parameter identity") + if not self.allowed_values: + raise ValueError("compiled capability constraint requires a non-empty domain") + + +@dataclass(frozen=True) +class RuntimeModel: + """Compiled SDL runtime model. + + Reusable definitions stay as templates or metadata. Only bound runtime + instances become planned resources. + """ + + scenario_name: str + feature_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) + condition_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) + inject_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) + vulnerability_templates: dict[str, RuntimeTemplate] = field(default_factory=dict) + entity_specs: dict[str, dict[str, Any]] = field(default_factory=dict) + agent_specs: dict[str, dict[str, Any]] = field(default_factory=dict) + relationship_specs: dict[str, dict[str, Any]] = field(default_factory=dict) + # Typed compiler metadata for finite pre-instantiation domains. It is + # consumed by planner capability checks and never enters backend resource + # payloads. + capability_constraints: tuple[CompiledCapabilityConstraint, ...] = () + networks: dict[str, NetworkRuntime] = field(default_factory=dict) + node_deployments: dict[str, NodeRuntime] = field(default_factory=dict) + feature_bindings: dict[str, FeatureBinding] = field(default_factory=dict) + propositions: dict[str, PropositionRuntime] = field(default_factory=dict) + assertions: dict[str, AssertionRuntime] = field(default_factory=dict) + condition_bindings: dict[str, ConditionBinding] = field(default_factory=dict) + injects: dict[str, InjectRuntime] = field(default_factory=dict) + inject_bindings: dict[str, InjectBinding] = field(default_factory=dict) + content_placements: dict[str, ContentPlacement] = field(default_factory=dict) + account_placements: dict[str, AccountPlacement] = field(default_factory=dict) + action_contracts: dict[str, ParticipantActionContractRuntime] = field(default_factory=dict) + observation_boundaries: dict[str, ParticipantObservationBoundaryRuntime] = field(default_factory=dict) + outcome_interpretation_rules: dict[str, ParticipantOutcomeInterpretationRuleRuntime] = field(default_factory=dict) + participant_behaviors: dict[str, ParticipantBehaviorRuntime] = field(default_factory=dict) + behavior_specifications: dict[str, ParticipantBehaviorSpecificationRuntime] = field(default_factory=dict) + events: dict[str, EventRuntime] = field(default_factory=dict) + scripts: dict[str, ScriptRuntime] = field(default_factory=dict) + stories: dict[str, StoryRuntime] = field(default_factory=dict) + workflows: dict[str, WorkflowRuntime] = field(default_factory=dict) + objectives: dict[str, ObjectiveRuntime] = field(default_factory=dict) + diagnostics: list[Diagnostic] = field(default_factory=list) + # SEM-218 typed compiler emission: each authored realization concern with + # its preserved explicitness class. Model-side metadata; it never enters the backend-facing + # `resource_payload()` envelope. Consumed by the planner realization gate. + realization_requirements: tuple[CompiledRealizationRequirement, ...] = () + realization_instance: InstantiatedScenario | None = None + + def __post_init__(self) -> None: + owners: dict[str, str] = {} + address_map_fields = ( + "networks", + "node_deployments", + "feature_bindings", + "propositions", + "assertions", + "condition_bindings", + "injects", + "inject_bindings", + "content_placements", + "account_placements", + "action_contracts", + "observation_boundaries", + "outcome_interpretation_rules", + "participant_behaviors", + "behavior_specifications", + "events", + "scripts", + "stories", + "workflows", + "objectives", + ) + for field_name in address_map_fields: + value = getattr(self, field_name) + for map_key, item in value.items(): + address = getattr(item, "address", None) + if not isinstance(address, str): + raise TypeError(f"RuntimeModel {field_name} entries must carry an address") + require_compiled_address(address) + require_compiled_address(map_key, field_name="runtime model map key") + if map_key != address: + raise ValueError(f"RuntimeModel {field_name} map key must equal embedded address") + previous_owner = owners.get(address) + if previous_owner is not None and previous_owner != field_name: + raise ValueError( + f"RuntimeModel duplicate compiled address across {previous_owner} and {field_name}" + ) + owners[address] = field_name + capability_keys = [(constraint.address, constraint.concern) for constraint in self.capability_constraints] + if len(capability_keys) != len(set(capability_keys)): + raise ValueError("RuntimeModel capability constraints must address unique fields") + + +@dataclass(frozen=True) +class ExecutionPlan: + """Composite runtime execution plan.""" + + target_name: str | None + manifest: BackendManifest + base_snapshot: "RuntimeSnapshot" + scenario_name: str + model: RuntimeModel + provisioning: ProvisioningPlan + orchestration: OrchestrationPlan + evaluation: EvaluationPlan + diagnostics: list[Diagnostic] = field(default_factory=list) + + @property + def is_valid(self) -> bool: + return not any(diag.is_error for diag in self.diagnostics) + + +def resource_payload(resource: ResolvedResource) -> dict[str, Any]: + """Convert a compiled resource to a stable planner payload.""" + + payload = asdict(resource) + payload.pop("address", None) + payload.pop("ordering_dependencies", None) + payload.pop("refresh_dependencies", None) + return payload diff --git a/implementations/python/packages/aces_processor/models/temporal.py b/implementations/python/packages/aces_processor/models/temporal.py new file mode 100644 index 000000000..91d8e6237 --- /dev/null +++ b/implementations/python/packages/aces_processor/models/temporal.py @@ -0,0 +1,359 @@ +"""Participant temporal runtime context and state-machine records.""" + +from collections.abc import Iterable, Iterator, Mapping +from dataclasses import dataclass +from typing import Any + +from aces_sdl.participant_temporal_semantics import ( + ParticipantTemporalEventPoint, + ParticipantTemporalState, + ParticipantTimeDomain, +) + +from .behavior_resources import ( + _optional_payload_string, + _tuple_of_non_empty_strings, + _validate_optional_string, + _validate_required_string, +) + + +def _participant_time_domain_from_payload(value: object) -> ParticipantTimeDomain: + if isinstance(value, ParticipantTimeDomain): + return value + return ParticipantTimeDomain(str(value)) + + +def _participant_temporal_event_point_from_payload(value: object) -> ParticipantTemporalEventPoint: + if isinstance(value, ParticipantTemporalEventPoint): + return value + return ParticipantTemporalEventPoint(str(value)) + + +def _participant_temporal_state_from_payload(value: object) -> ParticipantTemporalState: + if isinstance(value, ParticipantTemporalState): + return value + return ParticipantTemporalState(str(value)) + + +def _participant_temporal_event_points_from_payload(value: object) -> tuple[ParticipantTemporalEventPoint, ...]: + if isinstance(value, (str, bytes, Mapping)) or not isinstance(value, Iterable): + raise TypeError("temporal event_points must be a list of event-point strings") + points = tuple(_participant_temporal_event_point_from_payload(item) for item in value) + if not points: + raise ValueError("temporal event_points must be non-empty") + if len(set(points)) != len(points): + raise ValueError("temporal event_points must be unique") + return points + + +@dataclass(frozen=True) +class ParticipantTemporalRuntimeContext: + """Realized SEM-213 temporal context on a participant behavior event.""" + + temporal_contract_id: str + time_domain: ParticipantTimeDomain + clock_authority: str + event_points: tuple[ParticipantTemporalEventPoint, ...] + observation_point: str + backend_disclosure_refs: tuple[str, ...] = () + reset_boundary: str | None = None + replay_boundary: str | None = None + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantTemporalRuntimeContext": + if not isinstance(payload, Mapping): + raise TypeError("participant temporal runtime context must be a mapping") + missing = [ + key + for key in ( + "temporal_contract_id", + "time_domain", + "clock_authority", + "event_points", + "observation_point", + ) + if key not in payload + ] + if missing: + raise ValueError("participant temporal runtime context is missing required fields: " + ", ".join(missing)) + return cls( + temporal_contract_id=str(payload.get("temporal_contract_id")), + time_domain=_participant_time_domain_from_payload(payload.get("time_domain")), + clock_authority=str(payload.get("clock_authority")), + event_points=_participant_temporal_event_points_from_payload(payload.get("event_points")), + observation_point=str(payload.get("observation_point")), + backend_disclosure_refs=_tuple_of_non_empty_strings( + payload.get("backend_disclosure_refs", ()), + field_name="backend_disclosure_refs", + ), + reset_boundary=_optional_payload_string(payload, "reset_boundary"), + replay_boundary=_optional_payload_string(payload, "replay_boundary"), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "temporal_contract_id": self.temporal_contract_id, + "time_domain": self.time_domain.value, + "clock_authority": self.clock_authority, + "event_points": [point.value for point in self.event_points], + "observation_point": self.observation_point, + "backend_disclosure_refs": list(self.backend_disclosure_refs), + "reset_boundary": self.reset_boundary, + "replay_boundary": self.replay_boundary, + } + + def __post_init__(self) -> None: + _validate_required_string( + self.temporal_contract_id, + "participant temporal temporal_contract_id must be a non-empty string", + ) + if not isinstance(self.time_domain, ParticipantTimeDomain): + raise TypeError("time_domain must be a ParticipantTimeDomain") + _validate_required_string(self.clock_authority, "participant temporal clock_authority must be non-empty") + if not isinstance(self.event_points, tuple): + raise TypeError("event_points must be a tuple") + if not self.event_points: + raise ValueError("participant temporal event_points must be non-empty") + if any(not isinstance(point, ParticipantTemporalEventPoint) for point in self.event_points): + raise TypeError("event_points must contain ParticipantTemporalEventPoint values") + if len(set(self.event_points)) != len(self.event_points): + raise ValueError("participant temporal event_points must be unique") + _validate_required_string(self.observation_point, "participant temporal observation_point must be non-empty") + _tuple_of_non_empty_strings(self.backend_disclosure_refs, field_name="backend_disclosure_refs") + _validate_optional_string(self.reset_boundary, "reset_boundary must be a non-empty string or None") + _validate_optional_string(self.replay_boundary, "replay_boundary must be a non-empty string or None") + + +@dataclass(frozen=True) +class ParticipantTemporalStateTransition: + """Abstract SEM-213 deadline / dwell / timeout state transition.""" + + temporal_contract_id: str + from_state: ParticipantTemporalState + to_state: ParticipantTemporalState + event_point: ParticipantTemporalEventPoint + time_domain: ParticipantTimeDomain + clock_authority: str + boundary_ref: str + evidence_refs: tuple[str, ...] + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> "ParticipantTemporalStateTransition": + if not isinstance(payload, Mapping): + raise TypeError("participant temporal state transition must be a mapping") + missing = [ + key + for key in ( + "temporal_contract_id", + "from_state", + "to_state", + "event_point", + "time_domain", + "clock_authority", + "boundary_ref", + "evidence_refs", + ) + if key not in payload + ] + if missing: + raise ValueError("participant temporal state transition is missing required fields: " + ", ".join(missing)) + return cls( + temporal_contract_id=str(payload.get("temporal_contract_id")), + from_state=_participant_temporal_state_from_payload(payload.get("from_state")), + to_state=_participant_temporal_state_from_payload(payload.get("to_state")), + event_point=_participant_temporal_event_point_from_payload(payload.get("event_point")), + time_domain=_participant_time_domain_from_payload(payload.get("time_domain")), + clock_authority=str(payload.get("clock_authority")), + boundary_ref=str(payload.get("boundary_ref")), + evidence_refs=_tuple_of_non_empty_strings(payload.get("evidence_refs"), field_name="evidence_refs"), + ) + + def __post_init__(self) -> None: + _validate_required_string( + self.temporal_contract_id, + "participant temporal temporal_contract_id must be a non-empty string", + ) + if not isinstance(self.from_state, ParticipantTemporalState): + raise TypeError("from_state must be a ParticipantTemporalState") + if not isinstance(self.to_state, ParticipantTemporalState): + raise TypeError("to_state must be a ParticipantTemporalState") + if not isinstance(self.event_point, ParticipantTemporalEventPoint): + raise TypeError("event_point must be a ParticipantTemporalEventPoint") + if not isinstance(self.time_domain, ParticipantTimeDomain): + raise TypeError("time_domain must be a ParticipantTimeDomain") + _validate_required_string(self.clock_authority, "participant temporal clock_authority must be non-empty") + _validate_required_string(self.boundary_ref, "participant temporal boundary_ref must be non-empty") + evidence_refs = _tuple_of_non_empty_strings(self.evidence_refs, field_name="evidence_refs") + if not evidence_refs: + raise ValueError("participant temporal state transitions require evidence_refs") + + +def _participant_temporal_state_transition_from_payload( + value: ParticipantTemporalStateTransition | Mapping[str, Any], +) -> ParticipantTemporalStateTransition: + if isinstance(value, ParticipantTemporalStateTransition): + return value + return ParticipantTemporalStateTransition.from_payload(value) + + +def _try_parse_temporal_transition( + raw_transition: ParticipantTemporalStateTransition | Mapping[str, Any], +) -> tuple[ParticipantTemporalStateTransition | None, str | None]: + """Parse one raw transition, returning either the transition or an error message.""" + try: + return _participant_temporal_state_transition_from_payload(raw_transition), None + except (TypeError, ValueError) as exc: + return None, f"participant temporal state transition is invalid: {exc}" + + +def _iter_domain_authority_violations( + locator: str, + key: str, + transition: ParticipantTemporalStateTransition, + domain_authority: dict[str, tuple[ParticipantTimeDomain, str]], +) -> Iterator[tuple[str, str]]: + observed_domain_authority = (transition.time_domain, transition.clock_authority) + if key in domain_authority and domain_authority[key] != observed_domain_authority: + expected_domain, expected_authority = domain_authority[key] + yield ( + locator, + f"temporal contract {key!r} changed time domain or clock authority from " + f"{expected_domain.value}/{expected_authority!r} to " + f"{transition.time_domain.value}/{transition.clock_authority!r}", + ) + else: + domain_authority[key] = observed_domain_authority + + +def _iter_from_state_mismatch_violations( + locator: str, + key: str, + transition: ParticipantTemporalStateTransition, + prior_state: dict[str, ParticipantTemporalState], + boundary_states: set[ParticipantTemporalState], + crosses_boundary: bool, +) -> Iterator[tuple[str, str]]: + if ( + key in prior_state + and transition.from_state != prior_state[key] + and prior_state[key] not in boundary_states + and not crosses_boundary + ): + yield ( + locator, + f"temporal contract {key!r} transition from_state {transition.from_state.value!r} " + f"does not match prior to_state {prior_state[key].value!r}", + ) + + +def _iter_cadence_violations( + locator: str, + key: str, + transition: ParticipantTemporalStateTransition, + prior_state: dict[str, ParticipantTemporalState], + cadence_guard_events: set[ParticipantTemporalEventPoint], + cadence_ready_states: set[ParticipantTemporalState], + crosses_boundary: bool, +) -> Iterator[tuple[str, str]]: + if ( + transition.from_state == ParticipantTemporalState.CADENCE_WAITING + and transition.event_point in cadence_guard_events + and not crosses_boundary + ): + yield (locator, "cadence repeated event requires cadence_ready or reset/replay boundary before reuse") + elif ( + transition.to_state == ParticipantTemporalState.CADENCE_WAITING + and transition.from_state not in cadence_ready_states + and prior_state.get(key) not in cadence_ready_states + and not crosses_boundary + ): + yield (locator, "cadence_waiting requires prior cadence_ready or eligible state in the same segment") + + +def _iter_dwell_violations( + locator: str, + key: str, + transition: ParticipantTemporalStateTransition, + prior_state: dict[str, ParticipantTemporalState], +) -> Iterator[tuple[str, str]]: + if ( + transition.to_state == ParticipantTemporalState.DWELL_SATISFIED + and transition.from_state != ParticipantTemporalState.DWELL_ACTIVE + and prior_state.get(key) != ParticipantTemporalState.DWELL_ACTIVE + ): + yield (locator, "dwell_satisfied requires prior dwell_active state in the same temporal segment") + + +def _iter_terminal_state_violations( + locator: str, + transition: ParticipantTemporalStateTransition, + terminal_states: set[ParticipantTemporalState], + boundary_events: set[ParticipantTemporalEventPoint], + boundary_states: set[ParticipantTemporalState], +) -> Iterator[tuple[str, str]]: + if ( + transition.from_state in terminal_states + and transition.event_point not in boundary_events + and transition.to_state not in boundary_states + ): + yield (locator, "terminal temporal state requires reset or replay boundary before reuse") + + +def _iter_temporal_transition_violations( + locator: str, + transition: ParticipantTemporalStateTransition, + prior_state: dict[str, ParticipantTemporalState], + domain_authority: dict[str, tuple[ParticipantTimeDomain, str]], +) -> Iterator[tuple[str, str]]: + """Yield every SEM-213 violation raised by a single already-parsed transition.""" + terminal_states = {ParticipantTemporalState.DEADLINE_MISSED, ParticipantTemporalState.TIMEOUT} + boundary_events = {ParticipantTemporalEventPoint.RESET, ParticipantTemporalEventPoint.REPLAY} + boundary_states = {ParticipantTemporalState.RESET, ParticipantTemporalState.REPLAY_BOUNDARY} + cadence_guard_events = { + ParticipantTemporalEventPoint.SUBMIT, + ParticipantTemporalEventPoint.START, + ParticipantTemporalEventPoint.OBSERVED, + ParticipantTemporalEventPoint.EFFECTIVE, + } + cadence_ready_states = { + ParticipantTemporalState.CADENCE_READY, + ParticipantTemporalState.ELIGIBLE, + ParticipantTemporalState.RESET, + ParticipantTemporalState.REPLAY_BOUNDARY, + } + + key = transition.temporal_contract_id + crosses_boundary = transition.event_point in boundary_events or transition.to_state in boundary_states + + checks = ( + _iter_domain_authority_violations(locator, key, transition, domain_authority), + _iter_from_state_mismatch_violations(locator, key, transition, prior_state, boundary_states, crosses_boundary), + _iter_cadence_violations( + locator, key, transition, prior_state, cadence_guard_events, cadence_ready_states, crosses_boundary + ), + _iter_dwell_violations(locator, key, transition, prior_state), + _iter_terminal_state_violations(locator, transition, terminal_states, boundary_events, boundary_states), + ) + for check in checks: + yield from check + + prior_state[key] = transition.to_state + + +def iter_participant_temporal_state_machine_violations( + transitions: Iterable[ParticipantTemporalStateTransition | Mapping[str, Any]], +) -> Iterator[tuple[str, str]]: + """Yield SEM-213 abstract state-machine violations.""" + + prior_state: dict[str, ParticipantTemporalState] = {} + domain_authority: dict[str, tuple[ParticipantTimeDomain, str]] = {} + + for index, raw_transition in enumerate(transitions): + locator = f"participant temporal state transition[{index}]" + transition, parse_error = _try_parse_temporal_transition(raw_transition) + if transition is None: + yield (locator, parse_error or "") + continue + + yield from _iter_temporal_transition_violations(locator, transition, prior_state, domain_authority) diff --git a/implementations/python/packages/aces_processor/planner.py b/implementations/python/packages/aces_processor/planner.py index 1e0e99726..34b36bd34 100644 --- a/implementations/python/packages/aces_processor/planner.py +++ b/implementations/python/packages/aces_processor/planner.py @@ -1,5 +1,7 @@ """Planner for compiled SDL runtime models.""" +from dataclasses import replace + from aces_backend_protocols.account_features import provisioner_account_features from aces_backend_protocols.capabilities import BackendManifest from aces_sdl.infrastructure import MINIMUM_NODE_COUNT @@ -33,7 +35,13 @@ resource_dependency_cycles, resource_topological_order, ) -from .semantics.realization import realization_disclosure, realization_support_diagnostics +from .semantics.realization import ( + ApparatusRealizationDefaultResolver, + materialize_realization_requirements, + realization_disclosure, + realization_envelope_diagnostics, + realization_support_diagnostics, +) __all__ = ["plan", "realization_disclosure", "snapshot_delete_order"] @@ -55,111 +63,26 @@ def _planned_resource(address: str, domain: RuntimeDomain, resource_type: str, r def _collect_resources(model: RuntimeModel) -> dict[str, PlannedResource]: resources: dict[str, PlannedResource] = {} - for address, resource in model.networks.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.PROVISIONING, - "network", - resource, - ) - for address, resource in model.node_deployments.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.PROVISIONING, - "node", - resource, - ) - for address, resource in model.feature_bindings.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.PROVISIONING, - "feature-binding", - resource, - ) - for address, resource in model.content_placements.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.PROVISIONING, - "content-placement", - resource, - ) - for address, resource in model.account_placements.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.PROVISIONING, - "account-placement", - resource, - ) - for address, resource in model.inject_bindings.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "inject-binding", - resource, - ) - for address, resource in model.injects.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "inject", - resource, - ) - for address, resource in model.events.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "event", - resource, - ) - for address, resource in model.scripts.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "script", - resource, - ) - for address, resource in model.stories.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "story", - resource, - ) - for address, resource in model.workflows.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.ORCHESTRATION, - "workflow", - resource, - ) - for address, resource in model.condition_bindings.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.EVALUATION, - "condition-binding", - resource, - ) - for address, resource in model.propositions.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.EVALUATION, - "proposition", - resource, - ) - for address, resource in model.assertions.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.EVALUATION, - "assertion", - resource, - ) - for address, resource in model.objectives.items(): - resources[address] = _planned_resource( - address, - RuntimeDomain.EVALUATION, - "objective", - resource, - ) + resource_groups = ( + (model.networks, RuntimeDomain.PROVISIONING, "network"), + (model.node_deployments, RuntimeDomain.PROVISIONING, "node"), + (model.feature_bindings, RuntimeDomain.PROVISIONING, "feature-binding"), + (model.content_placements, RuntimeDomain.PROVISIONING, "content-placement"), + (model.account_placements, RuntimeDomain.PROVISIONING, "account-placement"), + (model.inject_bindings, RuntimeDomain.ORCHESTRATION, "inject-binding"), + (model.injects, RuntimeDomain.ORCHESTRATION, "inject"), + (model.events, RuntimeDomain.ORCHESTRATION, "event"), + (model.scripts, RuntimeDomain.ORCHESTRATION, "script"), + (model.stories, RuntimeDomain.ORCHESTRATION, "story"), + (model.workflows, RuntimeDomain.ORCHESTRATION, "workflow"), + (model.condition_bindings, RuntimeDomain.EVALUATION, "condition-binding"), + (model.propositions, RuntimeDomain.EVALUATION, "proposition"), + (model.assertions, RuntimeDomain.EVALUATION, "assertion"), + (model.objectives, RuntimeDomain.EVALUATION, "objective"), + ) + for group, domain, resource_type in resource_groups: + for address, resource in group.items(): + resources[address] = _planned_resource(address, domain, resource_type, resource) return resources @@ -429,26 +352,27 @@ def _validate_manifest(model: RuntimeModel, manifest: BackendManifest) -> list[D diagnostics: list[Diagnostic] = [] provisioner = manifest.provisioner - for network in model.networks.values(): - if "switch" not in provisioner.supported_node_types: + for resource in [*model.networks.values(), *model.node_deployments.values()]: + if resource.spec.get("infrastructure", {}).get("acls") and not provisioner.supports_acls: diagnostics.append( Diagnostic( - code="provisioner.unsupported-node-type", + code="provisioner.acls-unsupported", domain="provisioning", - address=network.address, - message="Provisioner does not support switch/network nodes.", + address=resource.address, + message="Provisioner does not support ACL declarations.", ) ) - if network.spec.get("infrastructure", {}).get("acls") and not provisioner.supports_acls: + + for network in model.networks.values(): + if "switch" not in provisioner.supported_node_types: diagnostics.append( Diagnostic( - code="provisioner.acls-unsupported", + code="provisioner.unsupported-node-type", domain="provisioning", address=network.address, - message="Provisioner does not support ACL declarations.", + message="Provisioner does not support switch/network nodes.", ) ) - for node in model.node_deployments.values(): if node.node_type and node.node_type not in provisioner.supported_node_types: diagnostics.append( @@ -833,20 +757,34 @@ def plan( snapshot: RuntimeSnapshot | None = None, *, target_name: str | None = None, + apparatus_realization_default: ApparatusRealizationDefaultResolver | None = None, ) -> ExecutionPlan: """Reconcile a compiled runtime model against the current snapshot.""" snapshot = snapshot or RuntimeSnapshot() - resources = _collect_resources(model) + effective_requirements = materialize_realization_requirements( + model.realization_requirements, + manifest, + apparatus_default=apparatus_realization_default, + ) + effective_model = replace(model, realization_requirements=effective_requirements) + resources = _collect_resources(effective_model) envelope_diagnostics = ( - list(member(model.realization_instance, manifest.realization_envelope.expression).diagnostics) - if manifest.realization_envelope is not None and model.realization_instance is not None + list(member(effective_model.realization_instance, manifest.realization_envelope.expression).diagnostics) + if manifest.realization_envelope is not None and effective_model.realization_instance is not None else [] ) diagnostics = [ - *model.diagnostics, - *_validate_manifest(model, manifest), - *realization_support_diagnostics(model.realization_requirements, manifest), + *effective_model.diagnostics, + *_validate_manifest(effective_model, manifest), + *realization_support_diagnostics( + effective_requirements, + manifest, + ), + *realization_envelope_diagnostics( + effective_requirements, + manifest, + ), *envelope_diagnostics, *_ordering_cycle_diagnostics(resources), ] @@ -861,7 +799,7 @@ def plan( manifest=manifest, base_snapshot=snapshot, scenario_name=model.scenario_name, - model=model, + model=effective_model, provisioning=provisioning, orchestration=orchestration, evaluation=evaluation, diff --git a/implementations/python/packages/aces_processor/semantics/realization.py b/implementations/python/packages/aces_processor/semantics/realization.py index d8564ecbb..73d38567b 100644 --- a/implementations/python/packages/aces_processor/semantics/realization.py +++ b/implementations/python/packages/aces_processor/semantics/realization.py @@ -1,42 +1,41 @@ -"""SEM-218 realization-support semantics: typed compiled requirements and the -manifest-bound planner gate. - -The normative boundary lives in -``specs/formal/realization/explicitness-and-realization.md``. The compiler -lowers each authored realization concern into a -``CompiledRealizationRequirement`` carrying its SEM-218 explicitness class -(part 1's ``aces_sdl.explicitness`` classifier output). The planner consumes -that compiled metadata directly and matches it against the selected backend's -``realization_support`` declarations. An unsupported exact or constrained -requirement kind becomes a stable error ``Diagnostic`` before deployment, -enforcing invariants I1/I2/I4 without silent approximation. - -The match is a single manifest-bound helper over -``BackendManifest.realization_support`` / ``RealizationSupportDeclaration``. -``domain`` and the kind strings are opaque non-empty strings today (the -extensibility seam; a governed vocabulary is future work), so matching is -exact string membership plus support-mode compatibility. +"""SEM-218 compiled realization demand, apparatus matching, and disclosure. + +The normative boundary is ``explicitness-and-realization.md``. Domain and kind +strings remain opaque; matching is exact membership plus support compatibility. """ from __future__ import annotations -from collections.abc import Iterable, Mapping -from dataclasses import dataclass +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass, replace from aces_backend_protocols.capabilities import BackendManifest from aces_contracts.addressing import require_compiled_address from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp +from aces_contracts.realization_envelope import ( + ClosureOverlay, + EnvelopeBinding, + EnvelopeScope, + Posture, + RealizationEnvelopeModel, +) from aces_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot +from aces_contracts.vocabulary import Closure, RealizationSupportMode from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_sdl.realization_envelope import effective_constraints, subsumes, tokenize_path __all__ = [ "CONCERN_PAYLOAD_PATH", "EXACT_REQUIREMENT_KIND", "REALIZATION_DOMAIN", + "ApparatusRealizationDefaultResolver", "CompiledRealizationRequirement", + "materialize_realization_requirements", "realization_disclosure", + "realization_envelope_diagnostics", "realization_support_diagnostics", + "registered_realization_concerns", "resolve_realization_concern", ] @@ -60,8 +59,8 @@ # capabilities — not a general per-field designation authority (that is staged # under the SEM-218 coverage row). _CONCERN_KIND_BY_PATH: dict[tuple[str, str], str] = { - ("nodes", "os"): "os-family", ("nodes", "type"): "node-type", + ("nodes", "os"): "os-family", ("content", "type"): "content-type", } @@ -91,10 +90,76 @@ class CompiledRealizationRequirement: address: str domain: str requirement_kind: str - explicitness: ExplicitnessClass + explicitness: ExplicitnessClass | None + provenance: ExplicitnessProvenance + governing_scope: str | None = None + delegated: bool = False def __post_init__(self) -> None: require_compiled_address(self.address) + if self.delegated != (self.explicitness is None): + raise ValueError("delegated realization requirements must carry unresolved explicitness") + + +ApparatusRealizationDefaultResolver = Callable[ + [CompiledRealizationRequirement, BackendManifest], + Closure, +] + + +def _closed_apparatus_default( + _requirement: CompiledRealizationRequirement, + _manifest: BackendManifest, +) -> Closure: + return Closure.CLOSED_WORLD + + +def _effective_explicitness( + requirement: CompiledRealizationRequirement, + manifest: BackendManifest, + apparatus_default: ApparatusRealizationDefaultResolver | None, +) -> ExplicitnessClass | None: + if not requirement.delegated: + return requirement.explicitness + resolver = apparatus_default or _closed_apparatus_default + closure = resolver(requirement, manifest) + return ExplicitnessClass.OPEN if closure is Closure.OPEN_WORLD else None + + +def materialize_realization_requirements( + requirements: tuple[CompiledRealizationRequirement, ...], + manifest: BackendManifest, + *, + apparatus_default: ApparatusRealizationDefaultResolver | None = None, +) -> tuple[CompiledRealizationRequirement, ...]: + """Resolve selected-apparatus delegation into the execution carrier.""" + + materialized: list[CompiledRealizationRequirement] = [] + for requirement in requirements: + if requirement.delegated: + if _effective_explicitness(requirement, manifest, apparatus_default) is not ExplicitnessClass.OPEN: + # A closed default carries no realizable demand into execution. + continue + requirement = replace( + requirement, + explicitness=ExplicitnessClass.OPEN, + delegated=False, + ) + materialized.append(requirement) + return tuple(materialized) + + +def registered_realization_concerns( + *, + declaration_names: Mapping[str, Iterable[str]], +) -> tuple[tuple[str, str, str, str], ...]: + """Enumerate ``(section, declaration, leaf, kind)`` registrations.""" + + return tuple( + (section, declaration_name, leaf_field, concern_kind) + for (section, leaf_field), concern_kind in _CONCERN_KIND_BY_PATH.items() + for declaration_name in declaration_names.get(section, ()) + ) def resolve_realization_concern( @@ -109,8 +174,10 @@ def resolve_realization_concern( a published kind and yields ``None``. """ - for (head, leaf), concern_kind in _CONCERN_KIND_BY_PATH.items(): - if any(field_path == f"{head}.{name}.{leaf}" for name in declaration_names.get(head, ())): + for section, declaration_name, leaf_field, concern_kind in registered_realization_concerns( + declaration_names=declaration_names + ): + if field_path == f"{section}.{declaration_name}.{leaf_field}": return concern_kind return None @@ -118,6 +185,8 @@ def resolve_realization_concern( def realization_support_diagnostics( requirements: tuple[CompiledRealizationRequirement, ...], manifest: BackendManifest, + *, + apparatus_default: ApparatusRealizationDefaultResolver | None = None, ) -> list[Diagnostic]: """Match compiled requirements against the manifest's ``realization_support``. @@ -126,7 +195,8 @@ def realization_support_diagnostics( requirements need their concern kind in ``supported_constraint_kinds``. Unsupported kinds become stable error diagnostics naming the resource address, SDL field path, requirement kind, and missing capability. Open - requirements are not emitted by the compiler and so are not gated here. + requirements require an explicit ``open-realization`` apparatus claim; + unresolved delegation uses the agreed closed fallback. Diagnostics deliberately name only the field path and kind strings, never the exact author value, which may carry sensitive material (SEM-218 @@ -135,10 +205,30 @@ def realization_support_diagnostics( diagnostics: list[Diagnostic] = [] for requirement in requirements: + explicitness = _effective_explicitness(requirement, manifest, apparatus_default) declarations = [ declaration for declaration in manifest.realization_support if declaration.domain == requirement.domain ] - if requirement.explicitness is ExplicitnessClass.EXACT: + if explicitness is ExplicitnessClass.OPEN: + supported = any( + declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations + ) + if not supported: + diagnostics.append( + Diagnostic( + code="realization.unsupported-open-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + "Backend declares no open realization support for " + f"'{requirement.requirement_kind}' requirement at " + f"'{requirement.field_path}' in domain '{requirement.domain}'." + ), + severity=Severity.ERROR, + ) + ) + continue + if explicitness is ExplicitnessClass.EXACT: supported = any( EXACT_REQUIREMENT_KIND in declaration.supported_exact_requirement_kinds for declaration in declarations ) @@ -158,7 +248,7 @@ def realization_support_diagnostics( severity=Severity.ERROR, ) ) - elif requirement.explicitness is ExplicitnessClass.CONSTRAINED: + elif explicitness is ExplicitnessClass.CONSTRAINED: supported = any( requirement.requirement_kind in declaration.supported_constraint_kinds for declaration in declarations ) @@ -180,6 +270,94 @@ def realization_support_diagnostics( return diagnostics +def realization_envelope_diagnostics( + requirements: tuple[CompiledRealizationRequirement, ...], + manifest: BackendManifest, + *, + apparatus_default: ApparatusRealizationDefaultResolver | None = None, +) -> list[Diagnostic]: + """Check open compiled demand against the offered envelope via subsumption.""" + + carrier = manifest.realization_envelope + if carrier is None: + return [] + open_paths = tuple( + requirement.field_path + for requirement in requirements + if _effective_explicitness(requirement, manifest, apparatus_default) is ExplicitnessClass.OPEN + ) + if not open_paths: + return [] + requested = _open_request_envelope(open_paths) + offered = _offered_open_projection(carrier.expression, open_paths) + return list(subsumes(offered, requested).diagnostics) + + +def _open_request_envelope(paths: tuple[str, ...]) -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id="compiled-open-realization-request", + scope=EnvelopeScope.SCENARIO, + bindings=[ + EnvelopeBinding(path=path, scope=EnvelopeScope.FIELD, posture=Posture.OPEN) for path in sorted(set(paths)) + ], + ) + + +def _offered_open_projection( + offered: RealizationEnvelopeModel, + paths: tuple[str, ...], +) -> RealizationEnvelopeModel: + constraints, closed = effective_constraints(offered) + domains = {} + bindings = [] + for index, path in enumerate(sorted(set(paths))): + constraint = constraints.get(path) + if constraint is None: + continue + domain_name = f"offered-domain-{index}" + domains[domain_name] = constraint.domain + bindings.append( + EnvelopeBinding( + path=path, + scope=EnvelopeScope.FIELD, + posture=constraint.posture, + domain=domain_name, + overrideable=constraint.overrideable, + ) + ) + excludes_open_path = any(_closed_scope_excludes(path, closed) for path in paths) + closure = ( + [ + ClosureOverlay( + path="", + scope=EnvelopeScope.SCENARIO, + closure=Closure.CLOSED_WORLD, + ) + ] + if excludes_open_path + else [] + ) + return RealizationEnvelopeModel( + id=f"{offered.id}.open-demand-projection", + scope=offered.scope, + domains=domains, + bindings=bindings, + closure=closure, + ) + + +def _closed_scope_excludes(path: str, closed: dict[str, set[str]]) -> bool: + target = tuple(tokenize_path(path)) + for scope_path, admitted_children in closed.items(): + scope = tuple(tokenize_path(scope_path)) if scope_path else () + if len(scope) >= len(target) or target[: len(scope)] != scope: + continue + child = target[len(scope)] + if not isinstance(child, str) or child not in admitted_children: + return True + return False + + def realization_disclosure( requirements: tuple[CompiledRealizationRequirement, ...], declared_plan: ProvisioningPlan, @@ -241,33 +419,31 @@ def _evaluate_realization( when a non-exact concern was left unrealized. """ + diagnostic: Diagnostic | None = None + entry: RealizationProvenanceEntry | None = None path = CONCERN_PAYLOAD_PATH.get(requirement.requirement_kind) op = declared_ops.get(requirement.address) - # No realization is owed when the concern is unmapped, the plan declares no op - # for this resource, or it removes it (a DELETE op — expected absence); none is - # a backend fault, so there is no baseline to gate or disclose. - if path is None or op is None or op.action is ChangeAction.DELETE: - return None, None - declared_value = _concern_value(op.payload, path) - if declared_value is _MISSING_CONCERN_VALUE: - # The plan op carries no value for this concern: no author baseline to - # enforce (an upstream processor invariant, not a backend contract). - return None, None + if requirement.explicitness is None or path is None or op is None or op.action is ChangeAction.DELETE: + return diagnostic, entry snapshot_entry = returned_snapshot.entries.get(requirement.address) realized_value = ( _concern_value(snapshot_entry.payload, path) if snapshot_entry is not None else _MISSING_CONCERN_VALUE ) - honoured = realized_value == declared_value - diagnostic: Diagnostic | None = None - entry: RealizationProvenanceEntry | None = None - if requirement.explicitness is ExplicitnessClass.EXACT and not honoured: - # The backend realized the exact concern with a different value or omitted - # it entirely; both are forbidden silent approximation (I2). - diagnostic = _silent_approximation_diagnostic(requirement) - elif realized_value is not _MISSING_CONCERN_VALUE: - # A located realized concern: disclose its provenance. (A non-exact concern - # the backend left unrealized falls through with nothing to disclose.) - entry = _realization_provenance_entry(requirement, honoured) + if requirement.explicitness is ExplicitnessClass.OPEN: + if realized_value is not _MISSING_CONCERN_VALUE: + entry = _realization_provenance_entry(requirement, False) + return diagnostic, entry + declared_value = _concern_value(op.payload, path) + if declared_value is not _MISSING_CONCERN_VALUE: + honoured = realized_value == declared_value + if requirement.explicitness is ExplicitnessClass.EXACT and not honoured: + # The backend realized the exact concern with a different value or omitted + # it entirely; both are forbidden silent approximation (I2). + diagnostic = _silent_approximation_diagnostic(requirement) + elif realized_value is not _MISSING_CONCERN_VALUE: + # A located realized concern: disclose its provenance. (A non-exact concern + # the backend left unrealized falls through with nothing to disclose.) + entry = _realization_provenance_entry(requirement, honoured) return diagnostic, entry @@ -281,7 +457,8 @@ def _realization_provenance_entry( domain=requirement.domain, requirement_kind=requirement.requirement_kind, explicitness=requirement.explicitness, - provenance=(ExplicitnessProvenance.AUTHOR_DECLARED if honoured else ExplicitnessProvenance.BACKEND_REALIZED), + provenance=(requirement.provenance if honoured else ExplicitnessProvenance.BACKEND_REALIZED), + governing_scope=requirement.governing_scope, ) diff --git a/implementations/python/packages/aces_reference_backend/__init__.py b/implementations/python/packages/aces_reference_backend/__init__.py index 3b92d6608..345eb2cc8 100644 --- a/implementations/python/packages/aces_reference_backend/__init__.py +++ b/implementations/python/packages/aces_reference_backend/__init__.py @@ -21,6 +21,7 @@ DeploymentDriver, NetworkHandle, NetworkSpec, + ServiceSpec, ) from .manifest import REFERENCE_BACKEND_NAME, create_reference_backend_manifest from .realization import Realization, interpret_provisioning_plan @@ -37,6 +38,7 @@ "DeploymentDriver", "NetworkHandle", "NetworkSpec", + "ServiceSpec", "Realization", "create_reference_backend_components", "create_reference_backend_manifest", diff --git a/implementations/python/packages/aces_reference_backend/driver.py b/implementations/python/packages/aces_reference_backend/driver.py index 189d1a16b..29595c627 100644 --- a/implementations/python/packages/aces_reference_backend/driver.py +++ b/implementations/python/packages/aces_reference_backend/driver.py @@ -34,13 +34,30 @@ class NetworkSpec: labels: dict[str, str] = field(default_factory=dict) +@dataclass(frozen=True) +class ServiceSpec: + """Portable authored service descriptor carried to a deployment driver. + + The descriptor identifies a node-local transport binding. Its presence + does not authorize traffic, publish a host port, or prove that a listener + exists. Drivers may inspect it when declaring service support, but must not + infer reachability from it. + """ + + port: int + protocol: str + name: str = "" + + @dataclass(frozen=True) class ContainerSpec: """Portable description of a container to realize. ``image_ref`` is a portable image reference (a name/tag or digest), not a pulled local image id. ``networks`` are ACES network resource - addresses. ``labels`` carries only non-sensitive classification labels. + addresses. ``services`` preserves authored node-local transport binding + descriptors without granting reachability or host publication. ``labels`` + carries only non-sensitive classification labels. """ address: str @@ -48,6 +65,7 @@ class ContainerSpec: image_ref: str networks: tuple[str, ...] = () labels: dict[str, str] = field(default_factory=dict) + services: tuple[ServiceSpec, ...] = () @dataclass(frozen=True) diff --git a/implementations/python/packages/aces_reference_backend/manifest.py b/implementations/python/packages/aces_reference_backend/manifest.py index 0c41a5ab9..e131fbf95 100644 --- a/implementations/python/packages/aces_reference_backend/manifest.py +++ b/implementations/python/packages/aces_reference_backend/manifest.py @@ -125,7 +125,7 @@ def _capabilities() -> BackendCapabilitySet: supported_content_types=frozenset({"file", "dataset", "directory"}), supported_account_features=frozenset({"groups", "mail", "spn", "shell", "home", "disabled", "auth_method"}), max_total_nodes=None, - supports_acls=True, + supports_acls=False, supports_accounts=True, ), orchestrator=OrchestratorCapabilities( diff --git a/implementations/python/packages/aces_reference_backend/realization.py b/implementations/python/packages/aces_reference_backend/realization.py index c2c28d1ab..41e54c115 100644 --- a/implementations/python/packages/aces_reference_backend/realization.py +++ b/implementations/python/packages/aces_reference_backend/realization.py @@ -17,7 +17,7 @@ from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain -from .driver import ContainerSpec, NetworkSpec +from .driver import ContainerSpec, NetworkSpec, ServiceSpec _DOMAIN = "runtime" @@ -81,7 +81,11 @@ def interpret_provisioning_plan(plan: ProvisioningPlan) -> Realization: networks = [_network_spec(resource, payload) for resource, payload in network_resources] network_lookup = _network_address_lookup(networks) - containers = [_container_spec(resource, payload, network_lookup) for resource, payload in node_resources] + containers: list[ContainerSpec] = [] + for resource, payload in node_resources: + container, service_diagnostics = _container_spec(resource, payload, network_lookup) + containers.append(container) + diagnostics.extend(service_diagnostics) placements = [_placement(resource, payload) for resource, payload in placement_resources] return Realization( @@ -135,7 +139,7 @@ def _container_spec( resource: PlannedResource, payload: Mapping[str, object], network_lookup: dict[str, str], -) -> ContainerSpec: +) -> tuple[ContainerSpec, tuple[Diagnostic, ...]]: infrastructure = _infrastructure_spec(payload) networks = infrastructure.get("networks") references: tuple[str, ...] = () @@ -146,14 +150,57 @@ def _container_spec( # contract stays total even when a node names a network not in this plan. network_addresses = tuple(network_lookup.get(ref, ref) for ref in references) image_ref = _image_ref(payload) - return ContainerSpec( - address=resource.address, - name=_resource_name(resource, payload), - image_ref=image_ref, - networks=network_addresses, + services, diagnostics = _service_specs(resource, payload) + return ( + ContainerSpec( + address=resource.address, + name=_resource_name(resource, payload), + image_ref=image_ref, + networks=network_addresses, + services=services, + ), + diagnostics, ) +def _service_specs( + resource: PlannedResource, + payload: Mapping[str, object], +) -> tuple[tuple[ServiceSpec, ...], tuple[Diagnostic, ...]]: + spec = payload.get("spec") + node = spec.get("node") if isinstance(spec, Mapping) else None + raw_services = node.get("services") if isinstance(node, Mapping) else None + if raw_services is None: + return (), () + if not isinstance(raw_services, (list, tuple)): + return (), (_invalid_services(resource),) + + services: list[ServiceSpec] = [] + diagnostics: list[Diagnostic] = [] + for index, raw_service in enumerate(raw_services): + service = _service_spec(raw_service) + if service is None: + diagnostics.append(_invalid_service(resource, index)) + continue + services.append(service) + return tuple(services), tuple(diagnostics) + + +def _service_spec(raw_service: object) -> ServiceSpec | None: + if not isinstance(raw_service, Mapping): + return None + port = raw_service.get("port") + protocol = raw_service.get("protocol", "tcp") + name = raw_service.get("name", "") + if not _valid_service_port(port) or not isinstance(protocol, str) or not isinstance(name, str): + return None + return ServiceSpec(port=port, protocol=protocol, name=name) + + +def _valid_service_port(value: object) -> bool: + return isinstance(value, int) and not isinstance(value, bool) and 1 <= value <= 65535 + + def _image_ref(payload: Mapping[str, object]) -> str: source = _node_source(payload) if source: @@ -214,3 +261,27 @@ def _invalid_payload(resource: PlannedResource) -> Diagnostic: ), severity=Severity.ERROR, ) + + +def _invalid_services(resource: PlannedResource) -> Diagnostic: + return Diagnostic( + code="reference-backend.realization.services-invalid", + domain=_DOMAIN, + address=resource.address, + message=(f"Reference backend expected provisioning node '{resource.address}' to carry services as a sequence."), + severity=Severity.ERROR, + ) + + +def _invalid_service(resource: PlannedResource, index: int) -> Diagnostic: + return Diagnostic( + code="reference-backend.realization.service-invalid", + domain=_DOMAIN, + address=resource.address, + message=( + f"Reference backend expected service entry {index} on provisioning " + f"node '{resource.address}' to contain a concrete port, protocol, " + "and optional name." + ), + severity=Severity.ERROR, + ) diff --git a/implementations/python/packages/aces_runtime/control_plane_api_models.py b/implementations/python/packages/aces_runtime/control_plane_api_models.py index 174e15713..416e80164 100644 --- a/implementations/python/packages/aces_runtime/control_plane_api_models.py +++ b/implementations/python/packages/aces_runtime/control_plane_api_models.py @@ -160,6 +160,18 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "shared_state_history": dict(snapshot.shared_state_history), "joint_action_records": dict(snapshot.joint_action_records), "time_management_contexts": dict(snapshot.time_management_contexts), + "realization_provenance": [ + { + "address": entry.address, + "field_path": entry.field_path, + "domain": entry.domain, + "requirement_kind": entry.requirement_kind, + "explicitness": entry.explicitness.value, + "provenance": entry.provenance.value, + "governing_scope": entry.governing_scope, + } + for entry in snapshot.realization_provenance + ], "realization_envelope": ( snapshot.realization_envelope.model_dump(mode="json") if snapshot.realization_envelope is not None diff --git a/implementations/python/packages/aces_runtime/control_plane_store.py b/implementations/python/packages/aces_runtime/control_plane_store.py index 0f7b75d34..f24b65a06 100644 --- a/implementations/python/packages/aces_runtime/control_plane_store.py +++ b/implementations/python/packages/aces_runtime/control_plane_store.py @@ -114,6 +114,7 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: "requirement_kind": entry.requirement_kind, "explicitness": entry.explicitness.value, "provenance": entry.provenance.value, + "governing_scope": entry.governing_scope, } for entry in snapshot.realization_provenance ], @@ -173,6 +174,7 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: provenance=ExplicitnessProvenance( str(item.get("provenance", ExplicitnessProvenance.AUTHOR_DECLARED.value)) ), + governing_scope=(str(item["governing_scope"]) if item.get("governing_scope") is not None else None), ) for item in payload.get("realization_provenance", []) if isinstance(item, dict) diff --git a/implementations/python/packages/aces_sdl/_composition_provenance.py b/implementations/python/packages/aces_sdl/_composition_provenance.py index 3051ecea2..c376ebf19 100644 --- a/implementations/python/packages/aces_sdl/_composition_provenance.py +++ b/implementations/python/packages/aces_sdl/_composition_provenance.py @@ -13,6 +13,7 @@ ExplicitnessProvenanceRecord, ResolvedImportProvenance, ) +from .realization_designation import RealizationDesignationRecord from .scenario import ImportDecl, ScenarioContent @@ -93,6 +94,27 @@ def prefixed_explicitness( return ExplicitnessProvenanceRecord.model_validate(payload) +def prefixed_realization_designation( + record: RealizationDesignationRecord, + *, + namespace: str, + symbols: dict[str, dict[str, str] | set[str]], +) -> RealizationDesignationRecord: + """Qualify one imported lexical scope through the composition symbol map.""" + + parts = record.field_pointer.split("/") + if len(parts) >= 3: + section_symbols = symbols.get(_decode_pointer_segment(parts[1])) + declaration_name = _decode_pointer_segment(parts[2]) + if isinstance(section_symbols, Mapping) and declaration_name in section_symbols: + parts[2] = _encode_pointer_segment(section_symbols[declaration_name]) + return RealizationDesignationRecord( + namespace=(namespace, *record.namespace), + field_pointer="/".join(parts), + posture=record.posture, + ) + + def _decode_pointer_segment(segment: str) -> str: return segment.replace("~1", "/").replace("~0", "~") diff --git a/implementations/python/packages/aces_sdl/_language_metadata.py b/implementations/python/packages/aces_sdl/_language_metadata.py index 3e68f8b9c..c26afc088 100644 --- a/implementations/python/packages/aces_sdl/_language_metadata.py +++ b/implementations/python/packages/aces_sdl/_language_metadata.py @@ -47,6 +47,7 @@ } SECTION_FIELD_COMPLETIONS = { + "realization": ("default", "scopes"), "nodes": ("type", "description", "os", "resources", "features", "conditions", "services", "roles"), "infrastructure": ("count", "links", "dependencies", "properties"), "features": ("type", "source", "version", "dependencies"), diff --git a/implementations/python/packages/aces_sdl/_realization_envelope_engine.py b/implementations/python/packages/aces_sdl/_realization_envelope_engine.py index b7e916195..5c336d578 100644 --- a/implementations/python/packages/aces_sdl/_realization_envelope_engine.py +++ b/implementations/python/packages/aces_sdl/_realization_envelope_engine.py @@ -18,12 +18,14 @@ from aces_contracts.realization_envelope import ( Closure, + ClosureOverlay, DomainDescriptor, EnvelopeBinding, Posture, RealizationEnvelopeModel, RecordDomain, WitnessPolicy, + scope_specificity, ) from pydantic import BaseModel @@ -47,17 +49,43 @@ class LeafConstraint: # --------------------------------------------------------------------------- # _PATH_TOKEN_RE = re.compile(r"[^.\[\]]+|\[\d+\]") +_FULL_PATH_RE = re.compile(r"^[^.\[\]]+(?:\.[^.\[\]]+|\[\d+\])*$") def tokenize_path(path: str) -> list[PathToken]: """Split an SDL path into attribute/key segments and ``[i]`` list indices.""" + if _FULL_PATH_RE.fullmatch(path) is None: + raise ValueError("path must use the complete canonical SDL path grammar") tokens: list[PathToken] = [] for raw in _PATH_TOKEN_RE.findall(path): tokens.append(int(raw[1:-1]) if raw.startswith("[") else raw) return tokens +def _path_tokens(path: str) -> tuple[PathToken, ...]: + return tuple(tokenize_path(path)) if path else () + + +def _is_same_or_descendant(path: str, ancestor: str) -> bool: + path_tokens = _path_tokens(path) + ancestor_tokens = _path_tokens(ancestor) + return len(ancestor_tokens) <= len(path_tokens) and path_tokens[: len(ancestor_tokens)] == ancestor_tokens + + +def _render_path(tokens: Sequence[PathToken]) -> str: + rendered = "" + for token in tokens: + if isinstance(token, int): + segment = f"[{token}]" + elif rendered: + segment = f".{token}" + else: + segment = token + rendered += segment + return rendered + + def _navigate_step(current: object, token: PathToken) -> tuple[bool, object]: result: tuple[bool, object] = (False, None) if isinstance(token, int): @@ -186,7 +214,7 @@ def _record_closed_scopes(envelope: RealizationEnvelopeModel) -> dict[str, set[s def _prefixed_paths(constraints: dict[str, LeafConstraint], prefix: str) -> list[str]: - return [path for path in constraints if path == prefix or path.startswith(prefix + ".")] + return [path for path in constraints if _is_same_or_descendant(path, prefix)] def _apply_binding( @@ -212,14 +240,34 @@ def _apply_binding( def _add_admitted_children(envelope: RealizationEnvelopeModel, scope_path: str, admitted: set[str]) -> None: - prefix = scope_path + "." if scope_path else "" + scope_tokens = _path_tokens(scope_path) for binding in envelope.bindings: - if not binding.path.startswith(prefix): + binding_tokens = _path_tokens(binding.path) + if len(binding_tokens) < len(scope_tokens) or binding_tokens[: len(scope_tokens)] != scope_tokens: continue - remainder = binding.path[len(prefix) :] - tokens = tokenize_path(remainder) if remainder else [] - if tokens and isinstance(tokens[0], str): - admitted.add(tokens[0]) + remainder = binding_tokens[len(scope_tokens) :] + if remainder and isinstance(remainder[0], str): + admitted.add(remainder[0]) + + +def _open_closed_subtree(closed: dict[str, set[str]], open_path: str) -> None: + """Shadow inherited closure only along ``open_path``, leaving siblings closed.""" + + for path in tuple(closed): + if _is_same_or_descendant(path, open_path): + del closed[path] + open_tokens = _path_tokens(open_path) + ancestors = tuple(path for path in closed if _is_same_or_descendant(open_path, path)) + for ancestor in ancestors: + current = _path_tokens(ancestor) + remaining = open_tokens[len(current) :] + for index, token in enumerate(remaining): + current_path = _render_path(current) + if isinstance(token, str): + closed.setdefault(current_path, set()).add(token) + current = (*current, token) + if index < len(remaining) - 1: + closed.setdefault(_render_path(current), set()) def effective_constraints( @@ -233,16 +281,26 @@ def effective_constraints( """ constraints: dict[str, LeafConstraint] = {} - for binding in sorted(envelope.bindings, key=lambda b: len(tokenize_path(b.path))): + for binding in sorted(envelope.bindings, key=_binding_specificity): _apply_binding(binding, envelope, constraints) closed = _record_closed_scopes(envelope) - for overlay in envelope.closure: + for overlay in sorted(envelope.closure, key=_overlay_specificity): if overlay.closure is Closure.CLOSED_WORLD: _add_admitted_children(envelope, overlay.path, closed.setdefault(overlay.path, set())) + else: + _open_closed_subtree(closed, overlay.path) return constraints, closed +def _binding_specificity(binding: EnvelopeBinding) -> tuple[int, int]: + return len(tokenize_path(binding.path)), scope_specificity(binding.scope) + + +def _overlay_specificity(overlay: ClosureOverlay) -> tuple[int, int]: + return len(tokenize_path(overlay.path)) if overlay.path else 0, scope_specificity(overlay.scope) + + # --------------------------------------------------------------------------- # # Overridability (R2 well-formedness) # # --------------------------------------------------------------------------- # @@ -296,7 +354,7 @@ def overridability_violations(envelope: RealizationEnvelopeModel) -> list[str]: violations: list[str] = [] constraints: dict[str, LeafConstraint] = {} - for binding in sorted(envelope.bindings, key=lambda b: len(tokenize_path(b.path))): + for binding in sorted(envelope.bindings, key=_binding_specificity): if binding.posture is Posture.OPEN: _record_open_widenings(constraints, binding.path, violations) elif binding.domain is not None: diff --git a/implementations/python/packages/aces_sdl/composition.py b/implementations/python/packages/aces_sdl/composition.py index c6a5120d8..0025da72f 100644 --- a/implementations/python/packages/aces_sdl/composition.py +++ b/implementations/python/packages/aces_sdl/composition.py @@ -19,6 +19,9 @@ from ._composition_provenance import ( prefixed_import_record as _prefixed_import_record, ) +from ._composition_provenance import ( + prefixed_realization_designation as _prefixed_realization_designation, +) from ._composition_provenance import ( resolved_import_record as _resolved_import_record, ) @@ -53,6 +56,7 @@ ExplicitnessProvenanceRecord, ResolvedImportProvenance, ) +from .realization_designation import RealizationDesignation, RealizationDesignationRecord, designation_records from .scenario import ExpandedScenario, ImportDecl, ModuleDescriptor, ScenarioContent @@ -421,6 +425,13 @@ def expand_sdl_modules( import_records: list[ResolvedImportProvenance] = [] capability_constraints: list[CapabilityConstraint] = [] explicitness_records: list[ExplicitnessProvenanceRecord] = [] + realization_records: list[RealizationDesignationRecord] = [] + raw_designation = merged.get("realization") + if raw_designation is not None: + try: + realization_records.extend(designation_records(RealizationDesignation.model_validate(raw_designation))) + except ValidationError as exc: + raise SDLParseError("Realization designation is structurally invalid", path=path) from exc lockfile = load_lockfile(resolved_path.parent) trust_policy = load_trust_policy(resolved_path.parent) @@ -505,14 +516,25 @@ def expand_sdl_modules( symbols=symbols, ) for record in bound.explicitness + if any(record.model_path.startswith(f"{section_name}.") for section_name in _HASHMAP_SECTIONS) + ) + realization_records.extend( + _prefixed_realization_designation( + record, + namespace=namespace, + symbols=symbols, + ) + for record in inner_provenance.realization_designations ) provenance = ExpansionProvenance( imports=tuple(import_records), capability_constraints=tuple(capability_constraints), explicitness=tuple(explicitness_records), + realization_designations=tuple(realization_records), ) merged.pop("imports", None) merged.pop("module", None) + merged.pop("realization", None) merged["expansion_provenance"] = provenance.model_dump(mode="python") return merged, provenance diff --git a/implementations/python/packages/aces_sdl/instantiate.py b/implementations/python/packages/aces_sdl/instantiate.py index b320fffdb..f5bd62675 100644 --- a/implementations/python/packages/aces_sdl/instantiate.py +++ b/implementations/python/packages/aces_sdl/instantiate.py @@ -16,6 +16,8 @@ from ._base import VARIABLE_TOKEN_RE, extract_variable_name from ._errors import SDLInstantiationError, SDLValidationError +from ._identifiers import QualifiedName +from ._mapping_scopes import HASHMAP_SECTIONS from .canonical import canonical_sdl_digest from .explicitness import ExplicitnessRecord, derive_instantiated_explicitness from .phase_contracts import ( @@ -26,6 +28,7 @@ ParameterBinding, SemanticDigest, ) +from .realization_designation import RealizationDesignationRecord, designation_records from .scenario import ExpandedScenario, InstantiatedScenario, Scenario, ScenarioContent from .validator import SemanticValidator from .variables import Variable, VariableType @@ -47,6 +50,7 @@ class _BoundScenarioResult: bindings: tuple[ParameterBinding, ...] capability_constraints: tuple[CapabilityConstraint, ...] explicitness: tuple[ExplicitnessProvenanceRecord, ...] + realization_designations: tuple[RealizationDesignationRecord, ...] def _matches_value_type(value: object, variable: Variable) -> bool: @@ -211,6 +215,43 @@ def _safe_model_validation_errors( return diagnostics or [f"{subject} failed structural validation."] +def _imported_declaration_prefixes(scenario: ExpandedScenario) -> set[str]: + imported_namespaces = tuple(record.namespace for record in scenario.expansion_provenance.imports) + prefixes: set[str] = set() + for section_name in HASHMAP_SECTIONS: + for declaration_name in getattr(scenario, section_name): + parts = QualifiedName.parse(declaration_name).parts + if any(parts[: len(namespace)] == namespace for namespace in imported_namespaces): + prefixes.add(f"{section_name}.{declaration_name}") + return prefixes + + +def _path_has_prefix(model_path: str, prefixes: set[str]) -> bool: + return any( + model_path == prefix or model_path.startswith(prefix + ".") or model_path.startswith(prefix + "[") + for prefix in prefixes + ) + + +def _merge_expanded_provenance( + scenario: ExpandedScenario, + local_constraints: tuple[CapabilityConstraint, ...], + explicitness_by_path: dict[str, ExplicitnessProvenanceRecord], +) -> tuple[tuple[CapabilityConstraint, ...], tuple[RealizationDesignationRecord, ...]]: + constraints = (*scenario.expansion_provenance.capability_constraints, *local_constraints) + imported_prefixes = _imported_declaration_prefixes(scenario) + portable_paths = {record.model_path for record in scenario.expansion_provenance.explicitness} + stale_paths = { + model_path + for model_path in explicitness_by_path + if model_path not in portable_paths and _path_has_prefix(model_path, imported_prefixes) + } + for model_path in stale_paths: + del explicitness_by_path[model_path] + explicitness_by_path.update({record.model_path: record for record in scenario.expansion_provenance.explicitness}) + return constraints, scenario.expansion_provenance.realization_designations + + def _bind_scenario_content( raw_scenario: Scenario | ExpandedScenario, parameters: Mapping[str, JSONLike] | None = None, @@ -235,7 +276,7 @@ def _bind_scenario_content( [f"Scenario contains unresolved variable references after instantiation: {unresolved_list}."] ) - for authoring_field in ("variables", "imports", "module", "expansion_provenance"): + for authoring_field in ("variables", "imports", "module", "realization", "expansion_provenance"): substituted_payload.pop(authoring_field, None) try: content = _BoundScenarioContent.model_validate(substituted_payload) @@ -247,10 +288,16 @@ def _bind_scenario_content( raise SDLInstantiationError(list(derived.errors)) explicitness_by_path = {record.path: _portable_explicitness_record(record) for record in derived.records.values()} constraints: tuple[CapabilityConstraint, ...] = local_constraints + realization_designations = ( + designation_records(raw_scenario.realization) + if isinstance(raw_scenario, Scenario) and raw_scenario.realization is not None + else () + ) if isinstance(raw_scenario, ExpandedScenario): - constraints = (*raw_scenario.expansion_provenance.capability_constraints, *local_constraints) - explicitness_by_path.update( - {record.model_path: record for record in raw_scenario.expansion_provenance.explicitness} + constraints, realization_designations = _merge_expanded_provenance( + raw_scenario, + local_constraints, + explicitness_by_path, ) return _BoundScenarioResult( @@ -265,6 +312,7 @@ def _bind_scenario_content( ), capability_constraints=constraints, explicitness=tuple(explicitness_by_path.values()), + realization_designations=realization_designations, ) @@ -322,6 +370,7 @@ def instantiate_scenario( imports=expansion.imports if expansion is not None else (), capability_constraints=bound.capability_constraints, explicitness=bound.explicitness, + realization_designations=bound.realization_designations, ) payload = bound.content.model_dump(mode="python", by_alias=True) payload["instantiation_provenance"] = provenance.model_dump(mode="python") diff --git a/implementations/python/packages/aces_sdl/language_service.py b/implementations/python/packages/aces_sdl/language_service.py index 585844a8e..2278949f1 100644 --- a/implementations/python/packages/aces_sdl/language_service.py +++ b/implementations/python/packages/aces_sdl/language_service.py @@ -27,7 +27,7 @@ from .scenario import Scenario _MAX_INPUT_BYTES = 64 * 1024 -_SCENARIO_METADATA_FIELDS = frozenset({"name", "version", "description", "module", "imports"}) +_SCENARIO_METADATA_FIELDS = frozenset({"name", "version", "description", "module", "imports", "realization"}) _SECTION_FIELDS = tuple(field for field in Scenario.model_fields if field not in _SCENARIO_METADATA_FIELDS) _TARGETABLE_SECTION_FIELDS = tuple(field for field in _SECTION_FIELDS if is_targetable_section(field)) _TOP_LEVEL_KEYS = tuple(Scenario.model_fields) @@ -54,6 +54,18 @@ def language_completions( if target_section is not None: items = _reference_completion_items(data, target_section, declaration_index=declaration_index) context = f"reference:{target_section}" + elif len(pointer) == 1 and pointer[0] in SECTION_FIELD_COMPLETIONS: + section = pointer[0] + items = [ + { + "label": field, + "kind": "field", + "detail": f"{section} field", + "insert_text": f"{field}: ", + } + for field in SECTION_FIELD_COMPLETIONS[section] + ] + context = f"section:{section}" elif len(pointer) <= 1: existing = set(data) if isinstance(data, dict) else set() items = [ diff --git a/implementations/python/packages/aces_sdl/nodes.py b/implementations/python/packages/aces_sdl/nodes.py index 5b2460e19..27d7fe2d5 100644 --- a/implementations/python/packages/aces_sdl/nodes.py +++ b/implementations/python/packages/aces_sdl/nodes.py @@ -50,9 +50,6 @@ RuntimeFilesystemEntryType, RuntimeFilesystemPresence, RuntimeFilesystemStability, - RuntimeHealthcheckLog, - RuntimeHealthObservation, - RuntimeHealthStatus, RuntimeIdentityProvenance, RuntimeInitProcess, RuntimeLocalGroup, @@ -65,12 +62,9 @@ RuntimeNetworkBackendDetail, RuntimeNetworkDriver, RuntimeNetworkEndpoint, - RuntimeNetworkIdStability, RuntimeNetworkRealization, RuntimeOperationalPolicy, RuntimePackage, - RuntimePackageVulnerabilityFinding, - RuntimePackageVulnerabilitySeverity, RuntimeProcessCapabilityOverride, RuntimeProcessIdentity, RuntimeProcessRole, @@ -129,9 +123,6 @@ "RuntimeFilesystemEntryType", "RuntimeFilesystemPresence", "RuntimeFilesystemStability", - "RuntimeHealthObservation", - "RuntimeHealthStatus", - "RuntimeHealthcheckLog", "RuntimeIdentityProvenance", "RuntimeInitProcess", "RuntimeLocalGroup", @@ -144,12 +135,9 @@ "RuntimeNetworkBackendDetail", "RuntimeNetworkDriver", "RuntimeNetworkEndpoint", - "RuntimeNetworkIdStability", "RuntimeNetworkRealization", "RuntimeOperationalPolicy", "RuntimePackage", - "RuntimePackageVulnerabilityFinding", - "RuntimePackageVulnerabilitySeverity", "RuntimeProcessCapabilityOverride", "RuntimeProcessIdentity", "RuntimeProcessRole", @@ -251,7 +239,13 @@ def normalize_asset_value( class ServicePort(SDLModel): - """A network service exposed by a node. From OCSF NetworkEndpoint.""" + """Authored identity of a node-local transport binding. + + A service declaration does not authorize traffic, prove a live listener, + publish a host port, or classify an internal/external audience. Traffic + authorization belongs to infrastructure ACLs; observed bind state and host + publication belong to their dedicated runtime surfaces. + """ port: int | str protocol: str = "tcp" diff --git a/implementations/python/packages/aces_sdl/phase_contracts.py b/implementations/python/packages/aces_sdl/phase_contracts.py index 46c3d317c..22afb004a 100644 --- a/implementations/python/packages/aces_sdl/phase_contracts.py +++ b/implementations/python/packages/aces_sdl/phase_contracts.py @@ -12,6 +12,7 @@ from ._base import SDLModel from ._identifiers import PortableIdentifier, QualifiedName, require_module_identifier from .explicitness import ExplicitnessClass, ExplicitnessProvenance +from .realization_designation import RealizationDesignationRecord _DIGEST_PATTERN = r"^sha256:[a-f0-9]{64}$" _JSON_POINTER_RE = re.compile(r"^(?:/(?:[^~/]|~[01])*)*$") @@ -136,6 +137,7 @@ class ExpansionProvenance(FrozenPhaseModel): imports: tuple[ResolvedImportProvenance, ...] = () capability_constraints: tuple[CapabilityConstraint, ...] = () explicitness: tuple[ExplicitnessProvenanceRecord, ...] = () + realization_designations: tuple[RealizationDesignationRecord, ...] = () @model_validator(mode="after") def _validate_unique_identities(self) -> ExpansionProvenance: @@ -143,6 +145,7 @@ def _validate_unique_identities(self) -> ExpansionProvenance: imports=self.imports, constraints=self.capability_constraints, explicitness=self.explicitness, + realization_designations=self.realization_designations, ) return self @@ -156,6 +159,7 @@ class InstantiationProvenance(FrozenPhaseModel): imports: tuple[ResolvedImportProvenance, ...] = () capability_constraints: tuple[CapabilityConstraint, ...] = () explicitness: tuple[ExplicitnessProvenanceRecord, ...] = () + realization_designations: tuple[RealizationDesignationRecord, ...] = () @model_validator(mode="after") def _validate_unique_identities(self) -> InstantiationProvenance: @@ -168,6 +172,7 @@ def _validate_unique_identities(self) -> InstantiationProvenance: imports=self.imports, constraints=self.capability_constraints, explicitness=self.explicitness, + realization_designations=self.realization_designations, root_bindings=self.bindings, ) return self @@ -184,6 +189,7 @@ def _validate_derivation_collections( imports: tuple[ResolvedImportProvenance, ...], constraints: tuple[CapabilityConstraint, ...], explicitness: tuple[ExplicitnessProvenanceRecord, ...], + realization_designations: tuple[RealizationDesignationRecord, ...], root_bindings: tuple[ParameterBinding, ...] = (), ) -> None: _require_unique( @@ -198,6 +204,10 @@ def _validate_derivation_collections( (record.model_path for record in explicitness), "explicitness records must have unique model paths", ) + _require_unique( + ((record.namespace, record.field_pointer) for record in realization_designations), + "realization designation records must have unique scope identities", + ) binding_values = _binding_environment(imports, root_bindings) _validate_constraint_bindings(constraints, binding_values) @@ -297,5 +307,6 @@ def _contains_registry_userinfo(source: str) -> bool: "InstantiationProvenance", "ParameterBinding", "ResolvedImportProvenance", + "RealizationDesignationRecord", "SemanticDigest", ] diff --git a/implementations/python/packages/aces_sdl/realization_designation.py b/implementations/python/packages/aces_sdl/realization_designation.py new file mode 100644 index 000000000..b4f472a5d --- /dev/null +++ b/implementations/python/packages/aces_sdl/realization_designation.py @@ -0,0 +1,211 @@ +"""SEM-218 typed authoring surface and scoped realization cascade.""" + +from __future__ import annotations + +import re +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import dataclass +from enum import Enum + +from aces_contracts.vocabulary import Closure +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from ._base import SDLModel +from ._identifiers import PortableIdentifier + +_JSON_POINTER_PATTERN = r"^(?:/(?:[^~/]|~[01])*)*$" +_JSON_POINTER_RE = re.compile(_JSON_POINTER_PATTERN) + + +class AuthorRealizationPosture(str, Enum): + """Authored default posture, distinct from capability support modes.""" + + CLOSED = "closed" + OPEN = "open" + UNSPECIFIED = "unspecified" + + +class RealizationScopeDesignation(SDLModel): + """One typed lexical override at a canonical composed-model scope.""" + + namespace: tuple[PortableIdentifier, ...] = Field(default=(), max_length=31) + field_pointer: str = Field(min_length=1, max_length=4096, pattern=_JSON_POINTER_PATTERN) + posture: AuthorRealizationPosture + + @model_validator(mode="after") + def _validate_pointer(self) -> RealizationScopeDesignation: + if _JSON_POINTER_RE.fullmatch(self.field_pointer) is None: + raise ValueError("field_pointer must be a canonical RFC 6901 JSON Pointer") + return self + + +class RealizationDesignation(SDLModel): + """Scenario-root SEM-218 designation table.""" + + default: AuthorRealizationPosture = AuthorRealizationPosture.UNSPECIFIED + scopes: tuple[RealizationScopeDesignation, ...] = () + + @model_validator(mode="after") + def _validate_unique_scopes(self) -> RealizationDesignation: + identities = [(entry.namespace, entry.field_pointer) for entry in self.scopes] + if len(identities) != len(set(identities)): + raise ValueError("realization scopes must have unique namespace and field_pointer identities") + return self + + +class RealizationDesignationRecord(SDLModel): + """Portable authored designation carried across SDL document phases.""" + + model_config = ConfigDict(extra="forbid", frozen=True, allow_inf_nan=False) + + namespace: tuple[PortableIdentifier, ...] = Field(default=(), max_length=31) + field_pointer: str = Field(default="", max_length=4096, pattern=_JSON_POINTER_PATTERN) + posture: AuthorRealizationPosture + + @model_validator(mode="after") + def _validate_pointer(self) -> RealizationDesignationRecord: + if _JSON_POINTER_RE.fullmatch(self.field_pointer) is None: + raise ValueError("field_pointer must be an RFC 6901 JSON Pointer") + return self + + @property + def scope_reference(self) -> str: + namespace = ".".join(self.namespace) + pointer = self.field_pointer or "/" + return f"{namespace}#{pointer}" + + +@dataclass(frozen=True) +class RealizationResolution: + """Effective closure and its non-value-bearing governing source.""" + + closure: Closure | None + governing_scope: str | None + delegated: bool + source: str + + +ApparatusDefaultResolver = Callable[[str, tuple[str, ...]], Closure] + + +def designation_records( + designation: RealizationDesignation, + *, + namespace: tuple[str, ...] = (), +) -> tuple[RealizationDesignationRecord, ...]: + """Lower one authoring surface into portable declaration records.""" + + records = [ + RealizationDesignationRecord( + namespace=namespace, + field_pointer="", + posture=designation.default, + ) + ] + records.extend( + RealizationDesignationRecord( + namespace=(*namespace, *entry.namespace), + field_pointer=entry.field_pointer, + posture=entry.posture, + ) + for entry in designation.scopes + ) + return tuple(records) + + +def resolve_realization_designation( + records: Iterable[RealizationDesignationRecord], + *, + field_pointer: str, + owner_namespace: tuple[str, ...] = (), + apparatus_default: ApparatusDefaultResolver | None = None, +) -> RealizationResolution: + """Resolve the deterministic most-specific concrete lexical posture.""" + + if _JSON_POINTER_RE.fullmatch(field_pointer) is None: + raise ValueError("field_pointer must be an RFC 6901 JSON Pointer") + applicable = tuple( + record + for record in records + if _namespace_contains(record.namespace, owner_namespace) + and _pointer_contains(record.field_pointer, field_pointer) + ) + concrete = tuple(record for record in applicable if record.posture is not AuthorRealizationPosture.UNSPECIFIED) + if concrete: + governing = max(concrete, key=_specificity) + closure = Closure.OPEN_WORLD if governing.posture is AuthorRealizationPosture.OPEN else Closure.CLOSED_WORLD + return RealizationResolution(closure, governing.scope_reference, False, "scope") + if applicable: + governing = max(applicable, key=_specificity) + resolver = apparatus_default or _closed_apparatus_default + return RealizationResolution( + resolver(field_pointer, owner_namespace), + governing.scope_reference, + True, + "apparatus-default", + ) + return RealizationResolution(Closure.CLOSED_WORLD, None, False, "legacy-default") + + +def resolve_json_pointer_surface(root: object, pointer: str) -> tuple[bool, object]: + """Resolve a strict RFC 6901 pointer against a typed SDL surface.""" + + if _JSON_POINTER_RE.fullmatch(pointer) is None: + return False, None + current = root + for raw_segment in pointer.split("/")[1:]: + segment = raw_segment.replace("~1", "/").replace("~0", "~") + if isinstance(current, BaseModel) and segment in type(current).model_fields: + current = getattr(current, segment) + elif isinstance(current, Mapping) and segment in current: + current = current[segment] + elif ( + isinstance(current, Sequence) + and not isinstance(current, (str, bytes, bytearray)) + and segment.isdigit() + and int(segment) < len(current) + ): + current = current[int(segment)] + else: + return False, None + return True, current + + +def _closed_apparatus_default(_field_pointer: str, _owner_namespace: tuple[str, ...]) -> Closure: + return Closure.CLOSED_WORLD + + +def _namespace_contains(scope: tuple[str, ...], owner: tuple[str, ...]) -> bool: + return len(scope) <= len(owner) and owner[: len(scope)] == scope + + +def _pointer_segments(pointer: str) -> tuple[str, ...]: + return tuple(pointer.split("/")[1:]) + + +def _pointer_contains(scope: str, field_pointer: str) -> bool: + scope_segments = _pointer_segments(scope) + field_segments = _pointer_segments(field_pointer) + return len(scope_segments) <= len(field_segments) and field_segments[: len(scope_segments)] == scope_segments + + +def _specificity(record: RealizationDesignationRecord) -> tuple[int, int, str, str]: + return ( + len(_pointer_segments(record.field_pointer)), + len(record.namespace), + ".".join(record.namespace), + record.field_pointer, + ) + + +__all__ = [ + "ApparatusDefaultResolver", + "AuthorRealizationPosture", + "RealizationDesignation", + "RealizationDesignationRecord", + "RealizationResolution", + "RealizationScopeDesignation", + "designation_records", + "resolve_json_pointer_surface", + "resolve_realization_designation", +] diff --git a/implementations/python/packages/aces_sdl/realization_envelope.py b/implementations/python/packages/aces_sdl/realization_envelope.py index 649b85c3f..fa614066d 100644 --- a/implementations/python/packages/aces_sdl/realization_envelope.py +++ b/implementations/python/packages/aces_sdl/realization_envelope.py @@ -62,8 +62,10 @@ "RelationResult", "WitnessResult", "generate_negative_probes", + "effective_constraints", "member", "subsumes", + "tokenize_path", "witness", ] diff --git a/implementations/python/packages/aces_sdl/runtime_configuration.py b/implementations/python/packages/aces_sdl/runtime_configuration.py index 19cf9f9e9..148da419a 100644 --- a/implementations/python/packages/aces_sdl/runtime_configuration.py +++ b/implementations/python/packages/aces_sdl/runtime_configuration.py @@ -1,4 +1,4 @@ -"""Observed runtime configuration models for SDL nodes.""" +"""Declarative runtime contract models for SDL nodes.""" from collections.abc import Iterable from enum import Enum @@ -39,9 +39,6 @@ RuntimeContainerConfiguration, RuntimeDeviceMapping, RuntimeExtraHost, - RuntimeHealthcheckLog, - RuntimeHealthObservation, - RuntimeHealthStatus, RuntimeInitProcess, RuntimeNamespaceConfiguration, ) @@ -72,7 +69,6 @@ RuntimeNetworkBackendDetail, RuntimeNetworkDriver, RuntimeNetworkEndpoint, - RuntimeNetworkIdStability, RuntimeNetworkRealization, RuntimePublishedPort, ) @@ -117,9 +113,6 @@ "RuntimeFilesystemEntryType", "RuntimeFilesystemPresence", "RuntimeFilesystemStability", - "RuntimeHealthObservation", - "RuntimeHealthStatus", - "RuntimeHealthcheckLog", "RuntimeIdentityProvenance", "RuntimeInitProcess", "RuntimeLocalGroup", @@ -132,12 +125,9 @@ "RuntimeNetworkBackendDetail", "RuntimeNetworkDriver", "RuntimeNetworkEndpoint", - "RuntimeNetworkIdStability", "RuntimeNetworkRealization", "RuntimeOperationalPolicy", "RuntimePackage", - "RuntimePackageVulnerabilityFinding", - "RuntimePackageVulnerabilitySeverity", "RuntimeProcessCapabilityOverride", "RuntimeProcessIdentity", "RuntimeProcessRole", @@ -164,19 +154,8 @@ ] -class RuntimePackageVulnerabilitySeverity(str, Enum): - """Scanner-derived package finding severity.""" - - UNKNOWN = "unknown" - LOW = "low" - MEDIUM = "medium" - HIGH = "high" - CRITICAL = "critical" - OTHER = "other" - - class RuntimeEnvironmentValueClassification(str, Enum): - """Sensitivity classification for an observed runtime environment value.""" + """Sensitivity classification for a required runtime environment value.""" PLAIN = "plain" REDACTED = "redacted" @@ -193,7 +172,7 @@ class RuntimeEnvironmentValueClassification(str, Enum): class RuntimeEnvironmentVariableProvenance(str, Enum): - """Origin class for an observed runtime environment variable.""" + """Required origin class for a runtime environment variable.""" COMPOSE = "compose" IMAGE = "image" @@ -205,7 +184,7 @@ class RuntimeEnvironmentVariableProvenance(str, Enum): class RuntimeRestartPolicy(str, Enum): - """Portable restart policy classification observed at runtime.""" + """Portable restart policy required by the scenario.""" NO = "no" ALWAYS = "always" @@ -216,7 +195,7 @@ class RuntimeRestartPolicy(str, Enum): class RuntimeEnvironmentVariable(SDLModel): - """Observed runtime environment variable with provenance and sensitivity.""" + """Required runtime environment variable with provenance and sensitivity.""" name: str value: str = "" @@ -267,7 +246,7 @@ def validate_redacted_value(self) -> "RuntimeEnvironmentVariable": class RuntimeResourceLimits(SDLModel): - """Observed runtime/cgroup resource limits for a node.""" + """Required runtime/cgroup resource limits for a node.""" memory: int | str | None = None memory_swap: int | str | None = None @@ -293,7 +272,7 @@ def parse_count_limit(cls, v: int | str | None, info: ValidationInfo) -> int | s class RuntimeOperationalPolicy(SDLModel): - """Observed restart and resource-limit policy for a runtime node.""" + """Required restart and resource-limit policy for a runtime node.""" restart: RuntimeRestartPolicy | str = RuntimeRestartPolicy.UNKNOWN resource_limits: RuntimeResourceLimits | None = None @@ -308,7 +287,7 @@ def normalize_restart(cls, v: RuntimeRestartPolicy | str | bool) -> RuntimeResta class RuntimePackage(SDLModel): - """A package observed in a runtime image or node.""" + """A package required in a runtime image or node.""" manager: str name: str @@ -319,7 +298,7 @@ class RuntimePackage(SDLModel): class RuntimeDependencyManifest(SDLModel): - """A dependency manifest visible in the realized runtime artifact.""" + """A dependency manifest required in the runtime artifact.""" ecosystem: str path: str @@ -333,30 +312,6 @@ def validate_path(cls, v: str) -> str: return _abs_path_or_var(v, field_name="path") -class RuntimePackageVulnerabilityFinding(SDLModel): - """A scanner-derived CVE/advisory finding for an observed package.""" - - id: str - package_name: str - installed_version: str - severity: RuntimePackageVulnerabilitySeverity | str = RuntimePackageVulnerabilitySeverity.UNKNOWN - scanner: str - image_digest: str - scan_time: str - fixed_version: str = "" - advisory_url: str = "" - scanner_version: str = "" - scanner_database: str = "" - - @field_validator("severity", mode="before") - @classmethod - def normalize_severity( - cls, - v: RuntimePackageVulnerabilitySeverity | str, - ) -> RuntimePackageVulnerabilitySeverity | str: - return _parse_runtime_enum_or_var(v, RuntimePackageVulnerabilitySeverity, field_name="severity") - - def _reject_duplicate_keys(items: Iterable[object], *, attr: str, label: str) -> None: """Raise on the first repeated non-empty key read from ``attr``.""" seen: set[object] = set() @@ -370,7 +325,11 @@ def _reject_duplicate_keys(items: Iterable[object], *, attr: str, label: str) -> class RuntimeConfiguration(SDLModel): - """Observed runtime configuration facts attached to a VM node.""" + """Declarative runtime state required by a VM node. + + Captured observations remain in evidence records unless an author + deliberately promotes the fact to one of these contract fields. + """ mounts: list[RuntimeMount] = Field(default_factory=list) filesystem_inventory: list[RuntimeFilesystemEntry] = Field(default_factory=list) @@ -380,7 +339,6 @@ class RuntimeConfiguration(SDLModel): linux_capabilities: RuntimeCapabilityPolicy | None = None operational_policy: RuntimeOperationalPolicy | None = None container: RuntimeContainerConfiguration | None = None - health: RuntimeHealthObservation | None = None local_identity: RuntimeLocalIdentityInventory | None = None identity_authorities: list[_runtime_directory_identity.RuntimeIdentityAuthority] = Field(default_factory=list) file_services: list[_runtime_file_service.RuntimeFileService] = Field(default_factory=list) @@ -408,7 +366,6 @@ class RuntimeConfiguration(SDLModel): packages: list[RuntimePackage] = Field(default_factory=list) software_components: list[RuntimeSoftwareComponent] = Field(default_factory=list) dependency_manifests: list[RuntimeDependencyManifest] = Field(default_factory=list) - package_vulnerabilities: list[RuntimePackageVulnerabilityFinding] = Field(default_factory=list) @model_validator(mode="after") def validate_unique_runtime_entries(self) -> "RuntimeConfiguration": diff --git a/implementations/python/packages/aces_sdl/runtime_container.py b/implementations/python/packages/aces_sdl/runtime_container.py index 51988cc72..4335ec344 100644 --- a/implementations/python/packages/aces_sdl/runtime_container.py +++ b/implementations/python/packages/aces_sdl/runtime_container.py @@ -1,35 +1,22 @@ -"""Observed container runtime host/security and health models for SDL nodes.""" +"""Declarative container runtime requirements for SDL nodes.""" import re -from enum import Enum from typing import Any from pydantic import Field, ValidationInfo, field_validator, model_validator -from ._base import SDLModel, is_variable_ref, parse_bool_or_var, parse_int_or_var +from ._base import SDLModel, is_variable_ref, parse_bool_or_var from .runtime_values import ( absolute_path_or_var, coerce_string_list, parse_optional_bool_or_var, parse_ram, - parse_runtime_enum_or_var, validate_absolute_paths, ) -class RuntimeHealthStatus(str, Enum): - """Observed health status for a runtime node or container.""" - - NONE = "none" - STARTING = "starting" - HEALTHY = "healthy" - UNHEALTHY = "unhealthy" - UNKNOWN = "unknown" - OTHER = "other" - - class RuntimeNamespaceConfiguration(SDLModel): - """Observed namespace modes for a runtime node or container.""" + """Required namespace modes for a runtime node or container.""" cgroup: str = "" ipc: str = "" @@ -39,7 +26,7 @@ class RuntimeNamespaceConfiguration(SDLModel): class RuntimeDeviceMapping(SDLModel): - """A host device mapping observed in runtime configuration.""" + """A host device mapping required in runtime configuration.""" host_path: str container_path: str @@ -53,7 +40,7 @@ def validate_device_path(cls, v: str, info: ValidationInfo) -> str: class RuntimeExtraHost(SDLModel): - """An observed extra host mapping in runtime configuration.""" + """A required extra host mapping in runtime configuration.""" hostname: str address: str @@ -68,7 +55,7 @@ def validate_non_empty(cls, v: str, info: ValidationInfo) -> str: class RuntimeInitProcess(SDLModel): - """Observed backend-injected container init / PID-1 reaper configuration. + """Required backend-injected container init / PID-1 reaper configuration. Models authored container runtime intent — for example Docker Compose ``init: true`` causing PID 1 to be ``/sbin/docker-init`` (tini). This is @@ -130,7 +117,7 @@ def _seccomp_values_from_security_opt(security_opt: list[str]) -> list[str]: class RuntimeContainerConfiguration(SDLModel): - """Observed container runtime host and security configuration facts.""" + """Required container runtime host and security configuration facts.""" entrypoint: list[str] = Field(default_factory=list) command: list[str] = Field(default_factory=list) @@ -234,48 +221,3 @@ def validate_unique_container_entries(self) -> "RuntimeContainerConfiguration": raise ValueError(f"Duplicate runtime extra host '{host.hostname}'") seen_hosts.add(host.hostname) return self - - -class RuntimeHealthcheckLog(SDLModel): - """An observed runtime healthcheck log entry.""" - - start: str = "" - end: str = "" - exit_code: int | str | None = None - output: str = "" - output_redacted: bool | str = False - - @field_validator("exit_code", mode="before") - @classmethod - def parse_exit_code(cls, v: int | str | None) -> int | str | None: - return parse_int_or_var(v, minimum=0, field_name="exit_code") if v is not None else v - - @field_validator("output_redacted", mode="before") - @classmethod - def parse_output_redacted(cls, v: bool | str) -> bool | str: - return parse_bool_or_var(v, field_name="output_redacted") - - @model_validator(mode="after") - def validate_redacted_output(self) -> "RuntimeHealthcheckLog": - if self.output_redacted is True and self.output: - raise ValueError("redacted healthcheck output must omit output") - return self - - -class RuntimeHealthObservation(SDLModel): - """Observed runtime health status and healthcheck log facts.""" - - status: RuntimeHealthStatus | str = RuntimeHealthStatus.UNKNOWN - failing_streak: int | str | None = None - log: list[RuntimeHealthcheckLog] = Field(default_factory=list) - description: str = "" - - @field_validator("status", mode="before") - @classmethod - def normalize_status(cls, v: RuntimeHealthStatus | str) -> RuntimeHealthStatus | str: - return parse_runtime_enum_or_var(v, RuntimeHealthStatus, field_name="status") - - @field_validator("failing_streak", mode="before") - @classmethod - def parse_failing_streak(cls, v: int | str | None) -> int | str | None: - return parse_int_or_var(v, minimum=0, field_name="failing_streak") if v is not None else v diff --git a/implementations/python/packages/aces_sdl/runtime_network.py b/implementations/python/packages/aces_sdl/runtime_network.py index 32ee9b3ab..25276fc28 100644 --- a/implementations/python/packages/aces_sdl/runtime_network.py +++ b/implementations/python/packages/aces_sdl/runtime_network.py @@ -1,16 +1,8 @@ -"""Observed container network realization models for SDL nodes. - -These models express the container network facts observable from inside a -realized range or by a harness (see ADR-025) — per-network aliases and DNS -names, container hostname/domain identity, endpoint MAC addresses, realized -per-endpoint IP/prefix/gateway, backend network and endpoint identifiers with -explicit stability classification, host-published port bindings, and observable -backend driver/IPAM detail. - -This is observed runtime state attached to ``Node.runtime``. It is distinct -from the ``infrastructure`` topology declaration: an endpoint may *reference* a -declared switch-backed network by name, but backend-generated identifiers, -aliases, and harness-only observations are never topology declarations. +"""Declarative container network contract models for SDL nodes. + +Values authored here are exact, constrained, or explicitly open scenario +requirements. Generated backend identifiers and harness-only observations +belong in evidence records instead. """ import re @@ -27,7 +19,6 @@ from .runtime_values import ( coerce_string_list, ip_address_or_var, - parse_optional_bool_or_var, parse_runtime_enum_or_var, ) @@ -35,7 +26,6 @@ "RuntimeNetworkBackendDetail", "RuntimeNetworkDriver", "RuntimeNetworkEndpoint", - "RuntimeNetworkIdStability", "RuntimeNetworkRealization", "RuntimePublishedPort", ] @@ -47,21 +37,8 @@ _MAX_PORT = 65535 -class RuntimeNetworkIdStability(str, Enum): - """Stability class for a backend-generated network or endpoint identifier. - - Distinct from :class:`RuntimeFilesystemStability`: filesystem stability and - backend-identifier stability are different concepts (ADR-025). - """ - - STABLE = "stable" - EPHEMERAL = "ephemeral" - UNKNOWN = "unknown" - OTHER = "other" - - class RuntimeNetworkDriver(str, Enum): - """Observed backend network driver class for a realized network.""" + """Backend network driver class required by the scenario.""" BRIDGE = "bridge" OVERLAY = "overlay" @@ -74,7 +51,7 @@ class RuntimeNetworkDriver(str, Enum): def _mac_address_or_var(value: str, *, field_name: str) -> str: - """Validate an observed MAC address, allowing empty and ``${var}`` values.""" + """Validate a required MAC address, allowing empty and ``${var}`` values.""" if not value or is_variable_ref(value): return value if not isinstance(value, str) or not _MAC_ADDRESS_RE.fullmatch(value): @@ -83,7 +60,7 @@ def _mac_address_or_var(value: str, *, field_name: str) -> str: class RuntimeNetworkBackendDetail(SDLModel): - """Observed backend network driver and IPAM detail for an endpoint's network. + """Required backend network driver and IPAM detail for an endpoint's network. ``driver_options`` and ``ipam_options`` are the bounded extension seam for backend-native key/value facts; raw backend inspect payloads are not stored. @@ -102,7 +79,7 @@ def normalize_driver(cls, v: RuntimeNetworkDriver | str) -> RuntimeNetworkDriver class RuntimePublishedPort(SDLModel): - """An observed host-published port binding for a container endpoint. + """A required host-published port binding for a container endpoint. Host IP, host port, container port, and protocol are kept distinct; this is a runtime/host exposure fact, not a node service declaration (ADR-025). @@ -142,27 +119,20 @@ def validate_host_ip(cls, v: str) -> str: class RuntimeNetworkEndpoint(SDLModel): - """An observed container network endpoint (per-network attachment). + """A required container network attachment. ``network`` references a declared switch-backed infrastructure network by - name. Per-network ``aliases``, observed ``dns_names``, and backend-generated - ``generated_dns_names`` are kept as three distinct lists; generated names - are not stable scenario identity. + name. Runtime-generated identifiers and names are capture evidence, not + declaration identity. """ network: str - network_id: str = "" - network_id_stability: RuntimeNetworkIdStability | str = RuntimeNetworkIdStability.UNKNOWN - endpoint_id: str = "" - endpoint_id_stability: RuntimeNetworkIdStability | str = RuntimeNetworkIdStability.UNKNOWN - backend_generated: bool | str | None = None ip_address: str = "" ip_prefix_length: int | str | None = None gateway: str = "" mac_address: str = "" aliases: list[str] = Field(default_factory=list) dns_names: list[str] = Field(default_factory=list) - generated_dns_names: list[str] = Field(default_factory=list) backend: RuntimeNetworkBackendDetail | None = None description: str = "" @@ -173,20 +143,6 @@ def validate_network(cls, v: str) -> str: raise ValueError("network must be a non-empty string") return v - @field_validator("network_id_stability", "endpoint_id_stability", mode="before") - @classmethod - def normalize_id_stability( - cls, - v: RuntimeNetworkIdStability | str, - info: ValidationInfo, - ) -> RuntimeNetworkIdStability | str: - return parse_runtime_enum_or_var(v, RuntimeNetworkIdStability, field_name=info.field_name) - - @field_validator("backend_generated", mode="before") - @classmethod - def parse_backend_generated(cls, v: bool | str | None) -> bool | str | None: - return parse_optional_bool_or_var(v, field_name="backend_generated") - @field_validator("ip_address", "gateway") @classmethod def validate_addresses(cls, v: str, info: ValidationInfo) -> str: @@ -204,14 +160,14 @@ def parse_ip_prefix_length(cls, v: int | str | None) -> int | str | None: def validate_mac_address(cls, v: str) -> str: return _mac_address_or_var(v, field_name="mac_address") - @field_validator("aliases", "dns_names", "generated_dns_names", mode="before") + @field_validator("aliases", "dns_names", mode="before") @classmethod def coerce_name_lists(cls, v: Any) -> list[str]: return coerce_string_list(v) @model_validator(mode="after") def validate_unique_names(self) -> "RuntimeNetworkEndpoint": - for field_name in ("aliases", "dns_names", "generated_dns_names"): + for field_name in ("aliases", "dns_names"): values = getattr(self, field_name) if len(values) != len(set(values)): raise ValueError(f"Duplicate runtime network {field_name} entry on endpoint '{self.network}'") @@ -219,9 +175,9 @@ def validate_unique_names(self) -> "RuntimeNetworkEndpoint": class RuntimeNetworkRealization(SDLModel): - """Observed container network realization facts for a node. + """Declarative container network state for a node. - ``hostname`` and ``domainname`` are the container's network identity facts, + ``hostname`` and ``domainname`` are required container network identities, kept distinct from per-network aliases and DNS names. """ diff --git a/implementations/python/packages/aces_sdl/runtime_software.py b/implementations/python/packages/aces_sdl/runtime_software.py index 52aafd123..51a22a95b 100644 --- a/implementations/python/packages/aces_sdl/runtime_software.py +++ b/implementations/python/packages/aces_sdl/runtime_software.py @@ -1,4 +1,4 @@ -"""Observed runtime software component inventory models for SDL nodes.""" +"""Declarative runtime software component requirements for SDL nodes.""" from enum import Enum @@ -15,7 +15,7 @@ class RuntimeSoftwareComponentType(str, Enum): - """Portable type for a software component observed on a runtime node.""" + """Portable type for a software component required on a runtime node.""" APPLICATION = "application" FRAMEWORK = "framework" @@ -33,15 +33,11 @@ class RuntimeSoftwareComponentType(str, Enum): class RuntimeSoftwareComponentProvenance(str, Enum): - """Origin class for an observed runtime software component fact.""" + """Required source origin for a runtime software component.""" PACKAGE_MANAGER = "package_manager" DEPENDENCY_MANIFEST = "dependency_manifest" - SBOM = "sbom" - SCANNER = "scanner" IMAGE_METADATA = "image_metadata" - FILESYSTEM = "filesystem" - PROCESS_INSPECTION = "process_inspection" OPERATOR = "operator" SELF_REPORTED = "self_reported" UNKNOWN = "unknown" @@ -49,7 +45,7 @@ class RuntimeSoftwareComponentProvenance(str, Enum): class RuntimeSoftwareComponentHash(SDLModel): - """Digest attached to an observed runtime software component.""" + """Digest required for a runtime software component.""" algorithm: str value: str @@ -63,7 +59,7 @@ def validate_non_empty(cls, v: str) -> str: class RuntimeSoftwareComponent(SDLModel): - """A software component observed as part of a runtime node's state.""" + """A software component required as part of a runtime node's state.""" component_id: str name: str diff --git a/implementations/python/packages/aces_sdl/scenario.py b/implementations/python/packages/aces_sdl/scenario.py index d16ec8bd6..f98cf16b9 100644 --- a/implementations/python/packages/aces_sdl/scenario.py +++ b/implementations/python/packages/aces_sdl/scenario.py @@ -51,6 +51,7 @@ _json_value_equal, ) from .propositions import Assertion, Proposition +from .realization_designation import RealizationDesignation from .relationships import Relationship from .runtime_forwarding_agent import RuntimeForwardingAgent from .variables import Variable @@ -354,6 +355,10 @@ class Scenario(ScenarioContent): module: ModuleDescriptor | None = None imports: list[ImportDecl] = Field(default_factory=list) + realization: RealizationDesignation | None = Field( + default=None, + json_schema_extra={"x-aces-realization-dimension": False}, + ) variables: VariableDefinitions = Field( default_factory=dict, json_schema_extra={"additionalProperties": False}, diff --git a/implementations/python/packages/aces_sdl/validator/_core.py b/implementations/python/packages/aces_sdl/validator/_core.py index 74da8a583..767f8bbc5 100644 --- a/implementations/python/packages/aces_sdl/validator/_core.py +++ b/implementations/python/packages/aces_sdl/validator/_core.py @@ -242,6 +242,7 @@ def validate(self) -> None: self._verify_participant_outcomes() self._verify_evidence_requirements() self._verify_variables() + self._verify_realization_designations() self._verify_explicitness() self._collect_advisories() diff --git a/implementations/python/packages/aces_sdl/validator/_sections.py b/implementations/python/packages/aces_sdl/validator/_sections.py index 1433c81bc..415a759af 100644 --- a/implementations/python/packages/aces_sdl/validator/_sections.py +++ b/implementations/python/packages/aces_sdl/validator/_sections.py @@ -8,7 +8,8 @@ from .._base import VARIABLE_TOKEN_RE from ..entities import flatten_entities from ..explicitness import classify_scenario_explicitness -from ..scenario import Scenario +from ..realization_designation import designation_records, resolve_json_pointer_surface +from ..scenario import ExpandedScenario, Scenario from ._support import _topological_sort @@ -58,6 +59,24 @@ def _verify_explicitness(self) -> None: for error in result.errors: self._err(error) + def _verify_realization_designations(self) -> None: + records = () + known_namespaces: set[tuple[str, ...]] = {()} + if isinstance(self._s, Scenario) and self._s.realization is not None: + records = designation_records(self._s.realization) + elif isinstance(self._s, ExpandedScenario): + records = self._s.expansion_provenance.realization_designations + known_namespaces.update(record.namespace for record in self._s.expansion_provenance.imports) + for record in records: + if record.namespace not in known_namespaces: + self._err("Realization designation references an unresolved module namespace") + found, _value = resolve_json_pointer_surface(self._s, record.field_pointer) + if not found: + self._err( + "Realization designation field_pointer does not resolve to a typed SDL surface: " + f"{record.field_pointer or '/'}" + ) + def _all_named_elements(self) -> set[str]: """Collect all named element keys across all scenario sections.""" return set(self._named_ref_index().keys()) diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index ce1f9ef3b..217522487 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "aces-sdl" -version = "0.19.1" +version = "0.20.0" description = "Backend-agnostic cyber range scenario description language and runtime." dynamic = ["readme"] requires-python = ">=3.11" diff --git a/implementations/python/tests/test_backend_conformance_cli.py b/implementations/python/tests/test_backend_conformance_cli.py index b60a2984d..3442b1a7d 100644 --- a/implementations/python/tests/test_backend_conformance_cli.py +++ b/implementations/python/tests/test_backend_conformance_cli.py @@ -48,6 +48,15 @@ def test_backend_conformance_cli_passes_for_provisioning_only_profile(): payload = json.loads(result.output) assert payload["profile"] == "provisioning-only" assert payload["passed"] is True + claim = payload["claim"] + assert claim["taxonomy_id"] == "aces-behavioral-relations" + assert claim["taxonomy_revision"] == "rev1" + assert claim["relation_id"] == "bounded-probe-success" + assert claim["quantifier_scope"] == "finite-cases" + assert claim["evidence_scope"] == "finite" + assert claim["observation_projection_ref"] == "backend-conformance-case-report" + assert claim["evidence_refs"] + assert any("equivalence" in nonclaim for nonclaim in claim["explicit_non_claims"]) contract_names = {case["contract_name"] for case in payload["cases"]} assert contract_names == { "backend-manifest-v2", diff --git a/implementations/python/tests/test_behavioral_relation_claims.py b/implementations/python/tests/test_behavioral_relation_claims.py new file mode 100644 index 000000000..f6a893cc8 --- /dev/null +++ b/implementations/python/tests/test_behavioral_relation_claims.py @@ -0,0 +1,99 @@ +"""Semantic policy tests for behavioral-relation claim surfaces.""" + +from __future__ import annotations + +from copy import deepcopy + +from aces_contracts.behavioral_relations import load_behavioral_relation_catalog +from tools.check_behavioral_relation_claims import ( + _validate_claim_text, + _validate_structured_bindings, +) + + +def _valid_binding() -> dict[str, object]: + return { + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "relation_id": "bounded-probe-success", + "subject": "Named backend fixture cases", + "left_carrier_ref": "backend-target:stub", + "right_carrier_ref": "backend-profile:provisioning-only", + "observation_projection_ref": "backend-conformance-case-report", + "observation_projection_revision": "rev1", + "quantifier_scope": "finite-cases", + "evidence_scope": "finite", + "evidence_boundary": "Only the two named fixture cases.", + "assurance_status": "tested", + "evidence_refs": ["conformance-case:manifest:valid"], + "limitations": ["Unexecuted inputs are outside the evidence boundary."], + "explicit_non_claims": ["Does not establish trace equivalence or bisimulation."], + } + + +def test_unbound_positive_behavioral_equivalence_claim_fails(): + failures = _validate_claim_text( + "The two backend implementations are behaviorally equivalent.", + "docs/conformance/example.md", + ) + + assert {failure.rule_id for failure in failures} == {"behavioral-relation-unbound-positive-claim"} + + +def test_explicit_weaker_nonclaim_is_permitted(): + assert not _validate_claim_text( + "The finite probe passes, but it does not establish behavioral equivalence or bisimulation.", + "docs/conformance/example.md", + ) + + +def test_relation_identity_and_evidence_boundary_permit_a_scoped_claim(): + assert not _validate_claim_text( + "Relation: `participant-projected-history-equivalence`. The two histories are equivalent only under " + "observation projection `participant-observation-boundary/v1`; the evidence boundary is the named " + "terminal observations in run-7.", + "docs/conformance/example.md", + ) + + +def test_legacy_behavior_history_alias_is_not_a_relation_binding(): + failures = _validate_claim_text( + '"form": "behavior-history-equivalent", "evidence_boundary": "one terminal record"', + "implementations/python/packages/example.py", + ) + + assert failures + assert "participant-projected-history-equivalence" in failures[0].message + + +def test_structured_claims_resolve_against_the_canonical_catalog(): + catalog = load_behavioral_relation_catalog() + assert not _validate_structured_bindings( + {"claim": _valid_binding()}, + catalog, + "contracts/example.json", + ) + + unknown = deepcopy(_valid_binding()) + unknown["relation_id"] = "unknown-relation" + failures = _validate_structured_bindings( + {"claim": unknown}, + catalog, + "contracts/example.json", + ) + assert {failure.rule_id for failure in failures} == {"behavioral-relation-binding-invalid"} + + +def test_bounded_binding_cannot_be_promoted_to_universal_scope(): + catalog = load_behavioral_relation_catalog() + promoted = deepcopy(_valid_binding()) + promoted["relation_id"] = "trace-equivalence" + promoted["quantifier_scope"] = "all-traces" + + failures = _validate_structured_bindings( + {"claim": promoted}, + catalog, + "contracts/example.json", + ) + assert failures + assert "universal quantification" in failures[0].message diff --git a/implementations/python/tests/test_behavioral_relations.py b/implementations/python/tests/test_behavioral_relations.py new file mode 100644 index 000000000..e5d1a63fe --- /dev/null +++ b/implementations/python/tests/test_behavioral_relations.py @@ -0,0 +1,367 @@ +"""Behavioral-relation authority, examples, and claim-binding tests.""" + +from __future__ import annotations + +import json +from collections import defaultdict, deque +from copy import deepcopy +from pathlib import Path + +import pytest +from aces_conformance.conformance import _fixture_case_diagnostics +from aces_contracts.behavioral_relations import ( + ExampleTransitionModel, + ExampleTransitionSystemModel, + load_behavioral_relation_catalog, +) +from aces_contracts.contracts import ( + BehavioralClaimBindingModel, + ExperimentStudyModel, + schema_bundle, +) +from aces_contracts.scientific_completeness import load_scientific_completeness_taxonomy +from hypothesis import assume, given, settings +from hypothesis import strategies as st +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +REQUIRED_RELATION_IDS = { + "structural-validity", + "semantic-validity", + "capability-declaration", + "profile-satisfaction", + "bounded-probe-success", + "canonical-artifact-identity", + "realization-envelope-membership", + "realization-envelope-subsumption", + "trace-inclusion", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "participant-projected-history-equivalence", + "epistemic-indistinguishability", + "alternating-strategic-equivalence", + "probabilistic-bisimulation", + "statistical-similarity", + "statistical-equivalence", + "empirical-adequacy", +} + + +def _bounded_empirical_claim() -> dict[str, object]: + return { + "taxonomy_id": "aces-behavioral-relations", + "taxonomy_revision": "rev1", + "relation_id": "empirical-adequacy", + "subject": "TechVault baseline study", + "left_carrier_ref": "study-techvault-baseline@1.0.0", + "observation_projection_ref": "analysis-plan:baseline-proportion", + "observation_projection_revision": "experiment-study/v1", + "quantifier_scope": "sampled-population", + "evidence_scope": "statistical", + "evidence_boundary": "The one predeclared baseline condition and included evaluation runs.", + "assurance_status": "deliberately-unproved", + "evidence_refs": ["analysis_plan:baseline-proportion"], + "limitations": ["The fixture has one run and cannot establish population adequacy."], + "explicit_non_claims": ["No behavioral equivalence or generalizability claim."], + } + + +def _transition_index(system): + index = defaultdict(list) + for transition in system.transitions: + index[transition.source].append((transition.action, transition.target)) + return index + + +def _trace_exists(system, actions: tuple[str, ...]) -> bool: + states = {system.initial_state} + index = _transition_index(system) + for action in actions: + states = {target for state in states for candidate, target in index[state] if candidate == action} + return bool(states) + + +def _strongly_bisimilar(left, right) -> bool: + left_index = _transition_index(left) + right_index = _transition_index(right) + candidates = {(lstate, rstate) for lstate in left.states for rstate in right.states} + changed = True + while changed: + changed = False + for pair in tuple(candidates): + lstate, rstate = pair + left_ok = all( + any( + action == other_action and (target, other_target) in candidates + for other_action, other_target in right_index[rstate] + ) + for action, target in left_index[lstate] + ) + right_ok = all( + any( + action == other_action and (other_target, target) in candidates + for other_action, other_target in left_index[lstate] + ) + for action, target in right_index[rstate] + ) + if not left_ok or not right_ok: + candidates.remove(pair) + changed = True + return (left.initial_state, right.initial_state) in candidates + + +def _tau_closure(system, states: set[str], tau: str) -> set[str]: + index = _transition_index(system) + closure = set(states) + queue = deque(states) + while queue: + state = queue.popleft() + for action, target in index[state]: + if action == tau and target not in closure: + closure.add(target) + queue.append(target) + return closure + + +def _weak_trace_exists(system, actions: tuple[str, ...], tau: str) -> bool: + index = _transition_index(system) + states = _tau_closure(system, {system.initial_state}, tau) + for action in actions: + targets = {target for state in states for candidate, target in index[state] if candidate == action} + states = _tau_closure(system, targets, tau) + return bool(states) + + +def test_authoritative_catalog_covers_required_relation_classes_and_dimensions(): + catalog = load_behavioral_relation_catalog() + + assert catalog.schema_version == "behavioral-relations/v1" + assert catalog.taxonomy_id == "aces-behavioral-relations" + assert set(catalog.relations) >= REQUIRED_RELATION_IDS + for relation_id, relation in catalog.relations.items(): + assert relation.left_carrier + assert relation.right_carrier + assert relation.direction + assert relation.quantification.states + assert relation.quantification.traces + assert relation.preservation.proof_obligation + assert relation.bounded_evidence + assert relation.explicit_non_claims + assert relation.source_refs + assert set(relation.dimensions.model_dump()) == { + "nondeterminism", + "concurrency", + "probability", + "time", + "partial_order", + }, relation_id + + +def test_catalog_bibliography_claim_surfaces_and_relation_references_resolve(): + catalog = load_behavioral_relation_catalog() + source_ids = {source.source_id for source in catalog.bibliography} + + assert source_ids >= { + "park-1981", + "milner-1980", + "van-glabbeek-1990", + "abadi-lamport-1991", + "lynch-vaandrager-1995", + "fagin-halpern-moses-vardi-1995", + "alur-henzinger-kupferman-vardi-1998", + "alur-henzinger-kupferman-2002", + } + assert all(source.immutable_locator.kind in {"doi", "isbn"} for source in catalog.bibliography) + assert all(set(relation.source_refs) <= source_ids for relation in catalog.relations.values()) + assert {surface.surface_id for surface in catalog.claim_surfaces} == { + "sdl-transformation", + "backend-realization", + "backend-comparison", + "participant-visible-behavior", + "multi-agent-interaction", + "independent-adequacy-study", + } + assert all( + set(surface.intended_relation_ids) <= set(catalog.relations) + and set(surface.prohibited_relation_ids) <= set(catalog.relations) + for surface in catalog.claim_surfaces + ) + + +def test_finite_probe_counterexample_passes_probe_but_fails_strong_bisimulation(): + example = load_behavioral_relation_catalog().worked_examples["finite-probe-counterexample"] + + trace = tuple(example.tested_visible_trace) + assert _trace_exists(example.left_system, trace) + assert _trace_exists(example.right_system, trace) + assert not _strongly_bisimilar(example.left_system, example.right_system) + assert example.expected_strong_bisimulation is False + + +def test_hidden_action_example_distinguishes_strong_from_weak_matching(): + example = load_behavioral_relation_catalog().worked_examples["hidden-action-counterexample"] + + trace = tuple(example.tested_visible_trace) + assert not _strongly_bisimilar(example.left_system, example.right_system) + assert _weak_trace_exists(example.left_system, trace, example.hidden_action) + assert _weak_trace_exists(example.right_system, trace, example.hidden_action) + assert example.expected_strong_bisimulation is False + assert example.expected_weak_matching is True + + +@settings(max_examples=40) +@given( + visible=st.text(alphabet="abcxyz", min_size=1, max_size=5), + unmatched=st.text(alphabet="abcxyz", min_size=1, max_size=5), +) +def test_any_unmatched_initial_branch_refutes_strong_bisimulation_after_a_shared_probe( + visible: str, + unmatched: str, +): + assume(visible != unmatched) + left = ExampleTransitionSystemModel( + states=["l0", "l1", "l2"], + initial_state="l0", + transitions=[ + ExampleTransitionModel(source="l0", action=visible, target="l1"), + ExampleTransitionModel(source="l0", action=unmatched, target="l2"), + ], + ) + right = ExampleTransitionSystemModel( + states=["r0", "r1"], + initial_state="r0", + transitions=[ExampleTransitionModel(source="r0", action=visible, target="r1")], + ) + + assert _trace_exists(left, (visible,)) + assert _trace_exists(right, (visible,)) + assert not _strongly_bisimilar(left, right) + + +@settings(max_examples=40) +@given( + visible=st.text(alphabet="abcxyz", min_size=1, max_size=5), + hidden=st.text(alphabet="tuv", min_size=1, max_size=5), +) +def test_a_generated_hidden_prefix_separates_strong_matching_from_weak_visible_trace_matching( + visible: str, + hidden: str, +): + assume(visible != hidden) + abstract = ExampleTransitionSystemModel( + states=["a0", "a1"], + initial_state="a0", + transitions=[ExampleTransitionModel(source="a0", action=visible, target="a1")], + ) + concrete = ExampleTransitionSystemModel( + states=["c0", "c1", "c2"], + initial_state="c0", + transitions=[ + ExampleTransitionModel(source="c0", action=hidden, target="c1"), + ExampleTransitionModel(source="c1", action=visible, target="c2"), + ], + ) + + assert not _strongly_bisimilar(abstract, concrete) + assert _weak_trace_exists(abstract, (visible,), hidden) + assert _weak_trace_exists(concrete, (visible,), hidden) + + +def test_claim_binding_rejects_bounded_evidence_promoted_to_universal_claim(): + with pytest.raises(ValidationError, match="universal quantification requires model-check or proof evidence"): + BehavioralClaimBindingModel( + taxonomy_id="aces-behavioral-relations", + taxonomy_revision="rev1", + relation_id="trace-equivalence", + subject="two finite backend runs", + left_carrier_ref="backend-run:left", + right_carrier_ref="backend-run:right", + quantifier_scope="all-traces", + evidence_scope="finite", + evidence_boundary="one named probe trace", + assurance_status="tested", + limitations=["Only one trace was exercised."], + explicit_non_claims=["Does not establish trace equivalence."], + ) + + +def test_schema_bundle_publishes_behavioral_relation_catalog(): + schema = schema_bundle()["behavioral-relations-v1"] + + assert schema["properties"]["schema_version"]["const"] == "behavioral-relations/v1" + assert schema["properties"]["relations"]["minProperties"] == 1 + assert {item["id"] for item in schema["x-aces-invariants"]} == {"behavioral-relations-reference-resolution"} + + +@pytest.mark.parametrize( + ("fixture", "valid"), + [ + ("valid/reference.json", True), + ("invalid/missing-taxonomy-id.json", False), + ], +) +def test_behavioral_relation_catalog_fixtures_exercise_conformance(fixture: str, valid: bool): + path = REPO_ROOT / "contracts/fixtures/concept-authority/behavioral-relations-v1" / fixture + diagnostics = _fixture_case_diagnostics( + "behavioral-relations-v1", + json.loads(path.read_text(encoding="utf-8")), + ) + assert (not diagnostics) is valid + + +def test_scientific_completeness_profiles_bind_claims_and_nonclaims_to_catalog(): + catalog = load_behavioral_relation_catalog() + taxonomy = load_scientific_completeness_taxonomy() + + for profile in taxonomy.profiles: + assert profile.behavioral_claims, profile.profile_id + assert profile.non_claimed_relation_ids, profile.profile_id + assert {claim.relation_id for claim in profile.behavioral_claims} <= set(catalog.relations) + assert set(profile.non_claimed_relation_ids) <= set(catalog.relations) + benchmark = next( + profile for profile in taxonomy.profiles if profile.profile_id == "reproducible-benchmark-study-input" + ) + assert {claim.relation_id for claim in benchmark.behavioral_claims} == {"profile-satisfaction"} + assert "empirical-adequacy" in benchmark.non_claimed_relation_ids + assert "trace-equivalence" in benchmark.non_claimed_relation_ids + + +def test_scientific_completeness_profile_rejects_unknown_relation_binding(): + payload = json.loads( + (REPO_ROOT / "contracts/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json").read_text( + encoding="utf-8" + ) + ) + payload["profiles"][0]["behavioral_claims"][0]["relation_id"] = "unknown-relation" + taxonomy_type = type(load_scientific_completeness_taxonomy()) + + with pytest.raises(ValidationError, match="unknown relation"): + taxonomy_type.model_validate(payload) + + +def test_study_claims_are_revisioned_bounded_and_required_for_claim_bearing_studies(): + fixture_path = REPO_ROOT / "contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json" + payload = json.loads(fixture_path.read_text(encoding="utf-8")) + payload["behavioral_claims"] = [_bounded_empirical_claim()] + + study = ExperimentStudyModel.model_validate(payload) + assert study.behavioral_claims[0].relation_id == "empirical-adequacy" + + missing_claims = deepcopy(payload) + del missing_claims["behavioral_claims"] + with pytest.raises(ValidationError, match="behavioral claim"): + ExperimentStudyModel.model_validate(missing_claims) + + +def test_study_rejects_unknown_behavioral_relation(): + fixture_path = REPO_ROOT / "contracts/fixtures/experiment-core/experiment-study-v1/valid/reference.json" + payload = json.loads(fixture_path.read_text(encoding="utf-8")) + payload["behavioral_claims"] = [_bounded_empirical_claim()] + payload["behavioral_claims"][0]["relation_id"] = "unknown-relation" + + with pytest.raises(ValidationError, match="unknown relation"): + ExperimentStudyModel.model_validate(payload) diff --git a/implementations/python/tests/test_corpus_packaging.py b/implementations/python/tests/test_corpus_packaging.py index aa9bcd87d..434f012ca 100644 --- a/implementations/python/tests/test_corpus_packaging.py +++ b/implementations/python/tests/test_corpus_packaging.py @@ -35,7 +35,7 @@ "profiles": "aces_contracts/_corpus/profiles/backend/provisioning-only.json", "scientific-completeness": "aces_contracts/_corpus/profiles/scientific-completeness/scientific-scenario-completeness-rev1.json", "fixtures": "aces_contracts/_corpus/fixtures/", - "concept-authority": "aces_contracts/_corpus/concept-authority/controlled-vocabularies-v1.json", + "concept-authority": "aces_contracts/_corpus/concept-authority/behavioral-relations-v1.json", "schemas": "aces_contracts/_corpus/schemas/", "provenance": "aces_contracts/_corpus/provenance/sdl-lineage-ledger-v1.json", } diff --git a/implementations/python/tests/test_design_drift_audit.py b/implementations/python/tests/test_design_drift_audit.py index c17eb2e4a..d0523e038 100644 --- a/implementations/python/tests/test_design_drift_audit.py +++ b/implementations/python/tests/test_design_drift_audit.py @@ -92,9 +92,11 @@ def test_scenario_delivery_drift_findings_have_evidence_fields() -> None: assert field in section -def test_sections_runtime_summary_uses_corrected_boundary() -> None: +def test_sections_runtime_summary_uses_authored_evidence_boundary() -> None: text = SECTIONS_DOC.read_text(encoding="utf-8") assert "not authored deployable\nfeatures or exposed network services" not in text - assert "participant-observable and analysis-relevant runtime state" in text - assert "does not exclude host-published\nbindings" in text + assert "`runtime` is authored declarative contract state" in text + assert "A value does not become an SDL\nrequirement merely because Docker" in text + assert "Container health results are evidence" in text + assert "Docker network IDs, endpoint IDs, generated DNS names" in text diff --git a/implementations/python/tests/test_language_service.py b/implementations/python/tests/test_language_service.py index 6ea712f2e..11590fb06 100644 --- a/implementations/python/tests/test_language_service.py +++ b/implementations/python/tests/test_language_service.py @@ -40,6 +40,13 @@ def test_language_completions_include_top_level_sections_and_reference_targets() top_level_labels = {item["label"] for item in top_level["items"]} assert "nodes" in top_level_labels assert "workflows" in top_level_labels + assert "realization" in top_level_labels + + realization_fields = language_completions( + "name: x\nrealization: {}\n", + cursor_path="/realization", + ) + assert {item["label"] for item in realization_fields["items"]} == {"default", "scopes"} feature_refs = language_completions( SAMPLE_SDL, diff --git a/implementations/python/tests/test_libvirt_backend_techvault_integration.py b/implementations/python/tests/test_libvirt_backend_techvault_integration.py index 759ffc704..331614399 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_integration.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_integration.py @@ -109,7 +109,18 @@ def test_techvault_operational_scenario_drives_full_libvirt_surface(): manager = RuntimeManager(target) scenario = parse_sdl((EXAMPLES_DIR / "techvault-operational.sdl.yaml").read_text(encoding="utf-8")) - execution_plan = manager.plan(scenario) + admission_plan = manager.plan(scenario) + + assert not admission_plan.is_valid + assert any(diagnostic.code == "evaluator.missing" for diagnostic in admission_plan.diagnostics) + + provisioning_projection = scenario.model_copy( + update={ + "conditions": {}, + "nodes": {name: node.model_copy(update={"conditions": {}}) for name, node in scenario.nodes.items()}, + } + ) + execution_plan = manager.plan(provisioning_projection) assert execution_plan.is_valid assert execution_plan.model.scenario_name == "techvault" diff --git a/implementations/python/tests/test_mcp_server.py b/implementations/python/tests/test_mcp_server.py index 2de04c535..b43d80643 100644 --- a/implementations/python/tests/test_mcp_server.py +++ b/implementations/python/tests/test_mcp_server.py @@ -728,11 +728,11 @@ def test_intended_use_profile_exposes_blockers_and_authoring_route(self, server) profile = payload["profile"] assert profile["aces_delivery"]["complete"] is False - assert "behavioral-relation-taxonomy" in profile["aces_delivery"]["blocking_concern_ids"] + assert "behavioral-relation-taxonomy" not in profile["aces_delivery"]["blocking_concern_ids"] assert "experiment_validate" in profile["next_tools"] required = {item["concern_id"]: item for item in profile["required_concerns"]} - assert required["behavioral-relation-taxonomy"]["status"] == "missing" - assert required["behavioral-relation-taxonomy"]["issue_refs"] == ["#747"] + assert required["behavioral-relation-taxonomy"]["status"] == "implemented" + assert required["behavioral-relation-taxonomy"]["issue_refs"] == [] assert payload["scenario_assessment"]["performed"] is False def test_intended_use_profile_rejects_unknown_id(self, server): diff --git a/implementations/python/tests/test_realization_envelope_relation.py b/implementations/python/tests/test_realization_envelope_relation.py index 4ed5349c3..e79fa7de9 100644 --- a/implementations/python/tests/test_realization_envelope_relation.py +++ b/implementations/python/tests/test_realization_envelope_relation.py @@ -37,6 +37,7 @@ parse_sdl, ) from aces_sdl._realization_envelope_domains import _MISSING, default_witness_value, out_of_domain_value +from aces_sdl._realization_envelope_engine import effective_constraints from aces_sdl.realization_envelope import generate_negative_probes, member, subsumes, witness from aces_sdl.scenario import InstantiatedScenario, Scenario from hypothesis import given, settings @@ -127,6 +128,16 @@ def test_contract_rejects_unknown_field() -> None: RealizationEnvelopeModel.model_validate({"id": "x", "scope": "scenario", "unexpected": True}) +@pytest.mark.parametrize("path", ["nodes.web]junk.os", "nodes..web.os", "nodes[nope].os"]) +def test_contract_rejects_partially_tokenizable_binding_paths(path: str) -> None: + with pytest.raises(ValidationError): + EnvelopeBinding( + path=path, + scope=EnvelopeScope.FIELD, + posture=Posture.OPEN, + ) + + def test_contract_rejects_arbitrary_predicate_domain() -> None: # The closed discriminated union is the portability guarantee: no predicate # domain kind exists, so one cannot be expressed (envelope-semantics.md R3). @@ -483,6 +494,181 @@ def test_open_posture_widens_overrideable_record_leaf() -> None: assert member(windows, env).holds +def test_binding_scope_breaks_equal_path_ties_independent_of_list_order() -> None: + def envelope(bindings: list[EnvelopeBinding]) -> RealizationEnvelopeModel: + return RealizationEnvelopeModel( + id="scope-order", + scope=EnvelopeScope.SCENARIO, + domains={ + "wide": EnumDomain(values=["linux", "windows"]), + "linux": ExactDomain(value="linux"), + }, + bindings=bindings, + ) + + outer = EnvelopeBinding( + path="nodes.web.os", + scope=EnvelopeScope.SCENARIO, + posture=Posture.CONSTRAINED, + domain="wide", + overrideable=True, + ) + leaf = EnvelopeBinding( + path="nodes.web.os", + scope=EnvelopeScope.FIELD, + posture=Posture.EXACT, + domain="linux", + ) + windows = _instantiate("name: scope-order\nnodes:\n web: {type: vm, os: windows}\n") + linux = _instantiate("name: scope-order\nnodes:\n web: {type: vm, os: linux}\n") + + for bindings in ([outer, leaf], [leaf, outer]): + env = envelope(bindings) + assert member(linux, env).holds + result = member(windows, env) + assert not result.holds + assert not any("non-overrideable-widen" in diagnostic.code for diagnostic in result.diagnostics) + + +def test_topology_and_app_bindings_conflict_as_sibling_scopes() -> None: + domains = { + "linux": ExactDomain(value="linux"), + "windows": ExactDomain(value="windows"), + } + bindings = [ + EnvelopeBinding( + path="nodes.web.os", + scope=EnvelopeScope.TOPOLOGY, + posture=Posture.EXACT, + domain="linux", + ), + EnvelopeBinding( + path="nodes.web.os", + scope=EnvelopeScope.APP, + posture=Posture.EXACT, + domain="windows", + ), + ] + with pytest.raises(ValidationError, match="conflicting equal-specificity bindings"): + RealizationEnvelopeModel( + id="sibling-binding-conflict", + scope=EnvelopeScope.SCENARIO, + domains=domains, + bindings=bindings, + ) + + +def test_topology_and_app_closure_conflicts_as_sibling_scopes() -> None: + closure = [ + ClosureOverlay( + path="nodes.web", + scope=EnvelopeScope.TOPOLOGY, + closure=Closure.CLOSED_WORLD, + ), + ClosureOverlay( + path="nodes.web", + scope=EnvelopeScope.APP, + closure=Closure.OPEN_WORLD, + ), + ] + with pytest.raises(ValidationError, match="conflicting equal-specificity closure overlays"): + RealizationEnvelopeModel( + id="sibling-closure-conflict", + scope=EnvelopeScope.SCENARIO, + closure=closure, + ) + + +def test_open_world_overlay_removes_inherited_closed_state() -> None: + env = RealizationEnvelopeModel( + id="open-overlay", + scope=EnvelopeScope.SCENARIO, + domains={ + "node": RecordDomain(fields={"type": "vm"}, extra=False), + "vm": ExactDomain(value="vm"), + }, + bindings=[ + EnvelopeBinding( + path="nodes.web", + scope=EnvelopeScope.NODE, + posture=Posture.CONSTRAINED, + domain="node", + ) + ], + closure=[ + ClosureOverlay( + path="nodes.web", + scope=EnvelopeScope.NODE, + closure=Closure.OPEN_WORLD, + ) + ], + ) + instance = _instantiate('name: open-overlay\nnodes:\n web: {type: vm, os: linux, os_version: "9"}\n') + + assert member(instance, env).holds + + +def test_open_descendant_shadows_closed_root_without_opening_siblings() -> None: + env = RealizationEnvelopeModel( + id="closed-root-open-node", + scope=EnvelopeScope.SCENARIO, + domains={"name": ExactDomain(value="scoped")}, + bindings=[ + EnvelopeBinding( + path="name", + scope=EnvelopeScope.SCENARIO, + posture=Posture.EXACT, + domain="name", + ) + ], + closure=[ + ClosureOverlay(path="", scope=EnvelopeScope.SCENARIO, closure=Closure.CLOSED_WORLD), + ClosureOverlay(path="nodes.web", scope=EnvelopeScope.NODE, closure=Closure.OPEN_WORLD), + ], + ) + web_only = _instantiate("name: scoped\nnodes:\n web: {type: vm, os: linux, resources: {ram: 1 gib, cpu: 1}}\n") + with_sibling = _instantiate( + "name: scoped\nnodes:\n" + " web: {type: vm, os: linux, resources: {ram: 1 gib, cpu: 1}}\n" + " worker: {type: vm, resources: {ram: 1 gib, cpu: 1}}\n" + ) + + assert member(web_only, env).holds + sibling_result = member(with_sibling, env) + assert not sibling_result.holds + assert any(diagnostic.address == "nodes.worker" for diagnostic in sibling_result.diagnostics) + + +def test_open_world_overlay_uses_token_ancestry_for_indexed_descendants() -> None: + env = RealizationEnvelopeModel( + id="indexed-open-overlay", + scope=EnvelopeScope.SCENARIO, + domains={ + "value": ExactDomain(value="allowed"), + "item": RecordDomain(fields={"value": "value"}, extra=False), + }, + bindings=[ + EnvelopeBinding( + path="items[0]", + scope=EnvelopeScope.NODE, + posture=Posture.CONSTRAINED, + domain="item", + ) + ], + closure=[ + ClosureOverlay( + path="items", + scope=EnvelopeScope.TOPOLOGY, + closure=Closure.OPEN_WORLD, + ) + ], + ) + + _constraints, closed = effective_constraints(env) + + assert "items[0]" not in closed + + def test_widening_non_overrideable_inherited_value_is_invalid() -> None: # Same shape but the record is NOT overrideable: opening os would widen a fixed # inherited value, which R2 makes an ill-formed envelope. The relation denies. diff --git a/implementations/python/tests/test_reference_backend_manifest.py b/implementations/python/tests/test_reference_backend_manifest.py index f228b4b13..e2370ace2 100644 --- a/implementations/python/tests/test_reference_backend_manifest.py +++ b/implementations/python/tests/test_reference_backend_manifest.py @@ -36,6 +36,12 @@ def test_manifest_declares_orchestrator_evaluator_participant_runtime_and_observ assert manifest.has_observation +def test_manifest_does_not_claim_unimplemented_acl_enforcement(): + manifest = create_reference_backend_manifest() + + assert manifest.provisioner.supports_acls is False + + def test_manifest_accepts_and_ignores_extra_config_kwargs(): # Config kwargs flow to both factories; the manifest factory must accept # and ignore extras such as ``driver``. diff --git a/implementations/python/tests/test_reference_backend_oci_driver.py b/implementations/python/tests/test_reference_backend_oci_driver.py index adbf330ed..3904a5271 100644 --- a/implementations/python/tests/test_reference_backend_oci_driver.py +++ b/implementations/python/tests/test_reference_backend_oci_driver.py @@ -6,7 +6,7 @@ import pytest from aces_backend_protocols.naming import provider_resource_name -from aces_reference_backend.driver import ContainerSpec, NetworkSpec +from aces_reference_backend.driver import ContainerSpec, NetworkSpec, ServiceSpec from aces_reference_backend.drivers.oci import ImageTrustPolicy, OciDeploymentDriver @@ -40,6 +40,15 @@ def _driver(recorder: _Recorder) -> OciDeploymentDriver: ) +def test_container_spec_preserves_legacy_positional_labels_argument(): + labels = {"environment": "test"} + + spec = ContainerSpec("provision.node.web", "web", "img", (), labels) + + assert spec.labels is labels + assert spec.services == () + + def test_oci_realize_uses_fixed_argv_list_never_shell(): recorder = _Recorder(stdout="container-native-id-abc123\n") driver = _driver(recorder) @@ -201,6 +210,28 @@ def test_oci_attaches_container_to_requested_networks(): assert run_argv[run_argv.index("--network") + 1] == provider_resource_name("provision.network.lan", prefix="aces") +def test_oci_service_descriptors_do_not_publish_host_ports(): + recorder = _Recorder(stdout="id\n") + driver = _driver(recorder) + + driver.realize( + networks=(), + containers=( + ContainerSpec( + address="provision.node.web", + name="web", + image_ref="img", + services=(ServiceSpec(port=8443, protocol="tcp", name="https"),), + ), + ), + ) + + run_argv = next(call["argv"] for call in recorder.calls if "run" in call["argv"]) + assert "--publish" not in run_argv + assert "-p" not in run_argv + assert "8443" not in run_argv + + def test_oci_rejects_plan_pinned_image_without_allowlist(): """Image-trust boundary: a plan-pinned tag that is not allowlisted, not the operator default, and not digest-pinned must NOT be run.""" diff --git a/implementations/python/tests/test_reference_backend_realization.py b/implementations/python/tests/test_reference_backend_realization.py index 49cbc2705..992f04ae8 100644 --- a/implementations/python/tests/test_reference_backend_realization.py +++ b/implementations/python/tests/test_reference_backend_realization.py @@ -11,10 +11,17 @@ RuntimeDomain, ) from aces_reference_backend import interpret_provisioning_plan +from aces_reference_backend.driver import ServiceSpec from aces_reference_backend.realization import Realization -def _node_resource(address: str, name: str, os_family: str = "linux") -> PlannedResource: +def _node_resource( + address: str, + name: str, + os_family: str = "linux", + *, + services: list[dict[str, object]] | None = None, +) -> PlannedResource: return PlannedResource( address=address, domain=RuntimeDomain.PROVISIONING, @@ -24,7 +31,10 @@ def _node_resource(address: str, name: str, os_family: str = "linux") -> Planned "node_name": name, "node_type": "vm", "os_family": os_family, - "spec": {"node": {}, "infrastructure": {"networks": ["lan"]}}, + "spec": { + "node": {"services": services or []}, + "infrastructure": {"networks": ["lan"]}, + }, }, ) @@ -64,6 +74,44 @@ def test_interpret_maps_nodes_to_container_specs(): assert not realization.diagnostics +def test_interpret_preserves_named_and_unnamed_service_descriptors(): + plan = _plan( + _node_resource( + "provision.node.web", + "web", + services=[ + {"port": 80, "protocol": "tcp", "name": "http"}, + {"port": 5000, "protocol": "sctp", "name": ""}, + ], + ) + ) + + realization = interpret_provisioning_plan(plan) + + assert realization.containers[0].services == ( + ServiceSpec(port=80, protocol="tcp", name="http"), + ServiceSpec(port=5000, protocol="sctp", name=""), + ) + assert not realization.diagnostics + + +def test_interpret_rejects_malformed_service_without_leaking_payload(): + sentinel = "TOKEN-LEAK-SENTINEL-XYZ" + plan = _plan( + _node_resource( + "provision.node.web", + "web", + services=[{"port": sentinel, "protocol": "tcp", "name": "http"}], + ) + ) + + realization = interpret_provisioning_plan(plan) + + codes = {diagnostic.code for diagnostic in realization.diagnostics} + assert "reference-backend.realization.service-invalid" in codes + assert all(sentinel not in diagnostic.message for diagnostic in realization.diagnostics) + + def test_interpret_maps_networks_to_network_specs(): plan = _plan(_network_resource("provision.network.lan", "lan")) diff --git a/implementations/python/tests/test_related_work_comparison.py b/implementations/python/tests/test_related_work_comparison.py new file mode 100644 index 000000000..0b425d6eb --- /dev/null +++ b/implementations/python/tests/test_related_work_comparison.py @@ -0,0 +1,219 @@ +"""Reproducible related-work comparison integrity tests.""" + +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path + +from tools.check_related_work_comparison import ( + EXPECTED_AXIS_IDS, + evaluate, + load_bundle, + render_publication, + validate_bundle, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _rule_ids(failures: list[object]) -> set[str]: + return {failure.rule_id for failure in failures} + + +def _bundle() -> tuple[dict, dict, dict]: + protocol, snapshot, analysis = load_bundle(REPO_ROOT) + return deepcopy(protocol), deepcopy(snapshot), deepcopy(analysis) + + +def test_current_bundle_and_publication_pass_the_integrity_gate() -> None: + assert evaluate(REPO_ROOT) == [] + + +def test_protocol_uses_all_required_axes_and_independent_systems() -> None: + protocol, _, _ = _bundle() + + assert {axis["axis_id"] for axis in protocol["axes"]} == EXPECTED_AXIS_IDS + assert {system["system_id"] for system in protocol["systems"]} == { + "aces", + "cacao-v2", + "crack", + "cyber-dem", + "cyber-fom", + "cyborg", + "ocr-sdl", + "vsdl", + } + + +def test_gate_rejects_a_missing_system_axis_cell() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["observations"].pop() + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-observations-rectangular" in _rule_ids(failures) + + +def test_gate_rejects_missing_or_non_primary_cell_evidence() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["observations"][0]["evidence_refs"] = [] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-observation-evidence" in _rule_ids(failures) + + +def test_gate_requires_a_reproducible_rationale_and_explicit_limit_for_every_cell() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["observations"][0]["rationale"] = "" + snapshot["observations"][0]["limitations"] = [] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert { + "related-work-observation-rationale", + "related-work-observation-limitations", + }.issubset(_rule_ids(failures)) + + +def test_gate_rejects_mutable_or_unpinned_sources() -> None: + protocol, snapshot, analysis = _bundle() + source = next(item for item in snapshot["sources"] if item["kind"] == "git") + source["revision"] = "main" + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-source-pin" in _rule_ids(failures) + + +def test_gate_rejects_composite_system_identities() -> None: + protocol, snapshot, analysis = _bundle() + protocol["systems"][0]["name"] = "Cyber DEM/FOM" + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-composite-system" in _rule_ids(failures) + + +def test_gate_rejects_secret_bearing_locators_and_unsafe_paths() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["sources"][0]["locator"] = "https://example.test/source?access_token=secret" + snapshot["sources"][0]["artifact_path"] = "../outside.json" + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert { + "related-work-source-locator-secret", + "related-work-source-path", + }.issubset(_rule_ids(failures)) + + +def test_gate_requires_rectangular_authoring_task_and_negative_case_coverage() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["task_observations"] = [ + item + for item in snapshot["task_observations"] + if not (item["system_id"] == "aces" and item["case_id"] == "negative-dangling-reference") + ] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-task-observations-rectangular" in _rule_ids(failures) + + +def test_gate_requires_reproducible_task_rationales_and_limits() -> None: + protocol, snapshot, analysis = _bundle() + snapshot["task_observations"][0]["rationale"] = "" + snapshot["task_observations"][0]["limitations"] = [] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert { + "related-work-task-observation-rationale", + "related-work-task-observation-limitations", + }.issubset(_rule_ids(failures)) + + +def test_gate_recomputes_weight_profiles_and_exposes_ranking_reversals() -> None: + protocol, snapshot, analysis = _bundle() + analysis["sensitivity"]["ranking_reversal_observed"] = False + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-sensitivity-drift" in _rule_ids(failures) + + +def test_gate_rejects_stale_publication_output(tmp_path: Path) -> None: + protocol, snapshot, analysis = _bundle() + publication = tmp_path / "docs/explain/sdl/related-work-comparison.md" + publication.parent.mkdir(parents=True) + publication.write_text("# Stale publication\n", encoding="utf-8") + + failures = validate_bundle(tmp_path, protocol, snapshot, analysis, validate_paths=False) + + assert "related-work-publication-drift" in _rule_ids(failures) + assert "No overall winner" in render_publication(protocol, snapshot, analysis) + + +def test_gate_rejects_an_unsupported_highest_quality_claim() -> None: + protocol, snapshot, analysis = _bundle() + claim = next(item for item in analysis["claims"] if item["kind"] == "scope-qualified-breadth") + claim["statement"] = "ACES has the highest quality." + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-claim-overreach" in _rule_ids(failures) + + +def test_gate_requires_falsification_first_evidence_records_for_public_claims() -> None: + protocol, snapshot, analysis = _bundle() + claim = analysis["claims"][0] + claim["evidence_status"] = "untested" + claim["threats_to_validity"] = [] + claim["falsification"]["evidence_artifact_refs"] = [] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-claim-evidence-gate" in _rule_ids(failures) + + +def test_gate_recomputes_public_claim_derivations_from_frozen_observations() -> None: + protocol, snapshot, analysis = _bundle() + claim = next(item for item in analysis["claims"] if item["kind"] == "scope-qualified-breadth") + claim["derivation"]["max_score"] = 0 + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-claim-derivation" in _rule_ids(failures) + + +def test_publication_exposes_each_claims_evidence_status() -> None: + protocol, snapshot, analysis = _bundle() + + publication = render_publication(protocol, snapshot, analysis) + + for claim in analysis["claims"]: + assert f"Evidence status: `{claim['evidence_status']}`." in publication + + +def test_gate_rejects_aces_delivery_scores_without_executable_evidence() -> None: + protocol, snapshot, analysis = _bundle() + non_executable_source = next( + source for source in snapshot["sources"] if source["source_id"] == "aces-scientific-assessment" + ) + assert non_executable_source["evidence_class"] == "normative" + observation = next( + item + for item in snapshot["observations"] + if item["system_id"] == "aces" and item["axis_id"] == "implementation-maturity" + ) + observation["evidence_refs"] = [ + { + "source_id": non_executable_source["source_id"], + "locator": "implemented, partial, missing, external, and excluded rows", + } + ] + + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + + assert "related-work-aces-executable-evidence" in _rule_ids(failures) diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index 1c5e953be..66b7d63ba 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -38,6 +38,12 @@ def test_fixture_suite_passes_for_orchestration_evaluation_profile(): report = run_fixture_suite(profile=BackendCapabilityProfile.ORCHESTRATION_EVALUATION) assert report.passed is True + assert report.claim.relation_id == "bounded-probe-success" + assert report.claim.right_carrier_ref == "backend-profile:orchestration-evaluation" + assert report.claim.quantifier_scope == "finite-cases" + assert set(report.claim.evidence_refs) == { + f"conformance-case:{case.contract_name}:{case.name}" for case in report.cases + } assert report.cases assert not report.diagnostics assert required_contracts(report.profile) @@ -48,6 +54,9 @@ def test_target_conformance_passes_for_stub_target(): assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE assert report.passed is True + assert report.claim.left_carrier_ref == "backend-target:stub" + assert report.claim.observation_projection_revision == "rev1" + assert "Does not establish trace equivalence or bisimulation." in report.claim.explicit_non_claims assert not report.unsupported_contract_gaps assert not report.unsupported_capability_gaps # RUN-311 finding 4: the live probe must actually drive every diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index 4a62cfcbb..7747f19c7 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -12,7 +12,12 @@ ParticipantHistoryViewModel, ParticipantStatusViewModel, ) -from aces_contracts.runtime_state import RuntimeSnapshot +from aces_contracts.runtime_state import ( + ExplicitnessClass, + ExplicitnessProvenance, + RealizationProvenanceEntry, + RuntimeSnapshot, +) from starlette.testclient import TestClient from aces.backends.stubs import create_stub_target @@ -434,6 +439,50 @@ def test_control_plane_api_persists_operations_and_snapshot(tmp_path: Path): assert restarted.get_snapshot().snapshot.entries +def test_authenticated_snapshot_preserves_realization_governing_scope_from_store(tmp_path: Path): + target = create_stub_target() + store = LocalControlPlaneStore(tmp_path / "cp-store") + store.save_snapshot( + RuntimeSnapshot( + realization_provenance=( + RealizationProvenanceEntry( + address="node.web", + field_path="nodes.web.os", + domain="runtime-realization", + requirement_kind="os-family", + explicitness=ExplicitnessClass.OPEN, + provenance=ExplicitnessProvenance.BACKEND_REALIZED, + governing_scope="#/", + ), + ) + ) + ) + restarted = RuntimeControlPlane(target, store=store) + app = create_control_plane_app( + restarted, + security=_test_security(target.name), + ) + + with TestClient(app) as client: + response = client.get( + "/snapshot", + headers={"authorization": "Bearer test-auditor-token"}, + ) + + assert response.status_code == 200 + assert response.json()["realization_provenance"] == [ + { + "address": "node.web", + "field_path": "nodes.web.os", + "domain": "runtime-realization", + "requirement_kind": "os-family", + "explicitness": "open", + "provenance": "backend-realized", + "governing_scope": "#/", + } + ] + + def test_control_plane_api_records_audit_events_for_denials(): target = create_stub_target() control_plane = RuntimeControlPlane(target) diff --git a/implementations/python/tests/test_runtime_models.py b/implementations/python/tests/test_runtime_models.py index 1e2e35275..a4bb83c52 100644 --- a/implementations/python/tests/test_runtime_models.py +++ b/implementations/python/tests/test_runtime_models.py @@ -165,14 +165,6 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): dns_options: ndots:0 dns_search: [techvault.local] group_add: [adm, "101"] - health: - status: healthy - failing_streak: "0" - log: - - start: "2026-05-20T12:00:00Z" - end: "2026-05-20T12:00:01Z" - exit_code: "0" - output: ok packages: - manager: apk name: musl @@ -182,7 +174,7 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): name: shuffle-backend version: 1.2.3 component_type: application - provenance: scanner + provenance: package-manager ecosystem: go purl: "pkg:golang/github.com/frikky/shuffle@1.2.3" package_manager: apk @@ -197,15 +189,6 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): - ecosystem: go path: /app/go.mod format: go-module - package_vulnerabilities: - - id: CVE-2026-12345 - package_name: musl - installed_version: 1.2.4-r2 - fixed_version: 1.2.5-r0 - severity: high - scanner: trivy - image_digest: sha256:abc123 - scan_time: "2026-05-20T12:00:00Z" """) ) @@ -257,16 +240,13 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): assert runtime["container"]["extra_hosts"][0]["hostname"] == "wazuh-manager" assert runtime["container"]["dns_options"] == ["ndots:0"] assert runtime["container"]["group_add"] == ["adm", "101"] - assert runtime["health"]["status"] == "healthy" - assert runtime["health"]["failing_streak"] == 0 - assert runtime["health"]["log"][0]["exit_code"] == 0 assert runtime["packages"][0]["manager"] == "apk" assert runtime["packages"][0]["name"] == "musl" assert runtime["packages"][0]["version"] == "1.2.4-r2" assert runtime["software_components"][0]["component_id"] == "shuffle-backend-app" assert runtime["software_components"][0]["name"] == "shuffle-backend" assert runtime["software_components"][0]["component_type"] == "application" - assert runtime["software_components"][0]["provenance"] == "scanner" + assert runtime["software_components"][0]["provenance"] == "package_manager" assert runtime["software_components"][0]["package_manager"] == "apk" assert runtime["software_components"][0]["manifest_path"] == "/app/go.mod" assert runtime["software_components"][0]["installed_paths"] == ["/app/shufflebackend", "/app/go.mod"] @@ -274,14 +254,6 @@ def test_node_runtime_preserves_runtime_configuration_metadata(self): assert runtime["dependency_manifests"][0]["ecosystem"] == "go" assert runtime["dependency_manifests"][0]["path"] == "/app/go.mod" assert runtime["dependency_manifests"][0]["format"] == "go-module" - assert runtime["package_vulnerabilities"][0]["id"] == "CVE-2026-12345" - assert runtime["package_vulnerabilities"][0]["package_name"] == "musl" - assert runtime["package_vulnerabilities"][0]["installed_version"] == "1.2.4-r2" - assert runtime["package_vulnerabilities"][0]["fixed_version"] == "1.2.5-r0" - assert runtime["package_vulnerabilities"][0]["severity"] == "high" - assert runtime["package_vulnerabilities"][0]["scanner"] == "trivy" - assert runtime["package_vulnerabilities"][0]["image_digest"] == "sha256:abc123" - assert runtime["package_vulnerabilities"][0]["scan_time"] == "2026-05-20T12:00:00Z" assert not model.diagnostics def test_node_runtime_preserves_identity_authority_inventory(self): diff --git a/implementations/python/tests/test_runtime_planner.py b/implementations/python/tests/test_runtime_planner.py index 765f2a306..8a7d5c7d9 100644 --- a/implementations/python/tests/test_runtime_planner.py +++ b/implementations/python/tests/test_runtime_planner.py @@ -1078,6 +1078,40 @@ def test_semantic_capability_validation_catches_real_requirements(self): assert "evaluator.objectives-unsupported" in codes assert not execution_plan.is_valid + def test_acl_capability_validation_covers_node_attached_rules(self): + limited = _limited_backend_manifest( + name="limited", + provisioner=ProvisionerCapabilities( + name="limited-provisioner", + supported_node_types=frozenset({"vm"}), + supported_os_families=frozenset({"linux"}), + supports_acls=False, + ), + ) + model = compile_runtime_model( + _scenario(""" +name: node-acl +nodes: + web: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} +infrastructure: + web: + count: 1 + acls: + - {direction: in, protocol: tcp, ports: [443], action: allow} +""") + ) + + execution_plan = plan(model, limited) + + acl_diagnostics = [ + diagnostic for diagnostic in execution_plan.diagnostics if diagnostic.code == "provisioner.acls-unsupported" + ] + assert [diagnostic.address for diagnostic in acl_diagnostics] == ["provision.node.web"] + assert not execution_plan.is_valid + def test_variable_backed_os_allowed_values_pass_when_all_supported(self): manifest = _limited_backend_manifest( name="limited", diff --git a/implementations/python/tests/test_scientific_scenario_completeness.py b/implementations/python/tests/test_scientific_scenario_completeness.py index 0984badb7..6982b45b9 100644 --- a/implementations/python/tests/test_scientific_scenario_completeness.py +++ b/implementations/python/tests/test_scientific_scenario_completeness.py @@ -80,7 +80,10 @@ def test_both_completeness_contracts_are_in_the_published_schema_bundle() -> Non assessment_invariants = { item["id"] for item in bundle["scientific-completeness-assessment-v1"]["x-aces-invariants"] } - assert taxonomy_invariants == {"scientific-completeness-taxonomy-rectangular"} + assert taxonomy_invariants == { + "scientific-completeness-behavioral-claim-resolution", + "scientific-completeness-taxonomy-rectangular", + } assert assessment_invariants == { "scientific-completeness-assessment-status-evidence", "scientific-completeness-taxonomy-assessment-join", diff --git a/implementations/python/tests/test_sdl_lineage.py b/implementations/python/tests/test_sdl_lineage.py index 1e8792b4f..843851ce0 100644 --- a/implementations/python/tests/test_sdl_lineage.py +++ b/implementations/python/tests/test_sdl_lineage.py @@ -36,7 +36,7 @@ def test_real_lineage_ledger_is_valid_and_covers_exact_current_subject_set() -> ledger = SDLLineageLedgerModel.model_validate(_payload()) current = {subject.subject_id for subject in ledger.subjects if subject.disposition.value == "current"} assert current == _canonical_subjects(REPO_ROOT) - assert len(current) == 70 + assert len(current) == 72 assert {subject.subject_id for subject in ledger.subjects if subject.disposition.value == "removed"} == { "sdl-field:evaluations", "sdl-field:goals", diff --git a/implementations/python/tests/test_sdl_models.py b/implementations/python/tests/test_sdl_models.py index 35d91b8cc..7cb7f1e8d 100644 --- a/implementations/python/tests/test_sdl_models.py +++ b/implementations/python/tests/test_sdl_models.py @@ -80,7 +80,6 @@ RuntimeFilesystemEntryType, RuntimeFilesystemPresence, RuntimeFilesystemStability, - RuntimeHealthStatus, RuntimeIdentityAttribute, RuntimeIdentityAuthorityKind, RuntimeIdentityAuthorityProtocol, @@ -97,9 +96,7 @@ RuntimeNetworkBackendDetail, RuntimeNetworkDriver, RuntimeNetworkEndpoint, - RuntimeNetworkIdStability, RuntimeNetworkRealization, - RuntimePackageVulnerabilitySeverity, RuntimeProcessCapabilityOverride, RuntimeProcessRole, RuntimePublishedPort, @@ -446,7 +443,7 @@ def test_vm_runtime_configuration_surfaces(self): "name": "shuffle-backend", "version": "1.2.3", "component_type": "application", - "provenance": "scanner", + "provenance": "package_manager", "ecosystem": "go", "purl": "pkg:golang/github.com/frikky/shuffle@1.2.3", "cpe": "cpe:2.3:a:shuffle:shuffle:1.2.3:*:*:*:*:*:*:*", @@ -465,18 +462,6 @@ def test_vm_runtime_configuration_surfaces(self): "format": "go-module", } ], - "package_vulnerabilities": [ - { - "id": "CVE-2026-12345", - "package_name": "musl", - "installed_version": "1.2.4-r2", - "fixed_version": "1.2.5-r0", - "severity": "high", - "scanner": "trivy", - "image_digest": "sha256:abc123", - "scan_time": "2026-05-20T12:00:00Z", - } - ], }, ) @@ -504,7 +489,7 @@ def test_vm_runtime_configuration_surfaces(self): assert runtime.software_components[0].name == "shuffle-backend" assert runtime.software_components[0].version == "1.2.3" assert runtime.software_components[0].component_type == RuntimeSoftwareComponentType.APPLICATION - assert runtime.software_components[0].provenance == RuntimeSoftwareComponentProvenance.SCANNER + assert runtime.software_components[0].provenance == RuntimeSoftwareComponentProvenance.PACKAGE_MANAGER assert runtime.software_components[0].ecosystem == "go" assert runtime.software_components[0].purl == "pkg:golang/github.com/frikky/shuffle@1.2.3" assert runtime.software_components[0].cpe == "cpe:2.3:a:shuffle:shuffle:1.2.3:*:*:*:*:*:*:*" @@ -517,14 +502,106 @@ def test_vm_runtime_configuration_surfaces(self): assert runtime.dependency_manifests[0].ecosystem == "go" assert runtime.dependency_manifests[0].path == "/app/go.mod" assert runtime.dependency_manifests[0].format == "go-module" - assert runtime.package_vulnerabilities[0].id == "CVE-2026-12345" - assert runtime.package_vulnerabilities[0].package_name == "musl" - assert runtime.package_vulnerabilities[0].installed_version == "1.2.4-r2" - assert runtime.package_vulnerabilities[0].fixed_version == "1.2.5-r0" - assert runtime.package_vulnerabilities[0].severity == RuntimePackageVulnerabilitySeverity.HIGH - assert runtime.package_vulnerabilities[0].scanner == "trivy" - assert runtime.package_vulnerabilities[0].image_digest == "sha256:abc123" - assert runtime.package_vulnerabilities[0].scan_time == "2026-05-20T12:00:00Z" + + @pytest.mark.parametrize( + "runtime", + [ + {"health": {"status": "healthy"}}, + { + "package_vulnerabilities": [ + { + "id": "CVE-2026-12345", + "package_name": "musl", + "installed_version": "1.2.4-r2", + "scanner": "trivy", + "image_digest": "sha256:abc123", + "scan_time": "2026-05-20T12:00:00Z", + } + ] + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "network_id": "docker-network-id"}, + ] + } + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "network_id_stability": "ephemeral"}, + ] + } + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "endpoint_id": "docker-endpoint-id"}, + ] + } + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "endpoint_id_stability": "ephemeral"}, + ] + } + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "backend_generated": True}, + ] + } + }, + { + "network": { + "endpoints": [ + {"network": "aptl-dmz", "generated_dns_names": ["generated-id"]}, + ] + } + }, + { + "software_components": [ + { + "component_id": "shuffle-backend-app", + "name": "shuffle-backend", + "provenance": "scanner", + } + ] + }, + { + "software_components": [ + { + "component_id": "shuffle-backend-app", + "name": "shuffle-backend", + "provenance": "sbom", + } + ] + }, + { + "software_components": [ + { + "component_id": "shuffle-backend-app", + "name": "shuffle-backend", + "provenance": "filesystem", + } + ] + }, + { + "software_components": [ + { + "component_id": "shuffle-backend-app", + "name": "shuffle-backend", + "provenance": "process_inspection", + } + ] + }, + ], + ) + def test_vm_runtime_rejects_observation_only_capture_facts(self, runtime): + with pytest.raises(ValidationError): + Node(type="vm", runtime=runtime) def test_vm_runtime_rejects_duplicate_software_component_id(self): with pytest.raises(ValidationError, match="Duplicate runtime software component 'webapp'"): @@ -949,24 +1026,6 @@ def test_vm_runtime_container_host_config_surfaces(self): "dns_search": ["techvault.local"], "group_add": ["adm", "101"], }, - "health": { - "status": "healthy", - "failing_streak": "0", - "log": [ - { - "start": "2026-05-20T12:00:00Z", - "end": "2026-05-20T12:00:01Z", - "exit_code": "0", - "output": "ok", - }, - { - "start": "2026-05-20T12:01:00Z", - "end": "2026-05-20T12:01:01Z", - "exit_code": 1, - "output_redacted": True, - }, - ], - }, }, ) @@ -997,11 +1056,6 @@ def test_vm_runtime_container_host_config_surfaces(self): assert runtime.container.init_process.reaps_children is True assert runtime.container.init_process.argv == ["/sbin/docker-init", "--", "/entrypoint.sh"] assert runtime.container.init_process.argv_redacted is False - assert runtime.health is not None - assert runtime.health.status == RuntimeHealthStatus.HEALTHY - assert runtime.health.failing_streak == 0 - assert runtime.health.log[0].exit_code == 0 - assert runtime.health.log[1].output_redacted is True @pytest.mark.parametrize( ("runtime", "message"), @@ -1063,7 +1117,6 @@ def test_vm_runtime_container_host_config_surfaces(self): {"container": {"seccomp_profile": "default", "security_opt": ["seccomp:unconfined"]}}, "seccomp_profile", ), - ({"health": {"log": [{"output": "secret", "output_redacted": True}]}}, "redacted healthcheck output"), ( { "environment": [ @@ -1777,18 +1830,12 @@ def test_vm_runtime_network_surface(self): "endpoints": [ { "network": "aptl-dmz", - "network_id": "net-a1b2c3d4e5f6", - "network_id_stability": "stable", - "endpoint_id": "ep-1a2b3c4d5e6f", - "endpoint_id_stability": "ephemeral", - "backend_generated": True, "ip_address": "172.20.0.20", "ip_prefix_length": 24, "gateway": "172.20.0.1", "mac_address": "02:42:ac:14:00:14", "aliases": ["aptl-webapp", "webapp"], "dns_names": ["aptl-webapp", "webapp"], - "generated_dns_names": ["a1b2c3d4e5f6"], "backend": { "driver": "bridge", "ipam_driver": "default", @@ -1819,24 +1866,17 @@ def test_vm_runtime_network_surface(self): assert net.domainname == "techvault.local" ep = net.endpoints[0] assert ep.network == "aptl-dmz" - assert ep.network_id == "net-a1b2c3d4e5f6" - assert ep.network_id_stability == RuntimeNetworkIdStability.STABLE - assert ep.endpoint_id_stability == RuntimeNetworkIdStability.EPHEMERAL - assert ep.backend_generated is True assert ep.ip_address == "172.20.0.20" assert ep.ip_prefix_length == 24 assert ep.gateway == "172.20.0.1" assert ep.mac_address == "02:42:ac:14:00:14" assert ep.aliases == ["aptl-webapp", "webapp"] assert ep.dns_names == ["aptl-webapp", "webapp"] - assert ep.generated_dns_names == ["a1b2c3d4e5f6"] assert ep.backend.driver == RuntimeNetworkDriver.BRIDGE assert ep.backend.ipam_driver == "default" assert ep.backend.driver_options == {"com.docker.network.bridge.name": "br-dmz"} assert ep.backend.ipam_options == {"foo": "bar"} - # Defaults on a sparsely-observed endpoint. - assert net.endpoints[1].network_id == "" - assert net.endpoints[1].network_id_stability == RuntimeNetworkIdStability.UNKNOWN + # Defaults on a sparsely-specified endpoint. assert net.endpoints[1].backend is None binding = net.published_ports[0] assert binding.container_port == 8080 @@ -2511,6 +2551,10 @@ def test_placeholder(self): sp = ServicePort(port="${service_port}", name="https") assert sp.port == "${service_port}" + def test_rejects_unmodeled_role_as_reachability_policy(self): + with pytest.raises(ValidationError, match="role"): + ServicePort.model_validate({"port": 80, "protocol": "tcp", "name": "http", "role": "internal"}) + class TestConditionExtensions: def test_timeout_and_retries(self): diff --git a/implementations/python/tests/test_sdl_parser.py b/implementations/python/tests/test_sdl_parser.py index f8a6dde7f..4ad1bba5d 100644 --- a/implementations/python/tests/test_sdl_parser.py +++ b/implementations/python/tests/test_sdl_parser.py @@ -368,14 +368,6 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): dns_options: ndots:0 dns_search: [techvault.local] group_add: [adm, "101"] - health: - status: healthy - failing_streak: "0" - log: - - start: "2026-05-20T12:00:00Z" - end: "2026-05-20T12:00:01Z" - exit_code: "0" - output: ok packages: - manager: apk name: musl @@ -385,7 +377,7 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): name: shuffle-backend version: 1.2.3 component_type: application - provenance: scanner + provenance: package-manager ecosystem: go purl: "pkg:golang/github.com/frikky/shuffle@1.2.3" cpe: "cpe:2.3:a:shuffle:shuffle:1.2.3:*:*:*:*:*:*:*" @@ -401,15 +393,6 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): - ecosystem: go path: /app/go.mod format: go-module - package_vulnerabilities: - - id: CVE-2026-12345 - package_name: musl - installed_version: 1.2.4-r2 - fixed_version: 1.2.5-r0 - severity: high - scanner: trivy - image_digest: sha256:abc123 - scan_time: "2026-05-20T12:00:00Z" """ scenario = parse_sdl(sdl) node = scenario.nodes["shuffle-backend"] @@ -476,10 +459,6 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): assert node.runtime.container.init_process.executable_path == "/sbin/docker-init" assert node.runtime.container.init_process.reaps_children is True assert node.runtime.container.init_process.argv == ["/sbin/docker-init", "--", "/entrypoint.sh"] - assert node.runtime.health is not None - assert node.runtime.health.status == "healthy" - assert node.runtime.health.failing_streak == 0 - assert node.runtime.health.log[0].exit_code == 0 assert node.runtime.packages[0].manager == "apk" assert node.runtime.packages[0].name == "musl" assert node.runtime.packages[0].version == "1.2.4-r2" @@ -487,7 +466,7 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): assert node.runtime.software_components[0].name == "shuffle-backend" assert node.runtime.software_components[0].version == "1.2.3" assert node.runtime.software_components[0].component_type == "application" - assert node.runtime.software_components[0].provenance == "scanner" + assert node.runtime.software_components[0].provenance == "package_manager" assert node.runtime.software_components[0].ecosystem == "go" assert node.runtime.software_components[0].purl == "pkg:golang/github.com/frikky/shuffle@1.2.3" assert node.runtime.software_components[0].package_manager == "apk" @@ -499,14 +478,6 @@ def test_runtime_configuration_parses_without_overloading_other_sections(self): assert node.runtime.dependency_manifests[0].ecosystem == "go" assert node.runtime.dependency_manifests[0].path == "/app/go.mod" assert node.runtime.dependency_manifests[0].format == "go-module" - assert node.runtime.package_vulnerabilities[0].id == "CVE-2026-12345" - assert node.runtime.package_vulnerabilities[0].package_name == "musl" - assert node.runtime.package_vulnerabilities[0].installed_version == "1.2.4-r2" - assert node.runtime.package_vulnerabilities[0].fixed_version == "1.2.5-r0" - assert node.runtime.package_vulnerabilities[0].severity == "high" - assert node.runtime.package_vulnerabilities[0].scanner == "trivy" - assert node.runtime.package_vulnerabilities[0].image_digest == "sha256:abc123" - assert node.runtime.package_vulnerabilities[0].scan_time == "2026-05-20T12:00:00Z" def test_runtime_local_identity_inventory_parses_with_canonical_keys(self): sdl = """ @@ -612,18 +583,12 @@ def test_runtime_network_realization_parses_with_canonical_keys(self): domainname: techvault.local endpoints: - network: aptl-dmz - network_id: net-a1b2c3d4e5f6 - network_id_stability: stable - endpoint_id: ep-1a2b3c4d5e6f - endpoint_id_stability: ephemeral - backend_generated: true ip_address: 172.20.0.20 ip_prefix_length: "24" gateway: 172.20.0.1 mac_address: 02:42:ac:14:00:14 aliases: [aptl-webapp, webapp] dns_names: [aptl-webapp, webapp] - generated_dns_names: [a1b2c3d4e5f6] backend: driver: bridge ipam_driver: default @@ -649,17 +614,12 @@ def test_runtime_network_realization_parses_with_canonical_keys(self): assert network.domainname == "techvault.local" endpoint = network.endpoints[0] assert endpoint.network == "aptl-dmz" - assert endpoint.network_id == "net-a1b2c3d4e5f6" - assert endpoint.network_id_stability == "stable" - assert endpoint.endpoint_id_stability == "ephemeral" - assert endpoint.backend_generated is True assert endpoint.ip_address == "172.20.0.20" assert endpoint.ip_prefix_length == 24 assert endpoint.gateway == "172.20.0.1" assert endpoint.mac_address == "02:42:ac:14:00:14" assert endpoint.aliases == ["aptl-webapp", "webapp"] assert endpoint.dns_names == ["aptl-webapp", "webapp"] - assert endpoint.generated_dns_names == ["a1b2c3d4e5f6"] # Backend-native option keys are preserved verbatim as literal-map data. assert endpoint.backend.driver == "bridge" assert endpoint.backend.driver_options == {"com.docker.network.bridge.name": "br-dmz"} diff --git a/implementations/python/tests/test_sdl_phase_contracts.py b/implementations/python/tests/test_sdl_phase_contracts.py index 98c6c932a..a68cdd37b 100644 --- a/implementations/python/tests/test_sdl_phase_contracts.py +++ b/implementations/python/tests/test_sdl_phase_contracts.py @@ -57,7 +57,7 @@ def _provenance( def test_phase_models_have_disjoint_authoring_and_instantiated_fields() -> None: shared = set(ScenarioContent.model_fields) assert not issubclass(InstantiatedScenario, Scenario) - assert set(Scenario.model_fields) == shared | {"module", "imports", "variables"} + assert set(Scenario.model_fields) == shared | {"module", "imports", "realization", "variables"} assert set(ExpandedScenario.model_fields) == shared | {"variables", "expansion_provenance"} assert set(InstantiatedScenario.model_fields) == shared | {"instantiation_provenance"} assert "ExpandedScenario" not in aces_sdl.__all__ @@ -384,6 +384,57 @@ def test_import_provenance_rejects_host_paths_and_registry_credentials( ResolvedImportProvenance.model_validate(payload) +def test_imported_explicitness_projects_only_surviving_namespaced_fields(tmp_path) -> None: + for module_name in ("first", "second"): + (tmp_path / f"{module_name}.yaml").write_text( + f""" +name: {module_name} +version: 1.0.0 +module: + id: aces/{module_name} + version: 1.0.0 + exports: {{nodes: [host]}} +behavior_specifications: {{}} +nodes: + host: + type: vm + os: linux + resources: {{ram: 1 gib, cpu: 1}} +""", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + """ +name: root +version: 1.0.0 +imports: + - path: first.yaml + namespace: first + version: 1.0.0 + - path: second.yaml + namespace: second + version: 1.0.0 +""", + encoding="utf-8", + ) + + expanded = parse_sdl_file(root) + paths = [record.model_path for record in expanded.expansion_provenance.explicitness] + + assert len(paths) == len(set(paths)) + assert {"name", "version", "behavior_specifications"}.isdisjoint(paths) + assert { + "nodes.first.host.os", + "nodes.second.host.os", + }.issubset(paths) + assert all(path.startswith("nodes.") for path in paths) + + concrete = instantiate_scenario(expanded) + instantiated_paths = {record.model_path for record in concrete.instantiation_provenance.explicitness} + assert set(paths).issubset(instantiated_paths) + + def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_path) -> None: inner = tmp_path / "inner.yaml" inner.write_text( @@ -440,7 +491,11 @@ def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_pa encoding="utf-8", ) - concrete = instantiate_scenario(parse_sdl_file(root)) + expanded = parse_sdl_file(root) + portable_explicitness = {record.model_path: record for record in expanded.expansion_provenance.explicitness} + assert portable_explicitness["nodes.outer.inner.host.os"].parameters == (("outer", "inner", "image"),) + + concrete = instantiate_scenario(expanded) imports = concrete.instantiation_provenance.imports assert [record.namespace for record in imports] == [("outer",), ("outer", "inner")] @@ -456,3 +511,5 @@ def test_nested_import_provenance_is_ordered_portable_and_replay_complete(tmp_pa constraint = concrete.instantiation_provenance.capability_constraints[0] assert constraint.parameter == ("outer", "inner", "image") assert constraint.field_pointer == "/nodes/outer.inner.host/os" + instantiated_explicitness = {record.model_path: record for record in concrete.instantiation_provenance.explicitness} + assert instantiated_explicitness["nodes.outer.inner.host.os"] == portable_explicitness["nodes.outer.inner.host.os"] diff --git a/implementations/python/tests/test_sem_208_participant_behavior.py b/implementations/python/tests/test_sem_208_participant_behavior.py index ed8e247f0..943c92355 100644 --- a/implementations/python/tests/test_sem_208_participant_behavior.py +++ b/implementations/python/tests/test_sem_208_participant_behavior.py @@ -10,6 +10,7 @@ from aces_processor.models import ( ParticipantBehaviorHistoryEvent, ParticipantBehaviorHistoryEventType, + ParticipantHistoryAddressScope, ParticipantObservationBoundaryRuntime, ParticipantObservationStatus, iter_participant_behavior_history_violations, @@ -1767,8 +1768,10 @@ def test_behavior_history_requires_terminal_observation_for_action_instance(): violations = list( iter_participant_behavior_history_violations( [action.to_payload()], - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) @@ -1816,8 +1819,10 @@ def test_behavior_history_pairs_state_transition_and_terminal_observation(): list( iter_participant_behavior_history_violations( [action.to_payload(), transition.to_payload(), observation.to_payload()], - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) == [] @@ -1835,8 +1840,10 @@ def test_behavior_history_rejects_observation_details_that_expose_hidden_truth() violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, participant_episode_history=_completed_episode_history_payloads(), ) @@ -1882,8 +1889,10 @@ def test_behavior_history_rejects_future_episode_close_disclosure_in_observation violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, participant_episode_history=_completed_episode_history_payloads(), ) @@ -1927,8 +1936,10 @@ def test_behavior_history_rejects_unresolved_episode_close_transition_anchor(): violations = list( iter_participant_behavior_history_violations( _complete_behavior_history_payloads(ACTION_INSTANCE), - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, participant_episode_history=[], ) @@ -2007,8 +2018,10 @@ def test_behavior_history_does_not_import_unanchored_lower_order_transition_snap violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries={OBSERVATION_ADDRESS: boundary}, ) ) @@ -2032,8 +2045,10 @@ def test_behavior_history_rejects_nested_observation_details_payload_side_channe violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, ) ) @@ -2057,8 +2072,10 @@ def test_behavior_history_rejects_caller_supplied_observation_effective_order(): violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, ) ) @@ -2081,8 +2098,10 @@ def test_behavior_history_rejects_details_on_non_observation_events(): violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) @@ -2101,8 +2120,10 @@ def test_behavior_history_rejects_unresolved_visibility_transition_anchor(): violations = list( iter_participant_behavior_history_violations( _complete_behavior_history_payloads(ACTION_INSTANCE), - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries=model.observation_boundaries, ) ) @@ -2124,8 +2145,10 @@ def test_behavior_history_rejects_duplicate_realized_order_in_joint_action_set() violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) @@ -2175,8 +2198,10 @@ def test_behavior_history_rejects_state_transition_observation_digest_mismatch() violations = list( iter_participant_behavior_history_violations( [action.to_payload(), transition.to_payload(), observation.to_payload()], - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) @@ -2213,8 +2238,10 @@ def test_behavior_history_allows_orphaned_action_observation_without_state_diges list( iter_participant_behavior_history_violations( [action.to_payload(), observation.to_payload()], - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), ) ) == [] diff --git a/implementations/python/tests/test_sem_211_participant_action_semantics.py b/implementations/python/tests/test_sem_211_participant_action_semantics.py index 916557c6c..c6cb3f783 100644 --- a/implementations/python/tests/test_sem_211_participant_action_semantics.py +++ b/implementations/python/tests/test_sem_211_participant_action_semantics.py @@ -15,6 +15,7 @@ ParticipantActionResultStatus, ParticipantBehaviorHistoryEvent, ParticipantBehaviorHistoryEventType, + ParticipantHistoryAddressScope, ParticipantObservationStatus, RuntimeSnapshot, iter_participant_behavior_history_violations, @@ -594,9 +595,11 @@ def test_terminal_observation_requires_sem_211_action_result_when_contract_avail violations = list( iter_participant_behavior_history_violations( events, - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -659,9 +662,11 @@ def test_action_result_must_report_every_declared_precondition(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -738,9 +743,11 @@ def test_action_result_precondition_refs_must_be_declared_by_contract(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -817,9 +824,11 @@ def test_action_result_effects_must_be_declared_by_compiled_contract(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -894,9 +903,11 @@ def test_action_result_effect_refs_must_be_declared_by_contract(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -966,9 +977,11 @@ def test_action_result_summary_evidence_refs_must_be_declared_by_contract(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -1004,9 +1017,11 @@ def test_action_result_summary_evidence_refs_must_be_grounded_in_reported_result violations = list( iter_participant_behavior_history_violations( _history_payloads_for_action_result(result), - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) @@ -1052,9 +1067,11 @@ def test_action_result_refs_must_be_authorized_by_observation_boundary(): violations = list( iter_participant_behavior_history_violations( _history_payloads_for_action_result(result), - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses=None, + ), action_contracts=model.action_contracts, - observation_boundary_addresses=None, observation_boundaries=model.observation_boundaries, ) ) @@ -1096,9 +1113,11 @@ def test_action_result_effect_targets_must_be_authorized_by_observation_boundary violations = list( iter_participant_behavior_history_violations( _history_payloads_for_action_result(result), - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses=None, + ), action_contracts=model.action_contracts, - observation_boundary_addresses=None, observation_boundaries=model.observation_boundaries, ) ) @@ -1140,9 +1159,11 @@ def test_action_result_evidence_refs_must_be_authorized_by_observation_boundary( violations = list( iter_participant_behavior_history_violations( _history_payloads_for_action_result(result), - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses=None, + ), action_contracts=model.action_contracts, - observation_boundary_addresses=None, observation_boundaries=model.observation_boundaries, ) ) @@ -1207,9 +1228,11 @@ def test_action_result_failure_class_must_be_declared_by_compiled_contract(): }, observation.to_payload(), ], - action_contract_addresses=set(model.action_contracts), + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses=set(model.action_contracts), + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), action_contracts=model.action_contracts, - observation_boundary_addresses={OBSERVATION_ADDRESS}, ) ) diff --git a/implementations/python/tests/test_sem_212_participant_attribution_semantics.py b/implementations/python/tests/test_sem_212_participant_attribution_semantics.py index ff038d1df..eb746cd2d 100644 --- a/implementations/python/tests/test_sem_212_participant_attribution_semantics.py +++ b/implementations/python/tests/test_sem_212_participant_attribution_semantics.py @@ -16,6 +16,7 @@ ParticipantAttributionOrderingBasis, ParticipantBehaviorHistoryEvent, ParticipantBehaviorHistoryEventType, + ParticipantHistoryAddressScope, ParticipantObservationBoundaryRuntime, ParticipantObservationStatus, iter_participant_behavior_history_violations, @@ -262,8 +263,10 @@ def test_attribution_evidence_refs_must_be_authorized_by_participant_boundary() violations = list( iter_participant_behavior_history_violations( _history_payloads(edge), - action_contract_addresses={ACTION_ADDRESS}, - observation_boundary_addresses={OBSERVATION_ADDRESS}, + address_scope=ParticipantHistoryAddressScope( + action_contract_addresses={ACTION_ADDRESS}, + observation_boundary_addresses={OBSERVATION_ADDRESS}, + ), observation_boundaries={OBSERVATION_ADDRESS: boundary}, ) ) diff --git a/implementations/python/tests/test_sem_218_explicitness.py b/implementations/python/tests/test_sem_218_explicitness.py index a211c34cf..15ba545f1 100644 --- a/implementations/python/tests/test_sem_218_explicitness.py +++ b/implementations/python/tests/test_sem_218_explicitness.py @@ -35,7 +35,8 @@ def _scenario_with_explicitness_cases(): network: endpoints: - network: net - network_id_stability: unknown + backend: + driver: unknown infrastructure: net: count: 1 @@ -56,10 +57,7 @@ def test_semantic_validator_attaches_exact_constrained_and_open_classifications( assert explicitness["nodes.vm.resources.cpu"].classification is ExplicitnessClass.EXACT assert explicitness["nodes.vm.os"].classification is ExplicitnessClass.CONSTRAINED - assert ( - explicitness["nodes.vm.runtime.network.endpoints[0].network_id_stability"].classification - is ExplicitnessClass.OPEN - ) + assert explicitness["nodes.vm.runtime.network.endpoints[0].backend.driver"].classification is ExplicitnessClass.OPEN def test_instantiation_downgrades_substituted_values_without_creating_false_exactness(): diff --git a/implementations/python/tests/test_sem_218_realization.py b/implementations/python/tests/test_sem_218_realization.py index c49a01192..c706cfbd6 100644 --- a/implementations/python/tests/test_sem_218_realization.py +++ b/implementations/python/tests/test_sem_218_realization.py @@ -15,7 +15,7 @@ from aces_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration from aces_contracts.vocabulary import RealizationSupportMode -from aces_sdl.explicitness import ExplicitnessClass +from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance from aces.backends.stubs import create_stub_manifest from aces.core.runtime.capabilities import BackendManifest, ProvisionerCapabilities @@ -102,6 +102,18 @@ def test_compiled_class_matches_scenario_classifier_output(): assert req.explicitness is classified[req.field_path].classification +def test_compiled_provenance_matches_scenario_classifier_output(): + """Differential: compilation preserves the classifier's origin authority.""" + + classified = instantiate_scenario(_scenario(_CONSTRAINED_SCENARIO)).explicitness + model = compile_runtime_model(_scenario(_CONSTRAINED_SCENARIO)) + + by_field = {req.field_path: req for req in model.realization_requirements} + assert by_field["nodes.web.os"].provenance is ExplicitnessProvenance.PROCESSOR_DERIVED + assert by_field["nodes.web.os"].provenance is classified["nodes.web.os"].provenance + assert by_field["nodes.web.type"].provenance is ExplicitnessProvenance.AUTHOR_DECLARED + + def test_planner_rejects_unrealizable_exact_declaration(): """Enforcement point 4 / invariant I2: an exact requirement with no exact realization support fails planning with a structured diagnostic.""" diff --git a/implementations/python/tests/test_sem_218_realization_designation.py b/implementations/python/tests/test_sem_218_realization_designation.py new file mode 100644 index 000000000..53dc31cb8 --- /dev/null +++ b/implementations/python/tests/test_sem_218_realization_designation.py @@ -0,0 +1,510 @@ +"""SEM-218 scoped realization designation and cascade behavior.""" + +from __future__ import annotations + +import json +import textwrap +from dataclasses import replace +from pathlib import Path + +import jsonschema +import pytest +from aces_backend_libvirt.manifest import create_libvirt_manifest +from aces_backend_protocols.capabilities import BackendManifest, ProvisionerCapabilities +from aces_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration +from aces_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp, RuntimeDomain +from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel, realization_envelope_digest +from aces_contracts.runtime_state import RuntimeSnapshot, RuntimeSnapshotEnvelope, SnapshotEntry +from aces_contracts.vocabulary import Closure, RealizationSupportMode +from aces_processor.compiler import compile_runtime_model +from aces_processor.planner import plan +from aces_processor.semantics.realization import realization_disclosure +from aces_runtime.control_plane_api_models import _snapshot_model +from aces_sdl._errors import SDLParseError +from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_sdl.instantiate import instantiate_scenario +from aces_sdl.parser import parse_sdl, parse_sdl_file +from aces_sdl.realization_designation import ( + AuthorRealizationPosture, + RealizationDesignationRecord, + RealizationScopeDesignation, + resolve_json_pointer_surface, +) +from pydantic import ValidationError + + +def _scenario(realization: str = "", *, web_os: str = ""): + realization_block = textwrap.dedent(realization).strip() + web_os_line = f" {web_os}\n" if web_os else "" + return parse_sdl( + f"name: scoped-realization\n" + f"{realization_block + chr(10) if realization_block else ''}" + "nodes:\n" + " web:\n" + " type: vm\n" + f"{web_os_line}" + " resources: {ram: 1 gib, cpu: 1}\n" + " worker:\n" + " type: vm\n" + " resources: {ram: 1 gib, cpu: 1}\n" + ) + + +def _manifest(mode: RealizationSupportMode) -> BackendManifest: + return BackendManifest( + name="designation-test", + version="1.0.0", + supported_contract_versions=frozenset({"backend-manifest-v2"}), + compatible_processors=frozenset({"aces-reference-processor"}), + realization_support=( + RealizationSupportDeclaration( + domain="runtime-realization", + support_mode=mode, + supported_constraint_kinds=frozenset({"os-family", "node-type"}), + supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), + disclosure_kinds=frozenset({"runtime-snapshot-v1"}), + ), + ), + concept_bindings=(ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"),), + provisioner=ProvisionerCapabilities( + name="designation-test", + supported_node_types=frozenset({"vm"}), + supported_os_families=frozenset({"linux"}), + ), + ) + + +def _requirement(model, field_path: str): + return next(requirement for requirement in model.realization_requirements if requirement.field_path == field_path) + + +def _open_manifest_with_envelope(*, path: str, value: str) -> BackendManifest: + manifest = create_libvirt_manifest() + assert manifest.realization_envelope is not None + payload = manifest.realization_envelope.model_dump(mode="json") + payload["expression"]["domains"] = {"fixed": {"kind": "exact", "value": value}} + payload["expression"]["bindings"] = [{"path": path, "scope": "field", "posture": "exact", "domain": "fixed"}] + payload["digest"] = realization_envelope_digest(payload) + envelope = BackendRealizationEnvelopeModel.model_validate(payload) + support = replace( + manifest.realization_support[0], + support_mode=RealizationSupportMode.OPEN_REALIZATION, + ) + return replace(manifest, realization_support=(support,), realization_envelope=envelope) + + +def test_root_open_is_typed_and_carried_to_compilation(): + scenario = _scenario("realization:\n default: open") + + instantiated = instantiate_scenario(scenario) + model = compile_runtime_model(instantiated) + + assert scenario.realization is not None + assert scenario.realization.default is AuthorRealizationPosture.OPEN + assert instantiated.instantiation_provenance.realization_designations[0].posture is AuthorRealizationPosture.OPEN + requirement = _requirement(model, "nodes.web.os") + assert requirement.explicitness is ExplicitnessClass.OPEN + assert requirement.governing_scope == "#/" + + +def test_most_specific_scopes_override_in_both_directions_and_ignore_order(): + open_then_closed = """realization: + default: open + scopes: + - field_pointer: /nodes + posture: closed + - field_pointer: /nodes/worker + posture: open + """ + reversed_scopes = """realization: + default: open + scopes: + - field_pointer: /nodes/worker + posture: open + - field_pointer: /nodes + posture: closed + """ + + first = compile_runtime_model(_scenario(open_then_closed)) + second = compile_runtime_model(_scenario(reversed_scopes)) + + assert { + requirement.field_path + for requirement in first.realization_requirements + if requirement.explicitness is ExplicitnessClass.OPEN + } == {"nodes.worker.os"} + assert first.realization_requirements == second.realization_requirements + + closed_then_open = """realization: + default: closed + scopes: + - field_pointer: /nodes + posture: open + - field_pointer: /nodes/web + posture: closed + """ + model = compile_runtime_model(_scenario(closed_then_open)) + assert { + requirement.field_path + for requirement in model.realization_requirements + if requirement.explicitness is ExplicitnessClass.OPEN + } == {"nodes.worker.os"} + + +def test_explicit_leaf_wins_over_inherited_open_posture(): + model = compile_runtime_model(_scenario("realization:\n default: open", web_os="os: linux")) + + requirement = _requirement(model, "nodes.web.os") + assert requirement.explicitness is ExplicitnessClass.EXACT + assert requirement.provenance is ExplicitnessProvenance.AUTHOR_DECLARED + assert requirement.governing_scope == "#/nodes/web/os" + + +def test_omission_and_explicit_root_delegation_remain_distinct(): + omitted = instantiate_scenario(_scenario()) + delegated = instantiate_scenario(_scenario("realization:\n default: unspecified")) + + assert omitted.instantiation_provenance.realization_designations == () + assert ( + delegated.instantiation_provenance.realization_designations[0].posture is AuthorRealizationPosture.UNSPECIFIED + ) + + omitted_model = compile_runtime_model(omitted) + delegated_model = compile_runtime_model(delegated) + assert not any(requirement.field_path == "nodes.web.os" for requirement in omitted_model.realization_requirements) + requirement = _requirement(delegated_model, "nodes.web.os") + assert requirement.delegated is True + assert requirement.explicitness is None + + +@pytest.mark.parametrize( + "realization", + [ + "realization:\n default: open\n scopes:\n - {field_pointer: /nodes/web~2os, posture: closed}", + "realization:\n default: open\n scopes:\n - {field_pointer: nodes/web, posture: closed}", + ( + "realization:\n default: open\n scopes:\n" + " - {field_pointer: /nodes/web, posture: closed}\n" + " - {field_pointer: /nodes/web, posture: open}" + ), + ], +) +def test_invalid_or_conflicting_canonical_scopes_are_rejected(realization: str): + with pytest.raises(SDLParseError): + _scenario(realization) + + +@pytest.mark.parametrize("field_pointer", ["nodes/web", "/nodes/web~2os"]) +def test_realization_designation_models_reject_non_rfc6901_pointers(field_pointer: str): + with pytest.raises(ValidationError): + RealizationScopeDesignation( + field_pointer=field_pointer, + posture=AuthorRealizationPosture.OPEN, + ) + with pytest.raises(ValidationError): + RealizationDesignationRecord( + field_pointer=field_pointer, + posture=AuthorRealizationPosture.OPEN, + ) + + +def test_rfc6901_scope_resolution_preserves_escaped_key_identity(): + found, value = resolve_json_pointer_surface( + {"nodes": {"slash/key": {"os": "linux"}}}, + "/nodes/slash~1key/os", + ) + + assert found and value == "linux" + assert not resolve_json_pointer_surface( + {"nodes": {"slash/key": {"os": "linux"}}}, + "/nodes/slash/key/os", + )[0] + + +def test_imported_scope_is_qualified_and_does_not_leak_to_host_or_sibling(tmp_path: Path): + (tmp_path / "open-module.yaml").write_text( + textwrap.dedent( + """ + name: open-module + version: 1.0.0 + module: + id: acme/open-module + version: 1.0.0 + exports: {nodes: [vm]} + realization: + default: closed + scopes: + - {field_pointer: /nodes/vm, posture: open} + nodes: + vm: {type: vm, resources: {ram: 1 gib, cpu: 1}} + """ + ).strip() + + "\n", + encoding="utf-8", + ) + (tmp_path / "closed-module.yaml").write_text( + textwrap.dedent( + """ + name: closed-module + version: 1.0.0 + module: + id: acme/closed-module + version: 1.0.0 + exports: {nodes: [vm]} + nodes: + vm: {type: vm, resources: {ram: 1 gib, cpu: 1}} + """ + ).strip() + + "\n", + encoding="utf-8", + ) + root = tmp_path / "root.yaml" + root.write_text( + textwrap.dedent( + """ + name: root + imports: + - {source: local:open-module.yaml, namespace: openmod} + - {source: local:closed-module.yaml, namespace: closedmod} + nodes: + host: {type: vm, resources: {ram: 1 gib, cpu: 1}} + """ + ).strip() + + "\n", + encoding="utf-8", + ) + + instantiated = instantiate_scenario(parse_sdl_file(root)) + model = compile_runtime_model(instantiated) + records = instantiated.instantiation_provenance.realization_designations + + assert any(record.namespace == ("openmod",) and record.field_pointer == "/nodes/openmod.vm" for record in records) + assert { + requirement.field_path + for requirement in model.realization_requirements + if requirement.explicitness is ExplicitnessClass.OPEN + } == {"nodes.openmod.vm.os"} + + +def test_open_demand_is_rejected_without_open_realization_support(): + model = compile_runtime_model(_scenario("realization:\n default: open")) + + rejected = plan(model, _manifest(RealizationSupportMode.CONSTRAINED)) + accepted = plan(model, _manifest(RealizationSupportMode.OPEN_REALIZATION)) + + diagnostics = [ + diagnostic + for diagnostic in rejected.diagnostics + if diagnostic.code == "realization.unsupported-open-requirement" + ] + assert diagnostics + assert "nodes.web.os" in diagnostics[0].message + assert "linux" not in diagnostics[0].message + assert accepted.is_valid + assert not any(diagnostic.code.startswith("realization.") for diagnostic in accepted.diagnostics) + + +def test_root_delegation_uses_injected_selected_apparatus_default(): + model = compile_runtime_model(_scenario("realization:\n default: unspecified")) + + legacy_closed = plan(model, _manifest(RealizationSupportMode.CONSTRAINED)) + delegated_open = plan( + model, + _manifest(RealizationSupportMode.CONSTRAINED), + apparatus_realization_default=lambda _requirement, _manifest: Closure.OPEN_WORLD, + ) + + assert not any(diagnostic.code.startswith("realization.") for diagnostic in legacy_closed.diagnostics) + assert any( + diagnostic.code == "realization.unsupported-open-requirement" for diagnostic in delegated_open.diagnostics + ) + + +def test_delegated_open_posture_is_materialized_through_runtime_disclosure(): + model = compile_runtime_model(_scenario("realization:\n default: unspecified")) + execution = plan( + model, + _manifest(RealizationSupportMode.OPEN_REALIZATION), + apparatus_realization_default=lambda _requirement, _manifest: Closure.OPEN_WORLD, + ) + requirement = _requirement(execution.model, "nodes.web.os") + entries = { + operation.address: SnapshotEntry( + address=operation.address, + domain=RuntimeDomain.PROVISIONING, + resource_type=operation.resource_type, + payload=dict(operation.payload), + ) + for operation in execution.provisioning.operations + if operation.action is not ChangeAction.DELETE + } + + diagnostics, provenance = realization_disclosure( + execution.model.realization_requirements, + execution.provisioning, + RuntimeSnapshot(entries=entries), + ) + + assert execution.is_valid + assert requirement.explicitness is ExplicitnessClass.OPEN + assert requirement.delegated is False + assert diagnostics == [] + assert any( + entry.field_path == "nodes.web.os" + and entry.provenance is ExplicitnessProvenance.BACKEND_REALIZED + and entry.governing_scope == "#/" + for entry in provenance + ) + + +def test_open_request_uses_envelope_subsumption_only_for_open_concern_paths(): + model = compile_runtime_model(_scenario("realization:\n default: open")) + + restricted = plan( + model, + _open_manifest_with_envelope(path="nodes.web.os", value="linux"), + ) + unrelated = plan( + model, + _open_manifest_with_envelope(path="name", value="scoped-realization"), + ) + + assert any( + diagnostic.code == "realization-envelope.subsumption.requested-unconstrained" + and diagnostic.address == "nodes.web.os" + for diagnostic in restricted.diagnostics + ) + assert unrelated.is_valid + + +def test_backend_realized_open_slot_discloses_governing_scope_through_api(): + model = compile_runtime_model(_scenario("realization:\n default: open")) + requirement = _requirement(model, "nodes.web.os") + plan_payload = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=requirement.address, + resource_type="node", + payload={"node_type": "vm"}, + ) + ] + ) + snapshot = RuntimeSnapshot( + entries={ + requirement.address: SnapshotEntry( + address=requirement.address, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"node_type": "vm", "os_family": "linux"}, + ) + } + ) + + diagnostics, provenance = realization_disclosure((requirement,), plan_payload, snapshot) + delivered = _snapshot_model(RuntimeSnapshotEnvelope(snapshot=RuntimeSnapshot(realization_provenance=provenance))) + + assert diagnostics == [] + assert provenance[0].provenance is ExplicitnessProvenance.BACKEND_REALIZED + assert provenance[0].governing_scope == "#/" + assert delivered.realization_provenance[0].governing_scope == "#/" + + +@pytest.mark.parametrize("action", [None, ChangeAction.DELETE]) +def test_open_disclosure_requires_an_active_plan_operation(action: ChangeAction | None): + model = compile_runtime_model(_scenario("realization:\n default: open")) + requirement = _requirement(model, "nodes.web.os") + operations = ( + [] + if action is None + else [ + ProvisionOp( + action=action, + address=requirement.address, + resource_type="node", + payload={}, + ) + ] + ) + snapshot = RuntimeSnapshot( + entries={ + requirement.address: SnapshotEntry( + address=requirement.address, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"os_family": "linux"}, + ) + } + ) + + diagnostics, provenance = realization_disclosure( + (requirement,), + ProvisioningPlan(operations=operations), + snapshot, + ) + + assert diagnostics == [] + assert provenance == () + + +def test_published_schemas_match_designation_and_governing_scope_models(): + repository = Path(__file__).resolve().parents[3] + scenario = _scenario("realization:\n default: open") + instantiated = instantiate_scenario(scenario) + provenance = [ + { + "address": "node.web", + "field_path": "nodes.web.os", + "domain": "runtime-realization", + "requirement_kind": "os-family", + "explicitness": "open", + "provenance": "backend-realized", + "governing_scope": "#/", + } + ] + cases = ( + ( + "contracts/schemas/sdl/sdl-authoring-input-v1.json", + scenario.model_dump(mode="json", by_alias=True), + ), + ( + "contracts/schemas/sdl/instantiated-scenario-v1.json", + instantiated.model_dump(mode="json", by_alias=True), + ), + ( + "contracts/schemas/snapshots/runtime-snapshot-v1.json", + {"schema_version": "runtime-snapshot/v1", "realization_provenance": provenance}, + ), + ) + + for relative_path, payload in cases: + schema = json.loads((repository / relative_path).read_text(encoding="utf-8")) + errors = list(jsonschema.Draft202012Validator(schema).iter_errors(payload)) + assert errors == [] + + +@pytest.mark.parametrize("field_pointer", ["nodes/web", "/nodes/web~2os"]) +def test_published_phase_schemas_reject_non_rfc6901_designation_pointers(field_pointer: str): + repository = Path(__file__).resolve().parents[3] + scenario = _scenario( + """realization: + default: closed + scopes: + - {field_pointer: /nodes/web, posture: open} + """ + ) + authoring_payload = scenario.model_dump(mode="json", by_alias=True) + authoring_payload["realization"]["scopes"][0]["field_pointer"] = field_pointer + + instantiated_payload = instantiate_scenario(scenario).model_dump(mode="json", by_alias=True) + instantiated_payload["instantiation_provenance"]["realization_designations"][1]["field_pointer"] = field_pointer + + cases = ( + ("contracts/schemas/sdl/sdl-authoring-input-v1.json", authoring_payload), + ("contracts/schemas/sdl/instantiated-scenario-v1.json", instantiated_payload), + ("contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json", instantiated_payload), + ) + for relative_path, payload in cases: + schema = json.loads((repository / relative_path).read_text(encoding="utf-8")) + errors = list(jsonschema.Draft202012Validator(schema).iter_errors(payload)) + assert errors diff --git a/implementations/python/tests/test_sem_218_runtime_realization.py b/implementations/python/tests/test_sem_218_runtime_realization.py index d441b7639..6855b767d 100644 --- a/implementations/python/tests/test_sem_218_runtime_realization.py +++ b/implementations/python/tests/test_sem_218_runtime_realization.py @@ -44,6 +44,17 @@ resources: {ram: 1 gib, cpu: 1} """ +_PARAMETERIZED_SCENARIO = """ +name: sem-218-runtime-parameterized +variables: + os_choice: {type: string, default: linux, allowed_values: [linux, windows]} +nodes: + web: + type: vm + os: ${os_choice} + resources: {ram: 1 gib, cpu: 1} +""" + def _plan_exact(manager: RuntimeManager): return manager.plan(parse_sdl(textwrap.dedent(_EXACT_SCENARIO))) @@ -168,6 +179,20 @@ def test_honest_apply_records_realization_provenance(): assert by_field["nodes.web.type"].explicitness is ExplicitnessClass.EXACT +def test_honoured_parameter_substitution_records_processor_derived_provenance(): + """I5: an honoured substituted value retains its processor-derived origin.""" + + manager = RuntimeManager(create_stub_target()) + plan = manager.plan(parse_sdl(textwrap.dedent(_PARAMETERIZED_SCENARIO))) + + result = manager.apply(plan) + + assert result.success + by_field = {entry.field_path: entry for entry in result.snapshot.realization_provenance} + assert by_field["nodes.web.os"].provenance is ExplicitnessProvenance.PROCESSOR_DERIVED + assert by_field["nodes.web.os"].provenance is not ExplicitnessProvenance.AUTHOR_DECLARED + + def test_realization_provenance_round_trips_through_control_plane_store(): """I5: the provenance ledger survives snapshot persistence serialization.""" diff --git a/noxfile.py b/noxfile.py index 308d69e20..9b29cb3dc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -529,6 +529,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / concept authority governance", "skipped on staged check; runs on push and verify", ) + reporter.skip( + "policy / behavioral relation claims", + "skipped on staged check; runs on push and verify", + ) reporter.skip( "policy / agent guidance profile", "skipped on staged check; runs on push and verify", @@ -562,6 +566,10 @@ def _run_policy(session: nox.Session, reporter: SessionReporter, *args: str) -> "policy / concept authority governance", lambda: _run_project_python(session, "tools/check_concept_authority_governance.py"), ) + reporter.run( + "policy / behavioral relation claims", + lambda: _run_project_python(session, "tools/check_behavioral_relation_claims.py"), + ) reporter.run( "policy / agent guidance profile", lambda: _run_project_python(session, "tools/check_agent_guidance.py"), @@ -623,6 +631,10 @@ def _run_contracts(session: nox.Session, reporter: SessionReporter, *args: str) "contracts / scientific-scenario completeness", lambda: _run_project_python(session, "tools/check_scientific_scenario_completeness.py"), ) + reporter.run( + "contracts / reproducible related-work comparison", + lambda: _run_project_python(session, "tools/check_related_work_comparison.py"), + ) reporter.run( "contracts / json artifact validation", lambda: _run_project_python(session, "tools/check_json_artifacts.py", *json_artifact_args), diff --git a/sonar-project.properties b/sonar-project.properties index 2bb8030cf..03621488e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -62,7 +62,7 @@ sonar.issue.ignore.multicriteria.e3.ruleKey=python:S1720 sonar.issue.ignore.multicriteria.e3.resourceKey=**/*.py sonar.issue.ignore.multicriteria.e4.ruleKey=python:S1128 -sonar.issue.ignore.multicriteria.e4.resourceKey=implementations/python/packages/aces_processor/models.py +sonar.issue.ignore.multicriteria.e4.resourceKey=implementations/python/packages/aces_processor/models/__init__.py # Generated libvirt boot artifacts must be readable by the QEMU process. # The appliance builder never writes secrets to those kernel/initramfs artifacts. diff --git a/specs/README.md b/specs/README.md index 090958390..90fae92a5 100644 --- a/specs/README.md +++ b/specs/README.md @@ -42,7 +42,8 @@ hook). (the catalog set the published `contracts/schemas/sdl/` schemas must agree with; governed by ADR-001 and ADR-009) - `formal/` — optional formal-methods artifacts for semantic and - stateful subsystems (governed by ADR-007 and ADR-018) + stateful subsystems (governed by ADR-007 and ADR-018), including the + behavioral-relation taxonomy and claim discipline governed by ADR-081 - `supply-chain/` — normative prose for the Packaging & Supply Chain wave, including the reusable-asset trust/authenticity/integrity policy (GOV-913, governed by ADR-071) diff --git a/specs/concept-authority/concept-authority.md b/specs/concept-authority/concept-authority.md index df269a800..e113166dc 100644 --- a/specs/concept-authority/concept-authority.md +++ b/specs/concept-authority/concept-authority.md @@ -130,6 +130,7 @@ ecosystem-specific concerns. |--------|-------| | `scenarios` | SDL scenarios, compositions, modules, and authoring constructs. | | `tasks-runs-studies` | Execution lifecycle, run records, and study organization. | +| `behavioral-relations` | Revisioned validation, conformance, comparison, refinement, equivalence, and empirical claim semantics. | | `episodes` | Participant runtime episode identity, lifecycle state, and history boundaries. | | `runtime-inventory` | Observed and declared runtime configuration state attached to scenario nodes. | | `apparatus-declarations` | Processor, backend, and participant-implementation manifests. | @@ -155,6 +156,14 @@ asset, identity, observable, tool, artifact, or relationship — still bind to that narrower family. Runtime inventory records the observed or declared state, never the actions or events that produce, change, or react to it. +The `behavioral-relations` family is native because ACES must govern how its +artifacts bind formal and empirical relations to carriers, observation +projections, quantifiers, evidence boundaries, and assurance states. The +individual mathematical relations retain their revision-pinned publication +lineage in the behavioral-relation catalog; this family owns ACES's claim +discipline, not a replacement definition of actions, observations, scenarios, +apparatuses, runs, studies, or evidence. + ## Extension Discipline ACES-native families must be explicit extensions over the shared concept diff --git a/specs/formal/assurance-fulfillment.yaml b/specs/formal/assurance-fulfillment.yaml index 7e699e275..b4e1bc5f5 100644 --- a/specs/formal/assurance-fulfillment.yaml +++ b/specs/formal/assurance-fulfillment.yaml @@ -27,6 +27,7 @@ adr_refs: - ADR-007 - ADR-018 - ADR-078 + - ADR-081 # Registry of classified formal-spec subsystems. Every immediate subdirectory of # specs/formal/ carrying a README.md must appear here (the checker fails on any @@ -68,6 +69,9 @@ subsystems: - id: sdl-phases path: specs/formal/sdl-phases fm_level: FM2 + - id: behavioral-relations + path: specs/formal/behavioral-relations + fm_level: FM2 # Fulfillment, keyed by subsystem id. delivered_artifacts name a concrete, # non-empty repo path that is the named kind of evidence. waived_artifacts record @@ -251,7 +255,7 @@ entries: - kind: unit_tests path: implementations/python/tests/test_runtime_conformance.py - kind: typed_ir_or_contract_coverage - path: implementations/python/packages/aces_processor/models.py + path: implementations/python/packages/aces_processor/models/history_event.py waived_artifacts: - kind: unit_tests date: 2026-06-23 @@ -367,3 +371,15 @@ entries: - kind: property_based_or_differential_tests path: implementations/python/tests/test_sdl_phase_contracts.py waived_artifacts: [] + + - subsystem: behavioral-relations + delivered_artifacts: + - kind: invariant_list + path: specs/formal/behavioral-relations/README.md + - kind: unit_tests + path: implementations/python/tests/test_behavioral_relations.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_contracts/behavioral_relations.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_behavioral_relations.py + waived_artifacts: [] diff --git a/specs/formal/behavioral-relations/README.md b/specs/formal/behavioral-relations/README.md new file mode 100644 index 000000000..91089cfb9 --- /dev/null +++ b/specs/formal/behavioral-relations/README.md @@ -0,0 +1,282 @@ +# Behavioral-Relation Taxonomy And Claim Discipline + +Classification: FM2, relation and assurance semantics. + +## Scope And Authority + +This specification governs the meaning of ACES claims about validation, +conformance, realization, comparison, refinement, equivalence, participant +visibility, multi-agent interaction, and empirical adequacy. It prevents a +weaker observation from being reported as a stronger behavioral result. + +The machine-readable authority is +`contracts/concept-authority/behavioral-relations-v1.json`, contract +`behavioral-relations/v1`, taxonomy `aces-behavioral-relations`, revision +`rev1`. Relation identifiers, formal dimensions, claim-surface defaults, +bibliography coordinates, assurance status, and worked transition systems are +normative there. ADR-081 governs the architecture. This document is the +normative reader-facing formalization of that catalog. + +The taxonomy defines claim vocabulary and proof obligations. It does not add a +model checker, theorem prover, stochastic simulator, game solver, scheduler, +or runtime engine. A relation whose definition is present but whose assurance +record is `future` or `deliberately-unproved` remains unproved. + +## Core Objects + +Let a labelled transition system be + +```text +L = (S, A, H, ->, I) +``` + +where `S` is a state set, `A` is the action-label set, `H` is the set of hidden +actions, `->` is a labelled transition relation, and `I` is an initial-state +set. A relation definition MUST say when this signature is applicable. A +non-transition predicate MUST explicitly record why the signature is not +applicable rather than silently borrowing transition-system language. + +An observation projection is a revisioned function + +```text +pi[p, policy, revision] : concrete history -> participant-visible history +``` + +with an identified subject, redaction scope, order treatment, and simultaneity +treatment. Two histories are never compared “from the participant view” unless +all of those coordinates are fixed. Projection equality is not access to +hidden global state and does not imply future knowledge or strategic power. + +A claim quantifies separately over states, traces, schedulers, strategies, +environments, and observations. “All” on one axis says nothing about an omitted +axis. A finite case set therefore cannot satisfy a universal quantifier merely +because every executed case passed. + +## Relation Families + +The following identifiers are distinct. A consumer MUST use the exact catalog +identifier rather than an artifact-local synonym. + +| Relation id | Class | What it establishes | +| --- | --- | --- | +| `structural-validity` | predicate | Closed-shape and field-constraint admission under a named schema revision. | +| `semantic-validity` | predicate | Cross-reference and semantic-invariant admission under a named semantic profile. | +| `capability-declaration` | predicate | What an apparatus declares, without proving that declaration true. | +| `profile-satisfaction` | predicate | Satisfaction of every obligation in one named profile. | +| `bounded-probe-success` | predicate | Expected results for an enumerated finite case set. | +| `canonical-artifact-identity` | predicate | Equality of canonical bytes or digest under one canonicalization profile. | +| `realization-envelope-membership` | set relation | A request lies inside one backend realization envelope. | +| `realization-envelope-subsumption` | set relation | Every request admitted by one envelope is admitted by another. | +| `trace-inclusion` | behavioral | Every projected trace on the left is admitted on the right. | +| `trace-equivalence` | behavioral | Mutual projected trace inclusion. | +| `forward-simulation` | behavioral | Each left step is matched by the right under a state relation. | +| `backward-simulation` | behavioral | The reverse directional simulation obligation. | +| `data-refinement` | behavioral | Concrete operations preserve an abstract data-type relation and observations. | +| `strong-bisimulation` | behavioral | Every labelled step is matched in both directions without hiding actions. | +| `weak-bisimulation` | behavioral | Both directions match visible steps while admitting governed hidden-action closure. | +| `participant-projected-history-equivalence` | behavioral | Two histories are equal after the same named participant projection. | +| `epistemic-indistinguishability` | epistemic | Two worlds are indistinguishable to a named participant under an information model. | +| `alternating-strategic-equivalence` | strategic | Named coalitions preserve abilities against quantified opponent choices. | +| `probabilistic-bisimulation` | behavioral | Related states assign equal probability mass to related equivalence classes. | +| `statistical-similarity` | empirical | A declared sample-level similarity result under a named metric and uncertainty procedure. | +| `statistical-equivalence` | empirical | A declared equivalence-test result inside a predeclared margin. | +| `empirical-adequacy` | empirical | Evidence satisfies a preregistered adequacy criterion for a bounded population and purpose. | + +None of these rows is an implication ladder. For example: + +- `canonical-artifact-identity` does not establish execution equality; +- `bounded-probe-success` does not establish `trace-inclusion`; +- `trace-equivalence` does not preserve branching structure and does not + establish `strong-bisimulation`; +- `participant-projected-history-equivalence` does not establish + `epistemic-indistinguishability` or `alternating-strategic-equivalence`; +- `statistical-equivalence` does not establish any behavioral equivalence; +- `profile-satisfaction` does not establish `empirical-adequacy`. + +## Required Formal Dimensions + +Every relation entry MUST state all of the following, even when a dimension is +outside scope: + +1. left and right carrier types and initial-state treatment; +2. labels, transition relation, observable actions, hidden actions, and + stuttering treatment; +3. observation-projection applicability and revision; +4. direction and quantification over states, traces, schedulers, strategies, + environments, and observations; +5. treatment of nondeterminism, concurrency, probability, time, and partial + order; +6. the preserved property and the proof obligation; +7. evidence that may support the relation and explicit nonclaims; +8. definition, implementation, test, and proof assurance states; and +9. revision-pinned primary-source references. + +Missing dimensions are not defaults. They are an incomplete claim. + +## Claim Binding + +A consumer claim is a `BehavioralClaimBindingModel`. It MUST carry: + +```text +(taxonomy_id, taxonomy_revision, relation_id, + subject, left_carrier_ref?, right_carrier_ref?, + observation_projection_ref?, observation_projection_revision?, + quantifier_scope, evidence_scope, evidence_boundary, + assurance_status, evidence_refs[], limitations[], explicit_non_claims[]) +``` + +The relation meaning remains in the catalog; the binding identifies the actual +subject and evidence. Projection coordinates are mandatory for a relation whose +catalog entry has `projection_required: true`. Universal scopes +`all-admitted-inputs`, `all-traces`, and `all-strategies` require model-check or +proof evidence. A finite or statistical record cannot carry one of those +universal scopes. + +Assurance axes are independent: + +| Axis | Question | +| --- | --- | +| definition | Is the relation defined precisely? | +| implementation | Does ACES implement a checker or realization? | +| test | What executable cases exercise it? | +| proof | Is a universal obligation proved, model-checked, deliberately unproved, or future? | + +“Defined” is not “implemented”, “implemented” is not “tested”, and “tested” is +not “proved”. The report truth value or study result remains separate from the +assurance status of the mechanism producing it. + +## Claim Surfaces + +### SDL transformations + +Parse, normalize, expand, instantiate, and canonicalize stages currently carry +`structural-validity`, `semantic-validity`, and +`canonical-artifact-identity` evidence. Their evidence boundary consists of +typed phase functions, invariants, finite round-trip/canonicalization tests, and +property tests. They do not establish `data-refinement`, simulation, +`trace-equivalence`, or bisimulation. + +### Backend realization + +Envelope admission establishes `realization-envelope-membership`. The intended +universal runtime obligation is projection-bound `trace-inclusion`, but it is +deliberately unproved in revision `rev1`. Current conformance reports establish +only `bounded-probe-success` for their named fixture and target-probe cases. +Provisioning success, snapshots, witnesses, and negative probes do not establish +reverse inclusion, equivalence, simulation, or bisimulation. + +### Backend comparison + +A backend comparison MAY report bounded invariant equality, +`canonical-artifact-identity`, `bounded-probe-success`, or a declared empirical +relation. It MUST name the compared population, metrics, case set, environment, +and uncertainty method. Shared results, digests, or finite traces are not a +universal same-behavior result. + +### Participant-visible behavior + +A comparison of two participant histories uses +`participant-projected-history-equivalence` only when both histories use the +same named participant and projection-policy revision. Its evidence boundary is +the compared histories, redaction policy, order policy, simultaneity policy, +and run context. A single projected history is a record, not an equivalence +comparison. + +### Multi-agent interaction + +Current joint-action, simultaneous-move, chance, and mean-field contracts +provide structural and finite evidence only. A future strategic claim MUST use +`alternating-strategic-equivalence` and identify agents, coalitions, action +availability, opponent quantification, information sets, schedulers, objectives, +and preserved abilities. A probabilistic claim MUST use +`probabilistic-bisimulation` and supply the probability kernel and equivalence +classes. Neither relation is implemented or proved in revision `rev1`. + +### Independent adequacy studies + +Study and benchmark contracts bind their conclusions to +`empirical-adequacy`, `statistical-similarity`, or +`statistical-equivalence` as appropriate. The evidence boundary includes the +preregistered population, tasks, conditions, metrics or coding scheme, +uncertainty procedure, equivalence margin when applicable, falsification +criteria, missing-data handling, and limitations. Results from issue #729 will +therefore be bounded empirical evidence, not universal behavioral proof. + +## Worked Counterexamples + +### A passing probe is not bisimulation + +Let the left system have transitions `l0 -a-> l1` and `l0 -b-> l2`. Let the +right system have only `r0 -a-> r1`. The named probe trace `[a]` passes on both +systems, so `bounded-probe-success` may hold for that case. The unmatched `b` +branch means `strong-bisimulation` does not hold. The evidence boundary is the +single `[a]` probe; it does not quantify over the untested branch. + +### Hidden actions separate strong from weak matching + +Let the abstract system perform `send` directly. Let the backend perform +`tau` and then `send`, where `tau` is governed as hidden. Strong matching fails +because the abstract initial state has no `tau` step. The visible trace can +still match after hidden-action closure, which is evidence relevant to +`weak-bisimulation`. A weak relation would still require the bidirectional +state-relation obligation; matching one visible trace does not prove it. + +The executable versions of both examples are embedded in the revisioned +catalog and checked by `implementations/python/tests/test_behavioral_relations.py`. + +## Assurance Boundary For Revision 1 + +Implemented and tested now: + +- structural and semantic validity; +- capability declarations and profile satisfaction; +- bounded conformance probes; +- canonical artifact identity; +- realization-envelope membership and subsumption; and +- participant projection machinery and bounded projected-history comparisons. + +Defined but deliberately unproved or only partially implemented: + +- universal `trace-inclusion` for backend realization; +- `trace-equivalence`, forward/backward simulation, and data refinement; +- strong and weak bisimulation. + +Defined for future governed work and inappropriate to claim from current +artifacts: + +- epistemic indistinguishability without a world/information model; +- alternating strategic equivalence without a game structure; +- probabilistic bisimulation without a probability kernel; +- timed or partial-order equivalence without the corresponding semantic model; +- statistical equivalence without a predeclared margin and test; and +- empirical adequacy without a preregistered population, criteria, and evidence. + +## Primary Sources + +The catalog records the complete title, authors, publication year and venue, +edition/version, and immutable DOI or ISBN for each source. Revision `rev1` +uses, among others: + +- Milner, *A Calculus of Communicating Systems* (1980), + DOI `10.1007/3-540-10235-3`; +- Park, “Concurrency and Automata on Infinite Sequences” (1981), + DOI `10.1007/BFb0017309`; +- van Glabbeek, “The Linear Time–Branching Time Spectrum” (1990), + DOI `10.1007/BFb0039066`; +- Abadi and Lamport, “The Existence of Refinement Mappings” (1991), + DOI `10.1016/0304-3975(91)90224-P`; +- Lynch and Vaandrager, “Forward and Backward Simulations” (1995), + DOI `10.1006/inco.1995.1134`; +- Fagin, Halpern, Moses, and Vardi, *Reasoning About Knowledge* (1995), + ISBN `9780262061629`; +- Alur, Henzinger, Kupferman, and Vardi, “Alternating Refinement Relations” + (1998), DOI `10.1007/BFb0055622`; +- Alur, Henzinger, and Kupferman, “Alternating-Time Temporal Logic” (2002), + DOI `10.1145/585265.585270`; +- Larsen and Skou, “Bisimulation Through Probabilistic Testing” (1991), + DOI `10.1016/0890-5401(91)90030-6`; and +- Wellek, *Testing Statistical Hypotheses of Equivalence and Noninferiority*, + second edition (2010), ISBN `9781439808184`. + +Bibliographic prose here is an aid. The machine-readable catalog is the +revision-pinned identity surface. diff --git a/specs/formal/objectives/README.md b/specs/formal/objectives/README.md index 701b0ae56..ea5211f0c 100644 --- a/specs/formal/objectives/README.md +++ b/specs/formal/objectives/README.md @@ -47,7 +47,7 @@ target-resolution, success-interpretation, and dependency-ordering semantics - compiled runtime objective resource, addresses, diagnostics, and ordering/refresh derivation: - `implementations/python/packages/aces_processor/compiler.py` - - `implementations/python/packages/aces_processor/models.py` + - `implementations/python/packages/aces_processor/models/` - planner ordering/refresh reconciliation: - `implementations/python/packages/aces_processor/planner.py` - `implementations/python/packages/aces_processor/semantics/planner.py` diff --git a/specs/formal/objectives/declarative-objective-semantics.md b/specs/formal/objectives/declarative-objective-semantics.md index 9b8994764..1e185e560 100644 --- a/specs/formal/objectives/declarative-objective-semantics.md +++ b/specs/formal/objectives/declarative-objective-semantics.md @@ -178,7 +178,7 @@ Avoid: - compiled runtime objective resource, canonical addresses, diagnostics, and ordering/refresh derivation: - `implementations/python/packages/aces_processor/compiler.py` - - `implementations/python/packages/aces_processor/models.py` (`ObjectiveRuntime`, + - `implementations/python/packages/aces_processor/models/` (`ObjectiveRuntime`, `Diagnostic`) - planner ordering/refresh reconciliation over the compiled edges: - `implementations/python/packages/aces_processor/planner.py` diff --git a/specs/formal/objectives/window-consistency.md b/specs/formal/objectives/window-consistency.md index f8150fc39..762445fe5 100644 --- a/specs/formal/objectives/window-consistency.md +++ b/specs/formal/objectives/window-consistency.md @@ -72,7 +72,7 @@ shape before compiler/planner semantics run. Each resolved reference carries: - validator checks: `implementations/python/packages/aces_sdl/validator/` - compiled runtime references and refresh derivation: - `implementations/python/packages/aces_processor/compiler.py` - - `implementations/python/packages/aces_processor/models.py` + - `implementations/python/packages/aces_processor/models/` - differential and property tests: - `implementations/python/tests/test_semantics_objectives.py` (`test_window_invariant_story_refs_must_resolve`, diff --git a/specs/formal/participant-runtime/README.md b/specs/formal/participant-runtime/README.md index 676de4e07..30bac2d99 100644 --- a/specs/formal/participant-runtime/README.md +++ b/specs/formal/participant-runtime/README.md @@ -3860,8 +3860,13 @@ to the supported conclusion scope or explicitly record ## Refinement And Conformance Obligations -An implementation refines this design when there is a projection from concrete -backend traces to valid abstract ACES traces. +The intended universal relation is `trace-inclusion`: under the named +participant observation projection, every admitted concrete backend trace must +map to a valid abstract ACES trace. This section defines that obligation; it +does not establish it. Current executable evidence is bounded to named fixtures +and target probes, so no simulation, data-refinement, trace-equivalence, or +bisimulation claim follows. The evidence boundary for each executed check must +be carried by its conformance report. Required preservation properties: diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index bc36b8da4..855c606eb 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -721,7 +721,7 @@ Current implementation artifacts for the `SEM-208` slice: - `implementations/python/packages/aces_processor/compiler.py` maps authored participants to compiled participant action, observation, and behavior addresses; -- `implementations/python/packages/aces_processor/models.py` defines +- `implementations/python/packages/aces_processor/models/` defines participant behavior-history events and validates action/observation/state transition totality over compiled addresses; - `implementations/python/tests/test_sem_208_participant_behavior.py` covers @@ -764,7 +764,7 @@ Current implementation artifacts for the `SEM-209` slice: - `implementations/python/packages/aces_processor/compiler.py` carries declared interaction classes and shared-state references into compiled participant action contracts; -- `implementations/python/packages/aces_processor/models.py` records +- `implementations/python/packages/aces_processor/models/` records `joint_action_set_id`, `realized_order`, interaction class, interaction reference, and shared-state references in participant behavior history, and rejects duplicate realized orders within one joint action set; @@ -838,7 +838,7 @@ Current implementation artifacts for the `SEM-210` slice: evidence-only, and realized-view disclosure metadata into compiled participant observation boundaries, including an ordered `view_relation_timeline` snapshot series for `V_p,t`; -- `implementations/python/packages/aces_processor/models.py` exposes the +- `implementations/python/packages/aces_processor/models/` exposes the compiled visibility metadata for runtime planning, snapshots, and conformance consumers, and validates observation detail refs against the corresponding timeline snapshot; @@ -910,7 +910,7 @@ Current implementation artifacts for the `SEM-211` slice: - `implementations/python/packages/aces_processor/compiler.py` carries the typed precondition classes, effect classes, failure classes, and backend failure mappings into compiled participant action contracts; -- `implementations/python/packages/aces_processor/models.py` defines typed +- `implementations/python/packages/aces_processor/models/` defines typed action precondition results, action effect results, action results, fail-closed validation for unsatisfied or unresolved preconditions, behavior history action-result embedding, compiled-contract validation for declared @@ -951,10 +951,10 @@ Current implementation artifacts for the first `SEM-212` slice: - `implementations/python/packages/aces_sdl/participant_attribution_semantics.py` defines controlled candidate, ordering-basis, and support-class vocabularies; -- `implementations/python/packages/aces_processor/models.py` defines typed +- `implementations/python/packages/aces_processor/models/` defines typed attribution candidates, ordering bases, evidence bases, and attribution edges on participant behavior-history observation events; -- `implementations/python/packages/aces_processor/models.py` validates +- `implementations/python/packages/aces_processor/models/` validates participant/episode/observation scope, explicit ordering and evidence bases, outcome interpretation-rule refs, timestamp-adjacency limits for strong causal support, effect grounding in actual observations/action @@ -1007,7 +1007,7 @@ Current implementation artifacts for the first `SEM-213` slice: - `implementations/python/packages/aces_processor/compiler.py` carries temporal contract ids, kinds, time domains, clock authorities, and backend timing disclosures into compiled participant action contracts; -- `implementations/python/packages/aces_processor/models.py` defines runtime +- `implementations/python/packages/aces_processor/models/` defines runtime temporal context on participant behavior-history events, validates it against the compiled action contract, and exposes an abstract state-machine checker for cadence, deadline, dwell, timeout, reset, and replay interactions; diff --git a/specs/formal/realization/envelope-semantics.md b/specs/formal/realization/envelope-semantics.md index b5ce4e6c9..eab581d8f 100644 --- a/specs/formal/realization/envelope-semantics.md +++ b/specs/formal/realization/envelope-semantics.md @@ -161,13 +161,23 @@ Invalid SDL is never a member, even if it satisfies the envelope domains. ### R2 - Effective bindings use most-specific-wins For a concrete SDL path, the effective binding is the most specific binding -whose scope contains that path. +whose scope contains that path. Implementations MUST parse the complete +canonical path; accepting only the tokenizable fragments of a malformed path is +not conforming. When bindings address the same path depth, semantic scope order +breaks the tie: field, node, topology/app, then scenario. Topology and app remain +sibling scopes. If two bindings have equal specificity and incompatible domains or posture, the envelope is invalid. If a more-specific binding widens a value that an enclosing closed-world scope made exact or excluded, the envelope is invalid unless the enclosing binding explicitly marks that child as overrideable. +That `overrideable` condition governs explicit envelope domain bindings. It +does not prohibit the separate SEM-218 lexical author-default cascade from +overriding inherited open/closed posture in either direction. In closure +resolution, an effective `open-world` overlay removes inherited +`closed-world` state at the same or a descendant path. + Most-specific-wins is therefore deterministic; it is not merge-order dependent. ### R3 - Domain membership is structural diff --git a/specs/formal/realization/explicitness-and-realization.md b/specs/formal/realization/explicitness-and-realization.md index 8666a74e5..4983a5ce2 100644 --- a/specs/formal/realization/explicitness-and-realization.md +++ b/specs/formal/realization/explicitness-and-realization.md @@ -25,9 +25,10 @@ that names a concern a backend will later realize, three questions: value or structure, and under what bounds? 3. if the realizer cannot honor a binding declaration, what must happen? -The note does not introduce new SDL syntax, define new exact-requirement -kinds, or describe wave-3 participant semantics that build on this -boundary. Those are governed elsewhere. +The note owns one typed scenario-root SDL `realization` designation surface. +It does not add realization fields to nested models, define new +exact-requirement kinds, or describe wave-3 participant semantics that build +on this boundary. Those are governed elsewhere. ## Realization Status @@ -52,6 +53,9 @@ is `active`. What is *enforced today* spans authoring through observation: - the SEM-218 classifier in `aces_sdl.explicitness`, invoked by `SemanticValidator`, which tags authored SDL declarations as *exact*, *constrained*, or *open* for downstream consumers; +- the typed `aces_sdl.realization_designation` authoring surface, which carries + a scenario default and RFC 6901 scoped overrides using `closed`, `open`, or + `unspecified`, resolved by semantic specificity rather than list order; - the instantiation downgrade rule in `instantiate_scenario`, which preserves authored explicitness metadata across parameter/default substitution without promoting substituted concrete values to false @@ -60,9 +64,9 @@ is `active`. What is *enforced today* spans authoring through observation: each authored realization concern into a `CompiledRealizationRequirement` on the `RuntimeModel` preserving its exact / constrained / open class; - the planner realization-support gate in `aces_processor.planner.plan`, - which matches each compiled exact / constrained requirement kind against - the selected backend's `realization_support` and rejects an unsupported - kind with a structured `Diagnostic` before deployment; + which matches each compiled exact / constrained / open requirement against + the selected backend's `realization_support` and rejects open demand unless + the selected domain explicitly declares `OPEN_REALIZATION`; - the runtime non-approximation gate on backend adapters in `aces_processor.semantics.realization.realization_disclosure` (invoked from `aces_runtime.backend_calls._call_backend_apply`, the runtime adapter @@ -72,7 +76,9 @@ is `active`. What is *enforced today* spans authoring through observation: accepted into runtime state; - the SEM-218 provenance ledger (`realization_provenance`) on the runtime snapshot envelope, which records each realized concern with its explicitness - class and author-declared / processor-derived / backend-realized origin. + class, author-declared / processor-derived / backend-realized origin, and a + redacted governing-scope reference preserved through store and authenticated + snapshot API conversion. With those last two enforcement points realized, every `SEM-200` lifecycle phase boundary is now enforced by named, tested code; no rule in this spec @@ -102,6 +108,8 @@ extend these rather than introduce parallel registries: - shared SDL static semantics: `implementations/python/packages/aces_sdl/validator/` (`SemanticValidator`, `SDLValidationError`) +- scoped author designation and canonical scope resolution: + `implementations/python/packages/aces_sdl/realization_designation.py` - instantiation and revalidation: `implementations/python/packages/aces_sdl/instantiate.py` (`instantiate_scenario`, `SDLInstantiationError`) @@ -109,7 +117,7 @@ extend these rather than introduce parallel registries: `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_processor/semantics/planner.py` - runtime diagnostics, results, and snapshots: - `implementations/python/packages/aces_processor/models.py` + `implementations/python/packages/aces_processor/models/` (`Diagnostic`, runtime plan / result / snapshot models) - semantic profiles, controlled vocabularies, and reference models: `specs/concept-authority/`, `implementations/python/packages/aces_contracts/` @@ -136,21 +144,12 @@ classes: realizable later. The realizer MAY pick any realization consistent with its declared capability domain. -The taxonomy is binding immediately. The **concrete classification -authority** — the per-field designation that says, for each SDL field -and apparatus-manifest slot, which class it carries (and, for open -fields, the set of realizable points the schema admits) — is staged -work and is not delivered by this spec. Until that authority lands, the -existing closed-Pydantic SDL models, controlled vocabularies, semantic -profiles, and apparatus-contract type system serve as the structural -floor: closed models reject undeclared fields (so silence is not -freedom), and `RealizationSupportDeclaration` requires explicit -exact-requirement-kinds / constraint-kinds (so apparatus capability is -typed). Per-field SDL classification (e.g., "field X on construct Y is -exact and bound to vocabulary Z; field Q on construct R is open and -realizable at point P") will land with the SEM-218 classifier -implementation as a designation table or annotation surface, and is -tracked under the SEM-218 coverage row. +The taxonomy and its **concrete classification authority** are binding. The +incumbent classifier owns explicit leaf declarations. The scenario-root typed +designation table owns inherited defaults only at realization points registered +by the same SEM-218 concern authority. A scope default never turns an arbitrary +missing model field into a realization point and never relaxes closed Pydantic +unknown-key rejection. Silence at any other point is **not** open realization. The SDL parser, `SemanticValidator`, instantiation, the compiler, and the runtime @@ -159,6 +158,24 @@ validation failure, not as permission to fill them in. This is the closed-world default for the ecosystem; the existing closed-Pydantic SDL model boundary (`extra="forbid"`) enforces it structurally today. +### Scoped default cascade + +`Scenario.realization` is optional. When present, it contains a root `default` +and zero or more `scopes`. The only author posture spellings are `closed`, +`open`, and `unspecified`. Scope identities combine a structured module +`namespace` tuple with an RFC 6901 `field_pointer`; JSONPath, wildcards, and +permissive dotted author paths are not accepted. + +Resolution is deterministic: an explicit leaf exact/constrained/open +declaration wins first, then the most-specific concrete scoped default, then an +inherited concrete outer default. `unspecified` delegates to the selected +apparatus default; until a typed apparatus-default contract exists, that seam +resolves closed. Omitting `realization` is distinct and preserves the legacy +closed fallback. Equal-scope duplicates are invalid, and list/import order has +no semantic effect. Imported declarations and designation scopes are qualified +through the composition symbol map, so a module default cannot govern host or +sibling declarations. + ### Five invariants The following invariants are normative for every stage that has authority @@ -188,7 +205,10 @@ does not pick values for underspecified concerns. concern only at a point where the owning SDL schema or semantic rule explicitly designates that concern as realizable, *and* where the backend's manifest declares a per-domain `RealizationSupportMode` of -`CONSTRAINED` or `OPEN_REALIZATION` covering that concern. Silence +`OPEN_REALIZATION` covering that open concern. When the backend also publishes +a finer realization envelope, its offered projection MUST subsume the compiled +open request under the canonical envelope relation. Constrained declarations +use the separately declared constrained support surface. Silence outside such designated points is fail-closed: the authoring artifact MUST be rejected with a structured error rather than filled in. @@ -229,12 +249,13 @@ history, or evidence envelopes MUST be recorded with provenance distinguishing three origins: - **author-declared** — the value is exactly as the author wrote it in - the SDL (or as the author's parameter input resolved at - instantiation); + the SDL and survives instantiation without parameter or default + substitution; - **processor-derived** — the value was produced by deterministic processor activity that does NOT constitute realization: parameter - substitution, defaulting permitted by the SDL schema, canonical - identity normalization, and compilation transformations. The + substitution (whether the selected value is caller-bound or comes from an + SDL default), defaulting permitted by the SDL schema, canonical identity + normalization, and compilation transformations. The processor does not realize underspecified concerns; "processor-derived" is the provenance label for deterministic processing of declared input. @@ -243,9 +264,9 @@ distinguishing three origins: Provenance lives on the existing runtime plan / result / snapshot / participant-episode contracts; no new private channel may carry -realization decisions out of band. The provenance contract above is -normative; the per-field provenance encoding on those existing runtime -contracts is staged work tracked under the SEM-218 coverage row. +realization decisions out of band. The provenance contract above is realized +on the existing runtime snapshot contract. Backend-filled open slots carry a +stable `governing_scope` reference, never the realized value itself. ### Phase responsibilities @@ -257,11 +278,11 @@ now enforced by named, tested code. | Phase | Responsibility | Status | | --- | --- | --- | -| Authoring | Source of declarations. The authoring layer is the only authority that may classify a construct as exact, constrained, or open; downstream stages MUST treat that classification as immutable input. | partial — closed Pydantic SDL models (`extra="forbid"`), the apparatus-contract type system, and the `aces_sdl.explicitness` classifier carry the exact / constrained / open classification for authored SDL declarations. | -| Validation | At the apparatus-contract layer: the shape gates on backend `RealizationSupportDeclaration` (I4 structural floor), the JSON-schema conditional gate, and `ProcessorManifestV2Model`'s asymmetric rejection of `realization_support` are enforced. At the SDL scenario layer: `SemanticValidator` enforces fail-closed validation on the *existing* closed SDL models and attaches SEM-218 classifier output to the validated scenario. | partial — apparatus-contract validation (manifest shape) and SDL-scenario classifier output are enforced; the compiler, planner, and runtime consumers of that output are realized (see those rows), and only the per-field SDL classification authority for the remaining concerns stays staged. | -| Instantiation | Parameter and default substitution may resolve open concerns and constrained surfaces. Substitution MUST NOT downgrade an exact declaration into a constrained or open one, and MUST NOT introduce an exact declaration that the author did not write. The concrete scenario MUST be revalidated after substitution. | partial — `instantiate_scenario` revalidates after substitution and derives instantiated explicitness from the authored classification so substituted values do not become false exact declarations. | -| Compilation | Lowers each declaration into a typed runtime requirement preserving class. Exact requirements carry their declared kind into the compiled representation; constrained requirements carry the typed constraint surface; open requirements are emitted as realizable slots tagged with the realization-and-disclosure family. | partial — `compile_runtime_model` emits `CompiledRealizationRequirement` metadata on the `RuntimeModel` preserving the exact / constrained / open class for the authored realization concerns the planner validates; per-field designation for the remaining concerns stays staged. | -| Planning | Matches every compiled requirement against the candidate backend manifest. An unsupported exact-requirement-kind MUST cause plan rejection through a structured `Diagnostic` before deployment; an unsupported constraint-kind MUST cause the same outcome. An open realizable slot MAY be left for the backend only when its manifest declares matching support. | partial — `aces_processor.planner.plan` matches each compiled exact / constrained requirement kind against the backend's `realization_support` and emits a rejecting `Diagnostic` for an unsupported kind; the Execution-phase runtime non-approximation gate it hands off to is realized in `aces_processor.semantics.realization` (invoked from the `aces_runtime` adapter boundary). | +| Authoring | Source of declarations. The authoring layer is the only authority that may classify a construct as exact, constrained, or open; downstream stages MUST treat that classification as immutable input. | active — closed Pydantic models, the leaf classifier, and the typed scenario/scoped `realization` designation surface carry author intent without conflating it with apparatus capability. | +| Validation | Apparatus declarations and SDL designation scopes are structurally and semantically validated, including canonical pointers, namespace ownership, and equal-specificity conflicts. | active — contract shape gates and `SemanticValidator` enforce both authorities fail closed. | +| Instantiation | Parameter and default substitution may resolve open concerns and constrained surfaces. Substitution MUST NOT downgrade an exact declaration into a constrained or open one, and MUST NOT introduce an exact declaration that the author did not write. The concrete scenario MUST be revalidated after substitution. | active — `instantiate_scenario` preserves explicitness plus typed designation provenance and revalidates concrete content. | +| Compilation | Lowers each declaration into a typed runtime requirement preserving class. Exact requirements carry their declared kind into the compiled representation; constrained requirements carry the typed constraint surface; open requirements are emitted as realizable slots tagged with the realization-and-disclosure family. | active — `compile_runtime_model` applies leaf precedence and the scoped cascade, carrying open and delegated demand plus its governing scope. | +| Planning | Matches every compiled requirement against the candidate backend manifest. An unsupported exact-requirement-kind MUST cause plan rejection through a structured `Diagnostic` before deployment; an unsupported constraint-kind MUST cause the same outcome. An open realizable slot MAY be left for the backend only when its manifest declares matching support. | active — the planner rejects open demand without explicit `OPEN_REALIZATION` support and never approximates it. | | Execution | Backend realizers honor the compiled class. A runtime adapter MUST NOT silently broaden an exact requirement, MUST NOT silently narrow an open realization beyond its declared constraints, and MUST surface incompatibilities through the existing runtime error envelope rather than approximate. | active — the runtime non-approximation gate `aces_processor.semantics.realization.realization_disclosure` (invoked from `aces_runtime.backend_calls._call_backend_apply`) compares each realized exact concern against the author declaration and rejects a silent approximation with a `runtime.backend-contract-invalid` diagnostic before the backend snapshot is accepted. | | Observation | Realized values land in plan, result, snapshot, history, and evidence surfaces with provenance per I5. Realization choices are observation data, not private backend state. | active — realized concerns are recorded on the runtime snapshot envelope's `realization_provenance` ledger with their explicitness class and author-declared / processor-derived / backend-realized origin; the snapshot is the aggregate observation surface that carries result and history data. | @@ -297,27 +318,31 @@ realization status, are: substitution (I1). *Enforced today* by `instantiate_scenario`, `SDLInstantiationError`, and `aces_sdl.explicitness` deriving the instantiated explicitness map from the pre-substitution authored map. -- **Compiler / planner gate** — compiled exact-requirement-kinds MUST - be matched against the selected backend's `realization_support`; - unsupported kinds MUST cause `Diagnostic`-bearing rejection before +- **Compiler / planner gate** — compiled exact, constrained, and open + requirements MUST be matched against the selected backend's + `realization_support`; unsupported kinds and unsupported open demand MUST + cause `Diagnostic`-bearing rejection before deployment (I1, I2, I4). *Enforced today* by the typed compiler emission on `RuntimeModel.realization_requirements` and the - realization-support gate in `aces_processor.planner.plan`. + realization-support plus open-request envelope-subsumption gates in + `aces_processor.planner.plan`. - **Error-envelope gate** — unsupported exact requirements and forbidden approximations MUST be surfaced through stable validation errors or structured diagnostics (I1, I2). *Enforced today*; the shape-gate errors and the planner's `realization.unsupported-exact-requirement` / - `realization.unsupported-constraint-requirement` diagnostics surface - unsupported kinds before deployment. + `realization.unsupported-constraint-requirement` / + `realization.unsupported-open-requirement` diagnostics surface unsupported + demand before deployment. - **Persistence and observation gate** — values entering snapshots, results, history, and evidence MUST carry provenance distinguishing author-declared, processor-derived, and backend-realized origins (I5). *Enforced today* by the `realization_provenance` ledger on the runtime snapshot envelope (`RealizationProvenanceEntry` / `RealizationProvenanceEntryModel`), which records each realized concern's - explicitness class and origin and round-trips through the control-plane - snapshot serializers. + explicitness class, origin, and governing designation scope and round-trips + through the control-plane snapshot serializers and authenticated snapshot + response model. - **Host / OS exposure gate** — exact values, credentials, and backend tokens MUST NOT be passed through process argv, logs, audit details, diagnostics, JSON fixtures, or semantic-profile artifacts when they @@ -436,7 +461,9 @@ invariant I1–I5 is enforced by named code. `aces_processor.semantics.realization.realization_support_diagnostics` (called from `aces_processor.planner.plan`) matches them against the backend's `realization_support`, rejecting unsupported kinds with a - `Diagnostic`. + `Diagnostic`. `realization_envelope_diagnostics` projects the offered + envelope onto compiled open concern paths and calls the canonical + `subsumes()` relation. - I2 runtime non-approximation gate — `aces_processor.semantics.realization.realization_disclosure` (re-exported through `aces_processor.planner`), invoked from @@ -478,15 +505,20 @@ invariant I1–I5 is enforced by named code. realization (I2), the `realization_provenance` ledger recorded for honoured concerns (I5), the rejection diagnostic naming the field path and kind but not the value, and the ledger's round-trip through snapshot persistence. +- `implementations/python/tests/test_sem_218_realization_designation.py` — + typed scenario/scoped designation parsing, both cascade override directions, + explicit-leaf precedence, delegation-versus-omission preservation, + namespace isolation, canonical-pointer rejection, open-support planning, + fine-envelope subsumption, governing-scope disclosure, authenticated + persistence delivery, and schema/model differential coverage. ## Non-Goals This spec does not: -- introduce new SDL syntax for marking a field as exact, constrained, or - open — the classes are derived from existing closed Pydantic SDL - models, controlled vocabularies, semantic profiles, and the apparatus - manifest, not from a new keyword; +- add realization fields to individual SDL declarations — explicit leaves are + still classified from their typed fields, while inherited defaults use the + single scenario-root `realization` designation surface; - enumerate the full set of governed exact-requirement-kinds — that expansion belongs to the controlled-vocabulary and reference-model authorities; diff --git a/specs/formal/runtime-contracts/README.md b/specs/formal/runtime-contracts/README.md index eb21c98a6..a6826296d 100644 --- a/specs/formal/runtime-contracts/README.md +++ b/specs/formal/runtime-contracts/README.md @@ -90,7 +90,7 @@ The participant behavior evidence set is ## Implementation Mapping - shared result constraints: `implementations/python/packages/aces_processor/semantics/workflow.py` -- typed result models: `implementations/python/packages/aces_processor/models.py` +- typed result models: `implementations/python/packages/aces_processor/models/` - manager contract validation: `implementations/python/packages/aces_processor/manager.py` - backend example: `implementations/python/packages/aces_backend_stubs/stubs.py` - participant capability contract model: diff --git a/specs/formal/sdl-phases/README.md b/specs/formal/sdl-phases/README.md index aa89a63c2..53564e687 100644 --- a/specs/formal/sdl-phases/README.md +++ b/specs/formal/sdl-phases/README.md @@ -162,7 +162,7 @@ equivalence or bisimilarity. - transition and admission functions: `implementations/python/packages/aces_sdl/instantiate.py` - typed processor lowering: - `implementations/python/packages/aces_processor/models.py` and + `implementations/python/packages/aces_processor/models/` and `implementations/python/packages/aces_processor/planner.py` - canonical snapshot: `implementations/python/packages/aces_sdl/canonical.py` diff --git a/specs/formal/workflows/README.md b/specs/formal/workflows/README.md index c96f715a3..4a9f29bb5 100644 --- a/specs/formal/workflows/README.md +++ b/specs/formal/workflows/README.md @@ -29,7 +29,7 @@ This directory holds the repo-native formal artifacts for SDL workflow control s - validator enforcement: `implementations/python/packages/aces_sdl/validator/` - compiled contracts: `implementations/python/packages/aces_processor/compiler.py` - typed runtime results and contract checks: - - `implementations/python/packages/aces_processor/models.py` + - `implementations/python/packages/aces_processor/models/` - `implementations/python/packages/aces_processor/manager.py` - smoke/regression coverage: - `implementations/python/tests/test_sdl_validator.py` diff --git a/specs/formal/workflows/compensation.md b/specs/formal/workflows/compensation.md index f158237bc..79460faa3 100644 --- a/specs/formal/workflows/compensation.md +++ b/specs/formal/workflows/compensation.md @@ -100,6 +100,6 @@ workflow can be `cancelled` or `timed_out` while compensation is `running` or - SDL authoring models: `implementations/python/packages/aces_sdl/orchestration.py` - semantic validation: `implementations/python/packages/aces_sdl/validator/` - compiled contracts: `implementations/python/packages/aces_processor/compiler.py` -- runtime state/history models: `implementations/python/packages/aces_processor/models.py` +- runtime state/history models: `implementations/python/packages/aces_processor/models/` - manager validation: `implementations/python/packages/aces_processor/manager.py` - control-plane lifecycle handling: `implementations/python/packages/aces_processor/control_plane.py` diff --git a/specs/formal/workflows/state-machine.md b/specs/formal/workflows/state-machine.md index 8ca8f73e7..8b21640be 100644 --- a/specs/formal/workflows/state-machine.md +++ b/specs/formal/workflows/state-machine.md @@ -70,5 +70,5 @@ derived from that envelope after validation. - validation and visibility checks: `implementations/python/packages/aces_sdl/validator/` - compiled step contracts: `implementations/python/packages/aces_processor/compiler.py` - typed runtime envelopes and result validation: - - `implementations/python/packages/aces_processor/models.py` + - `implementations/python/packages/aces_processor/models/` - `implementations/python/packages/aces_processor/manager.py` diff --git a/specs/sdl/runtime-inventory.md b/specs/sdl/runtime-inventory.md index e6a495bb9..ec961c738 100644 --- a/specs/sdl/runtime-inventory.md +++ b/specs/sdl/runtime-inventory.md @@ -23,6 +23,13 @@ sequence. nested runtime-family reference form ([references.md §1](references.md)): `nodes..runtime..[..…]`. +4. `Node.services[]` is adjacent authored identity, not runtime inventory. Each + entry identifies a node-local transport binding by port, protocol, and + optional name; it does not authorize traffic, prove a live listener, publish + a host port, or classify an audience. Authorization remains in + `infrastructure.*.acls`, observed bind state in + `runtime.service_listeners`, and host publication in + `runtime.network.published_ports`. ## 2. Family index diff --git a/specs/sdl/scientific-scenario-completeness.md b/specs/sdl/scientific-scenario-completeness.md index 450da0891..3cd0a9cf5 100644 --- a/specs/sdl/scientific-scenario-completeness.md +++ b/specs/sdl/scientific-scenario-completeness.md @@ -72,9 +72,9 @@ The following block is checked mechanically against the normative artifacts. | --- | --- | --- | | `valid-sdl-fragment` | yes | none | | `deployable-scenario-intent` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `credential-intent`, `scoped-specificity-open-world-intent` | -| `participant-evaluation-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `flexible-step-tooling`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | -| `controlled-experiment-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `host-architecture-constraints`, `pacing-and-synchronization`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | -| `reproducible-benchmark-study-input` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `behavioral-relation-taxonomy`, `clock-declaration`, `credential-intent`, `credential-materialization`, `deadlines-and-windows`, `flexible-step-tooling`, `hidden-benchmark-assets`, `host-architecture-constraints`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `reference-trajectories`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication`, `weakness-exploitability-semantics` | +| `participant-evaluation-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `flexible-step-tooling`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | +| `controlled-experiment-scenario` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `clock-declaration`, `credential-intent`, `deadlines-and-windows`, `host-architecture-constraints`, `pacing-and-synchronization`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication` | +| `reproducible-benchmark-study-input` | no | `authored-observed-state-separation`, `backend-teardown-reconciliation`, `clock-declaration`, `credential-intent`, `credential-materialization`, `deadlines-and-windows`, `flexible-step-tooling`, `hidden-benchmark-assets`, `host-architecture-constraints`, `pacing-and-synchronization`, `participant-action-observation`, `participant-budgets`, `participant-episode-reset`, `portable-behavior-contracts`, `reference-trajectories`, `scoped-specificity-open-world-intent`, `temporal-ordering-causality`, `time-domain-declaration`, `verifier-and-adjudication`, `weakness-exploitability-semantics` | Only a computed-complete profile may cite a minimal example as completeness @@ -101,10 +101,12 @@ Profile validity is also distinct from: - trace equivalence, refinement, observational equivalence, Park-Milner bisimulation, epistemic equivalence, or multi-agent strategic equivalence. -Issue #747 owns the behavioral-relation taxonomy. Until that work is delivered, -REV1 records the concern as missing and stronger profiles that require it remain -incomplete. Passing the same bounded probes or producing equal result tuples is -not evidence of bisimulation. +The revisioned behavioral-relation taxonomy is defined by +`aces-behavioral-relations@rev1`. Every profile binds its intended claim to that +catalog and lists relations it does not claim. This implements the taxonomy +concern without promoting any currently incomplete stronger profile: passing +the same bounded probes or producing equal result tuples is not evidence of +bisimulation. ## Security And Evidence Boundary diff --git a/specs/sdl/sections.md b/specs/sdl/sections.md index d33776ed9..497350d23 100644 --- a/specs/sdl/sections.md +++ b/specs/sdl/sections.md @@ -39,6 +39,7 @@ instructions do not survive as executable scenario meaning. "References" is | `description` | metadata | scalar | normalized, expanded, instantiated | optional; default empty string | none | none | [document model](document-model.md) | | `module` | composition | mapping | normalized, expanded-empty, instantiated-empty | optional; default null | `module.id` | none | [ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md) | | `imports` | composition | list | normalized, expanded-empty, instantiated-empty | optional; default empty list | `namespace` | none | [ADR-053](../../docs/decisions/adrs/adr-053-sdl-module-composition-for-inventory-backed-scenarios.md) | +| `realization` | composition | mapping | normalized | optional; default null | none | none | [explicitness and realization](../formal/realization/explicitness-and-realization.md) | | `nodes` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [nodes and runtime inventory](runtime-inventory.md) | | `infrastructure` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | | `features` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [document model](document-model.md) | @@ -65,13 +66,20 @@ instructions do not survive as executable scenario meaning. "References" is | `workflows` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | catalogued | [workflow semantics](workflow-semantics.md) | | `variables` | section | map | normalized, expanded, instantiated | optional; default empty map | `map_key` | none | [variables and instantiation](variables-and-instantiation.md) | - + The section set therefore has two authoring shapes: maps keyed by stable user-defined identifiers and the scenario-level `forwarding_agents` list, whose elements carry their own stable identity. The checked summary above is derived from the rows; changing a row without reconciling it fails the contract gate. +`realization` is the scenario-root, authoring-only designation table. Its +`default` is `closed`, `open`, or `unspecified`; optional `scopes` override that +posture at canonical namespace/pointer identities. Expansion and instantiation +remove the block from executable `ScenarioContent` and carry typed designation +records in phase provenance so compilation can resolve the cascade without +turning authoring machinery into runtime scenario content. + `forwarding_agents` is the **scenario-level** forwarding-agent inventory. It is distinct from the node-scoped `forwarding_agents` runtime-family collection that lives under `nodes..runtime` ([runtime-inventory.md](runtime-inventory.md)); diff --git a/tools/check_behavioral_relation_claims.py b/tools/check_behavioral_relation_claims.py new file mode 100644 index 000000000..99c38f452 --- /dev/null +++ b/tools/check_behavioral_relation_claims.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# ruff: noqa: E402 +"""Validate behavioral-relation bindings and unstructured positive claims. + +The gate is semantic rather than a keyword ban. Relation terminology is +allowed in definitions, explicit nonclaims, and revisioned claim bindings. A +positive claim must identify the governed relation and state its evidence +boundary; finite evidence therefore cannot silently become an equivalence, +simulation, refinement, or bisimulation claim. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from aces_contracts.behavioral_relations import ( + BehavioralRelationCatalogModel, + validate_behavioral_claim_binding, +) # noqa: E402 +from aces_contracts.contracts import BehavioralClaimBindingModel # noqa: E402 +from pydantic import ValidationError # noqa: E402 + +from tools.policy.common import ( + PolicyFailure, + apply_exceptions, + failures_to_json, + load_exceptions, +) # noqa: E402 + +CATALOG_RELATIVE_PATH = "contracts/concept-authority/behavioral-relations-v1.json" +RULE_CATALOG_INVALID = "behavioral-relation-catalog-invalid" +RULE_BINDING_INVALID = "behavioral-relation-binding-invalid" +RULE_UNBOUND_CLAIM = "behavioral-relation-unbound-positive-claim" + +_CLAIM_BINDING_KEYS = frozenset({"taxonomy_id", "taxonomy_revision", "relation_id"}) +_TEXT_SUFFIXES = frozenset({".json", ".md", ".py", ".toml", ".yaml", ".yml"}) +_SCAN_ROOTS = ( + "docs", + "specs", + "examples", + "contracts/profiles", + "contracts/fixtures", + "implementations/python/packages", +) +_EXCLUDED_PREFIXES = ( + "contracts/schemas/", + "docs/_build/", + "implementations/python/tests/", + "tools/real-daemon/evidence/", +) + +_CLAIM_PATTERNS: tuple[tuple[re.Pattern[str], frozenset[str]], ...] = ( + ( + re.compile(r"\bbehavior(?:al(?:ly)?)?[- ]history[- ]equivalent\b", re.IGNORECASE), + frozenset({"participant-projected-history-equivalence"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|remain|remains|establish|establishes|prove|proves|" + r"guarantee|guarantees|claim|claims)\b(?:\s+[a-z-]+){0,5}\s+" + r"(?:behaviorally equivalent|behavioral equivalence)\b", + re.IGNORECASE, + ), + frozenset( + { + "trace-equivalence", + "strong-bisimulation", + "weak-bisimulation", + "participant-projected-history-equivalence", + "probabilistic-bisimulation", + } + ), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+trace equivalen(?:ce|t)\b", + re.IGNORECASE, + ), + frozenset({"trace-equivalence"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+strong (?:bisimulation|bisimilar)\b", + re.IGNORECASE, + ), + frozenset({"strong-bisimulation"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+weak (?:bisimulation|bisimilar)\b", + re.IGNORECASE, + ), + frozenset({"weak-bisimulation"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+probabilistic (?:bisimulation|bisimilar)\b", + re.IGNORECASE, + ), + frozenset({"probabilistic-bisimulation"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+(?:bisimulation|bisimilar(?:ity)?)\b", + re.IGNORECASE, + ), + frozenset({"strong-bisimulation", "weak-bisimulation", "probabilistic-bisimulation"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+strategic(?:ally)? equivalen(?:ce|t)\b", + re.IGNORECASE, + ), + frozenset({"alternating-strategic-equivalence"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+epistemic(?:ally)? indistinguish(?:ability|able)\b", + re.IGNORECASE, + ), + frozenset({"epistemic-indistinguishability"}), + ), + ( + re.compile( + r"\b(?:are|is|was|were|establish|establishes|prove|proves|claim|claims)\b" + r"(?:\s+[a-z-]+){0,5}\s+statistical(?:ly)? equivalen(?:ce|t)\b", + re.IGNORECASE, + ), + frozenset({"statistical-equivalence"}), + ), + ( + re.compile(r"\bimplementation refines? (?:this|the) (?:design|model|specification)\b", re.IGNORECASE), + frozenset({"trace-inclusion", "forward-simulation", "backward-simulation", "data-refinement"}), + ), +) + +_NONCLAIM_RE = re.compile( + r"\b(?:does not|do not|is not|are not|was not|were not|neither|cannot|can not|must not|" + r"not evidence|not establish|not imply|" + r"not\s+(?:a|an)\s+[^.]{0,80}claim|no [^.]{0,80}claim|deliberately unproved|" + r"remains? unproved|future work)\b", + re.IGNORECASE, +) +_BOUNDARY_RE = re.compile( + r"\b(?:evidence boundary|bounded by|bounded to|only (?:the|these|named|under)|" + r"finite (?:case|probe|trace|observation)|quantifier.scope|evidence.scope|" + r"observation projection|projection revision|proof obligation)\b", + re.IGNORECASE, +) + + +def _iter_objects(value: Any) -> Iterator[dict[str, Any]]: + if isinstance(value, dict): + yield value + for child in value.values(): + yield from _iter_objects(child) + elif isinstance(value, list): + for child in value: + yield from _iter_objects(child) + + +def _validate_structured_bindings( + payload: Any, + catalog: BehavioralRelationCatalogModel, + path: str, +) -> list[PolicyFailure]: + failures: list[PolicyFailure] = [] + for candidate in _iter_objects(payload): + if not _CLAIM_BINDING_KEYS.issubset(candidate): + continue + try: + binding = BehavioralClaimBindingModel.model_validate(candidate) + validate_behavioral_claim_binding(binding, catalog) + except (ValidationError, ValueError) as exc: + failures.append( + PolicyFailure( + RULE_BINDING_INVALID, + f"behavioral claim binding is invalid: {exc}", + path, + ) + ) + return failures + + +def _sentence_around(text: str, start: int, end: int) -> str: + left = max(text.rfind(".", 0, start), text.rfind("\n\n", 0, start), start - 500) + right_candidates = [position for position in (text.find(".", end), text.find("\n\n", end)) if position >= 0] + right = min(right_candidates) + 1 if right_candidates else min(len(text), end + 500) + return text[left + 1 : right] + + +def _window_around(text: str, start: int, end: int) -> str: + return text[max(0, start - 700) : min(len(text), end + 700)] + + +def _relation_id_present(window: str, relation_ids: frozenset[str]) -> bool: + return any( + re.search(rf"(? list[PolicyFailure]: + failures: list[PolicyFailure] = [] + seen: set[tuple[int, str]] = set() + for pattern, expected_relation_ids in _CLAIM_PATTERNS: + for match in pattern.finditer(text): + key = (match.start(), match.group(0).lower()) + if key in seen: + continue + seen.add(key) + sentence = _sentence_around(text, match.start(), match.end()) + if _NONCLAIM_RE.search(sentence): + continue + window = _window_around(text, match.start(), match.end()) + if _relation_id_present(window, expected_relation_ids) and _BOUNDARY_RE.search(window): + continue + expected = ", ".join(sorted(expected_relation_ids)) + failures.append( + PolicyFailure( + RULE_UNBOUND_CLAIM, + f"positive phrase {match.group(0)!r} must bind relation identity ({expected}) and an " + "evidence boundary, or be stated as an explicit weaker nonclaim", + path, + ) + ) + return failures + + +def _iter_scan_paths(repo_root: Path) -> Iterator[Path]: + root_readme = repo_root / "README.md" + if root_readme.is_file(): + yield root_readme + for relative_root in _SCAN_ROOTS: + scan_root = repo_root / relative_root + if not scan_root.is_dir(): + continue + for path in sorted(scan_root.rglob("*")): + if not path.is_file() or path.suffix.lower() not in _TEXT_SUFFIXES: + continue + relative = path.relative_to(repo_root).as_posix() + is_preflight_record = relative.startswith("docs/decisions/issue-") and relative.endswith("-preflight.md") + if ( + relative == CATALOG_RELATIVE_PATH + or is_preflight_record + or any(relative.startswith(prefix) for prefix in _EXCLUDED_PREFIXES) + ): + continue + yield path + + +def evaluate(repo_root: Path) -> list[PolicyFailure]: + catalog_path = repo_root / CATALOG_RELATIVE_PATH + try: + catalog = BehavioralRelationCatalogModel.model_validate_json(catalog_path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, ValidationError, ValueError) as exc: + return [PolicyFailure(RULE_CATALOG_INVALID, f"cannot load governed catalog: {exc}", CATALOG_RELATIVE_PATH)] + + failures: list[PolicyFailure] = [] + for path in _iter_scan_paths(repo_root): + relative = path.relative_to(repo_root).as_posix() + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as exc: + failures.append(PolicyFailure(RULE_UNBOUND_CLAIM, f"cannot inspect claim surface: {exc}", relative)) + continue + if path.suffix.lower() == ".json": + try: + payload = json.loads(text) + except json.JSONDecodeError: + payload = None + if payload is not None: + failures.extend(_validate_structured_bindings(payload, catalog, relative)) + failures.extend(_validate_claim_text(text, relative)) + return failures + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="Emit failures as JSON.") + args = parser.parse_args() + + failures = apply_exceptions(evaluate(REPO_ROOT), load_exceptions(REPO_ROOT)) + if args.json: + print(failures_to_json(failures)) + else: + for failure in failures: + print(failure.render()) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_related_work_comparison.py b/tools/check_related_work_comparison.py new file mode 100644 index 000000000..5e8851a1b --- /dev/null +++ b/tools/check_related_work_comparison.py @@ -0,0 +1,1780 @@ +#!/usr/bin/env python3 +"""Validate the frozen, evidence-backed related-work comparison bundle.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from collections.abc import Mapping, Sequence +from pathlib import Path +from urllib.parse import parse_qsl, urlsplit + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from tools.policy.common import PolicyFailure, safe_repo_path # noqa: E402 + +PROTOCOL_PATH = "docs/research/related-work-comparison/protocol-v1.json" +SNAPSHOT_PATH = "docs/research/related-work-comparison/extraction-snapshot-2026-07-13.json" +ANALYSIS_PATH = "docs/research/related-work-comparison/analysis-v1.json" +PUBLICATION_PATH = "docs/explain/sdl/related-work-comparison.md" +PUBLICATION_START = "" +PUBLICATION_END = "" + +EXPECTED_AXIS_IDS = { + "expressive-breadth", + "semantic-precision", + "formal-analyzability", + "concrete-syntax-soundness", + "composition-versioning", + "experiment-design", + "participant-modeling", + "provenance-evidence", + "interoperability", + "usability", + "implementation-maturity", + "governance-community", +} + +_PROTOCOL_KEYS = { + "protocol_id", + "revision", + "registered_at", + "title", + "purpose", + "inclusion_criteria", + "exclusion_criteria", + "scope_strata", + "systems", + "axes", + "cases", + "analysis_rules", + "amendment_log", +} +_STRATUM_KEYS = {"stratum_id", "label", "description"} +_SYSTEM_KEYS = { + "system_id", + "name", + "version_label", + "scope_stratum", + "inclusion_rationale", + "exclusion_rationale", +} +_AXIS_KEYS = {"axis_id", "label", "construct", "direction", "rubric"} +_CASE_KEYS = { + "case_id", + "kind", + "title", + "authored_requirement", + "unit_of_observation", + "applicability_rule", + "permitted_assistance", + "inputs", + "expected_artifact", + "criteria", +} +_CASE_CRITERIA_KEYS = {"success", "partial", "failure"} +_ANALYSIS_RULE_KEYS = { + "primary_result", + "pareto_scope", + "missing_handling", + "weighting_policy", + "reversal_policy", + "quality_claim_policy", + "claim_evidence_policy", +} +_SNAPSHOT_KEYS = { + "snapshot_id", + "protocol_revision", + "frozen_at", + "assessor", + "review_status", + "limitations", + "sources", + "observations", + "task_observations", +} +_SOURCE_KEYS = { + "source_id", + "system_id", + "kind", + "title", + "locator", + "version", + "revision", + "retrieved_at", + "content_sha256", + "artifact_path", + "primary", + "evidence_class", + "license_note", +} +_OBSERVATION_KEYS = { + "system_id", + "axis_id", + "applicability", + "score", + "measure", + "extraction_method", + "rationale", + "evidence_refs", + "confidence", + "limitations", + "review_status", +} +_TASK_OBSERVATION_KEYS = { + "system_id", + "case_id", + "applicability", + "outcome", + "method", + "rationale", + "evidence_refs", + "limitations", +} +_EVIDENCE_REF_KEYS = {"source_id", "locator"} +_ANALYSIS_KEYS = { + "analysis_id", + "protocol_revision", + "snapshot_id", + "generated_at", + "score_method", + "pareto_groups", + "weight_profiles", + "sensitivity", + "claims", +} +_PARETO_KEYS = { + "group_id", + "scope_stratum", + "system_ids", + "axis_ids", + "frontier_system_ids", +} +_WEIGHT_PROFILE_KEYS = {"profile_id", "scope_stratum", "weights", "totals", "ranking"} +_SENSITIVITY_KEYS = { + "profile_ids", + "ranking_reversal_observed", + "winning_systems", + "disclosure", +} +_CLAIM_KEYS = { + "claim_id", + "kind", + "statement", + "system_ids", + "axis_ids", + "evidence_status", + "threats_to_validity", + "falsification", + "derivation", +} +_FALSIFICATION_KEYS = { + "protocol", + "objective_pass_criteria", + "objective_fail_criteria", + "allowed_evidence_classes", + "disallowed_evidence_classes", + "evidence_artifact_refs", +} +_CLAIM_DERIVATION_KEYS = { + "no-overall-winner": { + "kind", + "scope_strata", + "profile_winners", + "ranking_reversal_observed", + }, + "scope-qualified-breadth": { + "kind", + "scope_stratum", + "axis_id", + "system_scores", + "max_score", + "leading_system_ids", + }, + "maturity-governance": {"kind", "comparisons"}, + "sensitivity": {"kind", "profile_winners", "ranking_reversal_observed"}, +} +_CLAIM_EVIDENCE_STATUSES = {"untested", "partial", "demonstrated", "refuted"} + +_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_GIT_REVISION_RE = re.compile(r"^[0-9a-f]{40}$") +_SENSITIVE_QUERY_KEYS = { + "access_token", + "api_key", + "apikey", + "auth", + "authorization", + "client_secret", + "key", + "password", + "secret", + "sig", + "signature", + "token", +} +_MAX_FILE_BYTES = 2 * 1024 * 1024 +_MAX_SYSTEMS = 32 +_MAX_AXES = 32 +_MAX_CASES = 64 +_MAX_SOURCES = 512 +_MAX_OBSERVATIONS = 4096 +_MEASURE_BY_SCORE = {0: "absent", 1: "limited", 2: "substantial", 3: "strong"} + + +class _DuplicateKeyError(ValueError): + pass + + +def _failure(rule_id: str, message: str, path: str | None = None) -> PolicyFailure: + return PolicyFailure(rule_id, message, path) + + +def _object_without_duplicates(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise _DuplicateKeyError(f"duplicate JSON key {key!r}") + result[key] = value + return result + + +def _load_json(repo_root: Path, rel_path: str) -> dict[str, object]: + path = safe_repo_path(repo_root, rel_path) + if path is None or not path.is_file(): + raise ValueError(f"missing or unsafe repository artifact {rel_path!r}") + if path.stat().st_size > _MAX_FILE_BYTES: + raise ValueError(f"{rel_path!r} exceeds the {_MAX_FILE_BYTES}-byte limit") + payload = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=_object_without_duplicates) + if not isinstance(payload, dict): + raise ValueError(f"{rel_path!r} must contain a JSON object") + return payload + + +def load_bundle( + repo_root: Path = REPO_ROOT, +) -> tuple[dict[str, object], dict[str, object], dict[str, object]]: + """Load the three frozen bundle artifacts with strict duplicate-key handling.""" + + return ( + _load_json(repo_root, PROTOCOL_PATH), + _load_json(repo_root, SNAPSHOT_PATH), + _load_json(repo_root, ANALYSIS_PATH), + ) + + +def _exact_keys( + value: object, + expected: set[str], + failures: list[PolicyFailure], + *, + rule_id: str, + label: str, + path: str, +) -> bool: + if not isinstance(value, dict): + failures.append(_failure(rule_id, f"{label} must be an object", path)) + return False + actual = set(value) + if actual != expected: + failures.append( + _failure( + rule_id, + f"{label} fields must exactly match {sorted(expected)}; got {sorted(actual)}", + path, + ) + ) + return False + return True + + +def _bounded_list( + value: object, + limit: int, + failures: list[PolicyFailure], + *, + rule_id: str, + label: str, + path: str, +) -> list[object]: + if not isinstance(value, list): + failures.append(_failure(rule_id, f"{label} must be a list", path)) + return [] + if len(value) > limit: + failures.append(_failure(rule_id, f"{label} exceeds the {limit}-entry limit", path)) + return [] + return value + + +def _valid_id(value: object) -> bool: + return isinstance(value, str) and bool(_ID_RE.fullmatch(value)) + + +def _record_ids( + records: Sequence[object], + field: str, + failures: list[PolicyFailure], + *, + rule_id: str, + label: str, + path: str, +) -> set[str]: + ids: set[str] = set() + duplicates: set[str] = set() + for record in records: + if not isinstance(record, dict): + continue + value = record.get(field) + if not _valid_id(value): + failures.append(_failure(rule_id, f"{label} has invalid {field} {value!r}", path)) + continue + if value in ids: + duplicates.add(value) + ids.add(value) + if duplicates: + failures.append(_failure(rule_id, f"duplicate {label} ids: {sorted(duplicates)}", path)) + return ids + + +def _validate_locator(locator: object, failures: list[PolicyFailure], source_id: object) -> None: + if not isinstance(locator, str): + failures.append( + _failure( + "related-work-source-locator", + f"{source_id}: locator must be a string", + SNAPSHOT_PATH, + ) + ) + return + parsed = urlsplit(locator) + if parsed.scheme != "https" or not parsed.netloc: + failures.append( + _failure( + "related-work-source-locator", + f"{source_id}: locator must be an absolute HTTPS URL", + SNAPSHOT_PATH, + ) + ) + if parsed.username is not None or parsed.password is not None: + failures.append( + _failure( + "related-work-source-locator-secret", + f"{source_id}: locator must not contain URI userinfo", + SNAPSHOT_PATH, + ) + ) + query_keys = {key.casefold() for key, _ in parse_qsl(parsed.query, keep_blank_values=True)} + sensitive = sorted(query_keys & _SENSITIVE_QUERY_KEYS) + if sensitive: + failures.append( + _failure( + "related-work-source-locator-secret", + f"{source_id}: locator contains secret-bearing query keys {sensitive}", + SNAPSHOT_PATH, + ) + ) + + +def _safe_external_artifact_path(value: object) -> bool: + if not isinstance(value, str) or not value: + return False + path = Path(value) + return not path.is_absolute() and ".." not in path.parts + + +def _bounded_text(value: object, *, maximum: int = 4000) -> bool: + return isinstance(value, str) and bool(value.strip()) and len(value) <= maximum + + +def _bounded_text_list(value: object, *, maximum_items: int = 16) -> bool: + return isinstance(value, list) and 0 < len(value) <= maximum_items and all(_bounded_text(item) for item in value) + + +def _validate_sources( + repo_root: Path, + sources: Sequence[object], + system_ids: set[str], + failures: list[PolicyFailure], + *, + validate_paths: bool, +) -> dict[str, Mapping[str, object]]: + source_by_id: dict[str, Mapping[str, object]] = {} + allowed_kinds = { + "repository-internal", + "git", + "standard", + "publication", + "official-doc", + } + allowed_evidence_classes = { + "executable", + "normative", + "documentation", + "publication", + "source-code", + } + for index, raw_source in enumerate(sources): + label = f"sources[{index}]" + if not _exact_keys( + raw_source, + _SOURCE_KEYS, + failures, + rule_id="related-work-source-shape", + label=label, + path=SNAPSHOT_PATH, + ): + continue + source = raw_source + source_id = source["source_id"] + if not _valid_id(source_id): + failures.append( + _failure( + "related-work-source-id", + f"invalid source id {source_id!r}", + SNAPSHOT_PATH, + ) + ) + continue + if source_id in source_by_id: + failures.append( + _failure( + "related-work-source-id", + f"duplicate source id {source_id!r}", + SNAPSHOT_PATH, + ) + ) + continue + source_by_id[source_id] = source + if source["system_id"] not in system_ids: + failures.append( + _failure( + "related-work-source-system", + f"{source_id}: unknown system {source['system_id']!r}", + SNAPSHOT_PATH, + ) + ) + if source["kind"] not in allowed_kinds: + failures.append( + _failure( + "related-work-source-kind", + f"{source_id}: invalid source kind", + SNAPSHOT_PATH, + ) + ) + if source["evidence_class"] not in allowed_evidence_classes: + failures.append( + _failure( + "related-work-source-kind", + f"{source_id}: invalid evidence class", + SNAPSHOT_PATH, + ) + ) + if source["primary"] is not True: + failures.append( + _failure( + "related-work-source-primary", + f"{source_id}: comparison evidence must be primary", + SNAPSHOT_PATH, + ) + ) + _validate_locator(source["locator"], failures, source_id) + revision = source["revision"] + if source["kind"] in {"git", "repository-internal"} and ( + not isinstance(revision, str) or not _GIT_REVISION_RE.fullmatch(revision) + ): + failures.append( + _failure( + "related-work-source-pin", + f"{source_id}: Git evidence requires a full 40-hex commit", + SNAPSHOT_PATH, + ) + ) + if source["kind"] in {"standard", "publication"} and ( + not isinstance(revision, str) + or not revision.strip() + or revision.casefold() in {"current", "head", "latest", "main", "master"} + ): + failures.append( + _failure( + "related-work-source-pin", + f"{source_id}: standard/publication evidence requires an exact immutable revision", + SNAPSHOT_PATH, + ) + ) + digest = source["content_sha256"] + if digest is not None and (not isinstance(digest, str) or not _SHA256_RE.fullmatch(digest)): + failures.append( + _failure( + "related-work-source-pin", + f"{source_id}: invalid SHA-256 digest", + SNAPSHOT_PATH, + ) + ) + if source["kind"] == "official-doc" and (not isinstance(source["retrieved_at"], str) or digest is None): + failures.append( + _failure( + "related-work-source-pin", + f"{source_id}: mutable official documentation requires retrieval date and digest", + SNAPSHOT_PATH, + ) + ) + artifact_path = source["artifact_path"] + if source["kind"] in {"git", "repository-internal"}: + if not _safe_external_artifact_path(artifact_path): + failures.append( + _failure( + "related-work-source-path", + f"{source_id}: Git artifact path is missing or unsafe", + SNAPSHOT_PATH, + ) + ) + elif source["kind"] == "repository-internal" and validate_paths: + resolved = safe_repo_path(repo_root, artifact_path) + if resolved is None or not resolved.exists(): + failures.append( + _failure( + "related-work-source-path", + f"{source_id}: internal artifact path does not exist", + str(artifact_path), + ) + ) + elif artifact_path is not None: + failures.append( + _failure( + "related-work-source-path", + f"{source_id}: non-Git source must not claim a repository artifact path", + SNAPSHOT_PATH, + ) + ) + return source_by_id + + +def _validate_evidence_refs( + refs: object, + *, + system_id: object, + source_by_id: Mapping[str, Mapping[str, object]], + failures: list[PolicyFailure], + rule_id: str, + label: str, +) -> None: + if not isinstance(refs, list) or not refs: + failures.append( + _failure( + rule_id, + f"{label}: at least one primary-source reference is required", + SNAPSHOT_PATH, + ) + ) + return + for index, raw_ref in enumerate(refs): + if not _exact_keys( + raw_ref, + _EVIDENCE_REF_KEYS, + failures, + rule_id=rule_id, + label=f"{label}.evidence_refs[{index}]", + path=SNAPSHOT_PATH, + ): + continue + source = source_by_id.get(raw_ref["source_id"]) + if source is None or source.get("primary") is not True: + failures.append( + _failure( + rule_id, + f"{label}: unknown or non-primary source {raw_ref['source_id']!r}", + SNAPSHOT_PATH, + ) + ) + elif source.get("system_id") != system_id: + failures.append( + _failure( + rule_id, + f"{label}: source {raw_ref['source_id']!r} belongs to another system", + SNAPSHOT_PATH, + ) + ) + locator = raw_ref["locator"] + if not isinstance(locator, str) or not locator.strip() or len(locator) > 500: + failures.append( + _failure( + rule_id, + f"{label}: evidence locator must be bounded and exact", + SNAPSHOT_PATH, + ) + ) + + +def _validate_observations( + observations: Sequence[object], + system_ids: set[str], + axis_ids: set[str], + source_by_id: Mapping[str, Mapping[str, object]], + failures: list[PolicyFailure], +) -> dict[tuple[str, str], int | None]: + scores: dict[tuple[str, str], int | None] = {} + executable_classes = {"executable"} + for index, raw_observation in enumerate(observations): + label = f"observations[{index}]" + if not _exact_keys( + raw_observation, + _OBSERVATION_KEYS, + failures, + rule_id="related-work-observation-shape", + label=label, + path=SNAPSHOT_PATH, + ): + continue + observation = raw_observation + system_id = observation["system_id"] + axis_id = observation["axis_id"] + key = (system_id, axis_id) + if system_id not in system_ids or axis_id not in axis_ids: + failures.append( + _failure( + "related-work-observation-reference", + f"{label}: unknown system or axis", + SNAPSHOT_PATH, + ) + ) + continue + if key in scores: + failures.append( + _failure( + "related-work-observations-rectangular", + f"duplicate observation {key}", + SNAPSHOT_PATH, + ) + ) + continue + applicability = observation["applicability"] + score = observation["score"] + measure = observation["measure"] + if applicability == "out-of-scope": + if score is not None or measure != "not-applicable": + failures.append( + _failure( + "related-work-observation-score", + f"{label}: out-of-scope cells require null score and not-applicable measure", + SNAPSHOT_PATH, + ) + ) + elif applicability == "applicable": + if not isinstance(score, int) or score not in _MEASURE_BY_SCORE or measure != _MEASURE_BY_SCORE.get(score): + failures.append( + _failure( + "related-work-observation-score", + f"{label}: applicable cells require a 0..3 score and matching measure", + SNAPSHOT_PATH, + ) + ) + else: + failures.append( + _failure( + "related-work-observation-score", + f"{label}: invalid applicability", + SNAPSHOT_PATH, + ) + ) + scores[key] = score if isinstance(score, int) else None + if not _bounded_text(observation["rationale"]): + failures.append( + _failure( + "related-work-observation-rationale", + f"{label}: a bounded reproducible scoring rationale is required", + SNAPSHOT_PATH, + ) + ) + if not _bounded_text_list(observation["limitations"]): + failures.append( + _failure( + "related-work-observation-limitations", + f"{label}: at least one bounded limitation is required", + SNAPSHOT_PATH, + ) + ) + _validate_evidence_refs( + observation["evidence_refs"], + system_id=system_id, + source_by_id=source_by_id, + failures=failures, + rule_id="related-work-observation-evidence", + label=label, + ) + if system_id == "aces" and axis_id in { + "concrete-syntax-soundness", + "implementation-maturity", + }: + evidence_classes = { + source_by_id[ref["source_id"]]["evidence_class"] + for ref in observation["evidence_refs"] + if isinstance(ref, dict) and ref.get("source_id") in source_by_id + } + if not evidence_classes & executable_classes: + failures.append( + _failure( + "related-work-aces-executable-evidence", + f"{label}: ACES delivery claims require executable evidence", + SNAPSHOT_PATH, + ) + ) + expected = {(system_id, axis_id) for system_id in system_ids for axis_id in axis_ids} + if set(scores) != expected: + missing = sorted(expected - set(scores)) + unexpected = sorted(set(scores) - expected) + failures.append( + _failure( + "related-work-observations-rectangular", + f"observations must cover every system × axis cell; missing={missing[:10]}, unexpected={unexpected[:10]}", + SNAPSHOT_PATH, + ) + ) + return scores + + +def _validate_task_observations( + task_observations: Sequence[object], + system_ids: set[str], + case_ids: set[str], + source_by_id: Mapping[str, Mapping[str, object]], + failures: list[PolicyFailure], +) -> None: + seen: set[tuple[str, str]] = set() + for index, raw_observation in enumerate(task_observations): + label = f"task_observations[{index}]" + if not _exact_keys( + raw_observation, + _TASK_OBSERVATION_KEYS, + failures, + rule_id="related-work-task-observation-shape", + label=label, + path=SNAPSHOT_PATH, + ): + continue + observation = raw_observation + system_id = observation["system_id"] + case_id = observation["case_id"] + key = (system_id, case_id) + if system_id not in system_ids or case_id not in case_ids or key in seen: + failures.append( + _failure( + "related-work-task-observations-rectangular", + f"{label}: unknown or duplicate system/case pair {key}", + SNAPSHOT_PATH, + ) + ) + continue + seen.add(key) + if observation["applicability"] not in {"applicable", "out-of-scope"}: + failures.append( + _failure( + "related-work-task-observation-outcome", + f"{label}: invalid applicability", + SNAPSHOT_PATH, + ) + ) + if observation["outcome"] not in { + "supported", + "partial", + "unsupported", + "not-applicable", + "not-evaluated", + }: + failures.append( + _failure( + "related-work-task-observation-outcome", + f"{label}: invalid outcome", + SNAPSHOT_PATH, + ) + ) + if observation["method"] not in {"repository-execution", "source-walkthrough"}: + failures.append( + _failure( + "related-work-task-observation-outcome", + f"{label}: invalid method", + SNAPSHOT_PATH, + ) + ) + if not _bounded_text(observation["rationale"]): + failures.append( + _failure( + "related-work-task-observation-rationale", + f"{label}: a bounded reproducible case rationale is required", + SNAPSHOT_PATH, + ) + ) + if not _bounded_text_list(observation["limitations"]): + failures.append( + _failure( + "related-work-task-observation-limitations", + f"{label}: at least one bounded limitation is required", + SNAPSHOT_PATH, + ) + ) + _validate_evidence_refs( + observation["evidence_refs"], + system_id=system_id, + source_by_id=source_by_id, + failures=failures, + rule_id="related-work-task-observation-evidence", + label=label, + ) + expected = {(system_id, case_id) for system_id in system_ids for case_id in case_ids} + if seen != expected: + failures.append( + _failure( + "related-work-task-observations-rectangular", + f"task observations must cover every system × case pair; missing={sorted(expected - seen)[:10]}", + SNAPSHOT_PATH, + ) + ) + + +def _pareto_frontier( + system_ids: Sequence[str], + axis_ids: Sequence[str], + scores: Mapping[tuple[str, str], int | None], +) -> list[str]: + frontier: list[str] = [] + for candidate in system_ids: + dominated = False + for challenger in system_ids: + if challenger == candidate: + continue + candidate_scores = [scores.get((candidate, axis_id)) for axis_id in axis_ids] + challenger_scores = [scores.get((challenger, axis_id)) for axis_id in axis_ids] + if any(value is None for value in candidate_scores + challenger_scores): + continue + if all(right >= left for left, right in zip(candidate_scores, challenger_scores, strict=True)) and any( + right > left for left, right in zip(candidate_scores, challenger_scores, strict=True) + ): + dominated = True + break + if not dominated: + frontier.append(candidate) + return sorted(frontier) + + +def _weighted_totals( + system_ids: Sequence[str], + weights: Mapping[str, object], + scores: Mapping[tuple[str, str], int | None], +) -> dict[str, int]: + totals: dict[str, int] = {} + for system_id in system_ids: + total = 0 + for axis_id, raw_weight in weights.items(): + weight = raw_weight if isinstance(raw_weight, int) else 0 + score = scores.get((system_id, axis_id)) + if score is not None: + total += score * weight + totals[system_id] = total + return totals + + +def _ranking(totals: Mapping[str, int]) -> list[str]: + return sorted(totals, key=lambda system_id: (-totals[system_id], system_id)) + + +def _count_word(value: int) -> str: + return { + 0: "zero", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + }.get(value, str(value)) + + +def _expected_claim_derivations( + system_by_id: Mapping[str, Mapping[str, object]], + scores: Mapping[tuple[str, str], int | None], + profile_winner_by_id: Mapping[str, str], +) -> dict[str, dict[str, object]]: + scope_strata = sorted({str(system["scope_stratum"]) for system in system_by_id.values()}) + scenario_system_ids = sorted( + system_id for system_id, system in system_by_id.items() if system["scope_stratum"] == "scenario-authoring" + ) + breadth_scores = {system_id: scores.get((system_id, "expressive-breadth")) for system_id in scenario_system_ids} + numeric_breadth_scores = [score for score in breadth_scores.values() if isinstance(score, int)] + max_breadth_score = max(numeric_breadth_scores) if numeric_breadth_scores else None + breadth_leaders = sorted(system_id for system_id, score in breadth_scores.items() if score == max_breadth_score) + + comparison_specs = [ + ("cacao-v2", "governance-community", "aces"), + ("cyber-fom", "governance-community", "aces"), + ("cyborg", "implementation-maturity", "aces"), + ("ocr-sdl", "implementation-maturity", "aces"), + ] + comparisons = [ + { + "axis_id": axis_id, + "left_system_id": left_system_id, + "left_score": scores.get((left_system_id, axis_id)), + "operator": "greater-than", + "right_system_id": right_system_id, + "right_score": scores.get((right_system_id, axis_id)), + } + for left_system_id, axis_id, right_system_id in comparison_specs + ] + all_profile_winners = dict(sorted(profile_winner_by_id.items())) + selected_profile_winners = { + profile_id: profile_winner_by_id.get(profile_id) + for profile_id in ( + "breadth-and-composition", + "formal-rigor", + "maturity-and-governance", + ) + } + return { + "no-overall-winner": { + "kind": "scope-and-sensitivity", + "scope_strata": scope_strata, + "profile_winners": all_profile_winners, + "ranking_reversal_observed": len(set(all_profile_winners.values())) > 1, + }, + "scope-qualified-breadth": { + "kind": "axis-maximum", + "scope_stratum": "scenario-authoring", + "axis_id": "expressive-breadth", + "system_scores": breadth_scores, + "max_score": max_breadth_score, + "leading_system_ids": breadth_leaders, + }, + "maturity-governance": { + "kind": "cell-comparisons", + "comparisons": comparisons, + }, + "sensitivity": { + "kind": "profile-winners", + "profile_winners": selected_profile_winners, + "ranking_reversal_observed": len(set(selected_profile_winners.values())) > 1, + }, + } + + +def _expected_claim_statement( + kind: str, + derivation: Mapping[str, object], + system_by_id: Mapping[str, Mapping[str, object]], +) -> str: + def system_name(system_id: object) -> str: + system = system_by_id.get(str(system_id), {}) + name = system.get("name") + return name if isinstance(name, str) else str(system_id) + + if kind == "no-overall-winner": + scope_count = len(derivation["scope_strata"]) + profile_count = len(derivation["profile_winners"]) + return ( + f"No overall winner is supported: the {_count_word(scope_count)} declared scope strata are analyzed " + f"separately, and the scenario-authoring first-ranked system changes across the " + f"{_count_word(profile_count)} declared weight profiles." + ) + if kind == "scope-qualified-breadth": + system_scores = derivation["system_scores"] + leaders = derivation["leading_system_ids"] + leader_name = system_name(leaders[0]) if isinstance(leaders, list) and len(leaders) == 1 else "No system" + return ( + f"Within the {_count_word(len(system_scores))}-system scenario-authoring stratum and this frozen " + f"rubric, {leader_name} is the sole expressive-breadth leader at level {derivation['max_score']}. " + "This is the broadest combined surface observed in this corpus; `highest quality` is not supported, " + "and standardization or maturity do not follow from breadth." + ) + if kind == "maturity-governance": + comparisons = { + comparison["left_system_id"]: comparison + for comparison in derivation["comparisons"] + if isinstance(comparison, dict) + } + aces_level = comparisons["cacao-v2"]["right_score"] + return ( + f"Against {system_name('aces')}'s recorded level {aces_level}, {system_name('cacao-v2')} and " + f"{system_name('cyber-fom')} each record level {comparisons['cacao-v2']['left_score']} for governance " + f"and community, while {system_name('cyborg')} records level {comparisons['cyborg']['left_score']} " + f"and {system_name('ocr-sdl')} level {comparisons['ocr-sdl']['left_score']} for implementation " + "maturity. These are axis-specific evidence comparisons, not cross-scope overall rankings or " + "adoption claims." + ) + if kind == "sensitivity": + winners = derivation["profile_winners"] + return ( + "Across the declared scenario-authoring profiles, breadth and composition ranks " + f"{system_name(winners['breadth-and-composition'])} first, formal rigor ranks " + f"{system_name(winners['formal-rigor'])} first, and maturity and governance ranks " + f"{system_name(winners['maturity-and-governance'])} first; this observed reversal prohibits a " + "weight-independent winner claim." + ) + return "" + + +def _validate_claim_evidence_gate( + protocol: Mapping[str, object], + snapshot: Mapping[str, object], + analysis: Mapping[str, object], + raw_claim: Mapping[str, object], + label: str, + failures: list[PolicyFailure], +) -> None: + issues: list[str] = [] + status = raw_claim["evidence_status"] + if not isinstance(status, str) or status not in _CLAIM_EVIDENCE_STATUSES: + issues.append(f"unknown ADR-021 evidence status {status!r}") + elif status != "partial": + issues.append("corpus-bounded public claims must retain evidence_status='partial'") + if not _bounded_text_list(raw_claim["threats_to_validity"]): + issues.append("threats_to_validity must be a non-empty bounded text list") + + falsification = raw_claim["falsification"] + if not isinstance(falsification, dict) or set(falsification) != _FALSIFICATION_KEYS: + issues.append(f"falsification fields must exactly match {sorted(_FALSIFICATION_KEYS)}") + else: + if not _bounded_text(falsification["protocol"]): + issues.append("falsification protocol must be non-empty bounded text") + for field in ("objective_pass_criteria", "objective_fail_criteria"): + if not _bounded_text_list(falsification[field]): + issues.append(f"{field} must be a non-empty bounded text list") + allowed = falsification["allowed_evidence_classes"] + disallowed = falsification["disallowed_evidence_classes"] + if not _bounded_text_list(allowed) or not _bounded_text_list(disallowed): + issues.append("allowed and disallowed evidence classes must be non-empty bounded text lists") + else: + allowed_set = set(allowed) + disallowed_set = set(disallowed) + source_classes = { + source.get("evidence_class") + for source in snapshot.get("sources", []) + if isinstance(source, dict) and isinstance(source.get("evidence_class"), str) + } + if not source_classes.issubset(allowed_set): + issues.append(f"allowed evidence omits frozen source classes {sorted(source_classes - allowed_set)}") + if allowed_set & disallowed_set: + issues.append("allowed and disallowed evidence classes overlap") + expected_artifacts = [ + protocol.get("revision"), + snapshot.get("snapshot_id"), + analysis.get("analysis_id"), + ] + if falsification["evidence_artifact_refs"] != expected_artifacts: + issues.append(f"named evidence artifacts must exactly match {expected_artifacts}") + if issues: + failures.append( + _failure( + "related-work-claim-evidence-gate", + f"{label}: " + "; ".join(issues), + ANALYSIS_PATH, + ) + ) + + +def _validate_analysis( + protocol: Mapping[str, object], + snapshot: Mapping[str, object], + analysis: Mapping[str, object], + system_by_id: Mapping[str, Mapping[str, object]], + axis_ids: set[str], + scores: Mapping[tuple[str, str], int | None], + failures: list[PolicyFailure], +) -> None: + if not _exact_keys( + analysis, + _ANALYSIS_KEYS, + failures, + rule_id="related-work-analysis-shape", + label="analysis", + path=ANALYSIS_PATH, + ): + return + if analysis["protocol_revision"] != protocol.get("revision") or analysis["snapshot_id"] != snapshot.get( + "snapshot_id" + ): + failures.append( + _failure( + "related-work-analysis-join", + "analysis does not identify the loaded protocol and snapshot", + ANALYSIS_PATH, + ) + ) + pareto_groups = _bounded_list( + analysis["pareto_groups"], + _MAX_SYSTEMS, + failures, + rule_id="related-work-pareto-shape", + label="pareto_groups", + path=ANALYSIS_PATH, + ) + for index, raw_group in enumerate(pareto_groups): + label = f"pareto_groups[{index}]" + if not _exact_keys( + raw_group, + _PARETO_KEYS, + failures, + rule_id="related-work-pareto-shape", + label=label, + path=ANALYSIS_PATH, + ): + continue + system_ids = raw_group["system_ids"] + group_axis_ids = raw_group["axis_ids"] + if not isinstance(system_ids, list) or not isinstance(group_axis_ids, list): + failures.append( + _failure( + "related-work-pareto-shape", + f"{label}: ids must be lists", + ANALYSIS_PATH, + ) + ) + continue + if any(system_id not in system_by_id for system_id in system_ids) or any( + axis_id not in axis_ids for axis_id in group_axis_ids + ): + failures.append( + _failure( + "related-work-pareto-shape", + f"{label}: unknown system or axis", + ANALYSIS_PATH, + ) + ) + continue + if any(system_by_id[system_id]["scope_stratum"] != raw_group["scope_stratum"] for system_id in system_ids): + failures.append( + _failure( + "related-work-pareto-scope", + f"{label}: mixed scope strata", + ANALYSIS_PATH, + ) + ) + if any(scores.get((system_id, axis_id)) is None for system_id in system_ids for axis_id in group_axis_ids): + failures.append( + _failure( + "related-work-pareto-scope", + f"{label}: includes out-of-scope cells", + ANALYSIS_PATH, + ) + ) + continue + expected_frontier = _pareto_frontier(system_ids, group_axis_ids, scores) + if raw_group["frontier_system_ids"] != expected_frontier: + failures.append( + _failure( + "related-work-pareto-drift", + f"{label}: recorded frontier does not match {expected_frontier}", + ANALYSIS_PATH, + ) + ) + + profiles = _bounded_list( + analysis["weight_profiles"], + _MAX_CASES, + failures, + rule_id="related-work-weight-shape", + label="weight_profiles", + path=ANALYSIS_PATH, + ) + profile_ids: list[str] = [] + winners: list[str] = [] + profile_winner_by_id: dict[str, str] = {} + for index, raw_profile in enumerate(profiles): + label = f"weight_profiles[{index}]" + if not _exact_keys( + raw_profile, + _WEIGHT_PROFILE_KEYS, + failures, + rule_id="related-work-weight-shape", + label=label, + path=ANALYSIS_PATH, + ): + continue + profile_id = raw_profile["profile_id"] + profile_ids.append(profile_id) + weights = raw_profile["weights"] + if ( + not isinstance(weights, dict) + or set(weights) != axis_ids + or any(not isinstance(weight, int) or weight < 0 or weight > 10 for weight in weights.values()) + ): + failures.append( + _failure( + "related-work-weight-shape", + f"{label}: weights must cover all axes with bounded non-negative integers", + ANALYSIS_PATH, + ) + ) + continue + if not any(weights.values()): + failures.append( + _failure( + "related-work-weight-shape", + f"{label}: at least one weight is required", + ANALYSIS_PATH, + ) + ) + continue + systems = sorted( + system_id + for system_id, system in system_by_id.items() + if system["scope_stratum"] == raw_profile["scope_stratum"] + ) + expected_totals = _weighted_totals(systems, weights, scores) + expected_ranking = _ranking(expected_totals) + if raw_profile["totals"] != expected_totals or raw_profile["ranking"] != expected_ranking: + failures.append( + _failure( + "related-work-weight-drift", + f"{label}: totals/ranking do not match recomputation", + ANALYSIS_PATH, + ) + ) + if expected_ranking: + winners.append(expected_ranking[0]) + profile_winner_by_id[profile_id] = expected_ranking[0] + + sensitivity = analysis["sensitivity"] + if _exact_keys( + sensitivity, + _SENSITIVITY_KEYS, + failures, + rule_id="related-work-sensitivity-shape", + label="sensitivity", + path=ANALYSIS_PATH, + ): + expected_winners = sorted(set(winners)) + reversal = len(expected_winners) > 1 + if ( + sensitivity["profile_ids"] != profile_ids + or sensitivity["winning_systems"] != expected_winners + or sensitivity["ranking_reversal_observed"] is not reversal + ): + failures.append( + _failure( + "related-work-sensitivity-drift", + "sensitivity record hides or misstates recomputed profile reversals", + ANALYSIS_PATH, + ) + ) + + claims = _bounded_list( + analysis["claims"], + _MAX_CASES, + failures, + rule_id="related-work-claim-shape", + label="claims", + path=ANALYSIS_PATH, + ) + expected_derivations = _expected_claim_derivations(system_by_id, scores, profile_winner_by_id) + claim_kinds: set[str] = set() + claim_ids: set[str] = set() + for index, raw_claim in enumerate(claims): + label = f"claims[{index}]" + if not _exact_keys( + raw_claim, + _CLAIM_KEYS, + failures, + rule_id="related-work-claim-shape", + label=label, + path=ANALYSIS_PATH, + ): + continue + claim_id = raw_claim["claim_id"] + kind = raw_claim["kind"] + if not _valid_id(claim_id) or claim_id in claim_ids: + failures.append( + _failure( + "related-work-claim-shape", + f"{label}: claim_id must be unique and use the closed id grammar", + ANALYSIS_PATH, + ) + ) + else: + claim_ids.add(claim_id) + if not isinstance(kind, str) or kind not in expected_derivations: + failures.append( + _failure( + "related-work-claim-shape", + f"{label}: unknown claim kind {kind!r}", + ANALYSIS_PATH, + ) + ) + continue + claim_kinds.add(kind) + statement = raw_claim["statement"] + if not isinstance(statement, str): + failures.append( + _failure( + "related-work-claim-shape", + f"{label}: statement must be text", + ANALYSIS_PATH, + ) + ) + continue + _validate_claim_evidence_gate(protocol, snapshot, analysis, raw_claim, label, failures) + + raw_derivation = raw_claim["derivation"] + expected_derivation = expected_derivations[kind] + expected_derivation_keys = _CLAIM_DERIVATION_KEYS[kind] + if not isinstance(raw_derivation, dict) or set(raw_derivation) != expected_derivation_keys: + failures.append( + _failure( + "related-work-claim-derivation", + f"{label}: derivation fields must exactly match {sorted(expected_derivation_keys)}", + ANALYSIS_PATH, + ) + ) + elif raw_derivation != expected_derivation: + failures.append( + _failure( + "related-work-claim-derivation", + f"{label}: derivation does not match recomputed observations and profiles", + ANALYSIS_PATH, + ) + ) + + expected_statement = _expected_claim_statement(kind, expected_derivation, system_by_id) + if statement != expected_statement: + failures.append( + _failure( + "related-work-claim-derivation", + f"{label}: statement is not the canonical rendering of its recomputed derivation", + ANALYSIS_PATH, + ) + ) + + expected_system_ids: set[str] + expected_axis_ids: set[str] + if kind == "no-overall-winner": + expected_system_ids = set(system_by_id) + expected_axis_ids = set(axis_ids) + elif kind == "scope-qualified-breadth": + expected_system_ids = set(expected_derivation["system_scores"]) + expected_axis_ids = {str(expected_derivation["axis_id"])} + elif kind == "maturity-governance": + comparisons = expected_derivation["comparisons"] + expected_system_ids = { + str(comparison[field]) for comparison in comparisons for field in ("left_system_id", "right_system_id") + } + expected_axis_ids = {str(comparison["axis_id"]) for comparison in comparisons} + else: + profile_winners = expected_derivation["profile_winners"] + expected_system_ids = {str(system_id) for system_id in profile_winners.values()} + expected_axis_ids = set(axis_ids) + raw_system_ids = raw_claim["system_ids"] + raw_axis_ids = raw_claim["axis_ids"] + valid_raw_system_ids = isinstance(raw_system_ids, list) and all( + isinstance(system_id, str) for system_id in raw_system_ids + ) + valid_raw_axis_ids = isinstance(raw_axis_ids, list) and all( + isinstance(axis_id, str) for axis_id in raw_axis_ids + ) + if ( + not valid_raw_system_ids + or len(raw_system_ids) != len(set(raw_system_ids)) + or set(raw_system_ids) != expected_system_ids + or not valid_raw_axis_ids + or len(raw_axis_ids) != len(set(raw_axis_ids)) + or set(raw_axis_ids) != expected_axis_ids + ): + failures.append( + _failure( + "related-work-claim-derivation", + f"{label}: declared systems and axes do not match the recomputed derivation", + ANALYSIS_PATH, + ) + ) + folded = statement.casefold() + if "highest quality" in folded and "not supported" not in folded: + failures.append( + _failure( + "related-work-claim-overreach", + f"{label}: highest-quality claim is not supported", + ANALYSIS_PATH, + ) + ) + required_claim_kinds = { + "no-overall-winner", + "scope-qualified-breadth", + "maturity-governance", + "sensitivity", + } + if not required_claim_kinds.issubset(claim_kinds): + failures.append( + _failure( + "related-work-claim-coverage", + f"claims must distinguish {sorted(required_claim_kinds)}", + ANALYSIS_PATH, + ) + ) + + +def _display_score(score: int | None) -> str: + return "oos" if score is None else str(score) + + +def render_publication( + protocol: Mapping[str, object], + snapshot: Mapping[str, object], + analysis: Mapping[str, object], +) -> str: + """Render the mechanically checked reader-facing matrix and bounded conclusions.""" + + systems = protocol["systems"] + axes = protocol["axes"] + observations = snapshot["observations"] + score_by_key = {(item["system_id"], item["axis_id"]): item["score"] for item in observations} + system_ids = [system["system_id"] for system in systems] + system_name_by_id = {system["system_id"]: system["name"] for system in systems} + lines = [ + PUBLICATION_START, + f"Frozen snapshot: `{snapshot['snapshot_id']}` under protocol `{protocol['revision']}`.", + "Scores are axis-specific ordinal evidence levels: 0 absent, 1 limited, 2 substantial, 3 strong;", + "`oos` means the axis is outside the system's declared scope and is never treated as zero.", + "", + "| Axis | " + " | ".join(system["name"] for system in systems) + " |", + "| --- | " + " | ".join("---" for _ in systems) + " |", + ] + for axis in axes: + cells = [_display_score(score_by_key.get((system_id, axis["axis_id"]))) for system_id in system_ids] + lines.append(f"| {axis['label']} | " + " | ".join(cells) + " |") + lines.extend(["", "### Evidence-bounded findings", ""]) + for claim in analysis["claims"]: + lines.append( + f"- **{claim['kind'].replace('-', ' ').title()}.** Evidence status: " + f"`{claim['evidence_status']}`. {claim['statement']}" + ) + lines.extend(["", "### Sensitivity of scenario-authoring rankings", ""]) + lines.append("| Weight profile | First-ranked system | Recorded totals |") + lines.append("| --- | --- | --- |") + for profile in analysis["weight_profiles"]: + totals = ", ".join( + f"{system_name_by_id[system_id]}={profile['totals'][system_id]}" for system_id in profile["ranking"] + ) + lines.append(f"| `{profile['profile_id']}` | {system_name_by_id[profile['ranking'][0]]} | {totals} |") + lines.extend(["", "### ACES delivery limits retained in the matrix", ""]) + axis_by_id = {axis["axis_id"]: axis["label"] for axis in axes} + for observation in observations: + if observation["system_id"] != "aces" or observation["score"] == 3: + continue + limitation = observation["limitations"][0] if observation["limitations"] else observation["rationale"] + lines.append( + f"- **{axis_by_id[observation['axis_id']]} ({_display_score(observation['score'])}).** {limitation}" + ) + lines.extend( + [ + "", + "Cell rationales, exact source locators, task walkthroughs, and source digests are in the", + "[frozen extraction snapshot](../../research/related-work-comparison/extraction-snapshot-2026-07-13.json).", + PUBLICATION_END, + ] + ) + return "\n".join(lines) + + +def _validate_publication( + repo_root: Path, + protocol: Mapping[str, object], + snapshot: Mapping[str, object], + analysis: Mapping[str, object], + failures: list[PolicyFailure], +) -> None: + path = safe_repo_path(repo_root, PUBLICATION_PATH) + prose = path.read_text(encoding="utf-8") if path is not None and path.is_file() else "" + expected = render_publication(protocol, snapshot, analysis) + if expected not in prose: + failures.append( + _failure( + "related-work-publication-drift", + "reader-facing matrix and conclusions do not match the frozen bundle", + PUBLICATION_PATH, + ) + ) + + +def validate_bundle( + repo_root: Path, + protocol: dict[str, object], + snapshot: dict[str, object], + analysis: dict[str, object], + *, + validate_paths: bool = True, +) -> list[PolicyFailure]: + """Validate closed shapes, joins, evidence, analysis recomputation, and publication parity.""" + + failures: list[PolicyFailure] = [] + if not _exact_keys( + protocol, + _PROTOCOL_KEYS, + failures, + rule_id="related-work-protocol-shape", + label="protocol", + path=PROTOCOL_PATH, + ): + return failures + strata = _bounded_list( + protocol["scope_strata"], + _MAX_SYSTEMS, + failures, + rule_id="related-work-protocol-shape", + label="scope_strata", + path=PROTOCOL_PATH, + ) + systems = _bounded_list( + protocol["systems"], + _MAX_SYSTEMS, + failures, + rule_id="related-work-protocol-shape", + label="systems", + path=PROTOCOL_PATH, + ) + axes = _bounded_list( + protocol["axes"], + _MAX_AXES, + failures, + rule_id="related-work-protocol-shape", + label="axes", + path=PROTOCOL_PATH, + ) + cases = _bounded_list( + protocol["cases"], + _MAX_CASES, + failures, + rule_id="related-work-protocol-shape", + label="cases", + path=PROTOCOL_PATH, + ) + stratum_ids = _record_ids( + strata, + "stratum_id", + failures, + rule_id="related-work-protocol-id", + label="scope stratum", + path=PROTOCOL_PATH, + ) + system_ids = _record_ids( + systems, + "system_id", + failures, + rule_id="related-work-protocol-id", + label="system", + path=PROTOCOL_PATH, + ) + axis_ids = _record_ids( + axes, + "axis_id", + failures, + rule_id="related-work-protocol-id", + label="axis", + path=PROTOCOL_PATH, + ) + case_ids = _record_ids( + cases, + "case_id", + failures, + rule_id="related-work-protocol-id", + label="case", + path=PROTOCOL_PATH, + ) + if axis_ids != EXPECTED_AXIS_IDS: + failures.append( + _failure( + "related-work-axis-set", + f"protocol axes must exactly match {sorted(EXPECTED_AXIS_IDS)}; got {sorted(axis_ids)}", + PROTOCOL_PATH, + ) + ) + for index, stratum in enumerate(strata): + _exact_keys( + stratum, + _STRATUM_KEYS, + failures, + rule_id="related-work-protocol-shape", + label=f"scope_strata[{index}]", + path=PROTOCOL_PATH, + ) + system_by_id: dict[str, Mapping[str, object]] = {} + for index, raw_system in enumerate(systems): + if not _exact_keys( + raw_system, + _SYSTEM_KEYS, + failures, + rule_id="related-work-protocol-shape", + label=f"systems[{index}]", + path=PROTOCOL_PATH, + ): + continue + system_by_id[raw_system["system_id"]] = raw_system + if raw_system["scope_stratum"] not in stratum_ids: + failures.append( + _failure( + "related-work-system-stratum", + f"{raw_system['system_id']}: unknown scope stratum", + PROTOCOL_PATH, + ) + ) + name = raw_system["name"] + if not isinstance(name, str) or "/" in name or name.endswith("*"): + failures.append( + _failure( + "related-work-composite-system", + f"{raw_system['system_id']}: system identities must not be composite", + PROTOCOL_PATH, + ) + ) + for index, raw_axis in enumerate(axes): + if not _exact_keys( + raw_axis, + _AXIS_KEYS, + failures, + rule_id="related-work-protocol-shape", + label=f"axes[{index}]", + path=PROTOCOL_PATH, + ): + continue + if ( + raw_axis["direction"] != "higher-is-stronger-evidence" + or not isinstance(raw_axis["rubric"], dict) + or set(raw_axis["rubric"]) != {"0", "1", "2", "3"} + ): + failures.append( + _failure( + "related-work-axis-rubric", + f"{raw_axis['axis_id']}: invalid ordinal rubric", + PROTOCOL_PATH, + ) + ) + case_kinds: set[str] = set() + for index, raw_case in enumerate(cases): + if not _exact_keys( + raw_case, + _CASE_KEYS, + failures, + rule_id="related-work-protocol-shape", + label=f"cases[{index}]", + path=PROTOCOL_PATH, + ): + continue + case_kinds.add(raw_case["kind"]) + _exact_keys( + raw_case["criteria"], + _CASE_CRITERIA_KEYS, + failures, + rule_id="related-work-protocol-shape", + label=f"cases[{index}].criteria", + path=PROTOCOL_PATH, + ) + if not {"authoring-task", "negative-case"}.issubset(case_kinds): + failures.append( + _failure( + "related-work-case-coverage", + "protocol requires representative authoring tasks and negative cases", + PROTOCOL_PATH, + ) + ) + _exact_keys( + protocol["analysis_rules"], + _ANALYSIS_RULE_KEYS, + failures, + rule_id="related-work-protocol-shape", + label="analysis_rules", + path=PROTOCOL_PATH, + ) + if not isinstance(protocol["amendment_log"], list): + failures.append( + _failure( + "related-work-protocol-shape", + "amendment_log must be a list", + PROTOCOL_PATH, + ) + ) + + if not _exact_keys( + snapshot, + _SNAPSHOT_KEYS, + failures, + rule_id="related-work-snapshot-shape", + label="snapshot", + path=SNAPSHOT_PATH, + ): + return failures + if snapshot["protocol_revision"] != protocol["revision"]: + failures.append( + _failure( + "related-work-snapshot-join", + "snapshot protocol revision mismatch", + SNAPSHOT_PATH, + ) + ) + sources = _bounded_list( + snapshot["sources"], + _MAX_SOURCES, + failures, + rule_id="related-work-snapshot-shape", + label="sources", + path=SNAPSHOT_PATH, + ) + observations = _bounded_list( + snapshot["observations"], + _MAX_OBSERVATIONS, + failures, + rule_id="related-work-snapshot-shape", + label="observations", + path=SNAPSHOT_PATH, + ) + task_observations = _bounded_list( + snapshot["task_observations"], + _MAX_OBSERVATIONS, + failures, + rule_id="related-work-snapshot-shape", + label="task_observations", + path=SNAPSHOT_PATH, + ) + source_by_id = _validate_sources(repo_root, sources, system_ids, failures, validate_paths=validate_paths) + scores = _validate_observations(observations, system_ids, axis_ids, source_by_id, failures) + _validate_task_observations(task_observations, system_ids, case_ids, source_by_id, failures) + _validate_analysis(protocol, snapshot, analysis, system_by_id, axis_ids, scores, failures) + _validate_publication(repo_root, protocol, snapshot, analysis, failures) + return failures + + +def evaluate(repo_root: Path = REPO_ROOT) -> list[PolicyFailure]: + try: + protocol, snapshot, analysis = load_bundle(repo_root) + except (OSError, ValueError, json.JSONDecodeError) as exc: + return [_failure("related-work-bundle-invalid", str(exc))] + return validate_bundle(repo_root, protocol, snapshot, analysis) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="Emit failures as JSON") + parser.add_argument( + "--render-publication", + action="store_true", + help="Print the expected generated publication block", + ) + args = parser.parse_args() + try: + protocol, snapshot, analysis = load_bundle(REPO_ROOT) + except (OSError, ValueError, json.JSONDecodeError) as exc: + failures = [_failure("related-work-bundle-invalid", str(exc))] + else: + if args.render_publication: + print(render_publication(protocol, snapshot, analysis)) + return 0 + failures = validate_bundle(REPO_ROOT, protocol, snapshot, analysis) + if args.json: + print(json.dumps([failure.__dict__ for failure in failures], indent=2, sort_keys=True)) + else: + for failure in failures: + print(failure.render()) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_sdl_catalog_parity.py b/tools/check_sdl_catalog_parity.py index decdf3176..2f895584c 100644 --- a/tools/check_sdl_catalog_parity.py +++ b/tools/check_sdl_catalog_parity.py @@ -60,7 +60,7 @@ _MAX_CATALOG_BYTES = 512 * 1024 _MAX_CATALOG_ROWS = 512 _METADATA_FIELDS = frozenset({"name", "version", "description"}) -_COMPOSITION_FIELDS = frozenset({"module", "imports"}) +_COMPOSITION_FIELDS = frozenset({"module", "imports", "realization"}) _NODE_VALIDATOR = "[node validator](../../implementations/python/packages/aces_sdl/validator/_nodes_infra_network.py)" _INFRASTRUCTURE_VALIDATOR = ( diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index b8747f7e3..e37ca9043 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -28,7 +28,7 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "participant-implementation-manifest" / f"{name}.json" if name.startswith("participant-implementation-provenance-v"): return schemas_dir / "participant-implementation-provenance" / f"{name}.json" - if name == "concept-families-v1": + if name in {"concept-families-v1", "behavioral-relations-v1"}: return schemas_dir / "concept-authority" / f"{name}.json" if name == "reference-models-v1": return schemas_dir / "concept-authority" / f"{name}.json" diff --git a/tools/policy/oversized_allowlist.yaml b/tools/policy/oversized_allowlist.yaml index 531abb5b7..909fdad29 100644 --- a/tools/policy/oversized_allowlist.yaml +++ b/tools/policy/oversized_allowlist.yaml @@ -16,10 +16,8 @@ files: - implementations/python/packages/aces_contracts/workflow.py - implementations/python/packages/aces_mcp/tools/authoring.py - implementations/python/packages/aces_mcp/tools/inspection.py - - implementations/python/packages/aces_mcp/tools/reference.py - implementations/python/packages/aces_operations/_evidence_run_artifact.py - implementations/python/packages/aces_processor/compiler.py - - implementations/python/packages/aces_processor/models.py - implementations/python/packages/aces_processor/planner.py - implementations/python/packages/aces_runtime/control_plane_api.py - implementations/python/packages/aces_runtime/control_plane.py