@archie/devtools 0.0.26 → 0.0.28

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.
@@ -4063,6 +4063,19 @@ function dispatchRuntimeEvent(detail) {
4063
4063
  if (typeof window === "undefined") return;
4064
4064
  window.dispatchEvent(new CustomEvent(DND_RUNTIME_EVENT, { detail }));
4065
4065
  }
4066
+ function resolveRowBinding(el) {
4067
+ let node = el;
4068
+ for (let depth = 0; node && depth < MAX_ROW_BINDING_ANCESTOR_DEPTH; depth += 1) {
4069
+ const rowId = node.getAttribute("data-agent-row");
4070
+ if (rowId !== null && rowId !== "") {
4071
+ const entity = node.getAttribute("data-agent-entity");
4072
+ return entity ? { entity, rowId } : null;
4073
+ }
4074
+ node = node.parentElement;
4075
+ }
4076
+ return null;
4077
+ }
4078
+ var MAX_ROW_BINDING_ANCESTOR_DEPTH = 12;
4066
4079
  function mergeInspectorTheme(theme) {
4067
4080
  return {
4068
4081
  ...DEFAULT_INSPECTOR_THEME,
@@ -4449,16 +4462,6 @@ function createCommitLaneIdentitySnapshot(params) {
4449
4462
  committable: true
4450
4463
  };
4451
4464
  }
4452
- function hasSafeOccurrenceIdentities(lane, duplicateNodeKey) {
4453
- const effectiveNodeIds = lane.commitNodeIds.filter(
4454
- (commitNodeId) => lane.nodeKeysByCommitNodeId.get(commitNodeId) === duplicateNodeKey
4455
- ).map(
4456
- (commitNodeId) => lane.effectiveNodeIdsByCommitNodeId.get(commitNodeId) ?? null
4457
- );
4458
- return effectiveNodeIds.length > 1 && effectiveNodeIds.every(
4459
- (effectiveNodeId) => typeof effectiveNodeId === "string" && effectiveNodeId.length > 0
4460
- ) && new Set(effectiveNodeIds).size === effectiveNodeIds.length;
4461
- }
4462
4465
  function buildLanePersistenceDiagnostics(params) {
4463
4466
  const duplicateNodeKeys = detectDuplicateSourceBackedNodeKeys(
4464
4467
  buildNodeKeysFromCommitIds(
@@ -4469,24 +4472,10 @@ function buildLanePersistenceDiagnostics(params) {
4469
4472
  if (duplicateNodeKeys.length === 0) {
4470
4473
  return null;
4471
4474
  }
4472
- const safeDuplicateNodeKeys = duplicateNodeKeys.filter(
4473
- (nodeKey) => hasSafeOccurrenceIdentities(params.lane, nodeKey)
4474
- );
4475
- if (safeDuplicateNodeKeys.length === duplicateNodeKeys.length) {
4476
- return {
4477
- notes: safeDuplicateNodeKeys.map(
4478
- (nodeKey) => `Arrange lane ${params.lane.laneContainerId} repeats source-backed nodeKey ${nodeKey}, but runtime assigned explicit occurrence identities for each commit root.`
4479
- )
4480
- };
4481
- }
4482
4475
  return {
4483
- structurallyUnsafe: true,
4484
- blockedReasons: duplicateNodeKeys.map(
4485
- (nodeKey) => `Arrange lane ${params.lane.laneContainerId} contains multiple commit roots with the same source-backed nodeKey (${nodeKey}), so local save is unsafe.`
4486
- ),
4487
- notes: [
4488
- `Lane ${params.lane.laneContainerId} duplicates source-backed nodeKeys: ${duplicateNodeKeys.join(", ")}`
4489
- ]
4476
+ notes: duplicateNodeKeys.map(
4477
+ (nodeKey) => `Arrange lane ${params.lane.laneContainerId} repeats source-backed nodeKey ${nodeKey}, but runtime assigned explicit occurrence identities for each commit root.`
4478
+ )
4490
4479
  };
4491
4480
  }
4492
4481
  function removeCommitNodeId(commitNodeIds, commitNodeId) {
@@ -4701,6 +4690,7 @@ var useDesignModeStore = create((set, get) => ({
4701
4690
  beforeSibling,
4702
4691
  afterSibling,
4703
4692
  diagnostics,
4693
+ binding,
4704
4694
  componentName,
4705
4695
  before,
4706
4696
  after
@@ -4727,6 +4717,7 @@ var useDesignModeStore = create((set, get) => ({
4727
4717
  beforeSibling: beforeSibling ?? null,
4728
4718
  afterSibling: afterSibling ?? null,
4729
4719
  diagnostics: diagnostics ?? null,
4720
+ binding: binding ?? null,
4730
4721
  payload: { before, after },
4731
4722
  at: now
4732
4723
  };
@@ -6416,6 +6407,8 @@ function serializeArrangeChange(event, index) {
6416
6407
  sourceBeforeSibling: event.kind === "move" ? event.sourceBeforeSibling : void 0,
6417
6408
  sourceAfterSibling: event.kind === "move" ? event.sourceAfterSibling : void 0,
6418
6409
  diagnostics: event.kind === "move" ? event.diagnostics : void 0,
6410
+ // ARCH-1065 D17 — which slice row this move addresses.
6411
+ binding: event.kind === "move" ? event.binding : void 0,
6419
6412
  // Insert-specific fields
6420
6413
  displayName: event.kind === "insert" ? event.displayName : void 0,
6421
6414
  html: event.kind === "insert" ? event.html : void 0,
@@ -7946,6 +7939,7 @@ function recordCommittedMove(args) {
7946
7939
  });
7947
7940
  const componentName = getEditorMeta(anchorEl)?.componentName;
7948
7941
  const operationId = `${command.at}:${anchorNodeId}`;
7942
+ const binding = resolveRowBinding(selectedEl);
7949
7943
  const beforePayload = buildMovePayloadWithEffectiveIdentities({
7950
7944
  placement: command.from,
7951
7945
  containerIdentity: sourceContainerIdentity,
@@ -7975,6 +7969,7 @@ function recordCommittedMove(args) {
7975
7969
  sourceBeforeSibling: sourceBeforeSiblingIdentity,
7976
7970
  sourceAfterSibling: sourceAfterSiblingIdentity,
7977
7971
  diagnostics,
7972
+ binding,
7978
7973
  container: containerIdentity,
7979
7974
  beforeSibling: beforeSiblingIdentity,
7980
7975
  afterSibling: afterSiblingIdentity,
@@ -0,0 +1,221 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ /**
4
+ * babel-plugin-collection-binding (ARCH-1065 M2)
5
+ *
6
+ * Build-time codemod that stamps the DATA-BINDING GRAMMAR (ARCH-1065 design
7
+ * v2, D1) onto `.map()` collections whose source statically resolves back to
8
+ * the screen's slice — so the runtime inspector (M3, `window.__slice`) and the
9
+ * kit editing surfaces can offer per-row, per-field edits over bespoke
10
+ * (non-kit-component) rendered collections, the same way hand-written kit
11
+ * components (M1) already do.
12
+ *
13
+ * Applied in kit `build-d.mjs` to `s.source` BEFORE the SSR + client
14
+ * transforms, so the stamped attributes are present in both the SSR HTML and
15
+ * the hydrated client tree (hydration-safe by construction — nothing here
16
+ * reads the DOM or depends on a render pass).
17
+ *
18
+ * ── Grammar (D1) ──────────────────────────────────────────────────────────
19
+ *
20
+ * data-agent-entity static string, on the row root
21
+ * data-agent-row {String(param?.id)}, on the row root
22
+ * data-agent-field static string OR a runtime expression
23
+ * (??/ternary field selection, D5.5)
24
+ * data-agent-field-state "mixed" | "unresolved"
25
+ * data-agent-field-parts comma-separated field list (mixed leaves)
26
+ * data-agent-field-slot text-node index of the dynamic part
27
+ * (label+value leaves, D6)
28
+ * data-agent-collection-order "stable" | "derived" (D18), on the row root
29
+ *
30
+ * ── What counts as a slice source ────────────────────────────────────────
31
+ *
32
+ * `slice.<Entity>` or `slice['Entity Name']` / `slice["Entity Name"]`
33
+ * (both quote styles — Babel normalizes both to the same StringLiteral
34
+ * value, entities may contain spaces), threaded through any combination of
35
+ * `.filter()/.slice()/.sort()/.map()` chain calls, through single-assignment
36
+ * (`const`, never reassigned) local-variable hops (bounded to
37
+ * `MAX_SOURCE_CHAIN_HOPS`), and through a `useMemo(() => …, deps)` /
38
+ * `React.useMemo(() => …, deps)` unwrap (the common `() => expr` shape only —
39
+ * see `resolveSliceSource`). A `.sort(` ANYWHERE in the chain marks the
40
+ * collection `data-agent-collection-order="derived"`; otherwise `"stable"`
41
+ * (D18). The identifier root must be exactly `slice` — this mirrors the
42
+ * runtime's `window.__slice` (M3); the codemod does not trace how a `slice`
43
+ * binding entered scope (prop, destructure, import), only that its name at
44
+ * the point of use is literally `slice`.
45
+ *
46
+ * ── Row identity ──────────────────────────────────────────────────────────
47
+ *
48
+ * The `.map()` callback's first parameter is either a bare identifier (the
49
+ * whole row object — `data-agent-row` becomes `String(<param>?.id)`) or an
50
+ * ObjectPattern destructuring it (`data-agent-row` becomes `String(<local>)`
51
+ * using whichever local name the pattern bound the `id` key to). A
52
+ * destructured pattern that never binds `id` cannot form a row identity, so
53
+ * the ENTIRE map is left unstamped — row identity is foundational; there is
54
+ * no such thing as a field-only stamp without it.
55
+ *
56
+ * ── Row-component projection (D5.4) ─────────────────────────────────────
57
+ *
58
+ * `{list.map(e => <EventCard event={e} />)}` with a MODULE-LOCAL
59
+ * `function EventCard({ event }) { … }` co-located in the same source file
60
+ * (kit `build-d.mjs` hands this plugin the whole page/screen source, so
61
+ * screen-local helper components live in the same AST) resolves ACROSS that
62
+ * boundary: the row root is stamped on EventCard's OWN returned host
63
+ * element, using EventCard's own local parameter name for row-id and field
64
+ * resolution. The row-root projection specifically requires the WHOLE mapped
65
+ * item to be forwarded through one prop, verbatim (`event={e}`) — a partial
66
+ * field forward at that point cannot carry row identity, so the projection
67
+ * (and with it the whole map) fails closed.
68
+ *
69
+ * The SAME projection mechanism runs recursively for NESTED leaf components
70
+ * inside an already-established row (`<StatusBadge status={a.status} />`):
71
+ * there it is permissive about WHICH fields are forwarded (whole item OR a
72
+ * specific field OR several), because no row identity is at stake — only
73
+ * further field leaves.
74
+ *
75
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
76
+ *
77
+ * D5.4 only ever sees THROUGH a component whose own source is available
78
+ * (module-local, co-located in the same file). Kit/imported components
79
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
80
+ * plugin has no body to project into — but many of them forward unrecognized
81
+ * JSX attributes onto their own root DOM element (typically via
82
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
83
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
84
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
85
+ *
86
+ * Two independent mechanisms use that allowlist:
87
+ *
88
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
89
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
90
+ * root itself), the row-identity attributes stamp directly onto that
91
+ * component usage — NOT a projection into a separate definition, since
92
+ * there is none to project into. The field walk continues into the
93
+ * component's own JSX children with the SAME binding scope the row
94
+ * callback already established.
95
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
96
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
97
+ * the field attribute stamps onto the component itself IFF its name is
98
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
99
+ * given an invented carrier.
100
+ *
101
+ * Separately, and regardless of allowlist membership at all — walking a
102
+ * component's own JSX children costs nothing, since no attribute lands
103
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
104
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
105
+ * child that is a component usage, as long as that usage has its own JSX
106
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
107
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
108
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
109
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
110
+ * wrapper itself to carry anything.
111
+ *
112
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
113
+ * baked into this plugin — kit-manifest is the single source of truth for
114
+ * which components forward attributes (`forwardsDataAttributes`), and the
115
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
116
+ * option is exactly today's fail-closed behavior: zero component-carried
117
+ * stamps, byte-identical to the plugin before this mechanism existed.
118
+ *
119
+ * ── Field resolution ────────────────────────────────────────────────────
120
+ *
121
+ * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
122
+ * same purity classifier both plugins must agree on) to find LEAF elements:
123
+ * JSX elements whose only meaningful children are text/expressions, never
124
+ * nested JSX elements. A leaf's dynamic part(s) classify as:
125
+ *
126
+ * - a bare member expression off a "whole row object" binding
127
+ * (`a.title`) → static field, UNLESS the member is `id` (id is NEVER
128
+ * stamped as a field — it already carries the row identity);
129
+ * - `a.x ?? a.y [?? …]` (an N-part nullish chain of member fields) →
130
+ * a RUNTIME field-selector expression mirroring the chain
131
+ * (`a.x != null ? "x" : a.y != null ? "y" : "z"`), or, when the chain
132
+ * ends in a static literal fallback, a plain static field;
133
+ * - `a.x || 'fallback'` → static field (literal-fallback only; `||`
134
+ * does not get the N-part runtime treatment `??` does);
135
+ * - `cond ? a.x : a.y` (both branches member fields of the row object) →
136
+ * a RUNTIME field-selector expression duplicating `cond` verbatim
137
+ * (`cond ? "x" : "y"`) — CONSTRAINED (C3): `cond` must be CALL-FREE
138
+ * (checked recursively over the whole test subtree); a call anywhere in
139
+ * the duplicated test would double-evaluate it across SSR and hydration,
140
+ * so the whole leaf is left unstamped instead;
141
+ * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
142
+ * member (a boolean-typed field driving two literal branches) → static
143
+ * field named by `cond` — the FE's typed controls (D7, a different
144
+ * mechanism, M3) read the boolean's true/false rendering from
145
+ * `window.__slice`, not from this attribute;
146
+ * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
147
+ * name, `data-agent-field-state="unresolved"` (a positive "this is
148
+ * row-scoped data, but which field is unknown" signal — never silent).
149
+ *
150
+ * A leaf with exactly one meaningful child (no surrounding text) gets a bare
151
+ * `data-agent-field`. A leaf with 2+ meaningful children (label + value,
152
+ * D6) where EXACTLY ONE is dynamic gets the same `data-agent-field` PLUS
153
+ * `data-agent-field-slot`: the dynamic part's 0-based index among the
154
+ * element's own meaningful children. This is deliberately equal to its
155
+ * text-node index in the eventual SSR/hydrated DOM: every meaningful JSXText
156
+ * or JSXExpressionContainer child renders as exactly one text node (React 18
157
+ * inserts `<!-- -->` comment separators BETWEEN adjacent text-producing
158
+ * outputs to keep them independently locatable during hydration, but those
159
+ * comments are not text nodes and do not shift text-node indices). A leaf
160
+ * with 2+ dynamic parts that all resolve to STATIC fields with more than one
161
+ * DISTINCT field name gets `data-agent-field-state="mixed"` plus
162
+ * `data-agent-field-parts` (a comma-separated, source-order field list); any
163
+ * other multi-dynamic-part shape (an unresolvable part, or every part naming
164
+ * the same field) is left unstamped.
165
+ *
166
+ * Recursion into a leaf's structural siblings stops at the nearest NESTED
167
+ * `.map()` collection (a JSXExpressionContainer wrapping a call, not a JSX
168
+ * element child, so it is never a "structural child" this walker descends
169
+ * into) — the innermost-stamped-param rule: a field leaf only ever binds
170
+ * against its nearest enclosing map's own parameter. A nested collection
171
+ * that is itself slice-rooted gets discovered and stamped independently by
172
+ * the same Program-wide `.map()` scan, with its own entity/row identity.
173
+ *
174
+ * ── Failure posture ─────────────────────────────────────────────────────
175
+ *
176
+ * Every resolution step is an explicit allowlist; anything the resolver is
177
+ * not sure about degrades to NO STAMP, never a guessed one — a fragment or
178
+ * conditional expression at the callback's JSX root, an unresolvable
179
+ * component usage, a non-slice source, an ObjectPattern without an `id` key,
180
+ * a call inside a duplicated ternary test, ambiguous control flow (2+ return
181
+ * statements) — all leave the subtree untouched.
182
+ *
183
+ * ── Idempotence ─────────────────────────────────────────────────────────
184
+ *
185
+ * The plugin never rewrites SOURCE (D15) and never touches an existing
186
+ * attribute: every stamp is guarded by a same-name attribute-presence check,
187
+ * and a row root that already carries `data-agent-entity` short-circuits the
188
+ * whole map (no re-resolution, no field re-walk) — running the plugin twice
189
+ * produces byte-identical output.
190
+ */
191
+
192
+ interface CollectionBindingPluginOptions {
193
+ /** Skip files matching this pattern. @default /node_modules/ */
194
+ fileExclude?: RegExp;
195
+ /**
196
+ * Component names known to forward unrecognized JSX attributes onto their
197
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
198
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
199
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
200
+ * descent" section above).
201
+ *
202
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
203
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
204
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
205
+ * would drift the moment a kit component's forwarding behavior changed
206
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
207
+ * keep the plugin testable standalone.
208
+ *
209
+ * Absent or empty = today's fail-closed behavior: no component ever
210
+ * carries a stamp, byte-identical to the plugin before this option
211
+ * existed.
212
+ *
213
+ * @default []
214
+ */
215
+ forwardingComponents?: string[];
216
+ }
217
+ declare function babelPluginCollectionBinding({ types: t }: {
218
+ types: typeof types;
219
+ }, opts?: CollectionBindingPluginOptions): PluginObj;
220
+
221
+ export { type CollectionBindingPluginOptions, babelPluginCollectionBinding };
@@ -0,0 +1,221 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ /**
4
+ * babel-plugin-collection-binding (ARCH-1065 M2)
5
+ *
6
+ * Build-time codemod that stamps the DATA-BINDING GRAMMAR (ARCH-1065 design
7
+ * v2, D1) onto `.map()` collections whose source statically resolves back to
8
+ * the screen's slice — so the runtime inspector (M3, `window.__slice`) and the
9
+ * kit editing surfaces can offer per-row, per-field edits over bespoke
10
+ * (non-kit-component) rendered collections, the same way hand-written kit
11
+ * components (M1) already do.
12
+ *
13
+ * Applied in kit `build-d.mjs` to `s.source` BEFORE the SSR + client
14
+ * transforms, so the stamped attributes are present in both the SSR HTML and
15
+ * the hydrated client tree (hydration-safe by construction — nothing here
16
+ * reads the DOM or depends on a render pass).
17
+ *
18
+ * ── Grammar (D1) ──────────────────────────────────────────────────────────
19
+ *
20
+ * data-agent-entity static string, on the row root
21
+ * data-agent-row {String(param?.id)}, on the row root
22
+ * data-agent-field static string OR a runtime expression
23
+ * (??/ternary field selection, D5.5)
24
+ * data-agent-field-state "mixed" | "unresolved"
25
+ * data-agent-field-parts comma-separated field list (mixed leaves)
26
+ * data-agent-field-slot text-node index of the dynamic part
27
+ * (label+value leaves, D6)
28
+ * data-agent-collection-order "stable" | "derived" (D18), on the row root
29
+ *
30
+ * ── What counts as a slice source ────────────────────────────────────────
31
+ *
32
+ * `slice.<Entity>` or `slice['Entity Name']` / `slice["Entity Name"]`
33
+ * (both quote styles — Babel normalizes both to the same StringLiteral
34
+ * value, entities may contain spaces), threaded through any combination of
35
+ * `.filter()/.slice()/.sort()/.map()` chain calls, through single-assignment
36
+ * (`const`, never reassigned) local-variable hops (bounded to
37
+ * `MAX_SOURCE_CHAIN_HOPS`), and through a `useMemo(() => …, deps)` /
38
+ * `React.useMemo(() => …, deps)` unwrap (the common `() => expr` shape only —
39
+ * see `resolveSliceSource`). A `.sort(` ANYWHERE in the chain marks the
40
+ * collection `data-agent-collection-order="derived"`; otherwise `"stable"`
41
+ * (D18). The identifier root must be exactly `slice` — this mirrors the
42
+ * runtime's `window.__slice` (M3); the codemod does not trace how a `slice`
43
+ * binding entered scope (prop, destructure, import), only that its name at
44
+ * the point of use is literally `slice`.
45
+ *
46
+ * ── Row identity ──────────────────────────────────────────────────────────
47
+ *
48
+ * The `.map()` callback's first parameter is either a bare identifier (the
49
+ * whole row object — `data-agent-row` becomes `String(<param>?.id)`) or an
50
+ * ObjectPattern destructuring it (`data-agent-row` becomes `String(<local>)`
51
+ * using whichever local name the pattern bound the `id` key to). A
52
+ * destructured pattern that never binds `id` cannot form a row identity, so
53
+ * the ENTIRE map is left unstamped — row identity is foundational; there is
54
+ * no such thing as a field-only stamp without it.
55
+ *
56
+ * ── Row-component projection (D5.4) ─────────────────────────────────────
57
+ *
58
+ * `{list.map(e => <EventCard event={e} />)}` with a MODULE-LOCAL
59
+ * `function EventCard({ event }) { … }` co-located in the same source file
60
+ * (kit `build-d.mjs` hands this plugin the whole page/screen source, so
61
+ * screen-local helper components live in the same AST) resolves ACROSS that
62
+ * boundary: the row root is stamped on EventCard's OWN returned host
63
+ * element, using EventCard's own local parameter name for row-id and field
64
+ * resolution. The row-root projection specifically requires the WHOLE mapped
65
+ * item to be forwarded through one prop, verbatim (`event={e}`) — a partial
66
+ * field forward at that point cannot carry row identity, so the projection
67
+ * (and with it the whole map) fails closed.
68
+ *
69
+ * The SAME projection mechanism runs recursively for NESTED leaf components
70
+ * inside an already-established row (`<StatusBadge status={a.status} />`):
71
+ * there it is permissive about WHICH fields are forwarded (whole item OR a
72
+ * specific field OR several), because no row identity is at stake — only
73
+ * further field leaves.
74
+ *
75
+ * ── Forwarding-component allowlist descent (ARCH-1065 M-COV fix) ──────────
76
+ *
77
+ * D5.4 only ever sees THROUGH a component whose own source is available
78
+ * (module-local, co-located in the same file). Kit/imported components
79
+ * (`Card`, `CardContent`, `StatusBadge`, …) are opaque to the AST — the
80
+ * plugin has no body to project into — but many of them forward unrecognized
81
+ * JSX attributes onto their own root DOM element (typically via
82
+ * `{...rest}`), which makes them valid attribute CARRIERS even though
83
+ * they're not valid PROJECTION targets. `forwardingComponents` (a plugin
84
+ * option, see `CollectionBindingPluginOptions`) names exactly which ones.
85
+ *
86
+ * Two independent mechanisms use that allowlist:
87
+ *
88
+ * - (a2) ROW ROOT: when the `.map()` callback's own returned JSX element
89
+ * is an allowlisted component (`<Card key={r.id}>…</Card>` as the row
90
+ * root itself), the row-identity attributes stamp directly onto that
91
+ * component usage — NOT a projection into a separate definition, since
92
+ * there is none to project into. The field walk continues into the
93
+ * component's own JSX children with the SAME binding scope the row
94
+ * callback already established.
95
+ * - (c2-ii) LEAF: when a component usage directly hugs a field expression
96
+ * with no host element beneath it (`<StatusBadge>{w.status}</StatusBadge>`),
97
+ * the field attribute stamps onto the component itself IFF its name is
98
+ * allowlisted; otherwise the leaf is left unstamped — a leaf is never
99
+ * given an invented carrier.
100
+ *
101
+ * Separately, and regardless of allowlist membership at all — walking a
102
+ * component's own JSX children costs nothing, since no attribute lands
103
+ * anywhere until a valid carrier (host, or allowlisted leaf) is actually
104
+ * found — (a1) DESCENT: the field walk always continues INTO a structural
105
+ * child that is a component usage, as long as that usage has its own JSX
106
+ * children in source. `<Card><span>{w.name}</span></Card>` reaches the
107
+ * `<span>` (a host, stamped unconditionally) even though `Card` itself is
108
+ * never a stamp target unless allowlisted. This is what makes (c2-ii) safe
109
+ * to leave allowlist-gated: a wrapper with a host descendant never needs the
110
+ * wrapper itself to carry anything.
111
+ *
112
+ * The allowlist is ALWAYS an explicit option, never a built-in default list
113
+ * baked into this plugin — kit-manifest is the single source of truth for
114
+ * which components forward attributes (`forwardsDataAttributes`), and the
115
+ * kit build (`build-d.mjs`) is what threads that list in. An absent/empty
116
+ * option is exactly today's fail-closed behavior: zero component-carried
117
+ * stamps, byte-identical to the plugin before this mechanism existed.
118
+ *
119
+ * ── Field resolution ────────────────────────────────────────────────────
120
+ *
121
+ * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
122
+ * same purity classifier both plugins must agree on) to find LEAF elements:
123
+ * JSX elements whose only meaningful children are text/expressions, never
124
+ * nested JSX elements. A leaf's dynamic part(s) classify as:
125
+ *
126
+ * - a bare member expression off a "whole row object" binding
127
+ * (`a.title`) → static field, UNLESS the member is `id` (id is NEVER
128
+ * stamped as a field — it already carries the row identity);
129
+ * - `a.x ?? a.y [?? …]` (an N-part nullish chain of member fields) →
130
+ * a RUNTIME field-selector expression mirroring the chain
131
+ * (`a.x != null ? "x" : a.y != null ? "y" : "z"`), or, when the chain
132
+ * ends in a static literal fallback, a plain static field;
133
+ * - `a.x || 'fallback'` → static field (literal-fallback only; `||`
134
+ * does not get the N-part runtime treatment `??` does);
135
+ * - `cond ? a.x : a.y` (both branches member fields of the row object) →
136
+ * a RUNTIME field-selector expression duplicating `cond` verbatim
137
+ * (`cond ? "x" : "y"`) — CONSTRAINED (C3): `cond` must be CALL-FREE
138
+ * (checked recursively over the whole test subtree); a call anywhere in
139
+ * the duplicated test would double-evaluate it across SSR and hydration,
140
+ * so the whole leaf is left unstamped instead;
141
+ * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
142
+ * member (a boolean-typed field driving two literal branches) → static
143
+ * field named by `cond` — the FE's typed controls (D7, a different
144
+ * mechanism, M3) read the boolean's true/false rendering from
145
+ * `window.__slice`, not from this attribute;
146
+ * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
147
+ * name, `data-agent-field-state="unresolved"` (a positive "this is
148
+ * row-scoped data, but which field is unknown" signal — never silent).
149
+ *
150
+ * A leaf with exactly one meaningful child (no surrounding text) gets a bare
151
+ * `data-agent-field`. A leaf with 2+ meaningful children (label + value,
152
+ * D6) where EXACTLY ONE is dynamic gets the same `data-agent-field` PLUS
153
+ * `data-agent-field-slot`: the dynamic part's 0-based index among the
154
+ * element's own meaningful children. This is deliberately equal to its
155
+ * text-node index in the eventual SSR/hydrated DOM: every meaningful JSXText
156
+ * or JSXExpressionContainer child renders as exactly one text node (React 18
157
+ * inserts `<!-- -->` comment separators BETWEEN adjacent text-producing
158
+ * outputs to keep them independently locatable during hydration, but those
159
+ * comments are not text nodes and do not shift text-node indices). A leaf
160
+ * with 2+ dynamic parts that all resolve to STATIC fields with more than one
161
+ * DISTINCT field name gets `data-agent-field-state="mixed"` plus
162
+ * `data-agent-field-parts` (a comma-separated, source-order field list); any
163
+ * other multi-dynamic-part shape (an unresolvable part, or every part naming
164
+ * the same field) is left unstamped.
165
+ *
166
+ * Recursion into a leaf's structural siblings stops at the nearest NESTED
167
+ * `.map()` collection (a JSXExpressionContainer wrapping a call, not a JSX
168
+ * element child, so it is never a "structural child" this walker descends
169
+ * into) — the innermost-stamped-param rule: a field leaf only ever binds
170
+ * against its nearest enclosing map's own parameter. A nested collection
171
+ * that is itself slice-rooted gets discovered and stamped independently by
172
+ * the same Program-wide `.map()` scan, with its own entity/row identity.
173
+ *
174
+ * ── Failure posture ─────────────────────────────────────────────────────
175
+ *
176
+ * Every resolution step is an explicit allowlist; anything the resolver is
177
+ * not sure about degrades to NO STAMP, never a guessed one — a fragment or
178
+ * conditional expression at the callback's JSX root, an unresolvable
179
+ * component usage, a non-slice source, an ObjectPattern without an `id` key,
180
+ * a call inside a duplicated ternary test, ambiguous control flow (2+ return
181
+ * statements) — all leave the subtree untouched.
182
+ *
183
+ * ── Idempotence ─────────────────────────────────────────────────────────
184
+ *
185
+ * The plugin never rewrites SOURCE (D15) and never touches an existing
186
+ * attribute: every stamp is guarded by a same-name attribute-presence check,
187
+ * and a row root that already carries `data-agent-entity` short-circuits the
188
+ * whole map (no re-resolution, no field re-walk) — running the plugin twice
189
+ * produces byte-identical output.
190
+ */
191
+
192
+ interface CollectionBindingPluginOptions {
193
+ /** Skip files matching this pattern. @default /node_modules/ */
194
+ fileExclude?: RegExp;
195
+ /**
196
+ * Component names known to forward unrecognized JSX attributes onto their
197
+ * own root DOM element (e.g. via `{...rest}`) — the exact set that makes
198
+ * a component usage a valid attribute CARRIER (row-root or leaf) even
199
+ * though it's opaque to the AST (see the "Forwarding-component allowlist
200
+ * descent" section above).
201
+ *
202
+ * Sourced from kit-manifest's `forwardsDataAttributes` flags by the kit
203
+ * build (`build-d.mjs` passes it) — this plugin NEVER hardcodes a default
204
+ * list here; that would be a shadow copy of kit-manifest's own truth, and
205
+ * would drift the moment a kit component's forwarding behavior changed
206
+ * without this plugin's knowledge. Synthetic lists are fine in tests, to
207
+ * keep the plugin testable standalone.
208
+ *
209
+ * Absent or empty = today's fail-closed behavior: no component ever
210
+ * carries a stamp, byte-identical to the plugin before this option
211
+ * existed.
212
+ *
213
+ * @default []
214
+ */
215
+ forwardingComponents?: string[];
216
+ }
217
+ declare function babelPluginCollectionBinding({ types: t }: {
218
+ types: typeof types;
219
+ }, opts?: CollectionBindingPluginOptions): PluginObj;
220
+
221
+ export { type CollectionBindingPluginOptions, babelPluginCollectionBinding };