@graview/core 0.1.14 → 0.1.16

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 (238) hide show
  1. package/README.md +27 -8
  2. package/dist/app.d.ts +8 -8
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/arrangement.d.ts +2 -2
  5. package/dist/arrangement.d.ts.map +1 -1
  6. package/dist/arrangement.js +5 -5
  7. package/dist/arranging.js +3 -3
  8. package/dist/capabilities.js +1 -1
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/city.d.ts +5 -5
  11. package/dist/city.js +3 -3
  12. package/dist/cli/check/beginnings.js +2 -2
  13. package/dist/cli/check/brand.d.ts.map +1 -1
  14. package/dist/cli/check/brand.js +15 -9
  15. package/dist/cli/check/brand.js.map +1 -1
  16. package/dist/cli/check/city.js +1 -1
  17. package/dist/cli/check/context.js +1 -1
  18. package/dist/cli/check/hidden-reads.js +1 -1
  19. package/dist/cli/check/hidden-reads.js.map +1 -1
  20. package/dist/cli/check/relations.js +2 -2
  21. package/dist/cli/check/relations.js.map +1 -1
  22. package/dist/cli/check/settings.js +8 -8
  23. package/dist/cli/check/settings.js.map +1 -1
  24. package/dist/cli/check/views.d.ts +1 -1
  25. package/dist/cli/check/views.js +1 -1
  26. package/dist/cli/create.d.ts +1 -1
  27. package/dist/cli/create.js +5 -5
  28. package/dist/cli/describe.d.ts +1 -1
  29. package/dist/cli/describe.js +2 -2
  30. package/dist/cli/describe.js.map +1 -1
  31. package/dist/cli/index.d.ts +1 -1
  32. package/dist/columns.d.ts +1 -1
  33. package/dist/columns.js +1 -1
  34. package/dist/compiled.d.ts +19 -0
  35. package/dist/compiled.d.ts.map +1 -0
  36. package/dist/compiled.js +60 -0
  37. package/dist/compiled.js.map +1 -0
  38. package/dist/conformance/fixtures.d.ts.map +1 -1
  39. package/dist/conformance/fixtures.js +22 -0
  40. package/dist/conformance/fixtures.js.map +1 -1
  41. package/dist/describe.d.ts +1 -1
  42. package/dist/describe.d.ts.map +1 -1
  43. package/dist/document/blocks.d.ts.map +1 -1
  44. package/dist/document/blocks.js +2 -1
  45. package/dist/document/blocks.js.map +1 -1
  46. package/dist/document/brand-check.d.ts +10 -0
  47. package/dist/document/brand-check.d.ts.map +1 -0
  48. package/dist/document/brand-check.js +33 -0
  49. package/dist/document/brand-check.js.map +1 -0
  50. package/dist/document/brand.d.ts +10 -0
  51. package/dist/document/brand.d.ts.map +1 -0
  52. package/dist/document/brand.js +49 -0
  53. package/dist/document/brand.js.map +1 -0
  54. package/dist/document/compile-checked.d.ts.map +1 -1
  55. package/dist/document/compile-checked.js +6 -2
  56. package/dist/document/compile-checked.js.map +1 -1
  57. package/dist/document/compile.d.ts +10 -29
  58. package/dist/document/compile.d.ts.map +1 -1
  59. package/dist/document/compile.js +74 -393
  60. package/dist/document/compile.js.map +1 -1
  61. package/dist/document/compiled.d.ts +130 -0
  62. package/dist/document/compiled.d.ts.map +1 -0
  63. package/dist/document/compiled.js +467 -0
  64. package/dist/document/compiled.js.map +1 -0
  65. package/dist/document/describe-place.d.ts +16 -0
  66. package/dist/document/describe-place.d.ts.map +1 -1
  67. package/dist/document/describe-place.js +14 -1
  68. package/dist/document/describe-place.js.map +1 -1
  69. package/dist/document/diff.d.ts.map +1 -1
  70. package/dist/document/diff.js +45 -6
  71. package/dist/document/diff.js.map +1 -1
  72. package/dist/document/edit.d.ts +7 -2
  73. package/dist/document/edit.d.ts.map +1 -1
  74. package/dist/document/edit.js +174 -46
  75. package/dist/document/edit.js.map +1 -1
  76. package/dist/document/expr/analyze.d.ts +2 -2
  77. package/dist/document/expr/analyze.js +2 -2
  78. package/dist/document/expr/print.js +1 -1
  79. package/dist/document/index.d.ts +5 -2
  80. package/dist/document/index.d.ts.map +1 -1
  81. package/dist/document/index.js +3 -1
  82. package/dist/document/index.js.map +1 -1
  83. package/dist/document/quote.d.ts +5 -0
  84. package/dist/document/quote.d.ts.map +1 -0
  85. package/dist/document/quote.js +17 -0
  86. package/dist/document/quote.js.map +1 -0
  87. package/dist/document/remembered.d.ts +6 -0
  88. package/dist/document/remembered.d.ts.map +1 -0
  89. package/dist/document/remembered.js +16 -0
  90. package/dist/document/remembered.js.map +1 -0
  91. package/dist/document/rules.d.ts +2 -2
  92. package/dist/document/rules.d.ts.map +1 -1
  93. package/dist/document/rules.js +4 -3
  94. package/dist/document/rules.js.map +1 -1
  95. package/dist/document/schema.d.ts +45 -2
  96. package/dist/document/schema.d.ts.map +1 -1
  97. package/dist/document/schema.js +32 -5
  98. package/dist/document/schema.js.map +1 -1
  99. package/dist/document/template-parse.d.ts +18 -0
  100. package/dist/document/template-parse.d.ts.map +1 -0
  101. package/dist/document/template-parse.js +124 -0
  102. package/dist/document/template-parse.js.map +1 -0
  103. package/dist/document/template.d.ts +1 -17
  104. package/dist/document/template.d.ts.map +1 -1
  105. package/dist/document/template.js +6 -115
  106. package/dist/document/template.js.map +1 -1
  107. package/dist/document/thumbnail.d.ts +13 -1
  108. package/dist/document/thumbnail.d.ts.map +1 -1
  109. package/dist/document/thumbnail.js +106 -16
  110. package/dist/document/thumbnail.js.map +1 -1
  111. package/dist/document/to-document.d.ts +1 -4
  112. package/dist/document/to-document.d.ts.map +1 -1
  113. package/dist/document/to-document.js +7 -14
  114. package/dist/document/to-document.js.map +1 -1
  115. package/dist/document/views.d.ts.map +1 -1
  116. package/dist/document/views.js +1 -1
  117. package/dist/document/views.js.map +1 -1
  118. package/dist/graph/graph.js +3 -3
  119. package/dist/graph/primitives.d.ts +2 -2
  120. package/dist/graph/primitives.js +2 -2
  121. package/dist/index.d.ts +11 -6
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +9 -6
  124. package/dist/index.js.map +1 -1
  125. package/dist/invariants/engine.js +1 -1
  126. package/dist/invariants/types.d.ts +3 -3
  127. package/dist/invariants/types.d.ts.map +1 -1
  128. package/dist/labels.d.ts +1 -1
  129. package/dist/modules.d.ts +1 -1
  130. package/dist/modules.js +1 -1
  131. package/dist/mutations/define-mutation.d.ts +12 -0
  132. package/dist/mutations/define-mutation.d.ts.map +1 -1
  133. package/dist/mutations/define-mutation.js +37 -9
  134. package/dist/mutations/define-mutation.js.map +1 -1
  135. package/dist/mutations/derive-edits.js +4 -4
  136. package/dist/mutations/derive-edits.js.map +1 -1
  137. package/dist/mutations/node-ref.js +1 -1
  138. package/dist/mutations/types.d.ts +1 -1
  139. package/dist/mutations/words.js +3 -3
  140. package/dist/ops/types.d.ts +1 -1
  141. package/dist/ops/withheld.d.ts +3 -3
  142. package/dist/permissions/policy.d.ts +1 -1
  143. package/dist/permissions/policy.js +3 -3
  144. package/dist/permissions/types.d.ts +2 -2
  145. package/dist/places.d.ts +14 -1
  146. package/dist/places.d.ts.map +1 -1
  147. package/dist/places.js +27 -4
  148. package/dist/places.js.map +1 -1
  149. package/dist/refusal.d.ts +11 -6
  150. package/dist/refusal.d.ts.map +1 -1
  151. package/dist/refusal.js +11 -6
  152. package/dist/refusal.js.map +1 -1
  153. package/dist/refused.d.ts +13 -6
  154. package/dist/refused.d.ts.map +1 -1
  155. package/dist/refused.js +14 -7
  156. package/dist/refused.js.map +1 -1
  157. package/dist/scaffold/domain.js +3 -3
  158. package/dist/scaffold/index.d.ts +1 -1
  159. package/dist/scaffold/index.d.ts.map +1 -1
  160. package/dist/scaffold/names.d.ts +1 -1
  161. package/dist/scaffold/names.js +1 -1
  162. package/dist/scaffold/names.js.map +1 -1
  163. package/dist/scaffold/project.d.ts.map +1 -1
  164. package/dist/scaffold/project.js +2 -1
  165. package/dist/scaffold/project.js.map +1 -1
  166. package/dist/scene-districts.d.ts +1 -1
  167. package/dist/schema/define-node.d.ts +4 -4
  168. package/dist/schema/define-node.d.ts.map +1 -1
  169. package/dist/schema/define-node.js +8 -8
  170. package/dist/schema/define-node.js.map +1 -1
  171. package/dist/schema/figures.d.ts +1 -1
  172. package/dist/schema/figures.js +7 -7
  173. package/dist/schema/figures.js.map +1 -1
  174. package/dist/schema/json-schema.d.ts.map +1 -1
  175. package/dist/schema/json-schema.js +4 -1
  176. package/dist/schema/json-schema.js.map +1 -1
  177. package/dist/schema/types.d.ts +1 -1
  178. package/dist/search.d.ts +13 -1
  179. package/dist/search.d.ts.map +1 -1
  180. package/dist/search.js +33 -14
  181. package/dist/search.js.map +1 -1
  182. package/dist/seen.d.ts +2 -2
  183. package/dist/seen.js +12 -12
  184. package/dist/seen.js.map +1 -1
  185. package/dist/settings.d.ts +1 -1
  186. package/dist/settings.js +3 -3
  187. package/dist/settings.js.map +1 -1
  188. package/dist/store.d.ts +2 -2
  189. package/dist/store.d.ts.map +1 -1
  190. package/dist/store.js +8 -8
  191. package/dist/store.js.map +1 -1
  192. package/dist/sync/google-calendar.js +1 -1
  193. package/dist/sync/types.d.ts +2 -2
  194. package/dist/testing.d.ts +1 -1
  195. package/dist/testing.d.ts.map +1 -1
  196. package/dist/testing.js +2 -2
  197. package/dist/testing.js.map +1 -1
  198. package/dist/theme/accent.d.ts +37 -0
  199. package/dist/theme/accent.d.ts.map +1 -0
  200. package/dist/theme/accent.js +138 -0
  201. package/dist/theme/accent.js.map +1 -0
  202. package/dist/theme/contrast.d.ts +7 -7
  203. package/dist/theme/contrast.d.ts.map +1 -1
  204. package/dist/theme/contrast.js +14 -14
  205. package/dist/theme/contrast.js.map +1 -1
  206. package/dist/theme/derive.d.ts +4 -4
  207. package/dist/theme/derive.d.ts.map +1 -1
  208. package/dist/theme/derive.js +19 -19
  209. package/dist/theme/derive.js.map +1 -1
  210. package/dist/theme/fonts.d.ts +19 -0
  211. package/dist/theme/fonts.d.ts.map +1 -0
  212. package/dist/theme/fonts.js +73 -0
  213. package/dist/theme/fonts.js.map +1 -0
  214. package/dist/theme/kit.d.ts +9 -9
  215. package/dist/theme/kit.d.ts.map +1 -1
  216. package/dist/theme/kit.js +15 -15
  217. package/dist/theme/kit.js.map +1 -1
  218. package/dist/theme/layers.d.ts +2 -2
  219. package/dist/theme/layers.d.ts.map +1 -1
  220. package/dist/theme/layers.js +2 -2
  221. package/dist/theme/layers.js.map +1 -1
  222. package/dist/theme/look.d.ts +3 -3
  223. package/dist/theme/look.d.ts.map +1 -1
  224. package/dist/theme/look.js +1 -1
  225. package/dist/theme/marks.d.ts +28 -0
  226. package/dist/theme/marks.d.ts.map +1 -0
  227. package/dist/theme/marks.js +237 -0
  228. package/dist/theme/marks.js.map +1 -0
  229. package/dist/theme/palettes.d.ts +2 -2
  230. package/dist/theme/palettes.js +2 -2
  231. package/dist/theme/types.d.ts +28 -6
  232. package/dist/theme/types.d.ts.map +1 -1
  233. package/dist/theme/types.js.map +1 -1
  234. package/dist/validate-graph.js +2 -2
  235. package/dist/version.d.ts +1 -1
  236. package/dist/version.js +1 -1
  237. package/dist/watched.js +8 -8
  238. package/package.json +5 -1
@@ -1,20 +1,16 @@
1
- import { RuleBudgetError, withArticle, brandFromAccent, createSchema, defineInvariant, defineMutation, defineNode, SCHEMES, } from "../index.js";
2
- import * as z from "../schema/zod.js";
3
- import { refTo } from "../mutations/node-ref.js";
1
+ import { brandOf } from "./brand.js";
4
2
  import { ActRefusal } from "../refused.js";
5
3
  import { analyzeExpr } from "./expr/analyze.js";
6
- import { rememberDocument } from "./to-document.js";
7
- import { evaluateExpr, ExprBudgetError, ExprEvalError } from "./expr/evaluate.js";
8
4
  import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
9
5
  import { error, hasErrors, warning } from "./findings.js";
10
6
  import { DocumentSpec, MAX_DOCUMENT_BYTES, } from "./schema.js";
11
- import { parseTemplate, renderTemplate, TemplateError } from "./template.js";
7
+ import { parseTemplate, TemplateError } from "./template-parse.js";
12
8
  import { upgradeDocument } from "./upgrade.js";
13
9
  import { homeOf, validateViews, viewsOf } from "./views.js";
14
10
  import { farEnd } from "./far-end.js";
15
11
  import { computedOf, parsedComputed, validateComputed, workedOutAlone } from "./computed.js";
12
+ import { build, COMPILED_FORMAT, defaultLabelField, refName, SUBJECT_ARG, } from "./compiled.js";
16
13
  const BUILTIN_REFS = new Set(["subject", "now", "today"]);
17
- const SUBJECT_ARG = "id";
18
14
  /** Parse and validate without compiling — the document's own sentences, before the framework's. */
19
15
  export function readDocument(raw) {
20
16
  if (typeof raw === "string") {
@@ -129,7 +125,6 @@ export function effectsOf(act, document) {
129
125
  effects.push(...act.effects.filter((e) => !(act.creates && "set" in e && !("create" in e) && refName(e.target) === "new")));
130
126
  return effects;
131
127
  }
132
- const refName = (v) => (typeof v === "string" && /^\$[A-Za-z][A-Za-z0-9]*$/.test(v) ? v.slice(1) : undefined);
133
128
  function tryExpr(source, path, findings) {
134
129
  try {
135
130
  const expr = parseExpr(source);
@@ -351,67 +346,8 @@ function validate(document) {
351
346
  findings.push(...validateComputed(new Map(Object.entries(document.kinds).map(([kind, spec]) => [kind, { fields: new Set(Object.keys(spec.fields)), edges: new Set(Object.keys(spec.edges ?? {})), computed: computedOf(spec) }])), (kind, name) => `kinds.${kind}.computed.${name}`));
352
347
  return findings;
353
348
  }
354
- /*
355
- * A FIELD'S SCHEMA, in zod/mini (schema/zod.ts): a page that compiles a
356
- * document in the browser pays for the checks it calls, not for classic
357
- * zod whole (FR-57). The checks, their messages and their definitions are
358
- * the ones classic's methods make.
359
- */
360
- function fieldSchema(spec, optional) {
361
- let schema;
362
- switch (spec.type) {
363
- case "string":
364
- schema = z.string().check(z.maxLength(500));
365
- break;
366
- case "text":
367
- schema = z.string().check(z.maxLength(20_000));
368
- break;
369
- case "number":
370
- case "integer": {
371
- // Its range (FR-114) is the schema's: every form, tool and apply reads it from here.
372
- const range = [
373
- ...(spec.type === "integer" ? [z.int()] : []),
374
- ...(spec.min === undefined ? [] : [z.gte(spec.min)]),
375
- ...(spec.max === undefined ? [] : [z.lte(spec.max)]),
376
- ...(spec.step === undefined ? [] : [z.multipleOf(spec.step)]),
377
- ];
378
- schema = range.length > 0 ? z.number().check(...range) : z.number();
379
- break;
380
- }
381
- case "boolean":
382
- schema = z.boolean();
383
- break;
384
- case "date":
385
- schema = z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/, "a date like 2026-10-02"));
386
- break;
387
- case "datetime":
388
- schema = z.string().check(z.refine((s) => !Number.isNaN(Date.parse(s)), "a date and time like 2026-10-02T14:30:00Z"));
389
- break;
390
- case "enum":
391
- schema = z.enum(spec.options);
392
- break;
393
- case "list":
394
- schema = z.array(spec.of === "number" ? z.number() : spec.of === "date" ? z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/)) : z.string().check(z.maxLength(500))).check(z.maxLength(200));
395
- break;
396
- case "url":
397
- schema = z.url();
398
- break;
399
- case "email":
400
- schema = z.email();
401
- break;
402
- }
403
- if (spec.description)
404
- schema = schema.check(z.describe(spec.description));
405
- return optional ? z.optional(schema) : schema;
406
- }
407
- /** What one of these is called, when the kind says nothing: the first required word-ish field. */
408
- function defaultLabelField(spec) {
409
- const entries = Object.entries(spec.fields);
410
- return (entries.find(([, f]) => f.required && (f.type === "string" || f.type === "text"))?.[0] ??
411
- entries.find(([, f]) => f.type === "string")?.[0]);
412
- }
413
349
  /** The arguments an act asks for: declared ones, then whatever its effects reference. */
414
- function argsOf(name, act, effects, document, edges) {
350
+ function argsOf(act, effects, document, edges) {
415
351
  const args = new Map();
416
352
  const subjectKinds = asArray(act.on);
417
353
  const created = new Map();
@@ -420,7 +356,7 @@ function argsOf(name, act, effects, document, edges) {
420
356
  const usesSubject = subjectKinds.length > 0 ||
421
357
  effects.some((e) => ("remove" in e && refName(e.remove) === "subject") || ("from" in e && (refName(e.from) === "subject" || refName(e.to) === "subject")) || ("set" in e && !("create" in e) && refName(e.target ?? "$subject") === "subject"));
422
358
  if (usesSubject)
423
- args.set(SUBJECT_ARG, { schema: refTo(subjectKinds.length > 0 ? subjectKinds : "*"), required: true });
359
+ args.set(SUBJECT_ARG, { ref: subjectKinds.length > 0 ? subjectKinds : "*", required: true });
424
360
  const declared = act.args ?? {};
425
361
  const writesOnlyOne = act.writes?.length === 1;
426
362
  /*
@@ -433,7 +369,7 @@ function argsOf(name, act, effects, document, edges) {
433
369
  if (arg === SUBJECT_ARG || BUILTIN_REFS.has(arg) || created.has(arg) || args.has(arg))
434
370
  return;
435
371
  const d = declared[arg] ? withRangeOf(declared[arg], fed) : undefined;
436
- args.set(arg, d ? { schema: fieldSchema(d, !d.required), required: Boolean(d.required) } : schema());
372
+ args.set(arg, d ? { field: d, optional: !d.required, required: Boolean(d.required) } : schema());
437
373
  };
438
374
  for (const effect of effects) {
439
375
  if ("create" in effect) {
@@ -443,7 +379,7 @@ function argsOf(name, act, effects, document, edges) {
443
379
  const spec = kind?.fields[field];
444
380
  if (arg && spec) {
445
381
  const required = Boolean(spec.required) && spec.default === undefined;
446
- note(arg, () => ({ schema: fieldSchema(spec, !required), required }), spec);
382
+ note(arg, () => ({ field: spec, optional: !required, required }), spec);
447
383
  }
448
384
  }
449
385
  if (effect.as)
@@ -458,36 +394,33 @@ function argsOf(name, act, effects, document, edges) {
458
394
  const kinds = end === "to" ? edge?.to ?? "*" : edge?.from ?? "*";
459
395
  if (kinds !== "*" && !named.has(arg))
460
396
  named.set(arg, kinds);
461
- note(arg, () => ({ schema: refTo(kinds === "*" ? "*" : kinds), required: true }));
397
+ note(arg, () => ({ ref: kinds, required: true }));
462
398
  }
463
399
  }
464
400
  else if ("set" in effect) {
465
401
  const target = refName(effect.target ?? "$subject");
466
402
  const targetKind = target === "subject" ? subjectKinds[0] : (created.get(target ?? "") ?? named.get(target ?? "")?.[0]);
467
403
  if (target && target !== "subject" && !created.has(target))
468
- note(target, () => ({ schema: refTo("*"), required: true }));
404
+ note(target, () => ({ ref: "*", required: true }));
469
405
  for (const [field, value] of Object.entries(effect.set)) {
470
406
  const arg = refName(value);
471
407
  if (!arg)
472
408
  continue;
473
409
  const spec = targetKind ? document.kinds[targetKind]?.fields[field] : undefined;
474
- note(arg, () => (spec ? { schema: fieldSchema(spec, !writesOnlyOne), required: writesOnlyOne } : { schema: z.optional(z.unknown()), required: false }), spec);
410
+ note(arg, () => (spec ? { field: spec, optional: !writesOnlyOne, required: writesOnlyOne } : { any: true, required: false }), spec);
475
411
  }
476
412
  }
477
413
  else if ("remove" in effect) {
478
414
  const arg = refName(effect.remove);
479
415
  if (arg)
480
- note(arg, () => ({ schema: refTo("*"), required: true }));
416
+ note(arg, () => ({ ref: "*", required: true }));
481
417
  }
482
418
  }
483
419
  for (const [arg, d] of Object.entries(declared))
484
420
  if (!args.has(arg))
485
- args.set(arg, { schema: fieldSchema(d, !d.required), required: Boolean(d.required) });
486
- void name;
421
+ args.set(arg, { field: d, optional: !d.required, required: Boolean(d.required) });
487
422
  return args;
488
423
  }
489
- /** An act's own refusal: the framework's typed one (FR-110), which a host shows rather than calling it a failure. */
490
- const Refused = ActRefusal;
491
424
  /**
492
425
  * A DOCUMENT COMPILED WITHOUT THE FRAMEWORK'S CHECKER: its own sentences
493
426
  * still judged, `checkApp` not asked. For a page drawing a document its host
@@ -504,65 +437,36 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
504
437
  const findings = [...read.findings];
505
438
  if (options.previous)
506
439
  findings.push(...renamesFromNothing(document, options.previous));
507
- const today = options.today ?? (() => new Date().toISOString().slice(0, 10));
508
- const shapes = kindShapes(document);
440
+ const plan = planOf(document, findings);
441
+ if (hasErrors(plan.findings))
442
+ return { ok: false, findings: plan.findings };
443
+ return build(plan, options);
444
+ }
445
+ /**
446
+ * The first half of compiling (FR-123): what a document that has been read
447
+ * and judged compiles to, as data — each template and expression parsed,
448
+ * each act's arguments and effects said, the brand derived. `findings` is
449
+ * added to (a brand that cannot be read is a warning) and carried.
450
+ */
451
+ function planOf(document, findings) {
509
452
  const edges = edgeIndex(document);
510
- // What `{x | money}` says in a label, a sentence or a refusal: the app's currency and locale (FR-100).
511
- const { accent, name: wordmark, ...money } = document.brand ?? {};
512
453
  // ── kinds ────────────────────────────────────────────────────────────────
513
- const definitions = Object.entries(document.kinds).map(([kind, spec]) => {
514
- const shape = {};
515
- for (const [field, f] of Object.entries(spec.fields))
516
- shape[field] = fieldSchema(f, !f.required);
517
- const labelSource = spec.label ?? (defaultLabelField(spec) ? `{${defaultLabelField(spec)}}` : undefined);
518
- const labelParts = labelSource ? parseTemplate(labelSource) : undefined;
519
- const describeParts = spec.describe ? parseTemplate(spec.describe) : undefined;
520
- const computed = computedOf(spec);
521
- // A computed field's words are said like a field's: its label, where it has one.
522
- const labels = Object.fromEntries([
523
- ...Object.entries(spec.fields).filter(([, f]) => f.label).map(([n, f]) => [n, f.label]),
524
- ...[...computed].filter(([, c]) => c.label).map(([n, c]) => [n, c.label]),
525
- ]);
526
- // Defaults stay out of the zod schema (hydration must not invent values) and ride beside it, for a repair to read.
527
- const defaults = Object.fromEntries(Object.entries(spec.fields).filter(([, f]) => f.default !== undefined).map(([n, f]) => [n, f.default]));
528
- const edgeDecls = Object.fromEntries(Object.entries(spec.edges ?? {}).map(([n, e]) => [
529
- n,
530
- {
531
- to: e.to,
532
- ...(e.cardinality ? { cardinality: e.cardinality } : {}),
533
- ...(e.description ? { description: e.description } : {}),
534
- ...(e.inverse ? { inverse: e.inverse } : {}),
535
- ...(e.appendOnly ? { appendOnly: e.appendOnly } : {}),
536
- },
537
- ]));
538
- return defineNode(kind, {
539
- fields: z.object(shape),
540
- edges: edgeDecls,
541
- ...(spec.plural ? { plural: spec.plural } : {}),
542
- ...(spec.noun ? { noun: spec.noun } : {}),
543
- ...(spec.description ? { description: spec.description } : {}),
544
- ...(labelParts ? { label: (node) => renderTemplate(labelParts, { node: node, kinds: shapes, today: today(), money }) || node.id } : {}),
545
- ...(describeParts ? { describe: (node) => renderTemplate(describeParts, { node: node, kinds: shapes, today: today(), money }) } : {}),
546
- ...(spec.lifecycle ? { lifecycle: { field: spec.lifecycle.field, retired: spec.lifecycle.retired } } : {}),
547
- ...(spec.figure ? { figure: spec.figure } : {}),
548
- ...(spec.computed && Object.keys(spec.computed).length > 0 ? { computed: spec.computed } : {}),
549
- ...(Object.keys(labels).length > 0 || spec.glance ? { display: { ...(Object.keys(labels).length > 0 ? { labels } : {}), ...(spec.glance ? { glance: [...spec.glance] } : {}) } } : {}),
550
- ...(Object.keys(defaults).length > 0 ? { defaults } : {}),
551
- });
552
- });
553
- const schema = createSchema(definitions);
454
+ const kinds = {};
455
+ for (const [kind, spec] of Object.entries(document.kinds)) {
456
+ const labelField = defaultLabelField(spec);
457
+ const labelSource = spec.label ?? (labelField ? `{${labelField}}` : undefined);
458
+ const computed = parsedComputed(spec);
459
+ kinds[kind] = {
460
+ ...(labelSource ? { label: parseTemplate(labelSource) } : {}),
461
+ ...(spec.describe ? { describe: parseTemplate(spec.describe) } : {}),
462
+ ...(computed ? { computed: [...computed] } : {}),
463
+ };
464
+ }
554
465
  // ── acts ─────────────────────────────────────────────────────────────────
555
- const mutations = [];
466
+ const acts = {};
556
467
  for (const [name, act] of Object.entries(document.acts ?? {})) {
557
468
  const effects = effectsOf(act, document);
558
- const args = argsOf(name, act, effects, document, edges);
559
- const shape = {};
560
- for (const [arg, a] of args)
561
- shape[arg] = a.schema;
562
- const subjectKinds = asArray(act.on);
563
- const title = act.title ?? name.replace(/-/g, " ");
564
- const guard = act.allowedWhen ? parseExpr(act.allowedWhen) : undefined;
565
- const refusal = act.refusal ? parseTemplate(act.refusal) : undefined;
469
+ const args = argsOf(act, effects, document, edges);
566
470
  const destructive = act.destructive ?? effects.some((e) => "remove" in e || "sever" in e || "replace" in e);
567
471
  const creates = [...new Set(effects.flatMap((e) => ("create" in e ? [e.create] : [])))];
568
472
  const connects = [...new Set(effects.flatMap((e) => ("connect" in e ? [e.connect] : [])))];
@@ -621,273 +525,50 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
621
525
  if (v && typeof v === "object" && !Array.isArray(v) && "expr" in v)
622
526
  exprs.set(v.expr, parseExpr(v.expr));
623
527
  }
624
- mutations.push(defineMutation(name, {
625
- input: z.object(shape),
626
- title,
627
- ...(act.description ? { description: act.description } : {}),
628
- ...(act.fromTheOtherEnd ? { fromTheOtherEnd: act.fromTheOtherEnd } : {}),
629
- ...(args.has(SUBJECT_ARG) ? { subject: { kinds: subjectKinds.length > 0 ? subjectKinds : "*", arg: SUBJECT_ARG } } : {}),
630
- ...(destructive ? { destructive: true } : {}),
631
- ...(idempotent ? { idempotent: true } : {}),
632
- ...(creates.length > 0 ? { creates } : {}),
633
- ...(connects.length > 0 ? { connects } : {}),
634
- ...(severs.length > 0 ? { severs } : {}),
635
- // Said even when empty (FR-110): a document act's writes are read off it, so nothing guesses them from its arguments' names.
528
+ acts[name] = {
529
+ title: act.title ?? name.replace(/-/g, " "),
530
+ effects,
531
+ args: [...args],
532
+ ...(act.allowedWhen ? { guard: parseExpr(act.allowedWhen) } : {}),
533
+ ...(act.refusal ? { refusal: parseTemplate(act.refusal) } : {}),
534
+ exprs: [...exprs],
535
+ destructive,
536
+ idempotent,
537
+ creates,
538
+ connects,
539
+ severs,
636
540
  writes,
637
- ...(Object.keys(writesOther).length > 0 ? { writesOther } : {}),
638
- ...(Object.keys(sets).length > 0 ? { sets } : {}),
639
- describe: (input, graph) => {
640
- const subject = typeof input[SUBJECT_ARG] === "string" ? graph.getNode(input[SUBJECT_ARG]) : undefined;
641
- const what = subject ? labelFor(subject) : Object.values(input).find((v) => typeof v === "string");
642
- return what ? `${title}: ${String(what)}` : title;
643
- },
644
- apply(context, input) {
645
- const graph = context.graph;
646
- const subjectId = typeof input[SUBJECT_ARG] === "string" ? input[SUBJECT_ARG] : undefined;
647
- const subject = subjectId ? graph.getNode(subjectId) : undefined;
648
- if (subjectId && !subject)
649
- throw new Refused(`there is no record "${subjectId}"`);
650
- if (subject && subjectKinds.length > 0 && !subjectKinds.includes(subject.kind))
651
- throw new Refused(`"${title}" acts on ${subjectKinds.join(" or ")}, not on ${withArticle(subject.kind)}`);
652
- const bindings = {};
653
- for (const [k, v] of Object.entries(input))
654
- if (v !== undefined)
655
- bindings[k] = v;
656
- if (guard && subject) {
657
- let allowed;
658
- try {
659
- allowed = evaluateExpr(guard, { graph, subject, kinds: shapes, bindings, today: today() });
660
- }
661
- catch (e) {
662
- if (e instanceof ExprEvalError)
663
- throw new Refused(`"${title}" could not be judged: ${e.sentence}`);
664
- throw e;
665
- }
666
- if (allowed !== true) {
667
- throw new Refused(refusal ? renderTemplate(refusal, { node: subject, kinds: shapes, graph, bindings, today: today(), money }) : `"${title}" is not allowed for ${labelFor(subject)} right now`);
668
- }
669
- }
670
- const made = new Map();
671
- const resolve = (value, current) => {
672
- if (value && typeof value === "object" && !Array.isArray(value) && "expr" in value) {
673
- try {
674
- return evaluateExpr(exprs.get(value.expr), { graph, subject: current ?? subject ?? null, kinds: shapes, bindings, today: today() });
675
- }
676
- catch (e) {
677
- if (e instanceof ExprEvalError)
678
- throw new Refused(`"${title}" could not work out a value: ${e.sentence}`);
679
- throw e;
680
- }
681
- }
682
- const ref = refName(value);
683
- if (ref === undefined)
684
- return typeof value === "string" && value.startsWith("$$") ? value.slice(1) : value;
685
- if (ref === "subject")
686
- return subjectId;
687
- if (ref === "now")
688
- return new Date().toISOString();
689
- if (ref === "today")
690
- return today();
691
- if (made.has(ref))
692
- return made.get(ref);
693
- return input[ref];
694
- };
695
- for (const effect of effects) {
696
- if ("create" in effect) {
697
- const kind = document.kinds[effect.create];
698
- const node = { kind: effect.create };
699
- for (const [field, f] of Object.entries(kind.fields))
700
- if (f.default !== undefined)
701
- node[field] = f.default;
702
- for (const [field, value] of Object.entries(effect.set ?? {})) {
703
- const v = resolve(value, undefined);
704
- if (v !== undefined)
705
- node[field] = v;
706
- }
707
- const labelField = defaultLabelField(kind);
708
- const id = context.freshId(String((labelField && node[labelField]) ?? effect.create), effect.create);
709
- context.addNode({ id, ...node });
710
- if (effect.as)
711
- made.set(effect.as, id);
712
- }
713
- else if ("connect" in effect || "sever" in effect) {
714
- const kind = "connect" in effect ? effect.connect : effect.sever;
715
- const from = String(resolve(effect.from, undefined) ?? "");
716
- const to = String(resolve(effect.to, undefined) ?? "");
717
- if (!graph.has(from) && ![...made.values()].includes(from))
718
- throw new Refused(`there is no record "${from}"`);
719
- if (!graph.has(to) && ![...made.values()].includes(to))
720
- throw new Refused(`there is no record "${to}"`);
721
- if ("connect" in effect) {
722
- if (edges.get(kind)?.cardinality === "one")
723
- for (const existing of graph.out(from, kind))
724
- if (existing.id !== to)
725
- context.removeEdge({ kind, from, to: existing.id });
726
- if (!graph.out(from, kind).some((n) => n.id === to))
727
- context.addEdge({ kind, from, to });
728
- }
729
- else {
730
- context.removeEdge({ kind, from, to });
731
- }
732
- }
733
- else if ("set" in effect) {
734
- const targetId = String(resolve(effect.target ?? "$subject", undefined) ?? "");
735
- const target = graph.getNode(targetId);
736
- if (!target && ![...made.values()].includes(targetId))
737
- throw new Refused(`there is no record "${targetId}"`);
738
- const patch = {};
739
- for (const [field, value] of Object.entries(effect.set)) {
740
- const v = resolve(value, target);
741
- if (v !== undefined)
742
- patch[field] = v;
743
- }
744
- if (Object.keys(patch).length > 0)
745
- context.patchNode(targetId, patch);
746
- }
747
- else if ("remove" in effect) {
748
- const id = String(resolve(effect.remove, undefined) ?? "");
749
- if (!graph.has(id))
750
- throw new Refused(`there is no record "${id}"`);
751
- context.removeNode(id);
752
- }
753
- else if ("replace" in effect && subject) {
754
- // The subject's links of each relation, at its own end, severed — all but the one being made (FR-115).
755
- const kept = input["to"];
756
- for (const relation of effect.replace) {
757
- const fromSubject = edges.get(relation)?.from.includes(subject.kind) ?? true;
758
- for (const other of fromSubject ? graph.out(subject.id, relation) : graph.in(subject.id, relation)) {
759
- if (relation === effect.keep && other.id === kept)
760
- continue;
761
- context.removeEdge(fromSubject ? { kind: relation, from: subject.id, to: other.id } : { kind: relation, from: other.id, to: subject.id });
762
- }
763
- }
764
- }
765
- }
766
- },
767
- }));
768
- }
769
- function labelFor(node) {
770
- const definition = schema.tryDefinition(node.kind);
771
- return definition?.label?.(node) ?? node.id;
541
+ writesOther,
542
+ sets,
543
+ };
772
544
  }
773
545
  // ── rules ────────────────────────────────────────────────────────────────
774
- const invariants = [];
546
+ const rules = {};
775
547
  for (const [name, rule] of Object.entries(document.rules ?? {})) {
776
- const require = parseExpr(rule.require);
777
- const when = rule.when ? parseExpr(rule.when) : undefined;
778
- const says = rule.says ? parseTemplate(rule.says) : undefined;
779
- const title = rule.title ?? name.replace(/-/g, " ");
780
- const repairs = rule.repairs ?? [];
781
- const judge = (graph, subject) => {
782
- const ctx = { graph, subject, kinds: shapes, today: today() };
783
- let holds;
784
- /*
785
- * A RULE THAT CANNOT ANSWER IS THE ENGINE'S TO SAY (FR-29). Out of
786
- * budget it throws RuleBudgetError, and the violation's status is
787
- * `over-budget`; any other mistake (a word added to a number) is
788
- * `could-not-judge`. Both name the rule and its subject, and neither
789
- * is a hang or a crash that takes the other rules down.
790
- */
791
- try {
792
- if (when && evaluateExpr(when, ctx) !== true)
793
- return [];
794
- holds = evaluateExpr(require, ctx) === true;
795
- }
796
- catch (e) {
797
- if (e instanceof ExprBudgetError)
798
- throw new RuleBudgetError(e.sentence);
799
- if (e instanceof ExprEvalError)
800
- throw new Error(e.sentence);
801
- throw e;
802
- }
803
- if (holds)
804
- return [];
805
- const repairList = repairs.flatMap((r) => {
806
- const act = document.acts?.[r.act];
807
- const args = {};
808
- if (subject && (act?.on || /^(edit|remove)-/.test(r.act)))
809
- args[SUBJECT_ARG] = subject.id;
810
- for (const [k, v] of Object.entries(r.args ?? {}))
811
- args[k] = refName(v) === "subject" ? subject?.id : v;
812
- const declared = act ? argsOf(r.act, act, effectsOf(act, document), document, edges) : new Map();
813
- const missing = [...declared].filter(([k, a]) => a.required && args[k] === undefined).map(([k]) => k);
814
- return [{ mutation: r.act, label: r.label ?? act?.title ?? r.act.replace(/-/g, " "), args, ...(missing.length > 0 ? { missing } : {}) }];
815
- });
816
- return [
817
- {
818
- invariant: name,
819
- ...(subject ? { subjectId: subject.id } : {}),
820
- label: title,
821
- message: (says ? renderTemplate(says, { node: subject, kinds: shapes, graph, today: today(), money }) : subject ? `${labelFor(subject)}: ${title}` : title),
822
- nodeIds: subject ? [subject.id] : [],
823
- repairs: repairList,
824
- },
825
- ];
548
+ rules[name] = {
549
+ title: rule.title ?? name.replace(/-/g, " "),
550
+ require: parseExpr(rule.require),
551
+ ...(rule.when ? { when: parseExpr(rule.when) } : {}),
552
+ ...(rule.says ? { says: parseTemplate(rule.says) } : {}),
826
553
  };
827
- invariants.push((rule.over === "graph"
828
- ? defineInvariant(name, {
829
- scope: "graph",
830
- label: title,
831
- judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
832
- ...(rule.description ? { description: rule.description } : {}),
833
- ...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
834
- evaluate: ({ graph }) => judge(graph, null),
835
- })
836
- : defineInvariant(name, {
837
- scope: { kind: rule.over },
838
- label: title,
839
- judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
840
- ...(rule.description ? { description: rule.description } : {}),
841
- ...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
842
- evaluate: ({ graph, subject }) => judge(graph, subject),
843
- })));
844
- }
845
- // ── policy, brand, the rest ──────────────────────────────────────────────
846
- /*
847
- * WHO SEES WHAT is the app's, in the framework's one meaning (FR-02):
848
- * absent, everybody sees everything; present, a kind no sight names is
849
- * seen by nobody but the system, and `own` is the principal's own records.
850
- */
851
- const policy = document.policy
852
- ? {
853
- grants: document.policy.grants,
854
- ...(document.policy.sees ? { sees: document.policy.sees } : {}),
855
- ...(document.roles ? { roles: document.roles } : {}),
856
- }
857
- : undefined;
858
- let brand;
859
- if (document.brand) {
860
- const derived = accent ? brandFromAccent({ accent, base: SCHEMES }) : undefined;
861
- // A colour that cannot be read is not a reason to refuse an app: it wears the default colours and says why.
862
- if (derived && !derived.ok)
863
- findings.push(warning("brand", "brand.accent", `that accent cannot make a readable brand, so the app keeps Graview's colours: ${derived.why}`, "pick a colour further from orange-red, or a darker one"));
864
- // The app's money stands with or without an accent (FR-100).
865
- if (!derived || derived.ok || money.currency || money.locale)
866
- brand = { name: wordmark ?? document.name, schemes: derived?.ok ? derived.schemes : SCHEMES, ...money };
867
554
  }
868
- if (hasErrors(findings))
869
- return { ok: false, findings };
870
- const app = {
871
- name: document.name,
872
- schema,
873
- mutations,
874
- invariants,
875
- ...(policy ? { policy } : {}),
876
- ...(brand ? { brand } : {}),
877
- ...(document.modules ? { modules: document.modules } : {}),
878
- ...(document.lenses ? { lenses: document.lenses } : {}),
879
- // The arrangement is the app's (FR-80): every face reads it beside the places.
880
- ...(document.pages ? { pages: document.pages } : {}),
881
- ...(document.settings ? { settings: document.settings } : {}),
882
- // The document's views are the declaration's view specs (FR-03): data the framework draws.
883
- ...(Object.keys(viewsOf(document)).length > 0 ? { viewSpecs: viewsOf(document) } : {}),
884
- // And its home view, about no one record, is the app's home (FR-81).
885
- ...(homeOf(document) ? { home: homeOf(document) } : {}),
886
- version: document.version ?? 1,
555
+ // ── brand, views ─────────────────────────────────────────────────────────
556
+ // Every document's: the app is called what its document calls it, with the line under it (FR-124, FR-125).
557
+ const brand = brandOf(document, findings);
558
+ const viewSpecs = viewsOf(document);
559
+ const home = homeOf(document);
560
+ return {
561
+ format: COMPILED_FORMAT,
562
+ document,
563
+ findings,
564
+ kinds,
565
+ edges: [...edges].map(([name, { from, to, cardinality }]) => [name, { from, to, cardinality }]),
566
+ acts,
567
+ rules,
568
+ brand,
569
+ ...(Object.keys(viewSpecs).length > 0 ? { viewSpecs } : {}),
570
+ ...(home ? { home } : {}),
887
571
  };
888
- // `toDocument(app)` gives this document back, byte for byte.
889
- rememberDocument(app, document);
890
- return { ok: true, app, document, findings, sights: document.policy?.sees, kinds: shapes };
891
572
  }
892
573
  /** Map a framework check's "where" (a kind, an act, a rule by name) back to a document path when we can. */
893
574
  export function frameworkPath(where, document) {