@archie/devtools 0.0.26 → 0.0.27

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,156 @@
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
+ * ── Field resolution ────────────────────────────────────────────────────
76
+ *
77
+ * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
78
+ * same purity classifier both plugins must agree on) to find LEAF elements:
79
+ * JSX elements whose only meaningful children are text/expressions, never
80
+ * nested JSX elements. A leaf's dynamic part(s) classify as:
81
+ *
82
+ * - a bare member expression off a "whole row object" binding
83
+ * (`a.title`) → static field, UNLESS the member is `id` (id is NEVER
84
+ * stamped as a field — it already carries the row identity);
85
+ * - `a.x ?? a.y [?? …]` (an N-part nullish chain of member fields) →
86
+ * a RUNTIME field-selector expression mirroring the chain
87
+ * (`a.x != null ? "x" : a.y != null ? "y" : "z"`), or, when the chain
88
+ * ends in a static literal fallback, a plain static field;
89
+ * - `a.x || 'fallback'` → static field (literal-fallback only; `||`
90
+ * does not get the N-part runtime treatment `??` does);
91
+ * - `cond ? a.x : a.y` (both branches member fields of the row object) →
92
+ * a RUNTIME field-selector expression duplicating `cond` verbatim
93
+ * (`cond ? "x" : "y"`) — CONSTRAINED (C3): `cond` must be CALL-FREE
94
+ * (checked recursively over the whole test subtree); a call anywhere in
95
+ * the duplicated test would double-evaluate it across SSR and hydration,
96
+ * so the whole leaf is left unstamped instead;
97
+ * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
98
+ * member (a boolean-typed field driving two literal branches) → static
99
+ * field named by `cond` — the FE's typed controls (D7, a different
100
+ * mechanism, M3) read the boolean's true/false rendering from
101
+ * `window.__slice`, not from this attribute;
102
+ * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
103
+ * name, `data-agent-field-state="unresolved"` (a positive "this is
104
+ * row-scoped data, but which field is unknown" signal — never silent).
105
+ *
106
+ * A leaf with exactly one meaningful child (no surrounding text) gets a bare
107
+ * `data-agent-field`. A leaf with 2+ meaningful children (label + value,
108
+ * D6) where EXACTLY ONE is dynamic gets the same `data-agent-field` PLUS
109
+ * `data-agent-field-slot`: the dynamic part's 0-based index among the
110
+ * element's own meaningful children. This is deliberately equal to its
111
+ * text-node index in the eventual SSR/hydrated DOM: every meaningful JSXText
112
+ * or JSXExpressionContainer child renders as exactly one text node (React 18
113
+ * inserts `<!-- -->` comment separators BETWEEN adjacent text-producing
114
+ * outputs to keep them independently locatable during hydration, but those
115
+ * comments are not text nodes and do not shift text-node indices). A leaf
116
+ * with 2+ dynamic parts that all resolve to STATIC fields with more than one
117
+ * DISTINCT field name gets `data-agent-field-state="mixed"` plus
118
+ * `data-agent-field-parts` (a comma-separated, source-order field list); any
119
+ * other multi-dynamic-part shape (an unresolvable part, or every part naming
120
+ * the same field) is left unstamped.
121
+ *
122
+ * Recursion into a leaf's structural siblings stops at the nearest NESTED
123
+ * `.map()` collection (a JSXExpressionContainer wrapping a call, not a JSX
124
+ * element child, so it is never a "structural child" this walker descends
125
+ * into) — the innermost-stamped-param rule: a field leaf only ever binds
126
+ * against its nearest enclosing map's own parameter. A nested collection
127
+ * that is itself slice-rooted gets discovered and stamped independently by
128
+ * the same Program-wide `.map()` scan, with its own entity/row identity.
129
+ *
130
+ * ── Failure posture ─────────────────────────────────────────────────────
131
+ *
132
+ * Every resolution step is an explicit allowlist; anything the resolver is
133
+ * not sure about degrades to NO STAMP, never a guessed one — a fragment or
134
+ * conditional expression at the callback's JSX root, an unresolvable
135
+ * component usage, a non-slice source, an ObjectPattern without an `id` key,
136
+ * a call inside a duplicated ternary test, ambiguous control flow (2+ return
137
+ * statements) — all leave the subtree untouched.
138
+ *
139
+ * ── Idempotence ─────────────────────────────────────────────────────────
140
+ *
141
+ * The plugin never rewrites SOURCE (D15) and never touches an existing
142
+ * attribute: every stamp is guarded by a same-name attribute-presence check,
143
+ * and a row root that already carries `data-agent-entity` short-circuits the
144
+ * whole map (no re-resolution, no field re-walk) — running the plugin twice
145
+ * produces byte-identical output.
146
+ */
147
+
148
+ interface CollectionBindingPluginOptions {
149
+ /** Skip files matching this pattern. @default /node_modules/ */
150
+ fileExclude?: RegExp;
151
+ }
152
+ declare function babelPluginCollectionBinding({ types: t }: {
153
+ types: typeof types;
154
+ }, opts?: CollectionBindingPluginOptions): PluginObj;
155
+
156
+ export { type CollectionBindingPluginOptions, babelPluginCollectionBinding };
@@ -0,0 +1,156 @@
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
+ * ── Field resolution ────────────────────────────────────────────────────
76
+ *
77
+ * Reuses `meaningfulChildren` from `babel-plugin-editor-meta.ts` (the exact
78
+ * same purity classifier both plugins must agree on) to find LEAF elements:
79
+ * JSX elements whose only meaningful children are text/expressions, never
80
+ * nested JSX elements. A leaf's dynamic part(s) classify as:
81
+ *
82
+ * - a bare member expression off a "whole row object" binding
83
+ * (`a.title`) → static field, UNLESS the member is `id` (id is NEVER
84
+ * stamped as a field — it already carries the row identity);
85
+ * - `a.x ?? a.y [?? …]` (an N-part nullish chain of member fields) →
86
+ * a RUNTIME field-selector expression mirroring the chain
87
+ * (`a.x != null ? "x" : a.y != null ? "y" : "z"`), or, when the chain
88
+ * ends in a static literal fallback, a plain static field;
89
+ * - `a.x || 'fallback'` → static field (literal-fallback only; `||`
90
+ * does not get the N-part runtime treatment `??` does);
91
+ * - `cond ? a.x : a.y` (both branches member fields of the row object) →
92
+ * a RUNTIME field-selector expression duplicating `cond` verbatim
93
+ * (`cond ? "x" : "y"`) — CONSTRAINED (C3): `cond` must be CALL-FREE
94
+ * (checked recursively over the whole test subtree); a call anywhere in
95
+ * the duplicated test would double-evaluate it across SSR and hydration,
96
+ * so the whole leaf is left unstamped instead;
97
+ * - `cond ? 'Literal' : 'Other'` where `cond` is itself a bare field
98
+ * member (a boolean-typed field driving two literal branches) → static
99
+ * field named by `cond` — the FE's typed controls (D7, a different
100
+ * mechanism, M3) read the boolean's true/false rendering from
101
+ * `window.__slice`, not from this attribute;
102
+ * - a COMPUTED member (`a[expr]`, `expr` not a string literal) → no field
103
+ * name, `data-agent-field-state="unresolved"` (a positive "this is
104
+ * row-scoped data, but which field is unknown" signal — never silent).
105
+ *
106
+ * A leaf with exactly one meaningful child (no surrounding text) gets a bare
107
+ * `data-agent-field`. A leaf with 2+ meaningful children (label + value,
108
+ * D6) where EXACTLY ONE is dynamic gets the same `data-agent-field` PLUS
109
+ * `data-agent-field-slot`: the dynamic part's 0-based index among the
110
+ * element's own meaningful children. This is deliberately equal to its
111
+ * text-node index in the eventual SSR/hydrated DOM: every meaningful JSXText
112
+ * or JSXExpressionContainer child renders as exactly one text node (React 18
113
+ * inserts `<!-- -->` comment separators BETWEEN adjacent text-producing
114
+ * outputs to keep them independently locatable during hydration, but those
115
+ * comments are not text nodes and do not shift text-node indices). A leaf
116
+ * with 2+ dynamic parts that all resolve to STATIC fields with more than one
117
+ * DISTINCT field name gets `data-agent-field-state="mixed"` plus
118
+ * `data-agent-field-parts` (a comma-separated, source-order field list); any
119
+ * other multi-dynamic-part shape (an unresolvable part, or every part naming
120
+ * the same field) is left unstamped.
121
+ *
122
+ * Recursion into a leaf's structural siblings stops at the nearest NESTED
123
+ * `.map()` collection (a JSXExpressionContainer wrapping a call, not a JSX
124
+ * element child, so it is never a "structural child" this walker descends
125
+ * into) — the innermost-stamped-param rule: a field leaf only ever binds
126
+ * against its nearest enclosing map's own parameter. A nested collection
127
+ * that is itself slice-rooted gets discovered and stamped independently by
128
+ * the same Program-wide `.map()` scan, with its own entity/row identity.
129
+ *
130
+ * ── Failure posture ─────────────────────────────────────────────────────
131
+ *
132
+ * Every resolution step is an explicit allowlist; anything the resolver is
133
+ * not sure about degrades to NO STAMP, never a guessed one — a fragment or
134
+ * conditional expression at the callback's JSX root, an unresolvable
135
+ * component usage, a non-slice source, an ObjectPattern without an `id` key,
136
+ * a call inside a duplicated ternary test, ambiguous control flow (2+ return
137
+ * statements) — all leave the subtree untouched.
138
+ *
139
+ * ── Idempotence ─────────────────────────────────────────────────────────
140
+ *
141
+ * The plugin never rewrites SOURCE (D15) and never touches an existing
142
+ * attribute: every stamp is guarded by a same-name attribute-presence check,
143
+ * and a row root that already carries `data-agent-entity` short-circuits the
144
+ * whole map (no re-resolution, no field re-walk) — running the plugin twice
145
+ * produces byte-identical output.
146
+ */
147
+
148
+ interface CollectionBindingPluginOptions {
149
+ /** Skip files matching this pattern. @default /node_modules/ */
150
+ fileExclude?: RegExp;
151
+ }
152
+ declare function babelPluginCollectionBinding({ types: t }: {
153
+ types: typeof types;
154
+ }, opts?: CollectionBindingPluginOptions): PluginObj;
155
+
156
+ export { type CollectionBindingPluginOptions, babelPluginCollectionBinding };