@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chirag Phor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @cloudcrane/app-spec
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The CloudCrane app spec: its schema, the checks a spec must pass, and the
4
+ rule language its logic is written in.
5
+
6
+ ```js
7
+ import { validateSpec, deriveBlueprint } from "@cloudcrane/app-spec";
8
+
9
+ const { spec, problems } = validateSpec(JSON.parse(text));
10
+ if (!spec) for (const p of problems) console.log(`${p.at}: ${p.problem}`);
11
+ ```
12
+
13
+ - `validateSpec(input)`: the schema, the A2UI surface against its catalog, every
14
+ rule parses and reproduces its examples, and the leak scan (paths, code,
15
+ secrets, personal details). Each problem names its JSON Pointer.
16
+ - `deriveBlueprint(spec)`: the blueprint CloudCrane shows for an app (its purpose, screens, data,
17
+ journeys and promises), from the spec.
18
+ - `Rules` (`@cloudcrane/app-spec/logic`): evaluates a spec's CEL rules, with
19
+ date functions and a bounded `fold`.
20
+ - `@cloudcrane/app-spec/schema.json`: the spec as JSON Schema (2020-12).
21
+
22
+ `zod` 3.25 is a peer dependency. Includes A2UI v0.9.1's schemas, unchanged,
23
+ under the Apache License 2.0 (`vendor/a2ui`). MIT licensed.
@@ -0,0 +1,12 @@
1
+ export type Problem = {
2
+ at: string;
3
+ problem: string;
4
+ };
5
+ export type Catalog = {
6
+ catalogId: string;
7
+ [k: string]: unknown;
8
+ };
9
+ export declare const A2UI_VERSION = "v0.9.1";
10
+ export declare const BASIC_CATALOG: Catalog;
11
+ /** Problems with a spec's A2UI messages; none means a renderer can draw them. */
12
+ export declare function surfaceProblems(messages: unknown[], catalog?: Catalog): Problem[];
@@ -0,0 +1,89 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import addFormats from "ajv-formats";
3
+ import basicCatalog from "../vendor/a2ui/v0_9_1/catalogs/basic/catalog.json" with { type: "json" };
4
+ import commonTypes from "../vendor/a2ui/v0_9_1/common_types.json" with { type: "json" };
5
+ import serverToClient from "../vendor/a2ui/v0_9_1/server_to_client.json" with { type: "json" };
6
+ export const A2UI_VERSION = "v0.9.1";
7
+ export const BASIC_CATALOG = basicCatalog;
8
+ const CATALOG_SLOT = "https://a2ui.org/specification/v0_9/catalog.json";
9
+ const validators = new Map();
10
+ /** A2UI's message validator with `catalog` as the surface's catalog (built once per catalog). */
11
+ function messageValidator(catalog) {
12
+ const known = validators.get(catalog.catalogId);
13
+ if (known)
14
+ return known;
15
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
16
+ addFormats(ajv);
17
+ ajv.addSchema(commonTypes);
18
+ ajv.addSchema({ ...catalog, $id: CATALOG_SLOT });
19
+ const validate = ajv.compile(serverToClient);
20
+ validators.set(catalog.catalogId, validate);
21
+ return validate;
22
+ }
23
+ /** The ids a component names as its children: a list of ids, one child, or a template over a path. */
24
+ function childIdsOf(c) {
25
+ const out = [];
26
+ const add = (v) => {
27
+ if (typeof v === "string")
28
+ out.push(v);
29
+ else if (Array.isArray(v))
30
+ v.forEach(add);
31
+ else if (v && typeof v === "object" && "componentId" in v && typeof v.componentId === "string")
32
+ out.push(v.componentId);
33
+ };
34
+ add(c.children);
35
+ add(c.child);
36
+ for (const key of ["trigger", "content", "header", "body"])
37
+ add(c[key]);
38
+ if (Array.isArray(c["tabs"]))
39
+ for (const t of c["tabs"])
40
+ add(t?.child);
41
+ return out;
42
+ }
43
+ /** Problems with a spec's A2UI messages; none means a renderer can draw them. */
44
+ export function surfaceProblems(messages, catalog = BASIC_CATALOG) {
45
+ const problems = [];
46
+ const validate = messageValidator(catalog);
47
+ messages.forEach((m, i) => {
48
+ if (!validate(m)) {
49
+ for (const e of (validate.errors ?? []).slice(0, 5))
50
+ problems.push({ at: `surface.messages[${i}]${e.instancePath}`, problem: e.message ?? "invalid" });
51
+ }
52
+ });
53
+ if (problems.length)
54
+ return problems;
55
+ const first = messages[0];
56
+ if (!first?.createSurface)
57
+ return [{ at: "surface.messages[0]", problem: "must be createSurface" }];
58
+ const { surfaceId, catalogId } = first.createSurface;
59
+ if (catalogId !== catalog.catalogId)
60
+ problems.push({ at: "surface.messages[0].createSurface.catalogId", problem: `must be ${catalog.catalogId}` });
61
+ const components = new Map();
62
+ messages.forEach((m, i) => {
63
+ const body = Object.values(m)[1];
64
+ const kind = Object.keys(m).find((k) => k !== "version");
65
+ if (i > 0 && kind === "createSurface")
66
+ problems.push({ at: `surface.messages[${i}]`, problem: "only one createSurface: a spec is one surface" });
67
+ if (kind === "deleteSurface")
68
+ problems.push({ at: `surface.messages[${i}]`, problem: "a spec never deletes its surface" });
69
+ const sid = m[kind ?? ""]?.surfaceId ?? body?.surfaceId;
70
+ if (sid !== surfaceId)
71
+ problems.push({ at: `surface.messages[${i}]`, problem: `surfaceId must be ${surfaceId}` });
72
+ if (kind === "updateComponents") {
73
+ for (const c of m.updateComponents.components) {
74
+ const id = String(c.id);
75
+ if (components.has(id))
76
+ problems.push({ at: `component ${id}`, problem: "duplicate id" });
77
+ components.set(id, c);
78
+ }
79
+ }
80
+ });
81
+ if (!components.has("root"))
82
+ problems.push({ at: "surface", problem: 'no component has the id "root"' });
83
+ for (const [id, c] of components) {
84
+ for (const child of childIdsOf(c))
85
+ if (!components.has(child))
86
+ problems.push({ at: `component ${id}`, problem: `child "${child}" doesn't exist` });
87
+ }
88
+ return problems;
89
+ }
@@ -0,0 +1,27 @@
1
+ import type { Spec } from "./schema.js";
2
+ export type BlueprintV1 = {
3
+ v: 1;
4
+ purpose: string;
5
+ screens: {
6
+ id: string;
7
+ says: string;
8
+ }[];
9
+ data: {
10
+ version: number;
11
+ keeps: {
12
+ key: string;
13
+ what: string;
14
+ }[];
15
+ };
16
+ journeys: Spec["journeys"];
17
+ promises: Spec["promises"];
18
+ network: Spec["network"];
19
+ icon?: string;
20
+ category?: string;
21
+ tags?: string[];
22
+ design: {
23
+ palette: string[];
24
+ notes: string;
25
+ };
26
+ };
27
+ export declare function deriveBlueprint(spec: Spec): BlueprintV1;
@@ -0,0 +1,62 @@
1
+ const literal = (v) => (typeof v === "string" ? v.replace(/\$\{[^}]*\}/g, "…").trim() : null);
2
+ const list = (xs, max = 6) => {
3
+ const u = [...new Set(xs.filter(Boolean))];
4
+ return u.length > max ? `${u.slice(0, max).join(", ")} and more` : u.join(", ");
5
+ };
6
+ /** The plain words a person sees on the screen: titles, labels and buttons. */
7
+ function screenWords(spec) {
8
+ const components = spec.surface.messages.flatMap((m) => m.updateComponents?.components ?? []);
9
+ const byId = new Map(components.map((c) => [c.id, c]));
10
+ const texts = components.filter((c) => c.component === "Text").map((c) => literal(c.text));
11
+ const inputs = components
12
+ .filter((c) => ["TextField", "CheckBox", "ChoicePicker", "Slider", "DateTimeInput"].includes(c.component))
13
+ .map((c) => literal(c.label));
14
+ // A one-character label (×, +) is a glyph, not a word a person reads.
15
+ const buttons = components.filter((c) => c.component === "Button").map((c) => literal(byId.get(c.child ?? "")?.text));
16
+ const words = (xs) => xs.filter((t) => !!t && t.length > 1);
17
+ const widgets = components.filter((c) => !["Text", "Column", "Row", "List", "Card", "Button", "Divider"].includes(c.component)).map((c) => c.component);
18
+ const parts = [
19
+ words(texts).length && `Shows ${list(words(texts).filter((t) => !buttons.includes(t)))}`,
20
+ words(inputs).length && `asks for ${list(words(inputs))}`,
21
+ words(buttons).length && `with ${list(words(buttons))}`,
22
+ widgets.length && `using ${list(widgets)}`,
23
+ ].filter(Boolean);
24
+ return `${parts.join("; ")}.`;
25
+ }
26
+ /** Every colour in the tokens, as written. */
27
+ function palette(tokens) {
28
+ const out = [];
29
+ const walk = (n) => {
30
+ if (!n || typeof n !== "object")
31
+ return;
32
+ const v = n.$value;
33
+ if (typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v))
34
+ out.push(v.toLowerCase());
35
+ else if (v && typeof v === "object" && typeof v.hex === "string")
36
+ out.push(v.hex.toLowerCase());
37
+ for (const [k, c] of Object.entries(n))
38
+ if (!k.startsWith("$"))
39
+ walk(c);
40
+ };
41
+ walk(tokens);
42
+ return [...new Set(out)].slice(0, 12);
43
+ }
44
+ export function deriveBlueprint(spec) {
45
+ const saved = spec.data.initial && typeof spec.data.initial === "object" ? Object.keys(spec.data.initial) : [];
46
+ return {
47
+ v: 1,
48
+ purpose: spec.module.purpose,
49
+ screens: [{ id: spec.module.id, says: screenWords(spec) }],
50
+ data: {
51
+ version: spec.data.version,
52
+ keeps: [{ key: spec.data.key, what: saved.length ? `On this phone: ${list(saved)}` : "What you enter, on this phone" }],
53
+ },
54
+ journeys: spec.journeys,
55
+ promises: spec.promises,
56
+ network: spec.network,
57
+ ...(spec.module.icon ? { icon: spec.module.icon } : {}),
58
+ ...(spec.module.category ? { category: spec.module.category } : {}),
59
+ ...(spec.module.tags?.length ? { tags: spec.module.tags } : {}),
60
+ design: { palette: palette(spec.tokens), notes: `Built from a CloudCrane spec, runtime ${spec.manifest.runtime}.` },
61
+ };
62
+ }
@@ -0,0 +1,2 @@
1
+ /** A value as RFC 8785 (JCS) canonical JSON: the same bytes for the same value, everywhere. */
2
+ export declare function canonical(value: unknown): string;
@@ -0,0 +1,8 @@
1
+ import canonicalizeJson from "canonicalize";
2
+ /** A value as RFC 8785 (JCS) canonical JSON: the same bytes for the same value, everywhere. */
3
+ export function canonical(value) {
4
+ const out = canonicalizeJson(value);
5
+ if (out === undefined)
6
+ throw new TypeError("Not representable as JSON");
7
+ return out;
8
+ }
@@ -0,0 +1,7 @@
1
+ import { type Catalog } from "./a2ui.js";
2
+ export declare const CLOUDCRANE_CATALOG_ID = "https://cloudcrane.ai/catalogs/app/v1.json";
3
+ /** The CloudCrane catalog document, built from A2UI's basic catalog. */
4
+ export declare function cloudcraneCatalog(): Catalog;
5
+ export declare const CLOUDCRANE_CATALOG: Catalog;
6
+ /** The catalog a surface names, if it's one we know. */
7
+ export declare function catalogFor(id: string): Catalog | undefined;
@@ -0,0 +1,83 @@
1
+ import { BASIC_CATALOG } from "./a2ui.js";
2
+ // The CloudCrane catalog: A2UI's basic catalog plus what an app on a phone
3
+ // needs that an agent's chat surface doesn't, written as an A2UI catalog
4
+ // document (the format A2UI's own catalogs use), so A2UI's tools and
5
+ // renderers read it as they read theirs.
6
+ //
7
+ // Functions (renderer-only local actions; they change the surface's data model):
8
+ // set(path, value), push(path, value), removeAt(path) (the item the action
9
+ // was fired from, by its index in a list template), batch(steps).
10
+ // Components:
11
+ // DateStrip: the last N days as chips, today on the right, one selectable,
12
+ // each with a status dot.
13
+ export const CLOUDCRANE_CATALOG_ID = "https://cloudcrane.ai/catalogs/app/v1.json";
14
+ const COMMON = "https://a2ui.org/specification/v0_9/common_types.json#/$defs";
15
+ const path = { $ref: `${COMMON}/DynamicString`, description: "A JSON Pointer into the data model, like /saved/days/2026-10-27." };
16
+ const value = { $ref: `${COMMON}/DynamicValue`, description: "The value: a literal, or bound to the data model." };
17
+ const fn = (name, description, properties, required) => ({
18
+ type: "object",
19
+ description,
20
+ properties: {
21
+ call: { const: name },
22
+ args: { type: "object", properties, required, additionalProperties: false },
23
+ returnType: { const: "void" },
24
+ },
25
+ required: ["call", "args"],
26
+ unevaluatedProperties: false,
27
+ });
28
+ // A step names its place `target`, not `path`: in A2UI any object with a `path` key is a data binding.
29
+ const STEP = {
30
+ type: "object",
31
+ properties: { op: { enum: ["set", "push", "removeAt"] }, target: path, value },
32
+ required: ["op", "target"],
33
+ additionalProperties: false,
34
+ };
35
+ const FUNCTIONS = {
36
+ set: fn("set", "Sets the value at a path in the data model.", { path, value }, ["path", "value"]),
37
+ push: fn("push", "Adds a value to the end of the list at a path, creating the list if there isn't one.", { path, value }, ["path", "value"]),
38
+ removeAt: fn("removeAt", "Removes, from the list at a path, the item this action was fired from (by its index in a list template).", { path }, ["path"]),
39
+ batch: fn("batch", "Runs set, push and removeAt steps in order, as one change.", { steps: { type: "array", items: STEP, minItems: 1, maxItems: 10 } }, [
40
+ "steps",
41
+ ]),
42
+ };
43
+ const COMPONENTS = {
44
+ DateStrip: {
45
+ type: "object",
46
+ allOf: [
47
+ { $ref: `${COMMON}/ComponentCommon` },
48
+ { $ref: "#/$defs/CatalogComponentCommon" },
49
+ {
50
+ type: "object",
51
+ properties: {
52
+ component: { const: "DateStrip" },
53
+ days: { type: "integer", minimum: 2, maximum: 60, description: "How many days, ending today." },
54
+ today: { $ref: `${COMMON}/DynamicString`, description: "Today, as YYYY-MM-DD." },
55
+ value: { $ref: `${COMMON}/DynamicString`, description: "The selected day, as YYYY-MM-DD (two-way)." },
56
+ statuses: { $ref: `${COMMON}/DynamicValue`, description: "A map of day (YYYY-MM-DD) to complete, active, partial or incomplete, for its dot." },
57
+ },
58
+ required: ["component", "today", "value"],
59
+ },
60
+ ],
61
+ unevaluatedProperties: false,
62
+ },
63
+ };
64
+ /** The CloudCrane catalog document, built from A2UI's basic catalog. */
65
+ export function cloudcraneCatalog() {
66
+ const basic = structuredClone(BASIC_CATALOG);
67
+ basic.components = { ...basic.components, ...COMPONENTS };
68
+ basic.functions = { ...basic.functions, ...FUNCTIONS };
69
+ basic.$defs.anyComponent.oneOf.push(...Object.keys(COMPONENTS).map((k) => ({ $ref: `#/components/${k}` })));
70
+ basic.$defs.anyFunction.oneOf.push(...Object.keys(FUNCTIONS).map((k) => ({ $ref: `#/functions/${k}` })));
71
+ return {
72
+ ...basic,
73
+ $id: CLOUDCRANE_CATALOG_ID,
74
+ catalogId: CLOUDCRANE_CATALOG_ID,
75
+ title: "CloudCrane app catalog",
76
+ description: "A2UI's basic catalog, plus local actions (set, push, removeAt, batch) and DateStrip, for apps that run on a phone.",
77
+ };
78
+ }
79
+ export const CLOUDCRANE_CATALOG = cloudcraneCatalog();
80
+ /** The catalog a surface names, if it's one we know. */
81
+ export function catalogFor(id) {
82
+ return id === CLOUDCRANE_CATALOG_ID ? CLOUDCRANE_CATALOG : id === BASIC_CATALOG.catalogId ? BASIC_CATALOG : undefined;
83
+ }
@@ -0,0 +1,9 @@
1
+ export { A2UI_VERSION, BASIC_CATALOG, surfaceProblems, type Catalog, type Problem } from "./a2ui.js";
2
+ export { Id, Pointer, PointerPattern, SpecSchema, Tokens, type Spec, type TokenNode } from "./schema.js";
3
+ export { validateSpec } from "./validate.js";
4
+ export { canonical } from "./canonical.js";
5
+ export { specJsonSchema } from "./jsonschema.js";
6
+ export { dates, logicProblems, Rules, fromCel, toCel, MAX_RANGE } from "./logic.js";
7
+ export { leakProblems } from "./leak.js";
8
+ export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
9
+ export { deriveBlueprint, type BlueprintV1 } from "./blueprint.js";
@@ -0,0 +1,11 @@
1
+ // The CloudCrane app spec: its schema, the A2UI surface check, canonical JSON
2
+ // and the JSON Schema export. See the repo README.
3
+ export { A2UI_VERSION, BASIC_CATALOG, surfaceProblems } from "./a2ui.js";
4
+ export { Id, Pointer, PointerPattern, SpecSchema, Tokens } from "./schema.js";
5
+ export { validateSpec } from "./validate.js";
6
+ export { canonical } from "./canonical.js";
7
+ export { specJsonSchema } from "./jsonschema.js";
8
+ export { dates, logicProblems, Rules, fromCel, toCel, MAX_RANGE } from "./logic.js";
9
+ export { leakProblems } from "./leak.js";
10
+ export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
11
+ export { deriveBlueprint } from "./blueprint.js";
@@ -0,0 +1,2 @@
1
+ /** The spec's JSON Schema, for tools outside TypeScript (schema/app-spec.v1.json is this, checked in). */
2
+ export declare function specJsonSchema(): Record<string, unknown>;
@@ -0,0 +1,11 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ import { SpecSchema } from "./schema.js";
3
+ /** The spec's JSON Schema, for tools outside TypeScript (schema/app-spec.v1.json is this, checked in). */
4
+ export function specJsonSchema() {
5
+ return {
6
+ $id: "https://cloudcrane.ai/schemas/app-spec.v1.json",
7
+ title: "CloudCrane app spec v1",
8
+ description: "An A2UI surface in an envelope: DTCG tokens, saved data, CEL logic, journeys, promises, tests and mod points. No code.",
9
+ ...zodToJsonSchema(SpecSchema, { target: "jsonSchema2019-09", $refStrategy: "none" }),
10
+ };
11
+ }
@@ -0,0 +1,3 @@
1
+ import type { Problem } from "./a2ui.js";
2
+ /** What in a spec looks like code, a path, a secret, a personal detail or an internal host. */
3
+ export declare function leakProblems(spec: unknown): Problem[];
@@ -0,0 +1,141 @@
1
+ const re = (r) => (s) => r.test(s);
2
+ const SOURCE = "dart|ts|tsx|js|jsx|mjs|cjs|py|kt|kts|swift|java|go|rb|php|cs|m|mm|c|cc|cpp|h|vue|svelte";
3
+ /** Shannon entropy per character. */
4
+ function entropy(s) {
5
+ const counts = new Map();
6
+ for (const c of s)
7
+ counts.set(c, (counts.get(c) ?? 0) + 1);
8
+ let h = 0;
9
+ for (const n of counts.values())
10
+ h -= (n / s.length) * Math.log2(n / s.length);
11
+ return h;
12
+ }
13
+ /** A long run of key-like characters that looks random: a token, not a word. */
14
+ function highEntropy(s) {
15
+ for (const m of s.matchAll(/[A-Za-z0-9+/_=-]{32,}/g)) {
16
+ const run = m[0];
17
+ const hex = /^[0-9a-fA-F]+$/.test(run);
18
+ if (entropy(run) > (hex ? 3.5 : 4.3))
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ /** 10 to 15 digits with phone punctuation, that isn't a date or a time. */
24
+ function phoneNumber(s) {
25
+ for (const m of s.matchAll(/(?<![\w-])\+?\d[\d\s().-]{8,}\d(?![\w-])/g)) {
26
+ const run = m[0].trim();
27
+ if (/^\d{4}-\d{2}-\d{2}/.test(run) || /^\d{1,2}:\d{2}/.test(run))
28
+ continue;
29
+ const digits = run.replace(/\D/g, "").length;
30
+ if (digits >= 10 && digits <= 15)
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ const RULES = [
36
+ { kind: "path", why: "a source file path", test: re(new RegExp(`(^|[\\s"'(\`])(?:[\\w.-]+/)+[\\w.-]+\\.(?:${SOURCE})\\b`)) },
37
+ { kind: "path", why: "a file:line reference", test: re(new RegExp(`\\b[\\w./-]+\\.(?:${SOURCE}):\\d+`)) },
38
+ { kind: "path", why: "an absolute path on someone's machine", test: re(/(^|\s)(\/Users\/|\/home\/|[A-Z]:\\)/) },
39
+ { kind: "path", why: "a package folder", test: re(/node_modules\/|\.dart_tool\/|\/pubspec\.yaml/) },
40
+ { kind: "secret", why: "an AWS access key", test: re(/\bAKIA[0-9A-Z]{16}\b/) },
41
+ {
42
+ kind: "secret",
43
+ why: "an API key",
44
+ test: re(/\b(sk|pk|rk)[-_](live|test|proj)?[-_]?[A-Za-z0-9]{20,}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bgh[pousr]_[A-Za-z0-9]{36}\b|\bxox[abpr]-[A-Za-z0-9-]{10,}/),
45
+ },
46
+ { kind: "secret", why: "a JSON Web Token", test: re(/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\./) },
47
+ { kind: "secret", why: "a private key", test: re(/-----BEGIN [A-Z ]*PRIVATE KEY-----/) },
48
+ { kind: "secret", why: "a long random-looking string", test: highEntropy },
49
+ { kind: "personal", why: "an email address", test: re(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/) },
50
+ { kind: "personal", why: "a phone number", test: phoneNumber },
51
+ {
52
+ kind: "host",
53
+ why: "an internal address",
54
+ test: re(/\b(localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|[\w-]+\.(internal|local|corp|lan))\b/),
55
+ },
56
+ { kind: "code", why: "an arrow function", test: re(/=>/) },
57
+ { kind: "code", why: "a function definition", test: re(/\bfunction\s*\w*\s*\(|\bdef\s+\w+\s*\(|\bfun\s+\w+\s*\(/) },
58
+ { kind: "code", why: "an import", test: re(/^\s*(import|export)\s+[\w{*'"]|\brequire\(\s*['"]/m) },
59
+ { kind: "code", why: "a class definition", test: re(/\bclass\s+\w+\s+(extends|implements|with)\b/) },
60
+ { kind: "code", why: "a widget builder", test: re(/\bWidget\s+build\s*\(|\bsetState\s*\(|\buseState\s*\(/) },
61
+ { kind: "code", why: "a variable declaration", test: re(/\b(const|let|var|final)\s+[A-Za-z_]\w*\s*(:\s*\w+\s*)?=/) },
62
+ { kind: "code", why: "statement punctuation", test: (s) => (s.match(/[;{}]/g)?.length ?? 0) >= 4 },
63
+ { kind: "code", why: "a method call", test: re(/\b[A-Za-z_$][\w$]*(\?)?\.[A-Za-z_$][\w$]*(<[^>]*>)?\(/) },
64
+ { kind: "code", why: "a constructor call", test: re(/(?<![\w$])_?[A-Z][A-Za-z0-9_]*(<[^>]*>)?\(/) },
65
+ { kind: "code", why: "a property line", test: re(/^\s*[a-z_$][\w$]*\s*:\s*\S.*[,({[]\s*$/) },
66
+ { kind: "code", why: "control flow", test: re(/\b(if|for|while|switch|catch)\s*\(|\bawait\s+[\w$.]|\breturn\s+[\w$(!'"[{]|\}\s*else\b/) },
67
+ { kind: "code", why: "a code operator", test: re(/===?|!==?|&&|\|\||\?\?|\?\.|\+\+|--(?=[\w$])|\b\w+\s*[+\-*/]?=\s*[\w$'"(\[]/) },
68
+ {
69
+ kind: "code",
70
+ why: "a typed declaration",
71
+ test: re(/\b(final|late|required|static|private|public|protected|readonly)\s+[A-Za-z_$][\w$<>?,\s]*\s+[A-Za-z_$][\w$]*\s*[;=,)]/),
72
+ },
73
+ { kind: "code", why: "a statement end", test: re(/[)\]}]\s*;\s*$|^\s*[\w$.]+\s*\([^)]*\)\s*;?\s*$/) },
74
+ { kind: "code", why: "a generic type", test: re(/\b[A-Za-z_$][\w$]*<[A-Z][\w$<>?,\s]*>/) },
75
+ { kind: "code", why: "a block opener", test: re(/\)\s*(async\s*)?\{\s*$|\(\{\s*$|\)\s+[a-z_$][\w$]*\s*\(/) },
76
+ { kind: "code", why: "a member of this", test: re(/\bthis\.[A-Za-z_$]/) },
77
+ { kind: "code", why: "a trailing code comment", test: re(/[;,){\]]\s*\/\/\s/) },
78
+ { kind: "code", why: "a quoted key", test: re(/^\s*(['"])[\w-]+\1\s*:\s*\S/) },
79
+ { kind: "code", why: "a string literal in code", test: re(/^\s*(['"]).*\1\s*[,)]\s*(\1|$)|^\s*(['"])[^'"]*\4\s*,\s*(['"])|\\n/) },
80
+ { kind: "code", why: "a typed function", test: re(/\b(bool|int|double|String|void|dynamic|num|var|Future|Stream)\??\s+_?[A-Za-z$][\w$]*\s*\(/) },
81
+ { kind: "code", why: "a conditional expression", test: re(/(^|[\s(])\?\s*[\w$.'"!]+.*\s:\s*[\w$.'"]/) },
82
+ { kind: "code", why: "a member access", test: re(/\b[A-Z][A-Za-z0-9]*\.[a-z_][\w$]*\b|\b[a-z_$][\w$]*\.[a-z_$][\w$]*[A-Z][\w$]*\b/) },
83
+ { kind: "code", why: "string interpolation", test: re(/\$\{|\$[a-z_][\w]*/) },
84
+ { kind: "code", why: "a cast or an index", test: re(/\bas\s+[A-Z][\w$]*\??|\[(['"])[\w-]+\1\]|\w!\.|\w!\(|\}\s*from\s*['"]|\/\*\*|\*\//) },
85
+ {
86
+ kind: "code",
87
+ why: "a type annotation",
88
+ test: re(/^\s*[A-Za-z_$][\w$]*\??\s*:\s*[^\s].*[;,]\s*$|\|\s*(null|undefined|string|number|boolean)\b|\bPromise<|\basync\s+[\w$]+\s*\(/),
89
+ },
90
+ { kind: "code", why: "a call", test: re(/^\s*\.[a-z_$][\w$]*\(|\b[a-z_$][\w$]*\((['"`]|[\w$]{2,}[,)]|\)|\{)/) },
91
+ { kind: "code", why: "a constant name", test: re(/\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b/) },
92
+ ];
93
+ /** Strings the scan skips, by pointer: the source hash is a hash by design. */
94
+ const EXEMPT = new Set(["/manifest/source/fingerprint"]);
95
+ /** Where CEL lives: rules may look like code there, but nothing else may. */
96
+ const CEL_AT = [/^\/logic\/functions\/[^/]+\/cel$/, /^\/logic\/computed\/.+$/, /^\/data\/migrations\/\d+\/cel$/];
97
+ /** A test step's state path: a dotted path into what the app exposes (CloudCrane's form), which reads like a member access by design. */
98
+ const STATE_PATH_AT = /^\/tests\/\d+\/(steps|setup|check)\/\d+\/path$/;
99
+ const STATE_PATH = /^[A-Za-z0-9_$]+(\.[A-Za-z0-9_$]+)*$/;
100
+ const escape = (k) => k.replace(/~/g, "~0").replace(/\//g, "~1");
101
+ /** Every string in a value, with its JSON Pointer (object keys count as strings too). */
102
+ function* strings(v, at = "") {
103
+ if (typeof v === "string")
104
+ yield [at, v];
105
+ else if (Array.isArray(v))
106
+ for (let i = 0; i < v.length; i++)
107
+ yield* strings(v[i], `${at}/${i}`);
108
+ else if (v && typeof v === "object")
109
+ for (const [k, x] of Object.entries(v)) {
110
+ yield [`${at}/${escape(k)}#key`, k];
111
+ yield* strings(x, `${at}/${escape(k)}`);
112
+ }
113
+ }
114
+ /** What in a spec looks like code, a path, a secret, a personal detail or an internal host. */
115
+ export function leakProblems(spec) {
116
+ const problems = [];
117
+ for (const [at, raw] of strings(spec)) {
118
+ if (EXEMPT.has(at))
119
+ continue;
120
+ // DTCG's own keys ($type, $value, …) are the format's, not the partner's.
121
+ if (at.endsWith("#key") && /^\$[a-z]+$/.test(raw))
122
+ continue;
123
+ // Rule names, parameters and pointers under /logic are CEL identifiers the schema already restricts.
124
+ if (at.endsWith("#key") && at.startsWith("/logic/"))
125
+ continue;
126
+ // A2UI's bindings inside text (${/path}, ${fn(…)}) are the standard's syntax.
127
+ const s = at.startsWith("/surface/") || at.startsWith("/data/views/") ? raw.replace(/\$\{[^}]*\}/g, " ") : raw;
128
+ const codeShaped = CEL_AT.some((r) => r.test(at)) || (STATE_PATH_AT.test(at) && STATE_PATH.test(raw));
129
+ // Web addresses are checked as addresses (hosts, secrets), never as code.
130
+ const withoutUrls = s.replace(/https?:\/\/[^\s"'<>]+/g, " ");
131
+ for (const rule of RULES) {
132
+ if (codeShaped && rule.kind === "code")
133
+ continue;
134
+ if (rule.test(rule.kind === "code" ? withoutUrls : s)) {
135
+ problems.push({ at: at || "/", problem: `looks like ${rule.why} (${rule.kind})` });
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ return problems;
141
+ }
@@ -0,0 +1,36 @@
1
+ import type { Problem } from "./a2ui.js";
2
+ import type { Spec } from "./schema.js";
3
+ type Logic = Spec["logic"];
4
+ type CelValue = unknown;
5
+ export declare const MAX_RANGE = 3660;
6
+ /** JSON in: whole numbers become CEL ints (BigInt). */
7
+ export declare function toCel(v: unknown): CelValue;
8
+ /** CEL out: ints become numbers again, so results are plain JSON. */
9
+ export declare function fromCel(v: CelValue): unknown;
10
+ export declare const dates: {
11
+ addDays: (s: string, n: bigint) => string;
12
+ daysBetween: (a: string, b: string) => bigint;
13
+ /** The Monday that starts s's week. */
14
+ weekStart: (s: string) => string;
15
+ dateRange: (a: string, b: string) => string[];
16
+ };
17
+ /** A cycle among the spec's functions, as names, or null. */
18
+ export declare function cycleIn(functions: Logic["functions"]): string[] | null;
19
+ /** The rules of one spec, ready to evaluate. */
20
+ export declare class Rules {
21
+ private readonly logic;
22
+ private readonly env;
23
+ private readonly consts;
24
+ private depth;
25
+ constructor(logic: Logic);
26
+ private call;
27
+ /** An expression's value, as JSON; null when it can't be worked out. */
28
+ evaluate(cel: string, vars?: Record<string, unknown>): unknown;
29
+ /** A spec function applied to JSON arguments; null when it can't be worked out. */
30
+ apply(name: string, args: unknown[]): unknown;
31
+ /** Why an expression won't parse, or null when it does. */
32
+ parseError(cel: string): string | null;
33
+ }
34
+ /** Problems with a spec's rules, each at its JSON Pointer: expressions that don't parse, a call cycle, vectors the rules don't reproduce. */
35
+ export declare function logicProblems(spec: Pick<Spec, "logic" | "data">): Problem[];
36
+ export {};