@cloudcrane/app-spec 0.0.0-stage → 0.1.1

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/src/a2ui.d.ts +12 -0
  4. package/dist/src/a2ui.js +89 -0
  5. package/dist/src/blueprint.d.ts +27 -0
  6. package/dist/src/blueprint.js +62 -0
  7. package/dist/src/canonical.d.ts +2 -0
  8. package/dist/src/canonical.js +8 -0
  9. package/dist/src/catalog.d.ts +7 -0
  10. package/dist/src/catalog.js +83 -0
  11. package/dist/src/index.d.ts +9 -0
  12. package/dist/src/index.js +11 -0
  13. package/dist/src/jsonschema.d.ts +2 -0
  14. package/dist/src/jsonschema.js +11 -0
  15. package/dist/src/leak.d.ts +3 -0
  16. package/dist/src/leak.js +141 -0
  17. package/dist/src/logic.d.ts +36 -0
  18. package/dist/src/logic.js +197 -0
  19. package/dist/src/schema.d.ts +418 -0
  20. package/dist/src/schema.js +137 -0
  21. package/dist/src/validate.d.ts +6 -0
  22. package/dist/src/validate.js +54 -0
  23. package/dist/vendor/a2ui/v0_9_1/catalogs/basic/catalog.json +1383 -0
  24. package/dist/vendor/a2ui/v0_9_1/common_types.json +305 -0
  25. package/dist/vendor/a2ui/v0_9_1/server_to_client.json +131 -0
  26. package/package.json +78 -3
  27. package/schema/app-spec.v1.json +460 -0
  28. package/vendor/a2ui/v0_9_1/LICENSE +227 -0
  29. package/vendor/a2ui/v0_9_1/VENDOR.json +6 -0
  30. package/vendor/a2ui/v0_9_1/catalogs/basic/catalog.json +1383 -0
  31. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_complex-layout.json +65 -0
  32. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_formatted-text.json +56 -0
  33. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_incremental.json +134 -0
  34. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_interactive-button.json +51 -0
  35. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_row-layout.json +40 -0
  36. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_simple-login-form.json +77 -0
  37. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/00_simple-text.json +27 -0
  38. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/01_flight-status.json +201 -0
  39. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/02_email-compose.json +185 -0
  40. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/03_calendar-day.json +166 -0
  41. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/04_weather-current.json +168 -0
  42. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/05_product-card.json +151 -0
  43. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/06_music-player.json +165 -0
  44. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/07_task-card.json +107 -0
  45. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/08_user-profile.json +190 -0
  46. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/09_login-form.json +214 -0
  47. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/10_notification-permission.json +105 -0
  48. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/11_purchase-complete.json +169 -0
  49. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/12_chat-message.json +144 -0
  50. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/13_coffee-order.json +253 -0
  51. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/14_sports-player.json +177 -0
  52. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/15_account-balance.json +126 -0
  53. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/16_workout-summary.json +160 -0
  54. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/17_event-detail.json +144 -0
  55. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/18_track-list.json +152 -0
  56. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/19_software-purchase.json +194 -0
  57. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/20_restaurant-card.json +140 -0
  58. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/21_shipping-status.json +137 -0
  59. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/22_credit-card.json +117 -0
  60. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/23_step-counter.json +149 -0
  61. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/24_recipe-card.json +204 -0
  62. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/25_contact-card.json +175 -0
  63. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/26_podcast-episode.json +123 -0
  64. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/27_stats-card.json +106 -0
  65. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/28_countdown-timer.json +135 -0
  66. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/29_movie-card.json +156 -0
  67. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/30_live-invitation-builder.json +205 -0
  68. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/31_incremental-dashboard.json +128 -0
  69. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/32_advanced-form-validator.json +166 -0
  70. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/33_financial-data-grid.json +171 -0
  71. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/34_child-list-template.json +80 -0
  72. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/35_markdown-text.json +44 -0
  73. package/vendor/a2ui/v0_9_1/catalogs/basic/examples/36_modal.json +65 -0
  74. package/vendor/a2ui/v0_9_1/client_capabilities.json +89 -0
  75. package/vendor/a2ui/v0_9_1/client_data_model.json +22 -0
  76. package/vendor/a2ui/v0_9_1/client_to_server.json +98 -0
  77. package/vendor/a2ui/v0_9_1/client_to_server_list.json +10 -0
  78. package/vendor/a2ui/v0_9_1/client_to_server_list_wrapper.json +14 -0
  79. package/vendor/a2ui/v0_9_1/common_types.json +305 -0
  80. package/vendor/a2ui/v0_9_1/sample.json +22 -0
  81. package/vendor/a2ui/v0_9_1/server_capabilities.json +26 -0
  82. package/vendor/a2ui/v0_9_1/server_to_client.json +132 -0
  83. package/vendor/a2ui/v0_9_1/server_to_client_list.json +10 -0
  84. package/vendor/a2ui/v0_9_1/server_to_client_list_wrapper.json +14 -0
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ // The CloudCrane app spec: an A2UI surface (the screens) in a small envelope for
3
+ // what A2UI doesn't cover. Nothing in it is code from the app it came from:
4
+ // screens are A2UI components, the look is DTCG design tokens, rules are CEL
5
+ // expressions, saved data is described by a JSON Schema. Strict throughout, so a
6
+ // field the spec doesn't define (where code could hide) is refused.
7
+ /** A neutral id: lower-case words, dots, dashes, underscores. Partner symbol names live only in provenance. */
8
+ export const Id = z.string().regex(/^[a-z][a-zA-Z0-9_.-]{0,63}$/, "an id: a letter, then letters, digits, . _ or -, at most 64");
9
+ /** A CEL identifier (constants and parameters): letters, digits and underscores. */
10
+ export const CelName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}$/, "a CEL identifier: letters, digits and _");
11
+ /** An RFC 6901 JSON Pointer. */
12
+ export const Pointer = z.string().regex(/^(\/([^~/]|~[01])*)*$/, "a JSON Pointer, like /saved/days");
13
+ /** A pointer, or a pointer ending in /* for everything under it (mod points). */
14
+ export const PointerPattern = z.string().regex(/^(\/([^~/*]|~[01])*)*(\/\*)?$/, "a JSON Pointer, optionally ending in /*");
15
+ const Text = (max) => z.string().min(1).max(max);
16
+ /** DTCG Format 2025.10 (designtokens.org): a token has $value; a group holds tokens and groups. */
17
+ const DTCG_TYPES = [
18
+ "color",
19
+ "dimension",
20
+ "fontFamily",
21
+ "fontWeight",
22
+ "duration",
23
+ "cubicBezier",
24
+ "number",
25
+ "strokeStyle",
26
+ "border",
27
+ "transition",
28
+ "shadow",
29
+ "gradient",
30
+ "typography",
31
+ ];
32
+ export const Tokens = z.lazy(() => z
33
+ .object({ $value: z.unknown().optional(), $type: z.enum(DTCG_TYPES).optional(), $description: z.string().max(200).optional() })
34
+ .catchall(Tokens)
35
+ .superRefine((node, ctx) => {
36
+ const children = Object.keys(node).filter((k) => !k.startsWith("$"));
37
+ if (node.$value !== undefined && children.length)
38
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "a token can't also be a group" });
39
+ for (const k of children)
40
+ if (!/^[A-Za-z0-9_-]{1,40}$/.test(k))
41
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `token name "${k}"` });
42
+ }));
43
+ /** A CEL expression (cel.dev); parsed and checked by the logic validator. */
44
+ const Cel = z.string().min(1).max(4000);
45
+ export const SpecSchema = z
46
+ .object({
47
+ v: z.literal(1),
48
+ manifest: z
49
+ .object({
50
+ runtime: z.string().regex(/^\d+\.\d+\.\d+$/, "the runtime version, like 0.1.0"),
51
+ generator: Text(80),
52
+ source: z.object({ fingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/, "sha256:<64 hex>: a hash of the source, never the source") }).strict(),
53
+ })
54
+ .strict(),
55
+ module: z
56
+ .object({
57
+ id: Id,
58
+ name: Text(60),
59
+ purpose: Text(400),
60
+ /** How CloudCrane lists it: one of its icon names, its category, up to five tags (CloudCrane checks them). */
61
+ icon: z
62
+ .string()
63
+ .regex(/^[a-z][a-z0-9-]{0,39}$/)
64
+ .optional(),
65
+ category: z
66
+ .string()
67
+ .regex(/^[a-z][a-z0-9-]{0,39}$/)
68
+ .optional(),
69
+ tags: z
70
+ .array(z
71
+ .string()
72
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
73
+ .max(24))
74
+ .max(5)
75
+ .optional(),
76
+ })
77
+ .strict(),
78
+ tokens: Tokens,
79
+ /** A2UI v0.9.1 messages, the way A2UI's own samples hold them; checked by surfaceProblems. */
80
+ surface: z.object({ catalog: z.string().url(), messages: z.array(z.record(z.unknown())).min(2).max(200) }).strict(),
81
+ data: z
82
+ .object({
83
+ version: z.number().int().min(1),
84
+ /** Where it's saved on the phone. */
85
+ key: z.string().regex(/^[a-z][a-z0-9._-]{0,63}$/),
86
+ /** The part of the A2UI data model that's saved; the rest is screen state. */
87
+ persist: Pointer,
88
+ /** JSON Schema of the saved data. */
89
+ schema: z.record(z.unknown()),
90
+ initial: z.unknown(),
91
+ /**
92
+ * Two-way views: the runtime keeps `path` equal to the data at `source` (a
93
+ * pointer with A2UI's ${/path} interpolation, like /saved/days/${/ui/selected}),
94
+ * and writes edits made through the view back there. For lists a screen shows
95
+ * by a choice the person makes, since A2UI templates take a fixed path.
96
+ */
97
+ views: z
98
+ .array(z.object({ path: Pointer, source: z.string().regex(/^(\/([^~/$]|~[01]|\$\{\/[^}]*\})*)+$/, "a pointer, with ${/path} parts") }).strict())
99
+ .max(10)
100
+ .default([]),
101
+ /** Each step turns version `from` into `to`, as a CEL expression over `saved`. */
102
+ migrations: z.array(z.object({ from: z.number().int().min(1), to: z.number().int().min(2), cel: Cel }).strict()),
103
+ })
104
+ .strict(),
105
+ logic: z
106
+ .object({
107
+ consts: z.record(CelName, z.union([z.number(), z.string(), z.boolean()])).default({}),
108
+ functions: z.record(CelName, z.object({ params: z.array(CelName).max(8), cel: Cel }).strict()).default({}),
109
+ /** Values the screens read, kept up to date: a data-model pointer and the CEL that computes it. */
110
+ computed: z.record(Pointer, Cel).default({}),
111
+ /** Examples each function must reproduce: recorded from the source's own tests, or given. */
112
+ vectors: z.array(z.object({ fn: CelName, in: z.array(z.unknown()).max(8), out: z.unknown(), from: z.enum(["recorded", "hint"]) }).strict()).default([]),
113
+ })
114
+ .strict(),
115
+ network: z
116
+ .array(z.object({ site: z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/), why: Text(300) }).strict())
117
+ .max(5)
118
+ .default([]),
119
+ journeys: z
120
+ .array(z
121
+ .object({
122
+ id: z.string().regex(/^J-\d+$/),
123
+ title: Text(120),
124
+ steps: z.array(z.object({ id: z.string().regex(/^J-\d+\.\d+$/), text: Text(400) }).strict()).min(1),
125
+ })
126
+ .strict())
127
+ .min(1),
128
+ promises: z.array(z.object({ id: z.string().regex(/^P-\d+$/), text: Text(400), kind: z.literal("data").optional() }).strict()).default([]),
129
+ /** CloudCrane's test steps, as its blueprint tests take them; CloudCrane checks their shape. */
130
+ tests: z.array(z.record(z.unknown())).max(30).default([]),
131
+ /** What a remix may change and what stays as the partner made it. */
132
+ mods: z
133
+ .object({ open: z.array(PointerPattern), locked: z.array(PointerPattern) })
134
+ .strict()
135
+ .default({ open: [], locked: [] }),
136
+ })
137
+ .strict();
@@ -0,0 +1,6 @@
1
+ import { type Catalog, type Problem } from "./a2ui.js";
2
+ import { type Spec } from "./schema.js";
3
+ export declare function validateSpec(input: unknown, catalog?: Catalog): {
4
+ spec: Spec | null;
5
+ problems: Problem[];
6
+ };
@@ -0,0 +1,54 @@
1
+ import { surfaceProblems } from "./a2ui.js";
2
+ import { catalogFor } from "./catalog.js";
3
+ import { leakProblems } from "./leak.js";
4
+ import { logicProblems } from "./logic.js";
5
+ import { SpecSchema } from "./schema.js";
6
+ // Everything wrong with a spec, collected rather than stopping at the first: the
7
+ // envelope's shape, then the A2UI surface, then references between parts.
8
+ const pathOf = (path) => path
9
+ .map((p) => (typeof p === "number" ? `[${p}]` : `.${p}`))
10
+ .join("")
11
+ .replace(/^\./, "");
12
+ export function validateSpec(input, catalog) {
13
+ const parsed = SpecSchema.safeParse(input);
14
+ if (!parsed.success)
15
+ return { spec: null, problems: parsed.error.issues.map((i) => ({ at: pathOf(i.path) || "spec", problem: i.message })) };
16
+ const spec = parsed.data;
17
+ // The surface is checked against the catalog it names (CloudCrane's, or A2UI's basic one).
18
+ const named = catalog ?? catalogFor(spec.surface.catalog);
19
+ if (!named)
20
+ return { spec: null, problems: [{ at: "surface.catalog", problem: "not a catalog this runtime has" }] };
21
+ const problems = [...surfaceProblems(spec.surface.messages, named)];
22
+ const catalogId = spec.surface.messages[0].createSurface?.catalogId;
23
+ if (catalogId && catalogId !== spec.surface.catalog)
24
+ problems.push({ at: "surface.catalog", problem: "must be the catalog createSurface names" });
25
+ const ids = new Set();
26
+ for (const j of spec.journeys) {
27
+ if (ids.has(j.id))
28
+ problems.push({ at: `journeys ${j.id}`, problem: "duplicate id" });
29
+ ids.add(j.id);
30
+ for (const s of j.steps) {
31
+ if (!s.id.startsWith(`${j.id}.`))
32
+ problems.push({ at: `journeys ${s.id}`, problem: `a step of ${j.id} is numbered ${j.id}.n` });
33
+ if (ids.has(s.id))
34
+ problems.push({ at: `journeys ${s.id}`, problem: "duplicate id" });
35
+ ids.add(s.id);
36
+ }
37
+ }
38
+ for (const p of spec.promises) {
39
+ if (ids.has(p.id))
40
+ problems.push({ at: `promises ${p.id}`, problem: "duplicate id" });
41
+ ids.add(p.id);
42
+ }
43
+ for (const v of spec.logic.vectors)
44
+ if (!(v.fn in spec.logic.functions))
45
+ problems.push({ at: `logic.vectors ${v.fn}`, problem: "no such function" });
46
+ for (const m of spec.data.migrations)
47
+ if (m.to !== m.from + 1 || m.to > spec.data.version)
48
+ problems.push({ at: `data.migrations ${m.from}`, problem: "steps go one version at a time, up to data.version" });
49
+ problems.push(...logicProblems(spec), ...leakProblems(spec));
50
+ for (const locked of spec.mods.locked)
51
+ if (spec.mods.open.includes(locked))
52
+ problems.push({ at: `mods ${locked}`, problem: "both open and locked" });
53
+ return { spec: problems.length ? null : spec, problems };
54
+ }