@graview/core 0.1.41 → 0.1.42

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 (104) hide show
  1. package/dist/capabilities.js +1 -1
  2. package/dist/capabilities.js.map +1 -1
  3. package/dist/cli/check/brand.d.ts.map +1 -1
  4. package/dist/cli/check/brand.js +39 -2
  5. package/dist/cli/check/brand.js.map +1 -1
  6. package/dist/cli/document-schema.d.ts.map +1 -1
  7. package/dist/cli/document-schema.js +8 -5
  8. package/dist/cli/document-schema.js.map +1 -1
  9. package/dist/document/brand-check.d.ts +2 -0
  10. package/dist/document/brand-check.d.ts.map +1 -1
  11. package/dist/document/brand-check.js +8 -1
  12. package/dist/document/brand-check.js.map +1 -1
  13. package/dist/document/brand.d.ts.map +1 -1
  14. package/dist/document/brand.js +3 -2
  15. package/dist/document/brand.js.map +1 -1
  16. package/dist/document/connection-spec.d.ts +2 -2
  17. package/dist/document/connection-spec.d.ts.map +1 -1
  18. package/dist/document/connection-spec.js +40 -34
  19. package/dist/document/connection-spec.js.map +1 -1
  20. package/dist/document/describe-place.d.ts.map +1 -1
  21. package/dist/document/describe-place.js +3 -2
  22. package/dist/document/describe-place.js.map +1 -1
  23. package/dist/document/diff.d.ts.map +1 -1
  24. package/dist/document/diff.js +7 -2
  25. package/dist/document/diff.js.map +1 -1
  26. package/dist/document/edit.d.ts.map +1 -1
  27. package/dist/document/edit.js +150 -128
  28. package/dist/document/edit.js.map +1 -1
  29. package/dist/document/graview-template.d.ts +97 -43
  30. package/dist/document/graview-template.d.ts.map +1 -1
  31. package/dist/document/graview-template.js +77 -66
  32. package/dist/document/graview-template.js.map +1 -1
  33. package/dist/document/logo.d.ts +12 -0
  34. package/dist/document/logo.d.ts.map +1 -0
  35. package/dist/document/logo.js +36 -0
  36. package/dist/document/logo.js.map +1 -0
  37. package/dist/document/schema.d.ts +51 -57
  38. package/dist/document/schema.d.ts.map +1 -1
  39. package/dist/document/schema.js +372 -338
  40. package/dist/document/schema.js.map +1 -1
  41. package/dist/document.schema.json +30 -2
  42. package/dist/history.d.ts +32 -3
  43. package/dist/history.d.ts.map +1 -1
  44. package/dist/history.js.map +1 -1
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/installation.d.ts +1 -1
  50. package/dist/installation.d.ts.map +1 -1
  51. package/dist/installation.js +1 -1
  52. package/dist/installation.js.map +1 -1
  53. package/dist/mutations/node-ref.d.ts +1 -1
  54. package/dist/mutations/node-ref.d.ts.map +1 -1
  55. package/dist/mutations/node-ref.js +1 -1
  56. package/dist/mutations/node-ref.js.map +1 -1
  57. package/dist/ops/revisions.d.ts +19 -4
  58. package/dist/ops/revisions.d.ts.map +1 -1
  59. package/dist/ops/revisions.js +23 -8
  60. package/dist/ops/revisions.js.map +1 -1
  61. package/dist/past.d.ts +15 -0
  62. package/dist/past.d.ts.map +1 -1
  63. package/dist/past.js +148 -18
  64. package/dist/past.js.map +1 -1
  65. package/dist/schema/app-link-field.d.ts +1 -1
  66. package/dist/schema/app-link-field.d.ts.map +1 -1
  67. package/dist/schema/app-link-field.js +1 -1
  68. package/dist/schema/app-link-field.js.map +1 -1
  69. package/dist/schema/file-field.d.ts +1 -1
  70. package/dist/schema/file-field.d.ts.map +1 -1
  71. package/dist/schema/file-field.js +1 -1
  72. package/dist/schema/file-field.js.map +1 -1
  73. package/dist/schema/linked-field.d.ts +1 -1
  74. package/dist/schema/linked-field.d.ts.map +1 -1
  75. package/dist/schema/linked-field.js +1 -1
  76. package/dist/schema/linked-field.js.map +1 -1
  77. package/dist/schema/markdown-field.d.ts +1 -1
  78. package/dist/schema/markdown-field.d.ts.map +1 -1
  79. package/dist/schema/markdown-field.js +1 -1
  80. package/dist/schema/markdown-field.js.map +1 -1
  81. package/dist/schema/repeat-field.d.ts +1 -1
  82. package/dist/schema/repeat-field.d.ts.map +1 -1
  83. package/dist/schema/repeat-field.js +1 -1
  84. package/dist/schema/repeat-field.js.map +1 -1
  85. package/dist/schema/typed-field-builders.d.ts +1 -1
  86. package/dist/schema/typed-field-builders.d.ts.map +1 -1
  87. package/dist/schema/typed-field-builders.js +1 -1
  88. package/dist/schema/typed-field-builders.js.map +1 -1
  89. package/dist/temporal/effectivity.d.ts +1 -1
  90. package/dist/temporal/effectivity.d.ts.map +1 -1
  91. package/dist/temporal/effectivity.js +1 -1
  92. package/dist/temporal/effectivity.js.map +1 -1
  93. package/dist/testing.js +1 -1
  94. package/dist/testing.js.map +1 -1
  95. package/dist/theme/marks.d.ts +13 -2
  96. package/dist/theme/marks.d.ts.map +1 -1
  97. package/dist/theme/marks.js +16 -0
  98. package/dist/theme/marks.js.map +1 -1
  99. package/dist/theme/types.d.ts +17 -2
  100. package/dist/theme/types.d.ts.map +1 -1
  101. package/dist/theme/types.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +1 -1
@@ -21,367 +21,401 @@ export const NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
21
21
  /** Field names are identifiers so a rule can say them bare: `quote != null`. */
22
22
  export const FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
23
23
  const RESERVED_FIELDS = new Set(["id", "kind"]);
24
- /**
25
- * What every object already answers to — `constructor`, `toString` and the
26
- * rest of `Object.prototype` — is no field's or relation's name: a record's
27
- * fields and a kind's relations are looked up by name, and one of these is
28
- * "there" on every record whether it was declared or not.
29
- */
30
- const inherited = (n) => n in Object.prototype;
31
- const notInherited = z.refine((n) => !inherited(n), "every object already has this name — choose another");
32
- /** At least `min` and at most `max` long: a string's characters, a list's items. */
33
- const sized = (min, max) => [...(min === undefined ? [] : [z.minLength(min)]), ...(max === undefined ? [] : [z.maxLength(max)])];
34
- const str = (min, max) => z.string().check(...sized(min, max));
35
- const some = (of, min, max) => z.array(of).check(...sized(min, max));
36
- const name = (what) => z.string().check(z.regex(NAME, `${what} names are kebab-case, lower-case words joined by hyphens like "tool-note" (field and relation names are camelCase, like "supersededBy")`));
37
24
  /**
38
25
  * A RELATION'S NAME may be kebab-case or one camelCase word. camelCase is
39
26
  * recommended: a rule can then walk it bare (`tendedBy.name`), where
40
27
  * `tended-by.name` would read as a subtraction.
41
28
  */
42
29
  export const EDGE_NAME = /^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[a-z][A-Za-z0-9]*)$/;
43
- const edgeName = z.string().check(z.regex(EDGE_NAME, 'relation names are one camelCase word, like "supersededBy" (kind names are kebab-case, like "tool-note")'), notInherited);
44
- const fieldName = z
45
- .string()
46
- .check(z.regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate", so rules say them bare (kind names are kebab-case, like "tool-note")'), z.refine((n) => !RESERVED_FIELDS.has(n), 'a field cannot be called "id" or "kind" — the framework keeps those; try "type" or "category"'), notInherited);
47
- const sentence = str(1, 500);
48
- const expression = str(1, 2000);
49
- const template = str(1, 300);
50
30
  /**
51
31
  * `image` and `file` hold what the host stored (FR-162): a `FileValue`, its
52
32
  * `href` a path on the host. `phone` and `place` are words drawn as a link to
53
33
  * call and to a map; `duration` is a whole number of minutes, said "1 h 30 min".
54
34
  */
55
35
  export const FIELD_TYPES = ["string", "text", "number", "integer", "boolean", "date", "datetime", "enum", "list", "url", "email", "image", "video", "file", "app-link", "phone", "place", "duration"];
56
- export const FieldSpec = z
57
- .strictObject({
58
- type: z.enum(FIELD_TYPES),
59
- required: z.optional(z.boolean()),
60
- description: z.optional(sentence),
61
- /** What the field is called on screen, where its name does not say it. */
62
- label: z.optional(str(1, 60)),
63
- options: z.optional(some(str(1, 80), 1, 100)),
64
- of: z.optional(z.enum(["string", "number", "date", "image", "video", "file"])),
65
- default: z.optional(z.unknown()),
66
- unit: z.optional(str(undefined, 12)),
67
- /** How a number is shown (`money`, `percent`, `duration`), or that a `text` field is written in Markdown (FR-180; the checker's `field-format` says which goes where). */
68
- format: z.optional(z.enum(["money", "percent", "duration", "markdown"])),
69
- /** A number's range (FR-114): the least and the most it may be, and the step its values are whole multiples of (JSON Schema's `multipleOf`). */
70
- min: z.optional(z.number()),
71
- max: z.optional(z.number()),
72
- step: z.optional(z.number()),
73
- /** The name this field had in the previous version: its values move instead of being dropped. */
74
- renamedFrom: z.optional(z.string().check(z.regex(FIELD_NAME))),
75
- /**
76
- * A date that repeats (`repeat.ts`): a record may keep, beside the date
77
- * (its first occurrence), how it repeats under the date's name and
78
- * `Repeat` — `"startsAtRepeat": { "every": "week", "on": ["tue"] }`.
79
- */
80
- repeats: z.optional(z.boolean()),
81
- })
82
- .check(z.superRefine((f, ctx) => {
83
- if (f.type === "enum" && !f.options)
84
- ctx.addIssue({ code: "custom", message: 'an enum field lists its "options"', path: ["options"] });
85
- if (f.type !== "enum" && f.options)
86
- ctx.addIssue({ code: "custom", message: '"options" belongs only on an enum field', path: ["options"] });
87
- if (f.type !== "list" && f.of)
88
- ctx.addIssue({ code: "custom", message: '"of" belongs only on a list field', path: ["of"] });
89
- if (f.repeats && f.type !== "date" && f.type !== "datetime")
90
- ctx.addIssue({ code: "custom", message: '"repeats" belongs only on a date or datetime field', path: ["repeats"] });
91
- }));
92
- /**
93
- * WHERE A RELATION GOES: a kind of this document (`"house"`), or a kind in
94
- * ANOTHER APP the viewer can open (FR-171) — `@` + the app's address + `/`
95
- * + its kind: `"@nick/house/house"`, `"@app_kf5eydr9z58jzf/house"`. A link
96
- * to another app's record picks one there and leaves that app untouched.
36
+ /*
37
+ * THE SHAPES ARE BUILT ON FIRST USE. Some hundred and fifty zod schemas make
38
+ * up the document's shape, and building them is work a Worker's cold isolate
39
+ * or a page would do the moment it imported `@graview/core/document`, whether
40
+ * or not it ever read a document. Each exported shape is `z.lazy` over the one
41
+ * set `build` makes the first time any of them parses: the same names, the
42
+ * same parse, the same issues, and nothing done at import.
97
43
  */
98
- const edgeTarget = z.string().check(z.regex(new RegExp(`${NAME.source}|${LINKED_TARGET.source}`), 'a relation goes to a kind of this document, like "house", or of another app, like "@nick/house/house"'));
99
- export const EdgeSpec = z.strictObject({
100
- to: z.union([some(edgeTarget, 1), z.literal("*")]),
101
- cardinality: z.optional(z.enum(["one", "many"])),
102
- description: z.optional(sentence),
103
- inverse: z.optional(str(1, 80)),
104
- appendOnly: z.optional(z.boolean()),
105
- /** The name this relation had in the previous version: its links move. */
106
- renamedFrom: z.optional(z.string()),
107
- /** `"never"`: no automated author makes or breaks this relation on its own (`@graview/core/tidying`). */
108
- tidying: z.optional(z.literal("never")),
109
- });
110
- /** A computed field: its expression, or the expression with the words it is shown by (FR-83). */
111
- export const ComputedSpec = z.union([expression, z.strictObject({ expr: expression, label: z.optional(str(1, 60)), description: z.optional(sentence) })]);
112
- export const KindSpec = z.strictObject({
113
- noun: z.optional(str(1, 40)),
114
- plural: z.optional(str(1, 40)),
115
- description: z.optional(sentence),
116
- fields: z.record(fieldName, FieldSpec).check(z.refine((f) => Object.keys(f).length > 0, "a kind has at least one field")),
117
- label: z.optional(template),
118
- describe: z.optional(template),
119
- lifecycle: z.optional(z.strictObject({ field: fieldName, retired: some(z.union([z.string(), z.number(), z.boolean()]), 1) })),
120
- figure: z.optional(str(undefined, 20_000)),
121
- /** What a glance at one says — a card, a row, a hit in Find — first, in this order: the definition's `display.glance` (FR-39). */
122
- glance: z.optional(some(fieldName, 1, 20)),
44
+ function build() {
123
45
  /**
124
- * How a record's page orders and groups its facts (FR-148): `fields`
125
- * first, in this order, then each group under its title; the rest follow
126
- * in declared order, under "Details" when there are groups. The
127
- * definition's `display.page`.
46
+ * What every object already answers to — `constructor`, `toString` and the
47
+ * rest of `Object.prototype` — is no field's or relation's name: a record's
48
+ * fields and a kind's relations are looked up by name, and one of these is
49
+ * "there" on every record whether it was declared or not.
128
50
  */
129
- page: z.optional(z.strictObject({
130
- fields: z.optional(some(fieldName, 1, 40)),
131
- groups: z.optional(some(z.strictObject({ title: str(1, 60), fields: some(fieldName, 1, 40) }), 1, 12)),
132
- })),
133
- edges: z.optional(z.record(edgeName, EdgeSpec)),
51
+ const inherited = (n) => n in Object.prototype;
52
+ const notInherited = z.refine((n) => !inherited(n), "every object already has this name — choose another");
53
+ /** At least `min` and at most `max` long: a string's characters, a list's items. */
54
+ const sized = (min, max) => [...(min === undefined ? [] : [z.minLength(min)]), ...(max === undefined ? [] : [z.maxLength(max)])];
55
+ const str = (min, max) => z.string().check(...sized(min, max));
56
+ const some = (of, min, max) => z.array(of).check(...sized(min, max));
57
+ const name = (what) => z.string().check(z.regex(NAME, `${what} names are kebab-case, lower-case words joined by hyphens like "tool-note" (field and relation names are camelCase, like "supersededBy")`));
58
+ const edgeName = z.string().check(z.regex(EDGE_NAME, 'relation names are one camelCase word, like "supersededBy" (kind names are kebab-case, like "tool-note")'), notInherited);
59
+ const fieldName = z
60
+ .string()
61
+ .check(z.regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate", so rules say them bare (kind names are kebab-case, like "tool-note")'), z.refine((n) => !RESERVED_FIELDS.has(n), 'a field cannot be called "id" or "kind" — the framework keeps those; try "type" or "category"'), notInherited);
62
+ const sentence = str(1, 500);
63
+ const expression = str(1, 2000);
64
+ const template = str(1, 300);
65
+ const FieldSpec = z
66
+ .strictObject({
67
+ type: z.enum(FIELD_TYPES),
68
+ required: z.optional(z.boolean()),
69
+ description: z.optional(sentence),
70
+ /** What the field is called on screen, where its name does not say it. */
71
+ label: z.optional(str(1, 60)),
72
+ options: z.optional(some(str(1, 80), 1, 100)),
73
+ of: z.optional(z.enum(["string", "number", "date", "image", "video", "file"])),
74
+ default: z.optional(z.unknown()),
75
+ unit: z.optional(str(undefined, 12)),
76
+ /** How a number is shown (`money`, `percent`, `duration`), or that a `text` field is written in Markdown (FR-180; the checker's `field-format` says which goes where). */
77
+ format: z.optional(z.enum(["money", "percent", "duration", "markdown"])),
78
+ /** A number's range (FR-114): the least and the most it may be, and the step its values are whole multiples of (JSON Schema's `multipleOf`). */
79
+ min: z.optional(z.number()),
80
+ max: z.optional(z.number()),
81
+ step: z.optional(z.number()),
82
+ /** The name this field had in the previous version: its values move instead of being dropped. */
83
+ renamedFrom: z.optional(z.string().check(z.regex(FIELD_NAME))),
84
+ /**
85
+ * A date that repeats (`repeat.ts`): a record may keep, beside the date
86
+ * (its first occurrence), how it repeats under the date's name and
87
+ * `Repeat` — `"startsAtRepeat": { "every": "week", "on": ["tue"] }`.
88
+ */
89
+ repeats: z.optional(z.boolean()),
90
+ })
91
+ .check(z.superRefine((f, ctx) => {
92
+ if (f.type === "enum" && !f.options)
93
+ ctx.addIssue({ code: "custom", message: 'an enum field lists its "options"', path: ["options"] });
94
+ if (f.type !== "enum" && f.options)
95
+ ctx.addIssue({ code: "custom", message: '"options" belongs only on an enum field', path: ["options"] });
96
+ if (f.type !== "list" && f.of)
97
+ ctx.addIssue({ code: "custom", message: '"of" belongs only on a list field', path: ["of"] });
98
+ if (f.repeats && f.type !== "date" && f.type !== "datetime")
99
+ ctx.addIssue({ code: "custom", message: '"repeats" belongs only on a date or datetime field', path: ["repeats"] });
100
+ }));
134
101
  /**
135
- * Values worked out, not stored (FR-83): a name and a rule-language
136
- * expression read like a field — `"net": "sum(out('includes'), list * units)"` —
137
- * or `{ expr, label?, description? }`. Never written by an act.
102
+ * WHERE A RELATION GOES: a kind of this document (`"house"`), or a kind in
103
+ * ANOTHER APP the viewer can open (FR-171) — `@` + the app's address + `/`
104
+ * + its kind: `"@nick/house/house"`, `"@app_kf5eydr9z58jzf/house"`. A link
105
+ * to another app's record picks one there and leaves that app untouched.
138
106
  */
139
- computed: z.optional(z.record(z.string(), ComputedSpec).check(z.refine((c) => Object.keys(c).length <= 20, "a kind works out at most 20 computed fields"))),
140
- /**
141
- * Which field fills which role (the definition's `fieldRoles`): `when`, a
142
- * record's natural time, which lists open newest first by and a timeline
143
- * or calendar draws at when its lens binds no start; `who`, its author,
144
- * which a glance says; and any a lens binds by name (`start`, `end`,
145
- * `column`, `order`).
146
- */
147
- fieldRoles: z.optional(z.record(z.string(), z.string())),
148
- /** The name this kind had in the previous version: its records move. */
149
- renamedFrom: z.optional(z.string().check(z.regex(NAME))),
150
- /** This kind stands for the people: a record of it whose id is a member's principal id is that member (`$me`, `self`, `own`, `via`, `ownEnd`). */
151
- people: z.optional(z.boolean()),
152
- /** `false`: its records are kept or removed, never archived — the kind is given no `archive-`, `unarchive-` or `merge-<kind>` (FR-173). */
153
- archives: z.optional(z.literal(false)),
154
- /** What the app's own actor may do to its records on its own: `"never"` nothing, `"no-links"` all but link and unlink them (`@graview/core/tidying`). */
155
- tidying: z.optional(z.enum(["never", "no-links"])),
156
- });
157
- /** A value an effect writes: a literal, `$arg`, `$subject`, `$new`, `$now`, `$today`, or `{ expr }`. */
158
- export const ValueSpec = z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.strictObject({ expr: expression })]);
159
- const ref = z.string().check(z.regex(/^\$[A-Za-z][A-Za-z0-9]*$/, 'a reference is "$" and a name, like "$subject" or "$new"'));
160
- export const EffectSpec = z.union([
161
- z.strictObject({ create: name("kind"), as: z.optional(z.string().check(z.regex(FIELD_NAME))), set: z.optional(z.record(fieldName, ValueSpec)) }),
162
- z.strictObject({ set: z.record(fieldName, ValueSpec), target: z.optional(ref) }),
163
- z.strictObject({ connect: edgeName, from: ref, to: ref }),
164
- z.strictObject({ sever: edgeName, from: ref, to: ref }),
165
- z.strictObject({ remove: ref }),
166
- ]);
167
- export const ActSpec = z.strictObject({
168
- title: z.optional(str(1, 80)),
169
- description: z.optional(sentence),
170
- /** How the act reads standing on the far end of the relation it makes or breaks. */
171
- fromTheOtherEnd: z.optional(str(1, 80)),
172
- on: z.optional(z.union([name("kind"), some(name("kind"), 1)])),
173
- creates: z.optional(name("kind")),
174
- sets: z.optional(z.record(fieldName, ValueSpec)),
175
- writes: z.optional(some(fieldName, 1)),
176
- connects: z.optional(edgeName),
177
- severs: z.optional(edgeName),
107
+ const edgeTarget = z.string().check(z.regex(new RegExp(`${NAME.source}|${LINKED_TARGET.source}`), 'a relation goes to a kind of this document, like "house", or of another app, like "@nick/house/house"'));
108
+ const EdgeSpec = z.strictObject({
109
+ to: z.union([some(edgeTarget, 1), z.literal("*")]),
110
+ cardinality: z.optional(z.enum(["one", "many"])),
111
+ description: z.optional(sentence),
112
+ inverse: z.optional(str(1, 80)),
113
+ appendOnly: z.optional(z.boolean()),
114
+ /** The name this relation had in the previous version: its links move. */
115
+ renamedFrom: z.optional(z.string()),
116
+ /** `"never"`: no automated author makes or breaks this relation on its own (`@graview/core/tidying`). */
117
+ tidying: z.optional(z.literal("never")),
118
+ });
119
+ /** A computed field: its expression, or the expression with the words it is shown by (FR-83). */
120
+ const ComputedSpec = z.union([expression, z.strictObject({ expr: expression, label: z.optional(str(1, 60)), description: z.optional(sentence) })]);
121
+ const KindSpec = z.strictObject({
122
+ noun: z.optional(str(1, 40)),
123
+ plural: z.optional(str(1, 40)),
124
+ description: z.optional(sentence),
125
+ fields: z.record(fieldName, FieldSpec).check(z.refine((f) => Object.keys(f).length > 0, "a kind has at least one field")),
126
+ label: z.optional(template),
127
+ describe: z.optional(template),
128
+ lifecycle: z.optional(z.strictObject({ field: fieldName, retired: some(z.union([z.string(), z.number(), z.boolean()]), 1) })),
129
+ figure: z.optional(str(undefined, 20_000)),
130
+ /** What a glance at one says — a card, a row, a hit in Find — first, in this order: the definition's `display.glance` (FR-39). */
131
+ glance: z.optional(some(fieldName, 1, 20)),
132
+ /**
133
+ * How a record's page orders and groups its facts (FR-148): `fields`
134
+ * first, in this order, then each group under its title; the rest follow
135
+ * in declared order, under "Details" when there are groups. The
136
+ * definition's `display.page`.
137
+ */
138
+ page: z.optional(z.strictObject({
139
+ fields: z.optional(some(fieldName, 1, 40)),
140
+ groups: z.optional(some(z.strictObject({ title: str(1, 60), fields: some(fieldName, 1, 40) }), 1, 12)),
141
+ })),
142
+ edges: z.optional(z.record(edgeName, EdgeSpec)),
143
+ /**
144
+ * Values worked out, not stored (FR-83): a name and a rule-language
145
+ * expression read like a field — `"net": "sum(out('includes'), list * units)"` —
146
+ * or `{ expr, label?, description? }`. Never written by an act.
147
+ */
148
+ computed: z.optional(z.record(z.string(), ComputedSpec).check(z.refine((c) => Object.keys(c).length <= 20, "a kind works out at most 20 computed fields"))),
149
+ /**
150
+ * Which field fills which role (the definition's `fieldRoles`): `when`, a
151
+ * record's natural time, which lists open newest first by and a timeline
152
+ * or calendar draws at when its lens binds no start; `who`, its author,
153
+ * which a glance says; and any a lens binds by name (`start`, `end`,
154
+ * `column`, `order`).
155
+ */
156
+ fieldRoles: z.optional(z.record(z.string(), z.string())),
157
+ /** The name this kind had in the previous version: its records move. */
158
+ renamedFrom: z.optional(z.string().check(z.regex(NAME))),
159
+ /** This kind stands for the people: a record of it whose id is a member's principal id is that member (`$me`, `self`, `own`, `via`, `ownEnd`). */
160
+ people: z.optional(z.boolean()),
161
+ /** `false`: its records are kept or removed, never archived — the kind is given no `archive-`, `unarchive-` or `merge-<kind>` (FR-173). */
162
+ archives: z.optional(z.literal(false)),
163
+ /** What the app's own actor may do to its records on its own: `"never"` nothing, `"no-links"` all but link and unlink them (`@graview/core/tidying`). */
164
+ tidying: z.optional(z.enum(["never", "no-links"])),
165
+ });
166
+ /** A value an effect writes: a literal, `$arg`, `$subject`, `$new`, `$now`, `$today`, or `{ expr }`. */
167
+ const ValueSpec = z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.strictObject({ expr: expression })]);
168
+ const ref = z.string().check(z.regex(/^\$[A-Za-z][A-Za-z0-9]*$/, 'a reference is "$" and a name, like "$subject" or "$new"'));
169
+ const EffectSpec = z.union([
170
+ z.strictObject({ create: name("kind"), as: z.optional(z.string().check(z.regex(FIELD_NAME))), set: z.optional(z.record(fieldName, ValueSpec)) }),
171
+ z.strictObject({ set: z.record(fieldName, ValueSpec), target: z.optional(ref) }),
172
+ z.strictObject({ connect: edgeName, from: ref, to: ref }),
173
+ z.strictObject({ sever: edgeName, from: ref, to: ref }),
174
+ z.strictObject({ remove: ref }),
175
+ ]);
176
+ const ActSpec = z.strictObject({
177
+ title: z.optional(str(1, 80)),
178
+ description: z.optional(sentence),
179
+ /** How the act reads standing on the far end of the relation it makes or breaks. */
180
+ fromTheOtherEnd: z.optional(str(1, 80)),
181
+ on: z.optional(z.union([name("kind"), some(name("kind"), 1)])),
182
+ creates: z.optional(name("kind")),
183
+ sets: z.optional(z.record(fieldName, ValueSpec)),
184
+ writes: z.optional(some(fieldName, 1)),
185
+ connects: z.optional(edgeName),
186
+ severs: z.optional(edgeName),
187
+ /**
188
+ * Fields set on the record at the OTHER end of what the act connects or
189
+ * severs — its `$to` (FR-115): `"setsOther": { "ownership": "shared" }`.
190
+ * Values as `sets` takes them; an expression reads that record's fields.
191
+ */
192
+ setsOther: z.optional(z.record(fieldName, ValueSpec)),
193
+ /**
194
+ * The subject's links severed before it connects (FR-115): `true`, those
195
+ * of the relation it connects; or the relations named, each at the
196
+ * subject's own end. The link being made is kept.
197
+ */
198
+ replaces: z.optional(z.union([z.literal(true), some(edgeName, 1, 10)])),
199
+ removes: z.optional(z.literal(true)),
200
+ args: z.optional(z.record(fieldName, FieldSpec)),
201
+ effects: z.optional(some(EffectSpec, 1, 20)),
202
+ allowedWhen: z.optional(expression),
203
+ refusal: z.optional(template),
204
+ destructive: z.optional(z.boolean()),
205
+ });
206
+ const RepairSpec = z.strictObject({
207
+ act: name("act"),
208
+ label: z.optional(str(1, 80)),
209
+ args: z.optional(z.record(z.string(), ValueSpec)),
210
+ });
211
+ const RuleSpec = z.strictObject({
212
+ title: z.optional(str(1, 120)),
213
+ description: z.optional(sentence),
214
+ over: z.union([z.literal("graph"), name("kind")]),
215
+ when: z.optional(expression),
216
+ require: expression,
217
+ says: z.optional(template),
218
+ repairs: z.optional(some(RepairSpec, undefined, 10)),
219
+ });
220
+ const GrantSpec = z.strictObject({
221
+ roles: z.union([some(name("role"), 1), z.literal("*")]),
222
+ mutations: z.union([some(name("act"), 1), z.literal("*")]),
223
+ kinds: z.optional(z.union([z.array(name("kind")), z.literal("*")])),
224
+ describe: z.optional(sentence),
225
+ self: z.optional(z.boolean()),
226
+ /** Only on the principal's own end of the link the act connects or severs. */
227
+ ownEnd: z.optional(z.boolean()),
228
+ });
178
229
  /**
179
- * Fields set on the record at the OTHER end of what the act connects or
180
- * severs — its `$to` (FR-115): `"setsOther": { "ownership": "shared" }`.
181
- * Values as `sets` takes them; an expression reads that record's fields.
230
+ * Who sees which kinds — the framework's `Sight`, one meaning (FR-02).
231
+ * Absent: everyone sees everything. Present: a kind no sight names is seen
232
+ * by nobody but the system (a host that gives its owners everything serves
233
+ * them as the system, or names them in a sight).
182
234
  */
183
- setsOther: z.optional(z.record(fieldName, ValueSpec)),
235
+ const fieldNames = z.optional(some(name("field"), 1));
236
+ const SightSpec = z.strictObject({
237
+ roles: z.union([some(name("role"), 1), z.literal("*")]),
238
+ kinds: some(name("kind"), 1),
239
+ /** Only the principal's own records: the ones they made, their own record, and what an edge joins to it. */
240
+ own: z.optional(z.boolean()),
241
+ /** Only the records this relation joins to the principal's own record of the people kind. */
242
+ via: z.optional(edgeName),
243
+ /** Only these fields of the records it shows (Roadmap f-field-sight); sights add up. */
244
+ fields: fieldNames,
245
+ /** Every field but these of the records it shows: kept from the roles, on every surface, and not theirs to write. */
246
+ hide: fieldNames,
247
+ });
248
+ const PolicySpec = z.strictObject({
249
+ grants: z.array(GrantSpec),
250
+ sees: z.optional(z.array(SightSpec)),
251
+ });
184
252
  /**
185
- * The subject's links severed before it connects (FR-115): `true`, those
186
- * of the relation it connects; or the relations named, each at the
187
- * subject's own end. The link being made is kept.
188
- */
189
- replaces: z.optional(z.union([z.literal(true), some(edgeName, 1, 10)])),
190
- removes: z.optional(z.literal(true)),
191
- args: z.optional(z.record(fieldName, FieldSpec)),
192
- effects: z.optional(some(EffectSpec, 1, 20)),
193
- allowedWhen: z.optional(expression),
194
- refusal: z.optional(template),
195
- destructive: z.optional(z.boolean()),
196
- });
197
- export const RepairSpec = z.strictObject({
198
- act: name("act"),
199
- label: z.optional(str(1, 80)),
200
- args: z.optional(z.record(z.string(), ValueSpec)),
201
- });
202
- export const RuleSpec = z.strictObject({
203
- title: z.optional(str(1, 120)),
204
- description: z.optional(sentence),
205
- over: z.union([z.literal("graph"), name("kind")]),
206
- when: z.optional(expression),
207
- require: expression,
208
- says: z.optional(template),
209
- repairs: z.optional(some(RepairSpec, undefined, 10)),
210
- });
211
- export const GrantSpec = z.strictObject({
212
- roles: z.union([some(name("role"), 1), z.literal("*")]),
213
- mutations: z.union([some(name("act"), 1), z.literal("*")]),
214
- kinds: z.optional(z.union([z.array(name("kind")), z.literal("*")])),
215
- describe: z.optional(sentence),
216
- self: z.optional(z.boolean()),
217
- /** Only on the principal's own end of the link the act connects or severs. */
218
- ownEnd: z.optional(z.boolean()),
219
- });
220
- /**
221
- * Who sees which kinds — the framework's `Sight`, one meaning (FR-02).
222
- * Absent: everyone sees everything. Present: a kind no sight names is seen
223
- * by nobody but the system (a host that gives its owners everything serves
224
- * them as the system, or names them in a sight).
225
- */
226
- const fieldNames = z.optional(some(name("field"), 1));
227
- export const SightSpec = z.strictObject({
228
- roles: z.union([some(name("role"), 1), z.literal("*")]),
229
- kinds: some(name("kind"), 1),
230
- /** Only the principal's own records: the ones they made, their own record, and what an edge joins to it. */
231
- own: z.optional(z.boolean()),
232
- /** Only the records this relation joins to the principal's own record of the people kind. */
233
- via: z.optional(edgeName),
234
- /** Only these fields of the records it shows (Roadmap f-field-sight); sights add up. */
235
- fields: fieldNames,
236
- /** Every field but these of the records it shows: kept from the roles, on every surface, and not theirs to write. */
237
- hide: fieldNames,
238
- });
239
- export const PolicySpec = z.strictObject({
240
- grants: z.array(GrantSpec),
241
- sees: z.optional(z.array(SightSpec)),
242
- });
243
- /**
244
- * The installation's look and its money: an accent color the palette is
245
- * derived from, and the currency and locale a sum is said in wherever a
246
- * block names none (FR-100) — the locale its days, times and numbers are
247
- * said in too (FR-178), "en-US" when absent — and the time zone its
248
- * instants are said in (Roadmap f-app-time-zone), each reader's own clock
249
- * when absent. Any of them may stand alone.
250
- */
251
- export const BrandSpec = z.strictObject({
252
- accent: z.optional(z.string().check(z.regex(/^#[0-9a-fA-F]{6}$/, 'an accent is a color like "#c2577a"'))),
253
- name: z.optional(str(1, 60)),
254
- currency: z.optional(z.string().check(z.regex(/^[A-Z]{3}$/, 'a currency is its three-letter code, like "USD" or "EUR"'))),
255
- // A language tag ("en-US", "de-DE"); `graview check` says when it is none (`brand-locale`).
256
- locale: z.optional(str(2, 35)),
257
- // An IANA time zone ("America/New_York"); `graview check` says when it is none (`brand-time-zone`).
258
- timeZone: z.optional(str(1, 64)),
259
- /*
260
- * THE REST OF THE BRAND (FR-124), each optional and each checked by
261
- * `graview check` (`brand-mark`, `brand-font`): a logo and a page icon —
262
- * inline SVG, or a path on the app's own host — the faces words are set
263
- * in, how round and how roomy, a hue per kind, and the scheme it prefers.
264
- */
265
- logo: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ src: str(1, MARK_MAX), alt: z.optional(str(1, 120)) })])),
266
- // A page icon: a mark as the logo is one, or `{ "emoji": "🏡" }` — one standard emoji, the app's mark where it has no logo (`brand-emoji`).
267
- favicon: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ emoji: z.string() })])),
268
- typography: z.optional(z.strictObject({ display: z.optional(str(1, 200)), body: z.optional(str(1, 200)), mono: z.optional(str(1, 200)) })),
269
- shape: z.optional(z.strictObject({
270
- radius: z.optional(z.number().check(z.minimum(0), z.maximum(32))),
271
- density: z.optional(z.number().check(z.minimum(0.75), z.maximum(1.5))),
272
- })),
273
- accents: z.optional(z.record(z.string(), z.number().check(z.minimum(0), z.maximum(360)))),
274
- scheme: z.optional(z.enum(["light", "dark", "auto"])),
275
- });
276
- const loose = z.record(z.string(), z.unknown());
277
- /** A grid lens's order (`grid-sort.ts`): a field, or { by, direction }. */
278
- const GridSortSpec = z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction: z.optional(z.enum(["asc", "desc"])) })]);
279
- const Year = z.number().check(z.int(), z.minimum(1), z.maximum(9999));
280
- /**
281
- * A lens: checked by `graview check` against the lenses the framework ships
282
- * (`declaredLenses`), so loose here but for what its page is: `create`,
283
- * false where the place's page leaves off the forms that begin its kind —
284
- * and the options whose shape is the same on every lens that takes them:
285
- * a coverage's `sort` and `columnSort` and a status board's `sort` (the
286
- * order of its rows, columns or cards), and a calendar's `horizon` (its
287
- * span of years, reaching its records' first and last unless `from` and
288
- * `to` pin it). `search` and `filters` are fields, here only as lists of
289
- * names: which ones the kind has is the checker's to say, as is a status
290
- * board's `collapsed` and `hidden` columns.
291
- */
292
- const LensSpec = z.looseObject({
293
- create: z.optional(z.boolean()),
294
- options: z.optional(z.looseObject({
295
- sort: z.optional(GridSortSpec),
296
- columnSort: z.optional(GridSortSpec),
297
- search: z.optional(some(str(1, 120), undefined, 40)),
298
- filters: z.optional(some(str(1, 120), undefined, 40)),
299
- horizon: z.optional(z.union([z.number().check(z.int(), z.minimum(1), z.maximum(200)), z.looseObject({ years: z.number().check(z.int(), z.minimum(1), z.maximum(200)), title: str(1, 80), from: z.optional(Year), to: z.optional(Year) })])),
300
- })),
301
- });
302
- /** A kind's view specs: which slots it fills. The blocks are checked in views.ts, sentence by sentence. */
303
- const ViewSpecsSpec = z.strictObject({
304
- card: z.optional(z.array(z.unknown())),
305
- row: z.optional(z.array(z.unknown())),
306
- page: z.optional(z.array(z.unknown())),
307
- });
308
- /** A kind's list's declared order (`pages.lists.<kind>.sort`): a key, or a key with a direction and a tiebreak. */
309
- const ListSortSpec = () => {
310
- const direction = z.optional(z.enum(["asc", "desc"]));
311
- return z.union([
312
- str(1, 120),
313
- z.strictObject({ by: str(1, 120), direction, then: z.optional(z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction })])) }),
314
- ]);
315
- };
316
- /** A kind's list's declared grouping (`pages.lists.<kind>.group`): a field, or a field with its groups' order and the closed ones. */
317
- const ListGroupSpec = () => z.union([str(1, 120), z.strictObject({ by: str(1, 120), order: z.optional(some(str(1, 80), undefined, 40)), closed: z.optional(some(str(1, 80), undefined, 40)) })]);
318
- /**
319
- * THE ARRANGEMENT (FR-80): the kinds in order, the kinds the home leaves
320
- * off, the place the app opens on. Loose enough that a document which
321
- * carried anything else under `pages` before FR-80 — when it was accepted
322
- * and ignored — still parses; `graview check` says what it cannot honor.
323
- */
324
- export const PagesSpec = z.looseObject({
325
- order: z.optional(z.array(z.string())),
326
- hide: z.optional(z.array(z.string())),
327
- first: z.optional(z.string()),
328
- /* The places that stand on the bar beside Home (FR-145, ranked): kinds by name or plural, pictures by title. */
329
- primary: z.optional(some(str(1, 80), undefined, 40)),
330
- /*
331
- * What the bar's switch calls the scene and the pages (FR-137); "Scene" and "Pages" when unsaid.
332
- * At most 40 characters, as `pages.overview` was (FR-132), which a stored document says for `scene`
333
- * (RESPELLED): a respelling never narrows what the key may hold. The bar cuts a long word, whole on hover.
334
- */
335
- scene: z.optional(str(1, 40)),
336
- pages: z.optional(str(1, 40)),
337
- /*
338
- * What each kind's list offers to filter by, by kind (`pages.lists.request.filters`): fields, relations,
339
- * `is`, and `<relation>.<field>` for a choice at a relation's far end. Its choice fields stand as chips.
253
+ * The installation's look and its money: an accent color the palette is
254
+ * derived from, and the currency and locale a sum is said in wherever a
255
+ * block names none (FR-100) — the locale its days, times and numbers are
256
+ * said in too (FR-178), "en-US" when absent — and the time zone its
257
+ * instants are said in (Roadmap f-app-time-zone), each reader's own clock
258
+ * when absent. Any of them may stand alone.
340
259
  */
341
- lists: z.optional(z.record(z.string(), z.strictObject({
342
- filters: z.optional(some(str(1, 120), undefined, 40)),
343
- /* The order it opens in: a field, a computed field or `label`, lowest first — or { by, direction, then }. */
344
- sort: z.optional(ListSortSpec()),
345
- /* How it opens grouped: a choice or yes-or-no field — or { by, order, closed }, the closed groups last and shut. */
346
- group: z.optional(ListGroupSpec()),
347
- }))),
348
- });
349
- export const DocumentSpec = z.strictObject({
350
- /* Where an editor finds this shape as JSON Schema (`@graview/core/document.schema.json`); the compiler reads past it. */
351
- $schema: z.optional(z.string()),
352
- format: z.literal(FORMAT),
353
- formatVersion: z.literal(FORMAT_VERSION),
354
- name: str(1, 80),
355
- description: z.optional(sentence),
356
- version: z.optional(z.number().check(z.int(), z.minimum(1))),
357
- kinds: z
358
- .record(name("kind"), KindSpec)
359
- .check(z.refine((k) => Object.keys(k).length > 0, "a document declares at least one kind"), z.refine((k) => Object.keys(k).length <= 40, "a document declares at most 40 kinds")),
260
+ const BrandSpec = z.strictObject({
261
+ accent: z.optional(z.string().check(z.regex(/^#[0-9a-fA-F]{6}$/, 'an accent is a color like "#c2577a"'))),
262
+ name: z.optional(str(1, 60)),
263
+ currency: z.optional(z.string().check(z.regex(/^[A-Z]{3}$/, 'a currency is its three-letter code, like "USD" or "EUR"'))),
264
+ // A language tag ("en-US", "de-DE"); `graview check` says when it is none (`brand-locale`).
265
+ locale: z.optional(str(2, 35)),
266
+ // An IANA time zone ("America/New_York"); `graview check` says when it is none (`brand-time-zone`).
267
+ timeZone: z.optional(str(1, 64)),
268
+ /*
269
+ * THE REST OF THE BRAND (FR-124), each optional and each checked by
270
+ * `graview check` (`brand-mark`, `brand-font`): a logo and a page icon —
271
+ * inline SVG, or a path on the app's own host — the faces words are set
272
+ * in, how round and how roomy, a hue per kind, and the scheme it prefers.
273
+ */
274
+ logo: z.optional(z.union([
275
+ str(1, MARK_MAX),
276
+ z.strictObject({ src: str(1, MARK_MAX), alt: z.optional(str(1, 120)) }),
277
+ // One for each scheme: the dark one drawn in the dark; with none, `darkFallback: "invert"` asks for the light one inverted there (never guessed).
278
+ z.strictObject({ light: str(1, MARK_MAX), dark: z.optional(str(1, MARK_MAX)), alt: z.optional(str(1, 120)), darkFallback: z.optional(z.literal("invert")) }),
279
+ ])),
280
+ // A page icon: a mark as the logo is one, or `{ "emoji": "🏡" }` — one standard emoji, the app's mark where it has no logo (`brand-emoji`).
281
+ favicon: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ emoji: z.string() })])),
282
+ typography: z.optional(z.strictObject({ display: z.optional(str(1, 200)), body: z.optional(str(1, 200)), mono: z.optional(str(1, 200)) })),
283
+ shape: z.optional(z.strictObject({
284
+ radius: z.optional(z.number().check(z.minimum(0), z.maximum(32))),
285
+ density: z.optional(z.number().check(z.minimum(0.75), z.maximum(1.5))),
286
+ })),
287
+ accents: z.optional(z.record(z.string(), z.number().check(z.minimum(0), z.maximum(360)))),
288
+ scheme: z.optional(z.enum(["light", "dark", "auto"])),
289
+ });
290
+ const loose = z.record(z.string(), z.unknown());
291
+ /** A grid lens's order (`grid-sort.ts`): a field, or { by, direction }. */
292
+ const GridSortSpec = z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction: z.optional(z.enum(["asc", "desc"])) })]);
293
+ const Year = z.number().check(z.int(), z.minimum(1), z.maximum(9999));
360
294
  /**
361
- * FACTS OF THE WHOLE GRAPH: a name and a rule-language expression with no
362
- * record as its subject — `"mapProposed": "every(all('area'), proposed)"`
363
- * — or `{ expr, label?, description? }`. Read anywhere the language is, as
364
- * `fact('mapProposed')`; worked out on demand under the language's budget.
295
+ * A lens: checked by `graview check` against the lenses the framework ships
296
+ * (`declaredLenses`), so loose here but for what its page is: `create`,
297
+ * false where the place's page leaves off the forms that begin its kind —
298
+ * and the options whose shape is the same on every lens that takes them:
299
+ * a coverage's `sort` and `columnSort` and a status board's `sort` (the
300
+ * order of its rows, columns or cards), and a calendar's `horizon` (its
301
+ * span of years, reaching its records' first and last unless `from` and
302
+ * `to` pin it). `search` and `filters` are fields, here only as lists of
303
+ * names: which ones the kind has is the checker's to say, as is a status
304
+ * board's `collapsed` and `hidden` columns.
365
305
  */
366
- computed: z.optional(z.record(z.string(), ComputedSpec)),
367
- acts: z.optional(z.record(name("act"), ActSpec)),
368
- rules: z.optional(z.record(name("rule"), RuleSpec)),
369
- roles: z.optional(z.array(name("role"))),
370
- policy: z.optional(PolicySpec),
371
- modules: z.optional(loose),
372
- lenses: z.optional(z.array(LensSpec)),
373
- pages: z.optional(PagesSpec),
374
- /** Each kind's views by kind, and `home`: the home view, a list of blocks about no one record (FR-81). */
375
- views: z.optional(z.record(z.string(), z.union([ViewSpecsSpec, z.array(z.unknown())]))),
376
- brand: z.optional(BrandSpec),
377
- settings: z.optional(z.array(loose)),
378
- migrations: z.optional(z.array(loose)),
306
+ const LensSpec = z.looseObject({
307
+ create: z.optional(z.boolean()),
308
+ options: z.optional(z.looseObject({
309
+ sort: z.optional(GridSortSpec),
310
+ columnSort: z.optional(GridSortSpec),
311
+ search: z.optional(some(str(1, 120), undefined, 40)),
312
+ filters: z.optional(some(str(1, 120), undefined, 40)),
313
+ horizon: z.optional(z.union([z.number().check(z.int(), z.minimum(1), z.maximum(200)), z.looseObject({ years: z.number().check(z.int(), z.minimum(1), z.maximum(200)), title: str(1, 80), from: z.optional(Year), to: z.optional(Year) })])),
314
+ })),
315
+ });
316
+ /** A kind's view specs: which slots it fills. The blocks are checked in views.ts, sentence by sentence. */
317
+ const ViewSpecsSpec = z.strictObject({
318
+ card: z.optional(z.array(z.unknown())),
319
+ row: z.optional(z.array(z.unknown())),
320
+ page: z.optional(z.array(z.unknown())),
321
+ });
322
+ /** A kind's list's declared order (`pages.lists.<kind>.sort`): a key, or a key with a direction and a tiebreak. */
323
+ const ListSortSpec = () => {
324
+ const direction = z.optional(z.enum(["asc", "desc"]));
325
+ return z.union([
326
+ str(1, 120),
327
+ z.strictObject({ by: str(1, 120), direction, then: z.optional(z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction })])) }),
328
+ ]);
329
+ };
330
+ /** A kind's list's declared grouping (`pages.lists.<kind>.group`): a field, or a field with its groups' order and the closed ones. */
331
+ const ListGroupSpec = () => z.union([str(1, 120), z.strictObject({ by: str(1, 120), order: z.optional(some(str(1, 80), undefined, 40)), closed: z.optional(some(str(1, 80), undefined, 40)) })]);
379
332
  /**
380
- * Other systems this app is kept in step with, by name: today, Google
381
- * calendars. Read loosely here, as a page that draws the app never runs
382
- * one; their shape (`ConnectionSpec`, `./connection-spec.ts`) is the
383
- * checker's to judge, and the JSON schema says it to an editor.
333
+ * THE ARRANGEMENT (FR-80): the kinds in order, the kinds the home leaves
334
+ * off, the place the app opens on. Loose enough that a document which
335
+ * carried anything else under `pages` before FR-80 — when it was accepted
336
+ * and ignored — still parses; `graview check` says what it cannot honor.
384
337
  */
385
- connections: z.optional(z.record(name("connection"), loose)),
386
- });
338
+ const PagesSpec = z.looseObject({
339
+ order: z.optional(z.array(z.string())),
340
+ hide: z.optional(z.array(z.string())),
341
+ first: z.optional(z.string()),
342
+ /* The places that stand on the bar beside Home (FR-145, ranked): kinds by name or plural, pictures by title. */
343
+ primary: z.optional(some(str(1, 80), undefined, 40)),
344
+ /*
345
+ * What the bar's switch calls the scene and the pages (FR-137); "Scene" and "Pages" when unsaid.
346
+ * At most 40 characters, as `pages.overview` was (FR-132), which a stored document says for `scene`
347
+ * (RESPELLED): a respelling never narrows what the key may hold. The bar cuts a long word, whole on hover.
348
+ */
349
+ scene: z.optional(str(1, 40)),
350
+ pages: z.optional(str(1, 40)),
351
+ /*
352
+ * What each kind's list offers to filter by, by kind (`pages.lists.request.filters`): fields, relations,
353
+ * `is`, and `<relation>.<field>` for a choice at a relation's far end. Its choice fields stand as chips.
354
+ */
355
+ lists: z.optional(z.record(z.string(), z.strictObject({
356
+ filters: z.optional(some(str(1, 120), undefined, 40)),
357
+ /* The order it opens in: a field, a computed field or `label`, lowest first — or { by, direction, then }. */
358
+ sort: z.optional(ListSortSpec()),
359
+ /* How it opens grouped: a choice or yes-or-no field — or { by, order, closed }, the closed groups last and shut. */
360
+ group: z.optional(ListGroupSpec()),
361
+ }))),
362
+ });
363
+ const DocumentSpec = z.strictObject({
364
+ /* Where an editor finds this shape as JSON Schema (`@graview/core/document.schema.json`); the compiler reads past it. */
365
+ $schema: z.optional(z.string()),
366
+ format: z.literal(FORMAT),
367
+ formatVersion: z.literal(FORMAT_VERSION),
368
+ name: str(1, 80),
369
+ description: z.optional(sentence),
370
+ version: z.optional(z.number().check(z.int(), z.minimum(1))),
371
+ kinds: z
372
+ .record(name("kind"), KindSpec)
373
+ .check(z.refine((k) => Object.keys(k).length > 0, "a document declares at least one kind"), z.refine((k) => Object.keys(k).length <= 40, "a document declares at most 40 kinds")),
374
+ /**
375
+ * FACTS OF THE WHOLE GRAPH: a name and a rule-language expression with no
376
+ * record as its subject — `"mapProposed": "every(all('area'), proposed)"`
377
+ * — or `{ expr, label?, description? }`. Read anywhere the language is, as
378
+ * `fact('mapProposed')`; worked out on demand under the language's budget.
379
+ */
380
+ computed: z.optional(z.record(z.string(), ComputedSpec)),
381
+ acts: z.optional(z.record(name("act"), ActSpec)),
382
+ rules: z.optional(z.record(name("rule"), RuleSpec)),
383
+ roles: z.optional(z.array(name("role"))),
384
+ policy: z.optional(PolicySpec),
385
+ modules: z.optional(loose),
386
+ lenses: z.optional(z.array(LensSpec)),
387
+ pages: z.optional(PagesSpec),
388
+ /** Each kind's views by kind, and `home`: the home view, a list of blocks about no one record (FR-81). */
389
+ views: z.optional(z.record(z.string(), z.union([ViewSpecsSpec, z.array(z.unknown())]))),
390
+ brand: z.optional(BrandSpec),
391
+ settings: z.optional(z.array(loose)),
392
+ migrations: z.optional(z.array(loose)),
393
+ /**
394
+ * Other systems this app is kept in step with, by name: today, Google
395
+ * calendars. Read loosely here, as a page that draws the app never runs
396
+ * one; their shape (`ConnectionSpec`, `./connection-spec.ts`) is the
397
+ * checker's to judge, and the JSON schema says it to an editor.
398
+ */
399
+ connections: z.optional(z.record(name("connection"), loose)),
400
+ });
401
+ return { FieldSpec, EdgeSpec, ComputedSpec, KindSpec, ValueSpec, EffectSpec, ActSpec, RepairSpec, RuleSpec, GrantSpec, SightSpec, PolicySpec, BrandSpec, PagesSpec, DocumentSpec };
402
+ }
403
+ let built;
404
+ /** The document's shapes, built once, the first time one is read. */
405
+ const shapes = () => (built ??= build());
406
+ export const FieldSpec = z.lazy(() => shapes().FieldSpec);
407
+ export const EdgeSpec = z.lazy(() => shapes().EdgeSpec);
408
+ export const ComputedSpec = z.lazy(() => shapes().ComputedSpec);
409
+ export const KindSpec = z.lazy(() => shapes().KindSpec);
410
+ export const ValueSpec = z.lazy(() => shapes().ValueSpec);
411
+ export const EffectSpec = z.lazy(() => shapes().EffectSpec);
412
+ export const ActSpec = z.lazy(() => shapes().ActSpec);
413
+ export const RepairSpec = z.lazy(() => shapes().RepairSpec);
414
+ export const RuleSpec = z.lazy(() => shapes().RuleSpec);
415
+ export const GrantSpec = z.lazy(() => shapes().GrantSpec);
416
+ export const SightSpec = z.lazy(() => shapes().SightSpec);
417
+ export const PolicySpec = z.lazy(() => shapes().PolicySpec);
418
+ export const BrandSpec = z.lazy(() => shapes().BrandSpec);
419
+ export const PagesSpec = z.lazy(() => shapes().PagesSpec);
420
+ export const DocumentSpec = z.lazy(() => shapes().DocumentSpec);
387
421
  //# sourceMappingURL=schema.js.map