@graview/core 0.1.12 → 0.1.15

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 (176) hide show
  1. package/README.md +31 -2
  2. package/dist/address.d.ts +31 -0
  3. package/dist/address.d.ts.map +1 -0
  4. package/dist/address.js +53 -0
  5. package/dist/address.js.map +1 -0
  6. package/dist/app.d.ts +1 -1
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/arrange.d.ts +13 -183
  9. package/dist/arrange.d.ts.map +1 -1
  10. package/dist/arrange.js +12 -470
  11. package/dist/arrange.js.map +1 -1
  12. package/dist/arrangement.d.ts +169 -0
  13. package/dist/arrangement.d.ts.map +1 -0
  14. package/dist/arrangement.js +220 -0
  15. package/dist/arrangement.js.map +1 -0
  16. package/dist/arranging.d.ts +28 -0
  17. package/dist/arranging.d.ts.map +1 -0
  18. package/dist/arranging.js +264 -0
  19. package/dist/arranging.js.map +1 -0
  20. package/dist/capabilities.js +1 -1
  21. package/dist/capabilities.js.map +1 -1
  22. package/dist/cli/check/hidden-reads.d.ts.map +1 -1
  23. package/dist/cli/check/hidden-reads.js +32 -1
  24. package/dist/cli/check/hidden-reads.js.map +1 -1
  25. package/dist/cli/check/lenses.d.ts.map +1 -1
  26. package/dist/cli/check/lenses.js +29 -1
  27. package/dist/cli/check/lenses.js.map +1 -1
  28. package/dist/cli/describe.d.ts.map +1 -1
  29. package/dist/cli/describe.js +16 -6
  30. package/dist/cli/describe.js.map +1 -1
  31. package/dist/cli/docs.js +1 -1
  32. package/dist/cli/docs.js.map +1 -1
  33. package/dist/columns.d.ts +48 -16
  34. package/dist/columns.d.ts.map +1 -1
  35. package/dist/columns.js +65 -22
  36. package/dist/columns.js.map +1 -1
  37. package/dist/compiled.d.ts +19 -0
  38. package/dist/compiled.d.ts.map +1 -0
  39. package/dist/compiled.js +60 -0
  40. package/dist/compiled.js.map +1 -0
  41. package/dist/conformance/fixtures.d.ts.map +1 -1
  42. package/dist/conformance/fixtures.js +41 -1
  43. package/dist/conformance/fixtures.js.map +1 -1
  44. package/dist/describe.d.ts +2 -2
  45. package/dist/describe.d.ts.map +1 -1
  46. package/dist/describe.js +1 -1
  47. package/dist/describe.js.map +1 -1
  48. package/dist/document/act-findings.d.ts +5 -0
  49. package/dist/document/act-findings.d.ts.map +1 -0
  50. package/dist/document/act-findings.js +84 -0
  51. package/dist/document/act-findings.js.map +1 -0
  52. package/dist/document/blocks.d.ts.map +1 -1
  53. package/dist/document/blocks.js +2 -1
  54. package/dist/document/blocks.js.map +1 -1
  55. package/dist/document/compile-checked.d.ts.map +1 -1
  56. package/dist/document/compile-checked.js +11 -4
  57. package/dist/document/compile-checked.js.map +1 -1
  58. package/dist/document/compile.d.ts +7 -21
  59. package/dist/document/compile.d.ts.map +1 -1
  60. package/dist/document/compile.js +145 -371
  61. package/dist/document/compile.js.map +1 -1
  62. package/dist/document/compiled.d.ts +130 -0
  63. package/dist/document/compiled.d.ts.map +1 -0
  64. package/dist/document/compiled.js +466 -0
  65. package/dist/document/compiled.js.map +1 -0
  66. package/dist/document/describe-coverage.d.ts +37 -0
  67. package/dist/document/describe-coverage.d.ts.map +1 -0
  68. package/dist/document/describe-coverage.js +162 -0
  69. package/dist/document/describe-coverage.js.map +1 -0
  70. package/dist/document/describe-place.d.ts.map +1 -1
  71. package/dist/document/describe-place.js +14 -1
  72. package/dist/document/describe-place.js.map +1 -1
  73. package/dist/document/diff.d.ts +5 -0
  74. package/dist/document/diff.d.ts.map +1 -1
  75. package/dist/document/diff.js +12 -3
  76. package/dist/document/diff.js.map +1 -1
  77. package/dist/document/edit.d.ts +1 -1
  78. package/dist/document/edit.d.ts.map +1 -1
  79. package/dist/document/edit.js +64 -7
  80. package/dist/document/edit.js.map +1 -1
  81. package/dist/document/far-end.d.ts +15 -0
  82. package/dist/document/far-end.d.ts.map +1 -0
  83. package/dist/document/far-end.js +26 -0
  84. package/dist/document/far-end.js.map +1 -0
  85. package/dist/document/index.d.ts +5 -2
  86. package/dist/document/index.d.ts.map +1 -1
  87. package/dist/document/index.js +3 -1
  88. package/dist/document/index.js.map +1 -1
  89. package/dist/document/migrate.d.ts.map +1 -1
  90. package/dist/document/migrate.js +7 -5
  91. package/dist/document/migrate.js.map +1 -1
  92. package/dist/document/range.d.ts +17 -0
  93. package/dist/document/range.d.ts.map +1 -0
  94. package/dist/document/range.js +46 -0
  95. package/dist/document/range.js.map +1 -0
  96. package/dist/document/remembered.d.ts +6 -0
  97. package/dist/document/remembered.d.ts.map +1 -0
  98. package/dist/document/remembered.js +16 -0
  99. package/dist/document/remembered.js.map +1 -0
  100. package/dist/document/rules.d.ts.map +1 -1
  101. package/dist/document/rules.js +2 -1
  102. package/dist/document/rules.js.map +1 -1
  103. package/dist/document/schema.d.ts +23 -0
  104. package/dist/document/schema.d.ts.map +1 -1
  105. package/dist/document/schema.js +26 -2
  106. package/dist/document/schema.js.map +1 -1
  107. package/dist/document/template-parse.d.ts +18 -0
  108. package/dist/document/template-parse.d.ts.map +1 -0
  109. package/dist/document/template-parse.js +124 -0
  110. package/dist/document/template-parse.js.map +1 -0
  111. package/dist/document/template.d.ts +1 -17
  112. package/dist/document/template.d.ts.map +1 -1
  113. package/dist/document/template.js +6 -115
  114. package/dist/document/template.js.map +1 -1
  115. package/dist/document/thumbnail.d.ts +25 -1
  116. package/dist/document/thumbnail.d.ts.map +1 -1
  117. package/dist/document/thumbnail.js +167 -32
  118. package/dist/document/thumbnail.js.map +1 -1
  119. package/dist/document/to-document.d.ts +1 -4
  120. package/dist/document/to-document.d.ts.map +1 -1
  121. package/dist/document/to-document.js +18 -11
  122. package/dist/document/to-document.js.map +1 -1
  123. package/dist/document/views.d.ts.map +1 -1
  124. package/dist/document/views.js +1 -1
  125. package/dist/document/views.js.map +1 -1
  126. package/dist/index.d.ts +4 -3
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +4 -3
  129. package/dist/index.js.map +1 -1
  130. package/dist/mutations/define-mutation.d.ts +12 -0
  131. package/dist/mutations/define-mutation.d.ts.map +1 -1
  132. package/dist/mutations/define-mutation.js +38 -2
  133. package/dist/mutations/define-mutation.js.map +1 -1
  134. package/dist/mutations/derive-edits.d.ts +8 -0
  135. package/dist/mutations/derive-edits.d.ts.map +1 -1
  136. package/dist/mutations/derive-edits.js +30 -7
  137. package/dist/mutations/derive-edits.js.map +1 -1
  138. package/dist/mutations/form.d.ts +2 -0
  139. package/dist/mutations/form.d.ts.map +1 -1
  140. package/dist/mutations/form.js +1 -0
  141. package/dist/mutations/form.js.map +1 -1
  142. package/dist/mutations/node-ref.d.ts +4 -1
  143. package/dist/mutations/node-ref.d.ts.map +1 -1
  144. package/dist/mutations/node-ref.js +6 -0
  145. package/dist/mutations/node-ref.js.map +1 -1
  146. package/dist/mutations/types.d.ts +16 -0
  147. package/dist/mutations/types.d.ts.map +1 -1
  148. package/dist/places.js +1 -1
  149. package/dist/places.js.map +1 -1
  150. package/dist/refusal.d.ts +11 -6
  151. package/dist/refusal.d.ts.map +1 -1
  152. package/dist/refusal.js +15 -6
  153. package/dist/refusal.js.map +1 -1
  154. package/dist/refused.d.ts +27 -0
  155. package/dist/refused.d.ts.map +1 -0
  156. package/dist/refused.js +31 -0
  157. package/dist/refused.js.map +1 -0
  158. package/dist/scaffold/project.d.ts.map +1 -1
  159. package/dist/scaffold/project.js +4 -0
  160. package/dist/scaffold/project.js.map +1 -1
  161. package/dist/schema/json-schema.d.ts.map +1 -1
  162. package/dist/schema/json-schema.js +4 -1
  163. package/dist/schema/json-schema.js.map +1 -1
  164. package/dist/search.d.ts +1 -1
  165. package/dist/search.d.ts.map +1 -1
  166. package/dist/search.js +1 -1
  167. package/dist/search.js.map +1 -1
  168. package/dist/theme/derive.d.ts.map +1 -1
  169. package/dist/theme/derive.js +5 -4
  170. package/dist/theme/derive.js.map +1 -1
  171. package/dist/theme/types.d.ts.map +1 -1
  172. package/dist/theme/types.js +5 -0
  173. package/dist/theme/types.js.map +1 -1
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +9 -1
@@ -1,18 +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 { brandFromAccent, SCHEMES } from "../index.js";
2
+ import { ActRefusal } from "../refused.js";
4
3
  import { analyzeExpr } from "./expr/analyze.js";
5
- import { rememberDocument } from "./to-document.js";
6
- import { evaluateExpr, ExprBudgetError, ExprEvalError } from "./expr/evaluate.js";
7
4
  import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
8
5
  import { error, hasErrors, warning } from "./findings.js";
9
6
  import { DocumentSpec, MAX_DOCUMENT_BYTES, } from "./schema.js";
10
- import { parseTemplate, renderTemplate, TemplateError } from "./template.js";
7
+ import { parseTemplate, TemplateError } from "./template-parse.js";
11
8
  import { upgradeDocument } from "./upgrade.js";
12
9
  import { homeOf, validateViews, viewsOf } from "./views.js";
10
+ import { farEnd } from "./far-end.js";
13
11
  import { computedOf, parsedComputed, validateComputed, workedOutAlone } from "./computed.js";
12
+ import { build, COMPILED_FORMAT, defaultLabelField, refName, SUBJECT_ARG, } from "./compiled.js";
14
13
  const BUILTIN_REFS = new Set(["subject", "now", "today"]);
15
- const SUBJECT_ARG = "id";
16
14
  /** Parse and validate without compiling — the document's own sentences, before the framework's. */
17
15
  export function readDocument(raw) {
18
16
  if (typeof raw === "string") {
@@ -88,6 +86,13 @@ export function kindShapes(document) {
88
86
  export function effectsOf(act, document) {
89
87
  // Shorthands first, then any listed effects: an act may say "writes quote" and also set a status.
90
88
  const effects = [];
89
+ const subject = asArray(act.on);
90
+ // A relation joins its subject from whichever end the declaration puts it (FR-115): `owns` on a component links from the person given.
91
+ const link = (relation, verb) => {
92
+ const reversed = farEnd(document.kinds, relation, subject)?.reversed === true;
93
+ const [from, to] = reversed ? ["$to", "$subject"] : ["$subject", "$to"];
94
+ return verb === "connect" ? { connect: relation, from, to } : { sever: relation, from, to };
95
+ };
91
96
  if (act.creates) {
92
97
  const kind = document.kinds[act.creates];
93
98
  const set = {};
@@ -104,17 +109,22 @@ export function effectsOf(act, document) {
104
109
  // An act that makes a record already asks for each of its fields: `writes` beside `creates` (and no `on`) names some of them again, not a subject.
105
110
  if (act.writes && !(act.creates && act.on === undefined))
106
111
  effects.push({ set: Object.fromEntries(act.writes.map((f) => [f, `$${f}`])) });
107
- if (act.connects)
108
- effects.push({ connect: act.connects, from: "$subject", to: "$to" });
112
+ if (act.connects) {
113
+ if (act.replaces)
114
+ effects.push({ replace: act.replaces === true ? [act.connects] : [...act.replaces], keep: act.connects });
115
+ effects.push(link(act.connects, "connect"));
116
+ }
109
117
  if (act.severs)
110
- effects.push({ sever: act.severs, from: "$subject", to: "$to" });
118
+ effects.push(link(act.severs, "sever"));
119
+ // What it sets on the record at the other end (FR-115): that record is its `$to`.
120
+ if (act.setsOther && (act.connects || act.severs))
121
+ effects.push({ set: act.setsOther, target: "$to" });
111
122
  if (act.removes)
112
123
  effects.push({ remove: "$subject" });
113
124
  if (act.effects)
114
125
  effects.push(...act.effects.filter((e) => !(act.creates && "set" in e && !("create" in e) && refName(e.target) === "new")));
115
126
  return effects;
116
127
  }
117
- const refName = (v) => (typeof v === "string" && /^\$[A-Za-z][A-Za-z0-9]*$/.test(v) ? v.slice(1) : undefined);
118
128
  function tryExpr(source, path, findings) {
119
129
  try {
120
130
  const expr = parseExpr(source);
@@ -336,75 +346,30 @@ function validate(document) {
336
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}`));
337
347
  return findings;
338
348
  }
339
- /*
340
- * A FIELD'S SCHEMA, in zod/mini (schema/zod.ts): a page that compiles a
341
- * document in the browser pays for the checks it calls, not for classic
342
- * zod whole (FR-57). The checks, their messages and their definitions are
343
- * the ones classic's methods make.
344
- */
345
- function fieldSchema(spec, optional) {
346
- let schema;
347
- switch (spec.type) {
348
- case "string":
349
- schema = z.string().check(z.maxLength(500));
350
- break;
351
- case "text":
352
- schema = z.string().check(z.maxLength(20_000));
353
- break;
354
- case "number":
355
- schema = z.number();
356
- break;
357
- case "integer":
358
- schema = z.number().check(z.int());
359
- break;
360
- case "boolean":
361
- schema = z.boolean();
362
- break;
363
- case "date":
364
- schema = z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/, "a date like 2026-10-02"));
365
- break;
366
- case "datetime":
367
- schema = z.string().check(z.refine((s) => !Number.isNaN(Date.parse(s)), "a date and time like 2026-10-02T14:30:00Z"));
368
- break;
369
- case "enum":
370
- schema = z.enum(spec.options);
371
- break;
372
- case "list":
373
- 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));
374
- break;
375
- case "url":
376
- schema = z.url();
377
- break;
378
- case "email":
379
- schema = z.email();
380
- break;
381
- }
382
- if (spec.description)
383
- schema = schema.check(z.describe(spec.description));
384
- return optional ? z.optional(schema) : schema;
385
- }
386
- /** What one of these is called, when the kind says nothing: the first required word-ish field. */
387
- function defaultLabelField(spec) {
388
- const entries = Object.entries(spec.fields);
389
- return (entries.find(([, f]) => f.required && (f.type === "string" || f.type === "text"))?.[0] ??
390
- entries.find(([, f]) => f.type === "string")?.[0]);
391
- }
392
349
  /** The arguments an act asks for: declared ones, then whatever its effects reference. */
393
- function argsOf(name, act, effects, document, edges) {
350
+ function argsOf(act, effects, document, edges) {
394
351
  const args = new Map();
395
352
  const subjectKinds = asArray(act.on);
396
353
  const created = new Map();
354
+ /** The kinds a record argument names, by argument: what a `set` on it (a `setsOther`) is checked against. */
355
+ const named = new Map();
397
356
  const usesSubject = subjectKinds.length > 0 ||
398
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"));
399
358
  if (usesSubject)
400
- args.set(SUBJECT_ARG, { schema: refTo(subjectKinds.length > 0 ? subjectKinds : "*"), required: true });
359
+ args.set(SUBJECT_ARG, { ref: subjectKinds.length > 0 ? subjectKinds : "*", required: true });
401
360
  const declared = act.args ?? {};
402
361
  const writesOnlyOne = act.writes?.length === 1;
403
- const note = (arg, schema) => {
362
+ /*
363
+ * A declared argument that fills a number field and says no range of its
364
+ * own is asked for within the field's (FR-114): the form will not take
365
+ * what the record would refuse.
366
+ */
367
+ const withRangeOf = (d, fed) => fed && d.type === fed.type && d.min === undefined && d.max === undefined && d.step === undefined ? { ...d, ...(fed.min === undefined ? {} : { min: fed.min }), ...(fed.max === undefined ? {} : { max: fed.max }), ...(fed.step === undefined ? {} : { step: fed.step }) } : d;
368
+ const note = (arg, schema, fed) => {
404
369
  if (arg === SUBJECT_ARG || BUILTIN_REFS.has(arg) || created.has(arg) || args.has(arg))
405
370
  return;
406
- const d = declared[arg];
407
- args.set(arg, d ? { schema: fieldSchema(d, !d.required), required: Boolean(d.required) } : schema());
371
+ const d = declared[arg] ? withRangeOf(declared[arg], fed) : undefined;
372
+ args.set(arg, d ? { field: d, optional: !d.required, required: Boolean(d.required) } : schema());
408
373
  };
409
374
  for (const effect of effects) {
410
375
  if ("create" in effect) {
@@ -414,7 +379,7 @@ function argsOf(name, act, effects, document, edges) {
414
379
  const spec = kind?.fields[field];
415
380
  if (arg && spec) {
416
381
  const required = Boolean(spec.required) && spec.default === undefined;
417
- note(arg, () => ({ schema: fieldSchema(spec, !required), required }));
382
+ note(arg, () => ({ field: spec, optional: !required, required }), spec);
418
383
  }
419
384
  }
420
385
  if (effect.as)
@@ -427,36 +392,35 @@ function argsOf(name, act, effects, document, edges) {
427
392
  if (!arg)
428
393
  continue;
429
394
  const kinds = end === "to" ? edge?.to ?? "*" : edge?.from ?? "*";
430
- note(arg, () => ({ schema: refTo(kinds === "*" ? "*" : kinds), required: true }));
395
+ if (kinds !== "*" && !named.has(arg))
396
+ named.set(arg, kinds);
397
+ note(arg, () => ({ ref: kinds, required: true }));
431
398
  }
432
399
  }
433
400
  else if ("set" in effect) {
434
401
  const target = refName(effect.target ?? "$subject");
435
- const targetKind = target === "subject" ? subjectKinds[0] : created.get(target ?? "");
402
+ const targetKind = target === "subject" ? subjectKinds[0] : (created.get(target ?? "") ?? named.get(target ?? "")?.[0]);
436
403
  if (target && target !== "subject" && !created.has(target))
437
- note(target, () => ({ schema: refTo("*"), required: true }));
404
+ note(target, () => ({ ref: "*", required: true }));
438
405
  for (const [field, value] of Object.entries(effect.set)) {
439
406
  const arg = refName(value);
440
407
  if (!arg)
441
408
  continue;
442
409
  const spec = targetKind ? document.kinds[targetKind]?.fields[field] : undefined;
443
- note(arg, () => (spec ? { schema: fieldSchema(spec, !writesOnlyOne), required: writesOnlyOne } : { schema: z.optional(z.unknown()), required: false }));
410
+ note(arg, () => (spec ? { field: spec, optional: !writesOnlyOne, required: writesOnlyOne } : { any: true, required: false }), spec);
444
411
  }
445
412
  }
446
413
  else if ("remove" in effect) {
447
414
  const arg = refName(effect.remove);
448
415
  if (arg)
449
- note(arg, () => ({ schema: refTo("*"), required: true }));
416
+ note(arg, () => ({ ref: "*", required: true }));
450
417
  }
451
418
  }
452
419
  for (const [arg, d] of Object.entries(declared))
453
420
  if (!args.has(arg))
454
- args.set(arg, { schema: fieldSchema(d, !d.required), required: Boolean(d.required) });
455
- void name;
421
+ args.set(arg, { field: d, optional: !d.required, required: Boolean(d.required) });
456
422
  return args;
457
423
  }
458
- class Refused extends Error {
459
- }
460
424
  /**
461
425
  * A DOCUMENT COMPILED WITHOUT THE FRAMEWORK'S CHECKER: its own sentences
462
426
  * still judged, `checkApp` not asked. For a page drawing a document its host
@@ -473,71 +437,80 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
473
437
  const findings = [...read.findings];
474
438
  if (options.previous)
475
439
  findings.push(...renamesFromNothing(document, options.previous));
476
- const today = options.today ?? (() => new Date().toISOString().slice(0, 10));
477
- 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) {
478
452
  const edges = edgeIndex(document);
479
- // What `{x | money}` says in a label, a sentence or a refusal: the app's currency and locale (FR-100).
480
- const { accent, name: wordmark, ...money } = document.brand ?? {};
481
453
  // ── kinds ────────────────────────────────────────────────────────────────
482
- const definitions = Object.entries(document.kinds).map(([kind, spec]) => {
483
- const shape = {};
484
- for (const [field, f] of Object.entries(spec.fields))
485
- shape[field] = fieldSchema(f, !f.required);
486
- const labelSource = spec.label ?? (defaultLabelField(spec) ? `{${defaultLabelField(spec)}}` : undefined);
487
- const labelParts = labelSource ? parseTemplate(labelSource) : undefined;
488
- const describeParts = spec.describe ? parseTemplate(spec.describe) : undefined;
489
- const computed = computedOf(spec);
490
- // A computed field's words are said like a field's: its label, where it has one.
491
- const labels = Object.fromEntries([
492
- ...Object.entries(spec.fields).filter(([, f]) => f.label).map(([n, f]) => [n, f.label]),
493
- ...[...computed].filter(([, c]) => c.label).map(([n, c]) => [n, c.label]),
494
- ]);
495
- // Defaults stay out of the zod schema (hydration must not invent values) and ride beside it, for a repair to read.
496
- const defaults = Object.fromEntries(Object.entries(spec.fields).filter(([, f]) => f.default !== undefined).map(([n, f]) => [n, f.default]));
497
- const edgeDecls = Object.fromEntries(Object.entries(spec.edges ?? {}).map(([n, e]) => [
498
- n,
499
- {
500
- to: e.to,
501
- ...(e.cardinality ? { cardinality: e.cardinality } : {}),
502
- ...(e.description ? { description: e.description } : {}),
503
- ...(e.inverse ? { inverse: e.inverse } : {}),
504
- ...(e.appendOnly ? { appendOnly: e.appendOnly } : {}),
505
- },
506
- ]));
507
- return defineNode(kind, {
508
- fields: z.object(shape),
509
- edges: edgeDecls,
510
- ...(spec.plural ? { plural: spec.plural } : {}),
511
- ...(spec.noun ? { noun: spec.noun } : {}),
512
- ...(spec.description ? { description: spec.description } : {}),
513
- ...(labelParts ? { label: (node) => renderTemplate(labelParts, { node: node, kinds: shapes, today: today(), money }) || node.id } : {}),
514
- ...(describeParts ? { describe: (node) => renderTemplate(describeParts, { node: node, kinds: shapes, today: today(), money }) } : {}),
515
- ...(spec.lifecycle ? { lifecycle: { field: spec.lifecycle.field, retired: spec.lifecycle.retired } } : {}),
516
- ...(spec.figure ? { figure: spec.figure } : {}),
517
- ...(spec.computed && Object.keys(spec.computed).length > 0 ? { computed: spec.computed } : {}),
518
- ...(Object.keys(labels).length > 0 || spec.glance ? { display: { ...(Object.keys(labels).length > 0 ? { labels } : {}), ...(spec.glance ? { glance: [...spec.glance] } : {}) } } : {}),
519
- ...(Object.keys(defaults).length > 0 ? { defaults } : {}),
520
- });
521
- });
522
- 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
+ }
523
465
  // ── acts ─────────────────────────────────────────────────────────────────
524
- const mutations = [];
466
+ const acts = {};
525
467
  for (const [name, act] of Object.entries(document.acts ?? {})) {
526
468
  const effects = effectsOf(act, document);
527
- const args = argsOf(name, act, effects, document, edges);
528
- const shape = {};
529
- for (const [arg, a] of args)
530
- shape[arg] = a.schema;
531
- const subjectKinds = asArray(act.on);
532
- const title = act.title ?? name.replace(/-/g, " ");
533
- const guard = act.allowedWhen ? parseExpr(act.allowedWhen) : undefined;
534
- const refusal = act.refusal ? parseTemplate(act.refusal) : undefined;
535
- const destructive = act.destructive ?? effects.some((e) => "remove" in e || "sever" in e);
469
+ const args = argsOf(act, effects, document, edges);
470
+ const destructive = act.destructive ?? effects.some((e) => "remove" in e || "sever" in e || "replace" in e);
536
471
  const creates = [...new Set(effects.flatMap((e) => ("create" in e ? [e.create] : [])))];
537
472
  const connects = [...new Set(effects.flatMap((e) => ("connect" in e ? [e.connect] : [])))];
538
- const severs = [...new Set(effects.flatMap((e) => ("sever" in e ? [e.sever] : [])))];
473
+ // What it may sever: what it says it severs, and what it replaces (FR-115).
474
+ const severs = [...new Set(effects.flatMap((e) => ("sever" in e ? [e.sever] : "replace" in e ? e.replace : [])))];
539
475
  // An act WRITES only what it sets on its subject; setting fields on a record it just made is part of making it.
540
476
  const writes = [...new Set(effects.flatMap((e) => ("set" in e && !("create" in e) && (refName(e.target ?? "$subject") === "subject") ? Object.keys(e.set) : [])))];
477
+ /*
478
+ * And what it sets on the record at an end of a link it makes or breaks
479
+ * (FR-115's `setsOther`), by that end's kinds: not the subject's, but a
480
+ * field another act sets, so no derived edit offers it freely.
481
+ */
482
+ const ends = new Map();
483
+ for (const e of effects) {
484
+ if (!("connect" in e) && !("sever" in e))
485
+ continue;
486
+ const edge = edges.get("connect" in e ? e.connect : e.sever);
487
+ for (const [end, value] of [["from", e.from], ["to", e.to]]) {
488
+ const ref = refName(value);
489
+ const kinds = end === "to" ? edge?.to : edge?.from;
490
+ if (ref && ref !== "subject" && kinds && kinds !== "*")
491
+ ends.set(ref, [...new Set([...(ends.get(ref) ?? []), ...kinds])]);
492
+ }
493
+ }
494
+ const writesOther = {};
495
+ for (const e of effects) {
496
+ if (!("set" in e) || "create" in e)
497
+ continue;
498
+ const target = refName(e.target ?? "$subject");
499
+ for (const kind of (target && target !== "subject" && ends.get(target)) || [])
500
+ writesOther[kind] = [...new Set([...(writesOther[kind] ?? []), ...Object.keys(e.set)])];
501
+ }
502
+ // What it sets on its subject to a fixed value, whatever it is told — a named step (FR-108), resolved as `apply` resolves a literal.
503
+ const sets = {};
504
+ for (const e of effects) {
505
+ if (!("set" in e) || "create" in e || refName(e.target ?? "$subject") !== "subject")
506
+ continue;
507
+ for (const [field, v] of Object.entries(e.set ?? {})) {
508
+ if ((typeof v === "string" && refName(v) === undefined) || typeof v === "number" || typeof v === "boolean")
509
+ sets[field] = typeof v === "string" && v.startsWith("$$") ? v.slice(1) : v;
510
+ else
511
+ delete sets[field];
512
+ }
513
+ }
541
514
  /*
542
515
  * Twice is once when nothing is made and every value set is given
543
516
  * (a literal or an argument), never computed from what is there.
@@ -552,227 +525,37 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
552
525
  if (v && typeof v === "object" && !Array.isArray(v) && "expr" in v)
553
526
  exprs.set(v.expr, parseExpr(v.expr));
554
527
  }
555
- mutations.push(defineMutation(name, {
556
- input: z.object(shape),
557
- title,
558
- ...(act.description ? { description: act.description } : {}),
559
- ...(act.fromTheOtherEnd ? { fromTheOtherEnd: act.fromTheOtherEnd } : {}),
560
- ...(args.has(SUBJECT_ARG) ? { subject: { kinds: subjectKinds.length > 0 ? subjectKinds : "*", arg: SUBJECT_ARG } } : {}),
561
- ...(destructive ? { destructive: true } : {}),
562
- ...(idempotent ? { idempotent: true } : {}),
563
- ...(creates.length > 0 ? { creates } : {}),
564
- ...(connects.length > 0 ? { connects } : {}),
565
- ...(severs.length > 0 ? { severs } : {}),
566
- ...(writes.length > 0 ? { writes } : {}),
567
- describe: (input, graph) => {
568
- const subject = typeof input[SUBJECT_ARG] === "string" ? graph.getNode(input[SUBJECT_ARG]) : undefined;
569
- const what = subject ? labelFor(subject) : Object.values(input).find((v) => typeof v === "string");
570
- return what ? `${title}: ${String(what)}` : title;
571
- },
572
- apply(context, input) {
573
- const graph = context.graph;
574
- const subjectId = typeof input[SUBJECT_ARG] === "string" ? input[SUBJECT_ARG] : undefined;
575
- const subject = subjectId ? graph.getNode(subjectId) : undefined;
576
- if (subjectId && !subject)
577
- throw new Refused(`there is no record "${subjectId}"`);
578
- if (subject && subjectKinds.length > 0 && !subjectKinds.includes(subject.kind))
579
- throw new Refused(`"${title}" acts on ${subjectKinds.join(" or ")}, not on ${withArticle(subject.kind)}`);
580
- const bindings = {};
581
- for (const [k, v] of Object.entries(input))
582
- if (v !== undefined)
583
- bindings[k] = v;
584
- if (guard && subject) {
585
- let allowed;
586
- try {
587
- allowed = evaluateExpr(guard, { graph, subject, kinds: shapes, bindings, today: today() });
588
- }
589
- catch (e) {
590
- if (e instanceof ExprEvalError)
591
- throw new Refused(`"${title}" could not be judged: ${e.sentence}`);
592
- throw e;
593
- }
594
- if (allowed !== true) {
595
- throw new Refused(refusal ? renderTemplate(refusal, { node: subject, kinds: shapes, graph, bindings, today: today(), money }) : `"${title}" is not allowed for ${labelFor(subject)} right now`);
596
- }
597
- }
598
- const made = new Map();
599
- const resolve = (value, current) => {
600
- if (value && typeof value === "object" && !Array.isArray(value) && "expr" in value) {
601
- try {
602
- return evaluateExpr(exprs.get(value.expr), { graph, subject: current ?? subject ?? null, kinds: shapes, bindings, today: today() });
603
- }
604
- catch (e) {
605
- if (e instanceof ExprEvalError)
606
- throw new Refused(`"${title}" could not work out a value: ${e.sentence}`);
607
- throw e;
608
- }
609
- }
610
- const ref = refName(value);
611
- if (ref === undefined)
612
- return typeof value === "string" && value.startsWith("$$") ? value.slice(1) : value;
613
- if (ref === "subject")
614
- return subjectId;
615
- if (ref === "now")
616
- return new Date().toISOString();
617
- if (ref === "today")
618
- return today();
619
- if (made.has(ref))
620
- return made.get(ref);
621
- return input[ref];
622
- };
623
- for (const effect of effects) {
624
- if ("create" in effect) {
625
- const kind = document.kinds[effect.create];
626
- const node = { kind: effect.create };
627
- for (const [field, f] of Object.entries(kind.fields))
628
- if (f.default !== undefined)
629
- node[field] = f.default;
630
- for (const [field, value] of Object.entries(effect.set ?? {})) {
631
- const v = resolve(value, undefined);
632
- if (v !== undefined)
633
- node[field] = v;
634
- }
635
- const labelField = defaultLabelField(kind);
636
- const id = context.freshId(String((labelField && node[labelField]) ?? effect.create), effect.create);
637
- context.addNode({ id, ...node });
638
- if (effect.as)
639
- made.set(effect.as, id);
640
- }
641
- else if ("connect" in effect || "sever" in effect) {
642
- const kind = "connect" in effect ? effect.connect : effect.sever;
643
- const from = String(resolve(effect.from, undefined) ?? "");
644
- const to = String(resolve(effect.to, undefined) ?? "");
645
- if (!graph.has(from) && ![...made.values()].includes(from))
646
- throw new Refused(`there is no record "${from}"`);
647
- if (!graph.has(to) && ![...made.values()].includes(to))
648
- throw new Refused(`there is no record "${to}"`);
649
- if ("connect" in effect) {
650
- if (edges.get(kind)?.cardinality === "one")
651
- for (const existing of graph.out(from, kind))
652
- if (existing.id !== to)
653
- context.removeEdge({ kind, from, to: existing.id });
654
- if (!graph.out(from, kind).some((n) => n.id === to))
655
- context.addEdge({ kind, from, to });
656
- }
657
- else {
658
- context.removeEdge({ kind, from, to });
659
- }
660
- }
661
- else if ("set" in effect) {
662
- const targetId = String(resolve(effect.target ?? "$subject", undefined) ?? "");
663
- const target = graph.getNode(targetId);
664
- if (!target && ![...made.values()].includes(targetId))
665
- throw new Refused(`there is no record "${targetId}"`);
666
- const patch = {};
667
- for (const [field, value] of Object.entries(effect.set)) {
668
- const v = resolve(value, target);
669
- if (v !== undefined)
670
- patch[field] = v;
671
- }
672
- if (Object.keys(patch).length > 0)
673
- context.patchNode(targetId, patch);
674
- }
675
- else if ("remove" in effect) {
676
- const id = String(resolve(effect.remove, undefined) ?? "");
677
- if (!graph.has(id))
678
- throw new Refused(`there is no record "${id}"`);
679
- context.removeNode(id);
680
- }
681
- }
682
- },
683
- }));
684
- }
685
- function labelFor(node) {
686
- const definition = schema.tryDefinition(node.kind);
687
- return definition?.label?.(node) ?? node.id;
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,
540
+ writes,
541
+ writesOther,
542
+ sets,
543
+ };
688
544
  }
689
545
  // ── rules ────────────────────────────────────────────────────────────────
690
- const invariants = [];
546
+ const rules = {};
691
547
  for (const [name, rule] of Object.entries(document.rules ?? {})) {
692
- const require = parseExpr(rule.require);
693
- const when = rule.when ? parseExpr(rule.when) : undefined;
694
- const says = rule.says ? parseTemplate(rule.says) : undefined;
695
- const title = rule.title ?? name.replace(/-/g, " ");
696
- const repairs = rule.repairs ?? [];
697
- const judge = (graph, subject) => {
698
- const ctx = { graph, subject, kinds: shapes, today: today() };
699
- let holds;
700
- /*
701
- * A RULE THAT CANNOT ANSWER IS THE ENGINE'S TO SAY (FR-29). Out of
702
- * budget it throws RuleBudgetError, and the violation's status is
703
- * `over-budget`; any other mistake (a word added to a number) is
704
- * `could-not-judge`. Both name the rule and its subject, and neither
705
- * is a hang or a crash that takes the other rules down.
706
- */
707
- try {
708
- if (when && evaluateExpr(when, ctx) !== true)
709
- return [];
710
- holds = evaluateExpr(require, ctx) === true;
711
- }
712
- catch (e) {
713
- if (e instanceof ExprBudgetError)
714
- throw new RuleBudgetError(e.sentence);
715
- if (e instanceof ExprEvalError)
716
- throw new Error(e.sentence);
717
- throw e;
718
- }
719
- if (holds)
720
- return [];
721
- const repairList = repairs.flatMap((r) => {
722
- const act = document.acts?.[r.act];
723
- const args = {};
724
- if (subject && (act?.on || /^(edit|remove)-/.test(r.act)))
725
- args[SUBJECT_ARG] = subject.id;
726
- for (const [k, v] of Object.entries(r.args ?? {}))
727
- args[k] = refName(v) === "subject" ? subject?.id : v;
728
- const declared = act ? argsOf(r.act, act, effectsOf(act, document), document, edges) : new Map();
729
- const missing = [...declared].filter(([k, a]) => a.required && args[k] === undefined).map(([k]) => k);
730
- return [{ mutation: r.act, label: r.label ?? act?.title ?? r.act.replace(/-/g, " "), args, ...(missing.length > 0 ? { missing } : {}) }];
731
- });
732
- return [
733
- {
734
- invariant: name,
735
- ...(subject ? { subjectId: subject.id } : {}),
736
- label: title,
737
- message: (says ? renderTemplate(says, { node: subject, kinds: shapes, graph, today: today(), money }) : subject ? `${labelFor(subject)}: ${title}` : title),
738
- nodeIds: subject ? [subject.id] : [],
739
- repairs: repairList,
740
- },
741
- ];
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) } : {}),
742
553
  };
743
- invariants.push((rule.over === "graph"
744
- ? defineInvariant(name, {
745
- scope: "graph",
746
- label: title,
747
- judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
748
- ...(rule.description ? { description: rule.description } : {}),
749
- ...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
750
- evaluate: ({ graph }) => judge(graph, null),
751
- })
752
- : defineInvariant(name, {
753
- scope: { kind: rule.over },
754
- label: title,
755
- judgement: { require: rule.require, ...(rule.when ? { when: rule.when } : {}), ...(rule.says ? { says: rule.says } : {}) },
756
- ...(rule.description ? { description: rule.description } : {}),
757
- ...(repairs.length > 0 ? { repairs: repairs.map((r) => r.act) } : {}),
758
- evaluate: ({ graph, subject }) => judge(graph, subject),
759
- })));
760
554
  }
761
- // ── policy, brand, the rest ──────────────────────────────────────────────
762
- /*
763
- * WHO SEES WHAT is the app's, in the framework's one meaning (FR-02):
764
- * absent, everybody sees everything; present, a kind no sight names is
765
- * seen by nobody but the system, and `own` is the principal's own records.
766
- */
767
- const policy = document.policy
768
- ? {
769
- grants: document.policy.grants,
770
- ...(document.policy.sees ? { sees: document.policy.sees } : {}),
771
- ...(document.roles ? { roles: document.roles } : {}),
772
- }
773
- : undefined;
555
+ // ── brand, views ─────────────────────────────────────────────────────────
774
556
  let brand;
775
557
  if (document.brand) {
558
+ const { accent, name: wordmark, ...money } = document.brand;
776
559
  const derived = accent ? brandFromAccent({ accent, base: SCHEMES }) : undefined;
777
560
  // A colour that cannot be read is not a reason to refuse an app: it wears the default colours and says why.
778
561
  if (derived && !derived.ok)
@@ -781,29 +564,20 @@ export function compileDocumentWithoutCheck(raw, options = {}) {
781
564
  if (!derived || derived.ok || money.currency || money.locale)
782
565
  brand = { name: wordmark ?? document.name, schemes: derived?.ok ? derived.schemes : SCHEMES, ...money };
783
566
  }
784
- if (hasErrors(findings))
785
- return { ok: false, findings };
786
- const app = {
787
- name: document.name,
788
- schema,
789
- mutations,
790
- invariants,
791
- ...(policy ? { policy } : {}),
567
+ const viewSpecs = viewsOf(document);
568
+ const home = homeOf(document);
569
+ return {
570
+ format: COMPILED_FORMAT,
571
+ document,
572
+ findings,
573
+ kinds,
574
+ edges: [...edges].map(([name, { from, to, cardinality }]) => [name, { from, to, cardinality }]),
575
+ acts,
576
+ rules,
792
577
  ...(brand ? { brand } : {}),
793
- ...(document.modules ? { modules: document.modules } : {}),
794
- ...(document.lenses ? { lenses: document.lenses } : {}),
795
- // The arrangement is the app's (FR-80): every face reads it beside the places.
796
- ...(document.pages ? { pages: document.pages } : {}),
797
- ...(document.settings ? { settings: document.settings } : {}),
798
- // The document's views are the declaration's view specs (FR-03): data the framework draws.
799
- ...(Object.keys(viewsOf(document)).length > 0 ? { viewSpecs: viewsOf(document) } : {}),
800
- // And its home view, about no one record, is the app's home (FR-81).
801
- ...(homeOf(document) ? { home: homeOf(document) } : {}),
802
- version: document.version ?? 1,
578
+ ...(Object.keys(viewSpecs).length > 0 ? { viewSpecs } : {}),
579
+ ...(home ? { home } : {}),
803
580
  };
804
- // `toDocument(app)` gives this document back, byte for byte.
805
- rememberDocument(app, document);
806
- return { ok: true, app, document, findings, sights: document.policy?.sees, kinds: shapes };
807
581
  }
808
582
  /** Map a framework check's "where" (a kind, an act, a rule by name) back to a document path when we can. */
809
583
  export function frameworkPath(where, document) {
@@ -829,7 +603,7 @@ export function inDocumentWords(finding, where, document) {
829
603
  const first = Object.keys(spec.fields).filter((field) => field !== "label").slice(0, 3);
830
604
  return { ...finding, path: `kinds.${kind}.glance`, fix: `Say which: "glance": [${first.map((field) => `"${field}"`).join(", ")}] on kinds.${kind} — the facts a person compares one by, at a glance.` };
831
605
  }
832
- export { Refused as ActRefusal };
606
+ export { ActRefusal };
833
607
  /** Every `renamedFrom` that names nothing in the version before: the values it was meant to carry would be lost. */
834
608
  function renamesFromNothing(document, previous) {
835
609
  const out = [];