@agent-native/core 0.80.9 → 0.80.10

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 (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +28 -2
  5. package/corpus/core/src/cli/skills.ts +45 -24
  6. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  7. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  8. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  9. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  10. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  11. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  12. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  14. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  15. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  16. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  17. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  18. package/corpus/templates/design/actions/create-component.ts +447 -0
  19. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  20. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  21. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  22. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  23. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  24. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  25. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  26. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  27. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  28. package/corpus/templates/design/actions/index-components.ts +258 -0
  29. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  30. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  31. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  32. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  33. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  34. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  35. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  36. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  37. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  38. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  39. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  40. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  41. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  42. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  43. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
  44. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  45. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  46. package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
  47. package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
  48. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  49. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
  51. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  52. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  53. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  54. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  55. package/corpus/templates/design/app/components/design/index.ts +16 -0
  56. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  57. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  58. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  59. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  60. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  61. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  62. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
  64. package/corpus/templates/design/app/i18n-data.ts +269 -0
  65. package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
  66. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  67. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  68. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  69. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  70. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  71. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  72. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  73. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  74. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  75. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  76. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  77. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  78. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  79. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  80. package/corpus/templates/design/e2e/helpers.ts +10 -4
  81. package/corpus/templates/design/server/db/schema.ts +123 -0
  82. package/corpus/templates/design/server/plugins/db.ts +90 -0
  83. package/corpus/templates/design/shared/builder-app.ts +297 -0
  84. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  85. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  86. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  87. package/corpus/templates/design/shared/component-model.ts +239 -0
  88. package/corpus/templates/design/shared/design-review.ts +275 -0
  89. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  90. package/corpus/templates/design/shared/design-state.ts +112 -0
  91. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  92. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  93. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  94. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  95. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  96. package/corpus/templates/design/shared/source-mode.ts +245 -0
  97. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  98. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  99. package/dist/cli/design-connect.d.ts.map +1 -1
  100. package/dist/cli/design-connect.js +28 -2
  101. package/dist/cli/design-connect.js.map +1 -1
  102. package/dist/cli/skills.d.ts.map +1 -1
  103. package/dist/cli/skills.js +37 -21
  104. package/dist/cli/skills.js.map +1 -1
  105. package/dist/collab/routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  107. package/dist/notifications/routes.d.ts +2 -2
  108. package/dist/observability/routes.d.ts +7 -7
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/server/transcribe-voice.d.ts +1 -1
  111. package/package.json +1 -1
@@ -0,0 +1,362 @@
1
+ /**
2
+ * get-design-branch-diff — read action returning a code + visual diff for a
3
+ * design branch (§6.6 of DESIGN-STUDIO-PLAN.md).
4
+ *
5
+ * Two diff axes:
6
+ *
7
+ * 1. **Visual diff** — compares two `design_versions` snapshots (before and
8
+ * after branching). Always available when a `preSnapshotVersionId` is
9
+ * stored on the branch entry. Reuses the same lightweight snapshot-diff
10
+ * approach from `get-design-review.ts` (file presence + content hash).
11
+ *
12
+ * 2. **Code/branch diff** — available only when the source is `fusion` and
13
+ * `diffPatch` capability is advertised. For now this surfaces the branch
14
+ * metadata (name, url, status) and a clear `notAvailable` note when the
15
+ * bridge write path isn't yet hardened. Per the plan, real file-level code
16
+ * diffs land with bridge write hardening (phase 5); the action is structured
17
+ * to receive that data transparently once the bridge proves it.
18
+ *
19
+ * When neither diff axis is available (e.g. inline design without branches),
20
+ * returns `ctaRequired: true` with a "Make it real" CTA.
21
+ */
22
+
23
+ import { defineAction } from "@agent-native/core";
24
+ import { resolveAccess } from "@agent-native/core/sharing";
25
+ import { desc, eq } from "drizzle-orm";
26
+ import { z } from "zod";
27
+
28
+ import { getDb, schema } from "../server/db/index.js";
29
+ import "../server/db/index.js"; // ensure registerShareableResource runs
30
+ import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
31
+ import type {
32
+ VisualDiffEntry,
33
+ VisualDiffChangeKind,
34
+ } from "../shared/design-review.js";
35
+ import { hasCapability } from "../shared/design-source-capabilities.js";
36
+ import { normalizeDesignSourceType } from "../shared/source-mode.js";
37
+
38
+ // ─── Types ────────────────────────────────────────────────────────────────────
39
+
40
+ interface StoredBranchEntry {
41
+ branchName?: string;
42
+ projectId?: string;
43
+ url?: string;
44
+ status?: string;
45
+ purpose?: string | null;
46
+ preSnapshotVersionId?: string | null;
47
+ createdAt?: string;
48
+ }
49
+
50
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
51
+
52
+ function parseDesignData(raw: unknown): Record<string, unknown> {
53
+ if (typeof raw !== "string") return {};
54
+ try {
55
+ const parsed: unknown = JSON.parse(raw);
56
+ if (
57
+ parsed !== null &&
58
+ typeof parsed === "object" &&
59
+ !Array.isArray(parsed)
60
+ ) {
61
+ return parsed as Record<string, unknown>;
62
+ }
63
+ } catch {
64
+ // Stale JSON — return empty.
65
+ }
66
+ return {};
67
+ }
68
+
69
+ function parseBranches(
70
+ designData: Record<string, unknown>,
71
+ ): StoredBranchEntry[] {
72
+ const raw = designData["branches"];
73
+ if (!Array.isArray(raw)) return [];
74
+ return raw.filter(
75
+ (b): b is StoredBranchEntry =>
76
+ b !== null && typeof b === "object" && !Array.isArray(b),
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Parse a design_versions snapshot JSON into a flat map of
82
+ * `{ filename: { bytes, contentHash } }` for structural comparison.
83
+ * Mirrors the approach in `get-design-review.ts`.
84
+ */
85
+ function parseSnapshotFiles(
86
+ snapshotRaw: string,
87
+ ): Record<string, { bytes: number; content: string | undefined }> {
88
+ try {
89
+ const obj = JSON.parse(snapshotRaw) as unknown;
90
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {};
91
+ const record = obj as Record<string, unknown>;
92
+ const files = record["files"];
93
+ if (!Array.isArray(files)) return {};
94
+
95
+ const out: Record<string, { bytes: number; content: string | undefined }> =
96
+ {};
97
+ for (const file of files) {
98
+ if (!file || typeof file !== "object") continue;
99
+ const f = file as Record<string, unknown>;
100
+ const name = typeof f["filename"] === "string" ? f["filename"] : "?";
101
+ const content =
102
+ typeof f["content"] === "string" ? f["content"] : undefined;
103
+ out[name] = {
104
+ content,
105
+ bytes: typeof content === "string" ? content.length : 0,
106
+ };
107
+ }
108
+ return out;
109
+ } catch {
110
+ return {};
111
+ }
112
+ }
113
+
114
+ /** Produce a visual diff entry list from two snapshot file maps. */
115
+ function diffSnapshotFiles(
116
+ baseFiles: Record<string, { bytes: number; content: string | undefined }>,
117
+ compareFiles: Record<string, { bytes: number; content: string | undefined }>,
118
+ ): VisualDiffEntry[] {
119
+ const entries: VisualDiffEntry[] = [];
120
+ const baseKeys = new Set(Object.keys(baseFiles));
121
+ const compareKeys = new Set(Object.keys(compareFiles));
122
+
123
+ for (const key of compareKeys) {
124
+ if (!baseKeys.has(key)) {
125
+ entries.push({
126
+ id: `added:${key}`,
127
+ kind: "added" as VisualDiffChangeKind,
128
+ description: `File added: ${key}`,
129
+ });
130
+ }
131
+ }
132
+
133
+ for (const key of baseKeys) {
134
+ if (!compareKeys.has(key)) {
135
+ entries.push({
136
+ id: `removed:${key}`,
137
+ kind: "removed" as VisualDiffChangeKind,
138
+ description: `File removed: ${key}`,
139
+ });
140
+ }
141
+ }
142
+
143
+ for (const key of compareKeys) {
144
+ if (baseKeys.has(key)) {
145
+ const baseContent = baseFiles[key]?.content;
146
+ const compareContent = compareFiles[key]?.content;
147
+ const baseBytes = baseFiles[key]?.bytes ?? 0;
148
+ const compareBytes = compareFiles[key]?.bytes ?? 0;
149
+ if (baseContent !== compareContent) {
150
+ entries.push({
151
+ id: `modified:${key}`,
152
+ kind: "modified" as VisualDiffChangeKind,
153
+ description: `File modified: ${key} (${baseBytes}B → ${compareBytes}B)`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ return entries;
160
+ }
161
+
162
+ // ─── Action ───────────────────────────────────────────────────────────────────
163
+
164
+ export default defineAction({
165
+ description:
166
+ "Read action: return a code + visual diff for a design branch. " +
167
+ "Visual diff compares design_versions snapshots (before and after branching). " +
168
+ "Code/branch diff surfaces branch metadata (name, url, status) from Builder; " +
169
+ "file-level code diffs become available once bridge write hardening is complete " +
170
+ "(currently planned, not available). " +
171
+ "For inline/localhost designs without branches, returns ctaRequired=true with a " +
172
+ "Make-it-real upgrade CTA. " +
173
+ "Pass branchName to target a specific branch; omit for the most recent one.",
174
+ schema: z.object({
175
+ designId: z
176
+ .string()
177
+ .describe("Design project ID to retrieve the branch diff for"),
178
+ branchName: z
179
+ .string()
180
+ .optional()
181
+ .describe(
182
+ "Target branch name. Defaults to the most recently created branch when omitted.",
183
+ ),
184
+ baseVersionId: z
185
+ .string()
186
+ .optional()
187
+ .describe(
188
+ "design_versions.id for the base (pre-branch) snapshot to diff against. " +
189
+ "Defaults to the branch's stored preSnapshotVersionId when omitted.",
190
+ ),
191
+ compareVersionId: z
192
+ .string()
193
+ .optional()
194
+ .describe(
195
+ "design_versions.id for the compare (post-branch) snapshot. " +
196
+ "Defaults to the most recent design_version when omitted.",
197
+ ),
198
+ }),
199
+ readOnly: true,
200
+ http: { method: "GET" },
201
+ run: async ({ designId, branchName, baseVersionId, compareVersionId }) => {
202
+ const db = getDb();
203
+
204
+ // ── Access check ────────────────────────────────────────────────────────
205
+ const access = await resolveAccess("design", designId);
206
+ if (!access) throw new Error("Design not found");
207
+
208
+ const resource = access.resource as { data?: unknown };
209
+
210
+ // ── Source type + capability check ──────────────────────────────────────
211
+ const designData = parseDesignData(resource.data);
212
+ const sourceType =
213
+ normalizeDesignSourceType(designData["sourceType"]) ?? "inline";
214
+ const caps = resolveSourceCapabilities(sourceType);
215
+
216
+ // For inline/localhost without branch capability, return a CTA.
217
+ if (!hasCapability(caps, "branch") && !hasCapability(caps, "diffPatch")) {
218
+ return {
219
+ designId,
220
+ sourceType,
221
+ ctaRequired: true,
222
+ ctaKind:
223
+ sourceType === "fusion"
224
+ ? ("connect-builder" as const)
225
+ : ("make-it-real" as const),
226
+ ctaMessage:
227
+ sourceType === "fusion"
228
+ ? "Builder is not yet connected. Connect Builder.io to view branch diffs."
229
+ : "Branch diffs require a Builder-hosted app. Use 'Make it real' to upgrade " +
230
+ "this inline design to a real-app source.",
231
+ branch: null,
232
+ visualDiff: [] as VisualDiffEntry[],
233
+ codeDiff: null,
234
+ baseVersionId: null,
235
+ compareVersionId: null,
236
+ };
237
+ }
238
+
239
+ // ── Resolve branch entry ─────────────────────────────────────────────────
240
+ const branches = parseBranches(designData);
241
+
242
+ let branch: StoredBranchEntry | null = null;
243
+ if (branchName) {
244
+ branch =
245
+ branches.find(
246
+ (b) => b.branchName?.toLowerCase() === branchName.toLowerCase(),
247
+ ) ?? null;
248
+ } else {
249
+ // Default to the most recently created branch.
250
+ branch = branches.length > 0 ? branches[branches.length - 1]! : null;
251
+ }
252
+
253
+ if (!branch) {
254
+ return {
255
+ designId,
256
+ sourceType,
257
+ ctaRequired: false,
258
+ ctaKind: null,
259
+ ctaMessage: null,
260
+ branch: null,
261
+ note: "No branch found for this design. Use create-design-branch to create one.",
262
+ visualDiff: [] as VisualDiffEntry[],
263
+ codeDiff: null,
264
+ baseVersionId: null,
265
+ compareVersionId: null,
266
+ };
267
+ }
268
+
269
+ // ── Resolve version ids for the visual diff ──────────────────────────────
270
+ // Priority: explicit params > branch's pre-snapshot > most-recent version
271
+ let effectiveBaseId =
272
+ baseVersionId ?? branch.preSnapshotVersionId ?? undefined;
273
+ let effectiveCompareId = compareVersionId;
274
+
275
+ if (!effectiveCompareId) {
276
+ const [latestVersion] = await db
277
+ .select({ id: schema.designVersions.id })
278
+ .from(schema.designVersions)
279
+ .where(eq(schema.designVersions.designId, designId))
280
+ .orderBy(desc(schema.designVersions.createdAt))
281
+ .limit(1);
282
+ effectiveCompareId = latestVersion?.id;
283
+ }
284
+
285
+ // ── Visual diff ──────────────────────────────────────────────────────────
286
+ let visualDiff: VisualDiffEntry[] = [];
287
+ let resolvedBaseVersionId: string | null = null;
288
+ let resolvedCompareVersionId: string | null = null;
289
+
290
+ if (
291
+ effectiveBaseId &&
292
+ effectiveCompareId &&
293
+ effectiveBaseId !== effectiveCompareId
294
+ ) {
295
+ const versionRows = await db
296
+ .select({
297
+ id: schema.designVersions.id,
298
+ snapshot: schema.designVersions.snapshot,
299
+ })
300
+ .from(schema.designVersions)
301
+ .where(eq(schema.designVersions.designId, designId));
302
+
303
+ const byId = Object.fromEntries(
304
+ versionRows.map((r) => [r.id, r.snapshot]),
305
+ );
306
+
307
+ const baseSnap = byId[effectiveBaseId];
308
+ const compareSnap = byId[effectiveCompareId];
309
+
310
+ if (baseSnap && compareSnap) {
311
+ const baseFiles = parseSnapshotFiles(baseSnap);
312
+ const compareFiles = parseSnapshotFiles(compareSnap);
313
+ visualDiff = diffSnapshotFiles(baseFiles, compareFiles);
314
+ resolvedBaseVersionId = effectiveBaseId;
315
+ resolvedCompareVersionId = effectiveCompareId;
316
+ }
317
+ }
318
+
319
+ // ── Code/branch diff ─────────────────────────────────────────────────────
320
+ // File-level code diffs land with bridge write hardening (phase 5).
321
+ // Surface what is available now: branch metadata + a clear not-available note.
322
+ const codeDiffAvailable = hasCapability(caps, "diffPatch");
323
+ const codeDiff = {
324
+ available: codeDiffAvailable,
325
+ branchName: branch.branchName ?? null,
326
+ projectId: branch.projectId ?? null,
327
+ url: branch.url ?? null,
328
+ status: branch.status ?? null,
329
+ note: codeDiffAvailable
330
+ ? "Branch is connected. File-level diffs are available via the Builder Visual Editor."
331
+ : "File-level code diffs are planned pending bridge write hardening. " +
332
+ "The Builder Visual Editor at the branch URL shows the full code diff.",
333
+ };
334
+
335
+ return {
336
+ designId,
337
+ sourceType,
338
+ ctaRequired: false,
339
+ ctaKind: null,
340
+ ctaMessage: null,
341
+ branch: {
342
+ branchName: branch.branchName ?? null,
343
+ projectId: branch.projectId ?? null,
344
+ url: branch.url ?? null,
345
+ status: branch.status ?? null,
346
+ purpose: branch.purpose ?? null,
347
+ createdAt: branch.createdAt ?? null,
348
+ preSnapshotVersionId: branch.preSnapshotVersionId ?? null,
349
+ },
350
+ visualDiff,
351
+ visualDiffSummary: {
352
+ added: visualDiff.filter((d) => d.kind === "added").length,
353
+ removed: visualDiff.filter((d) => d.kind === "removed").length,
354
+ modified: visualDiff.filter((d) => d.kind === "modified").length,
355
+ total: visualDiff.length,
356
+ },
357
+ codeDiff,
358
+ baseVersionId: resolvedBaseVersionId,
359
+ compareVersionId: resolvedCompareVersionId,
360
+ };
361
+ },
362
+ });
@@ -0,0 +1,314 @@
1
+ /**
2
+ * get-design-review — read-only action returning the latest cached review for
3
+ * a design, optionally including a visual diff between two `design_versions`.
4
+ *
5
+ * The review combines:
6
+ * - A11yFindings from the most recent `design_review_snapshot` row (or empty
7
+ * if no audit has been run yet).
8
+ * - A structural visual diff between `baseVersionId` and `compareVersionId`
9
+ * when both are provided: each design_versions snapshot is parsed as JSON
10
+ * and compared for layer/style deltas using the existing `design_versions`
11
+ * table — no new tables needed.
12
+ *
13
+ * See DESIGN-STUDIO-PLAN.md §6.5 + §7 (Review surface).
14
+ */
15
+
16
+ import { defineAction } from "@agent-native/core";
17
+ import { accessFilter } from "@agent-native/core/sharing";
18
+ import { and, desc, eq } from "drizzle-orm";
19
+ import { z } from "zod";
20
+
21
+ import { getDb, schema } from "../server/db/index.js";
22
+ import "../server/db/index.js"; // ensure registerShareableResource runs
23
+ import type {
24
+ A11yFinding,
25
+ DesignReviewSnapshot,
26
+ DesignReviewStatus,
27
+ VisualDiffChangeKind,
28
+ VisualDiffEntry,
29
+ } from "../shared/design-review.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Visual diff helpers (operate on stored JSON snapshots)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Parse a design_versions snapshot JSON into a flat map of
37
+ * `{ nodeId: { ... style properties ... } }` for diff comparison.
38
+ *
39
+ * The snapshot format is whatever `generate-design` / `create-design-version`
40
+ * stores — typically an object with a `files` array. We extract a lightweight
41
+ * set of each file's token/style references for structural comparison.
42
+ */
43
+ function parseSnapshotNodes(
44
+ snapshotRaw: string,
45
+ ): Record<string, Record<string, unknown>> {
46
+ try {
47
+ const obj = JSON.parse(snapshotRaw) as unknown;
48
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {};
49
+ const record = obj as Record<string, unknown>;
50
+
51
+ // Flatten files → content strings into a map keyed by filename.
52
+ const files = record["files"];
53
+ if (!Array.isArray(files)) return {};
54
+
55
+ const out: Record<string, Record<string, unknown>> = {};
56
+ for (const file of files) {
57
+ if (!file || typeof file !== "object") continue;
58
+ const f = file as Record<string, unknown>;
59
+ const name = typeof f["filename"] === "string" ? f["filename"] : "?";
60
+ const content =
61
+ typeof f["content"] === "string" ? f["content"] : undefined;
62
+ out[name] = {
63
+ content,
64
+ bytes: typeof content === "string" ? content.length : 0,
65
+ };
66
+ }
67
+ return out;
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
73
+ /** Produce a visual diff entry list by comparing two snapshot node maps. */
74
+ function diffSnapshotNodes(
75
+ baseNodes: Record<string, Record<string, unknown>>,
76
+ compareNodes: Record<string, Record<string, unknown>>,
77
+ ): VisualDiffEntry[] {
78
+ const entries: VisualDiffEntry[] = [];
79
+ const baseKeys = new Set(Object.keys(baseNodes));
80
+ const compareKeys = new Set(Object.keys(compareNodes));
81
+
82
+ // Added files
83
+ for (const key of compareKeys) {
84
+ if (!baseKeys.has(key)) {
85
+ entries.push({
86
+ id: `added:${key}`,
87
+ kind: "added" as VisualDiffChangeKind,
88
+ description: `File added: ${key}`,
89
+ });
90
+ }
91
+ }
92
+
93
+ // Removed files
94
+ for (const key of baseKeys) {
95
+ if (!compareKeys.has(key)) {
96
+ entries.push({
97
+ id: `removed:${key}`,
98
+ kind: "removed" as VisualDiffChangeKind,
99
+ description: `File removed: ${key}`,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Modified files (byte-length change as the cheapest proxy for content diff)
105
+ for (const key of compareKeys) {
106
+ if (baseKeys.has(key)) {
107
+ const baseBytes = baseNodes[key]?.["bytes"] ?? 0;
108
+ const compareBytes = compareNodes[key]?.["bytes"] ?? 0;
109
+ const baseContent = baseNodes[key]?.["content"];
110
+ const compareContent = compareNodes[key]?.["content"];
111
+ if (baseContent !== compareContent) {
112
+ entries.push({
113
+ id: `modified:${key}`,
114
+ kind: "modified" as VisualDiffChangeKind,
115
+ description: `File modified: ${key} (${baseBytes}B → ${compareBytes}B)`,
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ return entries;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Action
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export default defineAction({
129
+ description:
130
+ "Return the latest accessibility review findings for a design, " +
131
+ "plus an optional structural visual diff between two design_versions. " +
132
+ "Pass baseVersionId and compareVersionId to include the diff; omit them " +
133
+ "for a findings-only response. Read-only — run-design-audit must be " +
134
+ "called first to populate the findings; this action reads the cached result.",
135
+ schema: z.object({
136
+ designId: z
137
+ .string()
138
+ .describe("Design project ID to retrieve the review for"),
139
+ sourceRef: z
140
+ .string()
141
+ .optional()
142
+ .describe(
143
+ "Opaque source ref (design_files.id or route id) to scope the review to a specific screen. " +
144
+ "When omitted, returns the most recent design-level snapshot.",
145
+ ),
146
+ baseVersionId: z
147
+ .string()
148
+ .optional()
149
+ .describe(
150
+ "design_versions.id for the older (base) version to diff against.",
151
+ ),
152
+ compareVersionId: z
153
+ .string()
154
+ .optional()
155
+ .describe(
156
+ "design_versions.id for the newer (compare) version. " +
157
+ "If omitted when baseVersionId is set, defaults to the most recent version.",
158
+ ),
159
+ }),
160
+ readOnly: true,
161
+ http: { method: "GET" },
162
+ run: async ({ designId, sourceRef, baseVersionId, compareVersionId }) => {
163
+ const db = getDb();
164
+
165
+ // Verify access to the design.
166
+ const [design] = await db
167
+ .select({ id: schema.designs.id })
168
+ .from(schema.designs)
169
+ .where(
170
+ and(
171
+ accessFilter(schema.designs, schema.designShares),
172
+ eq(schema.designs.id, designId),
173
+ ),
174
+ )
175
+ .limit(1);
176
+
177
+ if (!design) {
178
+ const err = new Error("Design not found") as Error & {
179
+ statusCode: number;
180
+ };
181
+ err.statusCode = 404;
182
+ throw err;
183
+ }
184
+
185
+ // -----------------------------------------------------------------------
186
+ // Load the latest review snapshot for this design (+ optional sourceRef).
187
+ // -----------------------------------------------------------------------
188
+ const snapshotConditions = [
189
+ eq(schema.designReviewSnapshot.designId, designId),
190
+ ];
191
+ if (sourceRef) {
192
+ snapshotConditions.push(
193
+ eq(schema.designReviewSnapshot.sourceRef, sourceRef),
194
+ );
195
+ }
196
+
197
+ const [snapshotRow] = await db
198
+ .select()
199
+ .from(schema.designReviewSnapshot)
200
+ .where(and(...snapshotConditions))
201
+ .orderBy(desc(schema.designReviewSnapshot.createdAt))
202
+ .limit(1);
203
+
204
+ let a11yFindings: A11yFinding[] = [];
205
+ let snapshotId: string | null = null;
206
+ let auditedAt: string | null = null;
207
+ let snapshotStatus: DesignReviewStatus = "pending";
208
+
209
+ if (snapshotRow) {
210
+ snapshotId = snapshotRow.id;
211
+ auditedAt = snapshotRow.createdAt ?? null;
212
+ snapshotStatus = (snapshotRow.status ?? "pending") as DesignReviewStatus;
213
+ try {
214
+ const parsed = snapshotRow.a11yFindings
215
+ ? (JSON.parse(snapshotRow.a11yFindings) as unknown)
216
+ : [];
217
+ if (Array.isArray(parsed)) {
218
+ a11yFindings = parsed as A11yFinding[];
219
+ }
220
+ } catch {
221
+ a11yFindings = [];
222
+ }
223
+ }
224
+
225
+ // -----------------------------------------------------------------------
226
+ // Visual diff between two design_versions (optional).
227
+ // -----------------------------------------------------------------------
228
+ let visualDiff: VisualDiffEntry[] = [];
229
+ let resolvedBaseVersionId: string | null = null;
230
+ let resolvedCompareVersionId: string | null = null;
231
+
232
+ const wantDiff = !!(baseVersionId || compareVersionId);
233
+
234
+ if (wantDiff) {
235
+ // Resolve compare version (most recent if not specified).
236
+ let effectiveCompareId = compareVersionId;
237
+ if (!effectiveCompareId) {
238
+ const [latestVersion] = await db
239
+ .select({ id: schema.designVersions.id })
240
+ .from(schema.designVersions)
241
+ .where(eq(schema.designVersions.designId, designId))
242
+ .orderBy(desc(schema.designVersions.createdAt))
243
+ .limit(1);
244
+ effectiveCompareId = latestVersion?.id;
245
+ }
246
+
247
+ if (baseVersionId && effectiveCompareId) {
248
+ const versionRows = await db
249
+ .select({
250
+ id: schema.designVersions.id,
251
+ snapshot: schema.designVersions.snapshot,
252
+ })
253
+ .from(schema.designVersions)
254
+ .where(and(eq(schema.designVersions.designId, designId)));
255
+
256
+ const byId = Object.fromEntries(
257
+ versionRows.map((r) => [r.id, r.snapshot]),
258
+ );
259
+
260
+ const baseSnapshot = byId[baseVersionId];
261
+ const compareSnapshot = byId[effectiveCompareId];
262
+
263
+ if (baseSnapshot && compareSnapshot) {
264
+ const baseNodes = parseSnapshotNodes(baseSnapshot);
265
+ const compareNodes = parseSnapshotNodes(compareSnapshot);
266
+ visualDiff = diffSnapshotNodes(baseNodes, compareNodes);
267
+ resolvedBaseVersionId = baseVersionId;
268
+ resolvedCompareVersionId = effectiveCompareId;
269
+ }
270
+ }
271
+ }
272
+
273
+ // -----------------------------------------------------------------------
274
+ // Build the response shape matching DesignReviewSnapshot.
275
+ // -----------------------------------------------------------------------
276
+ const review: DesignReviewSnapshot = {
277
+ id: snapshotId ?? "",
278
+ designId,
279
+ sourceRef: sourceRef ?? null,
280
+ baseVersionId: resolvedBaseVersionId,
281
+ compareVersionId: resolvedCompareVersionId,
282
+ a11yFindings,
283
+ visualDiff,
284
+ status: snapshotStatus,
285
+ createdAt: auditedAt ?? new Date().toISOString(),
286
+ updatedAt: auditedAt ?? new Date().toISOString(),
287
+ };
288
+
289
+ return {
290
+ designId,
291
+ snapshotId,
292
+ auditedAt,
293
+ status: snapshotStatus,
294
+ a11yFindings,
295
+ a11ySummary: {
296
+ errors: a11yFindings.filter((f) => f.severity === "error").length,
297
+ warnings: a11yFindings.filter((f) => f.severity === "warning").length,
298
+ info: a11yFindings.filter((f) => f.severity === "info").length,
299
+ total: a11yFindings.length,
300
+ },
301
+ visualDiff,
302
+ diffSummary: {
303
+ added: visualDiff.filter((d) => d.kind === "added").length,
304
+ removed: visualDiff.filter((d) => d.kind === "removed").length,
305
+ modified: visualDiff.filter((d) => d.kind === "modified").length,
306
+ moved: visualDiff.filter((d) => d.kind === "moved").length,
307
+ total: visualDiff.length,
308
+ },
309
+ baseVersionId: resolvedBaseVersionId,
310
+ compareVersionId: resolvedCompareVersionId,
311
+ review,
312
+ };
313
+ },
314
+ });