Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e1700b6
WIP: preserve incomplete action subject implementation before S0 repa…
woksin Oct 24, 2026
389a7f5
WIP: continue action subject inference, fixtures and authoring (#1052)
woksin Oct 24, 2026
60fb48f
Complete action-subject conformance, repairs and editor assistance
woksin Oct 24, 2026
091d96f
Merge remote-tracking branch 'origin/main' into screenplay-1052-actio…
woksin Oct 24, 2026
4fdc9c8
Align toolbar action subjects across parsers and keep Code Health (Cr…
woksin Oct 24, 2026
bb4c723
Merge origin/main into action subject repairs with complete Monaco di…
woksin Oct 25, 2026
91ff9c5
Merge remote-tracking branch 'origin/main' into screenplay-1052-actio…
woksin Oct 25, 2026
672081e
Merge remote-tracking branch 'origin/main' into screenplay-1052-actio…
woksin Oct 25, 2026
362be53
Pin the 0110 action subject advisories separately from the frozen v1 …
woksin Oct 25, 2026
d7ea663
Renumber the action subject diagnostics to PLAY0817-PLAY0821 after ma…
woksin Oct 25, 2026
2a99cc7
Merge remote-tracking branch 'origin/main' into screenplay-1052-actio…
woksin Oct 25, 2026
28da1fa
Regenerate native-owned writer collection contracts after merging inv…
woksin Oct 25, 2026
dd4e97d
Merge commit '28da1fa22e3a0bf81ae916aa40642161ab23b6e6' into screenpl…
woksin Oct 25, 2026
4043c60
Merge remote-tracking branch 'origin/main' into screenplay-train-1052b
woksin Oct 25, 2026
40dce42
Merge remote-tracking branch 'origin/main' into screenplay-1052-actio…
woksin Oct 25, 2026
441d103
Parse complete screen headers before subject clauses and verify subje…
woksin Oct 25, 2026
1d448e7
Merge #1052 blocker fixes 441d103a into candidate
woksin Oct 25, 2026
e87a0ca
Align action subject diagnostic filter with final allocated codes
woksin Oct 26, 2026
17bce3e
Merge #1052 diagnostic filter correction
woksin Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Documentation/screenplay/action-subjects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: "Action subjects"
description: "Explicit action scope, identifier-aware inputs, and the authored/effective syntax boundary."
---

# Action subjects

State intent explicitly (decision 0110):

```play
action RetryItem for item label "Retry"
with itemId from item.rowId
with note from $context.note

action AddItem for screen

toolbar actions
item retry action RetryItem for item
item add action AddItem for screen
```

The clause follows the command or guarded label, before an inline `label`. Only action toolbar items accept it; navigation and dialog items do not. These are contextual words, not new reserved declaration or property names. Plain-action `with <input> from <binding>` entries remain ordered authored syntax. Guarded alternatives retain their arguments; toolbar items retain their UI-binding parameters.

## Inference and inputs

`for item` needs one nearest `data` declaration: a record, or a selected collection item. Local sibling data takes precedence over inherited data regardless of source order, including sections, filled slots and outlets. Missing or equally near data is not guessed.

1. Explicit input bindings win.
2. Remaining non-generated inputs use compatible, case-sensitive same-name fields.
3. A required, non-generated, non-collection identifier can use one key field with the same declared nominal concept.
4. Remaining inputs can use an existing command form or renderer input.

Primitive equality, coincidentally similar names, optional identifiers and unmarked foreign keys are not identity proof. Multiple matching keys warn and need an explicit binding. There is no first-row or first-key fallback.

`for screen` keeps plain and toolbar actions independent of selection and does not fill inputs from a row. A guarded action can inspect a single record even with `for screen`; collection-valued data warns, stays unresolved, and never chooses the first row.

Without a clause, a plain or toolbar action infers Item only for exactly one collection data context, exactly one required non-generated identifier, and one matching declared key. Otherwise Screen remains the omitted legacy default. Guarded actions retain their existing single-subject behavior.

## Diagnostics and migration

| Code | Severity | Meaning |
|---|---|---|
| PLAY0817 | Error | Malformed, duplicate or misplaced subject clause. |
| PLAY0818 | Information | Explains an omitted plain/toolbar subject's inference. |
| PLAY0819 | Warning | Effective row binding differs from legacy case-insensitive name matching. |
| PLAY0820 | Warning | Multiple nominal keys could bind an input; write an explicit binding. |
| PLAY0821 | Warning | Guarded `for screen` has collection-valued data and cannot choose one item. |

PLAY0319 still covers malformed/duplicate input syntax, PLAY0345 still covers unknown subject fields, and PLAY0346 still covers a missing or ambiguous guarded subject. Fixes can make inference explicit or retain legacy row behavior using explicit bindings when declaration proof is unambiguous. No fix silently rewrites source or guesses an imported declaration.

## Authored and effective syntax

Native `ScreenActionSubjects.Resolve(application)` and TypeScript `resolveActionSubjects(application)` return a separate effective application and diagnostics without mutating authored syntax. Resolution is idempotent. Consumers needing subject behavior opt in; raw compilation, canonical JSON and printing preserve the source form. Empty inputs and omitted subjects remain absent in legacy raw JSON. The additive native properties do not alter positional record constructors or deconstruction arity.

The paired native/TypeScript fixture packs are under `Source/Screenplay/Compiler/Conformance/action-subjects`: authored source, raw syntax, effective syntax, diagnostics, and context/location sidecars. Each context includes the complete authored declaration tree and canonical-path source locations, so consumers can verify nominal identity without inventing it. Native/TS packs preserve their existing modeled syntax surfaces and assert common-member parity separately.

Runtime execution and rendering proof are Scene-owned. Compiler fixtures and editor assistance do not claim renderer support or change the executable semantic model.

## Corpus migration gate

Before making inference part of a Scene release, compile each real application workspace with its imports and collect PLAY0818 and PLAY0819 by source path, line and command. Treat an unresolvable declaration as insufficient evidence, not a successful migration. For every PLAY0819, review the previous row-name bindings alongside the proposed effective inputs; either state `for screen`, state `for item` with the intended explicit inputs, or retain the previous bindings explicitly. Apply a proposed repair transactionally and compile the complete workspace again. Do not bulk-accept a binding based only on its spelling.

Track that real-corpus scan and disposition review as a follow-up to Screenplay#1052, before the consuming Scene release. The repository samples and paired fixture packs are the compiler regression gate, not evidence that an external corpus has been migrated. Scene must separately verify selection, form/renderer inputs and guarded single-record behavior against the effective pack; it must not reinterpret the raw authored pack as resolved execution metadata.
12 changes: 12 additions & 0 deletions Documentation/screenplay/ast-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ The syntax tree is a public API. Every consumer that compiles Screenplay to some

This page states those promises. It is deliberately narrow: a guarantee you cannot rely on is worse than one that was never made.

## Action subject additions

Decision 0110 adds nullable init-only `Subject` properties to `ScreenActionSyntax`,
`ScreenGuardedActionSyntax` and `ToolbarItemSyntax`, plus ordered init-only `Inputs`
on plain actions. Positional constructors and deconstruction arities are unchanged.
TypeScript adds optional equivalents. JSON omits absent subjects and empty inputs;
unknown subject enum values and subjects on non-action toolbar items are rejected.
Raw compile/parse and printing preserve authored syntax. Effective semantics require
native `ScreenActionSubjects.Resolve(application)` or TypeScript
`resolveActionSubjects(application)`; neither mutates its input. See
[action subjects](action-subjects.md) for the paired fixture/context boundary.

## Clock operand authoring additions

Decisions 0106/0107 add optional `ApplicationSyntax.TimeZone`, `CommandSyntax.Calendar`,
Expand Down
14 changes: 14 additions & 0 deletions Documentation/screenplay/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ Every problem the compiler finds is reported as a **diagnostic**: a severity, a

A message is written for a person and gets reworded whenever a clearer wording is found. A **code never changes**. So a code is what you match on, suppress on, and group by - anything reading the message text breaks the next time the message is improved.

## Action subject diagnostics

Decision 0110 adds the following catalog entries:

| Code | Severity | Meaning |
|---|---|---|
| `PLAY0817` | Error | An action subject clause is malformed, duplicated, or attached to a non-action toolbar item. Use one `for item` or `for screen` before the inline label. |
| `PLAY0818` | Information | An omitted plain/toolbar action subject is inferred from its declared data and identifier types. |
| `PLAY0819` | Warning | Effective row binding differs from legacy case-insensitive name matching. Make intent explicit; a safe repair may retain legacy mappings. |
| `PLAY0820` | Warning | Several declared nominal keys could fill an input. Write an explicit binding. |
| `PLAY0821` | Warning | A guarded action `for screen` cannot choose an item from collection-valued data. |
See [action subjects](action-subjects.md#diagnostics-and-migration)
for triggers and safe fixes. PLAY0319, PLAY0345 and PLAY0346 keep their existing meanings.

## Reading a diagnostic

The CLI prints a diagnostic in the format editors and build servers already parse - the file, the position, the severity, the code, and the message - followed by the offending line and a caret:
Expand Down
14 changes: 11 additions & 3 deletions Documentation/screenplay/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -1557,7 +1557,12 @@ ScreenDirective = DataDecl
ToolbarDecl = "toolbar", Ident, NL,
[ INDENT, { ToolbarItem | ToolbarButton }, DEDENT ] ;

ToolbarItem = "item", Ident, ( "action" | "dialog" ), QualifiedName, NL,
ToolbarItem = "item", Ident, "action", QualifiedName, [ ActionSubject ], [ "label", LocalizableString ], NL,
[ INDENT, { "parameter", Ident, "from", (* UI binding *) NL
| "label", LocalizableString, NL
| "icon", ( Ident | String ), NL
| ToolbarConfirm }, DEDENT ]
| "item", Ident, "dialog", QualifiedName, NL,
[ INDENT, { "label", LocalizableString, NL
| "icon", ( Ident | String ), NL
| ToolbarConfirm }, DEDENT ]
Expand All @@ -1574,9 +1579,11 @@ ToolbarConfirm = "confirm", LocalizableString, NL ; (* asked before the comman
DataDecl = "data", RequiredTypeRef, "via", "query", QualifiedName,
[ "by", Ident ], NL ;

ActionDecl = "action", QualifiedName, NL,
ActionSubject = "for", ( "item" | "screen" ) ;

ActionDecl = "action", QualifiedName, [ ActionSubject ], [ "label", LocalizableString ], NL,
[ INDENT, { ActionOption }, DEDENT ]
| "action", LocalizableString, NL,
| "action", LocalizableString, [ ActionSubject ], NL,
INDENT, { ActionAlternative | ActionOtherwise | NavigateDecl }, DEDENT ;

ActionAlternative = "when", Condition, "execute", QualifiedName, NL,
Expand All @@ -1590,6 +1597,7 @@ ActionOtherwise = "otherwise", "hidden", NL
ordering requires numbers, contains / starts with require strings. *)

ActionOption = NavigateDecl
| InteractionArgument
| "label", LocalizableString, NL ;

NavigateDecl = "navigate", "to", QualifiedName, [ "by", Ident ], NL,
Expand Down
13 changes: 9 additions & 4 deletions Documentation/screenplay/screens.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ issues the command and discovers its command-bound form. An action's `navigate t

Screen `data ... via query ... by value` and `navigate ... by value` remain scalar lookups. They cannot supply a composite read-model key to a single-instance query. The compiler reports `PLAY0661`; no automatic screen repair is offered. Collection queries may filter by a subset. See [Read-model keys](readmodels.md#keys).

## Action subjects

Plain actions, guarded actions and action toolbar items accept `for item` or `for screen` after the command or label, before an inline `label`. Plain actions also accept ordered `with <input> from <binding>` children. Prefer explicit scope in new documents. See [action subjects](action-subjects.md) for inference, nominal identifier/key binding, migration diagnostics and the separate authored/effective compiler boundary.

## One action, several commands

Use a label-headed **guarded action** when one user decision can run different commands as the displayed item's state changes. The label is a quoted string or a `$strings.<key>` reference; it replaces the plain action's `label` child.
Expand All @@ -77,7 +81,7 @@ screen PlanDetails

### Subject and conditions

`item` is the nearest container's `data` item, or the selected element when that data is a collection. Sibling data in the action's section, filled slot or screen body takes precedence over outer data, regardless of source order. Without an item (loading or no selection), the action is always hidden, even with an execute fallback. Missing or equally near data declarations produce a warning rather than guessing a subject.
For a guarded action (omitted subject or `for item`), `item` is the nearest container's `data` item, or the selected element when that data is a collection. Sibling data in the action's section, filled slot or screen body takes precedence over outer data, regardless of source order. Without an item (loading or no selection), the action is always hidden, even with an execute fallback. Missing or equally near data declarations produce a warning rather than guessing a subject.

Conditions compare `item.<field>[.<field>…]` with a literal: a string, number, Boolean or `null`. They do not read route parameters, screen state, `$context` or `$env`, and cannot compare two item paths. Use `==`, `!=`, numeric `>`, `>=`, `<`, `<=`, or string `contains` and `starts with`. Combine comparisons with `and`, `or` and parentheses; `and` binds more tightly. Collection-valued fields are not supported in conditions. Unsupported punctuation, such as brackets around a literal, is rejected with `PLAY0344` rather than discarded.

Expand All @@ -92,9 +96,10 @@ Alternatives are checked in authored order whenever query data changes. The firs
Each alternative and execute fallback may bind inputs with `with <property> from <binding>`. A binding may pass a terminal collection field, such as `with ids from item.ids`, to a matching collection input; it cannot traverse through a collection. Inputs resolve in this order:

1. Explicit `with` bindings.
2. Subject fields with the same name as the command input.
3. The command's declared [form](forms.md).
4. Renderer input.
2. Compatible subject fields with the same case-sensitive name as the command input.
3. A unique declared nominal identifier/key match for required, non-generated identifier inputs.
4. The command's declared [form](forms.md).
5. Renderer input.

The guard controls what the user is offered, not what the system accepts. The selected command still enforces its authorization, validation and constraints. If the selected command is unavailable to the caller, present the action as unavailable, as [#335](https://github.com/Cratis/Screenplay/issues/335) defines once admitted. Authorization denial never falls through to another command. A click runs the choice shown to the user; if a click-time check changes that choice, the renderer must refresh instead of executing the new one. A single optional `navigate to` runs after whichever command succeeds, never to open that command's own input screen. If input needs a separate screen or dialog, open it first with a click or row-click carrying the item's identity; place the guarded action on that input screen.

Expand Down
2 changes: 2 additions & 0 deletions Documentation/screenplay/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
href: triggers.md
- name: Screens
href: screens.md
- name: Action subjects
href: action-subjects.md
- name: Interactions
href: interactions.md
- name: Layouts and templates
Expand Down
2 changes: 1 addition & 1 deletion Samples/AcmeCommerce/Purchasing/model/suppliers.play
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module Buying
list
toolbar actions
button register title "Register supplier" slice Buying.Suppliers.RegisterSupplier emphasis primary icon plus
action OfferSupplierProduct
action OfferSupplierProduct for item
label "Offer product"
data SupplierRow[] via query AllSuppliers
table SupplierRow
Expand Down
2 changes: 1 addition & 1 deletion Samples/AcmeCommerce/Warehouse/model/dispatch.play
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ module Stock
property title = "Shipments"
property subtitle = "Every parcel with its carrier and tracking number"
toolbar
action DispatchShipment
action DispatchShipment for item
label "Dispatch"
// The stage of the selected parcel decides what can be done to it; the Picking board only shows the stages.
data ShipmentRow[] via query AllShipments
Expand Down
8 changes: 5 additions & 3 deletions Samples/Library/library.play
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ module Lending
column title label "Title"
column author label "Author"
column available label "On the shelf"
action AddBook
action BorrowBook
action AddBook for screen
action BorrowBook for item
with bookId from item.bookId

specification BorrowingTakesABookOffTheShelf
given caller
Expand Down Expand Up @@ -458,7 +459,8 @@ module Lending
column bookId label "Book"
column dueDate label "Due"
column status label "Status"
action ReturnBook
action ReturnBook for item
with bookId from item.bookId

specification SeeingAReturnedLoan
given caller
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Screenplay.Diagnostics;

namespace Cratis.Screenplay.CanonicalVectors.Specs.for_ScreenCompositionCorpus;

/// <summary>
/// Separates the 0110 action subject advisories (PLAY0818/PLAY0819) from the frozen published corpus diagnostics.
/// </summary>
static class ActionSubjectAdvisories
{
/// <summary>
/// Determines whether a diagnostic is an action subject advisory the compiler reports but the frozen corpus does not pin.
/// </summary>
/// <param name="diagnostic">The diagnostic to inspect.</param>
/// <returns>True when the diagnostic is an inferred or changed action subject advisory.</returns>
public static bool IsAdvisory(Diagnostic diagnostic) =>
string.Equals(diagnostic.Code, DiagnosticCodes.InferredActionSubject, StringComparison.Ordinal) ||
string.Equals(diagnostic.Code, DiagnosticCodes.ChangedActionSubject, StringComparison.Ordinal);

/// <summary>
/// Gets the diagnostics that belong to the frozen corpus boundary.
/// </summary>
/// <param name="diagnostics">The compile diagnostics.</param>
/// <returns>The diagnostics without action subject advisories.</returns>
public static Diagnostic[] WithoutAdvisories(IEnumerable<Diagnostic> diagnostics) => [.. diagnostics.Where(diagnostic => !IsAdvisory(diagnostic))];

/// <summary>
/// Gets the action subject advisories as ordinally sorted "code: message" lines.
/// </summary>
/// <param name="diagnostics">The compile diagnostics.</param>
/// <returns>The sorted advisory lines.</returns>
public static string[] Describe(IEnumerable<Diagnostic> diagnostics) => [.. diagnostics.Where(IsAdvisory).Select(diagnostic => $"{diagnostic.Code}: {diagnostic.Message}").Order(StringComparer.Ordinal)];

/// <summary>
/// Gets the action subject advisories as ordinally sorted "code@line action" lines.
/// </summary>
/// <param name="diagnostics">The compile diagnostics.</param>
/// <returns>The sorted advisory locations.</returns>
public static string[] DescribeLocations(IEnumerable<Diagnostic> diagnostics) => [.. diagnostics.Where(IsAdvisory).Select(diagnostic => $"{diagnostic.Code}@{diagnostic.Location.Line} {ActionName(diagnostic)}").Order(StringComparer.Ordinal)];

/// <summary>
/// Gets the source line an advisory points at.
/// </summary>
/// <param name="diagnostic">The advisory.</param>
/// <param name="documents">The documents of the compiled source form, as display path, stable key and text.</param>
/// <returns>The source line, or null when the location does not resolve to a document line.</returns>
public static string? SourceLineOf(Diagnostic diagnostic, IReadOnlyList<(string DisplayPath, string StableKey, string Text)> documents)
{
var document = documents.Count == 1 && diagnostic.Location.Path is null
? documents[0]
: documents.FirstOrDefault(candidate => string.Equals(candidate.DisplayPath, diagnostic.Location.Path, StringComparison.Ordinal) || string.Equals(candidate.StableKey, diagnostic.Location.Path, StringComparison.Ordinal));
if (document.Text is null)
{
return null;
}

var lines = document.Text.Split('\n');
return diagnostic.Location.Line >= 1 && diagnostic.Location.Line <= lines.Length ? lines[diagnostic.Location.Line - 1] : null;
}

/// <summary>
/// Gets the action an advisory names.
/// </summary>
/// <param name="diagnostic">The advisory.</param>
/// <returns>The action name quoted first in the message.</returns>
public static string ActionName(Diagnostic diagnostic)
{
var start = diagnostic.Message.IndexOf('\'', StringComparison.Ordinal) + 1;
return diagnostic.Message[start..diagnostic.Message.IndexOf('\'', start)];
}
}
Loading
Loading