@cloudcrane/app-spec 0.4.0 → 0.7.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.
- package/dist/src/blueprint.d.ts +14 -0
- package/dist/src/blueprint.js +56 -3
- package/dist/src/design.d.ts +16 -0
- package/dist/src/design.js +137 -0
- package/dist/src/fidelity.d.ts +17 -0
- package/dist/src/fidelity.js +36 -0
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.js +6 -1
- package/dist/src/leak.js +9 -0
- package/dist/src/logic.js +14 -1
- package/dist/src/memory.d.ts +7 -0
- package/dist/src/memory.js +78 -0
- package/dist/src/page.d.ts +35 -0
- package/dist/src/page.js +72 -0
- package/dist/src/reach.d.ts +7 -0
- package/dist/src/reach.js +102 -0
- package/dist/src/schema.d.ts +211 -9
- package/dist/src/schema.js +54 -2
- package/dist/src/validate.d.ts +1 -0
- package/dist/src/validate.js +13 -5
- package/package.json +1 -1
- package/schema/app-spec.v1.json +129 -1
package/dist/src/schema.js
CHANGED
|
@@ -61,6 +61,8 @@ export const CodedComponent = z
|
|
|
61
61
|
kind: z.literal("coded"),
|
|
62
62
|
/** game: a canvas, a loop and touch, the code a script; page: a whole HTML page, drawn full screen (what an app written as HTML becomes). */
|
|
63
63
|
kit: z.enum(["game", "page"]),
|
|
64
|
+
/** Optional: the native widget or screen it stands in for, like "CupertinoSlidingSegmentedControl", so `blackbox coverage` can say which the catalog lacks. */
|
|
65
|
+
replaces: Text(120).optional(),
|
|
64
66
|
inputs: z.record(CelName, CodedType).default({}),
|
|
65
67
|
events: z.record(CelName, z.record(CelName, CodedType)).default({}),
|
|
66
68
|
code: z
|
|
@@ -80,6 +82,8 @@ export const CodedComponent = z
|
|
|
80
82
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["code"], message: "a game kit's code is at most 64 KB" });
|
|
81
83
|
})
|
|
82
84
|
.refine((c) => Object.keys(c.inputs).length <= 10 && Object.keys(c.events).length <= 10, "at most 10 inputs and 10 events");
|
|
85
|
+
/** The kinds of thing a ported app may not do that its original did. */
|
|
86
|
+
export const GAP_KINDS = ["background", "widget", "notifications", "purchases", "accounts", "sync", "device", "other"];
|
|
83
87
|
export const SpecSchema = z
|
|
84
88
|
.object({
|
|
85
89
|
v: z.literal(1),
|
|
@@ -138,6 +142,11 @@ export const SpecSchema = z
|
|
|
138
142
|
.default([]),
|
|
139
143
|
/** Each step turns version `from` into `to`, as a CEL expression over `saved`. */
|
|
140
144
|
migrations: z.array(z.object({ from: z.number().int().min(1), to: z.number().int().min(2), cel: Cel }).strict()),
|
|
145
|
+
/**
|
|
146
|
+
* What it keeps: the saved fields that are personal (health, money, people's names), as pointers into the saved
|
|
147
|
+
* data, like /days or /people. They stay on the phone; CloudCrane never puts them in a remix, screenshot or report.
|
|
148
|
+
*/
|
|
149
|
+
personal: z.array(Pointer).max(20).optional(),
|
|
141
150
|
})
|
|
142
151
|
.strict(),
|
|
143
152
|
logic: z
|
|
@@ -147,11 +156,21 @@ export const SpecSchema = z
|
|
|
147
156
|
/** Values the screens read, kept up to date: a data-model pointer and the CEL that computes it. */
|
|
148
157
|
computed: z.record(Pointer, Cel).default({}),
|
|
149
158
|
/** Examples each function must reproduce: recorded from the source's own tests, or given. */
|
|
150
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Examples each function must reproduce: `recorded` from the module's own tests, `app` from calls recorded while
|
|
161
|
+
* the original app ran, `hint` written by hand.
|
|
162
|
+
*/
|
|
163
|
+
vectors: z
|
|
164
|
+
.array(z.object({ fn: CelName, in: z.array(z.unknown()).max(8), out: z.unknown(), from: z.enum(["recorded", "app", "hint"]) }).strict())
|
|
165
|
+
.default([]),
|
|
151
166
|
})
|
|
152
167
|
.strict(),
|
|
168
|
+
/**
|
|
169
|
+
* What it can reach: each site, why, and what it's sent, in the person's words ("the city you typed"). Shown to the
|
|
170
|
+
* person before the app opens; a site the code names but this doesn't list is refused.
|
|
171
|
+
*/
|
|
153
172
|
network: z
|
|
154
|
-
.array(z.object({ site: z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/), why: Text(300) }).strict())
|
|
173
|
+
.array(z.object({ site: z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/), why: Text(300), sends: Text(200) }).strict())
|
|
155
174
|
.max(5)
|
|
156
175
|
.default([]),
|
|
157
176
|
/** The device features the app uses, by CloudCrane's names, each once with why; anything not listed is blocked. */
|
|
@@ -159,6 +178,29 @@ export const SpecSchema = z
|
|
|
159
178
|
.array(z.object({ what: z.enum(USE_NAMES), why: Text(300) }).strict())
|
|
160
179
|
.max(USE_NAMES.length)
|
|
161
180
|
.default([]),
|
|
181
|
+
/**
|
|
182
|
+
* What it does: each screen in words, and what it shows when there's nothing yet, while it loads, offline, and when
|
|
183
|
+
* something fails. Optional; without it the blueprint describes the one surface from its words.
|
|
184
|
+
*/
|
|
185
|
+
screens: z
|
|
186
|
+
.array(z
|
|
187
|
+
.object({
|
|
188
|
+
id: Id,
|
|
189
|
+
says: Text(300),
|
|
190
|
+
states: z
|
|
191
|
+
.object({ empty: Text(200).optional(), loading: Text(200).optional(), offline: Text(200).optional(), error: Text(200).optional() })
|
|
192
|
+
.strict()
|
|
193
|
+
.optional(),
|
|
194
|
+
})
|
|
195
|
+
.strict())
|
|
196
|
+
.min(1)
|
|
197
|
+
.max(20)
|
|
198
|
+
.optional(),
|
|
199
|
+
/** How it looks on the phone: the orientation it holds and whether it fills the screen (Android honours both). */
|
|
200
|
+
screen: z
|
|
201
|
+
.object({ orientation: z.enum(["portrait", "landscape", "any"]).optional(), fullscreen: z.boolean().optional() })
|
|
202
|
+
.strict()
|
|
203
|
+
.optional(),
|
|
162
204
|
journeys: z
|
|
163
205
|
.array(z
|
|
164
206
|
.object({
|
|
@@ -176,6 +218,16 @@ export const SpecSchema = z
|
|
|
176
218
|
.record(Id, CodedComponent)
|
|
177
219
|
.refine((c) => Object.keys(c).length <= 5, "at most 5 components")
|
|
178
220
|
.default({}),
|
|
221
|
+
/** The page-kit component that is the whole app: the spec is then that page, compiled to its HTML as written (see page.ts). */
|
|
222
|
+
page: Id.optional(),
|
|
223
|
+
/**
|
|
224
|
+
* What the original app does that this one doesn't, said plainly so no one is promised it: a native feature with
|
|
225
|
+
* no web equivalent yet (background jobs, home-screen widgets, push notifications, in-app purchases...).
|
|
226
|
+
*/
|
|
227
|
+
gaps: z
|
|
228
|
+
.array(z.object({ kind: z.enum(GAP_KINDS), what: Text(120), why: Text(300).optional() }).strict())
|
|
229
|
+
.max(20)
|
|
230
|
+
.optional(),
|
|
179
231
|
/** What a remix may change and what stays as the partner made it. */
|
|
180
232
|
mods: z
|
|
181
233
|
.object({ open: z.array(PointerPattern), locked: z.array(PointerPattern) })
|
package/dist/src/validate.d.ts
CHANGED
package/dist/src/validate.js
CHANGED
|
@@ -3,8 +3,15 @@ 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";
|
|
7
|
+
import { designWarnings, statesWarnings } from "./design.js";
|
|
8
|
+
import { memoryProblems } from "./memory.js";
|
|
9
|
+
import { reachProblems } from "./reach.js";
|
|
10
|
+
import { rulesWarnings } from "./fidelity.js";
|
|
6
11
|
// 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.
|
|
12
|
+
// envelope's shape, then the A2UI surface, then references between parts, what it keeps (memory.ts) and what its code
|
|
13
|
+
// reaches (reach.ts). Problems refuse it. Warnings (how it looks and what each screen shows when empty, offline or
|
|
14
|
+
// failing: design.ts) don't: they say what would make it better.
|
|
8
15
|
const pathOf = (path) => path
|
|
9
16
|
.map((p) => (typeof p === "number" ? `[${p}]` : `.${p}`))
|
|
10
17
|
.join("")
|
|
@@ -12,12 +19,12 @@ const pathOf = (path) => path
|
|
|
12
19
|
export function validateSpec(input, catalog) {
|
|
13
20
|
const parsed = SpecSchema.safeParse(input);
|
|
14
21
|
if (!parsed.success)
|
|
15
|
-
return { spec: null, problems: parsed.error.issues.map((i) => ({ at: pathOf(i.path) || "spec", problem: i.message })) };
|
|
22
|
+
return { spec: null, problems: parsed.error.issues.map((i) => ({ at: pathOf(i.path) || "spec", problem: i.message })), warnings: [] };
|
|
16
23
|
const spec = parsed.data;
|
|
17
24
|
// The surface is checked against the catalog it names (CloudCrane's, or A2UI's basic one).
|
|
18
25
|
const named = catalog ?? catalogFor(spec.surface.catalog);
|
|
19
26
|
if (!named)
|
|
20
|
-
return { spec: null, problems: [{ at: "surface.catalog", problem: "not a catalog this runtime has" }] };
|
|
27
|
+
return { spec: null, problems: [{ at: "surface.catalog", problem: "not a catalog this runtime has" }], warnings: [] };
|
|
21
28
|
const problems = [...surfaceProblems(spec.surface.messages, named)];
|
|
22
29
|
const catalogId = spec.surface.messages[0].createSurface?.catalogId;
|
|
23
30
|
if (catalogId && catalogId !== spec.surface.catalog)
|
|
@@ -46,11 +53,12 @@ export function validateSpec(input, catalog) {
|
|
|
46
53
|
for (const m of spec.data.migrations)
|
|
47
54
|
if (m.to !== m.from + 1 || m.to > spec.data.version)
|
|
48
55
|
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));
|
|
56
|
+
problems.push(...codedProblems(spec), ...pageProblems(spec), ...logicProblems(spec), ...leakProblems(spec), ...memoryProblems(spec), ...reachProblems(spec));
|
|
50
57
|
for (const locked of spec.mods.locked)
|
|
51
58
|
if (spec.mods.open.includes(locked))
|
|
52
59
|
problems.push({ at: `mods ${locked}`, problem: "both open and locked" });
|
|
53
|
-
|
|
60
|
+
const warnings = [...designWarnings(spec), ...statesWarnings(spec), ...rulesWarnings(spec)];
|
|
61
|
+
return { spec: problems.length ? null : spec, problems, warnings };
|
|
54
62
|
}
|
|
55
63
|
/** Each Coded placement names a component the spec defines, and only its declared inputs and events; each component is placed. */
|
|
56
64
|
function codedProblems(spec) {
|
package/package.json
CHANGED
package/schema/app-spec.v1.json
CHANGED
|
@@ -204,6 +204,14 @@
|
|
|
204
204
|
],
|
|
205
205
|
"additionalProperties": false
|
|
206
206
|
}
|
|
207
|
+
},
|
|
208
|
+
"personal": {
|
|
209
|
+
"type": "array",
|
|
210
|
+
"items": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"pattern": "^(\\/([^~/]|~[01])*)*$"
|
|
213
|
+
},
|
|
214
|
+
"maxItems": 20
|
|
207
215
|
}
|
|
208
216
|
},
|
|
209
217
|
"required": [
|
|
@@ -293,6 +301,7 @@
|
|
|
293
301
|
"type": "string",
|
|
294
302
|
"enum": [
|
|
295
303
|
"recorded",
|
|
304
|
+
"app",
|
|
296
305
|
"hint"
|
|
297
306
|
]
|
|
298
307
|
}
|
|
@@ -322,11 +331,17 @@
|
|
|
322
331
|
"type": "string",
|
|
323
332
|
"minLength": 1,
|
|
324
333
|
"maxLength": 300
|
|
334
|
+
},
|
|
335
|
+
"sends": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"minLength": 1,
|
|
338
|
+
"maxLength": 200
|
|
325
339
|
}
|
|
326
340
|
},
|
|
327
341
|
"required": [
|
|
328
342
|
"site",
|
|
329
|
-
"why"
|
|
343
|
+
"why",
|
|
344
|
+
"sends"
|
|
330
345
|
],
|
|
331
346
|
"additionalProperties": false
|
|
332
347
|
},
|
|
@@ -367,6 +382,73 @@
|
|
|
367
382
|
"maxItems": 9,
|
|
368
383
|
"default": []
|
|
369
384
|
},
|
|
385
|
+
"screens": {
|
|
386
|
+
"type": "array",
|
|
387
|
+
"items": {
|
|
388
|
+
"type": "object",
|
|
389
|
+
"properties": {
|
|
390
|
+
"id": {
|
|
391
|
+
"type": "string",
|
|
392
|
+
"pattern": "^[a-z][a-zA-Z0-9_.-]{0,63}$"
|
|
393
|
+
},
|
|
394
|
+
"says": {
|
|
395
|
+
"type": "string",
|
|
396
|
+
"minLength": 1,
|
|
397
|
+
"maxLength": 300
|
|
398
|
+
},
|
|
399
|
+
"states": {
|
|
400
|
+
"type": "object",
|
|
401
|
+
"properties": {
|
|
402
|
+
"empty": {
|
|
403
|
+
"type": "string",
|
|
404
|
+
"minLength": 1,
|
|
405
|
+
"maxLength": 200
|
|
406
|
+
},
|
|
407
|
+
"loading": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"minLength": 1,
|
|
410
|
+
"maxLength": 200
|
|
411
|
+
},
|
|
412
|
+
"offline": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"minLength": 1,
|
|
415
|
+
"maxLength": 200
|
|
416
|
+
},
|
|
417
|
+
"error": {
|
|
418
|
+
"type": "string",
|
|
419
|
+
"minLength": 1,
|
|
420
|
+
"maxLength": 200
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
"additionalProperties": false
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
"required": [
|
|
427
|
+
"id",
|
|
428
|
+
"says"
|
|
429
|
+
],
|
|
430
|
+
"additionalProperties": false
|
|
431
|
+
},
|
|
432
|
+
"minItems": 1,
|
|
433
|
+
"maxItems": 20
|
|
434
|
+
},
|
|
435
|
+
"screen": {
|
|
436
|
+
"type": "object",
|
|
437
|
+
"properties": {
|
|
438
|
+
"orientation": {
|
|
439
|
+
"type": "string",
|
|
440
|
+
"enum": [
|
|
441
|
+
"portrait",
|
|
442
|
+
"landscape",
|
|
443
|
+
"any"
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
"fullscreen": {
|
|
447
|
+
"type": "boolean"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"additionalProperties": false
|
|
451
|
+
},
|
|
370
452
|
"journeys": {
|
|
371
453
|
"type": "array",
|
|
372
454
|
"items": {
|
|
@@ -466,6 +548,11 @@
|
|
|
466
548
|
"page"
|
|
467
549
|
]
|
|
468
550
|
},
|
|
551
|
+
"replaces": {
|
|
552
|
+
"type": "string",
|
|
553
|
+
"minLength": 1,
|
|
554
|
+
"maxLength": 120
|
|
555
|
+
},
|
|
469
556
|
"inputs": {
|
|
470
557
|
"type": "object",
|
|
471
558
|
"additionalProperties": {
|
|
@@ -524,6 +611,47 @@
|
|
|
524
611
|
},
|
|
525
612
|
"default": {}
|
|
526
613
|
},
|
|
614
|
+
"page": {
|
|
615
|
+
"type": "string",
|
|
616
|
+
"pattern": "^[a-z][a-zA-Z0-9_.-]{0,63}$"
|
|
617
|
+
},
|
|
618
|
+
"gaps": {
|
|
619
|
+
"type": "array",
|
|
620
|
+
"items": {
|
|
621
|
+
"type": "object",
|
|
622
|
+
"properties": {
|
|
623
|
+
"kind": {
|
|
624
|
+
"type": "string",
|
|
625
|
+
"enum": [
|
|
626
|
+
"background",
|
|
627
|
+
"widget",
|
|
628
|
+
"notifications",
|
|
629
|
+
"purchases",
|
|
630
|
+
"accounts",
|
|
631
|
+
"sync",
|
|
632
|
+
"device",
|
|
633
|
+
"other"
|
|
634
|
+
]
|
|
635
|
+
},
|
|
636
|
+
"what": {
|
|
637
|
+
"type": "string",
|
|
638
|
+
"minLength": 1,
|
|
639
|
+
"maxLength": 120
|
|
640
|
+
},
|
|
641
|
+
"why": {
|
|
642
|
+
"type": "string",
|
|
643
|
+
"minLength": 1,
|
|
644
|
+
"maxLength": 300
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
"required": [
|
|
648
|
+
"kind",
|
|
649
|
+
"what"
|
|
650
|
+
],
|
|
651
|
+
"additionalProperties": false
|
|
652
|
+
},
|
|
653
|
+
"maxItems": 20
|
|
654
|
+
},
|
|
527
655
|
"mods": {
|
|
528
656
|
"type": "object",
|
|
529
657
|
"properties": {
|