@sublang/playbook 15.1.0 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/docs/embedding.md +2 -2
  2. package/package.json +1 -1
  3. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  4. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  5. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  6. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  7. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  8. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  9. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  10. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  11. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  12. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  13. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  15. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  16. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  17. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  18. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  19. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  20. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  21. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  22. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  23. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  24. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  25. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  26. package/reference/sdlc/decide.md +1 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  28. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  29. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  30. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  31. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  32. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  33. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  34. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  35. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  36. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  37. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  38. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  39. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  40. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  41. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  42. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  43. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  44. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  45. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  46. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  47. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  48. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  49. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  50. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  51. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  52. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  53. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  54. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  55. package/reference/sdlc/review.md +3 -3
  56. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  57. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  58. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  59. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  60. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  61. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  62. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  63. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  64. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  65. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  66. package/slc/gears2fsm.md +72 -3
  67. package/slc/link.md +50 -27
  68. package/slc/materialize-link.mjs +51 -13
  69. package/slc/optimize.md +4 -1
  70. package/slc/prefix-prompts.mjs +29 -28
  71. package/slc/prefix.md +27 -24
  72. package/slc/slc.pin-inputs.json +11 -2
  73. package/slc/text2gears.md +71 -11
  74. package/src/xstate-playbook-runtime.d.ts +54 -15
  75. package/src/xstate-playbook-runtime.js +1154 -248
  76. package/src/xstate-playbook-runtime.ts +1603 -317
package/slc/gears2fsm.md CHANGED
@@ -141,9 +141,11 @@ generic Captain forms, wire `<boss-intent>` from `bossIntent`,
141
141
  `remainingPlan`, and `<completed-call-results>` from
142
142
  `completedCallResults`. Other non-identity placeholders shall retain the
143
143
  semantic typed field established by Source (for example `<#>` from
144
- `irNumber`). Leaving an ordinary runtime-value placeholder literal, replacing
145
- it with an empty default because its field was omitted, or making the linker
146
- recover it from untyped context is malformed.
144
+ `irNumber`); a created-commit or labelled-section placeholder binds as
145
+ [Context and prompts](#context-and-prompts) states. Leaving an ordinary
146
+ runtime-value placeholder literal, replacing it with an empty default because
147
+ its field was omitted, or making the linker recover it from untyped context is
148
+ malformed.
147
149
  The corresponding `invoke.input` object shall include that field beside `prompt`; storing it only in machine context does not satisfy the actor-input contract.
148
150
  When Source declares a placeholder as the current identity of a local acting
149
151
  role, preserve the placeholder literal in the FSM prompt and do not add any
@@ -610,6 +612,37 @@ or remove remaining entries as evidence arrives, but it cannot grow or retain
610
612
  the same-length plan indefinitely; the initial finite array therefore bounds
611
613
  the number of sequential child calls without an arbitrary runtime call limit.
612
614
 
615
+ A placeholder that reads the commit an earlier call created — `<code-commit>`,
616
+ `<decide-commit>` — binds to a typed context field named by its canonical
617
+ mapping, assigned from that call's accepted `latestCommit`, the effect-owned
618
+ property the runtime fills from the repository receipt
619
+ ([text2gears](text2gears.md#result-contracts)); the actor input carries the
620
+ field beside the prompt like any other relayed value.
621
+ A placeholder the GEARS defines as a labelled section of another relayed
622
+ text binds to a typed context field named by its canonical mapping. The
623
+ definition names the label that opens the section and the labels that end it —
624
+ `<original-intent>` as the `Original intent:` section of the caller's request,
625
+ which runs to the `Review scope:` line or to the end of the request. The
626
+ machine derives the field deterministically in the entry action or transition
627
+ that stores the text and again in every action that replaces it, such as a
628
+ fresh entry directive or an interrupt that restarts with new text:
629
+
630
+ - the text as read is the text itself, except that where every non-blank line
631
+ begins with `>`, each line is read without that marker and one optional space
632
+ after it, the literal quote layer a quoted relay adds;
633
+ - the section is the lines of the text as read from the first line that
634
+ begins with the opening label, the label removed, through the line before the
635
+ first later line that begins with an ending label, or through the last line,
636
+ with the result trimmed; a line that merely looks labelled, such as `Note:`
637
+ or `Constraints:`, stays in the section;
638
+ - where no line begins with the opening label, or the section is empty once
639
+ trimmed — its label directly followed by an ending label or by the end of
640
+ the text — the field is the whole text as read, trimmed, so the request is
641
+ never lost.
642
+
643
+ The derived field is ordinary context that actor inputs relay, never a player
644
+ or judge output.
645
+
613
646
  Prompts shall pass only the **specific extracted fields** the player needs.
614
647
  The compiler shall not dump `JSON.stringify(lastResult)` or any opaque blob: it leaks internal `guard` strings, wastes tokens, and confuses the LLM.
615
648
 
@@ -663,6 +696,26 @@ The compiler shall update or reset those source-owned facts only at their actual
663
696
  Transitions shall be self-driving when source items define the next obligation.
664
697
  Routing to an idle hub is for recovery, unrecoverable Boss input, or one-shot entry events — not the happy path.
665
698
 
699
+ Where an artifact-schema-3 delegated-player state's outcomes are governed by the
700
+ linked runtime ([link.md "Captain adjudication"](link.md#captain-adjudication)),
701
+ every accepted `onDone` arm — each arm that routes a declared outcome, not the
702
+ malformed-output fallback — shall carry, first among its actions, the
703
+ root-machine action
704
+ `{ type: 'playbook.acceptedOutcome', params: { source: '<stateId>', target: '<target stateId>', acceptedOutcome: '<guard>' } }`,
705
+ with the setup's `actions` declaring `'playbook.acceptedOutcome'` as a no-op
706
+ that types those three string params.
707
+ `target` names the state the next public snapshot shows for the arm: the arm's
708
+ own target, or the parallel parent's `onDone` target where the arm's target is a
709
+ region's final leaf that completes that parent, because the machine leaves that
710
+ leaf before the snapshot; an arm that completes the join only when every
711
+ sibling region is already final therefore splits into two arms guarded on that
712
+ condition, each naming the state it reaches.
713
+ The linked runtime retains the marker until the next public snapshot confirms
714
+ `source` in the prior snapshot and `target` in the new one, and only then
715
+ publishes the outcome's accepted trace and `→ <acceptedOutcome>` status; an arm
716
+ without the marker accepts its outcome silently, and the host counts none of the
717
+ work it saved.
718
+
666
719
  ### Auto-advance on approval
667
720
 
668
721
  A review/approval state's success outcome shall **target the next workflow step**, not idle back to a hub.
@@ -811,6 +864,12 @@ Where several questions are pending, the classifier prompt and event shall
811
864
  require `questionId` and shall reject an omitted or unknown id without moving
812
865
  the FSM.
813
866
 
867
+ Every exit from a Captain- or player-invoking state other than into its own
868
+ Boss-reply wait — an accepted outcome, the malformed-output fallback, an actor
869
+ error — and every non-reply exit from a wait shall clear that state's pending
870
+ question and reply context, so a call that fails after resuming leaves no
871
+ stale question for the next turn's classifier to offer.
872
+
814
873
  The scalar `awaitBossReply` state and every local branch wait are quiescent for
815
874
  the runtime drive boundary.
816
875
  They shall allow a fresh root entry event or interrupt to abandon the relevant
@@ -882,6 +941,16 @@ Where Source declares a JSON-safe terminal result, the setup types shall
882
941
  declare that output and the root machine shall derive it from typed context
883
942
  through XState's machine `output` function. A final-state transition alone does
884
943
  not satisfy a declared output contract.
944
+ Where the selected pipeline supplies the
945
+ [workflow contracts](workflow-contracts.json) catalog and the compiled Source's
946
+ basename is one of its `literalTargetBindings`, the declared terminal output
947
+ shall be exactly that builtin's public interface — its variants, `status`
948
+ constants, property names, and requiredness — derived from typed context,
949
+ because callers compiled against the catalog consume it by those names; an
950
+ authored outcome the interface cannot express is an inconsistency between the
951
+ Source and the catalog, reported as an incompatible compiler input under
952
+ [Nested playbook calls](#nested-playbook-calls) rather than expressed by an
953
+ invented or renamed outcome.
885
954
  Fields that Source requires in every terminal output shall be required in the
886
955
  TypeScript output type. In particular, a declared `{ response }` result shall
887
956
  compile as `{ response: string }`, not `{ response?: string }`; reaching the
package/slc/link.md CHANGED
@@ -38,15 +38,20 @@ The link compiler shall not modify the FSM artifact and shall not re-derive Capt
38
38
 
39
39
  ## Optional deterministic materialization
40
40
 
41
- For an ordinary flat workflow using only `player` and `script` actors,
42
- the supported prompt profile below, shared remaining strategies, and primitive configured options, the linker may
41
+ For an ordinary workflow using only `player` and `script` actors — flat, or
42
+ with root parallel groups of the [Parallel groups](gears2fsm.md#parallel-groups)
43
+ shape — the supported prompt profile below, shared remaining strategies, and primitive configured options, the linker may
43
44
  use the adjacent `materialize-link.mjs` tool to emit the thin module.
44
45
  The complete definition below remains binding; the tool replaces repetitive
45
46
  module generation, not semantic analysis or emitted conformance verification.
46
47
  Read the actual FSM and supply every erased or authored contract exactly.
47
48
  Do not use `flat-defaults` or `flat-quoted-relays` when another custom composer, classifier, required-field
48
49
  extractor, session-derived input mapping, controller strategy, nested call,
49
- parallel state, or compound state is needed.
50
+ or compound state other than such a parallel group is needed.
51
+ Despite their names, every profile accepts those parallel groups: the tool
52
+ takes each region working leaf's role and label under its stable state id
53
+ like any other player state, declares nothing further, and leaves the
54
+ compiled shape to the factory preflight, which rejects any other form.
50
55
 
51
56
  Invoke the tool with the actual definition directory, source FSM, and declared
52
57
  target; supply a JSON descriptor on standard input:
@@ -101,6 +106,12 @@ session-derived input values are outside this profile; do not widen their
101
106
  contracts to an unconstrained primitive.
102
107
  The tool adds optional string `cwd` for script-bearing machines; it does not
103
108
  put `cwd` in FSM input unless the descriptor explicitly maps it.
109
+ When the linked module is part of a package that compiles and ships
110
+ JavaScript siblings, supply the optional `fsmSpecifier` — the `.js` sibling
111
+ the build emits for the source FSM, such as `./code.fsm.js`, in the same
112
+ directory and with the same basename — so the emitted import satisfies the
113
+ NodeNext rule under Output; omit it for a source-only host, where the tool
114
+ derives the specifier from the `--fsm` path.
104
115
  An explicit `entryEvent: null` selects the shared classifier only where this
105
116
  definition permits no deterministic entry; it does not relax entry rules.
106
117
  `bossEvents` retains the exact additional erased event fields, source ownership,
@@ -125,8 +136,9 @@ correct the identified problem before treating linking as successful.
125
136
 
126
137
  ### Experimental labelled-string and nested-call profile
127
138
 
128
- `flat-labelled-relays` is an unmeasured candidate for flat single-region
129
- machines with delegated players, scripts, and nested `playbook` calls. It
139
+ `flat-labelled-relays` is an unmeasured candidate for machines with flat
140
+ root states or root parallel groups of that shape, whose delegated players,
141
+ scripts, and nested `playbook` calls it supports. It
130
142
  requires the installed shared `composePlayerContinuation` API. It leaves
131
143
  nested input composition, child targets, result guards, recovery, and terminal
132
144
  semantics in the unchanged FSM; the shared factory provides the nested bridge.
@@ -154,8 +166,8 @@ source tokens. Undeclared empty relay lines retain their authored text.
154
166
  The emitted verification composer has the same identity and optional third
155
167
  resume arguments as the runtime; the shared helper owns fresh/resumed Q&A.
156
168
  Structured rendering, custom classifiers/extractors/controllers, direct
157
- Captain actors, compound/parallel topology, and constrained/nonprimitive
158
- options remain unsupported. Source-derived metadata and all existing
169
+ Captain actors, compound topology other than such a parallel group, and
170
+ constrained/nonprimitive options remain unsupported. Source-derived metadata and all existing
159
171
  conformance checks remain mandatory. This profile changes no runtime bridge,
160
172
  workflow semantics, or measured performance claim.
161
173
 
@@ -1239,7 +1251,7 @@ undeclared extra key, so hidden adjudication cannot replace, paraphrase, or
1239
1251
  decorate prose Boss already saw.
1240
1252
  For an artifact-schema-3 governed delegated-player call, the shared engine
1241
1253
  shall instead use one semantic reconciler for both the default linked runtime
1242
- and any bespoke linked runtime that adopts schema `3`.
1254
+ and any linked machinery of an artifact's own that adopts schema `3`.
1243
1255
  That reconciler shall retain the validated player's exact non-empty
1244
1256
  `finalText` as opaque presentation evidence, let the hidden adjudicator read
1245
1257
  it only as semantic evidence, and require the adjudicator's detached
@@ -1257,8 +1269,8 @@ authored placeholder or guidance, with every presentation-, effect-, or
1257
1269
  runtime-owned field named as runtime-supplied to omit — so the judge is never asked for a
1258
1270
  field it does not own; the artifact's description text stays unchanged.
1259
1271
  The shared engine shall export that rendering as `renderGovernedOutcomeContract`
1260
- on `@sublang/playbook/xstate-runtime`, and a bespoke linked runtime shall
1261
- render its judge prompt through it rather than restate the contract.
1272
+ on `@sublang/playbook/xstate-runtime`, and linked machinery of an artifact's
1273
+ own shall render its judge prompt through it rather than restate the contract.
1262
1274
  The reconciler shall construct the complete actor output rather than accept a
1263
1275
  cross-authority object from the judge: every presentation-owned payload field
1264
1276
  shall receive the canonical `finalText.trim()` value; every effect-owned
@@ -1960,9 +1972,11 @@ A linked runtime may implement the optional adoption capability of
1960
1972
  initialization path distinct from `init` and same-engagement `restore`.
1961
1973
  Adoption may bind a retained generation to a fresh valid `PlaybookSession`
1962
1974
  identity. Every runtime the shared `createXStatePlaybookRuntime` factory
1963
- constructs implements `adopt`, regardless of whether the artifact supplies
1964
- retained-generation classification metadata; a bespoke runtime may omit it,
1965
- and hosts feature-detect the capability by member presence.
1975
+ constructs for a flat machine implements `adopt`, regardless of whether the
1976
+ artifact supplies retained-generation classification metadata; one it
1977
+ constructs for a machine that declares a parallel state omits it, as linked
1978
+ machinery of an artifact's own may, and hosts feature-detect the capability
1979
+ by member presence.
1966
1980
 
1967
1981
  Before actor construction or any player-session-store, port, trace, status,
1968
1982
  or telemetry effect, `adopt` shall validate and detach the target session, the
@@ -2053,9 +2067,9 @@ and detached from that declaration. Absence means the runtime contributes no
2053
2067
  retained generation; presence supplies only terminal classification metadata
2054
2068
  and does not itself supply the adoption operation.
2055
2069
  Every runtime the shared `createXStatePlaybookRuntime` factory constructs from
2056
- a supplied `unfinishedFinalStateIds` spec member shall expose the marker; a
2057
- bespoke runtime opts into classification only by implementing the public member
2058
- itself.
2070
+ a supplied `unfinishedFinalStateIds` spec member shall expose the marker;
2071
+ linked machinery of an artifact's own opts into classification only by
2072
+ implementing the public member itself.
2059
2073
 
2060
2074
  ## Control surface (optional)
2061
2075
 
@@ -2387,14 +2401,19 @@ Every linked artifact shall emit an `unfinishedFinalStateIds` set beside its res
2387
2401
  The set shall contain exactly the stable ids of root `type: 'final'` states whose terminal outcomes leave the procedure unfinished, and shall be explicitly empty when no terminal outcome does.
2388
2402
  The linker shall not infer the set from a state description, opaque output, or procedure prose.
2389
2403
  The linker shall reject a declared id that does not name a root final state, and the shared factory shall independently reject it at construction before runtime effects.
2390
- For a factory-backed artifact the set is a `spec` member; a bespoke artifact shall retain equivalent linked metadata, and the artifact declaration is not itself the public runtime retention marker or an adoption capability.
2391
- For an FSM that declares no `type: 'parallel'` state — necessarily flat
2392
- under [gears2fsm.md](gears2fsm.md)'s one-state-per-item mapping — it shall
2404
+ For a factory-backed artifact the set is a `spec` member; linked machinery of an artifact's own shall retain equivalent linked metadata, and the artifact declaration is not itself the public runtime retention marker or an adoption capability.
2405
+ For every FSM [gears2fsm.md](gears2fsm.md) produces — flat under its
2406
+ one-state-per-item mapping, or carrying root parallel groups of its
2407
+ [Parallel groups](gears2fsm.md#parallel-groups) shape — the linker shall
2393
2408
  emit the thin shared-factory module defined below.
2394
- For an FSM that declares a parallel state, it shall emit bespoke linked
2395
- machinery satisfying this document's runtime contract and shall not invoke
2396
- `createXStatePlaybookRuntime`, whose supported domain is flat single-region
2397
- FSMs under [DR-019](../specs/decisions/019-shared-linked-runtime-factory.md).
2409
+ The shared factory interprets such a parallel group through its parallel
2410
+ profile under [DR-067](../specs/decisions/067-parallel-proposals-through-the-shared-factory.md), deriving the regions, their leaves, and the
2411
+ cohort of working leaves from the FSM and its exported `concurrentRoleSets`,
2412
+ so the emitted module declares nothing further for it; bespoke linked
2413
+ machinery is no longer the prescribed output for any such FSM.
2414
+ Linked machinery of an artifact's own that satisfies this document's runtime
2415
+ contract remains a valid implementation for an artifact outside this package
2416
+ and declares the `bespoke` registry profile below, so hosts keep accepting it.
2398
2417
  The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
2399
2418
  mapping, adjudication, script execution, nested-playbook bridging, session
2400
2419
  lifecycle, abort handling, and the optional parked-session snapshot and
@@ -2510,8 +2529,9 @@ The thin emitted module:
2510
2529
  error.
2511
2530
  `NO_ACTION` and `BOSS_REPLY` are runtime-owned event types the factory
2512
2531
  supplies itself — `NO_ACTION` as exactly `{ type: 'NO_ACTION' }`, and
2513
- `BOSS_REPLY` as an optional judge-selected `questionId` plus the exact-text
2514
- `answer` the runtime attaches. `bossEvents` shall carry no entry for either
2532
+ `BOSS_REPLY` as a judge-selected `questionId` — optional while one question
2533
+ is pending and required, from the pending ids, while several are — plus the
2534
+ exact-text `answer` the runtime attaches. `bossEvents` shall carry no entry for either
2515
2535
  type; supplying one is a construction error, so a linker that judges a
2516
2536
  runtime-owned arm to have lost payload detail under erasure shall report
2517
2537
  that gap rather than emit the entry.
@@ -2553,8 +2573,11 @@ The thin emitted module:
2553
2573
  host-construction boundary's setup diagnostic.
2554
2574
  - Exposes, under an `_internal` export, the pure helpers verification
2555
2575
  needs — at least the prompt composers its own machine uses, which may
2556
- re-export the shared defaults when the spec does not override composition —
2557
- so compilation-correctness tests can exercise composition without a host.
2576
+ re-export the shared defaults when the spec does not override composition,
2577
+ and the `VERBATIM_PAYLOAD_FIELDS` and `UNFINISHED_FINAL_STATE_IDS` sets the
2578
+ spec declares, which the linked-ownership check reads back — so
2579
+ compilation-correctness tests can exercise composition and ownership
2580
+ without a host.
2558
2581
  A playbook that calls players exposes `composePlayerPrompt`; a playbook
2559
2582
  whose states make direct-Captain calls exposes `composeCaptainPrompt`. A
2560
2583
  controller playbook that calls no players exposes no player composer:
@@ -7,7 +7,7 @@
7
7
  import { createRequire } from 'node:module';
8
8
  import { lstat, open, realpath, rename, unlink } from 'node:fs/promises';
9
9
  import { randomUUID } from 'node:crypto';
10
- import { dirname, extname, relative, resolve } from 'node:path';
10
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
 
13
13
  // Compile-time source only; emitted modules depend only on the shared engine.
@@ -78,6 +78,10 @@ const DESCRIPTOR_KEYS = [
78
78
  'transitionEventFields', 'verbatimPayloadFields', 'resumableStateIds',
79
79
  'unfinishedFinalStateIds', 'controlContextFields',
80
80
  ];
81
+ // Optional: the runtime specifier of the FSM's emitted JavaScript sibling, for
82
+ // a module inside a package that compiles and ships `.js` beside its sources.
83
+ const OPTIONAL_KEYS = ['fsmSpecifier'];
84
+ const JS_SPECIFIER = /^\.\.?\/.+\.js$/;
81
85
  const LABELLED_KEYS = ['playerInputExport', 'omitEmptyRelayLines', 'identityPlaceholders'];
82
86
  const TOKEN = /^(#|[A-Za-z_$][A-Za-z0-9_$-]*)$/;
83
87
  const CONTRACT_TYPES = [
@@ -126,7 +130,10 @@ function validateDescriptor(value, engine) {
126
130
  const descriptor = engine.snapshotJsonValue(value, 'link descriptor');
127
131
  const labelled = descriptor?.profile === 'flat-labelled-relays';
128
132
  const keys = labelled ? [...DESCRIPTOR_KEYS, ...LABELLED_KEYS] : DESCRIPTOR_KEYS;
129
- record(descriptor, 'descriptor', keys, keys);
133
+ record(descriptor, 'descriptor', [...keys, ...OPTIONAL_KEYS], keys);
134
+ if (descriptor.fsmSpecifier !== undefined && (typeof descriptor.fsmSpecifier !== 'string' || !JS_SPECIFIER.test(descriptor.fsmSpecifier))) {
135
+ throw new TypeError('fsmSpecifier must be a relative runtime specifier ending in .js');
136
+ }
130
137
  if (descriptor.schema !== SCHEMA) throw new TypeError(`descriptor.schema must equal ${SCHEMA}`);
131
138
  if (!['flat-defaults', 'flat-quoted-relays', 'flat-labelled-relays'].includes(descriptor.profile)) throw new UnsupportedLinkProfile('profile requires ordinary normative linking');
132
139
  if (labelled) {
@@ -228,27 +235,46 @@ function validateDescriptor(value, engine) {
228
235
  function inspectMachine(machine, engine, labelled = false) {
229
236
  const config = machine?.config;
230
237
  if (!config || typeof config !== 'object' || !config.states) throw new TypeError('FSM export must be an XState machine');
231
- if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('root parallel states or invocations require ordinary linking');
238
+ if (config.type === 'parallel' || config.invoke) throw new UnsupportedLinkProfile('a parallel machine root or root invocation requires ordinary linking');
232
239
  const descriptions = engine.stateDescriptionsFromMachine(machine);
233
240
  const roleStates = Object.create(null);
234
241
  let hasScript = false;
235
- for (const [key, state] of Object.entries(config.states)) {
242
+ const children = (value) => value && typeof value === 'object' ? Object.entries(value) : [];
243
+ const inspectState = (key, state) => {
236
244
  if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
237
245
  if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
238
- if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
239
- throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
240
- }
241
246
  const invokes = Array.isArray(state.invoke) ? state.invoke : state.invoke ? [state.invoke] : [];
242
247
  if (invokes.length > 1) throw new UnsupportedLinkProfile(`state ${key} has multiple actors`);
243
248
  for (const invoke of invokes) {
244
249
  if (!(labelled ? ['player', 'script', 'playbook'] : ['player', 'script']).includes(invoke.src)) throw new UnsupportedLinkProfile(`state ${key} actor ${String(invoke.src)} requires ordinary linking`);
245
250
  if (invoke.src === 'script') hasScript = true;
246
251
  else if (invoke.src === 'player') {
247
- const role = nonempty(state.meta?.playbook?.role, `state ${key} role`);
248
- const label = nonempty(descriptions.get(key), `state ${key} description`);
249
- roleStates[key] = { role, label };
252
+ // The engine keys a player state by its stable playbook id, which a
253
+ // parallel region's leaf carries distinct from its local key.
254
+ const stateId = typeof state.meta?.playbook?.stateId === 'string' ? state.meta.playbook.stateId
255
+ : typeof state.id === 'string' ? state.id : key;
256
+ if (RESERVED_KEYS.has(stateId)) throw new UnsupportedLinkProfile(`state ${stateId} requires ordinary linking`);
257
+ const role = nonempty(state.meta?.playbook?.role, `state ${stateId} role`);
258
+ const label = nonempty(descriptions.get(stateId), `state ${stateId} description`);
259
+ roleStates[stateId] = { role, label };
260
+ }
261
+ }
262
+ };
263
+ for (const [key, state] of Object.entries(config.states)) {
264
+ if (RESERVED_KEYS.has(key)) throw new UnsupportedLinkProfile(`state ${key} requires ordinary linking`);
265
+ if (!state || typeof state !== 'object') throw new TypeError(`state ${key} must be an object`);
266
+ if (state.type === 'parallel') {
267
+ // DR-067: walk a root parallel state's region leaves; the engine's
268
+ // construction preflight below owns validation of the compiled shape.
269
+ for (const [, region] of children(state.states)) {
270
+ for (const [leafKey, leaf] of children(region?.states)) inspectState(leafKey, leaf);
250
271
  }
272
+ continue;
273
+ }
274
+ if (state.states || (state.type !== undefined && !['atomic', 'final'].includes(state.type))) {
275
+ throw new UnsupportedLinkProfile(`state ${key} is outside the flat single-region profile`);
251
276
  }
277
+ inspectState(key, state);
252
278
  }
253
279
  return { roleStates, hasScript };
254
280
  }
@@ -355,6 +381,7 @@ const OPTION_SCHEMA = ${json(options)} as const;
355
381
  const INPUT_MAPPING = ${json(descriptor.inputMapping)} as const;
356
382
  const RESUMABLE_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.resumableStateIds)});
357
383
  const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set(${json(descriptor.unfinishedFinalStateIds)});
384
+ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set(${json(descriptor.verbatimPayloadFields)});
358
385
  export function validateOptions(value: unknown): PlaybookRuntimeOptions {
359
386
  const captured = snapshotJsonValue(value === undefined ? {} : value, ${JSON.stringify(`${descriptor.label} runtime options`)});
360
387
  if (captured === null || typeof captured !== 'object' || Array.isArray(captured)) {
@@ -380,7 +407,7 @@ ${quotedPlayer || labelledPlayer ? ' composePlayerPrompt,\n' : ''} ...${json(d
380
407
  Object.entries(INPUT_MAPPING).filter(([, key]) => options[key as keyof PlaybookRuntimeOptions] !== undefined)
381
408
  .map(([field, key]) => [field, options[key as keyof PlaybookRuntimeOptions]]),
382
409
  ),
383
- verbatimPayloadFields: new Set<string>(${json(descriptor.verbatimPayloadFields)}),
410
+ verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
384
411
  resumableStateIds: RESUMABLE_STATE_IDS,
385
412
  unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
386
413
  } satisfies XStatePlaybookRuntimeSpecV3<PlaybookRuntimeOptions>;
@@ -389,6 +416,7 @@ export const _internal = {
389
416
  ${quotedPlayer || labelledPlayer ? 'composePlayerPrompt,\n ' : hasPlayer ? `composePlayerPrompt: (input: PlaybookPlayerInput, _identity?: unknown${hasContinuationMode ? ', resuming = false' : ''}) =>
390
417
  defaultComposePlayerPrompt(input, ${json(descriptor.placeholderFields)}${hasContinuationMode ? ', resuming' : ''}),\n ` : ''}RESUMABLE_STATE_IDS,
391
418
  UNFINISHED_FINAL_STATE_IDS,
419
+ VERBATIM_PAYLOAD_FIELDS,
392
420
  };
393
421
  const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
394
422
  XStatePlaybookRuntimeConstruction<PlaybookRuntimeOptions, PlaybookHostCapabilities>, 3
@@ -428,9 +456,19 @@ export async function runMaterializer(argv, input) {
428
456
  // Validate the export name before selecting it from the imported module.
429
457
  validateDescriptor(descriptor, engine);
430
458
  const module = await import(pathToFileURL(source).href);
431
- const specifier = relative(dirname(out), fsm).replaceAll('\\', '/');
459
+ const derived = relative(dirname(out), fsm).replaceAll('\\', '/');
460
+ let specifier = derived.startsWith('.') ? derived : `./${derived}`;
461
+ if (descriptor.fsmSpecifier !== undefined) {
462
+ // The declared `.js` sibling must be the build output of exactly this FSM:
463
+ // same directory and same basename, differing only in the extension.
464
+ const declared = resolve(dirname(out), descriptor.fsmSpecifier);
465
+ if (dirname(declared) !== dirname(fsm) || basename(declared, '.js') !== basename(fsm, extname(fsm))) {
466
+ throw new TypeError('fsmSpecifier must name the .js sibling of the --fsm file');
467
+ }
468
+ specifier = descriptor.fsmSpecifier;
469
+ }
432
470
  const code = materializeLink({ machine: module[descriptor.machineExport], descriptor,
433
- fsmSpecifier: specifier.startsWith('.') ? specifier : `./${specifier}`, engine });
471
+ fsmSpecifier: specifier, engine });
434
472
  const temporary = `${out}.${randomUUID()}.tmp`;
435
473
  try {
436
474
  const file = await open(temporary, 'wx', 0o644);
package/slc/optimize.md CHANGED
@@ -116,7 +116,10 @@ shell script are language-independent.
116
116
 
117
117
  The pass shall append one `## Optimizations` section at the end of the target
118
118
  listing every rewritten item, one bullet per item:
119
- `- <ITEM-ID>: <original behavior kind> → script`.
119
+ `- <ITEM-ID>: captain → script`, where `captain` is the fixed token for the
120
+ direct-Captain behavior kind the pass rewrites — the only kind eligible — so
121
+ the section reads the same in every target and a consumer can match it
122
+ exactly.
120
123
  When no item is eligible, the target shall be the source content unchanged,
121
124
  with no `## Optimizations` section.
122
125
 
@@ -4,10 +4,10 @@
4
4
 
5
5
  // Deterministic realization of the prompt-prefix pass (slc/prefix.md): every
6
6
  // standalone relay block of an eligible item moves after its last instruction
7
- // line, byte-for-byte, and one `## Prefixed prompts` section at the end lists
8
- // the items this and every earlier application rewrote. `--keep <ITEM-ID>`
9
- // excludes an item the pass judged ineligible; the tool never modifies the
10
- // source.
7
+ // line, byte-for-byte, and nothing records the rewrite beside the prompts; a
8
+ // `## Prefixed prompts` section an earlier version of the pass appended is
9
+ // removed. `--keep <ITEM-ID>` excludes an item the pass judged ineligible; the
10
+ // tool never modifies the source.
11
11
 
12
12
  import { readFile, writeFile } from 'node:fs/promises';
13
13
  import { resolve } from 'node:path';
@@ -16,10 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
  const HEADING = /^#{1,3}\s/;
17
17
  const ITEM_HEADING = /^###\s+([^\s]+)\s*$/;
18
18
  const PROMPT_LINE = /^>\s?(.*)$/;
19
- const SECTION = '## Prefixed prompts';
20
- const SECTION_HEADING = /^##\s+Prefixed prompts\s*$/;
21
- const LISTED = /^-\s+([^\s:]+):\s+relays → tail\s*$/;
22
- const bullet = (id) => `- ${id}: relays → tail`;
19
+ const LEGACY_SECTION = /^##\s+Prefixed prompts\s*$/;
23
20
 
24
21
  /** Classify one prompt line by its content inside the outer blockquote marker. */
25
22
  function classify(line) {
@@ -97,9 +94,9 @@ function prefixBlockquote(quote) {
97
94
  }
98
95
 
99
96
  /**
100
- * Rewrite every eligible item of a GEARS package not named by `keep`.
101
- * Returns the rewritten text with its provenance section, or the source text
102
- * unchanged when no item was eligible, plus the rewritten item ids.
97
+ * Rewrite every eligible item of a GEARS package not named by `keep` and
98
+ * remove any legacy `## Prefixed prompts` section. Returns the target text —
99
+ * the source text itself when neither applies — and the rewritten item ids.
103
100
  */
104
101
  export function prefixPrompts(text, { keep = [] } = {}) {
105
102
  // Keep the source's own line-ending convention so untouched lines stay bytes.
@@ -130,24 +127,25 @@ export function prefixPrompts(text, { keep = [] } = {}) {
130
127
  cursor = quoteEnd;
131
128
  rewritten.push(start.id);
132
129
  }
133
- if (rewritten.length === 0) return { text, rewritten };
134
130
  out.push(...lines.slice(cursor));
135
- // One provenance section follows every other section, ending in one newline;
136
- // it replaces every section an earlier application appended and lists, in
137
- // item order, each item this or that application rewrote.
138
- const listed = new Set(rewritten);
131
+ // A legacy section runs to the next heading; at the end of the file it
132
+ // takes the blank lines before it too, so the file ends as it did before
133
+ // the section was appended.
134
+ let removed = false;
139
135
  for (let index = out.length - 1; index >= 0; index--) {
140
- if (!SECTION_HEADING.test(out[index])) continue;
136
+ if (!LEGACY_SECTION.test(out[index])) continue;
137
+ let start = index;
141
138
  let end = index + 1;
142
- for (; end < out.length && !HEADING.test(out[end]); end++) {
143
- const entry = LISTED.exec(out[end]);
144
- if (entry !== null) listed.add(entry[1]);
139
+ while (end < out.length && !HEADING.test(out[end])) end++;
140
+ if (end === out.length) {
141
+ while (start > 0 && out[start - 1] === '') start--;
142
+ out.splice(start, end - start, '');
143
+ } else {
144
+ out.splice(start, end - start);
145
145
  }
146
- out.splice(index, end - index);
146
+ removed = true;
147
147
  }
148
- while (out.length > 0 && out[out.length - 1] === '') out.pop();
149
- const ids = starts.map((start) => start.id).filter((id) => listed.has(id));
150
- out.push('', SECTION, '', ...ids.map(bullet), '');
148
+ if (rewritten.length === 0 && !removed) return { text, rewritten };
151
149
  return { text: out.join(newline), rewritten };
152
150
  }
153
151
 
@@ -172,12 +170,15 @@ async function main(argv) {
172
170
  'usage: prefix-prompts.mjs --source <gears.md> --target <gears.md> [--keep <ITEM-ID>]...',
173
171
  );
174
172
  }
175
- const { text, rewritten } = prefixPrompts(await readFile(resolve(source), 'utf8'), { keep });
173
+ const input = await readFile(resolve(source), 'utf8');
174
+ const { text, rewritten } = prefixPrompts(input, { keep });
176
175
  await writeFile(resolve(target), text);
177
176
  process.stdout.write(
178
- rewritten.length === 0
179
- ? 'no eligible item; target equals source\n'
180
- : `${rewritten.join('\n')}\n`,
177
+ rewritten.length > 0
178
+ ? `${rewritten.join('\n')}\n`
179
+ : text === input
180
+ ? 'no eligible item; target equals source\n'
181
+ : 'no eligible item; legacy ## Prefixed prompts section removed\n',
181
182
  );
182
183
  }
183
184
 
package/slc/prefix.md CHANGED
@@ -79,8 +79,8 @@ rewrite, merge, split, or drop any line.
79
79
  Source prose that places a relay after an instruction, or an instruction at
80
80
  the end of the prompt, tells [text2gears](text2gears.md) how to compose the
81
81
  prompt faithfully; it does not make the item ineligible.
82
- This pass overrides that composition order by design, records the override in
83
- its provenance section, and `--no-optimize` restores it.
82
+ This pass overrides that composition order by design, and `--no-optimize`
83
+ restores it.
84
84
 
85
85
  ## Rewriting
86
86
 
@@ -98,7 +98,8 @@ For each eligible item, the pass shall rewrite only the blockquote:
98
98
  - Keep the item's ID, heading, condition, acting clause, `Results:`, and any
99
99
  other metadata unchanged.
100
100
  - Preserve every other item and every non-item section byte-for-byte,
101
- including an `## Optimizations` section the optimize pass appended.
101
+ including an `## Optimizations` section the optimize pass appended, except
102
+ a legacy `## Prefixed prompts` section (see [Provenance](#provenance)).
102
103
 
103
104
  Because no line changes, the target stays in the Source language and every
104
105
  authored fragment remains intact; only the order of relay blocks relative to
@@ -142,22 +143,24 @@ Results:
142
143
 
143
144
  ## Provenance
144
145
 
145
- The target shall end with one `## Prefixed prompts` section — after an
146
- `## Optimizations` section when one is present — listing every rewritten
147
- item, one bullet per item in item order: `- <ITEM-ID>: relays → tail`.
148
- A section an earlier application left in the source is replaced, and the
149
- items it listed stay listed.
150
- When no item is eligible, the target shall be the source content unchanged;
151
- a package the pass never rewrote carries no `## Prefixed prompts` section.
152
-
153
- The section is what lets a Source-to-GEARS fidelity checker accept the new
154
- order: for a listed item it requires each instruction and relay block of the
155
- fragments the item carries intact, blank lines inside a block included, and
156
- used exactly once, the instruction blocks in Source order, the relay blocks in
157
- Source order, and every relay after the last instruction, a relay Source joined
158
- directly to an instruction staying beside it with its authored boundary, a bare
159
- relay the prose authored free to trail, and one occurrence standing for one
160
- authored fragment; for any other item it requires Source order throughout.
146
+ The pass shall record nothing beside the rewritten prompts, whose layout
147
+ shows the rewrite.
148
+ A `## Prefixed prompts` section an earlier version of this pass appended shall
149
+ be removed — up to the next heading, and at the end of the file with the blank
150
+ lines before it; when no item is eligible and no such section is present, the
151
+ target shall be the source content unchanged.
152
+ Re-running the pass is a no-op by layout: an item whose relays already trail
153
+ is not eligible.
154
+
155
+ A Source-to-GEARS fidelity checker therefore reads the layout from each prompt:
156
+ it accepts an item either in Source order throughout or in the prefix-first
157
+ layout, which requires each instruction and relay block of the fragments the
158
+ item carries intact, blank lines inside a block included, and used exactly
159
+ once, the instruction blocks in Source order, the relay blocks in Source order,
160
+ and every relay after the last instruction, a relay Source joined directly to
161
+ an instruction staying beside it with its authored boundary, a bare relay the
162
+ prose authored free to trail, and one occurrence standing for one authored
163
+ fragment.
161
164
 
162
165
  ## Deterministic rewriting
163
166
 
@@ -168,9 +171,8 @@ node "<definition-directory>/prefix-prompts.mjs" --source "<source.gears.md>" --
168
171
  ```
169
172
 
170
173
  It reads the source, rewrites every item eligible by the mechanical rules
171
- above, writes the one provenance section and the target, and prints the
172
- rewritten item IDs — or reports that no item was eligible and the target
173
- equals the source.
174
+ above, removes a legacy `## Prefixed prompts` section, writes the target, and
175
+ prints the rewritten item IDs — or reports that no item was eligible.
174
176
  `--keep` excludes an item the tool would otherwise rewrite.
175
177
 
176
178
  The pass shall use the tool rather than rewrite by hand:
@@ -179,8 +181,9 @@ The pass shall use the tool rather than rewrite by hand:
179
181
  instruction line, whether an instruction depends on a relay standing before
180
182
  it; that judgment is the pass's only non-mechanical step.
181
183
  2. Run the tool with `--keep` for each item so excluded.
182
- 3. Read the target and confirm that every listed item's instructions and
183
- relays are intact and that nothing else changed.
184
+ 3. Read the target and confirm that every rewritten item's instructions and
185
+ relays are intact and that nothing changed beyond the rewritten items and
186
+ the removal of a legacy `## Prefixed prompts` section.
184
187
 
185
188
  The tool writes only the declared target and never modifies the source.
186
189
 
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "schema": "sublang.slc.pin-inputs.v1",
3
3
  "closures": {
4
- "gears2fsm": ["text2gears.md", "link.md", "workflow-contracts.json"],
5
- "prefix": ["prefix-prompts.mjs"],
4
+ "text2gears": [
5
+ "workflow-contracts.json"
6
+ ],
7
+ "gears2fsm": [
8
+ "text2gears.md",
9
+ "link.md",
10
+ "workflow-contracts.json"
11
+ ],
12
+ "prefix": [
13
+ "prefix-prompts.mjs"
14
+ ],
6
15
  "link": [
7
16
  "text2gears.md",
8
17
  "gears2fsm.md",