@vendoai/apps 0.4.0

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 (203) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +8 -0
  3. package/dist/adapter-conformance.d.ts +48 -0
  4. package/dist/adapter-conformance.d.ts.map +1 -0
  5. package/dist/adapter-conformance.js +196 -0
  6. package/dist/adapter-conformance.js.map +1 -0
  7. package/dist/agent-tools.d.ts +10 -0
  8. package/dist/agent-tools.d.ts.map +1 -0
  9. package/dist/agent-tools.js +252 -0
  10. package/dist/agent-tools.js.map +1 -0
  11. package/dist/app-data.d.ts +23 -0
  12. package/dist/app-data.d.ts.map +1 -0
  13. package/dist/app-data.js +129 -0
  14. package/dist/app-data.js.map +1 -0
  15. package/dist/app-token.d.ts +24 -0
  16. package/dist/app-token.d.ts.map +1 -0
  17. package/dist/app-token.js +61 -0
  18. package/dist/app-token.js.map +1 -0
  19. package/dist/audit.d.ts +11 -0
  20. package/dist/audit.d.ts.map +1 -0
  21. package/dist/audit.js +19 -0
  22. package/dist/audit.js.map +1 -0
  23. package/dist/automation-plan.d.ts +43 -0
  24. package/dist/automation-plan.d.ts.map +1 -0
  25. package/dist/automation-plan.js +212 -0
  26. package/dist/automation-plan.js.map +1 -0
  27. package/dist/bench/client.d.ts +43 -0
  28. package/dist/bench/client.d.ts.map +1 -0
  29. package/dist/bench/client.js +129 -0
  30. package/dist/bench/client.js.map +1 -0
  31. package/dist/bench/fixtures.d.ts +42 -0
  32. package/dist/bench/fixtures.d.ts.map +1 -0
  33. package/dist/bench/fixtures.js +146 -0
  34. package/dist/bench/fixtures.js.map +1 -0
  35. package/dist/bench/judge.d.ts +9 -0
  36. package/dist/bench/judge.d.ts.map +1 -0
  37. package/dist/bench/judge.js +71 -0
  38. package/dist/bench/judge.js.map +1 -0
  39. package/dist/bench/metrics.d.ts +32 -0
  40. package/dist/bench/metrics.d.ts.map +1 -0
  41. package/dist/bench/metrics.js +172 -0
  42. package/dist/bench/metrics.js.map +1 -0
  43. package/dist/bench/prompts.d.ts +17 -0
  44. package/dist/bench/prompts.d.ts.map +1 -0
  45. package/dist/bench/prompts.js +100 -0
  46. package/dist/bench/prompts.js.map +1 -0
  47. package/dist/bench/report.d.ts +43 -0
  48. package/dist/bench/report.d.ts.map +1 -0
  49. package/dist/bench/report.js +74 -0
  50. package/dist/bench/report.js.map +1 -0
  51. package/dist/bench/runner.d.ts +15 -0
  52. package/dist/bench/runner.d.ts.map +1 -0
  53. package/dist/bench/runner.js +26 -0
  54. package/dist/bench/runner.js.map +1 -0
  55. package/dist/bench/toolfork.d.ts +18 -0
  56. package/dist/bench/toolfork.d.ts.map +1 -0
  57. package/dist/bench/toolfork.js +125 -0
  58. package/dist/bench/toolfork.js.map +1 -0
  59. package/dist/box-agent.d.ts +78 -0
  60. package/dist/box-agent.d.ts.map +1 -0
  61. package/dist/box-agent.js +137 -0
  62. package/dist/box-agent.js.map +1 -0
  63. package/dist/box-env.d.ts +26 -0
  64. package/dist/box-env.d.ts.map +1 -0
  65. package/dist/box-env.js +52 -0
  66. package/dist/box-env.js.map +1 -0
  67. package/dist/call.d.ts +23 -0
  68. package/dist/call.d.ts.map +1 -0
  69. package/dist/call.js +45 -0
  70. package/dist/call.js.map +1 -0
  71. package/dist/cloud.d.ts +4917 -0
  72. package/dist/cloud.d.ts.map +1 -0
  73. package/dist/cloud.js +16 -0
  74. package/dist/cloud.js.map +1 -0
  75. package/dist/e2b/index.d.ts +30 -0
  76. package/dist/e2b/index.d.ts.map +1 -0
  77. package/dist/e2b/index.js +273 -0
  78. package/dist/e2b/index.js.map +1 -0
  79. package/dist/egress-approval.d.ts +73 -0
  80. package/dist/egress-approval.d.ts.map +1 -0
  81. package/dist/egress-approval.js +74 -0
  82. package/dist/egress-approval.js.map +1 -0
  83. package/dist/engine.d.ts +188 -0
  84. package/dist/engine.d.ts.map +1 -0
  85. package/dist/engine.js +1507 -0
  86. package/dist/engine.js.map +1 -0
  87. package/dist/fn.d.ts +22 -0
  88. package/dist/fn.d.ts.map +1 -0
  89. package/dist/fn.js +108 -0
  90. package/dist/fn.js.map +1 -0
  91. package/dist/history.d.ts +21 -0
  92. package/dist/history.d.ts.map +1 -0
  93. package/dist/history.js +158 -0
  94. package/dist/history.js.map +1 -0
  95. package/dist/inclient.d.ts +48 -0
  96. package/dist/inclient.d.ts.map +1 -0
  97. package/dist/inclient.js +68 -0
  98. package/dist/inclient.js.map +1 -0
  99. package/dist/index.d.ts +24 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +25 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/interchange.d.ts +17 -0
  104. package/dist/interchange.d.ts.map +1 -0
  105. package/dist/interchange.js +133 -0
  106. package/dist/interchange.js.map +1 -0
  107. package/dist/machine-lifecycle.d.ts +102 -0
  108. package/dist/machine-lifecycle.d.ts.map +1 -0
  109. package/dist/machine-lifecycle.js +360 -0
  110. package/dist/machine-lifecycle.js.map +1 -0
  111. package/dist/manifest.d.ts +42 -0
  112. package/dist/manifest.d.ts.map +1 -0
  113. package/dist/manifest.js +53 -0
  114. package/dist/manifest.js.map +1 -0
  115. package/dist/open.d.ts +45 -0
  116. package/dist/open.d.ts.map +1 -0
  117. package/dist/open.js +245 -0
  118. package/dist/open.js.map +1 -0
  119. package/dist/parked-action.d.ts +51 -0
  120. package/dist/parked-action.d.ts.map +1 -0
  121. package/dist/parked-action.js +29 -0
  122. package/dist/parked-action.js.map +1 -0
  123. package/dist/persistence.d.ts +41 -0
  124. package/dist/persistence.d.ts.map +1 -0
  125. package/dist/persistence.js +86 -0
  126. package/dist/persistence.js.map +1 -0
  127. package/dist/pins.d.ts +231 -0
  128. package/dist/pins.d.ts.map +1 -0
  129. package/dist/pins.js +213 -0
  130. package/dist/pins.js.map +1 -0
  131. package/dist/pipeline.d.ts +142 -0
  132. package/dist/pipeline.d.ts.map +1 -0
  133. package/dist/pipeline.js +1014 -0
  134. package/dist/pipeline.js.map +1 -0
  135. package/dist/prewired-schema.d.ts +16 -0
  136. package/dist/prewired-schema.d.ts.map +1 -0
  137. package/dist/prewired-schema.js +48 -0
  138. package/dist/prewired-schema.js.map +1 -0
  139. package/dist/redaction.d.ts +13 -0
  140. package/dist/redaction.d.ts.map +1 -0
  141. package/dist/redaction.js +67 -0
  142. package/dist/redaction.js.map +1 -0
  143. package/dist/runtime.d.ts +463 -0
  144. package/dist/runtime.d.ts.map +1 -0
  145. package/dist/runtime.js +1715 -0
  146. package/dist/runtime.js.map +1 -0
  147. package/dist/sandbox.d.ts +90 -0
  148. package/dist/sandbox.d.ts.map +1 -0
  149. package/dist/sandbox.js +2 -0
  150. package/dist/sandbox.js.map +1 -0
  151. package/dist/schedules.d.ts +89 -0
  152. package/dist/schedules.d.ts.map +1 -0
  153. package/dist/schedules.js +334 -0
  154. package/dist/schedules.js.map +1 -0
  155. package/dist/secret-exposure.d.ts +55 -0
  156. package/dist/secret-exposure.d.ts.map +1 -0
  157. package/dist/secret-exposure.js +53 -0
  158. package/dist/secret-exposure.js.map +1 -0
  159. package/dist/ship-diff.d.ts +47 -0
  160. package/dist/ship-diff.d.ts.map +1 -0
  161. package/dist/ship-diff.js +39 -0
  162. package/dist/ship-diff.js.map +1 -0
  163. package/dist/testing/fake-box.d.ts +67 -0
  164. package/dist/testing/fake-box.d.ts.map +1 -0
  165. package/dist/testing/fake-box.js +166 -0
  166. package/dist/testing/fake-box.js.map +1 -0
  167. package/dist/testing/fake-sandbox-v2.d.ts +59 -0
  168. package/dist/testing/fake-sandbox-v2.d.ts.map +1 -0
  169. package/dist/testing/fake-sandbox-v2.js +121 -0
  170. package/dist/testing/fake-sandbox-v2.js.map +1 -0
  171. package/dist/testing/fake-sandbox.d.ts +94 -0
  172. package/dist/testing/fake-sandbox.d.ts.map +1 -0
  173. package/dist/testing/fake-sandbox.js +239 -0
  174. package/dist/testing/fake-sandbox.js.map +1 -0
  175. package/dist/testing/guard-fixture.d.ts +19 -0
  176. package/dist/testing/guard-fixture.d.ts.map +1 -0
  177. package/dist/testing/guard-fixture.js +134 -0
  178. package/dist/testing/guard-fixture.js.map +1 -0
  179. package/dist/testing/index.d.ts +8 -0
  180. package/dist/testing/index.d.ts.map +1 -0
  181. package/dist/testing/index.js +8 -0
  182. package/dist/testing/index.js.map +1 -0
  183. package/dist/testing/memory-store.d.ts +5 -0
  184. package/dist/testing/memory-store.d.ts.map +1 -0
  185. package/dist/testing/memory-store.js +4 -0
  186. package/dist/testing/memory-store.js.map +1 -0
  187. package/dist/testing/scripted-model.d.ts +32 -0
  188. package/dist/testing/scripted-model.d.ts.map +1 -0
  189. package/dist/testing/scripted-model.js +112 -0
  190. package/dist/testing/scripted-model.js.map +1 -0
  191. package/dist/testing/seed-app-row.d.ts +4 -0
  192. package/dist/testing/seed-app-row.d.ts.map +1 -0
  193. package/dist/testing/seed-app-row.js +7 -0
  194. package/dist/testing/seed-app-row.js.map +1 -0
  195. package/dist/unified-diff.d.ts +13 -0
  196. package/dist/unified-diff.d.ts.map +1 -0
  197. package/dist/unified-diff.js +127 -0
  198. package/dist/unified-diff.js.map +1 -0
  199. package/dist/version-hash.d.ts +9 -0
  200. package/dist/version-hash.d.ts.map +1 -0
  201. package/dist/version-hash.js +12 -0
  202. package/dist/version-hash.js.map +1 -0
  203. package/package.json +84 -0
@@ -0,0 +1,1014 @@
1
+ import { compileWireV2, compileWirePatchV2, printWireV2, isPathBinding, isStateBinding, kitPropClasses, KIT_WIRE_COMPONENT_NAMES, shapeAtPointer, } from "@vendoai/core";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ // ---------------------------------------------------------------------------
4
+ // Action-wiring honesty (verify-v2 #4 reminder, #6 intake). Owned here so the
5
+ // structured-repair fix space and the engine's validation share ONE detector;
6
+ // engine.ts imports these (one-directional).
7
+ // ---------------------------------------------------------------------------
8
+ /** Button labels that promise a state change. A Button carrying one of these
9
+ * is a submit/primary affordance: it must DO something (a mutating host tool
10
+ * bound to real context) or honestly say it can't — a no-op submit is the
11
+ * facade class. Read-only verbs (view/show/open) and dismiss verbs
12
+ * (cancel/clear/close) are deliberately excluded. */
13
+ export const SUBMIT_LABEL = /\b(submit|save|create|add|send|remind|reminder|transfer|pay|confirm|update|delete|remove|apply|schedule|book|post|approve|generate|register|enroll|invite|assign)\b/i;
14
+ export const isMutatingRisk = (risk) => risk === "write" || risk === "destructive";
15
+ export const hasPayload = (payload) => isRecord(payload) ? Object.keys(payload).length > 0 : payload !== undefined && payload !== null;
16
+ const isActionBinding = (value) => isRecord(value) && typeof value.action === "string";
17
+ /** Every {action,payload?} binding reachable in a node's props, with the prop
18
+ * it sits under (actions can nest inside arrays/objects, not just top-level
19
+ * on* attributes). */
20
+ export const actionBindingsInProps = (props) => {
21
+ const found = [];
22
+ const walk = (prop, value) => {
23
+ if (isActionBinding(value)) {
24
+ const record = value;
25
+ found.push({ prop, action: record.action, payload: record.payload });
26
+ return;
27
+ }
28
+ if (Array.isArray(value)) {
29
+ for (const item of value)
30
+ walk(prop, item);
31
+ return;
32
+ }
33
+ if (isRecord(value)) {
34
+ for (const child of Object.values(value))
35
+ walk(prop, child);
36
+ }
37
+ };
38
+ for (const [prop, value] of Object.entries(props))
39
+ walk(prop, value);
40
+ return found;
41
+ };
42
+ export const actionFaults = (tree, tools) => {
43
+ const byName = new Map((tools ?? []).map((tool) => [tool.name, tool]));
44
+ const faults = [];
45
+ for (const node of tree.nodes) {
46
+ const props = node.props;
47
+ const buttonLabel = node.component === "Button" && typeof props?.label === "string" ? props.label : "";
48
+ // W3 law 2 — a Kit <Form> IS a submit affordance whatever its label says.
49
+ const isForm = node.component === "Form";
50
+ const label = isForm
51
+ ? (typeof props?.submitLabel === "string" ? props.submitLabel : "Submit")
52
+ : buttonLabel;
53
+ const submitLike = isForm || (buttonLabel !== "" && SUBMIT_LABEL.test(buttonLabel));
54
+ const bindings = props === undefined ? [] : actionBindingsInProps(props);
55
+ if (submitLike && bindings.length === 0) {
56
+ faults.push({ nodeId: node.id, kind: "dead-submit", label });
57
+ }
58
+ for (const { prop, action, payload } of bindings) {
59
+ if (action.startsWith("fn:"))
60
+ continue;
61
+ const tool = byName.get(action);
62
+ if (tool === undefined) {
63
+ // W3 law 2 — grounding is checkable only when the registry is known.
64
+ if (byName.size > 0)
65
+ faults.push({ nodeId: node.id, kind: "unknown-tool", prop, action, label });
66
+ continue;
67
+ }
68
+ if (isMutatingRisk(tool.risk) && !hasPayload(payload)) {
69
+ faults.push({ nodeId: node.id, kind: "missing-payload", prop, action });
70
+ }
71
+ if (submitLike && tool.risk === "read") {
72
+ faults.push({ nodeId: node.id, kind: "read-only-submit", prop, action, label });
73
+ }
74
+ // W3 law 2 — payload fields must be the tool's real input parameters,
75
+ // and every REQUIRED parameter must be present (a nonempty partial
76
+ // payload would otherwise invoke the tool without e.g. its invoiceId).
77
+ if (hasPayload(payload) && isRecord(payload)) {
78
+ const schema = tool.inputSchema;
79
+ const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : undefined;
80
+ const open = isRecord(schema) && schema.additionalProperties === true;
81
+ if (properties !== undefined && !open) {
82
+ const allowed = Object.keys(properties);
83
+ const unknown = Object.keys(payload).filter((field) => !allowed.includes(field));
84
+ const required = isRecord(schema) && Array.isArray(schema.required)
85
+ ? schema.required.filter((field) => typeof field === "string")
86
+ : [];
87
+ const missing = required.filter((field) => !(field in payload));
88
+ if (unknown.length > 0 || missing.length > 0) {
89
+ faults.push({ nodeId: node.id, kind: "ungrounded-payload", prop, action, unknownFields: unknown, allowedFields: allowed, missingFields: missing });
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return faults;
96
+ };
97
+ // ---------------------------------------------------------------------------
98
+ // Law 1 (W3, v3 spec §The design in five lines) — data-classed props must be
99
+ // bindings. One detector, shared by the engine's validation (message
100
+ // rendering) and the structured-repair fix space.
101
+ // ---------------------------------------------------------------------------
102
+ /** Legacy prewired data props (the Kit carries classes in its specs; the
103
+ * legacy set is classed here until the Wave-5 retirement). */
104
+ const LEGACY_DATA_PROPS = {
105
+ Table: ["rows"],
106
+ Select: ["options"],
107
+ Stat: ["value"],
108
+ };
109
+ const KIT_WIRE_NAMES = new Set(KIT_WIRE_COMPONENT_NAMES);
110
+ /** A HOST catalog prop counts as data-classed when its declared JSON-schema
111
+ * type is a number or an array, its name says business data, and it is not
112
+ * enum-constrained (enums are config by construction). Strings stay free —
113
+ * they are labels/copy far more often than data. */
114
+ const HOST_DATA_PROP = /(cents|amount|balance|total|value|series|rows|items|data|points|slices|history)/i;
115
+ const hostDataProps = (schema) => {
116
+ if (!isRecord(schema) || !isRecord(schema.properties))
117
+ return [];
118
+ return Object.entries(schema.properties).flatMap(([name, propSchema]) => {
119
+ if (!isRecord(propSchema) || Array.isArray(propSchema.enum))
120
+ return [];
121
+ const type = propSchema.type;
122
+ if (type !== "number" && type !== "integer" && type !== "array")
123
+ return [];
124
+ return HOST_DATA_PROP.test(name) ? [name] : [];
125
+ });
126
+ };
127
+ /** The data-classed prop names of one node: Kit prop classes for adopted Kit
128
+ * names, the legacy map for the old prewired set, schema-derived for host
129
+ * catalog components. */
130
+ export const dataClassedProps = (node, catalog) => {
131
+ if (node.source === "host") {
132
+ const entry = catalog.find((component) => component.name === node.component);
133
+ return hostDataProps(entry?.propsJsonSchema);
134
+ }
135
+ if (node.source === "generated")
136
+ return [];
137
+ if (KIT_WIRE_NAMES.has(node.component)) {
138
+ const classes = kitPropClasses(node.component);
139
+ return classes === undefined ? [] : Object.entries(classes).flatMap(([prop, cls]) => cls === "data" ? [prop] : []);
140
+ }
141
+ return LEGACY_DATA_PROPS[node.component] ?? [];
142
+ };
143
+ const isBindingValue = (value) => isPathBinding(value) || isStateBinding(value);
144
+ /** Law 1 — every data-classed prop present on a node must be a `$path` /
145
+ * `$state` binding. A literal there is hand-typed business data. */
146
+ export const literalDataFaults = (tree, catalog) => {
147
+ const faults = [];
148
+ for (const node of tree.nodes) {
149
+ const props = node.props;
150
+ if (props === undefined)
151
+ continue;
152
+ for (const prop of dataClassedProps(node, catalog)) {
153
+ const value = props[prop];
154
+ if (value === undefined || value === null)
155
+ continue;
156
+ if (isBindingValue(value))
157
+ continue;
158
+ faults.push({ nodeId: node.id, component: node.component, prop });
159
+ }
160
+ }
161
+ return faults;
162
+ };
163
+ // ---------------------------------------------------------------------------
164
+ // Structured repair — one strict tool-use call over the closed fix space.
165
+ // ---------------------------------------------------------------------------
166
+ export const NO_VALID_FIX = "__no_valid_fix__";
167
+ const OMIT_FIELD = "__omit__";
168
+ const MAX_PATH_OPTIONS = 60;
169
+ const MAX_PATH_DEPTH = 4;
170
+ /** Every legal binding path into a tool's response shape, query-name-prefixed
171
+ * (JSON-Pointer form, numeric `0` for array elements), bounded so the strict
172
+ * schema stays small. Includes non-leaf paths — binding a whole array/object
173
+ * (rows, slices) is legal. */
174
+ const enumerateShapePaths = (shape, prefix) => {
175
+ const out = [];
176
+ const walk = (current, path, depth) => {
177
+ if (out.length >= MAX_PATH_OPTIONS)
178
+ return;
179
+ out.push(path);
180
+ if (depth >= MAX_PATH_DEPTH)
181
+ return;
182
+ if (current.kind === "object") {
183
+ for (const [field, child] of Object.entries(current.fields)) {
184
+ walk(child, `${path}/${field.replaceAll("~", "~0").replaceAll("/", "~1")}`, depth + 1);
185
+ }
186
+ }
187
+ else if (current.kind === "array") {
188
+ walk(current.items, `${path}/0`, depth + 1);
189
+ }
190
+ };
191
+ walk(shape, prefix, 0);
192
+ return out;
193
+ };
194
+ const kindMatchesSchemaType = (kind, type) => {
195
+ if (kind === "json")
196
+ return true;
197
+ if (type === "array")
198
+ return kind === "array";
199
+ if (type === "number" || type === "integer")
200
+ return kind === "number";
201
+ if (type === "string")
202
+ return kind === "string";
203
+ if (type === "boolean")
204
+ return kind === "boolean";
205
+ if (type === "object")
206
+ return kind === "object";
207
+ return true;
208
+ };
209
+ const hostPropRequirement = (deps, node, prop) => {
210
+ if (node === undefined)
211
+ return { required: false };
212
+ const entry = deps.catalog.find((component) => component.name === node.component);
213
+ const schema = entry?.propsJsonSchema;
214
+ if (!isRecord(schema))
215
+ return { required: false };
216
+ const required = Array.isArray(schema.required) && schema.required.includes(prop);
217
+ const properties = isRecord(schema.properties) ? schema.properties : undefined;
218
+ const propSchema = properties !== undefined && isRecord(properties[prop]) ? properties[prop] : undefined;
219
+ const type = typeof propSchema?.type === "string" ? propSchema.type : undefined;
220
+ return { required, ...(type === undefined ? {} : { type }) };
221
+ };
222
+ /** Candidate payload-context bindings: every bounded path into every KNOWN
223
+ * query shape in the app (the per-row id, the form field values live here). */
224
+ const contextPaths = (tree, deps) => {
225
+ const out = [];
226
+ for (const query of tree.queries ?? []) {
227
+ const shape = deps.toolShapes?.[query.tool];
228
+ if (shape === undefined)
229
+ continue;
230
+ for (const path of enumerateShapePaths(shape, `/${query.name}`)) {
231
+ if (out.length >= MAX_PATH_OPTIONS)
232
+ return out;
233
+ out.push(path);
234
+ }
235
+ }
236
+ return out;
237
+ };
238
+ /** The closed fix space for one failed compile: only failure classes the
239
+ * compiler localizes with an enumerable set of legal fixes. Everything else
240
+ * stays with the free-form fallback loop. */
241
+ const deriveFixes = (compiled, deps) => {
242
+ const fixes = [];
243
+ const nodes = new Map(compiled.tree.nodes.map((node) => [node.id, node]));
244
+ const seenBindings = new Set();
245
+ for (const error of compiled.bindingErrors) {
246
+ const bindingKey = `${error.nodeId} ${error.prop} ${error.path}`;
247
+ if (seenBindings.has(bindingKey))
248
+ continue;
249
+ seenBindings.add(bindingKey);
250
+ const shape = deps.toolShapes?.[error.tool];
251
+ if (shape === undefined)
252
+ continue;
253
+ const node = nodes.get(error.nodeId);
254
+ const requirement = node?.source === "host"
255
+ ? hostPropRequirement(deps, node, error.prop)
256
+ : { required: false };
257
+ let options = enumerateShapePaths(shape, `/${error.query}`);
258
+ // Kind-filter only when the broken binding IS the whole prop value on a
259
+ // host node with a declared prop type and carries no reshape (reshape
260
+ // changes the delivered kind) — prevents a fix from introducing the
261
+ // silent kind-mismatch class.
262
+ const propValue = node?.props?.[error.prop];
263
+ const wholeProp = isPathBinding(propValue) && propValue.$path === error.path
264
+ && !("$reshape" in propValue);
265
+ if (wholeProp && requirement.type !== undefined) {
266
+ const filtered = options.filter((path) => {
267
+ const bound = shapeAtPointer(shape, path.slice(`/${error.query}`.length));
268
+ return bound !== undefined && kindMatchesSchemaType(bound.kind, requirement.type);
269
+ });
270
+ if (filtered.length > 0)
271
+ options = filtered;
272
+ }
273
+ fixes.push({
274
+ kind: "binding",
275
+ nodeId: error.nodeId,
276
+ prop: error.prop,
277
+ path: error.path,
278
+ query: error.query,
279
+ tool: error.tool,
280
+ options,
281
+ requiredProp: requirement.required,
282
+ message: error.message,
283
+ });
284
+ }
285
+ if (deps.tools !== undefined && deps.tools.length > 0) {
286
+ const known = new Set(deps.tools.map((tool) => tool.name));
287
+ const readTools = deps.tools.filter((tool) => tool.risk === "read").map((tool) => tool.name);
288
+ for (const query of compiled.tree.queries ?? []) {
289
+ if (query.tool.startsWith("fn:") || known.has(query.tool))
290
+ continue;
291
+ fixes.push({ kind: "query-tool", query: query.name, tool: query.tool, options: readTools });
292
+ }
293
+ }
294
+ // W3 law 1 — literal business data on data-classed props: the legal fixes
295
+ // are the bounded real field paths (or the disclaimer arm).
296
+ for (const fault of literalDataFaults(compiled.tree, deps.catalog)) {
297
+ fixes.push({
298
+ kind: "literal-data",
299
+ nodeId: fault.nodeId,
300
+ component: fault.component,
301
+ prop: fault.prop,
302
+ options: contextPaths(compiled.tree, deps),
303
+ });
304
+ }
305
+ const toolByName = new Map((deps.tools ?? []).map((tool) => [tool.name, tool]));
306
+ const seenActions = new Set();
307
+ for (const fault of actionFaults(compiled.tree, deps.tools)) {
308
+ if (seenActions.has(fault.nodeId))
309
+ continue;
310
+ seenActions.add(fault.nodeId);
311
+ // W3 law 2 — an invented action tool: choose from the real registry.
312
+ if (fault.kind === "unknown-tool" && fault.action !== undefined && fault.prop !== undefined) {
313
+ fixes.push({
314
+ kind: "action-tool",
315
+ nodeId: fault.nodeId,
316
+ prop: fault.prop,
317
+ action: fault.action,
318
+ options: (deps.tools ?? []).map((tool) => tool.name),
319
+ });
320
+ continue;
321
+ }
322
+ // W3 law 2 — an ungrounded payload is rebuilt whole from the tool's REAL
323
+ // input schema (same strict shape as the missing-payload fill).
324
+ if (fault.kind === "ungrounded-payload" && fault.action !== undefined && fault.prop !== undefined) {
325
+ const schema = toolByName.get(fault.action)?.inputSchema;
326
+ const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : undefined;
327
+ const fields = properties === undefined ? [] : Object.keys(properties);
328
+ const requiredFields = isRecord(schema) && Array.isArray(schema.required)
329
+ ? schema.required.filter((field) => typeof field === "string")
330
+ : [];
331
+ const options = contextPaths(compiled.tree, deps);
332
+ if (fields.length > 0 && options.length > 0) {
333
+ fixes.push({
334
+ kind: "action-payload",
335
+ nodeId: fault.nodeId,
336
+ prop: fault.prop,
337
+ action: fault.action,
338
+ fields,
339
+ requiredFields,
340
+ options,
341
+ replace: true,
342
+ });
343
+ continue;
344
+ }
345
+ fixes.push({ kind: "action-disclaim", nodeId: fault.nodeId, reason: `action "${fault.action}" carries payload fields (${(fault.unknownFields ?? []).join(", ")}) the tool does not declare` });
346
+ continue;
347
+ }
348
+ if (fault.kind === "missing-payload" && fault.action !== undefined && fault.prop !== undefined) {
349
+ const schema = toolByName.get(fault.action)?.inputSchema;
350
+ const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : undefined;
351
+ const fields = properties === undefined ? [] : Object.keys(properties);
352
+ const requiredFields = isRecord(schema) && Array.isArray(schema.required)
353
+ ? schema.required.filter((field) => typeof field === "string")
354
+ : [];
355
+ const options = contextPaths(compiled.tree, deps);
356
+ if (fields.length > 0 && options.length > 0) {
357
+ fixes.push({
358
+ kind: "action-payload",
359
+ nodeId: fault.nodeId,
360
+ prop: fault.prop,
361
+ action: fault.action,
362
+ fields,
363
+ requiredFields,
364
+ options,
365
+ });
366
+ continue;
367
+ }
368
+ }
369
+ const reason = fault.kind === "dead-submit"
370
+ ? `submit button ("${fault.label}") with no action`
371
+ : fault.kind === "read-only-submit"
372
+ ? `submit button ("${fault.label}") wired to read-only tool "${fault.action}"`
373
+ : `mutating tool "${fault.action}" invoked with no payload and no derivable payload skeleton`;
374
+ fixes.push({ kind: "action-disclaim", nodeId: fault.nodeId, reason });
375
+ }
376
+ return fixes;
377
+ };
378
+ const fixKey = (index) => `fix_${index}`;
379
+ /** The flat strict schema (Anthropic strict: additionalProperties:false +
380
+ * required everywhere, no recursion): one property per pending failure, its
381
+ * enum the failure's legal fixes plus the explicit no-valid-fix arm. */
382
+ const buildFixSchema = (fixes) => {
383
+ const properties = {};
384
+ fixes.forEach((fix, index) => {
385
+ if (fix.kind === "binding") {
386
+ properties[fixKey(index)] = {
387
+ type: "string",
388
+ enum: [...fix.options, NO_VALID_FIX],
389
+ description: `Node "${fix.nodeId}" prop "${fix.prop}" binds ${fix.path} — ${fix.message}. Choose the correct field path from query "${fix.query}" (tool ${fix.tool}), or ${NO_VALID_FIX} to drop the binding${fix.requiredProp ? " (the node becomes an honest disclaimer)" : ""}.`,
390
+ };
391
+ }
392
+ else if (fix.kind === "query-tool") {
393
+ properties[fixKey(index)] = {
394
+ type: "string",
395
+ enum: [...fix.options, NO_VALID_FIX],
396
+ description: `Query "${fix.query}" names unknown tool "${fix.tool}". Choose the real host tool it should call, or ${NO_VALID_FIX} to remove the query (dependent content becomes an honest disclaimer).`,
397
+ };
398
+ }
399
+ else if (fix.kind === "literal-data") {
400
+ properties[fixKey(index)] = {
401
+ type: "string",
402
+ enum: [...fix.options, NO_VALID_FIX],
403
+ description: `Node "${fix.nodeId}" prop "${fix.prop}" on <${fix.component}> carries hand-typed LITERAL business data — law 1: data props must bind a tool result. Choose the real field path to bind, or ${NO_VALID_FIX} to replace the node with an honest disclaimer.`,
404
+ };
405
+ }
406
+ else if (fix.kind === "action-tool") {
407
+ properties[fixKey(index)] = {
408
+ type: "string",
409
+ enum: [...fix.options, NO_VALID_FIX],
410
+ description: `Node "${fix.nodeId}" prop "${fix.prop}" invokes unknown tool "${fix.action}" — law 2: actions must name a REAL host tool. Choose the real tool, or ${NO_VALID_FIX} to replace the control with an honest disclaimer.`,
411
+ };
412
+ }
413
+ else if (fix.kind === "action-payload") {
414
+ properties[fixKey(index)] = {
415
+ type: "object",
416
+ additionalProperties: false,
417
+ required: fix.fields,
418
+ properties: Object.fromEntries(fix.fields.map((field) => [field, {
419
+ type: "string",
420
+ enum: [...fix.options, OMIT_FIELD],
421
+ description: `Binding for payload field "${field}" of tool "${fix.action}"${fix.requiredFields.includes(field) ? " (required)" : ""}, or ${OMIT_FIELD} to leave it out.`,
422
+ }])),
423
+ description: `Node "${fix.nodeId}" prop "${fix.prop}" invokes mutating tool "${fix.action}" with no payload. Bind the context it acts on: choose a data path for each payload field. Omitting every field replaces the control with an honest disclaimer.`,
424
+ };
425
+ }
426
+ else {
427
+ properties[fixKey(index)] = {
428
+ type: "string",
429
+ enum: [NO_VALID_FIX],
430
+ description: `Node "${fix.nodeId}": ${fix.reason}. No valid wiring exists on this host; confirm ${NO_VALID_FIX} to replace it with an honest disclaimer.`,
431
+ };
432
+ }
433
+ });
434
+ return {
435
+ type: "object",
436
+ additionalProperties: false,
437
+ required: fixes.map((_, index) => fixKey(index)),
438
+ properties,
439
+ };
440
+ };
441
+ /** One strict tool-use call choosing a fix per failure. Returns undefined on
442
+ * any model/transport failure — the caller falls back to the free-form loop. */
443
+ const strictToolCall = async (deps, toolName, description, inputSchema, system, prompt) => {
444
+ try {
445
+ const { generateText, jsonSchema } = await import("ai");
446
+ const result = await generateText({
447
+ model: deps.model,
448
+ system,
449
+ prompt,
450
+ tools: {
451
+ [toolName]: {
452
+ description,
453
+ inputSchema: jsonSchema(inputSchema),
454
+ // Anthropic strict tool use (GA): the arguments MUST validate
455
+ // against the schema — enum values become unsamplable otherwise.
456
+ strict: true,
457
+ },
458
+ },
459
+ toolChoice: { type: "tool", toolName },
460
+ temperature: 0,
461
+ maxRetries: 0,
462
+ });
463
+ const call = result.toolCalls.find((candidate) => candidate.toolName === toolName);
464
+ if (call === undefined || !isRecord(call.input))
465
+ return undefined;
466
+ return call.input;
467
+ }
468
+ catch {
469
+ return undefined;
470
+ }
471
+ };
472
+ const DISCLAIMER_TEXT = "This part of the request isn't available on this host.";
473
+ const disclaimNode = (tree, nodeId) => {
474
+ const node = tree.nodes.find((candidate) => candidate.id === nodeId);
475
+ if (node === undefined)
476
+ return;
477
+ node.component = "Text";
478
+ node.source = "prewired";
479
+ node.props = { text: DISCLAIMER_TEXT };
480
+ delete node.children;
481
+ };
482
+ /** Deletes the value at the walk position where `predicate` matches, deep in
483
+ * a props tree (object keys deleted, array elements spliced). Returns how
484
+ * many values were removed. */
485
+ const removeDeep = (container, predicate) => {
486
+ let removed = 0;
487
+ if (Array.isArray(container)) {
488
+ for (let index = container.length - 1; index >= 0; index -= 1) {
489
+ const value = container[index];
490
+ if (predicate(value)) {
491
+ container.splice(index, 1);
492
+ removed += 1;
493
+ }
494
+ else if (isRecord(value) || Array.isArray(value)) {
495
+ removed += removeDeep(value, predicate);
496
+ }
497
+ }
498
+ return removed;
499
+ }
500
+ for (const [key, value] of Object.entries(container)) {
501
+ if (predicate(value)) {
502
+ delete container[key];
503
+ removed += 1;
504
+ }
505
+ else if (isRecord(value) || Array.isArray(value)) {
506
+ removed += removeDeep(value, predicate);
507
+ }
508
+ }
509
+ return removed;
510
+ };
511
+ const replaceBindingPath = (container, fromPath, toPath) => {
512
+ const values = Array.isArray(container) ? container : Object.values(container);
513
+ for (const value of values) {
514
+ if (isPathBinding(value) && value.$path === fromPath) {
515
+ value.$path = toPath;
516
+ }
517
+ else if (isRecord(value) || Array.isArray(value)) {
518
+ replaceBindingPath(value, fromPath, toPath);
519
+ }
520
+ }
521
+ };
522
+ const bindingReferencesQuery = (value, queryName) => isPathBinding(value) && (value.$path === `/${queryName}` || value.$path.startsWith(`/${queryName}/`));
523
+ const pruneUnreachable = (tree) => {
524
+ const nodes = new Map(tree.nodes.map((node) => [node.id, node]));
525
+ const reachable = new Set();
526
+ const pending = [tree.root];
527
+ while (pending.length > 0) {
528
+ const id = pending.pop();
529
+ if (id === undefined || reachable.has(id))
530
+ continue;
531
+ reachable.add(id);
532
+ pending.push(...(nodes.get(id)?.children ?? []));
533
+ }
534
+ tree.nodes = tree.nodes.filter((node) => reachable.has(node.id));
535
+ };
536
+ /** Applies chosen fixes to a deep-cloned tree. Every splice is deterministic:
537
+ * the chosen value was already validated against the fix's enum. */
538
+ const spliceFixes = (compiled, fixes, chosen) => {
539
+ const tree = structuredClone(compiled.tree);
540
+ const nodes = new Map(tree.nodes.map((node) => [node.id, node]));
541
+ fixes.forEach((fix, index) => {
542
+ const value = chosen[fixKey(index)];
543
+ if (fix.kind === "binding") {
544
+ const node = nodes.get(fix.nodeId);
545
+ if (node?.props === undefined)
546
+ return;
547
+ const pick = typeof value === "string" && fix.options.includes(value) ? value : NO_VALID_FIX;
548
+ if (pick === NO_VALID_FIX) {
549
+ removeDeep(node.props, (candidate) => isPathBinding(candidate) && candidate.$path === fix.path);
550
+ if (node.props[fix.prop] === undefined && fix.requiredProp)
551
+ disclaimNode(tree, fix.nodeId);
552
+ }
553
+ else {
554
+ replaceBindingPath(node.props, fix.path, pick);
555
+ }
556
+ }
557
+ else if (fix.kind === "query-tool") {
558
+ const pick = typeof value === "string" && fix.options.includes(value) ? value : NO_VALID_FIX;
559
+ const query = (tree.queries ?? []).find((candidate) => candidate.name === fix.query);
560
+ if (query === undefined)
561
+ return;
562
+ if (pick === NO_VALID_FIX) {
563
+ tree.queries = (tree.queries ?? []).filter((candidate) => candidate.name !== fix.query);
564
+ for (const node of tree.nodes) {
565
+ if (node.props === undefined)
566
+ continue;
567
+ const removed = removeDeep(node.props, (candidate) => bindingReferencesQuery(candidate, fix.query));
568
+ if (removed > 0) {
569
+ const stillRenders = Object.keys(node.props).length > 0;
570
+ if (!stillRenders || node.source === "host")
571
+ disclaimNode(tree, node.id);
572
+ }
573
+ }
574
+ }
575
+ else {
576
+ query.tool = pick;
577
+ }
578
+ }
579
+ else if (fix.kind === "literal-data") {
580
+ const node = nodes.get(fix.nodeId);
581
+ if (node?.props === undefined)
582
+ return;
583
+ const pick = typeof value === "string" && fix.options.includes(value) ? value : NO_VALID_FIX;
584
+ if (pick === NO_VALID_FIX) {
585
+ disclaimNode(tree, fix.nodeId);
586
+ }
587
+ else {
588
+ node.props[fix.prop] = { $path: pick };
589
+ }
590
+ }
591
+ else if (fix.kind === "action-tool") {
592
+ const node = nodes.get(fix.nodeId);
593
+ if (node?.props === undefined)
594
+ return;
595
+ const pick = typeof value === "string" && fix.options.includes(value) ? value : NO_VALID_FIX;
596
+ if (pick === NO_VALID_FIX) {
597
+ disclaimNode(tree, fix.nodeId);
598
+ }
599
+ else {
600
+ const rename = (container) => {
601
+ const values = Array.isArray(container) ? container : Object.values(container);
602
+ for (const candidate of values) {
603
+ if (isRecord(candidate) && candidate.action === fix.action) {
604
+ candidate.action = pick;
605
+ }
606
+ else if (isRecord(candidate) || Array.isArray(candidate)) {
607
+ rename(candidate);
608
+ }
609
+ }
610
+ };
611
+ rename(node.props);
612
+ }
613
+ }
614
+ else if (fix.kind === "action-payload") {
615
+ const node = nodes.get(fix.nodeId);
616
+ if (node?.props === undefined)
617
+ return;
618
+ const picks = isRecord(value) ? value : {};
619
+ const payload = {};
620
+ for (const field of fix.fields) {
621
+ const pick = picks[field];
622
+ if (typeof pick === "string" && fix.options.includes(pick)) {
623
+ payload[field] = { $path: pick };
624
+ }
625
+ }
626
+ if (Object.keys(payload).length === 0) {
627
+ disclaimNode(tree, fix.nodeId);
628
+ }
629
+ else {
630
+ const apply = (container) => {
631
+ const values = Array.isArray(container) ? container : Object.values(container);
632
+ for (const candidate of values) {
633
+ if (isRecord(candidate) && candidate.action === fix.action
634
+ && (fix.replace === true || !hasPayload(candidate.payload))) {
635
+ candidate.payload = structuredClone(payload);
636
+ }
637
+ else if (isRecord(candidate) || Array.isArray(candidate)) {
638
+ apply(candidate);
639
+ }
640
+ }
641
+ };
642
+ apply(node.props);
643
+ }
644
+ }
645
+ else {
646
+ disclaimNode(tree, fix.nodeId);
647
+ }
648
+ });
649
+ pruneUnreachable(tree);
650
+ return {
651
+ tree,
652
+ components: structuredClone(compiled.components),
653
+ ...(compiled.name === undefined ? {} : { name: compiled.name }),
654
+ };
655
+ };
656
+ const recompile = (base, context) => compileWireV2(printWireV2(base, { includeIds: false }), {
657
+ hostComponents: [...context.hostComponents],
658
+ ...(context.deps.toolShapes === undefined ? {} : { toolShapes: context.deps.toolShapes }),
659
+ });
660
+ /** v3 pipeline step 5 — structured repair. Compile errors with a closed fix
661
+ * space are fixed by ONE strict tool-use call per round (max `maxRounds`),
662
+ * spliced deterministically into the canonical tree, re-printed, re-compiled
663
+ * and re-validated. Anything outside the closed space (or a failed call)
664
+ * returns with `document` unset and the caller falls back to today's
665
+ * free-form regeneration loop. */
666
+ export const structuredRepair = async (compiled, userRequest, context, maxRounds = 2) => {
667
+ const repairStart = Date.now();
668
+ const finish = (result) => {
669
+ if (result.rounds > 0) {
670
+ context.deps.onPipeline?.({
671
+ stage: "repair",
672
+ rounds: result.rounds,
673
+ repaired: result.document !== undefined,
674
+ noValidFix: result.noValidFixCount,
675
+ ms: Date.now() - repairStart,
676
+ });
677
+ }
678
+ return result;
679
+ };
680
+ const { deps } = context;
681
+ let current = compiled;
682
+ let issues = [];
683
+ let noValidFixCount = 0;
684
+ for (let round = 0; round < maxRounds; round += 1) {
685
+ const fixes = deriveFixes(current, deps);
686
+ if (fixes.length === 0)
687
+ return finish({ rounds: round, issues, noValidFixCount });
688
+ const schema = buildFixSchema(fixes);
689
+ const wire = printWireV2({ tree: current.tree, components: current.components, ...(current.name === undefined ? {} : { name: current.name }) }, { includeIds: true });
690
+ const chosen = await strictToolCall(deps, "apply_fixes", "Apply one fix per pending compile failure. Every fix is chosen from that failure's closed set of legal values.", schema, "You repair Vendo apps. For each pending failure, choose the fix that best serves the user's request. Choose the no-valid-fix arm only when no listed value can honestly satisfy the intent.", `USER_REQUEST: ${userRequest}\nCURRENT_APP (wire markup; id attributes locate the failing nodes):\n${wire}`);
691
+ deps.onTiming?.({ lane: "repair", phase: "complete", atMs: Date.now() - context.startedAt, thinking: false });
692
+ if (chosen === undefined)
693
+ return finish({ rounds: round + 1, issues, noValidFixCount });
694
+ fixes.forEach((fix, index) => {
695
+ const value = chosen[fixKey(index)];
696
+ if (fix.kind === "action-disclaim") {
697
+ noValidFixCount += 1;
698
+ return;
699
+ }
700
+ if (fix.kind === "action-payload") {
701
+ const picks = isRecord(value) ? value : {};
702
+ if (!fix.fields.some((field) => typeof picks[field] === "string" && fix.options.includes(picks[field])))
703
+ noValidFixCount += 1;
704
+ return;
705
+ }
706
+ if (typeof value !== "string" || !fix.options.includes(value))
707
+ noValidFixCount += 1;
708
+ });
709
+ const recompiled = recompile(spliceFixes(current, fixes, chosen), context);
710
+ const validated = await context.validate(recompiled);
711
+ if (validated.document !== undefined) {
712
+ return finish({ document: validated.document, rounds: round + 1, issues, noValidFixCount });
713
+ }
714
+ issues = [...new Set([...issues, ...validated.issues])];
715
+ current = recompiled;
716
+ }
717
+ return finish({ rounds: maxRounds, issues, noValidFixCount });
718
+ };
719
+ // ---------------------------------------------------------------------------
720
+ // End pass — one no-think read-through emitting 0–4 validated proofread
721
+ // patches; priority one is label-vs-binding truth (v4: the M6/M14/F5 class).
722
+ // ---------------------------------------------------------------------------
723
+ const END_PASS_CONTRACT = `You are the Vendo end-pass editor: one quick read-through of a finished app against the user's ask. Return ONLY one vendo-genui/v2 <Edit>...</Edit> patch document. No prose, no markdown, no JSON.
724
+ PROOFREAD ONLY, AT MOST 4 ops. Priority one — labels must tell the truth about their bindings: a stat, badge, title, or caption claiming "total", "all accounts", "this month", or a specific figure must match what its bound data actually is. When it doesn't, RELABEL to describe the real data (never invent numbers, never rebind) — e.g. a card bound to one checking account labeled "Total balance" becomes <Set id="..." label="Checking balance"/>. Prefer emitting a fix when a label overstates its binding; an unfixed lying label is worse than an extra op. Then: deduplicate repeated titles/stats, retitle so the app answers the ask, drop a redundant node. Never restructure, never add features, never touch queries or islands.
725
+ Ops (patch the CURRENT_APP wire against its id="..." anchors):
726
+ - <Set id="node-id" attr=.../> merges attributes into the node's props. Set/Unset may touch ONLY copy props (label, title, text, caption, description, subtitle, heading, placeholder, helper, emptyLabel, badgeLabel) with plain string values — touching any other attribute, a binding, or a non-string value drops your whole patch.
727
+ - <Unset id="node-id" propName/> removes the named props.
728
+ - <Remove id="node-id"/> removes a redundant node and its subtree.
729
+ - <SetName name="..."/> renames the app.
730
+ If nothing needs polish, emit exactly <Edit></Edit>.`;
731
+ /** v4 review hardening — the contract's "relabel, never rebind, never
732
+ * restructure" was prompt-only: any compiling <Set> (including one that
733
+ * overwrites a live value or binding with an invented figure) survived. This
734
+ * makes the contract structural. A surviving patch may only rename the app,
735
+ * remove nodes, and set/unset STRING copy props; anything else — new nodes,
736
+ * component/island changes, query or data changes, a binding on either side
737
+ * of a change, a non-copy prop, a non-string value — drops the whole patch
738
+ * (the original document ships, per the pass's drop-silently invariant). */
739
+ const END_PASS_COPY_PROPS = new Set([
740
+ "label", "title", "text", "caption", "description", "subtitle",
741
+ "heading", "placeholder", "helper", "emptyLabel", "badgeLabel",
742
+ ]);
743
+ /** True when `after` only removes entries from `before` (order preserved) —
744
+ * the shape a <Remove> leaves behind; anything else is a restructure. */
745
+ const onlyRemovals = (before, after) => {
746
+ let cursor = 0;
747
+ for (const id of before)
748
+ if (after[cursor] === id)
749
+ cursor += 1;
750
+ return cursor === after.length;
751
+ };
752
+ const sameJson = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
753
+ export const endPassViolations = (base, patched, baseComponents, patchedComponents) => {
754
+ const violations = [];
755
+ const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
756
+ for (const node of patched.nodes) {
757
+ const before = baseNodes.get(node.id);
758
+ if (before === undefined) {
759
+ violations.push(`adds node ${node.id}`);
760
+ continue;
761
+ }
762
+ if (node.component !== before.component)
763
+ violations.push(`changes component on ${node.id}`);
764
+ if (!onlyRemovals(before.children ?? [], node.children ?? [])) {
765
+ violations.push(`restructures children of ${node.id}`);
766
+ }
767
+ const beforeProps = (before.props ?? {});
768
+ const afterProps = (node.props ?? {});
769
+ for (const key of new Set([...Object.keys(beforeProps), ...Object.keys(afterProps)])) {
770
+ const beforeValue = beforeProps[key];
771
+ const afterValue = afterProps[key];
772
+ if (sameJson(beforeValue, afterValue))
773
+ continue;
774
+ if (!END_PASS_COPY_PROPS.has(key)) {
775
+ violations.push(`touches non-copy prop "${key}" on ${node.id}`);
776
+ continue;
777
+ }
778
+ if (isPathBinding(beforeValue) || isStateBinding(beforeValue)) {
779
+ violations.push(`unbinds "${key}" on ${node.id}`);
780
+ }
781
+ if (afterValue !== undefined && typeof afterValue !== "string") {
782
+ violations.push(`sets non-string copy "${key}" on ${node.id}`);
783
+ }
784
+ }
785
+ }
786
+ if (!sameJson(base.queries ?? [], patched.queries ?? []))
787
+ violations.push("touches queries");
788
+ if (!sameJson(base.data ?? {}, patched.data ?? {}))
789
+ violations.push("touches data");
790
+ if (!sameJson(baseComponents, patchedComponents))
791
+ violations.push("touches islands");
792
+ return violations;
793
+ };
794
+ /** Same fence tolerance as engine.ts's extractWire, for <Edit> documents
795
+ * (shared by the edit dialect and the end pass). */
796
+ export const extractEdit = (text) => {
797
+ const start = text.indexOf("<Edit");
798
+ if (start === -1)
799
+ return text;
800
+ const closeTag = "</Edit>";
801
+ const close = text.lastIndexOf(closeTag);
802
+ return close === -1 ? text.slice(start) : text.slice(start, close + closeTag.length);
803
+ };
804
+ /** v3 pipeline step 6, sharpened in v4 — the end pass. Runs only under the
805
+ * `endPass` flag (default flips when the v4 A/B earns it); a patch survives
806
+ * only if it compiles clean, applies at most 4 ops, and the patched app
807
+ * re-validates — otherwise the original document ships untouched.
808
+ * Structurally cannot break the app. */
809
+ export const endPass = async (document, userRequest, context) => {
810
+ if (context.deps.pipeline?.endPass !== true)
811
+ return document;
812
+ const { deps } = context;
813
+ const endPassStart = Date.now();
814
+ const finish = (polished) => {
815
+ deps.onPipeline?.({ stage: "end-pass", applied: polished !== document, ms: Date.now() - endPassStart });
816
+ return polished;
817
+ };
818
+ try {
819
+ const base = {
820
+ tree: structuredClone(document.tree),
821
+ components: { ...(document.components ?? {}) },
822
+ name: document.name,
823
+ };
824
+ const wire = printWireV2(base, { includeIds: true });
825
+ // The no-think switch: the paint model is the configured thinking-disabled
826
+ // instance; the read-through never needs reasoning depth.
827
+ const model = deps.paint?.model ?? deps.model;
828
+ const { generateText } = await import("ai");
829
+ const result = await generateText({
830
+ model,
831
+ system: END_PASS_CONTRACT,
832
+ prompt: `USER_ASK: ${userRequest}\nCURRENT_APP (wire markup; id attributes are your anchors):\n${wire}`,
833
+ temperature: 0,
834
+ maxRetries: 0,
835
+ });
836
+ deps.onTiming?.({ lane: "end-pass", phase: "complete", atMs: Date.now() - context.startedAt, thinking: false });
837
+ const patched = compileWirePatchV2(extractEdit(result.text), base, {
838
+ hostComponents: [...context.hostComponents],
839
+ ...(deps.toolShapes === undefined ? {} : { toolShapes: deps.toolShapes }),
840
+ });
841
+ if (!patched.complete || patched.issues.length > 0 || patched.bindingErrors.length > 0)
842
+ return finish(document);
843
+ if (patched.appliedOps === 0 || patched.appliedOps > 4 || patched.extensionOps.length > 0)
844
+ return finish(document);
845
+ // Structural proofread guard: the patch may only relabel, remove, and
846
+ // rename — a Set that rebinds or rewrites data drops the whole patch.
847
+ if (endPassViolations(base.tree, patched.tree, base.components, patched.components).length > 0) {
848
+ return finish(document);
849
+ }
850
+ const validated = await context.validate(recompile({
851
+ tree: patched.tree,
852
+ components: patched.components,
853
+ ...(patched.name === undefined ? {} : { name: patched.name }),
854
+ }, context));
855
+ return finish(validated.document ?? document);
856
+ }
857
+ catch {
858
+ return finish(document);
859
+ }
860
+ };
861
+ const MAX_SECTIONS = 4;
862
+ const outlineSchema = (toolNames) => ({
863
+ type: "object",
864
+ additionalProperties: false,
865
+ required: ["appName", "sharedFacts", "sections"],
866
+ properties: {
867
+ appName: { type: "string", description: "The app's display name." },
868
+ sharedFacts: {
869
+ type: "string",
870
+ description: "Shared data/state facts every section must agree on (which queries feed what, shared filters, units). Empty string when none.",
871
+ },
872
+ sections: {
873
+ type: "array",
874
+ description: `2 to ${MAX_SECTIONS} independent screen regions, top to bottom. Merge deeply coupled pieces (a picker filtering another view) into ONE section — a section is generated in isolation.`,
875
+ items: {
876
+ type: "object",
877
+ additionalProperties: false,
878
+ required: ["id", "brief", "tools", "coupledWithPrevious"],
879
+ properties: {
880
+ id: { type: "string", description: "Short lowercase identifier, e.g. s1_summary." },
881
+ brief: { type: "string", description: "What this region shows and does." },
882
+ tools: {
883
+ type: "array",
884
+ description: "The host tools feeding this section.",
885
+ items: { type: "string", enum: toolNames },
886
+ },
887
+ coupledWithPrevious: {
888
+ type: "boolean",
889
+ description: "True when this region shares live state with the previous one and must be generated together with it.",
890
+ },
891
+ },
892
+ },
893
+ },
894
+ },
895
+ });
896
+ const planOutline = async (userRequest, context) => {
897
+ const { deps } = context;
898
+ const toolNames = (deps.tools ?? []).map((tool) => tool.name);
899
+ if (toolNames.length === 0)
900
+ return undefined;
901
+ const input = await strictToolCall(deps, "plan_outline", "Plan the app as independent screen regions before parallel generation.", outlineSchema(toolNames), "You plan Vendo app generations. Split the requested app into independent regions that can be generated in parallel; keep coupled pieces in one region.", `USER_REQUEST: ${userRequest}\nHOST TOOLS:\n${(deps.tools ?? []).map(({ name, description, risk }) => `- ${name} [${risk}]: ${description}`).join("\n")}`);
902
+ deps.onTiming?.({ lane: "outline", phase: "complete", atMs: Date.now() - context.startedAt, thinking: false });
903
+ if (input === undefined || typeof input.appName !== "string" || !Array.isArray(input.sections))
904
+ return undefined;
905
+ const known = new Set(toolNames);
906
+ const sections = [];
907
+ for (const raw of input.sections) {
908
+ if (!isRecord(raw) || typeof raw.id !== "string" || typeof raw.brief !== "string")
909
+ return undefined;
910
+ const tools = Array.isArray(raw.tools) ? raw.tools.filter((tool) => typeof tool === "string" && known.has(tool)) : [];
911
+ const id = raw.id.toLowerCase().replaceAll(/[^a-z0-9_]/g, "_").replaceAll(/^_+|_+$/g, "") || `s${sections.length + 1}`;
912
+ if (raw.coupledWithPrevious === true && sections.length > 0) {
913
+ const previous = sections[sections.length - 1];
914
+ previous.brief = `${previous.brief}\nAND, coupled in the same region: ${raw.brief}`;
915
+ previous.tools = [...new Set([...previous.tools, ...tools])];
916
+ }
917
+ else {
918
+ sections.push({ id, brief: raw.brief, tools });
919
+ }
920
+ }
921
+ if (sections.length < 2 || sections.length > MAX_SECTIONS)
922
+ return undefined;
923
+ const ids = new Set(sections.map((section) => section.id));
924
+ if (ids.size !== sections.length)
925
+ return undefined;
926
+ return {
927
+ appName: input.appName,
928
+ sharedFacts: typeof input.sharedFacts === "string" ? input.sharedFacts : "",
929
+ sections,
930
+ };
931
+ };
932
+ /** The section's inner markup: everything between the App open tag and its
933
+ * close (islands included — they are top-level siblings inside App). */
934
+ const innerAppMarkup = (text) => {
935
+ const start = text.indexOf("<App");
936
+ if (start === -1)
937
+ return undefined;
938
+ const open = text.indexOf(">", start);
939
+ if (open === -1)
940
+ return undefined;
941
+ const close = text.lastIndexOf("</App>");
942
+ const inner = close === -1 ? text.slice(open + 1) : text.slice(open + 1, close);
943
+ return inner.trim() === "" ? undefined : inner;
944
+ };
945
+ /** v3 pipeline steps 1+3 — outline then N parallel per-section writers whose
946
+ * outputs assemble (in outline order) into one wire document that goes
947
+ * through the normal whole-app validation. Any planning/assembly failure
948
+ * returns fallback info and the engine's single-stream lane takes over —
949
+ * never blocks. */
950
+ export const regionParallelCreate = async (context, hooks) => {
951
+ const parallelStart = Date.now();
952
+ const finish = (result) => {
953
+ context.deps.onPipeline?.({
954
+ stage: "region-parallel",
955
+ ...(result.fallback === undefined ? {} : { fallback: result.fallback }),
956
+ ...(result.sectionsPlanned === undefined ? {} : { sectionsPlanned: result.sectionsPlanned }),
957
+ ...(result.sectionsLanded === undefined ? {} : { sectionsLanded: result.sectionsLanded }),
958
+ ms: Date.now() - parallelStart,
959
+ });
960
+ return result;
961
+ };
962
+ const outline = await planOutline(hooks.userRequest, context);
963
+ if (outline === undefined)
964
+ return finish({ fallback: "no-outline" });
965
+ const appName = outline.appName.replaceAll('"', "'");
966
+ const sectionPrompt = (section, index) => [
967
+ `TASK: CREATE_APP\nUSER_REQUEST: ${hooks.userRequest}`,
968
+ `OUTLINE_SECTION ${section.id} (${index + 1} of ${outline.sections.length}): ${section.brief}`,
969
+ `- Emit ONE complete <App name="${appName}"> containing ONLY this section's queries and markup — the other sections are generated separately and composed around yours in order.`,
970
+ section.tools.length === 0
971
+ ? "- This section uses NO host tools; render honest static/empty-state content only."
972
+ : `- Use ONLY these host tools: ${section.tools.join(", ")}.`,
973
+ `- Prefix every <Query id> with "${section.id}_" so query names never collide with other sections.`,
974
+ `- Do NOT repeat content that belongs to other sections.`,
975
+ outline.sharedFacts.trim() === "" ? "" : `SHARED FACTS (every section must agree): ${outline.sharedFacts}`,
976
+ ].filter((line) => line !== "").join("\n");
977
+ const landed = outline.sections.map(() => undefined);
978
+ const assemble = () => `<App name="${appName}">${landed.filter((part) => part !== undefined).join("\n")}</App>`;
979
+ await Promise.all(outline.sections.map(async (section, index) => {
980
+ const text = await hooks.generateSection(sectionPrompt(section, index));
981
+ if (text === undefined)
982
+ return;
983
+ const inner = innerAppMarkup(text);
984
+ if (inner === undefined)
985
+ return;
986
+ landed[index] = inner;
987
+ // (streamWire already emits the lane:"section" complete timing event.)
988
+ hooks.emitPartial?.(assemble());
989
+ }));
990
+ const landedCount = landed.filter((part) => part !== undefined).length;
991
+ // EVERY planned section must land — assembling a subset would silently ship
992
+ // an app missing a region the user asked for (Devin review, PR #417). The
993
+ // single-stream fallback still produces the complete app, so falling back
994
+ // here never blocks and never drops content.
995
+ if (landedCount !== outline.sections.length) {
996
+ return finish({ fallback: "sections-failed", sectionsPlanned: outline.sections.length, sectionsLanded: landedCount });
997
+ }
998
+ const compiled = compileWireV2(assemble(), {
999
+ hostComponents: [...context.hostComponents],
1000
+ ...(context.deps.toolShapes === undefined ? {} : { toolShapes: context.deps.toolShapes }),
1001
+ });
1002
+ const validated = await context.validate(compiled);
1003
+ if (validated.document !== undefined) {
1004
+ return finish({ document: validated.document, sectionsPlanned: outline.sections.length, sectionsLanded: landedCount });
1005
+ }
1006
+ if (context.deps.pipeline?.structuredRepair !== false) {
1007
+ const repaired = await structuredRepair(compiled, hooks.userRequest, context);
1008
+ if (repaired.document !== undefined) {
1009
+ return finish({ document: repaired.document, sectionsPlanned: outline.sections.length, sectionsLanded: landedCount });
1010
+ }
1011
+ }
1012
+ return finish({ fallback: "assembly-invalid", sectionsPlanned: outline.sections.length, sectionsLanded: landedCount });
1013
+ };
1014
+ //# sourceMappingURL=pipeline.js.map