polydat 0.2.0

Polydat — a variates construction engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
# Parent-Gated Polydat Sub-Context Construction


The walled-off construction protocol that enforces the
chokepoint
[composition_substrate.md L1/S5](composition_substrate.md)
identifies as load-bearing. SubcontextBuilder,
ScopeKernel\<M\>, ScopeModule, the parent-walking lookup
contract, Rule-2 SharedCell write-through, the named-child
registry, and the compile_fail seals over
`bind_outer_scope` / `from_program` are specified here.

**Status:** Pushes 1–5 shipped 2026-05-08. Typed surface
landed (Phase 1); do-loop migrated and Rule 2 SharedCell
write-through implemented as compile-time AST rewrite
(Phase 2); remaining synthesisers migrated through
`build_kernel_under_parent_with_options` /
`bind_program_under_parent` bridges (Phase 3); public API
narrowed — `bind_outer_scope` and `from_program` are
`pub(crate)`, two `compile_fail` doctests guard the seal,
typed shims `instance_program` and `chain_kernel_under_parent`
cover the legitimate cross-crate cases (Phase 4); SRD-66's
result-bindings wired via `SubcontextBuilder::add_result_bindings`
with closure-binding economy on `body`/`count`/`ok`/external-write slots,
per-cycle write through `FiberBuilder::commit_op_template_write_throughs`
(Phase 5). 18 unit tests pass; full workspace + integration
green; CQL workload `full_cql_vector.yaml` compiles cleanly
through the new kernel-driven path with the dialect-detection
booleans materialising as workload-root shared cells.
**Owner:** polydat (kernel/program API), nbrs-runtime
(scope synthesis call sites)
**Cross-refs:** SRD-13c (scope model — `bind_outer_scope`,
manifest extraction), SRD-13d (op-template scope layer),
SRD-13e (scope-as-module typed contracts — the data model
this SRD's API operates on), SRD-16 (`shared` mutability),
SRD-32 (init-time fixture / pull plan — read-side analogue),
SRD-66 (the motivating use case — result-wire writes to
outer-scope `shared` wires)

---

## What this SRD specifies


The **construction protocol** for Polydat sub-contexts: how a
child kernel comes into existence as a function of a parent
kernel.

SRD-13e specifies *what* sub-contexts are (typed
`ScopeModule` with import / export contracts). SRD-67
specifies *how* they're constructed: the API surface, the
lifecycle sequence, and the parent-side responsibility that
makes cross-bindings (cell-backed externs, shared writes,
import resolution) deterministic at construction time
instead of at use time.

The load-bearing rule:

> **A Polydat sub-context instance can only come into existence
> through its parent. The parent gates construction by
> handing out a builder, accepting a closed module-matter
> artifact, and producing the child kernel as the single
> point where every cross-binding is decided.**

No ad-hoc `bind_outer_scope`-on-an-already-constructed
kernel. No "compile here, attach there" patchwork. One
typed protocol; one place where the parent/child contract
is resolved.

---

## Why this SRD now


Implementing SRD-66 §"Push 2" surfaced the cross-binding
trap: result-wires that want to write back to outer-scope
`shared` wires need an `extern` slot in the inner kernel,
but declaring `extern X` AND assigning `X := <expr>` in
the same scope produces "duplicate node name `__port_X`"
because the kernel synthesiser treats them as independent
declarations rather than coordinated lifecycle events.

The deeper problem is the same one SRD-13e diagnosed: **the
parent/child contract is implicit and resolved late.**
SRD-13e proposes typed contracts (the *what*); SRD-67
proposes the construction protocol (the *how*). The two
are complementary — typed contracts without a marshalled
construction path still allow the bypass that produces
duplicate-port errors; a marshalled construction path
without typed contracts still allows mismatched-shape
binding. Both pieces are needed.

The bug shapes that motivate SRD-67 mirror those that
motivated 13e:

1. **`bind_outer_scope` on a fully-constructed inner
   kernel.** Today's pattern: synthesise inner source,
   compile inner kernel, THEN call
   `inner.bind_outer_scope(outer)`. The inner is fully
   formed before the parent gets to inspect it; any cross-
   binding that needs structural changes to the inner
   (like "this LHS should write to my shared cell, not
   declare a new node") can't happen — the inner's program
   is already frozen.

2. **Externs declared independently of imports.** An inner
   kernel can declare `extern X: T` whether or not the
   parent has a matching export. Today the mismatch
   produces a runtime no-op
   (`bind_outer_scope` walks parent outputs and only
   attaches when the inner has a matching input —
   missing-extern silently drops the binding). The compiler
   never sees the mismatch.

3. **Multiple paths to inner-kernel creation.** The
   activity layer calls `compile_polydat_*` directly; the SRD-13d
   Phase 9 op-template synthesisers build their own
   strings; polydat's comprehension synthesis path (the
   surface backing polydat spec §9.5's `scope_once`) does
   another variant; the do-loop synthesiser does yet
   another. Each path knows its own tribal rules for what
   to extern and what to inline; none of them go through a
   parent that could enforce the contract.

The construction protocol below collapses all three into
"the parent's builder is the only way in."

---

## Vocabulary


- **Parent context.** A `ScopeKernel` (in SRD-13e terms) or
  `PolydatKernel` (today) that owns a compiled program and a
  state. It has `outputs` (named values it exposes) and
  `imports` (named values it expects from its own parent,
  recursively).
- **Sub-context builder.** An opaque value handed out by
  the parent. Borrows the parent for its lifetime;
  accumulates module-matter declarations (bindings, externs,
  outputs, lifecycle modifiers, etc.); cannot be cloned or
  outlive its parent.
- **Module-matter artifact.** The closed, immutable result
  of finalising a builder. A self-describing data structure
  carrying everything needed to compile the child: source
  expressions, declared imports, declared exports, lifecycle
  classifications. Has no live references to the parent —
  it can be moved, persisted, hashed, debug-printed.
- **Spawn.** The parent-side operation that takes a
  module-matter artifact and produces a `ScopeKernel` for
  the child. The single point where cross-bindings are
  resolved: each artifact import is matched against the
  parent's exports; each artifact export is registered as a
  child-only output; cell-backed externs are wired; init
  passes run.
- **Cross-binding.** Any wiring between parent and child
  that requires both sides to agree: shared-cell attachment
  for `shared` writes, import-export type matching, init-
  binding lifecycle propagation. SRD-67 makes every cross-
  binding happen at spawn time, never later.

---

## The construction protocol


### Step 1 — Parent yields a builder


```rust
impl<P> ScopeKernel<P> {
    /// Begin construction of a child sub-context. Takes
    /// an `Arc` of the parent — kernels are already shared
    /// via `Arc` across fibers in the nbrs-runtime layer,
    /// so this is the natural shape; an `&self` overload
    /// would force a clone for the builder's lifetime.
    /// The builder is the ONLY way to produce a child
    /// `ScopeModule` for this parent.
    pub fn subcontext_builder(self: Arc<Self>) -> SubcontextBuilder<P>;
}
```

The builder owns an `Arc<ScopeKernel<P>>` for the parent.
The parent's reference count rises by one for the builder's
lifetime; the parent can be passed around freely (other
Arcs unaffected). Dropping the builder releases its arc.

`P` is the parent's module-identity type (per §"Decision 5"
below) — the builder is parameterised by it, so the child
module identity is computed at finalize as a function of
the parent's identity, preserving the typed-handle property
SRD-13e specifies.

### Step 2 — Builder accumulates module matter


```rust
impl<P> SubcontextBuilder<P> {
    /// Declare an import: a name the child's body will
    /// reference, expecting the parent to export it.
    /// At finalize-time, the artifact records this name
    /// and its expected type; at spawn-time, the parent
    /// validates that it does export this name with a
    /// compatible type and lifecycle classification.
    pub fn import(&mut self, spec: ImportSpec) -> &mut Self;

    /// Declare an export: a named value this child
    /// produces, available to its own descendants. Modifier
    /// (`final` / `shared` / none) classifies its
    /// lifecycle; `shared` exports get cell-backed slots.
    pub fn export(&mut self, spec: ExportSpec) -> &mut Self;

    /// Add a body fragment. Free identifiers in the
    /// fragment will be resolved against this scope's
    /// imports and prior body fragments at finalize-time.
    pub fn body(&mut self, fragment: BodyFragment) -> &mut Self;

    /// Diagnostic context for compile errors and runtime
    /// panics — file path, line range, originating SRD.
    pub fn context(&mut self, ctx: SourceContext) -> &mut Self;

    /// Register a wrapper / dispenser consumer that pulls
    /// named Polydat values at runtime. Equivalent to the
    /// pre-SRD-67 `ScopeFixture::register_consumer` —
    /// folded into the builder so init-time pull-plan
    /// accumulation goes through the same single surface
    /// as everything else. The seal-into-PullPlan step
    /// happens at `finalize()` alongside compile.
    pub fn register_pull(&mut self, consumer: PullConsumer)
        -> &mut Self;

    /// Close the builder. Validates that imports match the
    /// parent's exports (types + modifiers + lifecycle
    /// classifications) and that body fragments compile
    /// against the resolved import set + prior body
    /// fragments. Seals the registered pull consumers
    /// into the artifact's `PullPlan`. Returns a closed,
    /// immutable `ScopeModule`; the parent arc held by
    /// the builder is released.
    pub fn finalize(self) -> Result<ScopeModule<Child<P>>, ContractViolation>;
}

/// Body fragments — the two shapes a builder accepts.
pub enum BodyFragment {
    /// User-facing Polydat source (the content of `bindings:`
    /// / `result:` block strings). Parsed into
    /// `Vec<Statement>` at finalize.
    GkSource(String),

    /// Pre-parsed statements, for synthesisers that
    /// construct Polydat programmatically (the activity-layer
    /// scope synthesisers under SRD-13d, polydat's
    /// comprehension synthesis path, etc.).
    Statements(Vec<crate::dsl::ast::Statement>),
}
```

Notes:

- `Statement` is reused from `polydat/src/dsl/ast.rs`
  — no parallel enum. Synthesisers that already produce
  `Statement`s can submit them directly without round-
  tripping through Polydat source strings.
- `Child<P>` is a phantom-marker type that brands the
  resulting `ScopeModule`'s identity as "a child of `P`."
  Spawn returns `ScopeKernel<Child<P>>`, distinct from any
  sibling's `ScopeKernel<Child<P>>` instance at the value
  level (each spawn produces a fresh kernel with a fresh
  state) but type-compatible at the module-identity level
  (handles bound to one sibling can't be applied to
  another — the value-level distinction enforces that even
  when types coincide).

Notes:

- The builder's methods take `&mut self` and return
  `&mut Self`, so the builder pattern composes fluently.
- `finalize` consumes the builder — the parent borrow ends
  at this point. The artifact is now a free-standing value.
- Validation happens at `finalize`, not at each `body`
  call. The body might reference identifiers that aren't
  yet declared as imports (the imports could be added
  later); finalize is the point where everything must
  resolve.

### Step 3 — Artifact is a closed value


The closed artifact IS SRD-13e's `ScopeModule` type — same
data, single name across both SRDs. SRD-67 retains
"module matter" as descriptive vocabulary for what the
type carries, not as a competing type name.

```rust
pub struct ScopeModule<M> {
    /// Imports (typed) the child requires.
    imports: Vec<ImportSpec>,

    /// Exports (typed) the child produces.
    exports: Vec<ExportSpec>,

    /// Compiled program plus the typed handle bundle
    /// (SRD-13e §1.2).
    program: Arc<GkProgram>,
    contract: ScopeContract<M>,

    /// Diagnostic context.
    context: SourceContext,
}
```

`M` is the module-identity type — `Child<P>` for a module
built under parent `P` via this protocol. The phantom
parameter makes spawn's return type
`ScopeKernel<Child<P>>` distinguishable from any other
spawn target.

Key properties:

- **No live reference to the parent.** The artifact
  encodes everything needed for the parent to spawn — type
  contracts, the compiled program, the contract handles —
  but it doesn't borrow the parent. The parent's arc that
  the builder held is released at finalize. Artifacts can
  be moved, stored in a `Vec`, hashed for caching,
  serialised for inspection.
- **Self-describing.** Anyone holding a `ScopeModule<M>`
  can inspect its imports without running the spawn.
  Useful for `nbrs describe` tooling and for caching
  identical artifacts across many parent instances (e.g.
  one op-template module artifact spawning under one
  parent per fiber).
- **Immutable.** No `add_binding` after finalize. Anything
  the child needs to know is captured.

### Step 4 — Parent spawns the child kernel


```rust
impl<P> ScopeKernel<P> {
    /// Construct a child kernel from a `ScopeModule`
    /// artifact. This is the SINGLE place where cross-
    /// bindings are resolved:
    ///
    /// - Each artifact import is matched against `self`'s
    ///   exports. Type / lifecycle mismatches surface here
    ///   as a `ContractViolation`.
    /// - Cell-backed externs (artifact imports where
    ///   `self` exports a `shared`-modifier output) get
    ///   their `SharedCell` attached.
    /// - Imports flagged `compile-const` get folded into
    ///   the child's compile-time constants (parent's
    ///   `final` exports are sufficient sources here).
    /// - The child's `ScopeInit` lifecycle nodes evaluate
    ///   once, after extern wiring, before the kernel is
    ///   handed back. This replaces the "remember to call
    ///   init pull after bind_outer_scope" pattern with
    ///   an unconditional spawn-time step.
    /// - The scope-coordinates path is computed and frozen.
    /// - The parent's named-child registry records the
    ///   spawn under `name`; a second spawn with the same
    ///   `name` is a `ContractViolation::DuplicateChild`.
    ///   See §"Named-child registry" below.
    ///
    /// `name` identifies the LOGICAL sub-kernel — typically
    /// the workload yaml's phase / op-template / scope
    /// label. It's the unit at which "spawn this once"
    /// applies. Per-fiber GkState cloning happens via the
    /// existing Polydat API on the returned `ScopeKernel`, NOT
    /// via re-spawn (see §"Compile once, spawn once,
    /// fiber-state separately").
    ///
    /// Returns the spawned child as a `ScopeKernel<Child<P>>`.
    /// The returned kernel's module-identity type is bound
    /// to the parent's via `Child<P>`; handles issued by
    /// the spawned kernel can't be applied against a
    /// sibling spawn at the type level.
    pub fn spawn(
        self: &Arc<Self>,
        name: ChildName,
        artifact: ScopeModule<Child<P>>,
    ) -> Result<ScopeKernel<Child<P>>, ContractViolation>;
}
```

Spawn takes `&Arc<Self>` so the parent's arc isn't moved
into spawn — the spawned child holds its own arc to the
parent (for shared-cell binding lifetimes), and the caller
keeps using the parent for further spawns of sibling
children.

`ChildName` is a structured identifier (`PathBuf`-shaped:
hierarchical, comparable, debug-printable). The runtime
typically constructs it from the workload's scope-tree
node label.

---

## Compile once, spawn once, fiber-state separately


A `ScopeModule<M>` is compiled exactly once per logical
scope — at workload-load time, by the
synthesisers / parsers that produce it. Spawn is exactly
once per parent → named-child relationship. Per-fiber
state is a separate concern handled by the existing GK
API:

| Concern               | Cardinality                      | API                                |
|-----------------------|----------------------------------|------------------------------------|
| Compile               | Once per logical scope           | `SubcontextBuilder::finalize()`    |
| Spawn                 | Once per (parent, named-child)   | `ScopeKernel::spawn(name, module)` |
| Per-fiber GkState     | Once per (fiber, kernel)         | Existing Polydat state-clone on the returned `ScopeKernel<M>` |

Spawn is NOT called per fiber. The spawned `ScopeKernel<M>`
carries an `Arc<GkProgram>` (the compiled program — shared
freely across fibers) plus a "canonical" `PolydatState`; each
fiber receives its own state clone via the existing
per-fiber clone machinery, not via a fresh spawn.

This separation matters for two reasons:

1. **Cross-binding shouldn't repeat.** Spawn's contract-
   resolution work (cell attachment, init pulls, scope-
   coord freeze) is per-scope, not per-fiber. Re-spawning
   per fiber would redo all of it for no benefit and
   risk inconsistency across fibers if the parent state
   shifted between spawns.
2. **Module identity is per logical scope.** Two fibers
   running the same op-template see the same logical
   `ScopeKernel<Child<P>>`. They diverge only at the
   GkState level, which is the right boundary — typed
   handles issued at spawn time stay valid across all
   fibers' state instances.

The named-child registry below makes this discipline
enforceable: re-spawn would be caught at the API boundary,
not as a subtle correctness drift downstream.

---

## Named-child registry


Each parent `ScopeKernel<P>` maintains an internal
registry of names it has spawned children under. The
registry is part of the parent's structural state, not a
side-channel; it lives alongside the parent's exports and
shared cells.

### Spawn semantics


- `parent.spawn(name, module)` records `name` in the
  registry. The recorded entry includes the spawn site's
  diagnostic context (so the duplicate diagnostic can
  point to both spawns).
- A second `parent.spawn(name, _)` with the same `name`
  returns `ContractViolation::DuplicateChild { name,
  prior_site, this_site }`. The runtime never silently
  overwrites; the operator must either pick a different
  name or drop the prior child first (see "release"
  below).

### Release semantics


A spawned child can be released (e.g. when a scope ends
and the kernel goes out of use):

```rust
impl<P> ScopeKernel<P> {
    /// Drop the named child from this parent's registry.
    /// The child kernel itself is unaffected — only the
    /// registry entry. After release, the same name may
    /// be spawned again (typical for scope-tree
    /// re-traversal across iterations).
    pub fn release_child(&self, name: &ChildName);
}
```

For comprehension scopes that iterate (e.g. `for_each`
re-spawning the inner scope per iteration tuple), the
loop releases the prior child before spawning the next:

```rust
for tuple in iter {
    if let Some(prior) = prior_child {
        parent.release_child(prior.name());
        drop(prior);
    }
    let child = parent.spawn(tuple.scope_name(), module.clone())?;
    ...
}
```

This is explicit by design: per-iteration re-spawn is a
valid pattern (same logical module, fresh per-iteration
state); but the writer must opt in by releasing first.
The default — silently allowing duplicate spawn — is the
shape that produced the bug class this SRD is solving.

### Why named, not anonymous


Two reasons:

1. **Diagnostics.** When a workload misconfiguration
   produces a duplicate spawn, the error names which
   logical sub-kernel collided. Without names, the only
   diagnostic available is "two spawns under the same
   parent" with no way to identify which one is the
   accident.
2. **Cross-cutting tooling.** `nbrs describe op` and
   similar walks the scope tree by name. Aligning the
   spawn-time name with the workload yaml's scope label
   means the protocol's registry IS the discovery
   surface; no parallel "what scopes exist" map needed.

`ChildName` should match the scope-tree node naming used
elsewhere in nbrs-runtime (per SRD-13d / SRD-18b's scope-
tree pre-walk). The runtime constructs the name
structurally; user-visible identifiers (phase names, op-
template names, iteration coords) compose into it.

`spawn` is the chokepoint. Every cross-binding decision
happens here. The parent has full information: it knows
its own exports, it sees every artifact import, it can
detect that `artifact.exports` contains a name that's also
one of `self.exports` and route the child's write through
the parent's shared cell rather than declare a new local
output.

This is what unsticks the SRD-66 collision:

- Workload root has `shared X := false` (export with
  `shared` modifier).
- Op-template artifact declares an export `X := <expr>`
  (the result-wire assignment).
- At spawn time, the parent recognises `X` as a name it
  itself exports with `shared`. Instead of letting the
  child declare a new output `__port_X`, the parent rewrites
  the spawn to route the child's `X := <expr>` evaluation
  to a write-through node that calls
  `parent.shared_cell(X).set(value)` per cycle.
- The duplicate-port error is structurally impossible
  because the parent owns the resolution; there's no path
  by which the child can declare `X` independently.

---

## Cross-binding rules


The spawn step applies these rules in order; each is a
hard error if it fails.

### Rule 1 — Import resolution


Every artifact import name must exist as an export on the
parent (or a transitive ancestor reachable through the
import chain). If not: `ContractViolation::UnboundImport`
naming the artifact's source context, the import name, and
the closest match among parent exports.

Type / modifier compatibility:

| Artifact import | Parent export                     | Resolution                           |
|-----------------|-----------------------------------|--------------------------------------|
| `final X: T`    | `final X: T` (or compile-const)   | Fold parent's value into child       |
| `extern X: T`   | any modifier, type T              | Wire input slot to parent value      |
| `shared X: T`   | `shared X: T`                     | Share cell                           |
| `shared X: T`   | non-shared `X: T`                 | Error — `ContractViolation::Modifier`|
| `extern X: T`   | export `X: U` (T ≠ U, no widen)   | Error — `ContractViolation::Type`    |

### Rule 2 — Export collision with parent


When an artifact export `X` matches a parent export `X`
(same name), spawn checks the parent's modifier:

- Parent `shared X := init` + child `X := <expr>`  spawn rewrites the child's assignment as a write-
  through to the parent's shared cell. The child's local
  output table does NOT include `X`; reads of `X` in the
  child resolve to the parent's cell-backed value (via the
  cell's read path, picking up the latest write).
- Parent `final X := ...` + child `X := <expr>`  `ContractViolation::FinalShadow`. The child can't
  redefine an immutable parent export.
- Parent has no `X` export, child exports `X`  child-only export, registered locally.

This rule is the crux of SRD-66's blocker: the parent
recognises that the child's `X := <expr>` is a write-
through to the shared cell, not a duplicate declaration.

### Rule 3 — Lifecycle propagation


`ScopeInit` lifecycle nodes in the child run once at
spawn, after extern wiring (so the inits see post-bind
values, not the compile-time fold default). This replaces
the "post-bind init pull" pattern in
`OpBuilder::create_fiber_builder` with a spawn-time step.

`ScopeShared` lifecycle nodes — the `shared X := init`
declarations on the child itself — are materialised at
spawn: a `SharedCell` is created on the child's input
slot, initialised to the literal, and exposed as a child
export with `shared` modifier so the child's own children
can bind cells against it.

### Rule 4 — Coordinate / iteration externs


When an artifact body references a name classified by the
parent as a `Coordinate` export (iteration variable in a
comprehension), spawn routes the read through the parent's
coordinate buffer rather than declaring a child-side
extern slot. This codifies the SRD-13d Phase 9 followup
fix that today is a manual rule in the synthesiser.

### Rule 5 — Closure-binding economy


An artifact import is materialised as a child-side input
slot ONLY if the artifact's body actually references it.
The compiler walks the body's free identifiers; unused
imports are dropped at spawn (not ignored — dropped, with
no slot, no per-cycle write). This is the
"closure bindings only where Polydat module matter detects
linkages" rule from SRD-66 §"Compilation lifecycle."

---

## Walled-off invariant


The `polydat` crate's public API exposes ONLY:

- `ScopeKernel<M>` (the typed kernel handle).
- `Arc<ScopeKernel<P>>::subcontext_builder() -> SubcontextBuilder<P>`.
- `SubcontextBuilder<P>` methods: `import`, `export`,
  `body`, `context`, `register_pull`, `finalize`.
- `ScopeModule<M>` (the closed artifact, defined by SRD-13e).
- `ScopeKernel<P>::spawn(name, module) -> ScopeKernel<Child<P>>`.
- `ScopeKernel<P>::release_child(name)`.
- `ChildName`, `PullConsumer`, `ImportSpec` / `ExportSpec`
  / `BodyFragment` / `SourceContext` / `ContractViolation`
  / `Child<P>` / `ScopeContract<M>` (re-exported from SRD-
  13e where applicable).

It does **NOT** expose:

- `PolydatKernel::new` / `PolydatKernel::compile` style direct
  constructors that bypass the builder.
- `bind_outer_scope` as public surface (becomes
  `pub(crate)`, called only from `spawn`).
- `from_program` as public surface (becomes
  `pub(crate)`, called only from `spawn`).
- Any `set_input` path on a kernel that hasn't been
  spawned through the protocol.

The crate's tests can use the internal surfaces; consumers
(including `nbrs-runtime`) can't. This is the "walled-off"
property the user's guidelines call for.

Direct compile-from-string (`compile_polydat(src)`) stays as a
test / scratch utility but produces a kernel with no
parent — the result can't be used as a child. To use it
as a parent, you call `subcontext_builder()` on it like
any other kernel. To use it as a child, you compile via
the builder.

---

## What disappears


When this SRD lands, the following call sites — each of
which is a manual cross-binding implementation — collapse
into `parent.subcontext_builder() ... .spawn()`:

1. `nbrs-runtime/src/synthesis.rs::build_op_template_scope_kernel`
   — string-concatenated externs, hand-rolled bind, manual
   init pull.
2. `nbrs-runtime/src/scope_tree.rs::synthesize_for_each_scope`
   — comprehension scope synthesis with iteration-var
   externs.
3. `nbrs-runtime/src/scope_tree.rs::build_do_loop_scope_kernel`
   — do-loop scope synthesis with counter externs.
4. `nbrs-runtime/src/scope.rs::build_scope` — phase scope
   synthesis with cascade externs.
5. The `bind_outer_scope` + `propagate_parent_inputs` +
   `mark_inherited_outputs` + post-bind init-pull dance in
   `OpBuilder::create_fiber_builder` — all of it lives
   inside `spawn` after this SRD.

Each of these becomes a "fill the builder, finalize,
spawn" sequence:

```rust
// `parent_kernel: Arc<ScopeKernel<WorkloadRoot>>`
let mut b = parent_kernel.clone().subcontext_builder();
b.context(SourceContext::for_phase(phase_name));
for (name, ty) in iter_vars {
    b.export(ExportSpec::iter_var(name, ty));
}
b.body(BodyFragment::GkSource(phase.bindings.as_str().into()));
// Fixture / pull-plan accumulation happens through the
// same builder — `register_pull` replaces SRD-32's
// separate `ScopeFixture::register_consumer`.
for consumer in dispenser_pull_consumers(phase) {
    b.register_pull(consumer);
}
let phase_module: ScopeModule<Child<WorkloadRoot>> = b.finalize()?;
let phase_kernel: ScopeKernel<Child<WorkloadRoot>> =
    parent_kernel.spawn(ChildName::phase(phase_name), phase_module)?;
```

The type chain (`WorkloadRoot` → `Child<WorkloadRoot>`)
makes the parent/child relationship visible at the type
level. Handles issued by `phase_kernel` carry
`Child<WorkloadRoot>` and can't be applied to a sibling
spawn (different `phase_kernel` instance) thanks to the
SRD-13e typed-handle property — value-level identity
catches what type-level identity alone can't.

No more strings. No more "remember to call X after Y". No
more synthesiser-specific tribal rules.

---

## Lifecycle boundary contract


The protocol enforces these lifecycle boundaries by
construction:

| Boundary             | Before SRD-67                                         | After SRD-67                                          |
|----------------------|-------------------------------------------------------|-------------------------------------------------------|
| Parent active        | Implicit (no relationship)                            | Explicit — builder borrows `&parent`                  |
| Module matter open   | While string is being built up                        | While builder exists                                  |
| Module matter closed | When `compile_polydat` returns                             | At `builder.finalize()`                               |
| Child constructed    | Some time after compile, after `bind_outer_scope`     | At `parent.spawn(artifact)`                           |
| Cross-bindings live  | After `bind_outer_scope` (and later `set_input` calls)| At spawn return — single moment                       |
| Child handles valid  | While the program object exists                       | While the parent and child kernels both exist (typed) |

The "single moment" property is the load-bearing one. Today
cross-bindings can be added, modified, or omitted at any
point after compile; that means any code path that gets a
kernel handle can affect the binding shape. After SRD-67,
the only place cross-bindings happen is `spawn`, and any
mutation after that is a violation of the kernel's frozen
contract.

---

## Composition with SRD-13e


SRD-13e specifies the `ScopeModule` data shape and typed
contracts. SRD-67 specifies the construction API.

The mapping is direct:

- `ScopeModule<M>` is the same type in both SRDs — SRD-13e
  defines it; SRD-67 produces it via the builder. SRD-67's
  prose uses "module matter" as descriptive vocabulary
  for what the type carries, but the type name in the
  Rust API is `ScopeModule<M>` (Decision 2).
- SRD-13e's `ImportSpec` / `ExportSpec` / `ScopeContract` /
  `ScopeKernel` types stay as 13e specifies; SRD-67
  re-exports them through its public surface (Decision 3).
- SRD-13e's `ScopeModule::instance_under(parent)` is
  SRD-67's `parent.spawn(artifact)`. Same operation, named
  from the parent's perspective in 67 to emphasise that
  the parent is the agent. The two SRDs converge on a
  single `spawn` method; `instance_under` (if it appears
  in 13e prose) is renamed to match.

If 13e and 67 land together, the implementation merges
their migration plans. If 13e ships first, SRD-67's
contribution is the API surface (builder, walled-off
construction). If 67 ships first, SRD-13e's typed
contracts layer on top of 67's API once the
`ImportSpec` / `ExportSpec` types land.

---

## Composition with SRD-66


The motivating use case. After SRD-67 lands:

1. Workload root compiles as a top-level
   `ScopeKernel<WorkloadRoot>`.
   `shared has_sai_column_indexes := false` becomes an
   export with `shared` modifier — a `SharedCell` is
   created at workload-root construction.
2. The `detect_dialect` phase's `ScopeModule
   <Child<WorkloadRoot>>` is built via
   `workload_root.clone().subcontext_builder()`. The
   builder records `result: |\n
   has_sai_column_indexes := …` as exports.
3. At `workload_root.spawn(detect_dialect_module)`,
   Rule 2 (export collision with parent) fires:
   `has_sai_column_indexes` exists as a parent `shared`
   export. The phase module's `:= <expr>` is rewritten
   as a write-through to the parent's shared cell.
4. Per cycle, the phase's expression evaluates and the
   value flows to the parent's cell.
5. The `await_index` phase compiles as another
   `ScopeModule<Child<WorkloadRoot>>` and spawns under the
   same workload root. Its `pick(has_sai_column_indexes,
   …)` reads from the parent cell, which now carries the
   probe's value.

No `extern` declarations needed in the workload YAML. No
duplicate-port errors. No special-case for the result-wire
write path — it's the same Rule 2 that handles every
shared-cell write. The type chain makes the
detect-then-consume ordering visible at the API layer:
both phase kernels share `Child<WorkloadRoot>` so handles
can flow between them via the parent's exports.

---

## Migration plan


### Phase 1 — Add the surface (additive)


- Implement `SubcontextBuilder<P>`, `ScopeModule<M>` (if
  not already landed by SRD-13e), `BodyFragment`,
  `Child<P>` phantom marker, `ChildName`, `PullConsumer`,
  the parent's named-child registry, `ScopeKernel<P>
  ::subcontext_builder`, `ScopeKernel<P>::spawn`,
  `ScopeKernel<P>::release_child` in `polydat`.
- The implementation reuses the existing `compile_polydat`,
  `bind_outer_scope`, `from_program` machinery internally.
  No semantic change; the builder is a typed shim.
- `bind_outer_scope` and friends stay public temporarily
  for the existing call sites to keep working.
- `ScopeFixture` (SRD-32) gets `pub(crate)` /
  deprecation-flag treatment; its `register_consumer`
  bridges to `SubcontextBuilder::register_pull` for the
  call sites that haven't migrated yet.

### Phase 2 — Migrate one synthesiser


Pick the simplest one — probably `build_do_loop_scope_kernel`
— and rewrite it to go through the builder. Validate that
the runtime behaviour is byte-identical.

### Phase 3 — Migrate the rest


`build_op_template_scope_kernel`, `synthesize_for_each_scope`,
`build_scope`, the `OpBuilder` post-bind dance. Each is its
own commit. Per-step validation against existing test
suites.

### Phase 4 — Lock the door


`bind_outer_scope`, `from_program`, direct `compile_polydat`
become `pub(crate)`. The `nbrs-runtime` crate compiles
only against the new public surface.

### Phase 5 — Wire SRD-66's polydat-call form


With the protocol in place, the result-wire kernel-driven
path is a small additional builder method —
`SubcontextBuilder::add_result_bindings(source)` — that
parses the result-bindings and adds the LHS names as
exports. Spawn applies Rule 2 to route shared writes; the
duplicate-port collision is gone by construction.

---

## Strict mode


Per SRD-15, the following promote from warn → error under
`--strict`:

- An artifact with an unused import (declared but not
  referenced in the body). Today the closure-binding
  economy silently drops these; under strict, declaring
  an unused import is a hint that the workload author
  expected something to bind that doesn't.
- An artifact with an export that no descendant ever
  imports (workload-resolve-time check). Suggests an
  unused declaration.

Always-error, strict-independent:

- Unbound import (Rule 1).
- Type mismatch on import (Rule 1).
- Final-shadow on export (Rule 2).
- Direct construction of a kernel bypassing the protocol
  — caught at compile time by the public-API constraint
  (the constructors aren't exposed).

---

## Out of scope


- **Persistence of `ScopeModule<M>` artifacts.** Caching
  artifacts across runs (for resume / hot-reload) is a
  future concern. The artifact is `Serialize`-friendly by
  construction, but the serialisation format and cache
  invalidation rules belong in their own SRD.
- **Multi-parent spawn.** A child with two parents (e.g.
  inheriting from both the workload root and a sibling
  phase's exports) is not in scope. The protocol assumes
  a single parent at spawn time.
- **Hot rebinding.** Changing the parent of a constructed
  child is impossible by construction. If a workload needs
  a child to bind against a different parent, it spawns a
  fresh child from a fresh artifact under the new parent.
- **Direct Polydat source compilation as a public surface.**
  `compile_polydat(src)` stays for tests, but produces a parent-
  less kernel that can only be used as a parent (via
  `subcontext_builder()`), never a child. Workload
  consumers always go through the builder.

---

## Decisions made


1. **Builder pattern stays.** The user's original
   guidelines explicitly named "get a Polydat sub-context
   builder from the parent" as step 1 — the fluent
   construction surface IS the design, not a wrapper. The
   builder lets imports / exports / body fragments be
   added incrementally (matters for synthesisers that
   discover declarations during a walk) at the cost of
   one extra type beyond `ScopeModule` itself.

2. **Single name: `ScopeModule`.** Same type as SRD-13e.
   SRD-67's prose uses "module matter" as descriptive
   vocabulary for the type's contents, but the type name
   in the Rust API is `ScopeModule<M>`. Avoids the
   parallel-names-for-the-same-thing trap.

3. **`ImportSpec` / `ExportSpec` re-exported from
   SRD-13e.** The typed contract types are 13e's
   contribution; SRD-67 re-exports them rather than
   redefining. One source of truth; one place where
   contract evolution happens.

4. **`BodyFragment` is a small enum.**
   `GkSource(String)` for user-facing `bindings:` /
   `result:` content (parsed at finalize), and
   `Statements(Vec<Statement>)` for synthesisers that
   produce Polydat programmatically (the existing comprehension
   synthesis path, do-loop walker, op-template walker).
   Reuses polydat's public `Statement` type directly — no
   parallel enum.

5. **Parent ref: `Arc<ScopeKernel<P>>` only.** Single API
   signature on `subcontext_builder`. Kernels in
   `nbrs-runtime` are already shared via `Arc` across
   fibers; an `&self` overload would force a clone for
   the builder's lifetime. Callers that have `&kernel`
   wrap with `Arc::new(...)` (rare) or use the existing
   `Arc<ScopeKernel<P>>` (common case). The single shape
   keeps the public surface minimal.

6. **`spawn` returns `ScopeKernel<Child<P>>` (typed by
   module identity).** Inherits SRD-13e's typed-handle
   safety property: handles issued by one spawn can't be
   applied to a sibling spawn at the type level. The
   `Child<P>` phantom marker brands the child's identity
   as a function of the parent's; spawn's return type is
   a function of the parent type. Builder construction
   carries `P` through so the type chain stays intact.

7. **`ScopeFixture` (SRD-32) folds into the builder.**
   Pre-SRD-67 the fixture was a separate init-time
   accumulator owned by the wrapper-construction site;
   under this SRD it's `SubcontextBuilder::register_pull`,
   sharing the builder's lifecycle and seal point. There
   is no `ScopeFixture` type after this SRD lands. The
   pull-plan output is sealed into the artifact at
   `finalize()` and made available through the spawned
   kernel's typed handle bundle.

8. **Compile once, spawn once, fiber-state separate.**
   `ScopeModule<M>` is compiled exactly once per logical
   scope; `spawn` is exactly once per (parent, named-
   child); per-fiber `PolydatState` cloning happens via the
   existing Polydat API on the spawned kernel, NOT via re-
   spawn. Conflating these would re-run cross-binding
   work per fiber — wasteful and risk-prone if the parent
   shifted between spawns. See §"Compile once, spawn
   once, fiber-state separately" for the load-bearing
   table.

9. **Named-child registry; duplicate spawn is an error.**
   Each parent tracks the names it has spawned children
   under. A second spawn with the same name returns
   `ContractViolation::DuplicateChild`, naming both
   spawn sites. Comprehension iteration explicitly
   releases the prior child before spawning the next
   tuple; per-iteration re-spawn is a valid pattern but
   must opt in by calling `release_child(name)`. The
   default — silently allowing duplicate spawn — is the
   shape that produces the bug class this SRD addresses.

---

## Open questions


(All three open items from the prior draft were resolved
by review feedback and folded into the body of the SRD as
load-bearing rules. They're recorded here as the decision
trail.)

1. ~~Builder vs `ScopeFixture` duplication~~**resolved.**
   `ScopeFixture` is absorbed into `SubcontextBuilder`.
   See §"Construction protocol §Step 2" for the merged
   surface and §"What disappears" for the call-site
   collapse. There is no `ScopeFixture` type after this
   SRD lands; the builder is the single init-time
   accumulator.
2. ~~Compile vs spawn cost; caching~~**resolved.**
   `ScopeModule<M>` is compiled once per logical scope.
   Spawn is the per-scope-instance step that produces a
   `ScopeKernel<M>`. Per-fiber concerns are orthogonal —
   they're handled by Polydat's existing program / state
   separation (`PolydatProgram` shared, `PolydatState` cloned per
   fiber); spawn is NOT called per fiber. See §"Compile
   once, spawn once, fiber-state separately" for the
   load-bearing rule.
3. ~~Parallel spawn concurrency~~**resolved.**
   Recast as a safety constraint: duplicate spawn of the
   same logical sub-kernel name under the same parent is
   an error. Parents track their named children
   internally. See §"Named-child registry" for the rule.

---

## See also


- SRD-11 — Polydat evaluation lifecycles (the
  `compile-const` / `scope-init` / `dynamic` taxonomy).
- SRD-13c — Scope model (today's `bind_outer_scope`
  surface).
- SRD-13d — Op-template scope (a current consumer of the
  ad-hoc protocol).
- SRD-13e — Scope-as-Module (the typed contracts SRD-67's
  protocol operates on).
- SRD-16 — Mutability rules (`shared` semantics, the
  cell-backed export lifecycle).
- SRD-32 — Init-time fixture / pull plan (the read-side
  surface). SRD-67 absorbs `ScopeFixture` into
  `SubcontextBuilder::register_pull`; SRD-32's pull-plan
  spec (the data shape, the per-consumer contract) stays
  as 32 specifies. The accumulator-side surface unifies;
  the plan-side data shape doesn't change.
- SRD-66 — Runtime feature detection (the motivating
  consumer; result-wires write to outer-scope `shared`
  via Rule 2).