@buoy-gg/shared-ui 7.0.34 → 7.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/lib/commonjs/dataViewer/CyberpunkInput.js +3 -2
  2. package/lib/commonjs/dataViewer/DataTreeActionBar.js +3 -2
  3. package/lib/commonjs/dataViewer/DataTreeActionDock.js +2 -1
  4. package/lib/commonjs/dataViewer/DiffSummary.js +4 -3
  5. package/lib/commonjs/dataViewer/LiveExplorer.js +11 -10
  6. package/lib/commonjs/dataViewer/SplitDiffViewer.js +10 -9
  7. package/lib/commonjs/dataViewer/ThemedSplitView.js +12 -11
  8. package/lib/commonjs/dataViewer/ValueEditModal.js +6 -5
  9. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +4 -3
  10. package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +15 -14
  11. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  12. package/lib/commonjs/icons/index.js +540 -1
  13. package/lib/commonjs/index.js +48 -0
  14. package/lib/commonjs/license/FreeAccountPrompt.js +2 -1
  15. package/lib/commonjs/license/LicenseEntryModal.js +2 -1
  16. package/lib/commonjs/sync/keyedArrays.js +209 -0
  17. package/lib/commonjs/sync/toolAttention.test.js +51 -0
  18. package/lib/commonjs/sync/toolUiBridge.js +58 -0
  19. package/lib/commonjs/sync/typedEdit.js +228 -0
  20. package/lib/commonjs/sync/valuePath.js +337 -0
  21. package/lib/commonjs/sync/valueShape.js +161 -0
  22. package/lib/commonjs/ui/components/ChangeRow.js +2 -1
  23. package/lib/commonjs/ui/components/ClipboardHintBanner.js +2 -1
  24. package/lib/commonjs/ui/components/CompactRow.js +3 -2
  25. package/lib/commonjs/ui/components/DataInspector.js +3 -2
  26. package/lib/commonjs/ui/components/DetailView.js +2 -1
  27. package/lib/commonjs/ui/components/DynamicFilterView.js +8 -7
  28. package/lib/commonjs/ui/components/EntityFilterView.js +9 -8
  29. package/lib/commonjs/ui/components/EventHistoryViewer/CompareBar.js +5 -4
  30. package/lib/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.js +2 -1
  31. package/lib/commonjs/ui/components/EventHistoryViewer/EventPickerModal.js +5 -4
  32. package/lib/commonjs/ui/components/EventStepperFooter.js +4 -3
  33. package/lib/commonjs/ui/components/ExpandablePopover.js +48 -8
  34. package/lib/commonjs/ui/components/ExpandedInfoRow.js +3 -2
  35. package/lib/commonjs/ui/components/FilterComponents.js +2 -1
  36. package/lib/commonjs/ui/components/InfoRow.js +2 -1
  37. package/lib/commonjs/ui/components/Pill.js +2 -1
  38. package/lib/commonjs/ui/components/ToolFooter.js +3 -2
  39. package/lib/commonjs/ui/console/CyberpunkSectionButton.js +3 -2
  40. package/lib/commonjs/ui/fonts.js +12 -0
  41. package/lib/commonjs/ui/gameUI/components/GameUICollapsibleSection.js +4 -3
  42. package/lib/commonjs/ui/gameUI/components/GameUICompactStats.js +12 -11
  43. package/lib/commonjs/ui/gameUI/components/GameUIIssuesList.js +9 -8
  44. package/lib/commonjs/ui/gameUI/components/GameUIStatusHeader.js +4 -3
  45. package/lib/commonjs/ui/index.js +8 -0
  46. package/lib/commonjs/ui/toolTheme.js +4 -3
  47. package/lib/module/dataViewer/CyberpunkInput.js +3 -2
  48. package/lib/module/dataViewer/DataTreeActionBar.js +3 -2
  49. package/lib/module/dataViewer/DataTreeActionDock.js +2 -1
  50. package/lib/module/dataViewer/DiffSummary.js +4 -3
  51. package/lib/module/dataViewer/LiveExplorer.js +11 -11
  52. package/lib/module/dataViewer/SplitDiffViewer.js +10 -9
  53. package/lib/module/dataViewer/ThemedSplitView.js +12 -11
  54. package/lib/module/dataViewer/ValueEditModal.js +6 -5
  55. package/lib/module/dataViewer/VirtualizedDataExplorer.js +5 -4
  56. package/lib/module/dataViewer/tree/TreeDiffViewer.js +15 -15
  57. package/lib/module/hooks/safe-area-impl.js +1 -1
  58. package/lib/module/icons/index.js +29 -2
  59. package/lib/module/index.js +4 -0
  60. package/lib/module/license/FreeAccountPrompt.js +2 -2
  61. package/lib/module/license/LicenseEntryModal.js +3 -3
  62. package/lib/module/sync/keyedArrays.js +204 -0
  63. package/lib/module/sync/toolAttention.test.js +50 -0
  64. package/lib/module/sync/toolUiBridge.js +55 -0
  65. package/lib/module/sync/typedEdit.js +220 -0
  66. package/lib/module/sync/valuePath.js +330 -0
  67. package/lib/module/sync/valueShape.js +153 -0
  68. package/lib/module/ui/components/ChangeRow.js +2 -1
  69. package/lib/module/ui/components/ClipboardHintBanner.js +2 -1
  70. package/lib/module/ui/components/CompactRow.js +3 -2
  71. package/lib/module/ui/components/DataInspector.js +3 -2
  72. package/lib/module/ui/components/DetailView.js +2 -1
  73. package/lib/module/ui/components/DynamicFilterView.js +8 -7
  74. package/lib/module/ui/components/EntityFilterView.js +9 -8
  75. package/lib/module/ui/components/EventHistoryViewer/CompareBar.js +6 -5
  76. package/lib/module/ui/components/EventHistoryViewer/DiffModeTabs.js +3 -2
  77. package/lib/module/ui/components/EventHistoryViewer/EventPickerModal.js +6 -5
  78. package/lib/module/ui/components/EventStepperFooter.js +4 -3
  79. package/lib/module/ui/components/ExpandablePopover.js +48 -8
  80. package/lib/module/ui/components/ExpandedInfoRow.js +3 -2
  81. package/lib/module/ui/components/FilterComponents.js +2 -2
  82. package/lib/module/ui/components/InfoRow.js +2 -1
  83. package/lib/module/ui/components/Pill.js +2 -1
  84. package/lib/module/ui/components/ToolFooter.js +3 -2
  85. package/lib/module/ui/console/CyberpunkSectionButton.js +3 -2
  86. package/lib/module/ui/fonts.js +8 -0
  87. package/lib/module/ui/gameUI/components/GameUICollapsibleSection.js +4 -3
  88. package/lib/module/ui/gameUI/components/GameUICompactStats.js +12 -11
  89. package/lib/module/ui/gameUI/components/GameUIIssuesList.js +9 -8
  90. package/lib/module/ui/gameUI/components/GameUIStatusHeader.js +4 -3
  91. package/lib/module/ui/index.js +3 -0
  92. package/lib/module/ui/toolTheme.js +4 -4
  93. package/lib/typescript/commonjs/dataViewer/CyberpunkInput.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/dataViewer/DiffSummary.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/dataViewer/SplitDiffViewer.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/dataViewer/ThemedSplitView.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  104. package/lib/typescript/commonjs/icons/index.d.ts +4 -1
  105. package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/index.d.ts +4 -0
  107. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/license/LicenseEntryModal.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/sync/keyedArrays.d.ts +35 -0
  111. package/lib/typescript/commonjs/sync/keyedArrays.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts +2 -0
  113. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +25 -0
  115. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/sync/typedEdit.d.ts +70 -0
  117. package/lib/typescript/commonjs/sync/typedEdit.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sync/valuePath.d.ts +52 -0
  119. package/lib/typescript/commonjs/sync/valuePath.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/sync/valueShape.d.ts +77 -0
  121. package/lib/typescript/commonjs/sync/valueShape.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/DataInspector.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/DetailView.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/CompareBar.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts +18 -1
  134. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/ui/components/Pill.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/ui/console/CyberpunkSectionButton.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/fonts.d.ts +7 -0
  142. package/lib/typescript/commonjs/ui/fonts.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/ui/gameUI/components/GameUICollapsibleSection.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/gameUI/components/GameUICompactStats.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/gameUI/components/GameUIIssuesList.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  148. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/toolTheme.d.ts.map +1 -1
  150. package/lib/typescript/module/dataViewer/CyberpunkInput.d.ts.map +1 -1
  151. package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts.map +1 -1
  152. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  153. package/lib/typescript/module/dataViewer/DiffSummary.d.ts.map +1 -1
  154. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -1
  155. package/lib/typescript/module/dataViewer/SplitDiffViewer.d.ts.map +1 -1
  156. package/lib/typescript/module/dataViewer/ThemedSplitView.d.ts.map +1 -1
  157. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts.map +1 -1
  158. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  159. package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  160. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  161. package/lib/typescript/module/icons/index.d.ts +4 -1
  162. package/lib/typescript/module/icons/index.d.ts.map +1 -1
  163. package/lib/typescript/module/index.d.ts +4 -0
  164. package/lib/typescript/module/index.d.ts.map +1 -1
  165. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -1
  166. package/lib/typescript/module/license/LicenseEntryModal.d.ts.map +1 -1
  167. package/lib/typescript/module/sync/keyedArrays.d.ts +35 -0
  168. package/lib/typescript/module/sync/keyedArrays.d.ts.map +1 -0
  169. package/lib/typescript/module/sync/toolAttention.test.d.ts +2 -0
  170. package/lib/typescript/module/sync/toolAttention.test.d.ts.map +1 -0
  171. package/lib/typescript/module/sync/toolUiBridge.d.ts +25 -0
  172. package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -1
  173. package/lib/typescript/module/sync/typedEdit.d.ts +70 -0
  174. package/lib/typescript/module/sync/typedEdit.d.ts.map +1 -0
  175. package/lib/typescript/module/sync/valuePath.d.ts +52 -0
  176. package/lib/typescript/module/sync/valuePath.d.ts.map +1 -0
  177. package/lib/typescript/module/sync/valueShape.d.ts +77 -0
  178. package/lib/typescript/module/sync/valueShape.d.ts.map +1 -0
  179. package/lib/typescript/module/ui/components/ChangeRow.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  181. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/components/DataInspector.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/components/DetailView.d.ts.map +1 -1
  184. package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/EntityFilterView.d.ts.map +1 -1
  186. package/lib/typescript/module/ui/components/EventHistoryViewer/CompareBar.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts +18 -1
  191. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  193. package/lib/typescript/module/ui/components/FilterComponents.d.ts.map +1 -1
  194. package/lib/typescript/module/ui/components/InfoRow.d.ts.map +1 -1
  195. package/lib/typescript/module/ui/components/Pill.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/components/ToolFooter.d.ts.map +1 -1
  197. package/lib/typescript/module/ui/console/CyberpunkSectionButton.d.ts.map +1 -1
  198. package/lib/typescript/module/ui/fonts.d.ts +7 -0
  199. package/lib/typescript/module/ui/fonts.d.ts.map +1 -0
  200. package/lib/typescript/module/ui/gameUI/components/GameUICollapsibleSection.d.ts.map +1 -1
  201. package/lib/typescript/module/ui/gameUI/components/GameUICompactStats.d.ts.map +1 -1
  202. package/lib/typescript/module/ui/gameUI/components/GameUIIssuesList.d.ts.map +1 -1
  203. package/lib/typescript/module/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/index.d.ts +1 -0
  205. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  206. package/lib/typescript/module/ui/toolTheme.d.ts.map +1 -1
  207. package/package.json +4 -4
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The typed-edit guard — ONE implementation, for every surface that takes a
5
+ * merge patch from a remote caller.
6
+ *
7
+ * The rule, which is the React Query devtools value-editor rule: you may change
8
+ * the VALUE of a field that already exists, to the same kind of thing. You may
9
+ * not add a field, change a field's type, or null a list or object the screen
10
+ * renders. A list may grow or shrink — carts are lists you add to — but every
11
+ * item in it must have the same fields as the items already there, because an
12
+ * item missing a field the others have is what crashes the list on render.
13
+ *
14
+ * WHY IT LIVES HERE. This existed twice, once in the zustand adapter and once in
15
+ * react-query, drifting slightly (one said "the ones already in the list", the
16
+ * other "the items"; only one had the did-you-mean hint). Jotai had NO copy at
17
+ * all, so `setAtom` accepted a string over a number or a one-item array over a
18
+ * fifty-item one with nothing to stop it. Three copies of a data-destroying
19
+ * safety rule is two more than anyone can keep in step — the same argument
20
+ * `keyedArrays.ts` makes about the expansion it owns.
21
+ *
22
+ * The one behavioural knob is `noun`: what to call the thing being edited in a
23
+ * refusal ("this store", "this data", "this atom"). Everything else is shared,
24
+ * deliberately — a rule that differs per surface is a rule nobody can state.
25
+ */
26
+
27
+ const isPlainObject = v => !!v && typeof v === "object" && !Array.isArray(v);
28
+
29
+ /** The shape category the screen depends on: array vs object vs the rest. */
30
+ export function shapeKind(v) {
31
+ if (Array.isArray(v)) return "array";
32
+ if (v === null) return "null";
33
+ if (typeof v === "object") return "object";
34
+ return typeof v;
35
+ }
36
+ const SCALARS = ["string", "number", "boolean"];
37
+
38
+ /**
39
+ * Where a key actually lives in a nested value, as a dotted path — first match,
40
+ * breadth-first, capped so a huge payload cannot stall the walk.
41
+ *
42
+ * This is what turns "you put `name` at the top" into "`name` lives at
43
+ * `item.name`", which is the difference between a caller that gives up and one
44
+ * that fixes it. Ten of eleven measured `query` write failures were this exact
45
+ * mistake.
46
+ */
47
+ export function findKeyPath(obj, key, maxNodes = 2000) {
48
+ const queue = [{
49
+ node: obj,
50
+ path: ""
51
+ }];
52
+ let seen = 0;
53
+ while (queue.length && seen < maxNodes) {
54
+ const {
55
+ node,
56
+ path
57
+ } = queue.shift();
58
+ seen++;
59
+ if (Array.isArray(node)) {
60
+ node.slice(0, 20).forEach((v, i) => queue.push({
61
+ node: v,
62
+ path: `${path}[${i}]`
63
+ }));
64
+ } else if (isPlainObject(node)) {
65
+ for (const [k, v] of Object.entries(node)) {
66
+ const p = path ? `${path}.${k}` : k;
67
+ if (k === key && path) return p; // path set => nested, not the top level
68
+ if (isPlainObject(v) || Array.isArray(v)) queue.push({
69
+ node: v,
70
+ path: p
71
+ });
72
+ }
73
+ }
74
+ }
75
+ return undefined;
76
+ }
77
+ /**
78
+ * Every way this patch would break the value it is merged into. Empty means the
79
+ * edit is safe to apply.
80
+ */
81
+ export function typedEditViolations(current, patch, opts = {}, root = current, path = "", out = []) {
82
+ const noun = opts.noun ?? "data";
83
+ const here = path || "(root)";
84
+ if (Array.isArray(patch)) {
85
+ if (!Array.isArray(current)) {
86
+ out.push({
87
+ path: here,
88
+ kind: "type-change",
89
+ detail: `${shapeKind(current)} → array`
90
+ });
91
+ return out;
92
+ }
93
+ // Length may change — a cart is a list you add to and remove from — but each
94
+ // item must match the shape of the ones already there. The first item is the
95
+ // template; an added item missing a field the others have is what makes the
96
+ // list throw when it renders that field.
97
+ const template = current.length ? current[0] : undefined;
98
+ if (template !== undefined && (isPlainObject(template) || Array.isArray(template))) {
99
+ patch.forEach((el, i) => {
100
+ const ep = `${path}[${i}]`;
101
+ if (isPlainObject(template) && isPlainObject(el)) {
102
+ for (const k of Object.keys(template)) {
103
+ if (!(k in el)) {
104
+ out.push({
105
+ path: `${ep}.${k}`,
106
+ kind: "missing-key",
107
+ detail: `list items need \`${k}\` — the items already in the list have it`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ typedEditViolations(template, el, opts, root, ep, out);
113
+ });
114
+ }
115
+ return out;
116
+ }
117
+ if (isPlainObject(patch)) {
118
+ if (!isPlainObject(current)) {
119
+ out.push({
120
+ path: here,
121
+ kind: "type-change",
122
+ detail: `${shapeKind(current)} → object`
123
+ });
124
+ return out;
125
+ }
126
+ for (const [k, v] of Object.entries(patch)) {
127
+ const p = path ? `${path}.${k}` : k;
128
+ if (!(k in current)) {
129
+ const elsewhere = findKeyPath(root, k);
130
+ out.push({
131
+ path: p,
132
+ kind: "add-key",
133
+ detail: elsewhere ? `no such field here — did you mean \`${elsewhere}\`?` : `no field named \`${k}\` exists in this ${noun}`,
134
+ ...(elsewhere ? {
135
+ movedTo: elsewhere
136
+ } : {})
137
+ });
138
+ } else {
139
+ typedEditViolations(current[k], v, opts, root, p, out);
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+
145
+ // A leaf: the edit itself.
146
+ const ck = shapeKind(current);
147
+ const pk = shapeKind(patch);
148
+ // Filling an empty field and CLEARING one are the same kind of edit, and this
149
+ // guard used to allow only the first. That asymmetry had a cost: "take the
150
+ // promo off my bag" is `string → null`, the refusal named `force: true` as the
151
+ // only way through, and a model that anticipated it wrote the forced call
152
+ // pre-emptively — reaching for the one parameter that bypasses EVERY
153
+ // protection in order to do something completely ordinary. Measured on
154
+ // `cart/take-promo-off`: an S1 on an otherwise correct flow.
155
+ //
156
+ // Nulling a LIST or OBJECT the screen renders stays refused. That is the crash
157
+ // this guard exists for (`sizes.map` on null); a null scalar renders as empty.
158
+ const fillingNull = ck === "null" && SCALARS.includes(pk);
159
+ const clearingScalar = pk === "null" && SCALARS.includes(ck);
160
+ if (ck !== pk && !fillingNull && !clearingScalar) {
161
+ out.push({
162
+ path: here,
163
+ kind: "type-change",
164
+ detail: `${ck} → ${pk}`
165
+ });
166
+ }
167
+ return out;
168
+ }
169
+
170
+ /**
171
+ * The patch the caller should have sent, when every problem was a field written
172
+ * at the wrong DEPTH.
173
+ *
174
+ * The guard already worked out where each field really lives in order to say
175
+ * "did you mean `item.name`?", so it can rebuild the patch instead of leaving
176
+ * the caller to re-derive it from prose. Returns undefined unless EVERY
177
+ * violation is a relocatable add-key: a partial suggestion that silently drops
178
+ * the parts we could not fix would be worse than none, because the caller would
179
+ * send it, be refused again, and learn nothing.
180
+ */
181
+ export function suggestedPatch(patch, violations) {
182
+ if (!violations.length || !violations.every(v => v.kind === "add-key" && v.movedTo)) return undefined;
183
+ const out = {};
184
+ for (const v of violations) {
185
+ const value = getAtDotted(patch, v.path);
186
+ if (value === undefined) return undefined;
187
+ // `movedTo` can name a list element (`results[0].name`); a merge patch
188
+ // cannot address one by index, and rewriting it to the id-addressed form
189
+ // needs the list itself. Leave those to the prose rather than suggest
190
+ // something that will not apply.
191
+ if (v.movedTo.includes("[")) return undefined;
192
+ let node = out;
193
+ const segs = v.movedTo.split(".");
194
+ segs.slice(0, -1).forEach(seg => {
195
+ node[seg] ??= {};
196
+ node = node[seg];
197
+ });
198
+ node[segs[segs.length - 1]] = value;
199
+ }
200
+ return out;
201
+ }
202
+ function getAtDotted(obj, path) {
203
+ let cur = obj;
204
+ for (const seg of path.split(".")) {
205
+ if (!isPlainObject(cur)) return undefined;
206
+ cur = cur[seg];
207
+ }
208
+ return cur;
209
+ }
210
+
211
+ /**
212
+ * The sentence a refusal ends with. Shared so every surface refuses in the same
213
+ * words — a caller that learns the rule on one tool should recognise it on the
214
+ * next.
215
+ */
216
+ export function refusalText(violations, noun, suggestion) {
217
+ const listed = violations.slice(0, 6).map(v => `\`${v.path}\`: ${v.detail}`).join("; ");
218
+ const more = violations.length > 6 ? `; +${violations.length - 6} more` : "";
219
+ return `Refused: this isn't a same-type edit of existing fields (the rule the React Query devtools enforces). ${listed}${more}. ` + `You can only change the VALUE of a field that already exists, to the same type — you can't add fields, change a field's type, ` + `or null a list/object the screen renders. Clearing a scalar to null is fine. Send just the field(s) you're changing, nested to ` + `match the current shape of this ${noun}. Use force:true only to deliberately break that.` + (suggestion ? ` This is the same edit at the right depth — send it as the patch: ${JSON.stringify(suggestion)}` : "");
220
+ }
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Addressing a single value inside cached data or a store — one implementation,
5
+ * shared by every adapter that reads or merges.
6
+ *
7
+ * WHY. The most common measured write failure is not a wrong value, it is a
8
+ * wrong DEPTH: ten of eleven `query` failures on the bench sent `{name: "x"}`
9
+ * for a payload shaped `{item: {name}}`. That is not a missing instruction — the
10
+ * system prompt says, verbatim, "if the value is {item:{name}} send
11
+ * {"item":{"name":"test123"}}, never {name}" — and models that had just called
12
+ * `getQueryData` still flattened it. The rule is being read past, not missed.
13
+ *
14
+ * So stop asking. `path: "item.name", value: "x"` has no depth to get wrong:
15
+ * there is exactly one way to write it and the adapter builds the nested patch.
16
+ * The same syntax on the READ side returns one slice instead of a payload that
17
+ * gets cut off at the engine's 24,000-character cap, which is the other half of
18
+ * the same problem — a caller cannot write a shape it was never able to see.
19
+ *
20
+ * Lists are addressed by IDENTITY, never by position. `lines[0]` is resolved to
21
+ * the id of whatever is at index 0 right now and rewritten as an id-addressed
22
+ * edit, because between the read and the write an item may have been removed and
23
+ * index 0 is then a different row. A list whose items carry no stable id is
24
+ * refused rather than guessed at — see keyedArrays.ts for why silently picking a
25
+ * row is the failure mode that loses data.
26
+ */
27
+ import { identityField } from "./keyedArrays.js";
28
+ const isPlainObject = v => typeof v === "object" && v !== null && !Array.isArray(v);
29
+ /**
30
+ * Where a field of this name actually lives, as a dotted path — breadth-first,
31
+ * capped so a big payload cannot stall the walk.
32
+ *
33
+ * A wrong `path` is not the same mistake as a wrong depth, and `path` does not
34
+ * prevent it. Measured on TWO cases in one run: grok wrote
35
+ * `path: "item.name"` against the flat PokéAPI payload `{name, id, stats, …}`,
36
+ * having generalised the shape from a different screen it had seen. The value it
37
+ * wanted was at `name`, one segment, right there. A refusal that says so is a
38
+ * one-turn recovery; a refusal that only lists the available fields makes the
39
+ * caller re-derive it.
40
+ */
41
+ function findFieldPath(obj, key, maxNodes = 2000) {
42
+ const queue = [{
43
+ node: obj,
44
+ path: ""
45
+ }];
46
+ let seen = 0;
47
+ while (queue.length && seen < maxNodes) {
48
+ const {
49
+ node,
50
+ path
51
+ } = queue.shift();
52
+ seen++;
53
+ if (Array.isArray(node)) {
54
+ node.slice(0, 20).forEach((v, i) => queue.push({
55
+ node: v,
56
+ path: `${path}[${i}]`
57
+ }));
58
+ } else if (isPlainObject(node)) {
59
+ for (const [k, v] of Object.entries(node)) {
60
+ const p = path ? `${path}.${k}` : k;
61
+ if (k === key) return p;
62
+ if (isPlainObject(v) || Array.isArray(v)) queue.push({
63
+ node: v,
64
+ path: p
65
+ });
66
+ }
67
+ }
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ /**
73
+ * "No field X here" plus, when the field exists somewhere else, the path that
74
+ * would have worked. `leaf` is the segment the caller was ultimately after.
75
+ */
76
+ function missingFieldError(root, at, key, available, leaf) {
77
+ const elsewhere = findFieldPath(root, leaf ?? key);
78
+ const has = available.slice(0, 12).join(", ") || "(no fields)";
79
+ return elsewhere ? `No field "${key}" at "${at}" — did you mean path "${elsewhere}"? "${at}" has: ${has}.` : `No field "${key}" at "${at}". It has: ${has}.`;
80
+ }
81
+
82
+ /** Read a dotted field path inside one list item. */
83
+ function fieldOf(item, field) {
84
+ let cur = item;
85
+ for (const seg of field.split(".")) {
86
+ if (!isPlainObject(cur)) return undefined;
87
+ cur = cur[seg];
88
+ }
89
+ return cur;
90
+ }
91
+
92
+ /**
93
+ * `lines[0].addonIds`, `item.name`, `results[name=pikachu].url` → segments.
94
+ *
95
+ * Returns undefined for syntax it does not understand rather than doing
96
+ * something approximate with it.
97
+ */
98
+ export function parsePath(path) {
99
+ if (!path.trim()) return undefined;
100
+ const out = [];
101
+ // Walk the string rather than splitting on ".", because a [field=value]
102
+ // selector may legitimately contain a dot ("results[url=a.b/c]").
103
+ let i = 0;
104
+ while (i < path.length) {
105
+ if (path[i] === ".") {
106
+ i++;
107
+ continue;
108
+ }
109
+ if (path[i] === "[") {
110
+ const end = path.indexOf("]", i);
111
+ if (end === -1) return undefined;
112
+ const inner = path.slice(i + 1, end);
113
+ if (/^\d+$/.test(inner)) out.push({
114
+ kind: "index",
115
+ index: Number(inner)
116
+ });else {
117
+ const eq = inner.indexOf("=");
118
+ if (eq <= 0) return undefined;
119
+ out.push({
120
+ kind: "match",
121
+ field: inner.slice(0, eq).trim(),
122
+ value: inner.slice(eq + 1).trim()
123
+ });
124
+ }
125
+ i = end + 1;
126
+ continue;
127
+ }
128
+ let j = i;
129
+ while (j < path.length && path[j] !== "." && path[j] !== "[") j++;
130
+ const key = path.slice(i, j);
131
+ if (!key) return undefined;
132
+ out.push({
133
+ kind: "key",
134
+ key
135
+ });
136
+ i = j;
137
+ }
138
+ return out.length ? out : undefined;
139
+ }
140
+
141
+ /** Read the value at `path`, or undefined if any step of it does not exist. */
142
+ export function readPath(value, path) {
143
+ const segs = parsePath(path);
144
+ if (!segs) return {
145
+ ok: false,
146
+ error: `Could not read the path "${path}". Use dotted keys with optional [0] or [field=value], like "item.name" or "lines[lineId=seed-1].qty".`
147
+ };
148
+ let cur = value;
149
+ const walked = [];
150
+ for (const seg of segs) {
151
+ if (cur === null || cur === undefined) {
152
+ return {
153
+ ok: false,
154
+ error: `"${walked.join(".") || "(root)"}" is ${cur === null ? "null" : "not set"}, so there is nothing at "${path}".`
155
+ };
156
+ }
157
+ if (seg.kind === "key") {
158
+ if (!isPlainObject(cur)) return {
159
+ ok: false,
160
+ error: `"${walked.join(".") || "(root)"}" is not an object, so it has no field "${seg.key}".`
161
+ };
162
+ if (!(seg.key in cur)) {
163
+ const leafSeg = segs[segs.length - 1];
164
+ return {
165
+ ok: false,
166
+ error: missingFieldError(value, walked.join(".") || "(root)", seg.key, Object.keys(cur), leafSeg?.kind === "key" ? leafSeg.key : undefined)
167
+ };
168
+ }
169
+ cur = cur[seg.key];
170
+ walked.push(seg.key);
171
+ } else if (seg.kind === "index") {
172
+ if (!Array.isArray(cur)) return {
173
+ ok: false,
174
+ error: `"${walked.join(".") || "(root)"}" is not a list, so [${seg.index}] means nothing.`
175
+ };
176
+ cur = cur[seg.index];
177
+ walked.push(`[${seg.index}]`);
178
+ } else {
179
+ if (!Array.isArray(cur)) return {
180
+ ok: false,
181
+ error: `"${walked.join(".") || "(root)"}" is not a list, so [${seg.field}=${seg.value}] means nothing.`
182
+ };
183
+ const hit = cur.find(el => isPlainObject(el) && String(fieldOf(el, seg.field)) === seg.value);
184
+ if (hit === undefined) return {
185
+ ok: false,
186
+ error: `No item with ${seg.field}="${seg.value}" in "${walked.join(".") || "(root)"}".`
187
+ };
188
+ cur = hit;
189
+ walked.push(`[${seg.field}=${seg.value}]`);
190
+ }
191
+ }
192
+ return {
193
+ ok: true,
194
+ value: cur
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Build the merge patch that sets `path` to `value`, against the data as it is
200
+ * now.
201
+ *
202
+ * The result is an ordinary merge patch, so it goes through the SAME typed-edit
203
+ * guard as a hand-written one and gains no new powers — `path` is a way to say
204
+ * the same thing without being able to get the depth wrong, not a way around
205
+ * the rules. A list step becomes the id-addressed form, so the write stays
206
+ * proof against a truncated read.
207
+ */
208
+ export function patchFromPath(current, path, value) {
209
+ const segs = parsePath(path);
210
+ if (!segs) return {
211
+ ok: false,
212
+ error: `Could not read the path "${path}". Use dotted keys with optional [0] or [field=value], like "item.name" or "lines[lineId=seed-1].qty".`
213
+ };
214
+ if (segs[0]?.kind !== "key") return {
215
+ ok: false,
216
+ error: `A path must start with a field name, not a list index — "${path}" does not say which list.`
217
+ };
218
+ const build = (node, at) => {
219
+ if (at >= segs.length) return {
220
+ ok: true,
221
+ patch: value
222
+ };
223
+ const seg = segs[at];
224
+ if (seg.kind === "key") {
225
+ // A field that does not exist is left for the typed-edit guard, which owns
226
+ // that decision — EXCEPT when there are more segments to walk, because
227
+ // then the caller is describing a shape that is not there at all and the
228
+ // guard's message ("no field named `item`") would not say where the value
229
+ // it wanted actually lives.
230
+ if (isPlainObject(node) && !(seg.key in node) && at + 1 < segs.length) {
231
+ const leafSeg = segs[segs.length - 1];
232
+ return {
233
+ ok: false,
234
+ error: missingFieldError(current, pathUpTo(segs, at), seg.key, Object.keys(node), leafSeg?.kind === "key" ? leafSeg.key : undefined)
235
+ };
236
+ }
237
+ const child = isPlainObject(node) ? node[seg.key] : undefined;
238
+ const inner = build(child, at + 1);
239
+ return inner.ok ? {
240
+ ok: true,
241
+ patch: {
242
+ [seg.key]: inner.patch
243
+ }
244
+ } : inner;
245
+ }
246
+ if (!Array.isArray(node)) {
247
+ return {
248
+ ok: false,
249
+ error: `"${pathUpTo(segs, at)}" is not a list, so "${path}" cannot be resolved. Read it first — \`shape\` on the read says what each field holds.`
250
+ };
251
+ }
252
+ const index = seg.kind === "index" ? seg.index : node.findIndex(el => isPlainObject(el) && String(fieldOf(el, seg.field)) === seg.value);
253
+ const item = index >= 0 ? node[index] : undefined;
254
+ const missing = seg.kind === "index" ? index < 0 || index >= node.length : index < 0;
255
+ if (missing) {
256
+ return {
257
+ ok: false,
258
+ error: seg.kind === "index" ? `"${pathUpTo(segs, at)}" has ${node.length} item(s), so there is nothing at [${seg.index}].` : `No item with ${seg.field}="${seg.value}" in "${pathUpTo(segs, at)}".`
259
+ };
260
+ }
261
+ /**
262
+ * A list of PRIMITIVES, addressed by index — `tags[0]` on
263
+ * `["alpha","beta"]`.
264
+ *
265
+ * The guard below used to be `!isPlainObject(item)`, which folded this in
266
+ * with a genuinely out-of-range index and answered `"tags" has 2 item(s),
267
+ * so there is nothing at [0]` — a sentence that contradicts itself, for the
268
+ * commonest index there is. A model reading the data, seeing two items and
269
+ * being told [0] and [1] are both empty has nowhere left to go.
270
+ *
271
+ * There is nothing to descend into on a string, so the index IS the leaf:
272
+ * rebuild the list with that one element replaced, exactly as the no-stable-id
273
+ * branch below does. Anything deeper genuinely cannot be resolved, and says so.
274
+ */
275
+ if (!isPlainObject(item)) {
276
+ if (at < segs.length - 1) {
277
+ return {
278
+ ok: false,
279
+ error: `"${pathUpTo(segs, at)}[${seg.kind === "index" ? seg.index : `${seg.field}=${seg.value}`}]" holds ${item === null ? "null" : typeof item}, not an object, so "${path}" cannot go any deeper. Set the item itself.`
280
+ };
281
+ }
282
+ const replaced = node.slice();
283
+ replaced[index] = value;
284
+ return {
285
+ ok: true,
286
+ patch: replaced
287
+ };
288
+ }
289
+ const inner = build(item, at + 1);
290
+ if (!inner.ok) return inner;
291
+ const idField = identityField(node);
292
+ // Positions move; ids do not. Prefer the addressed form whichever way the
293
+ // caller named the row.
294
+ if (idField) return {
295
+ ok: true,
296
+ patch: {
297
+ [String(item[idField])]: inner.patch
298
+ }
299
+ };
300
+
301
+ // No stable id — a Pokémon's `stats` are `{stat: {name}, base_stat}`, with
302
+ // nothing flat and unique on the item itself, and that is an ordinary shape
303
+ // rather than a broken one. Rebuild the whole list here instead of refusing.
304
+ //
305
+ // This is safe in a way the SAME array coming from a model is not: we are
306
+ // holding the current list in memory, so nothing is being reconstructed from
307
+ // a truncated read and no row can be dropped by omission. The caller still
308
+ // only named one value. What it cannot be is proof against the list changing
309
+ // between this call and the write, which addressing by id would be — so the
310
+ // id form stays preferred wherever one exists.
311
+ const next = node.slice();
312
+ next[index] = isPlainObject(inner.patch) ? {
313
+ ...item,
314
+ ...inner.patch
315
+ } : inner.patch;
316
+ return {
317
+ ok: true,
318
+ patch: next
319
+ };
320
+ };
321
+ const built = build(current, 0);
322
+ if (!built.ok) return built;
323
+ return {
324
+ ok: true,
325
+ patch: built.patch
326
+ };
327
+ }
328
+ function pathUpTo(segs, at) {
329
+ return segs.slice(0, at).map(s => s.kind === "key" ? s.key : s.kind === "index" ? `[${s.index}]` : `[${s.field}=${s.value}]`).join(".").replace(/\.\[/g, "[") || "(root)";
330
+ }