From 15dc97272364b741d13dafcb795ee25daddad31c Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:22:00 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Substitute=20?= =?UTF-8?q?=20at=20every=20position=20in=20a=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `` was replaced only where a body's segments were mapped directly: a top-level segment, or a direct child of a top-level ``. Written anywhere else — inside another invocation, inside a structural construct, or several levels down — it survived substitution, reached component resolution, and failed as a reserved name. Substitution now descends through the body. Two identity rules the top-level path never exercised come with it: projection leaves a resolved `` alone, because a copy loses the claim that tells it apart from one an author wrote where nothing projects; and reading a slot consumes its `slot` prop, so a resolved projection nested inside another invocation partitions there by position rather than being read a second time as that invocation's named slot. Closes #328 --- packages/core/src/expand.ts | 32 ++++- packages/core/tests/invocation-scope.test.ts | 16 +++ packages/core/tests/named-slots.test.ts | 117 +++++++++++++++++++ specs/executable-mdx-spec.md | 56 +++++++-- 4 files changed, 205 insertions(+), 16 deletions(-) diff --git a/packages/core/src/expand.ts b/packages/core/src/expand.ts index 59204093..7fa6ed9d 100644 --- a/packages/core/src/expand.ts +++ b/packages/core/src/expand.ts @@ -2796,13 +2796,16 @@ function makeProjectFn(callerEnv: EvalEnv | undefined): ProjectFn { return segments; } return segments.map((seg) => { - if (seg.type === "component") { + if (seg.type === "component" && seg.name !== "Content") { return { ...seg, projectedEnv: callerEnv, children: project(seg.children), }; } + // A `` passing through is a projection the invocation that + // wrote it already resolved: its content carries that invocation's env, + // and it is recognized by identity, which a copy would lose. return seg; }); }; @@ -2815,6 +2818,11 @@ function makeProjectFn(callerEnv: EvalEnv | undefined): ProjectFn { * expansion frame is installed, so a current binding named `props` is used. * Slot validation errors are emitted once, at the first projection point, * tracked via the shared `state`. + * + * A projection point is wherever the body writes one, so this descends through + * every authored element on the way. Only the body is walked: what a projection + * resolves to belongs to the caller, and the caller's own body already + * substituted it. */ function substituteSegmentList( segments: Segment[], @@ -2824,7 +2832,10 @@ function substituteSegmentList( claim: ClaimFn, ): Segment[] { return segments.flatMap((segment): Segment[] => { - if (segment.type === "component" && segment.name === "Content") { + if (segment.type !== "component") { + return [segment]; + } + if (segment.name === "Content") { const targetSlot = segment.props.slot; const pendingErrors = !state.errorsEmitted ? slots.errors : []; if (pendingErrors.length > 0) { @@ -2839,10 +2850,23 @@ function substituteSegmentList( targetSlot !== undefined ? project((slots.named.get(String(targetSlot)) ?? []).map(stripSlotProp)) : project(slots.default); - const element: ComponentElement = { ...segment, children: projected, selfClosing: false }; + // `slot` names which of this invocation's slots to read, and reading + // them consumes it (§6.3.5). A resolved projection nested inside another + // invocation is ordinary content there, so it partitions by position. + const { slot: _, ...props } = segment.props; + const element: ComponentElement = { + ...segment, + props, + children: projected, + selfClosing: false, + }; return [...pendingErrors, claim(element)]; } - return [segment]; + const children = substituteSegmentList(segment.children, slots, project, state, claim); + const untouched = + children.length === segment.children.length && + children.every((child, index) => child === segment.children[index]); + return untouched ? [segment] : [{ ...segment, children }]; }); } diff --git a/packages/core/tests/invocation-scope.test.ts b/packages/core/tests/invocation-scope.test.ts index 25f77102..79aa72f4 100644 --- a/packages/core/tests/invocation-scope.test.ts +++ b/packages/core/tests/invocation-scope.test.ts @@ -154,6 +154,22 @@ describe("Tier O — Eval scope hierarchy", () => { expect(timeline).toEqual(["start:projected", "start:own", "stop:projected", "stop:own"]); }); + // O41: the same contract when the projection point sits inside another + // invocation. The wrapper completes before the provider retains its own + // resource, so a projection that had moved into the wrapper's scope would + // stop before `start:own` rather than after it. + it("O41: a nested keeps the invocation's content scope", function* () { + const timeline: string[] = []; + const definitions = { + Provider: markdown("Provider", `\n\n${WATCH_BLOCK_OWN}`), + Wrapper: markdown("Wrapper", ""), + }; + + yield* expandAll(`\n${WATCH_BLOCK}\n`, definitions, timeline); + + expect(timeline).toEqual(["start:projected", "start:own", "stop:projected", "stop:own"]); + }); + // O11/O12: the same teardown order on a propagated body error, for both // component forms. The body throws after projecting, so the projected // resource is still alive when the invocation starts unwinding. diff --git a/packages/core/tests/named-slots.test.ts b/packages/core/tests/named-slots.test.ts index 706f1276..4c8401b8 100644 --- a/packages/core/tests/named-slots.test.ts +++ b/packages/core/tests/named-slots.test.ts @@ -1030,3 +1030,120 @@ describe("Tier NS-G — Boundary scanner", () => { expect(segments[0]!.type).toBe("text"); }); }); + +// ═══════════════════════════════════════════════════════════════════════════ +// Tier NS-H — Nested projection points +// ═══════════════════════════════════════════════════════════════════════════ + +describe("Tier NS-H — Nested projection points", () => { + it("NS-H1: inside another component invocation", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", "prefix suffix"); + const segments = scanSegments("MATERIAL"); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host }); + expect(renderSegments(expanded)).toContain("ECHO(prefix MATERIAL suffix)"); + }); + + it("NS-H2: several levels deep", function* () { + const inner = makeComponent("Inner", "IN()"); + const outer = makeComponent("Outer", "OUT{}"); + const host = makeComponent("Host", "deep "); + const segments = scanSegments("MATERIAL"); + const expanded = yield* expandAll(segments, { Inner: inner, Outer: outer, Host: host }); + expect(renderSegments(expanded)).toContain("OUT{IN(deep MATERIAL)}"); + }); + + it("NS-H3: named slot in a nested position", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", '\n\n'); + const headerComp = makeComponent("Header", "HEADER-TEXT"); + const segments = scanSegments('\n
\ndefault text\n'); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host, Header: headerComp }); + const output = renderSegments(expanded); + expect(output).toContain("ECHO(HEADER-TEXT)"); + expect(output).toContain("default text"); + expect(output.indexOf("ECHO(HEADER-TEXT)")).toBeLessThan(output.indexOf("default text")); + }); + + it("NS-H4: two projections at different depths receive the same content", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", "top: \n\n"); + const segments = scanSegments("MATERIAL"); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host }); + const output = renderSegments(expanded); + expect(output).toContain("top: MATERIAL"); + expect(output).toContain("ECHO(MATERIAL)"); + }); + + it("NS-H5: nested inside a top-level region", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", ""); + const segments = scanSegments("MATERIAL"); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host }); + expect(renderSegments(expanded)).toContain("ECHO(MATERIAL)"); + }); + + it("NS-H6: slot validation errors are still emitted once", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", "\nsecond: "); + const bad = makeComponent("Bad", "BAD"); + const segments = scanSegments('\n\n'); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host, Bad: bad }); + const errors = expanded.filter((segment) => segment.type === "error"); + expect(errors).toHaveLength(1); + }); + + it("NS-H7: nested inside a structural construct", function* () { + const tmpDir = makeTempDir(); + try { + writeFiles(tmpDir, { + "components/HostCap.md": [ + "---", + "props: { type: object, properties: {}, additionalProperties: false }", + "---", + 'captured: ', + "host got: {c}", + ].join("\n"), + "doc.md": "MATERIAL", + }); + const output = yield* collect( + yield* execute({ + path: path.join(tmpDir, "doc.md"), + stream: new InMemoryStream(), + componentDirs: [path.join(tmpDir, "components"), tmpDir], + }), + ); + expect(output).toContain("host got: captured: MATERIAL"); + } finally { + cleanup(tmpDir); + } + }); + + it("NS-H8: nested inside an iterating construct", function* () { + const tmpDir = makeTempDir(); + try { + writeFiles(tmpDir, { + "components/EachHost.md": [ + "---", + "props: { type: object, properties: {}, additionalProperties: false }", + "---", + '', + "item {n}: ", + "", + ].join("\n"), + "doc.md": "MATERIAL", + }); + const output = yield* collect( + yield* execute({ + path: path.join(tmpDir, "doc.md"), + stream: new InMemoryStream(), + componentDirs: [path.join(tmpDir, "components"), tmpDir], + }), + ); + expect(output).toContain("item 1: MATERIAL"); + expect(output).toContain("item 2: MATERIAL"); + } finally { + cleanup(tmpDir); + } + }); +}); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 700e7c4e..08cf48b0 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2837,6 +2837,14 @@ During expansion, this is a special case — it is not resolved from the file system. Instead, it is replaced by the caller's children, partitioned by slot assignment. +A projection point is anywhere the body writes one: a top-level segment, a +position inside an `` region, and equally a position nested inside +another invocation, inside a structural construct, or several levels down. Only +the body is walked. Content that arrives through a projection was written by the +caller, whose own body resolved its projections already, so a `` that +rides in on projected content belongs to the invocation that resolved it and is +not re-read here. + #### 6.3.1 Named slots Components can render caller-provided content in multiple distinct @@ -2943,21 +2951,41 @@ function substituteContent( callerEnv: EvalEnv | undefined, ): Segment[] { const slots = partitionBySlot(children); - return bodySegments.flatMap((segment) => { - if (segment.type === "component" && segment.name === "Content") { - const targetSlot = segment.props.slot as string | undefined; - if (targetSlot !== undefined) { - // Named slot projection — strip slot prop from each child - return (slots.named.get(targetSlot) ?? []).map(stripSlotProp); + return substitute(bodySegments); + + function substitute(segments: Segment[]): Segment[] { + return segments.flatMap((segment) => { + if (segment.type !== "component") { + return [segment]; } - // Default slot projection - return slots.default; - } - return [segment]; - }); + if (segment.name === "Content") { + const targetSlot = segment.props.slot as string | undefined; + if (targetSlot !== undefined) { + // Named slot projection — strip slot prop from each child + return (slots.named.get(targetSlot) ?? []).map(stripSlotProp); + } + // Default slot projection + return slots.default; + } + // Every authored position, however deep + return [{ ...segment, children: substitute(segment.children) }]; + }); + } } ``` +The `slot` prop is consumed by the projection that reads it. A resolved +projection nested inside another invocation therefore carries no `slot` prop of +its own and partitions into that invocation's default slot, like any other +content written at that position. To place caller content in a named slot of a +nested invocation, wrap it: + +```markdown + +
+
+``` + Text interpolation is deferred until the expansion frame is installed. This keeps a scoped authored binding named `props` authoritative for text just as it is for eval blocks and executable-block interpolation. @@ -2989,7 +3017,9 @@ components that use `renderChildren()` continue to receive all content. If the component body does not contain ``, children from the invocation site are silently discarded. If the component body contains multiple `` or multiple ``, each is -replaced independently (all receive the same children for that slot). +replaced independently (all receive the same children for that slot), +and their depth in the body makes no difference: two at different depths +behave exactly like two at top level. ### 6.4 Frontmatter interpolation: `{meta.key}` and `{props.key}` @@ -6072,6 +6102,7 @@ visible warning blocks, gather into a separate error report). | C47 | **Nested enum rejected** | A property with `enum: [a, b]` nested inside an object/array item rejects a value outside the set → PropValidationError | | C48 | **No bare prop binding** | Declaring `name` makes `{props.name}` available but leaves `{name}` verbatim until authored code creates that binding | | C49 | **Validated object identity** | The environment and function-component argument observe the exact defaulted object returned by validation | +| C50 | Nested `` | Caller content projects from a position inside another invocation, inside a structural construct, several levels deep, and inside an `` region; a nested named slot resolves and consumes its `slot` prop; two projections at different depths receive the same content; slot errors are still emitted once | ### Tier D — Code execution and modifier middleware @@ -6306,6 +6337,7 @@ visible warning blocks, gather into a separate error report). | O5 | Projected content stops first | `start:own, start:projected, stop:projected, stop:own` — no `ephemeral()`, `scoped()` or wrapper in the component | | O6 | Ordering is the boundary's | Same order when the resource is acquired after the first projection: `start:projected, start:own, stop:projected, stop:own` | | O7 | Markdown `` lifetime | A provider retaining a resource *after* projecting still releases it after the projected content stops | +| O41 | Nested `` lifetime | A projection written inside another invocation still runs in the projecting invocation's content scope: it outlives the wrapper and stops before the provider's own resource | | O11/O12 | Propagated body error | Both component forms stop projected content before releasing their own | | O13/O14 | Cancellation | Both forms tear down in the same order when halted mid-projection | | O15 | TypeScript nesting | Nested invocations leaf-first; siblings isolated | From f7b9028702b456be803532263f400bf5fd524551 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:35:08 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=AA=20Observe=20nested=20projectio?= =?UTF-8?q?n=20identity=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendered output cannot tell two expansions apart, so a nested projection under `` now reads `Expansion.id` from a probe: the iterations carry different identifiers and re-expansion reproduces the pair. Two behaviors the recursion settles get their own coverage. An unclaimed `` passed through a nested wrapper stays the reserved-name failure rather than being consumed by the enclosing component. The specification's `
` wrap reaches a nested invocation's named slot. §6.3.4 states the substitution algorithm in prose. Its pseudocode spliced the selected children into the body, which the normative lifecycle in the same section already contradicted: the claimed element is what lets expansion run them in the invocation's content scope. The section-divider comments in named-slots.test.ts go with it; every tier is already named by its describe. --- .../core/tests/expansion-identity.test.ts | 20 ++++++ packages/core/tests/named-slots.test.ts | 71 ++++++++++--------- specs/executable-mdx-spec.md | 67 ++++++++--------- 3 files changed, 91 insertions(+), 67 deletions(-) diff --git a/packages/core/tests/expansion-identity.test.ts b/packages/core/tests/expansion-identity.test.ts index 82350693..add2f77a 100644 --- a/packages/core/tests/expansion-identity.test.ts +++ b/packages/core/tests/expansion-identity.test.ts @@ -443,6 +443,26 @@ describe("Tier XP — expansion identity", () => { expect(twoProbes[0]).not.toBe(twoProbes[1]); }); + // A projection written inside an iterating construct is one authored + // `` expanded once per item, so its identity has to come from the + // path it expands under rather than from the element. `` adds its item + // frame before the projection point is reached; dropping that frame, or + // deriving the projection from the element that carries it, is what this + // kills — both would report one identifier twice. + it("XP26: a projection nested in differs per iteration and reproduces itself", function* () { + const source = "\n\n\n"; + const definitions = { + EachHost: markdown("EachHost", '\n\n\n'), + }; + + const first = yield* identifiers(source, definitions); + const second = yield* identifiers(source, definitions); + + expect(first).toHaveLength(2); + expect(first[0]).not.toBe(first[1]); + expect(second).toEqual(first); + }); + // The projection ordinal is the one discriminator that is not read off the // source, so it has to follow the component's own program order rather than // the order projections finish in. Two runs of one component, with the diff --git a/packages/core/tests/named-slots.test.ts b/packages/core/tests/named-slots.test.ts index 4c8401b8..e3faaa93 100644 --- a/packages/core/tests/named-slots.test.ts +++ b/packages/core/tests/named-slots.test.ts @@ -1,9 +1,9 @@ /** - * Named slots tests — Tiers NS-A through NS-G. + * Named slots tests — Tiers NS-A through NS-H. * * Covers slot partitioning, content substitution, expansion integration, - * slot prop reservation, renderChildren interaction, edge cases, and - * boundary scanner confirmation. + * slot prop reservation, renderChildren interaction, edge cases, + * boundary scanner confirmation, and nested projection points. */ import { describe, it, beforeAll } from "@executablemd/test-support/bdd"; @@ -162,10 +162,6 @@ function cleanup(dir: string): void { fs.rmSync(dir, { recursive: true, force: true }); } -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-A — Slot partitioning (unit) -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-A — Slot partitioning", () => { // deno-lint-ignore require-yield it("NS-A1: no slot props — all in default", function* () { @@ -268,10 +264,6 @@ describe("Tier NS-A — Slot partitioning", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-B — Content substitution (unit) -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-B — Content substitution", () => { it("NS-B1: backward compat — no slots anywhere", function* () { const layout = makeComponent("Layout", "before\n\nafter"); @@ -410,10 +402,6 @@ describe("Tier NS-B — Content substitution", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-C — Expansion integration -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-C — Expansion integration", () => { it("NS-C1: basic named slot expansion", function* () { const report = makeComponent( @@ -617,10 +605,6 @@ describe("Tier NS-C — Expansion integration", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-D — slot prop reservation -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-D — slot prop reservation", () => { it("NS-D1: slot in props frontmatter → error", function* () { const { props } = parseFrontmatter({ @@ -691,10 +675,6 @@ describe("Tier NS-D — slot prop reservation", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-E — renderChildren() interaction -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-E — renderChildren interaction", () => { beforeAll(() => useTempFileCompiler()); it("NS-E1: renderChildren includes all slots", function* () { @@ -814,10 +794,6 @@ describe("Tier NS-E — renderChildren interaction", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-F — Edge cases -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-F — Edge cases", () => { it("NS-F1: slot on self-closing component", function* () { const layout = makeComponent("Layout", '\n'); @@ -984,10 +960,6 @@ describe("Tier NS-F — Edge cases", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-G — Boundary scanner (confirms no changes needed) -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-G — Boundary scanner", () => { // deno-lint-ignore require-yield it("NS-G1: slot parsed as string prop", function* () { @@ -1031,10 +1003,6 @@ describe("Tier NS-G — Boundary scanner", () => { }); }); -// ═══════════════════════════════════════════════════════════════════════════ -// Tier NS-H — Nested projection points -// ═══════════════════════════════════════════════════════════════════════════ - describe("Tier NS-H — Nested projection points", () => { it("NS-H1: inside another component invocation", function* () { const echo = makeComponent("Echo", "ECHO()"); @@ -1146,4 +1114,37 @@ describe("Tier NS-H — Nested projection points", () => { cleanup(tmpDir); } }); + + it("NS-H9: an unclaimed stays reserved through a nested wrapper", function* () { + const echo = makeComponent("Echo", "ECHO()"); + const host = makeComponent("Host", ""); + // Written in the document, where nothing projects: no invocation resolved + // it, so passing it through two nested bodies must not make it mean one. + const segments = scanSegments(""); + const expanded = yield* expandAll(segments, { Echo: echo, Host: host }); + const errors = expanded.filter((segment) => segment.type === "error"); + expect(errors).toHaveLength(1); + expect(errors[0]!.message).toContain(" is reserved"); + }); + + it("NS-H10: a wrapped projection reaches a nested invocation's named slot", function* () { + const layout = makeComponent("Layout", 'LAYOUT()'); + const section = makeComponent("Section", ""); + const host = makeComponent( + "Host", + '
', + ); + const headerComp = makeComponent("Header", "HEADER-TEXT"); + const segments = scanSegments('\n
\ndefault text\n'); + const expanded = yield* expandAll(segments, { + Layout: layout, + Section: section, + Host: host, + Header: headerComp, + }); + const output = renderSegments(expanded); + expect(output).toContain("LAYOUT(HEADER-TEXT)"); + // Host's default slot is not projected, so nothing else rides along. + expect(output).not.toContain("default text"); + }); }); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 08cf48b0..b756d111 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2942,37 +2942,39 @@ presents the failure as `ContentError` under either error mode (§5.1.2), a catc there is the same explicit recovery, and the projected work still unwinds with the content scope. -`substituteContent` resolves the slots: - -```typescript -function substituteContent( - bodySegments: Segment[], - children: Segment[], - callerEnv: EvalEnv | undefined, -): Segment[] { - const slots = partitionBySlot(children); - return substitute(bodySegments); - - function substitute(segments: Segment[]): Segment[] { - return segments.flatMap((segment) => { - if (segment.type !== "component") { - return [segment]; - } - if (segment.name === "Content") { - const targetSlot = segment.props.slot as string | undefined; - if (targetSlot !== undefined) { - // Named slot projection — strip slot prop from each child - return (slots.named.get(targetSlot) ?? []).map(stripSlotProp); - } - // Default slot projection - return slots.default; - } - // Every authored position, however deep - return [{ ...segment, children: substitute(segment.children) }]; - }); - } -} -``` +Substitution resolves the slots. It runs once per invocation, over the +component's own body, and does the following: + +1. Partition the caller's children into slot buckets (§6.3.3). The buckets and + the once-only error flag are shared by every projection point in the body. +2. Walk the body segments. A segment that is not a component element is kept as + it is; a component element that is not `` is kept with its + children walked the same way, so a projection point is found wherever the + body writes one — nested inside another invocation, inside a structural + construct, or several levels down. +3. At a ``, select the bucket its `slot` prop names, or the default + bucket when it has none. Named-slot children have their own `slot` prop + stripped (§6.3.2), and children of either kind are tagged with the caller's + binding environment so their expression props resolve where the JSX was + written. +4. Replace the element with a **claimed** ``: the same element, + carrying the selected children and with its own `slot` prop removed. The + claim is recorded by object identity on the invocation that made it. The + selected children are not spliced into the body, because the claimed element + is what expansion needs in order to run them in the invocation's content + scope rather than its own. +5. Emit the slot-name errors of §6.3.3 immediately before the first claimed + element, and only there. + +The walk covers the body and stops at what a projection resolves to. Content +that arrives through a projection was written by the caller, whose own body +substituted its projections already, so a claimed `` riding in on +projected content is passed through untouched: re-reading it would give the +enclosing component content addressed to someone else, and copying it would lose +the claim its lifecycle depends on. A `` that no invocation claimed — +one an author wrote in a document rather than a component body — never becomes a +projection, however many bodies it passes through; it reaches component +resolution and fails as the reserved name it is (§5.3). The `slot` prop is consumed by the projection that reads it. A resolved projection nested inside another invocation therefore carries no `slot` prop of @@ -6102,7 +6104,7 @@ visible warning blocks, gather into a separate error report). | C47 | **Nested enum rejected** | A property with `enum: [a, b]` nested inside an object/array item rejects a value outside the set → PropValidationError | | C48 | **No bare prop binding** | Declaring `name` makes `{props.name}` available but leaves `{name}` verbatim until authored code creates that binding | | C49 | **Validated object identity** | The environment and function-component argument observe the exact defaulted object returned by validation | -| C50 | Nested `` | Caller content projects from a position inside another invocation, inside a structural construct, several levels deep, and inside an `` region; a nested named slot resolves and consumes its `slot` prop; two projections at different depths receive the same content; slot errors are still emitted once | +| C50 | Nested `` | Caller content projects from a position inside another invocation, inside a structural construct, several levels deep, and inside an `` region; a nested named slot resolves and consumes its `slot` prop; wrapping a projection in `
` reaches a nested invocation's named slot; two projections at different depths receive the same content; slot errors are still emitted once; an unclaimed `` passed through nested bodies stays the reserved-name failure | ### Tier D — Code execution and modifier middleware @@ -6532,6 +6534,7 @@ Each row names the derivation it kills. | XP15 | Whose projection | The same content through two components differs; two probes inside one component differ by their own positions | | XP16 | Concurrent projections | Reversing which projection completes first does not move either identifier | | XP17 | Lazy | A projection operation constructed and never interpreted consumes no ordinal | +| XP26 | Nested projection under iteration | One `` written inside `` gives each item its own identifier, and re-expansion reproduces the ordered pair — an identity taken from the element rather than the path it expands under would report one twice | ### Tier AF — Agent components as function components