Skip to content

feat(sdl): add authored participant episode policies to behavior specifications - #1463

Draft
doublewhy wants to merge 3 commits into
devfrom
307-participant-episode-surface
Draft

doublewhy wants to merge 3 commits into
devfrom
307-participant-episode-surface

Conversation

@doublewhy

@doublewhy doublewhy commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

DSL-120 asks the SDL to support authored participant episode structure: initialization, turn or interaction structure, termination conditions, truncation conditions, and reset-related declarations. The #122 design (specs/formal/participant-episode-model/README.md, matrix row "Authored episode policy is intent (EBM-01)") and ADR-013's 2026-08-02 amendment fix the shape. It is one typed, versioned episode-policy record on the existing behavior specification, built only from existing closed references. That includes reset_policy.memory_reset_authority_ref, which must resolve to exactly one declared SDL element, because the #122 preflight resolves the reset authority before compilation and fails closed. The record declares intent and compiles to participant.episode-policy.<name> beside the ADR-013 episode addresses. This PR implements that row, including its mandated negative fixture: a policy that asserts realized episode state must be rejected.

behavior_specifications.*.episode_policy (profile participant-episode-policy/v1) declares at least one of these elements:

  • initialization: precondition assertions for every new episode generation.
  • interaction_structure: turns ordered by the decision-epoch authority, and the action contracts a turn offers. Each must belong to the owning specification and to every participant it selects.
  • terminal_conditions: keyed conditions. A completed condition names invariant or postcondition assertions. A timed_out condition names one deadline, window, or duration constraint that binds the specification or every participant it selects.
  • truncation_conditions: keyed conditions that name invariant or postcondition assertions and end the episode as truncated. They carry no terminal_reason, and none may reuse the exact assertion set of a completed condition.
  • reset_policy: the admitted reset/restart control actions and the ADR-095 participant-memory scope. As on the ADR-095 decision-surface assurance and information-state contracts, episode_local_reset names a memory_reset_authority_ref and persistent_across_episodes names none. The policy does not supply those contracts' values. The authority must resolve to exactly one declared targetable element, such as agents.analyst or nodes.siem.services.console; a dangling or ambiguous ref is rejected.

Each condition maps to exactly one ADR-013 terminal reason and names the evidence requirements that back the transition. A condition holds only when every assertion it names holds. Validation checks that each named evidence requirement is declared, not which record it captures. interrupted cannot be authored. A participant is governed by at most one episode policy, by name or by role, because realized episodes are keyed by participant. Checks that need a participant selection or a reference still written as a ${var} wait for the post-instantiation pass. The record rejects realized episode coordinates (episode_id, status, timestamps, sequence numbers, decision_epoch, a terminal reason outside a condition) with a diagnostic that names the ADR-013 contracts. Compilation emits one record per policy in the new RuntimeModel.participant_episode_policies map. Each record is keyed to the selected participants' participant.behavior.<agent> addresses, the same keys that ADR-013 episode state uses, and carries the reset authority's canonical sdl.* address. Nothing plans, executes, or evaluates the policy, and no executable or runtime adoption is claimed.

Because this changes source bound by the research evidence, both captures are republished at the next free releases (specification coverage 69.0.0, formal validation 70.0.0); retained outcomes, classifications and claim limits are unchanged. Two formal compile controls record digest-only deviations, because the compiled runtime model gains the new field.

Requirement UIDs

  • DSL-120. Issue Participant Episode Structure And Termination Surface (DSL-120) #307 names DSL-120 as the CI branch UID. This branch name carries none, because a DSL-120 branch fails requirement governance on ownership. With the UID forced, tools/check_requirement_governance.py --base-rev origin/dev --requirement-uid DSL-120 reports 18 failures. Seven are requirement-ownership-mismatch, because the sdl-language ownership roots in tools/policy/requirement_order.yaml do not include these files: contracts/fixtures/sdl/participant-episode-policy-v1/README.md, contracts/fixtures/sdl/participant-episode-policy-v1/valid/analyst-shift-episode.yaml, contracts/fixtures/sdl/participant-episode-policy-v1/invalid/realized-episode-state.yaml, docs/public/participant-control.md, docs/requirements/DSL-120/requirement.md, specs/concept-authority/semantic-profiles.md, and specs/formal/assurance-fulfillment.yaml (tracked in Requirement ownership roots do not cover the files of DSL-117, DSL-120, DSL-121, ACT-624, SEM-223 and SEM-204 #1510). The other 11 are trace lines that docs/requirements/DSL-120/requirement.md does not record: IMPLEMENTS for raes/_language_metadata.py, raes/_mapping_scopes.py, raes/validator/__init__.py, raes/validator/_content_objectives.py, raes_processor/compiler/addresses.py, raes_processor/compiler/pipeline.py, raes_processor/models/__init__.py, and raes_processor/models/runtime_model.py, and TESTS for the three evidence modules listed under Traceability as revision pins only. Without a UID, the policy runner skips requirement governance.

Related Issues

Closes #307

ADR Impact

Changes

  • raes/participant_episode_policy.py (new): the closed ParticipantEpisodePolicy record and its parts. Every part rejects realized episode fields by name (the field sets come from ParticipantEpisodeExecutionState, ParticipantEpisodeHistoryEvent, and decision_epoch). Terminal conditions are a discriminated union on terminal_reason (completed or timed_out). Truncation conditions carry no reason field and no temporal reference, and a stray terminal_reason there gets its own diagnostic. Condition keys are portable identifiers, unique across both maps, and a truncation condition may not reuse the exact assertion set of a completion condition, so one evidenced fact never maps to both completed and truncated. Reference items are non-blank in the model and the schema. The reset policy pairs episode_local_reset with memory_reset_authority_ref, as ParticipantDecisionSurfaceAssuranceV2Model and ParticipantInformationStateRecordModel do, and its schema states the same pairing with if/then/else.
  • raes/participant_behavior_specification.py: optional episode_policy member, excluded from dumps when absent, so policy-free specifications keep their existing canonical shape.
  • raes/validator/_participant_episode_policies.py (new), wired through raes/validator/__init__.py and one call in _content_objectives.py: assertions must resolve with the right role. Turn actions must belong to the owning specification and to each selected participant's actions, like the tool-affordance and autonomous-execution checks. Timeouts must name a deadline/window/duration constraint that binds the specification or every selected participant (explicit or role-selected), like the autonomous window rule. Evidence requirements must resolve, and at most one policy may govern each participant, like the autonomous owner-conflict rule. The reset authority must resolve to exactly one declared targetable element through the shared named-reference check that authority_scope_refs uses, so a dangling or ambiguous authority is rejected. Checks that depend on a ${var} participant selection, role, action list, or reference are skipped before instantiation and run again after it.
  • raes/_mapping_scopes.py: condition maps are literal-key maps, so condition identifiers keep their spelling and ${var} keys are rejected.
  • raes/composition/_behavior.py: module composition rewrites the policy's assertion, action, temporal, evidence, and reset-authority references and keeps condition identifiers local.
  • raes/_language_metadata.py: editor completions for episode_policy, assertion_refs, and temporal_constraint_ref.
  • raes_processor/models/participant_episode_resources.py (new) and raes_processor/compiler/participant_episode_policies.py (new), registered in addresses.py, pipeline.py, models/runtime_model.py, and models/__init__.py: ParticipantEpisodePolicyRuntime at participant.episode-policy.<name> with resolved assertion, action, time, and evidence addresses and the reset authority's canonical sdl.<section>.<name> address (memory_reset_authority_address). The compiler resolves that address through the pipeline's declaration index (pipeline.py passes it in), raises if a validated ref does not resolve to one declaration, and lists the address in refresh_dependencies. The compiler returns early when no specification declares a policy. ParticipantEpisodeConditionRuntime.__post_init__ re-asserts on the compiled record that the reason is completed, timed_out, or truncated, that evidence is present, and that only timed_out uses a temporal constraint. ParticipantEpisodePolicyRuntime.__post_init__ re-asserts that a reset authority address is present exactly when the scope is episode_local_reset.
  • Schemas: sdl-authoring-input-v1, instantiated-scenario-v1, instantiated-scenario-snapshot-v1, materialized-scenario-v1, and scenario-satisfiability-evidence-v1 regenerated from schema_bundle(), with last_change ledger entries under contracts/schema-publication/entries/. The manifest itself is the v2 pointer and is unchanged. materialized-scenario-v1.json is 511,867 bytes, inside the 512,000-byte budget that test_issue_1241_schema_publication_size.py enforces (the merge-order hazard on current dev is tracked in The materialized-scenario-v1 schema exceeds its 512,000-byte budget if #1463 lands before #1467 #1556). Three parser rules stay out of the schema: a policy must declare at least one element (like the behavior-specification aggregate rule, and the budget leaves no room for the anyOf), condition keys must differ across the two maps, and a truncation condition must not reuse a completion condition's assertion set (JSON Schema cannot compare keys or values across two objects). The reset authority stays a string in the schema; its resolution is a semantic-validation rule.
  • contracts/fixtures/sdl/participant-episode-policy-v1/: valid/analyst-shift-episode.yaml (a human-supervised participant with every element) and invalid/realized-episode-state.yaml (the mandated negative: the same policy asserting status, initialized_at, decision_epoch, and terminal_reason), plus a README.
  • specs/sdl/references.md: nine reference rows, including the targetable reset-authority row (fatal dangling or ambiguous), checked against the new tools/sdl_catalog_parity/_expectations_4.py (registered in _registry.py).
  • specs/sdl/sections.md: new "Participant episode structure" normative section.
  • docs/explain/sdl/sections.md: authoring guide with an example.
  • docs/explain/sdl/lineage.md: DSL-120 lineage and delivery-evidence bullets. The lineage ledger is unchanged because no top-level field, concept family, or reference model is added.
  • docs/public/participant-control.md: one Scenario-author bullet for episode_policy, and a link to the valid fixture.
  • specs/concept-authority/semantic-profiles.md: replaces the sentence that said SDL has no episode authoring syntax. The profile JSON is unchanged.
  • specs/formal/assurance-fulfillment.yaml: records Participant Episode Structure And Termination Surface (DSL-120) #307's delivered unit tests and typed contract, and removes Participant Episode Structure And Termination Surface (DSL-120) #307 from the two waivers that tracked it.
  • docs/requirements/DSL-120/requirement.md: 13 traceability lines. status (DRAFT) and updated_at are unchanged.
  • Specification-coverage release 69.0.0 (execution-snapshot-v69.json, analysis-v69.json, issue-307 bundle) replays the retained matrix against this branch's source.
  • Formal-validation release 70.0.0 (execution-snapshot-v70.json, analysis-v70.json, retest-v70.json) replays the retained formal cases with baseline 69.0.0. compile-repeatability-control and compile-non-vacuity-control keep their outcomes, and only their result_digest changes, recorded as accepted compiled-policy-metadata deviations (precedent: feat: carry authored execution and recovery requirements #1413's execution-policy field).
  • Revision pins advanced in tools/check_specification_coverage.py, tools/formal_semantic_validation/, and the three evidence test modules. Both research indexes record the new releases.

Test Plan

  • Unit tests pass
  • Integration tests pass if applicable
  • Full completion suite required in CI before merge
  • No coverage regression

uv run --project implementations/python --frozen --all-extras python -m pytest implementations/python/tests/test_issue_307_dsl_120_participant_episode_policy.py -q -p no:cacheprovider passed (75 passed). The same command with -m integration selected nothing (75 deselected), because the module has no integration-marked tests.

The tests fail without the change. With implementations/python/packages checked out from origin/dev, the module fails at collection (ImportError from raes_processor.models). With that import moved into the two tests that use it, all 75 cases fail.

The review-round cases were written before the fix. Run against the previous head cbf38eee source, the updated module gave 11 failed, 64 passed. The failures were the dangling (analyst-console-reset) and ambiguous (ticket-closed, both an assertion and a proposition) reset-authority rows, the three reset-authority address cases, the namespaced authority in the module-composition case, the ${var} reset-authority revalidation case, the truncation-alias case, and the three compiled-record pairing cases.

The tests are not vacuous. At this head, six temporary mutations, applied one at a time and restored before the next run, each failed the cases they cover: ignoring the reset authority in the validator (3 failed), leaving it un-namespaced in composition (1), dropping it from refresh_dependencies (3), rendering the raw section instead of the sdl.* address (5), skipping the compiled pairing check (3), and accepting a truncation alias of a completion (1). At the previous head cbf38eee, 22 earlier mutations, applied the same way, each failed their cases:

  • Disabling the realized-state guard failed 6 cases, the five realized-state cases and the invalid-fixture case. The input still fails as an extra field, but without the ADR-013 diagnostic.
  • Accepting any assertion role failed the 3 role cases.
  • Dropping the timeout binding check failed 2 cases. Accepting a binding to any one participant instead of every participant failed the two-participant case.
  • Removing the validator call from _content_objectives.py failed 17 cases.
  • Ignoring role-selected participants failed 3 cases.
  • Disabling the one-policy rule failed both uniqueness cases. Counting policy-free specifications in it failed the shared-participant case.
  • Dropping the per-participant turn-action check failed its case.
  • Treating a ${var} participant selection, or a ${var} action list, as resolved failed the matching deferral case.
  • Dropping either side of the reset-authority pairing failed its case.
  • Dropping the truncation terminal_reason diagnostic failed its case.
  • Skipping the composition rewrite failed the module-composition case.
  • Removing the condition maps from NESTED_HASHMAP_FIELDS failed 56 cases, because hyphenated condition keys are then rejected as non-canonical fields.
  • Compiling truncation as completed failed the compilation case.
  • Dumping an absent policy as null failed the policy-free-shape case.
  • Removing the assertion_refs completion failed one language-service case.
  • Disabling the compiled-reason guard failed the interrupted IR case.
  • In the published sdl-authoring-input-v1.json, removing the reference-item pattern failed the blank-ref schema case, and removing the reset-policy allOf failed both reset-authority schema cases.

With the change, 22 related modules plus the new one passed: catalog parity, lineage, language service, DSL-142 delivery, ACT-617 mixed control, classification migration, instantiated-scenario schema, example schema conformance, three #1241 schema modules (materialization schemas, schema omissions, publication size), processor manifest, reference models, SEM-208, YAML mapping keys, SDL parser, runtime models, assurance policy, semantic profiles, concept authority and its governance module, and #1361 policy composition (938 passed, 1 skipped, 1 deselected). With -m integration they passed too (1 passed: test_live_sdl_catalogs_match_authorities).

tools/check_generated_schemas.py, tools/check_schema_publication.py --base-rev origin/dev, tools/check_sdl_catalog_parity.py, tools/check_sdl_lineage.py, and tools/check_assurance_policy.py pass, and the review fixes leave every published schema unchanged. tools/check_specification_coverage.py and tools/check_formal_semantic_validation.py pass (integrity and replay). The three evidence modules pass with default markers (209 passed, 19 deselected) and with -m integration (19 passed). nox -s contracts -- --base-rev origin/dev passed (14 of 14 steps).

nox -s verify-fast-feedback -- --base-rev origin/dev, nox -s lint, make policy, and nox -s docs-local (public source boundary, Vale, executable quickstart, Sphinx HTML, output inventory) passed. The policy runner skips requirement governance on its own, because this UID-free branch has no requirement-scope file.

No coverage regression: at head a6218096, the SonarCloud PR gate is OK (96.9% new-code coverage, 0 new violations, 0.0% duplication), and canonical / coverage-reduce passed in CI run 38059289132.

Ground Control Checks

  • Repository policy command passes
  • Each new assertion group fails when the source path it covers is broken (mutation runs above)

Traceability

  • IMPLEMENTS: DSL-120 ← implementations/python/packages/raes/participant_episode_policy.py, implementations/python/packages/raes/participant_behavior_specification.py, implementations/python/packages/raes/validator/_participant_episode_policies.py, implementations/python/packages/raes/composition/_behavior.py, implementations/python/packages/raes_processor/compiler/participant_episode_policies.py, implementations/python/packages/raes_processor/models/participant_episode_resources.py, contracts/schemas/sdl/sdl-authoring-input-v1.json, contracts/schemas/sdl/instantiated-scenario-v1.json, specs/sdl/sections.md, specs/sdl/references.md
  • TESTS: DSL-120 ← implementations/python/tests/test_issue_307_dsl_120_participant_episode_policy.py
  • Evidence revision pins only: implementations/python/tests/test_specification_coverage.py, implementations/python/tests/test_formal_semantic_validation.py, implementations/python/tests/test_issue_989_versioned_evidence.py

Checklist

  • Code follows the project coding standards (ruff format and ruff check clean; every touched package file stays under the 500-line cap; raes/_declarations.py and raes_contracts/contracts/_exports.py are untouched)
  • FM2: authored-surface semantic constraints with typed IR coverage. Invariants: an episode policy holds references only and no realized episode coordinate. Initialization assertions are preconditions; completion and truncation assertions are invariants or postconditions. Turn actions belong to the owning specification and every selected participant, a timeout binds the specification or every selected participant, and each participant has at most one policy. An episode_local_reset scope names its reset authority, which resolves to exactly one declared targetable element. A truncation condition never reuses a completion condition's assertion set. Each condition maps to exactly one of completed, timed_out, or truncated, and names evidence. The compiled records re-assert the reason and backing pairing and the reset-authority pairing. The FM3 abstract state machine is the Participant episode/reset & budget/quota: semantics + language surfaces + model (SEM-222, SEM-223, DSL-120, DSL-121, ACT-623, ACT-624) #122 design's and is unchanged. No executable or runtime adoption is claimed.
  • Published contract schemas regenerated if models changed (five schemas, each with a ledger entry)
  • PR title is a Conventional Commit; feat cuts a minor release for the new authoring surface
  • Architectural docs updated if applicable: normative SDL section, reference catalog, explain guide, and lineage notes

Documentation

Updated: specs/sdl/sections.md, specs/sdl/references.md, docs/explain/sdl/sections.md, docs/explain/sdl/lineage.md, docs/public/participant-control.md, specs/concept-authority/semantic-profiles.md. Verified unchanged: docs/explain/sdl/limitations.md (pinned evidence artifact) and the ADR-013 text.

…ifications

Add the optional behavior_specifications.*.episode_policy record (DSL-120):
initialization precondition assertions, decision-epoch turn structure,
completion and timeout terminal conditions, truncation conditions, and a
reset policy with admitted control actions and a participant-memory scope.
Every condition maps to exactly one ADR-013 terminal reason and names the
evidence requirements that back the transition; interruption is never
authored. The closed record rejects realized episode coordinates by name.

Validate references, assertion roles, owner-bound timeouts and evidence
fail closed, rewrite references under module composition, and compile each
policy to participant.episode-policy.<name> keyed to the selected
participants' participant.behavior.<agent> addresses. Regenerate the five
scenario schemas with their publication entries, add the reference-catalog
rows, valid and invalid fixtures, normative and explain documentation,
lineage notes and DSL-120 traceability.
…nct from completion

Resolve reset_policy.memory_reset_authority_ref to exactly one declared
targetable SDL element during semantic validation. Dangling and ambiguous
refs fail closed, and ${var} refs wait for the post-instantiation pass.
Module composition now namespaces the authority. Compilation records the
element's canonical sdl.* address as memory_reset_authority_address, adds
it to the policy's refresh dependencies, and re-asserts that only an
episode_local_reset scope names one. Add the matching reference-catalog
row.

Reject a truncation condition that reuses the exact assertion set of a
completion condition, so one evidenced fact cannot end an episode as both
completed and truncated.

Reword the reset-policy comment, the normative section and the explain
guide: the ADR-095 assurance and information-state contracts require the
same pairing, but the policy does not supply their values. State that a
condition holds only when every assertion it names holds and that
validation checks only that its evidence requirements are declared, and
point the public participant-control guide at the episode policy fixture.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant