From e547fb6d7d7142fc8e20ae1191e7258efb801f66 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 24 Sep 2026 23:56:57 +0200 Subject: [PATCH 1/2] Accept decisions 0004 and 0005 Sindre Alstad Wilting accepted both records on 2026-09-24. 0005 shipped in v4.29.0 (#251), so it moves to stage implemented; 0004 stays at stage none because ESM v3 has no source-backed corpus vector yet. Decision: 0004 Decision: 0005 --- ...-and-governance-of-portable-executable-semantics.md | 8 +++++++- ...-predicates-as-an-implementation-attachment-role.md | 10 ++++++++-- decisions/README.md | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/decisions/0004-admission-and-governance-of-portable-executable-semantics.md b/decisions/0004-admission-and-governance-of-portable-executable-semantics.md index 9335ce6..1b3f26d 100644 --- a/decisions/0004-admission-and-governance-of-portable-executable-semantics.md +++ b/decisions/0004-admission-and-governance-of-portable-executable-semantics.md @@ -1,8 +1,10 @@ --- id: 0004 title: Admission and governance of portable executable semantics -status: proposed +status: accepted stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting class: contract reversibility: costly applies-to: @@ -82,3 +84,7 @@ Adding a construct costs more: vectors, an unsupported path and a record when a ## Related issues Screenplay: [#128](https://github.com/Cratis/Screenplay/issues/128), [#135](https://github.com/Cratis/Screenplay/issues/135), [#136](https://github.com/Cratis/Screenplay/issues/136), [#139](https://github.com/Cratis/Screenplay/issues/139), [#167](https://github.com/Cratis/Screenplay/issues/167), [#218](https://github.com/Cratis/Screenplay/issues/218). Chronicle: [#4130](https://github.com/Cratis/Chronicle/issues/4130). + +## Status notes + +**2026-09-24 — accepted.** Accepted as written, including the two rules this record inferred from v2 and v3 rather than from #128: a construct that previously failed binding may join the highest existing ESM version when no model that bound before changes bytes, and each release that adds an ESM version opens a tracking issue in Stage, CLI, Studio and Generation. The decision text is unchanged. It stays at `stage: none` because its *Done when* is not met: ESM v3 has no source-backed `Cratis.Screenplay.CanonicalCorpus` vector yet. diff --git a/decisions/0005-policy-predicates-as-an-implementation-attachment-role.md b/decisions/0005-policy-predicates-as-an-implementation-attachment-role.md index c478480..8111a5d 100644 --- a/decisions/0005-policy-predicates-as-an-implementation-attachment-role.md +++ b/decisions/0005-policy-predicates-as-an-implementation-attachment-role.md @@ -1,8 +1,10 @@ --- id: 0005 title: "Policy predicates as an implementation attachment role, composed in authored order" -status: proposed -stage: none +status: accepted +stage: implemented +decided: 2026-09-24 +decider: Sindre Alstad Wilting class: contract reversibility: costly applies-to: @@ -76,3 +78,7 @@ Models with code policies reach the ESM, and their unrelated specifications run. ## Related issues Screenplay: [#139](https://github.com/Cratis/Screenplay/issues/139). Decisions: [0001](0001-chronicle-runtime-semantic-authority.md), [0002](0002-implementation-attachments-envelope-and-reducer-role.md), [0004](0004-admission-and-governance-of-portable-executable-semantics.md). + +## Status notes + +**2026-09-24 — accepted and implemented.** Accepted as written; the decision text is unchanged. The references above to what decision 0004 "proposes" now point at an accepted record. `feature/policy-predicate-role` merged as [#251](https://github.com/Cratis/Screenplay/pull/251) and shipped in v4.29.0, with the specs named under *Verification* in the tree. It is not yet `verified`. diff --git a/decisions/README.md b/decisions/README.md index 2de4e29..f1078b7 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -5,5 +5,5 @@ | [0001](0001-chronicle-runtime-semantic-authority.md) | Use Chronicle's runtime meaning for portable executable semantics | accepted | implemented | 2026-09-24 | Sindre Alstad Wilting | | [0002](0002-implementation-attachments-envelope-and-reducer-role.md) | Implementation attachments: envelope first, reducer transitions as the first role | accepted | none | 2026-09-24 | Sindre Alstad Wilting | | [0003](0003-decision-consistency-for-command-reads.md) | Decision consistency for command reads | accepted | none | 2026-09-24 | Sindre Alstad Wilting | -| [0004](0004-admission-and-governance-of-portable-executable-semantics.md) | Admission and governance of portable executable semantics | proposed | none | - | - | -| [0005](0005-policy-predicates-as-an-implementation-attachment-role.md) | Policy predicates as an implementation attachment role, composed in authored order | proposed | none | - | - | +| [0004](0004-admission-and-governance-of-portable-executable-semantics.md) | Admission and governance of portable executable semantics | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0005](0005-policy-predicates-as-an-implementation-attachment-role.md) | Policy predicates as an implementation attachment role, composed in authored order | accepted | implemented | 2026-09-24 | Sindre Alstad Wilting | From 0165f33acd137088a4366e89fa79212c97864d35 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 24 Sep 2026 23:56:58 +0200 Subject: [PATCH 2/2] Record the 2026-09-24 language and contract rulings as 0006-0014 - 0006 reaction triggers declare reads (#69) - 0007 affected read-model instances follow Chronicle (#132) - 0008 one data subject per event (#141) - 0009 external event origin and Translate slices (#73) - 0010 query paging, ordering and live delivery (#140) - 0011 event generations (#71) - 0012 typed-context descriptor, then command handlers (#139, #84) - 0013 equivalence for Screenplay and code round trips (#148) - 0014 diagnostic repairs are typed workspace proposals (#138) Decision: 0006 Decision: 0007 Decision: 0008 Decision: 0009 Decision: 0010 Decision: 0011 Decision: 0012 Decision: 0013 Decision: 0014 --- .../0006-reaction-triggers-declare-reads.md | 73 +++++++++++++++++ .../0007-affected-read-model-instances.md | 81 +++++++++++++++++++ decisions/0008-one-data-subject-per-event.md | 70 ++++++++++++++++ ...nal-event-origin-and-translation-slices.md | 69 ++++++++++++++++ ...query-paging-ordering-and-live-delivery.md | 78 ++++++++++++++++++ decisions/0011-event-generations.md | 74 +++++++++++++++++ ...ext-descriptor-and-command-handler-role.md | 69 ++++++++++++++++ ...valence-for-screenplay-code-round-trips.md | 66 +++++++++++++++ ...c-repairs-are-typed-workspace-proposals.md | 62 ++++++++++++++ decisions/README.md | 9 +++ 10 files changed, 651 insertions(+) create mode 100644 decisions/0006-reaction-triggers-declare-reads.md create mode 100644 decisions/0007-affected-read-model-instances.md create mode 100644 decisions/0008-one-data-subject-per-event.md create mode 100644 decisions/0009-external-event-origin-and-translation-slices.md create mode 100644 decisions/0010-query-paging-ordering-and-live-delivery.md create mode 100644 decisions/0011-event-generations.md create mode 100644 decisions/0012-typed-context-descriptor-and-command-handler-role.md create mode 100644 decisions/0013-equivalence-for-screenplay-code-round-trips.md create mode 100644 decisions/0014-diagnostic-repairs-are-typed-workspace-proposals.md diff --git a/decisions/0006-reaction-triggers-declare-reads.md b/decisions/0006-reaction-triggers-declare-reads.md new file mode 100644 index 0000000..e858ae7 --- /dev/null +++ b/decisions/0006-reaction-triggers-declare-reads.md @@ -0,0 +1,73 @@ +--- +id: 0006 +title: Reaction triggers declare the views they decide from with reads +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Syntax/ReactionSyntax.cs + - Source/DotNET/Screenplay/Parsing/ReactionParser.cs + - Source/DotNET/Screenplay/Parsing/TriggerParser.cs + - Source/DotNET/Screenplay/Parsing/ReadsParser.cs + - Source/DotNET/Screenplay/Parsing/ScreenplayValidator.cs + - Source/DotNET/Screenplay/Semantics/SemanticModelBinder.SliceMembers.cs + - Documentation/screenplay/reactions.md + - Documentation/screenplay/grammar.md + - Source/Screenplay/Monaco/** + - Source/Screenplay/VSCodeExtension/** +--- + +## Context + +An automation decides from state it consults: in Event Modeling, a process watches a to-do view and acts on each item. [#69](https://github.com/Cratis/Screenplay/issues/69) asks for that relationship. A reaction can already start from an event, a declared trigger or the clock, and can `produces` or `invokes`, but it cannot name the views behind its decision. `ReactionTriggerSyntax` has no reads member ([`ReactionSyntax.cs:44-52`](../Source/DotNET/Screenplay/Syntax/ReactionSyntax.cs)), so the inputs of an automation are invisible in the document. + +Commands already have `reads [as ] [by ]`. [Decision 0003](0003-decision-consistency-for-command-reads.md) makes a command's reads a protected decision dependency and rejects giving one keyword two meanings ([0003, options](0003-decision-consistency-for-command-reads.md#options-considered)). Reactions do not bind in the executable semantic model (ESM) today: every reaction fails with "requires portable occurrence and effect semantics" ([`SemanticModelBinder.SliceMembers.cs:93`](../Source/DotNET/Screenplay/Semantics/SemanticModelBinder.SliceMembers.cs)). + +Today a line `reads X` under a trigger parses as a trigger value named `reads` of type `X`: the trigger body falls through to `TriggerParser.ParseData`, which accepts any property line ([`ReactionParser.cs:175-178`](../Source/DotNET/Screenplay/Parsing/ReactionParser.cs), [`TriggerParser.cs:87-89`](../Source/DotNET/Screenplay/Parsing/TriggerParser.cs)). + +## Decision + +A reaction trigger may declare `reads [as ] [by ]`, with the single meaning `reads` has on commands: the state the behavior decides from. + +1. **Syntax.** `reads` is a trigger body line, parsed by the existing `ReadsParser` into the existing `ReadsSyntax`. The alias rules are the command rules: a second read of the same view needs an alias, and aliases are unique per trigger. +2. **`by`.** On a reaction, `by` names a value the trigger takes. Clock triggers (`every`, `at`) take no values, so they can only read a whole view. +3. **Protection.** A reaction that `invokes` a command decides nothing itself; the command decides, and the command's own reads carry decision 0003's protection. A reaction that `produces` directly is held to decision 0003's rule once reactions bind: its reads are protected only where Chronicle can check them exactly, and otherwise binding fails with a diagnostic that names the reason. +4. **Reserved form.** `for each ` is reserved for a later view-driven trigger. It is not admitted by this record. +5. **Compatibility.** A trigger value named `reads` must now be written `@reads`, the escape the parser already applies to other directive names ([`ReactionParser.cs:172-174`](../Source/DotNET/Screenplay/Parsing/ReactionParser.cs)). + +## Options considered + +- **`reads` on the trigger with one meaning (taken).** It states the automation's inputs with the word commands already use for the same thing, and it does not foreclose a view-driven trigger, which would still want to name what it reads ([#69 sweep comment](https://github.com/Cratis/Screenplay/issues/69)). +- **Report-only `reads` on reactions.** Not taken: `reads` on commands becomes protected under decision 0003, so a report-only reaction `reads` would give one keyword two meanings. +- **A different word such as `uses`.** Not taken: `uses` already attaches UI behaviors to screens ([`ScreenParser.cs:73-76`](../Source/DotNET/Screenplay/Parsing/ScreenParser.cs)), and a second word for the same relationship is what decision 0003 avoids. +- **The view as the trigger (`when `).** Not taken: `when` resolves against events, declared triggers and registered triggers ([`triggers.md`, "How a name resolves"](../Documentation/screenplay/triggers.md#how-a-name-resolves)), so views would be a fourth set with event/view name collisions. It also misstates the runtime: Chronicle reports a read-model change as `Added`, `Modified` or `Removed` ([`ReadModelChangeType.cs:9-25`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/ReadModels/ReadModelChangeType.cs)), not as "the view has rows". +- **Wait for the view-driven trigger.** Not taken: the automation's inputs stay unwritable in the meantime, and nothing in the view-driven design depends on leaving them out. + +## Default if unanswered + +Reactions keep hiding their inputs. `reads X` under a trigger keeps parsing as a trigger value, so authors who write it get a silent misreading rather than a diagnostic. Studio and Stage cannot draw or realize the view-to-automation edge. + +## Timeline and scope + +Settle before any reaction grammar work under #69, and keep it until superseded. The grammar is additive apart from the `@reads` escape and can ship first. Protection for `produces` ships only when reactions bind in the ESM, under decision 0003's rule and decision [0004](0004-admission-and-governance-of-portable-executable-semantics.md)'s admission gates. + +In scope: the trigger `reads` grammar in the parser, syntax tree, printer, walker, TextMate and Monaco grammars; the alias and unknown-view checks; the `by`-against-trigger-values check; the `@reads` compatibility note in `reactions.md`; the protection rule stated above. + +Out of scope: a view-driven trigger (`for each `); business due time (`due at`) and terminal-fact or cancellation semantics, which are deferred because neither Chronicle nor Arc has a scheduling primitive to mirror, Chronicle's read-model reactors are best-effort with no retries or ordering guarantee ([`reacting-to-changes.mdx:56-65`](https://github.com/Cratis/Chronicle/blob/main/Documentation/read-models/reacting-to-changes.mdx)), and the work waits for Stage to decide how view-driven automation is realized and for the logical clock in [#87](https://github.com/Cratis/Screenplay/issues/87); binding reactions in the ESM; `where` over read paths; Stage's rendering of trigger reads. + +## Verification + +**Done when:** `reads as by ` under an event or named trigger parses into `ReactionTriggerSyntax` with a `ReadsSyntax` entry, and round-trips through the printer. Under a clock trigger, `reads ` parses and `reads by ` is a diagnostic. A `by` that names no trigger value, an unknown view, a missing alias on a repeated view and a duplicate alias are diagnostics. `@reads X` still parses as a trigger value named `reads`. `reactions.md` and `grammar.md` describe the clause and the escape. + +**Verify by:** Parser, validator and printer specs for each case above, and an editor-grammar check that `reads` is highlighted under a trigger. When reactions bind, binder specs show a `produces` reaction's read is either protected under decision 0003 or rejected with the reason. + +## Consequences + +An automation's inputs become visible to readers, Studio and renderers, using the same word and shape as commands. Protection for reactions comes for free through invoked commands and is never weaker than decision 0003 for direct `produces`. Documents that used a trigger value named `reads` need the `@reads` escape. Due time, terminal facts and a view-driven trigger remain open, and `for each` is kept free for them. + +## Related issues + +Screenplay: [#69](https://github.com/Cratis/Screenplay/issues/69), [#87](https://github.com/Cratis/Screenplay/issues/87), [#129](https://github.com/Cratis/Screenplay/issues/129). Decisions: [0003](0003-decision-consistency-for-command-reads.md), [0004](0004-admission-and-governance-of-portable-executable-semantics.md). diff --git a/decisions/0007-affected-read-model-instances.md b/decisions/0007-affected-read-model-instances.md new file mode 100644 index 0000000..ca2c223 --- /dev/null +++ b/decisions/0007-affected-read-model-instances.md @@ -0,0 +1,81 @@ +--- +id: 0007 +title: Which read-model instances an event affects follows Chronicle's keys and joins +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Semantics/SemanticBehaviors.cs + - Source/DotNET/Screenplay/Semantics/ExecutableSemanticModel.cs + - Source/DotNET/Screenplay/Semantics/SemanticProjectionScopes.cs + - Source/DotNET/Screenplay/Semantics/SemanticModelBinder.Projections.cs + - Source/DotNET/Screenplay/Semantics/Execution/SemanticExecutionPlan.cs + - Source/DotNET/Screenplay/Semantics/Execution/SemanticScopedProjection.cs + - Source/DotNET/Screenplay/Semantics/Serialization/** +--- + +## Context + +[#132](https://github.com/Cratis/Screenplay/issues/132) asks which read-model instance, or instances, an event affects, and suggests a key-list form such as `on CustomersShared keys customerIds`. The executable semantic model (ESM) already carries `SemanticAffectedInstance` with a cardinality of `One`, `ZeroOrOne` or `Many` ([`SemanticBehaviors.cs:328`](../Source/DotNET/Screenplay/Semantics/SemanticBehaviors.cs)). The validator accepts all three, reading `Many` as a collection-typed key and `ZeroOrOne` as an optional key ([`ExecutableSemanticModel.cs:640-645`](../Source/DotNET/Screenplay/Semantics/ExecutableSemanticModel.cs)); the binder only ever produces `One` ([`SemanticModelBinder.Projections.cs:107`](../Source/DotNET/Screenplay/Semantics/SemanticModelBinder.Projections.cs)); the evaluator rejects anything else ([`SemanticExecutionPlan.cs:169-172`](../Source/DotNET/Screenplay/Semantics/Execution/SemanticExecutionPlan.cs)). Both values are pinned in the golden vectors. + +Under [decision 0001](0001-chronicle-runtime-semantic-authority.md), Chronicle defines the meaning: + +- A key resolver returns a resolved key, a deferred key (retried once the parent exists) or an unresolvable key. There is no list-of-keys outcome ([`KeyResolverResult.cs:12-35`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Core/Projections/Engine/KeyResolverResult.cs)). +- "Many" exists only structurally. A root join updates every existing document whose join property equals the value, with `UpdateMany` and no upsert ([`ChangesetConverter.cs:354-401`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Storage.MongoDB/Sinks/ChangesetConverter.cs), [`Sink.cs:164-172`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Storage.MongoDB/Sinks/Sink.cs)). A child join matches children across parents. Remove-via-join pulls the child from every document ([`Sink.cs:662-674`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Storage.MongoDB/Sinks/Sink.cs)). + +## Decision + +Affected instances mirror Chronicle. No new syntax is added. + +1. **`from` affects one instance.** A `from` transition affects exactly one instance, possibly deferred until its parent exists. A key never resolves to a list, so a key-list syntax is rejected. +2. **"Many" is structural.** It is expressed only by the scoped shapes the ESM already binds (`SemanticProjectionJoin`, `SemanticProjectionJoinRemoval` in [`SemanticProjectionScopes.cs`](../Source/DotNET/Screenplay/Semantics/SemanticProjectionScopes.cs)): + - a root join updates every existing instance whose join property equals the event source id, and never creates one; + - a child join updates matching children across parents; + - remove-via-join pulls the child from every document. +3. **Transition cardinality.** `ZeroOrOne` and `Many` on a projection transition have no Chronicle meaning. They get a deprecation diagnostic now, and are removed from canonical bytes in the next ESM version, following [decision 0004](0004-admission-and-governance-of-portable-executable-semantics.md). Query cardinality (`SemanticQueryCardinality`) is unaffected. +4. **Derived view for Studio.** Screenplay derives a read-only affected-instance view per (event, block) from the projection scope: one by key (`from`, removal); one per event source (`all`); many where a property equals the event source id (root join); many where the child identity equals the key, across parents (child join, remove-via-join). It is derived, not authored, and adds nothing to canonical bytes. +5. **Reducers.** Reducers are keyed by the event source only, as [decision 0002](0002-implementation-attachments-envelope-and-reducer-role.md) settled. + +## Known differences to resolve + +- **Chronicle, root join key.** A root join resolves its key as the event source id and ignores the join's key expression ([`ProjectionFactory.cs:969-981`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Core/Projections/Engine/ProjectionFactory.cs)), although the .NET client's variant support sets one. Tracked in [Cratis/Chronicle#4165](https://github.com/Cratis/Chronicle/issues/4165). +- **Chronicle, in-memory sink.** It applies a root join to the one document it resolved ([`InMemorySink.cs:430-432`](https://github.com/Cratis/Chronicle/blob/main/Source/Kernel/Storage.InMemory/Sinks/InMemorySink.cs)) instead of fanning out as the MongoDB and SQL sinks do. Tracked in Chronicle#4165. +- **Reference evaluator.** It matches Chronicle's join fan-out ([`SemanticScopedProjection.cs:144-163`](../Source/DotNET/Screenplay/Semantics/Execution/SemanticScopedProjection.cs)) with two exceptions: it honors the key of a variant join ([`SemanticModelBinder.Variants.cs:108-110`](../Source/DotNET/Screenplay/Semantics/SemanticModelBinder.Variants.cs)) where Chronicle uses the event source id, and it fails a child event with no parent ([`SemanticScopedProjection.cs:265-271`](../Source/DotNET/Screenplay/Semantics/Execution/SemanticScopedProjection.cs)) where Chronicle defers it. + +Neither Chronicle difference was reproduced at runtime; both come from reading the code. + +## Options considered + +- **Mirror Chronicle; many is structural (taken).** Every construct has a runtime counterpart, and the ESM stops accepting states the evaluator and Chronicle cannot run. +- **Key-list syntax (`keys customerIds`).** Not taken: Chronicle has no construct that resolves one event to a list of keys, so it would be invented semantics, which decision 0001 rules out. +- **Keep `ZeroOrOne`/`Many` as reachable transition cardinalities.** Not taken: the validator accepts shapes nothing can execute. `ZeroOrOne` only corresponds to deferral, which is operational and not something a modeler writes. +- **Remove them from canonical bytes immediately.** Not taken: it changes existing bytes, which decision 0004 allows only in a new ESM version. + +## Default if unanswered + +The validator keeps accepting cardinalities the evaluator rejects, #132 keeps inviting a key-list syntax with no runtime, and Studio has no data to show which instances an event touches. + +## Timeline and scope + +Settle before any #132 syntax or Studio visualization work, and keep it until superseded. The diagnostic and the derived view ship first; byte removal ships with the next ESM version. + +In scope: the deprecation diagnostic, removal in the next ESM version, the derived affected-instance view, the reducer keying statement, and specs for the known differences. + +Out of scope: new projection syntax; Chronicle runtime changes (owned by Chronicle#4165); Studio's rendering of the view; reducers keyed other than by event source. + +## Verification + +**Done when:** A model whose transition carries `ZeroOrOne` or `Many` gets a deprecation diagnostic, and the next ESM version's golden vectors no longer contain them on transitions. The derived view returns the four shapes above for a model with `from`, `all`, a root join, a child join and a remove-via-join. A spec shows one child-join event updating children under two parents. + +**Verify by:** Validator and binder specs for the diagnostic; specs for the derived view per shape; the child-join spec against `SemanticScopedProjection`; golden-vector diff on the version bump, with a `Decision: 0004` and `Decision: 0007` trailer. + +## Consequences + +The ESM says only what Chronicle can do, and "many" is visible through the derived view without new syntax. Authors who expect a key list must model a join. The byte removal costs a version bump. The known differences remain open until Chronicle#4165 and the evaluator's deferral gap are resolved. + +## Related issues + +Screenplay: [#132](https://github.com/Cratis/Screenplay/issues/132), [#128](https://github.com/Cratis/Screenplay/issues/128). Chronicle: [#4165](https://github.com/Cratis/Chronicle/issues/4165). diff --git a/decisions/0008-one-data-subject-per-event.md b/decisions/0008-one-data-subject-per-event.md new file mode 100644 index 0000000..f821653 --- /dev/null +++ b/decisions/0008-one-data-subject-per-event.md @@ -0,0 +1,70 @@ +--- +id: 0008 +title: Personal data in an event belongs to one subject +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Syntax/EventSyntax.cs + - Source/DotNET/Screenplay/Parsing/EventParser.cs + - Source/DotNET/Screenplay/Parsing/PropertyLineParser.cs + - Source/DotNET/Screenplay/Parsing/ScreenplayValidator.cs + - Source/DotNET/Screenplay/Semantics/** + - Documentation/screenplay/events.md + - Documentation/screenplay/concepts.md +--- + +## Context + +[#141](https://github.com/Cratis/Screenplay/issues/141) asks for a way to say which identity a personal value is about, separate from PII classification, and illustrates it per property: `email EmailAddress about customerId`. Screenplay already classifies personal data on the concept (`@pii`, [`concepts.md`](../Documentation/screenplay/concepts.md), [`ConceptSyntax.cs:23-25`](../Source/DotNET/Screenplay/Syntax/ConceptSyntax.cs)), but nothing says whose data it is. The triage on #141 left three choices open: where the relationship attaches, the default when it is absent, and how strong the promise is. + +Under [decision 0001](0001-chronicle-runtime-semantic-authority.md), Chronicle defines the meaning. It has one subject per event: + +- `[Subject]` marks one property or record parameter of an event as its subject ([`SubjectAttribute.cs`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/SubjectAttribute.cs)). The resolver reads the first marked member ([`SubjectResolver.cs:17-25,51`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/SubjectResolver.cs)). +- An append carries one optional subject; when it is omitted, the event source id is the subject ([`IEventSequence.cs:113`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/EventSequences/IEventSequence.cs)). The subject selects the encryption key for PII. + +## Decision + +Personal data in an event belongs to exactly one subject, as in Chronicle. + +1. **Default.** An event's subject is its event source. +2. **Explicit subject.** A property of an event may be marked as the event's subject. The keyword is chosen by the implementing change; this record fixes what it attaches to and what it means. +3. **Mixed subjects fail.** An event that carries personal data (`@pii` values) of more than one subject fails compilation with a diagnostic, and the fix is to split the event. Marking more than one property as the subject is the first case the compiler detects. Any further detection it adds is stated in the implementing change and must not guess. +4. **Promise.** The relationship is lineage metadata only. The ESM records the subject for each event and preserves it into projections. It makes no erasure, export or redaction guarantee; any such capability needs its own record. + +## Options considered + +- **One subject per event, marked on a property (taken).** It is what Chronicle can realize: one subject selects one encryption key per event. +- **Per-property `about` (the issue's illustration).** Not taken: Chronicle cannot hold two subjects in one event, so a model could state a relationship no realization preserves. It would also change the shared property-line grammar for commands, events and types at once ([`PropertyLineParser.cs`](../Source/DotNET/Screenplay/Parsing/PropertyLineParser.cs)). +- **Subject on the concept.** Not taken: a concept such as `EmailAddress` is used for many subjects, so the concept cannot know whose value it holds. +- **No default, or a warning when absent.** Not taken: Chronicle defaults to the event source, and a warning on every event with personal data would be noise for the common case. +- **An ESM capability that blocks targets without erasure.** Not taken now: it needs a capability model the ESM does not have, and it would imply a guarantee beyond lineage. + +## Default if unanswered + +Models keep classifying personal data without saying whose it is. A realization defaults to the event source silently, and events that mix two people's data pass compilation and are encrypted under one key. + +## Timeline and scope + +Settle before any #141 syntax, and keep it until superseded. Admission to the ESM follows [decision 0004](0004-admission-and-governance-of-portable-executable-semantics.md). + +In scope: the event-level subject mark, the event-source default, the mixed-subject compile error, subject lineage in the ESM and into projections, and documentation. + +Out of scope: erasure, export and redaction guarantees; read-model subject marking for manual release; subjects on commands or types; changes to Chronicle. + +## Verification + +**Done when:** An event without a mark has its event source as subject in the ESM. An event with one marked property has that property as subject. Marking two properties fails compilation. A projection built from the event carries the originating subject in its lineage. `events.md` states the rule and the default, and says the relationship promises no erasure. + +**Verify by:** Parser, validator and binder specs for each case, a golden vector for the subject in canonical form, and a lineage spec through one projection. + +## Consequences + +The model states whose data an event holds in a form Chronicle can realize. Events that mix subjects must be split, which is sometimes more events than a modeler would write by instinct. The issue's per-property `about` is closed off unless Chronicle gains per-property subjects. Anyone reading the relationship as an erasure guarantee is wrong by design; that promise needs its own record. + +## Related issues + +Screenplay: [#141](https://github.com/Cratis/Screenplay/issues/141), [#128](https://github.com/Cratis/Screenplay/issues/128). Decisions: [0001](0001-chronicle-runtime-semantic-authority.md), [0004](0004-admission-and-governance-of-portable-executable-semantics.md). diff --git a/decisions/0009-external-event-origin-and-translation-slices.md b/decisions/0009-external-event-origin-and-translation-slices.md new file mode 100644 index 0000000..5853967 --- /dev/null +++ b/decisions/0009-external-event-origin-and-translation-slices.md @@ -0,0 +1,69 @@ +--- +id: 0009 +title: External events declare their origin; translating them is a Translate slice +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Syntax/EventSyntax.cs + - Source/DotNET/Screenplay/Syntax/SliceSyntax.cs + - Source/DotNET/Screenplay/Parsing/EventParser.cs + - Source/DotNET/Screenplay/Parsing/ScreenplayParser.cs + - Source/DotNET/Screenplay/Semantics/SemanticModel.cs + - Source/DotNET/Screenplay/Semantics/SemanticModelBinder.SliceMembers.cs + - Documentation/screenplay/events.md + - Documentation/screenplay/slices.md + - Documentation/screenplay/reactions.md +--- + +## Context + +[#73](https://github.com/Cratis/Screenplay/issues/73) asks how a model states that an occurrence comes from another application, and how it becomes a local fact, without observer, sequence, subscription or transport vocabulary. Its criteria require that the source occurrence and the translated local fact stay distinct. The triage left three choices open: where the origin lives, whether the source name is resolved or opaque, and whether translation is a construct of its own or a reaction that produces. + +A projection can already name a `sequence` ([`projections/grammar.md:39`](../Documentation/screenplay/projections/grammar.md)); a reaction trigger cannot say where its events come from ([`ReactionSyntax.cs:44-52`](../Source/DotNET/Screenplay/Syntax/ReactionSyntax.cs)). `import ` already says an event type is declared elsewhere ([`grammar.md:22`](../Documentation/screenplay/grammar.md)). The language has a `Translate` slice type, today documented for captures that convert external data into events ([`SliceSyntax.cs:31-34`](../Source/DotNET/Screenplay/Syntax/SliceSyntax.cs), [`slices.md`](../Documentation/screenplay/slices.md)). The executable semantic model (ESM) admits only `StateChange` and `StateView` slices ([`SemanticModel.cs:11-27`](../Source/DotNET/Screenplay/Semantics/SemanticModel.cs)), and reactions and captures fail binding ([`SemanticModelBinder.SliceMembers.cs:77-99`](../Source/DotNET/Screenplay/Semantics/SemanticModelBinder.SliceMembers.cs)). + +Under [decision 0001](0001-chronicle-runtime-semantic-authority.md), Chronicle defines the meaning. It puts origin on the event type: `[EventStore("x")]` names the event store an event type originates from ([`EventStoreAttribute.cs`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Events/EventStoreAttribute.cs)). A reactor whose handled events all come from one store subscribes to that store's inbox without naming a sequence ([`ReactorAttribute.cs:24-29`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Reactors/ReactorAttribute.cs)). + +## Decision + +1. **Origin is on the event.** An event declaration, or the `import` that brings it in, may state its origin as an opaque store name. Every reaction or projection that consumes the event inherits the origin; none of them names a sequence or inbox. The keyword is chosen by the implementing change; this record fixes where it attaches and what it holds. +2. **The name is opaque.** Screenplay does not resolve the store name against other applications. It is carried as given, the way Chronicle carries `[EventStore("x")]`. +3. **Translation is a `Translate` slice.** Turning an external event into a local fact is declared in a slice of type `Translate`, not in an `Automation` slice. The slice kind is what keeps the source occurrence and the local fact distinct. The body reuses existing trigger and `produces` mapping forms where they fit; the implementing change states the exact form. + +## Options considered + +- **Origin on the event or import (taken).** It mirrors Chronicle, states the origin once where the name lives, and composes with `import`. +- **Origin on the reaction trigger.** Not taken: it recouples each reaction to where its events come from, which the trigger design deliberately avoids ([`triggers.md`](../Documentation/screenplay/triggers.md)), and it has to be repeated on every consumer. +- **Origin on a declared `trigger`.** This was the triage's first recommendation. Not taken: Chronicle attaches origin to the event type, and a trigger is not an event type. +- **First-class "other application" declarations.** Not taken: they make a document depend on how another application is deployed, which #73's non-goals exclude. +- **A resolved store name.** Not taken: it needs cross-application knowledge the compiler does not have. +- **Translation as an ordinary reaction that produces.** Not taken: in the syntax tree and the ESM it would look like any other automation, so criterion 2 (source and local fact stay distinct) could not be checked. The maintainer ruled it is a translation slice. + +## Default if unanswered + +Cross-application reactions read as though they observed the local log, the triage's trigger-origin option stays on the table against Chronicle's model, and Stage keeps failing closed on Translation slices because the ESM has no contract for them (Cratis/Stage#79). + +## Timeline and scope + +Settle before any #73 syntax or ESM work, and keep it until superseded. Admission to the ESM, including a Translation slice kind, follows [decision 0004](0004-admission-and-governance-of-portable-executable-semantics.md). + +In scope: the origin clause on events and imports, the opaque store name, and Translation slices as the home of external-to-local translation. + +Out of scope: the exact origin keyword and translation body syntax (left to the implementing change within the rules above); the outbox side (publishing to another application); cross-application identity and correlation typing; delivery topology, which a realization profile supplies; captures, which keep their existing CDL form. + +## Verification + +**Done when:** An event declaration and an `import` can each carry an origin store name, and the syntax tree and printer preserve it. A translation from an external event to a local fact is declared in a `Translate` slice, and the syntax tree keeps the source event and the produced local fact apart. `events.md` and `slices.md` describe origin and Translation slices without framework vocabulary. + +**Verify by:** Parser, printer and validator specs for each case. When the Translation slice kind is admitted to the ESM, a golden vector and a reference-execution vector show the source occurrence and the local fact as distinct entries. + +## Consequences + +Origin is stated once and inherited, as in Chronicle, and a renderer can derive the inbox subscription without the model naming it. Translation gets its own slice kind, which gives Studio and AI a visible boundary between another application's facts and local facts. An opaque name cannot fail closed on a misspelled store; that check belongs to the realization. The outbox direction and cross-application correlation remain open. + +## Related issues + +Screenplay: [#73](https://github.com/Cratis/Screenplay/issues/73), [#69](https://github.com/Cratis/Screenplay/issues/69), [#128](https://github.com/Cratis/Screenplay/issues/128). Stage: [#79](https://github.com/Cratis/Stage/issues/79). diff --git a/decisions/0010-query-paging-ordering-and-live-delivery.md b/decisions/0010-query-paging-ordering-and-live-delivery.md new file mode 100644 index 0000000..daa16e2 --- /dev/null +++ b/decisions/0010-query-paging-ordering-and-live-delivery.md @@ -0,0 +1,78 @@ +--- +id: 0010 +title: "Queries: page-number paging, one sort field, change-set live delivery, unordered by default" +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Syntax/QuerySyntax.cs + - Source/DotNET/Screenplay/Parsing/QueryParser.cs + - Source/DotNET/Screenplay/Semantics/SemanticModelBinder.ReadModels.cs + - Source/DotNET/Screenplay/Semantics/SemanticBehaviors.cs + - Source/DotNET/Screenplay/Semantics/Execution/** + - Source/DotNET/Screenplay/Semantics/Serialization/** + - Documentation/screenplay/queries.md + - Documentation/screenplay/specifications.md +--- + +## Context + +[#140](https://github.com/Cratis/Screenplay/issues/140) asks for queries as executable portable contracts: selection, ordering, paging, live delivery and query specifications. The triage left three choices open: ordering syntax and default determinism, the paging model, and what `live` promises. + +Today the executable semantic model (ESM) admits one query shape: one caller-supplied `by` argument returning one optional read model. Observable queries, filters, scopes and performers fail binding, and so do collection results ([`SemanticModelBinder.ReadModels.cs:63-77`](../Source/DotNET/Screenplay/Semantics/SemanticModelBinder.ReadModels.cs)). `SemanticQueryDelivery.Live` exists but is unreachable ([`SemanticBehaviors.cs:152`](../Source/DotNET/Screenplay/Semantics/SemanticBehaviors.cs)). Specifications compare repeated `result` entries in authored order ([`specifications.md`, "Query results"](../Documentation/screenplay/specifications.md#query-results)), while nothing in a query can declare an order. For events, `then events in any order` already compares without order. + +Under [decision 0001](0001-chronicle-runtime-semantic-authority.md) the runtime defines the meaning. For queries the runtime is Arc: + +- Paging is a page number and a page size ([`Paging.cs`](https://github.com/Cratis/Arc/blob/main/Source/DotNET/Arc.Core/Queries/Paging.cs)). Arc has no cursor paging. +- Sorting is one field and one direction ([`Sorting.cs`](https://github.com/Cratis/Arc/blob/main/Source/DotNET/Arc.Core/Queries/Sorting.cs)). +- An observable collection query delivers change sets of added, replaced and removed items between snapshots ([`ChangeSetComputor.cs:10-27`](https://github.com/Cratis/Arc/blob/main/Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs)). + +## Decision + +Queries mirror Arc. + +1. **Paging.** A query that pages takes a page number and a page size. +2. **Sorting.** The caller may select one sort field and a direction. +3. **Live.** A live query delivers the initial current answer, then change sets (added, replaced, removed) as the answer changes. +4. **Result comparison.** A specification compares a collection result without regard to order, unless the query declares `order by`. This is the query counterpart of `then events in any order`. +5. **First increment.** Collection queries with equality filters are admitted first. Ordering, paging and live delivery follow in later increments, under this record. + +## Options considered + +- **Mirror Arc (taken).** Every construct has a runtime counterpart that Stage can render onto. +- **Cursor paging.** Deterministic with a stable sort key, but not taken: Arc has no cursor paging, so the model would promise something the target cannot realize. +- **Ordering on the projection, or left to realization profiles.** Not taken: the order is part of what the caller observes, so it belongs on the query contract. +- **Compare results in authored order by default.** Not taken for collection results: it asserts an order no declaration guarantees, and a target that returns the same set in another order would fail. +- **Whole-result replacement or per-instance notifications for `live`.** Not taken: Arc delivers change sets, and a different promise would diverge from the runtime. +- **Never admit `live`.** Not taken: #140's criterion for live queries could then never be met. + +## Default if unanswered + +Every query beyond single-by-key stays outside the ESM, specifications keep asserting an undeclared order, and `live` stays unreachable. Stage's portable query rendering stays blocked on a contract it can render without inventing broader queries (Cratis/Stage#58). + +## Timeline and scope + +Settle before any #140 work, and keep it until superseded. Each increment is admitted under [decision 0004](0004-admission-and-governance-of-portable-executable-semantics.md), starting with collection queries with equality filters. + +In scope: equality-filter selection and its lowering from existing `by` and same-name filters; collection results and unordered comparison; `order by`; caller sorting by one field; page-number paging; live change-set delivery; query specifications for absence, single, ordered collection, page and live update. + +Out of scope: cursor paging; multi-field caller sorting; non-equality predicates; tie-breaking rules and how a specification asserts a page of an unordered query, which are settled with the paging increment; query performers, which wait on this record and [decision 0012](0012-typed-context-descriptor-and-command-handler-role.md); SQL, LINQ or transport syntax. + +## Verification + +**Done when (first increment):** A query returning a collection with equality filters binds to the ESM, and the reference evaluator returns every matching instance. A specification that lists the matching results in any order passes; one that lists a wrong or missing result fails. `queries.md` and `specifications.md` state the unordered comparison rule. + +**Done when (whole record):** `order by`, caller sorting, paging and live delivery are each admitted with golden vectors and reference execution, and a specification with `order by` fails when results arrive in another order. + +**Verify by:** Binder and evaluator specs per shape, specification-runner specs for ordered and unordered comparison, and golden vectors for each admitted form. + +## Consequences + +Query contracts match what Arc runs, so renderers realize them without invention. Specifications written against collection queries become order-independent unless the author declares an order, which removes a class of false failures and makes order an explicit decision. Cursor paging is closed off unless Arc gains it. + +## Related issues + +Screenplay: [#140](https://github.com/Cratis/Screenplay/issues/140), [#128](https://github.com/Cratis/Screenplay/issues/128). Stage: [#58](https://github.com/Cratis/Stage/issues/58). diff --git a/decisions/0011-event-generations.md b/decisions/0011-event-generations.md new file mode 100644 index 0000000..13d1ae7 --- /dev/null +++ b/decisions/0011-event-generations.md @@ -0,0 +1,74 @@ +--- +id: 0011 +title: Event generations are declared in full, each succeeding the one before +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Syntax/EventSyntax.cs + - Source/DotNET/Screenplay/Parsing/EventParser.cs + - Source/DotNET/Screenplay/Parsing/ScreenplayValidator.cs + - Source/DotNET/Screenplay/Semantics/ExecutableSemanticModel.cs + - Source/DotNET/Screenplay/Semantics/SemanticIdentityCatalog.cs + - Source/DotNET/Screenplay/Semantics/Serialization/** + - Source/DotNET/Screenplay.CanonicalCorpus/** + - Documentation/screenplay/events.md +--- + +## Context + +[#71](https://github.com/Cratis/Screenplay/issues/71) asks for portable event evolution: immutable generations, predecessor lineage and deterministic transformation. Identity already survives renames and moves, but nothing links a revision to its predecessor. The executable semantic model (ESM) rejects any event contract revision other than the initial one ([`ExecutableSemanticModel.cs:297-300`](../Source/DotNET/Screenplay/Semantics/ExecutableSemanticModel.cs), [`SemanticIdentityCatalog.cs:594-598`](../Source/DotNET/Screenplay/Semantics/SemanticIdentityCatalog.cs)). + +The open question in #71 is how a document names the prior shape, since today it declares only the current one. The sweep comment offered three answers: declare prior generations in full, declare only the properties a migration touches, or leave the source untyped. + +[#168](https://github.com/Cratis/Screenplay/issues/168) waits on this. Its v2 criterion needs `ProjectRegistered` to advance its revision and drop `projectId` from the payload, but the v2 vector still declares `projectId` ([`RegisterProject.play:15-17`](../Source/DotNET/Screenplay.CanonicalCorpus/Corpus/RegisterProject/v2/source/RegisterProject.play)) at contract revision 1. + +Under [decision 0001](0001-chronicle-runtime-semantic-authority.md), Chronicle defines the meaning: + +- A prior generation is a full type, marked with `[EventTypeGenerationFor(N)]` ([`EventTypeGenerationForAttribute.cs`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Events/EventTypeGenerationForAttribute.cs)), and every generation resolves to the same event type id ([`MigrationGenerationsMustShareEventTypeId.cs`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Events/Migrations/MigrationGenerationsMustShareEventTypeId.cs)). +- A migration must go from generation N−1 to N; a gap throws ([`InvalidMigrationGenerationGap.cs`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Events/Migrations/InvalidMigrationGenerationGap.cs), raised in [`EventTypeMigration.cs:44`](https://github.com/Cratis/Chronicle/blob/main/Source/Clients/DotNET/Events/Migrations/EventTypeMigration.cs)). + +## Decision + +Event generations mirror Chronicle. + +1. **Marker.** `event X generation N` declares generation N of event X. Without the marker, a declaration is generation 1, so existing documents are unchanged. +2. **Prior generations in full.** Each earlier generation is declared as a complete event declaration with its own generation number. All generations of X share one event contract identity. +3. **Predecessor.** The predecessor of generation N is always N−1. Generations are consecutive from 1; a gap or a duplicate number is a compile error. +4. **ESM.** A generation maps to the event contract revision, and each revision records its predecessor. Admission follows [decision 0004](0004-admission-and-governance-of-portable-executable-semantics.md). + +## Options considered + +- **Prior generations in full with a marker (taken).** It matches Chronicle, where every generation is a real type, and the document shows exactly what can be replayed. +- **Diff-style declarations (only what changed, or only what a migration touches).** Not taken: the prior shape could not be read on its own, and it has no Chronicle counterpart, which keeps each generation a full type. +- **Marker only, with no prior shape.** Not taken: a migration would map from a shape nobody wrote down, which #71 calls "a comment with syntax". +- **Explicit predecessor numbers that may skip.** Not taken: Chronicle rejects migrations that are not consecutive. + +## Default if unanswered + +Documents cannot say an event has evolved, the ESM keeps rejecting non-initial revisions, and #168's v2 vector cannot advance its revision or drop `projectId` without a silent change that keeps the initial revision, which #71 rules out. + +## Timeline and scope + +Settle before any generation syntax, and keep it until superseded. The marker and full prior declarations ship first. This unblocks the representation #168's v2 vector needs: `ProjectRegistered` generation 1 with `projectId` and generation 2 without it, at revision 2 with revision 1 as predecessor. + +In scope: the `generation` marker, full prior declarations, the consecutive-numbering rule, shared contract identity across generations, predecessor lineage in the ESM, and lifting the initial-revision guards. + +Out of scope: the transformation (migration) construct that maps generation N−1 to N, which stays a design question under #71; tombstone and compensation markers; changes to Chronicle. + +## Verification + +**Done when:** `event X generation 2` parses, prints and round-trips, and an event without the marker prints unchanged. A document that declares generations 1 and 3 of X without 2, or two generation-2 declarations, fails compilation. All generations of X share one event contract id in the ESM, and revision N names N−1 as predecessor. The ESM no longer rejects non-initial revisions that satisfy these rules. + +**Verify by:** Parser, printer and validator specs for each case; identity-catalog and ESM specs for shared identity and predecessor lineage; a golden vector for a two-generation event; the #168 v2 corpus vector, once updated, pinning revision 2 without `projectId`. + +## Consequences + +Evolution becomes visible and checkable in the document, and matches what a Chronicle renderer emits. A document that has evolved an event five times carries five shapes of it, which is heavy but true. The migration construct still needs its own design; this record gives it typed shapes to map between. + +## Related issues + +Screenplay: [#71](https://github.com/Cratis/Screenplay/issues/71), [#168](https://github.com/Cratis/Screenplay/issues/168), [#167](https://github.com/Cratis/Screenplay/issues/167), [#128](https://github.com/Cratis/Screenplay/issues/128). diff --git a/decisions/0012-typed-context-descriptor-and-command-handler-role.md b/decisions/0012-typed-context-descriptor-and-command-handler-role.md new file mode 100644 index 0000000..66c8eec --- /dev/null +++ b/decisions/0012-typed-context-descriptor-and-command-handler-role.md @@ -0,0 +1,69 @@ +--- +id: 0012 +title: A language-neutral typed-context descriptor, then command handlers as the next role +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Contexts/** + - Source/DotNET/Screenplay/Semantics/SemanticImplementationRequirement.cs + - Source/DotNET/Screenplay/Semantics/SemanticModelBinder.Implementations.cs + - Source/DotNET/Screenplay/Semantics/Serialization/** + - Source/DotNET/Screenplay.Mcp/** + - Documentation/screenplay/context.md +--- + +## Context + +[Decision 0002](0002-implementation-attachments-envelope-and-reducer-role.md) delegated editing support to the host editor and said Screenplay emits wrapper and source-map data only. Exact source maps shipped in v4.30.0 ([#252](https://github.com/Cratis/Screenplay/pull/252)). The wrapper data has no defined shape yet: an editor or renderer cannot learn, from Screenplay, what an implementation body can see and with which types. + +The code contexts are still `dynamic` where they carry model data: `CommandContext.Command`, `QueryContext.Arguments`, `RuleContext.Artifact` and `Value`, `PolicyContext.Artifact`, `ReducerContext.State` and `Event` ([`CommandContext.cs:25`](../Source/DotNET/Screenplay/Contexts/CommandContext.cs), [`QueryContext.cs:26`](../Source/DotNET/Screenplay/Contexts/QueryContext.cs), [`RuleContext.cs:38-39`](../Source/DotNET/Screenplay/Contexts/RuleContext.cs), [`PolicyContext.cs:33`](../Source/DotNET/Screenplay/Contexts/PolicyContext.cs), [`ReducerContext.cs:29-30`](../Source/DotNET/Screenplay/Contexts/ReducerContext.cs)). v4.21.0 added typed accessors beside them (option 2 of [#84](https://github.com/Cratis/Screenplay/issues/84)), but the consumer names the type. Option 1, a context typed per artifact, is what #84 still asks for. + +Screenplay already has one precedent for publishing a catalog as data: `EventContextCatalog` restates Chronicle's event context once, and specs hold the editor surfaces and documentation to it ([`EventContextCatalog.cs`](../Source/DotNET/Screenplay/Syntax/EventContextCatalog.cs), `Syntax/for_EventContextCatalog`). + +[#139](https://github.com/Cratis/Screenplay/issues/139) lists later roles: command handlers, query performers and reaction effects. Policy predicates were the third role, shipped in v4.29.0 ([decision 0005](0005-policy-predicates-as-an-implementation-attachment-role.md)). + +## Decision + +1. **Descriptor.** For each implementation role and each artifact that needs code, Screenplay publishes a language-neutral typed-context descriptor: the members the body can see, their types, and the model property each member comes from. It is data, versioned with the role's context contract, and pinned by shared vectors like the event-context catalog. +2. **Language providers generate wrappers.** A language provider turns the descriptor into typed code; for C#, that is Stage ([Cratis/Stage#151](https://github.com/Cratis/Stage/issues/151)). Screenplay stays free of Roslyn, as decision 0002 requires. +3. **#84.** Option 1 of #84 is realized as this descriptor plus Stage's generation. +4. **Next role.** Command handlers are the next implementation role after policy predicates. They are implemented after the descriptor, because a handler's context is the richest and is `dynamic` today. +5. **Later roles.** Query performers wait on [decision 0010](0010-query-paging-ordering-and-live-delivery.md). Reaction effects wait on [decision 0006](0006-reaction-triggers-declare-reads.md), [decision 0009](0009-external-event-origin-and-translation-slices.md), and the deferred due-time work. + +## Options considered + +- **Language-neutral descriptor (taken).** One shape serves every language provider and every editor, and Screenplay keeps no compiler dependency. +- **Wrapper text per language from Screenplay.** Not taken: Screenplay would have to know C# and every later language, and decision 0002 already keeps language services in the host. +- **Keep typed accessors only.** Not taken as the answer: the consumer still names the type, so completion has nothing the document declares to work from. +- **Next role: query performer.** Not taken: its contract depends on selection, paging and live delivery (decision 0010). +- **Next role: reaction effect.** Not taken: reactions do not bind in the executable semantic model, and their occurrence and effect semantics are still being settled (decisions 0006 and 0009). + +## Default if unanswered + +Hosts keep seeing `dynamic` contexts, each renderer or editor derives its own idea of what a body can see, and command handlers stay unbound with nothing to type their context against. + +## Timeline and scope + +Settle before the command-handler role or any wrapper generation in Stage, and keep it until superseded. Order: the descriptor, then the command-handler role. + +In scope: the descriptor shape and its vectors, one descriptor per role for the roles already bound (reducer transitions, rule predicates, policy predicates) and for command handlers, exposure through the compilation result and MCP, and the role order. + +Out of scope: wrapper generation in any language (owned by providers, Stage#151 for C#); language-service hosting (decision 0002); removing the `dynamic` members; the command-handler contract beyond its context, which gets its own record if it changes the envelope. + +## Verification + +**Done when:** Every bound implementation requirement exposes a typed-context descriptor listing each member, its type and its model source. A shared vector pins the descriptor for a command, a rule, a policy and a reducer, and a spec fails when the context contract changes without the vector. Command handlers then bind as the next role with their descriptor. + +**Verify by:** Descriptor specs per role, vector specs in the style of `for_EventContextCatalog`, an MCP view spec, and Stage#151 generating a typed C# context from the vector. + +## Consequences + +Editors and renderers get one source of truth for what a body can see, and #84's option 1 becomes possible without Screenplay learning C#. Completion for inline code becomes possible in hosts. Query performers and reaction effects are explicitly sequenced behind other records, so they will not ship first. + +## Related issues + +Screenplay: [#139](https://github.com/Cratis/Screenplay/issues/139), [#84](https://github.com/Cratis/Screenplay/issues/84), [#65](https://github.com/Cratis/Screenplay/issues/65), [#252](https://github.com/Cratis/Screenplay/pull/252). Stage: [#151](https://github.com/Cratis/Stage/issues/151), [#119](https://github.com/Cratis/Stage/issues/119). diff --git a/decisions/0013-equivalence-for-screenplay-code-round-trips.md b/decisions/0013-equivalence-for-screenplay-code-round-trips.md new file mode 100644 index 0000000..19b3f9d --- /dev/null +++ b/decisions/0013-equivalence-for-screenplay-code-round-trips.md @@ -0,0 +1,66 @@ +--- +id: 0013 +title: What "equivalent" means for Screenplay and code round trips +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay.CanonicalCorpus/** + - Source/DotNET/Screenplay.CanonicalVectors.Specs/** + - Documentation/screenplay/interoperability.md +--- + +## Context + +[#148](https://github.com/Cratis/Screenplay/issues/148) asks Screenplay to work in both directions: Screenplay to code, and code back to a reviewed Screenplay proposal, meeting at the same portable meaning. Its criteria ask that the recovered snapshot is "equivalent" to the rendered one, without saying what equivalent means. Since it was filed, [#168](https://github.com/Cratis/Screenplay/issues/168) took over the shared corpus vectors and [#167](https://github.com/Cratis/Screenplay/issues/167) the stream-context fix. The triage left two questions: does #148 still own anything, and what is equivalence. + +Today the corpus pins a semantic revision and a hand-listed set of identities ([`when_loading_the_legacy_v1_source.cs:42-56`](../Source/DotNET/Screenplay.CanonicalVectors.Specs/for_RegisterProjectCorpus/when_loading_the_legacy_v1_source.cs)). The corpus contracts model no realization loss ([`CanonicalCorpusContracts.cs`](../Source/DotNET/Screenplay.CanonicalCorpus/CanonicalCorpusContracts.cs)). Revision equality alone breaks as soon as realization detail or implementation attachments enter the model. + +A round trip cannot always be lossless. Complex automations, state changes with many moving parts, and code bodies carry more than a declarative model can recover from code. + +## Decision + +Two models are equivalent when they have the same set of semantic identities and the same normalized specification outcomes. Revision equality alone is not the test. Equivalence is defined per part: + +1. **Declarative constructs** round-trip exactly: same identities, same canonical meaning. +2. **Opaque implementation attachments** ([decision 0002](0002-implementation-attachments-envelope-and-reducer-role.md)) are compared by requirement identity and content hash, not by source text or structure. +3. **What a realization cannot recover** is reported as an explicit, classified loss. A loss is neither a failure nor a silent pass. +4. **Determinism.** Everything that is compared must be deterministic: the same inputs produce the same identities, hashes, outcomes and loss report. + +#148 stays open, scoped to defining and testing this predicate. The recovery side's consumers are #168 and Screenplay.Generation. + +## Options considered + +- **Identity set plus normalized outcomes, per part (taken).** It survives realization metadata, treats code as code, and makes loss visible instead of hiding it. +- **Semantic revision equality.** Not taken: it fails as soon as anything outside the portable meaning changes, and it says nothing about which part differs. +- **Lossless round trips for everything.** Not taken: code bodies and complex automations cannot be decompiled into declarative meaning, and #148's non-goals exclude inventing business meaning from code. +- **Close #148 as superseded by #168.** Not taken: #168 owns vectors and identities, but nothing else defines the comparison they are checked with. + +## Default if unanswered + +Each consumer invents its own comparison, most likely revision equality, which breaks on the first realization detail. Loss is either reported as failure, which blocks every round trip that touches code, or ignored, which lets a lossy round trip pass. + +## Timeline and scope + +Settle before any render-and-recover vector asserts equivalence, and keep it until superseded. + +In scope: the equivalence predicate, the per-part rules, the loss classification and its determinism, and specs that test the predicate on corpus vectors. + +Out of scope: the corpus package and identity catalog (#168); recovery adapters and reports (Screenplay.Generation); byte-identical `.play` formatting; recovering business meaning from hand-written code. + +## Verification + +**Done when:** A predicate over two models returns equivalent, not equivalent with the differing identities or outcomes, or equivalent with a classified loss list. Specs show it treats a changed declarative construct as a difference, a changed attachment hash as a difference, a realization-only change as no difference, and an unrecoverable part as a classified loss. Running it twice on the same inputs gives the same result. + +**Verify by:** Specs for the predicate in `Screenplay.CanonicalVectors.Specs` against the RegisterProject corpus, including one vector with an implementation attachment, and a check that #148's body cites this record. + +## Consequences + +Round-trip vectors have one definition to assert, and consumers stop comparing revisions. Losses become visible and classified, so a round trip through code can succeed honestly. The loss classification is new contract surface that consumers must learn to read. + +## Related issues + +Screenplay: [#148](https://github.com/Cratis/Screenplay/issues/148), [#168](https://github.com/Cratis/Screenplay/issues/168), [#167](https://github.com/Cratis/Screenplay/issues/167), [#139](https://github.com/Cratis/Screenplay/issues/139), [#128](https://github.com/Cratis/Screenplay/issues/128). diff --git a/decisions/0014-diagnostic-repairs-are-typed-workspace-proposals.md b/decisions/0014-diagnostic-repairs-are-typed-workspace-proposals.md new file mode 100644 index 0000000..e50c9c8 --- /dev/null +++ b/decisions/0014-diagnostic-repairs-are-typed-workspace-proposals.md @@ -0,0 +1,62 @@ +--- +id: 0014 +title: Diagnostic repairs are typed workspace proposals +status: accepted +stage: none +decided: 2026-09-24 +decider: Sindre Alstad Wilting +class: contract +reversibility: costly +applies-to: + - Source/DotNET/Screenplay/Workspaces/** + - Source/DotNET/Screenplay/Diagnostics/** + - Source/DotNET/Screenplay.Mcp/** + - Documentation/screenplay/ast-authoring.md + - Documentation/screenplay/workspace-transport.md +--- + +## Context + +[#138](https://github.com/Cratis/Screenplay/issues/138) asks for revision-checked semantic patches that humans and AI apply through one transaction contract. Two of its criteria are open. Criterion 2 lists typed `link` operations without saying what a link is. Criterion 4 asks for compiler-authored repair templates that use diagnostic codes and node ids rather than parsing messages. No repair or code-action type exists in `Source/DotNET` today. + +The transaction contract exists. A workspace authoring request carries an expected workspace and catalog revision and is refused when either is stale ([`WorkspaceAuthoringTransaction.cs:23-30`](../Source/DotNET/Screenplay/Workspaces/WorkspaceAuthoringTransaction.cs)). Its operations are typed: `AddWorkspaceNode`, `ReplaceWorkspaceNode`, `RemoveWorkspaceNode` and `MoveWorkspaceNode`, each addressed by a `WorkspaceNodeHandle` and checked against the node expected in the base snapshot ([`WorkspaceAuthoring.cs:56-101`](../Source/DotNET/Screenplay/Workspaces/WorkspaceAuthoring.cs)). `ProposeAuthoring` returns a verdict and a write plan without writing anything ([`ScreenplayWorkspace.cs:147-154`](../Source/DotNET/Screenplay/Workspaces/ScreenplayWorkspace.cs)), as `Propose` does for document transactions (lines 139-145). New references are validated by the authoring reference policy, which never silently retargets an existing binding ([`WorkspaceAuthoringReferencePolicy.cs`](../Source/DotNET/Screenplay/Workspaces/WorkspaceAuthoringReferencePolicy.cs)). + +## Decision + +1. **A repair is a proposal.** A compiler-authored repair is a typed workspace AST operation proposal: one or more `WorkspaceAstOperation`s, keyed by the diagnostic code and the node handle the diagnostic is about. +2. **One contract.** A repair is reviewed and applied through the same revision-checked transaction contract as every other edit. It is previewed as a proposal and written only when its write plan is explicitly accepted, whether a person or an AI asked for it. +3. **No text edits.** Raw text edits are rejected as a repair form. +4. **Link.** "Link" is covered by adding nodes whose references are validated when the candidate compiles. It is not a separate operation. + +## Options considered + +- **Typed AST operation proposals (taken).** They reuse the one transaction contract, get stale-revision and expected-node checks for free, and meet criterion 4 because they are keyed by code and handle, not by message text. +- **Text edits (LSP-style).** Not taken: they bypass the typed contract, can go stale without detection, and would give humans and AI a second edit path, which criterion 5 rules out. +- **A dedicated `link` operation.** Not taken: a link is a reference inside a node, and adding the node already validates the reference on compile. A second operation would duplicate that check. +- **Repairs applied automatically by the compiler.** Not taken: #138 requires preview without applying, and a repair can be wrong. + +## Default if unanswered + +Hosts write their own quick fixes as text edits keyed by message text, which breaks when a message changes and bypasses revision checks. `link` stays an undefined criterion, so #138 cannot close. + +## Timeline and scope + +Settle before any repair template ships, and keep it until superseded. + +In scope: the repair shape (diagnostic code, node handle, typed operations), preview and apply through the existing contract, exposure over MCP, and the meaning of `link` in #138. + +Out of scope: which diagnostics get repairs first; new workspace operation kinds beyond those a repair needs; editor UI for presenting repairs; a `specification` operation, which #138 tracks separately. + +## Verification + +**Done when:** At least one diagnostic produces a repair that carries its diagnostic code, a node handle and typed operations. Proposing it against the current revision returns a candidate that compiles without that diagnostic. Proposing it against a stale revision returns a typed stale conflict with no partial change. No repair is expressed as a text edit. #138's `link` criterion cites this record. + +**Verify by:** Workspace specs for a repair's shape, its preview, its stale-revision refusal and its apply; an MCP spec that returns the repair for a diagnostic; a reference-validation spec showing an added node with an unresolved reference is refused under the rejecting policy. + +## Consequences + +Repairs are safe by construction: they can go stale only detectably, and humans and AI apply them the same way. Hosts that want quick fixes consume typed proposals instead of writing their own. Diagnostics that want a repair must carry a node handle, which some diagnostics may not have today. + +## Related issues + +Screenplay: [#138](https://github.com/Cratis/Screenplay/issues/138), [#174](https://github.com/Cratis/Screenplay/issues/174), [#128](https://github.com/Cratis/Screenplay/issues/128). diff --git a/decisions/README.md b/decisions/README.md index f1078b7..c32bc92 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -7,3 +7,12 @@ | [0003](0003-decision-consistency-for-command-reads.md) | Decision consistency for command reads | accepted | none | 2026-09-24 | Sindre Alstad Wilting | | [0004](0004-admission-and-governance-of-portable-executable-semantics.md) | Admission and governance of portable executable semantics | accepted | none | 2026-09-24 | Sindre Alstad Wilting | | [0005](0005-policy-predicates-as-an-implementation-attachment-role.md) | Policy predicates as an implementation attachment role, composed in authored order | accepted | implemented | 2026-09-24 | Sindre Alstad Wilting | +| [0006](0006-reaction-triggers-declare-reads.md) | Reaction triggers declare the views they decide from with reads | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0007](0007-affected-read-model-instances.md) | Which read-model instances an event affects follows Chronicle's keys and joins | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0008](0008-one-data-subject-per-event.md) | Personal data in an event belongs to one subject | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0009](0009-external-event-origin-and-translation-slices.md) | External events declare their origin; translating them is a Translate slice | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0010](0010-query-paging-ordering-and-live-delivery.md) | Queries: page-number paging, one sort field, change-set live delivery, unordered by default | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0011](0011-event-generations.md) | Event generations are declared in full, each succeeding the one before | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0012](0012-typed-context-descriptor-and-command-handler-role.md) | A language-neutral typed-context descriptor, then command handlers as the next role | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0013](0013-equivalence-for-screenplay-code-round-trips.md) | What "equivalent" means for Screenplay and code round trips | accepted | none | 2026-09-24 | Sindre Alstad Wilting | +| [0014](0014-diagnostic-repairs-are-typed-workspace-proposals.md) | Diagnostic repairs are typed workspace proposals | accepted | none | 2026-09-24 | Sindre Alstad Wilting |