@graview/core 0.1.10 → 0.1.14

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 (225) hide show
  1. package/README.md +41 -9
  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/blocks.d.ts +5 -0
  21. package/dist/blocks.d.ts.map +1 -0
  22. package/dist/blocks.js +14 -0
  23. package/dist/blocks.js.map +1 -0
  24. package/dist/capabilities.js +1 -1
  25. package/dist/capabilities.js.map +1 -1
  26. package/dist/check.d.ts +8 -0
  27. package/dist/check.d.ts.map +1 -0
  28. package/dist/check.js +19 -0
  29. package/dist/check.js.map +1 -0
  30. package/dist/cli/check/brand.d.ts.map +1 -1
  31. package/dist/cli/check/brand.js +21 -0
  32. package/dist/cli/check/brand.js.map +1 -1
  33. package/dist/cli/check/hidden-reads.d.ts +4 -0
  34. package/dist/cli/check/hidden-reads.d.ts.map +1 -0
  35. package/dist/cli/check/hidden-reads.js +183 -0
  36. package/dist/cli/check/hidden-reads.js.map +1 -0
  37. package/dist/cli/check/index.d.ts +1 -0
  38. package/dist/cli/check/index.d.ts.map +1 -1
  39. package/dist/cli/check/index.js +1 -0
  40. package/dist/cli/check/index.js.map +1 -1
  41. package/dist/cli/check/lenses.d.ts.map +1 -1
  42. package/dist/cli/check/lenses.js +29 -1
  43. package/dist/cli/check/lenses.js.map +1 -1
  44. package/dist/cli/check/views.d.ts.map +1 -1
  45. package/dist/cli/check/views.js +5 -1
  46. package/dist/cli/check/views.js.map +1 -1
  47. package/dist/cli/check.d.ts.map +1 -1
  48. package/dist/cli/check.js +2 -1
  49. package/dist/cli/check.js.map +1 -1
  50. package/dist/cli/create.d.ts.map +1 -1
  51. package/dist/cli/create.js +1 -1
  52. package/dist/cli/create.js.map +1 -1
  53. package/dist/cli/describe.d.ts.map +1 -1
  54. package/dist/cli/describe.js +23 -1
  55. package/dist/cli/describe.js.map +1 -1
  56. package/dist/cli/docs.js +1 -1
  57. package/dist/cli/docs.js.map +1 -1
  58. package/dist/cli/index.d.ts +1 -1
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +2 -1
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/columns.d.ts +105 -0
  63. package/dist/columns.d.ts.map +1 -0
  64. package/dist/columns.js +143 -0
  65. package/dist/columns.js.map +1 -0
  66. package/dist/conformance/fixtures.d.ts.map +1 -1
  67. package/dist/conformance/fixtures.js +19 -1
  68. package/dist/conformance/fixtures.js.map +1 -1
  69. package/dist/conformance/index.js +1 -1
  70. package/dist/conformance/index.js.map +1 -1
  71. package/dist/describe.d.ts +2 -0
  72. package/dist/describe.d.ts.map +1 -1
  73. package/dist/describe.js +7 -0
  74. package/dist/describe.js.map +1 -1
  75. package/dist/document/act-findings.d.ts +5 -0
  76. package/dist/document/act-findings.d.ts.map +1 -0
  77. package/dist/document/act-findings.js +84 -0
  78. package/dist/document/act-findings.js.map +1 -0
  79. package/dist/document/blocks.d.ts +11 -6
  80. package/dist/document/blocks.d.ts.map +1 -1
  81. package/dist/document/blocks.js +21 -31
  82. package/dist/document/blocks.js.map +1 -1
  83. package/dist/document/compile-checked.d.ts +8 -0
  84. package/dist/document/compile-checked.d.ts.map +1 -0
  85. package/dist/document/compile-checked.js +43 -0
  86. package/dist/document/compile-checked.js.map +1 -0
  87. package/dist/document/compile.d.ts +22 -9
  88. package/dist/document/compile.d.ts.map +1 -1
  89. package/dist/document/compile.js +116 -52
  90. package/dist/document/compile.js.map +1 -1
  91. package/dist/document/describe-coverage.d.ts +37 -0
  92. package/dist/document/describe-coverage.d.ts.map +1 -0
  93. package/dist/document/describe-coverage.js +162 -0
  94. package/dist/document/describe-coverage.js.map +1 -0
  95. package/dist/document/describe-place.d.ts.map +1 -1
  96. package/dist/document/describe-place.js +43 -1
  97. package/dist/document/describe-place.js.map +1 -1
  98. package/dist/document/diff.d.ts +5 -0
  99. package/dist/document/diff.d.ts.map +1 -1
  100. package/dist/document/diff.js +18 -4
  101. package/dist/document/diff.js.map +1 -1
  102. package/dist/document/edit.d.ts +1 -1
  103. package/dist/document/edit.d.ts.map +1 -1
  104. package/dist/document/edit.js +155 -10
  105. package/dist/document/edit.js.map +1 -1
  106. package/dist/document/expr/analyze.d.ts.map +1 -1
  107. package/dist/document/expr/analyze.js +2 -1
  108. package/dist/document/expr/analyze.js.map +1 -1
  109. package/dist/document/expr/evaluate.d.ts.map +1 -1
  110. package/dist/document/expr/evaluate.js +26 -2
  111. package/dist/document/expr/evaluate.js.map +1 -1
  112. package/dist/document/expr/parse.d.ts +2 -1
  113. package/dist/document/expr/parse.d.ts.map +1 -1
  114. package/dist/document/expr/parse.js +6 -3
  115. package/dist/document/expr/parse.js.map +1 -1
  116. package/dist/document/expr/print.d.ts.map +1 -1
  117. package/dist/document/expr/print.js +4 -2
  118. package/dist/document/expr/print.js.map +1 -1
  119. package/dist/document/far-end.d.ts +15 -0
  120. package/dist/document/far-end.d.ts.map +1 -0
  121. package/dist/document/far-end.js +26 -0
  122. package/dist/document/far-end.js.map +1 -0
  123. package/dist/document/graview-template.d.ts +20 -12
  124. package/dist/document/graview-template.d.ts.map +1 -1
  125. package/dist/document/graview-template.js +5 -44
  126. package/dist/document/graview-template.js.map +1 -1
  127. package/dist/document/index.d.ts +5 -11
  128. package/dist/document/index.d.ts.map +1 -1
  129. package/dist/document/index.js +3 -6
  130. package/dist/document/index.js.map +1 -1
  131. package/dist/document/instantiate-template.d.ts +14 -0
  132. package/dist/document/instantiate-template.d.ts.map +1 -0
  133. package/dist/document/instantiate-template.js +46 -0
  134. package/dist/document/instantiate-template.js.map +1 -0
  135. package/dist/document/migrate.d.ts.map +1 -1
  136. package/dist/document/migrate.js +7 -5
  137. package/dist/document/migrate.js.map +1 -1
  138. package/dist/document/range.d.ts +17 -0
  139. package/dist/document/range.d.ts.map +1 -0
  140. package/dist/document/range.js +46 -0
  141. package/dist/document/range.js.map +1 -0
  142. package/dist/document/schema.d.ts +34 -2
  143. package/dist/document/schema.d.ts.map +1 -1
  144. package/dist/document/schema.js +25 -1
  145. package/dist/document/schema.js.map +1 -1
  146. package/dist/document/template.d.ts +18 -2
  147. package/dist/document/template.d.ts.map +1 -1
  148. package/dist/document/template.js +27 -9
  149. package/dist/document/template.js.map +1 -1
  150. package/dist/document/thumbnail.d.ts +26 -4
  151. package/dist/document/thumbnail.d.ts.map +1 -1
  152. package/dist/document/thumbnail.js +165 -50
  153. package/dist/document/thumbnail.js.map +1 -1
  154. package/dist/document/to-document.d.ts.map +1 -1
  155. package/dist/document/to-document.js +18 -2
  156. package/dist/document/to-document.js.map +1 -1
  157. package/dist/document/views.d.ts +11 -0
  158. package/dist/document/views.d.ts.map +1 -1
  159. package/dist/document/views.js +76 -12
  160. package/dist/document/views.js.map +1 -1
  161. package/dist/figures.d.ts +3 -0
  162. package/dist/figures.d.ts.map +1 -0
  163. package/dist/figures.js +14 -0
  164. package/dist/figures.js.map +1 -0
  165. package/dist/index.d.ts +3 -13
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +3 -9
  168. package/dist/index.js.map +1 -1
  169. package/dist/mutations/define-mutation.d.ts.map +1 -1
  170. package/dist/mutations/define-mutation.js +9 -1
  171. package/dist/mutations/define-mutation.js.map +1 -1
  172. package/dist/mutations/derive-edits.d.ts +8 -0
  173. package/dist/mutations/derive-edits.d.ts.map +1 -1
  174. package/dist/mutations/derive-edits.js +30 -7
  175. package/dist/mutations/derive-edits.js.map +1 -1
  176. package/dist/mutations/form.d.ts +2 -0
  177. package/dist/mutations/form.d.ts.map +1 -1
  178. package/dist/mutations/form.js +1 -0
  179. package/dist/mutations/form.js.map +1 -1
  180. package/dist/mutations/node-ref.d.ts +4 -1
  181. package/dist/mutations/node-ref.d.ts.map +1 -1
  182. package/dist/mutations/node-ref.js +6 -0
  183. package/dist/mutations/node-ref.js.map +1 -1
  184. package/dist/mutations/types.d.ts +28 -0
  185. package/dist/mutations/types.d.ts.map +1 -1
  186. package/dist/places.d.ts +11 -0
  187. package/dist/places.d.ts.map +1 -1
  188. package/dist/places.js +19 -1
  189. package/dist/places.js.map +1 -1
  190. package/dist/refusal.d.ts.map +1 -1
  191. package/dist/refusal.js +4 -0
  192. package/dist/refusal.js.map +1 -1
  193. package/dist/refused.d.ts +20 -0
  194. package/dist/refused.d.ts.map +1 -0
  195. package/dist/refused.js +24 -0
  196. package/dist/refused.js.map +1 -0
  197. package/dist/scaffold/domain.js +1 -1
  198. package/dist/scaffold/from-template.d.ts.map +1 -1
  199. package/dist/scaffold/from-template.js +3 -3
  200. package/dist/scaffold/from-template.js.map +1 -1
  201. package/dist/scaffold/project.d.ts.map +1 -1
  202. package/dist/scaffold/project.js +7 -0
  203. package/dist/scaffold/project.js.map +1 -1
  204. package/dist/scene-districts.d.ts +5 -1
  205. package/dist/scene-districts.d.ts.map +1 -1
  206. package/dist/scene-districts.js +11 -4
  207. package/dist/scene-districts.js.map +1 -1
  208. package/dist/scene.d.ts +7 -0
  209. package/dist/scene.d.ts.map +1 -0
  210. package/dist/scene.js +16 -0
  211. package/dist/scene.js.map +1 -0
  212. package/dist/search.d.ts +1 -1
  213. package/dist/search.d.ts.map +1 -1
  214. package/dist/search.js +1 -1
  215. package/dist/search.js.map +1 -1
  216. package/dist/theme/derive.d.ts.map +1 -1
  217. package/dist/theme/derive.js +5 -4
  218. package/dist/theme/derive.js.map +1 -1
  219. package/dist/theme/types.d.ts +9 -0
  220. package/dist/theme/types.d.ts.map +1 -1
  221. package/dist/theme/types.js +5 -0
  222. package/dist/theme/types.js.map +1 -1
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/package.json +21 -1
@@ -3,6 +3,8 @@ import { ExprSyntaxError, parseExpr } from "./expr/parse.js";
3
3
  import { printExpr } from "./expr/print.js";
4
4
  import { error } from "./findings.js";
5
5
  import { coerce } from "./migrate.js";
6
+ import { outsideRange, rangeWords } from "./range.js";
7
+ import { farEnd } from "./far-end.js";
6
8
  import { ActSpec, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, KindSpec, NAME, RuleSpec, } from "./schema.js";
7
9
  import { SHIPPED_LENSES, isShippedLens, SHIPPED_LENS_NAMES } from "../places.js";
8
10
  import { placeSlug } from "../views/types.js";
@@ -18,6 +20,7 @@ export const EDIT_OPS = [
18
20
  "rename-field",
19
21
  "retype-field",
20
22
  "set-options",
23
+ "set-range",
21
24
  "set-required",
22
25
  "set-default",
23
26
  "remove-field",
@@ -56,6 +59,7 @@ const SHAPES = {
56
59
  "rename-field": z.object({ op: z.literal("rename-field"), kind: kindName, field: fieldName, to: fieldName }).strict(),
57
60
  "retype-field": z.object({ op: z.literal("retype-field"), kind: kindName, field: fieldName, type: z.enum(FIELD_TYPES), options: z.array(z.string().min(1).max(80)).min(1).max(100).optional(), of: z.enum(["string", "number", "date"]).optional(), format: z.enum(["money", "percent", "duration"]).optional() }).strict(),
58
61
  "set-options": z.object({ op: z.literal("set-options"), kind: kindName, field: fieldName, add: z.array(z.string().min(1).max(80)).optional(), remove: z.array(z.string().min(1).max(80)).optional() }).strict(),
62
+ "set-range": z.object({ op: z.literal("set-range"), kind: kindName, field: fieldName, min: z.union([z.number(), z.null()]).optional(), max: z.union([z.number(), z.null()]).optional(), step: z.union([z.number().positive(), z.null()]).optional() }).strict(),
59
63
  "set-required": z.object({ op: z.literal("set-required"), kind: kindName, field: fieldName, required: z.boolean() }).strict(),
60
64
  "set-default": z.object({ op: z.literal("set-default"), kind: kindName, field: fieldName, default: z.unknown() }).strict(),
61
65
  "remove-field": z.object({ op: z.literal("remove-field"), kind: kindName, field: fieldName }).strict(),
@@ -66,7 +70,15 @@ const SHAPES = {
66
70
  "remove-act": z.object({ op: z.literal("remove-act"), act: actName }).strict(),
67
71
  "add-rule": z.object({ op: z.literal("add-rule"), rule: actName, replace: z.boolean().optional() }).passthrough(),
68
72
  "remove-rule": z.object({ op: z.literal("remove-rule"), rule: actName }).strict(),
69
- "set-brand": z.object({ op: z.literal("set-brand"), accent: z.union([z.string(), z.null()]), name: z.string().min(1).max(60).optional() }).strict(),
73
+ "set-brand": z
74
+ .object({
75
+ op: z.literal("set-brand"),
76
+ accent: z.union([z.string(), z.null()]).optional(),
77
+ name: z.string().min(1).max(60).optional(),
78
+ currency: z.union([z.string(), z.null()]).optional(),
79
+ locale: z.union([z.string(), z.null()]).optional(),
80
+ })
81
+ .strict(),
70
82
  "set-label": z.object({ op: z.literal("set-label"), kind: kindName, field: fieldName.optional(), label: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
71
83
  "set-describe": z.object({ op: z.literal("set-describe"), kind: kindName, describe: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
72
84
  // A kind's slot; the front page (`slot: "home"`, no kind); or a blocks lens's blocks, by its title (FR-84).
@@ -148,6 +160,12 @@ function kindsOf(doc, e, ctx) {
148
160
  return kindsOf(doc, e.set, ctx);
149
161
  case "call": {
150
162
  const lit = e.args[0]?.t === "lit" && typeof e.args[0].value === "string" ? e.args[0].value : undefined;
163
+ // A walk from every member of a set (FR-101): its relation is its second word.
164
+ const walked = e.args.length === 2 && e.args[1]?.t === "lit" && typeof e.args[1].value === "string" ? e.args[1].value : undefined;
165
+ if (e.fn === "out" && walked)
166
+ return targets(doc, kindsOf(doc, e.args[0], ctx), walked);
167
+ if (e.fn === "in" && walked)
168
+ return sources(doc, walked);
151
169
  if (e.fn === "out" && lit)
152
170
  return targets(doc, "*", lit);
153
171
  if (e.fn === "in" && lit)
@@ -212,6 +230,12 @@ function mapNames(doc, e, ctx, rename) {
212
230
  const name = rename({ role: x.fn === "all" ? "kind" : "edge", kinds: NONE, name: literal });
213
231
  args = name === literal ? [...x.args] : [{ ...first, value: name }];
214
232
  }
233
+ else if ((x.fn === "out" || x.fn === "in") && x.args.length === 2 && x.args[1].t === "lit" && typeof x.args[1].value === "string") {
234
+ // out(S, 'edge'): the set is read where the walk stands, and its relation renamed like any walk's (FR-101).
235
+ const relation = x.args[1];
236
+ const name = rename({ role: "edge", kinds: NONE, name: relation.value });
237
+ args = [go(first, at), name === relation.value ? relation : { ...relation, value: name }];
238
+ }
215
239
  else if (((x.fn === "every" || x.fn === "some") && x.args.length === 2) || (x.fn === "sort" && x.args.length >= 2)) {
216
240
  // Read once per member, with the member as its subject: its names are the members'.
217
241
  args = [go(x.args[0], at), go(x.args[1], kindsOf(doc, x.args[0], at)), ...x.args.slice(2).map((a) => go(a, at))];
@@ -341,8 +365,8 @@ function proseSwap(from, to) {
341
365
  }
342
366
  // ── what an act touches ──────────────────────────────────────────────────────
343
367
  const subjectKinds = (act) => (act.on ? new Set(asArray(act.on)) : NONE);
344
- /** The kinds each `set` in an act writes to: its subject, a record it made, or (for a `$ref` target) any kind. */
345
- function setTargets(act) {
368
+ /** The kinds each `set` in an act writes to: its subject, a record it made, the other end of what it connects (FR-115), or (for a `$ref` target) any kind. */
369
+ function setTargets(act, doc) {
346
370
  const out = [];
347
371
  const made = new Map();
348
372
  if (act.creates)
@@ -353,6 +377,11 @@ function setTargets(act) {
353
377
  });
354
378
  if (act.sets)
355
379
  out.push({ set: act.sets, kinds: subjectKinds(act), where: "sets" });
380
+ if (act.setsOther) {
381
+ const relation = act.connects ?? act.severs;
382
+ const end = doc && relation ? farEnd(doc.kinds, relation, asArray(act.on)) : undefined;
383
+ out.push({ set: act.setsOther, kinds: end && end.kinds !== "*" ? new Set(end.kinds) : "*", where: "setsOther" });
384
+ }
356
385
  (act.effects ?? []).forEach((e, index) => {
357
386
  if (e.create)
358
387
  out.push({ set: e.set ?? {}, kinds: new Set([e.create]), where: "create", index });
@@ -449,6 +478,8 @@ class Editor {
449
478
  return this.retypeField(i, e);
450
479
  case "set-options":
451
480
  return this.setOptions(i, e);
481
+ case "set-range":
482
+ return this.setRange(i, e);
452
483
  case "set-required": {
453
484
  const f = this.field(i, e.kind, e.field);
454
485
  if (!f)
@@ -471,6 +502,9 @@ class Editor {
471
502
  else {
472
503
  if (f.type === "enum" && !(f.options ?? []).includes(e.default))
473
504
  return this.fail(i, "default", `"${String(e.default)}" is not one of ${e.field}'s options (${(f.options ?? []).join(", ")})`);
505
+ const outside = outsideRange(e.default, f, e.field);
506
+ if (outside)
507
+ return this.fail(i, "default", outside);
474
508
  f.default = e.default;
475
509
  this.said.push(`${e.kind}'s ${e.field} starts as ${JSON.stringify(e.default)} on new records.`);
476
510
  }
@@ -499,16 +533,45 @@ class Editor {
499
533
  return;
500
534
  }
501
535
  case "set-brand": {
536
+ // The colours and the money (FR-100) are set apart: what an edit does not name stays as it was.
537
+ const brand = { ...(this.doc.brand ?? {}) };
502
538
  if (e.accent === null) {
503
- delete this.doc.brand;
539
+ delete brand["accent"];
504
540
  this.said.push("The app goes back to Graview's colours.");
505
541
  }
506
- else {
542
+ else if (e.accent !== undefined) {
507
543
  if (!/^#[0-9a-fA-F]{6}$/.test(e.accent))
508
544
  return this.fail(i, "accent", 'an accent is a colour like "#c2577a"');
509
- this.doc.brand = { accent: e.accent, ...(e.name ? { name: e.name } : this.doc.brand?.name ? { name: this.doc.brand.name } : {}) };
545
+ brand["accent"] = e.accent;
510
546
  this.said.push(`The app's accent colour becomes ${e.accent}.`);
511
547
  }
548
+ if (e.currency === null) {
549
+ delete brand["currency"];
550
+ this.said.push("Money is said with no currency.");
551
+ }
552
+ else if (e.currency !== undefined) {
553
+ if (!/^[A-Z]{3}$/.test(e.currency))
554
+ return this.fail(i, "currency", 'a currency is its three-letter code, like "USD" or "EUR"');
555
+ brand["currency"] = e.currency;
556
+ this.said.push(`Money is said in ${e.currency}.`);
557
+ }
558
+ if (e.locale === null)
559
+ delete brand["locale"];
560
+ else if (e.locale !== undefined) {
561
+ if (!/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(e.locale))
562
+ return this.fail(i, "locale", 'a locale is a language tag, like "en-US" or "de-DE"');
563
+ brand["locale"] = e.locale;
564
+ this.said.push(`Money is written for ${e.locale}.`);
565
+ }
566
+ if (e.name) {
567
+ brand["name"] = e.name;
568
+ this.said.push(`The app's wordmark says ${e.name}.`);
569
+ }
570
+ // A brand that holds only a name holds nothing to apply.
571
+ if (Object.keys(brand).every((key) => key === "name"))
572
+ delete this.doc.brand;
573
+ else
574
+ this.doc.brand = brand;
512
575
  return;
513
576
  }
514
577
  case "set-label": {
@@ -699,6 +762,8 @@ class Editor {
699
762
  if (!Array.isArray(e.options?.blocks) || e.options.blocks.length === 0)
700
763
  return this.fail(i, "options.blocks", 'a lens drawn from blocks has blocks: "options": {"blocks": [{"headline": "…"}, {"list": "all(\'<kind>\')", "as": "card"}]}');
701
764
  }
765
+ if (name === "columns" && !this.columnsHold(i, e.bindings))
766
+ return;
702
767
  if (was)
703
768
  lenses[at] = lens;
704
769
  else
@@ -721,6 +786,35 @@ class Editor {
721
786
  }
722
787
  this.said.push(retitled ? `The lens "${String(was["title"])}" is now "${e.title}"${where}${follows}.` : `The lens "${e.title}" changes${where}.`);
723
788
  }
789
+ /**
790
+ * A STATUS BOARD'S BINDINGS (FR-97), held before it is added: each kind it
791
+ * binds is a kind, and its `column` is a field of that kind whose values
792
+ * are choices — the columns. Refused at the binding's path otherwise, so a
793
+ * chat is told which word to change rather than shown an empty board.
794
+ */
795
+ columnsHold(i, bindings) {
796
+ const example = '"bindings": {"<kind>": {"column": "<a choice field, like status>"}}';
797
+ if (!isObject(bindings) || Object.keys(bindings).length === 0)
798
+ return (this.fail(i, "bindings", "a board binds a kind's choice field to its columns", example), false);
799
+ for (const [kind, roles] of Object.entries(bindings)) {
800
+ if (!isObject(this.doc.kinds[kind]))
801
+ return (this.fail(i, `bindings.${kind}`, `"${kind}" is not a kind this app has; it has ${Object.keys(this.doc.kinds).join(", ")}`), false);
802
+ const field = isObject(roles) ? roles["column"] : undefined;
803
+ const fields = this.doc.kinds[kind].fields;
804
+ if (typeof field !== "string")
805
+ return (this.fail(i, `bindings.${kind}.column`, `the board does not say which field of ${kind} its columns are`, example), false);
806
+ if (!(field in fields))
807
+ return (this.fail(i, `bindings.${kind}.column`, `${kind} has no field "${field}"; it has ${Object.keys(fields).join(", ")}`), false);
808
+ if (fields[field]["type"] !== "enum") {
809
+ const choices = Object.keys(fields).filter((one) => fields[one]["type"] === "enum");
810
+ return (this.fail(i, `bindings.${kind}.column`, `${kind}'s ${field} is not a choice, so it has no columns`, choices.length > 0 ? `bind one of ${choices.join(", ")}` : `give ${kind} a choice field first: add-field with "type": "enum" and its "options", in the order the columns go`), false);
811
+ }
812
+ const others = Object.keys(roles).filter((role) => role !== "column");
813
+ if (others.length > 0)
814
+ return (this.fail(i, `bindings.${kind}.${others[0]}`, `a board binds only "column"`), false);
815
+ }
816
+ return true;
817
+ }
724
818
  removeLens(i, e) {
725
819
  const index = this.lensIndex(i, e.title, e.on);
726
820
  if (index < 0)
@@ -1140,12 +1234,43 @@ class Editor {
1140
1234
  f.format = e.format;
1141
1235
  else if (e.type !== "number" && e.type !== "integer")
1142
1236
  delete f.format;
1237
+ // A range is a number's (FR-114): a field that is no longer one lets it go.
1238
+ if (e.type !== "number" && e.type !== "integer")
1239
+ for (const key of ["min", "max", "step"])
1240
+ delete f[key];
1143
1241
  if (f.default !== undefined && coerce(f.default, was, f) === undefined)
1144
1242
  delete f.default;
1145
1243
  else if (f.default !== undefined)
1146
1244
  f.default = coerce(f.default, was, f);
1147
1245
  this.said.push(`${e.kind}'s ${e.field} changes from ${was} to ${e.type}; values that convert are kept.`);
1148
1246
  }
1247
+ /** A number's range (FR-114): a key given sets it, `null` clears it, one left out stays. */
1248
+ setRange(i, e) {
1249
+ const f = this.field(i, e.kind, e.field);
1250
+ if (!f)
1251
+ return;
1252
+ if (f.type !== "number" && f.type !== "integer")
1253
+ return this.fail(i, "field", `${e.field} is ${f.type === "integer" ? "an" : "a"} ${f.type}, not a number; a range belongs on a number or integer field`);
1254
+ const next = { ...f };
1255
+ for (const key of ["min", "max", "step"]) {
1256
+ if (e[key] === null)
1257
+ delete next[key];
1258
+ else if (e[key] !== undefined)
1259
+ next[key] = e[key];
1260
+ }
1261
+ if (next.min !== undefined && next.max !== undefined && next.min > next.max)
1262
+ return this.fail(i, "max", `the most (${next.max}) is below the least (${next.min})`);
1263
+ const outside = next.default !== undefined ? outsideRange(next.default, next, e.field) : undefined;
1264
+ if (outside)
1265
+ return this.fail(i, "default", `${e.field}'s default, ${outside.replace(/ is /, ", is ")}; change the default first`);
1266
+ for (const key of ["min", "max", "step"]) {
1267
+ if (next[key] === undefined)
1268
+ delete f[key];
1269
+ else
1270
+ f[key] = next[key];
1271
+ }
1272
+ this.said.push(`${e.kind}'s ${e.field} takes ${rangeWords(f)}.`);
1273
+ }
1149
1274
  setOptions(i, e) {
1150
1275
  const f = this.field(i, e.kind, e.field);
1151
1276
  if (!f)
@@ -1172,7 +1297,7 @@ class Editor {
1172
1297
  }
1173
1298
  // An act that sets a dropped option cannot stand.
1174
1299
  for (const [name, act] of Object.entries(this.doc.acts ?? {})) {
1175
- for (const t of setTargets(act))
1300
+ for (const t of setTargets(act, this.doc))
1176
1301
  if (hasKind(t.kinds, e.kind) && typeof t.set[e.field] === "string" && remove.includes(t.set[e.field])) {
1177
1302
  this.removeActQuietly(name);
1178
1303
  out.push(`the act ${name} (it set that option) is removed`);
@@ -1420,6 +1545,8 @@ class Editor {
1420
1545
  act.connects = r.to;
1421
1546
  if (act.severs === r.from)
1422
1547
  act.severs = r.to;
1548
+ if (Array.isArray(act.replaces))
1549
+ act.replaces = act.replaces.map((e) => (e === r.from ? r.to : e));
1423
1550
  for (const e of act.effects ?? []) {
1424
1551
  if (e.connect === r.from)
1425
1552
  e.connect = r.to;
@@ -1429,7 +1556,7 @@ class Editor {
1429
1556
  }
1430
1557
  if (r.t === "field") {
1431
1558
  const args = new Map();
1432
- for (const t of setTargets(act)) {
1559
+ for (const t of setTargets(act, doc)) {
1433
1560
  if (!hasKind(t.kinds, r.kind) || !(r.from in t.set))
1434
1561
  continue;
1435
1562
  if (t.kinds === "*" && Object.entries(doc.kinds).some(([k, s]) => k !== r.kind && s.fields[r.from]))
@@ -1441,6 +1568,8 @@ class Editor {
1441
1568
  args.set(r.from, r.to);
1442
1569
  if (t.where === "sets")
1443
1570
  act.sets = renamed;
1571
+ else if (t.where === "setsOther")
1572
+ act.setsOther = renamed;
1444
1573
  else
1445
1574
  act.effects[t.index].set = renamed;
1446
1575
  }
@@ -1469,7 +1598,7 @@ class Editor {
1469
1598
  act.allowedWhen = argRename?.has(r.from) && r.t === "field" && !hasKind(on, r.kind) ? rewriteBinding(guard, r.from, r.to) : guard;
1470
1599
  if (act.refusal !== undefined)
1471
1600
  act.refusal = tmplAt(act.refusal, on);
1472
- for (const t of setTargets(act))
1601
+ for (const t of setTargets(act, doc))
1473
1602
  for (const [k, v] of Object.entries(t.set))
1474
1603
  if (isObject(v) && typeof v["expr"] === "string")
1475
1604
  t.set[k] = { expr: exprAt(v["expr"], t.kinds === "*" ? on : t.kinds) };
@@ -1624,9 +1753,15 @@ class Editor {
1624
1753
  let uses = exprMentions(doc, act.allowedWhen, on, r) || templateMentions(doc, act.refusal, on, r);
1625
1754
  if (r.t === "edge")
1626
1755
  uses ||= act.connects === r.from || act.severs === r.from || (act.effects ?? []).some((e) => e.connect === r.from || e.sever === r.from);
1756
+ // A relation it only replaced goes from the list; one that replaced nothing else goes with it (FR-115).
1757
+ if (r.t === "edge" && !uses && Array.isArray(act.replaces) && act.replaces.includes(r.from)) {
1758
+ act.replaces = act.replaces.filter((e) => e !== r.from);
1759
+ if (act.replaces.length === 0)
1760
+ delete act.replaces;
1761
+ }
1627
1762
  if (r.t === "field" && !worked) {
1628
1763
  // An act that sets other things too loses only this field; one that did nothing else goes.
1629
- for (const t of setTargets(act)) {
1764
+ for (const t of setTargets(act, doc)) {
1630
1765
  if (!hasKind(t.kinds, r.kind) || t.kinds === "*")
1631
1766
  continue;
1632
1767
  if (r.from in t.set)
@@ -1646,6 +1781,8 @@ class Editor {
1646
1781
  delete act.args;
1647
1782
  if (act.sets && Object.keys(act.sets).length === 0)
1648
1783
  delete act.sets;
1784
+ if (act.setsOther && Object.keys(act.setsOther).length === 0)
1785
+ delete act.setsOther;
1649
1786
  const doesSomething = act.creates || act.writes || act.sets || act.connects || act.severs || act.removes || (act.effects ?? []).some((e) => e.create || e.connect || e.sever || e.remove || (e.set && Object.keys(e.set).length > 0));
1650
1787
  if (!doesSomething)
1651
1788
  uses = true;
@@ -2064,6 +2201,9 @@ function rewriteBlock(doc, b, ctx, r, kind) {
2064
2201
  next[key] = rewriteTemplate(doc, next[key], ctx, r);
2065
2202
  if (typeof next["figure"] === "string")
2066
2203
  next["figure"] = rewriteExpr(doc, next["figure"], ctx, r);
2204
+ // A figure's and a meter's label is a template (FR-99); a field's is words.
2205
+ if ((typeof next["figure"] === "string" || "progress" in next) && typeof next["label"] === "string")
2206
+ next["label"] = rewriteTemplate(doc, next["label"], ctx, r);
2067
2207
  if (typeof next["list"] === "string") {
2068
2208
  const members = listMembers(doc, next["list"], ctx);
2069
2209
  next["list"] = rewriteExpr(doc, next["list"], ctx, r);
@@ -2111,6 +2251,11 @@ function pruneBlock(doc, b, ctx, r, kind) {
2111
2251
  for (const k of ["value", "max"])
2112
2252
  if (exprMentions(doc, b["progress"][k], ctx, r))
2113
2253
  return null;
2254
+ // A label that says what is gone goes; the figure or meter stays (FR-99).
2255
+ if ((typeof b["figure"] === "string" || "progress" in b) && typeof b["label"] === "string" && templateMentions(doc, b["label"], ctx, r)) {
2256
+ const { label: _gone, ...kept } = b;
2257
+ return kept;
2258
+ }
2114
2259
  if (typeof b["list"] === "string") {
2115
2260
  if (exprMentions(doc, b["list"], ctx, r))
2116
2261
  return null;