@cloudcrane/app-spec 0.4.0 → 0.5.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.
@@ -8,3 +8,4 @@ export { leakProblems } from "./leak.js";
8
8
  export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
9
9
  export { deriveBlueprint, type BlueprintV1 } from "./blueprint.js";
10
10
  export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf, type Use } from "./uses.js";
11
+ export { pageOf, pageProblems, wrapPage, type PageApp } from "./page.js";
package/dist/src/index.js CHANGED
@@ -10,3 +10,4 @@ export { leakProblems } from "./leak.js";
10
10
  export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
11
11
  export { deriveBlueprint } from "./blueprint.js";
12
12
  export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf } from "./uses.js";
13
+ export { pageOf, pageProblems, wrapPage } from "./page.js";
@@ -0,0 +1,35 @@
1
+ import type { Spec } from "./schema.js";
2
+ /** The spec as the page it is, or null when it isn't one. */
3
+ export declare function pageOf(spec: Spec): string | null;
4
+ type Problem = {
5
+ at: string;
6
+ problem: string;
7
+ };
8
+ /** What a spec that says it's a page may hold: the page, full screen, and no logic of its own. */
9
+ export declare function pageProblems(spec: Spec): Problem[];
10
+ /** What an HTML app says about itself, to make its spec from. */
11
+ export type PageApp = {
12
+ id: string;
13
+ name: string;
14
+ purpose: string;
15
+ html: string;
16
+ /** What made the spec, like "cloudcrane, from an HTML app" (the leak scan refuses one shaped like a call). */
17
+ generator: string;
18
+ /** The runtime version the spec is for; a page needs none, but the spec names the compiler's. */
19
+ runtime?: string;
20
+ data: {
21
+ version: number;
22
+ key: string;
23
+ };
24
+ journeys: Spec["journeys"];
25
+ promises?: Spec["promises"];
26
+ network?: Spec["network"];
27
+ uses?: Spec["uses"];
28
+ tests?: Spec["tests"];
29
+ icon?: string;
30
+ category?: string;
31
+ tags?: string[];
32
+ };
33
+ /** An HTML app as a spec whose one component is the page, full screen. Check it with validateSpec. */
34
+ export declare function wrapPage(app: PageApp): Record<string, unknown>;
35
+ export {};
@@ -0,0 +1,72 @@
1
+ import { CLOUDCRANE_CATALOG_ID } from "./catalog.js";
2
+ // An app written as one HTML page, as a spec. The page is its one page-kit
3
+ // component, and `page` names it: the spec is then the page, and the compiler
4
+ // gives back its HTML as written, with no runtime. It runs, saves and is
5
+ // tested exactly as it would on its own (the same storage keys, the same
6
+ // address, the same tests), while it's described, versioned and checked like
7
+ // any spec. Without `page`, the same component runs in a frame among the
8
+ // spec's own screens.
9
+ /** The spec as the page it is, or null when it isn't one. */
10
+ export function pageOf(spec) {
11
+ if (!spec.page)
12
+ return null;
13
+ const c = spec.components[spec.page];
14
+ return c?.kit === "page" ? c.code : null;
15
+ }
16
+ /** What a spec that says it's a page may hold: the page, full screen, and no logic of its own. */
17
+ export function pageProblems(spec) {
18
+ if (!spec.page)
19
+ return [];
20
+ const c = spec.components[spec.page];
21
+ if (c?.kit !== "page")
22
+ return [{ at: "page", problem: "names no page-kit component" }];
23
+ const problems = [];
24
+ if (Object.keys(spec.components).length !== 1)
25
+ problems.push({ at: "page", problem: "the page is the spec's only component" });
26
+ if (Object.keys(c.inputs).length || Object.keys(c.events).length)
27
+ problems.push({ at: "page", problem: "the page takes no inputs and sends no events" });
28
+ const placed = spec.surface.messages.flatMap((m) => (m["updateComponents"]?.components ?? []));
29
+ if (placed.length !== 1 || placed[0]["component"] !== "Coded" || placed[0]["use"] !== spec.page)
30
+ problems.push({ at: "page", problem: "the screen can hold nothing but the page" });
31
+ const { functions, computed, vectors } = spec.logic;
32
+ if (Object.keys(functions).length || Object.keys(computed).length || vectors.length || spec.data.views.length || spec.data.migrations.length)
33
+ problems.push({ at: "page", problem: "a page has no logic, views or migrations of the spec's own: its code keeps its data" });
34
+ return problems;
35
+ }
36
+ // A page draws itself, so these only satisfy the spec's shape; they're never shown.
37
+ const TOKENS = {
38
+ color: { $type: "color", background: { $value: "#ffffff" }, "on-background": { $value: "#111111" }, primary: { $value: "#0f766e" } },
39
+ };
40
+ /** An HTML app as a spec whose one component is the page, full screen. Check it with validateSpec. */
41
+ export function wrapPage(app) {
42
+ return {
43
+ v: 1,
44
+ manifest: { runtime: app.runtime ?? "0.0.0", generator: app.generator, source: { fingerprint: `sha256:${"0".repeat(64)}` } },
45
+ module: {
46
+ id: app.id,
47
+ name: app.name,
48
+ purpose: app.purpose,
49
+ ...(app.icon ? { icon: app.icon } : {}),
50
+ ...(app.category ? { category: app.category } : {}),
51
+ ...(app.tags?.length ? { tags: app.tags } : {}),
52
+ },
53
+ tokens: TOKENS,
54
+ surface: {
55
+ catalog: CLOUDCRANE_CATALOG_ID,
56
+ messages: [
57
+ { version: "v0.9.1", createSurface: { surfaceId: app.id, catalogId: CLOUDCRANE_CATALOG_ID } },
58
+ { version: "v0.9.1", updateComponents: { surfaceId: app.id, components: [{ id: "root", component: "Coded", use: "page", label: app.name }] } },
59
+ ],
60
+ },
61
+ data: { version: app.data.version, key: app.data.key, persist: "/saved", views: [], schema: { type: "object" }, initial: {}, migrations: [] },
62
+ logic: { consts: {}, functions: {}, computed: {}, vectors: [] },
63
+ network: app.network ?? [],
64
+ uses: app.uses ?? [],
65
+ journeys: app.journeys,
66
+ promises: app.promises ?? [],
67
+ tests: app.tests ?? [],
68
+ components: { page: { kind: "coded", kit: "page", inputs: {}, events: {}, code: app.html } },
69
+ page: "page",
70
+ mods: { open: [], locked: [] },
71
+ };
72
+ }
@@ -377,6 +377,8 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
377
377
  inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
378
378
  events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
379
379
  }>>>;
380
+ /** The page-kit component that is the whole app: the spec is then that page, compiled to its HTML as written (see page.ts). */
381
+ page: z.ZodOptional<z.ZodString>;
380
382
  /** What a remix may change and what stays as the partner made it. */
381
383
  mods: z.ZodDefault<z.ZodObject<{
382
384
  open: z.ZodArray<z.ZodString, "many">;
@@ -473,6 +475,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
473
475
  open: string[];
474
476
  locked: string[];
475
477
  };
478
+ page?: string | undefined;
476
479
  }, {
477
480
  surface: {
478
481
  catalog: string;
@@ -540,6 +543,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
540
543
  inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
541
544
  events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
542
545
  }> | undefined;
546
+ page?: string | undefined;
543
547
  network?: {
544
548
  site: string;
545
549
  why: string;
@@ -643,6 +647,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
643
647
  open: string[];
644
648
  locked: string[];
645
649
  };
650
+ page?: string | undefined;
646
651
  }, {
647
652
  surface: {
648
653
  catalog: string;
@@ -710,6 +715,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
710
715
  inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
711
716
  events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
712
717
  }> | undefined;
718
+ page?: string | undefined;
713
719
  network?: {
714
720
  site: string;
715
721
  why: string;
@@ -176,6 +176,8 @@ export const SpecSchema = z
176
176
  .record(Id, CodedComponent)
177
177
  .refine((c) => Object.keys(c).length <= 5, "at most 5 components")
178
178
  .default({}),
179
+ /** The page-kit component that is the whole app: the spec is then that page, compiled to its HTML as written (see page.ts). */
180
+ page: Id.optional(),
179
181
  /** What a remix may change and what stays as the partner made it. */
180
182
  mods: z
181
183
  .object({ open: z.array(PointerPattern), locked: z.array(PointerPattern) })
@@ -3,6 +3,7 @@ import { catalogFor } from "./catalog.js";
3
3
  import { leakProblems } from "./leak.js";
4
4
  import { logicProblems } from "./logic.js";
5
5
  import { SpecSchema } from "./schema.js";
6
+ import { pageProblems } from "./page.js";
6
7
  // Everything wrong with a spec, collected rather than stopping at the first: the
7
8
  // envelope's shape, then the A2UI surface, then references between parts.
8
9
  const pathOf = (path) => path
@@ -46,7 +47,7 @@ export function validateSpec(input, catalog) {
46
47
  for (const m of spec.data.migrations)
47
48
  if (m.to !== m.from + 1 || m.to > spec.data.version)
48
49
  problems.push({ at: `data.migrations ${m.from}`, problem: "steps go one version at a time, up to data.version" });
49
- problems.push(...codedProblems(spec), ...logicProblems(spec), ...leakProblems(spec));
50
+ problems.push(...codedProblems(spec), ...pageProblems(spec), ...logicProblems(spec), ...leakProblems(spec));
50
51
  for (const locked of spec.mods.locked)
51
52
  if (spec.mods.open.includes(locked))
52
53
  problems.push({ at: `mods ${locked}`, problem: "both open and locked" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcrane/app-spec",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The CloudCrane app spec: its schema, the expression language its rules are written in, and the checks that keep partner code out of it.",
5
5
  "keywords": [
6
6
  "cloudcrane",
@@ -524,6 +524,10 @@
524
524
  },
525
525
  "default": {}
526
526
  },
527
+ "page": {
528
+ "type": "string",
529
+ "pattern": "^[a-z][a-zA-Z0-9_.-]{0,63}$"
530
+ },
527
531
  "mods": {
528
532
  "type": "object",
529
533
  "properties": {