@graview/core 0.1.62 → 0.1.64

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 (340) hide show
  1. package/dist/app.d.ts +12 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js.map +1 -1
  4. package/dist/arranging.js +2 -2
  5. package/dist/arranging.js.map +1 -1
  6. package/dist/cli/check/index.d.ts +1 -0
  7. package/dist/cli/check/index.d.ts.map +1 -1
  8. package/dist/cli/check/index.js +1 -0
  9. package/dist/cli/check/index.js.map +1 -1
  10. package/dist/cli/check/workflows.d.ts +11 -0
  11. package/dist/cli/check/workflows.d.ts.map +1 -0
  12. package/dist/cli/check/workflows.js +20 -0
  13. package/dist/cli/check/workflows.js.map +1 -0
  14. package/dist/cli/check.d.ts.map +1 -1
  15. package/dist/cli/check.js +2 -1
  16. package/dist/cli/check.js.map +1 -1
  17. package/dist/cli/describe.d.ts.map +1 -1
  18. package/dist/cli/describe.js +31 -1
  19. package/dist/cli/describe.js.map +1 -1
  20. package/dist/cli/document-schema.d.ts.map +1 -1
  21. package/dist/cli/document-schema.js +9 -0
  22. package/dist/cli/document-schema.js.map +1 -1
  23. package/dist/compose.d.ts +1 -1
  24. package/dist/compose.d.ts.map +1 -1
  25. package/dist/document/amount.d.ts +1 -1
  26. package/dist/document/amount.d.ts.map +1 -1
  27. package/dist/document/amount.js +2 -2
  28. package/dist/document/amount.js.map +1 -1
  29. package/dist/document/blocks.d.ts.map +1 -1
  30. package/dist/document/blocks.js +17 -1
  31. package/dist/document/blocks.js.map +1 -1
  32. package/dist/document/brand-check.d.ts.map +1 -1
  33. package/dist/document/brand-check.js +8 -2
  34. package/dist/document/brand-check.js.map +1 -1
  35. package/dist/document/compile-checked.d.ts.map +1 -1
  36. package/dist/document/compile-checked.js +4 -2
  37. package/dist/document/compile-checked.js.map +1 -1
  38. package/dist/document/compile.d.ts.map +1 -1
  39. package/dist/document/compile.js +48 -9
  40. package/dist/document/compile.js.map +1 -1
  41. package/dist/document/compiled.d.ts +14 -0
  42. package/dist/document/compiled.d.ts.map +1 -1
  43. package/dist/document/compiled.js +39 -4
  44. package/dist/document/compiled.js.map +1 -1
  45. package/dist/document/connection-findings.js +93 -21
  46. package/dist/document/connection-findings.js.map +1 -1
  47. package/dist/document/connection-spec.d.ts +54 -1
  48. package/dist/document/connection-spec.d.ts.map +1 -1
  49. package/dist/document/connection-spec.js +76 -8
  50. package/dist/document/connection-spec.js.map +1 -1
  51. package/dist/document/derives-findings.d.ts +4 -0
  52. package/dist/document/derives-findings.d.ts.map +1 -0
  53. package/dist/document/derives-findings.js +68 -0
  54. package/dist/document/derives-findings.js.map +1 -0
  55. package/dist/document/describe-forms.js +2 -2
  56. package/dist/document/describe-forms.js.map +1 -1
  57. package/dist/document/diff.d.ts +6 -0
  58. package/dist/document/diff.d.ts.map +1 -1
  59. package/dist/document/diff.js +43 -1
  60. package/dist/document/diff.js.map +1 -1
  61. package/dist/document/edit.d.ts +1 -1
  62. package/dist/document/edit.d.ts.map +1 -1
  63. package/dist/document/edit.js +264 -23
  64. package/dist/document/edit.js.map +1 -1
  65. package/dist/document/expr/evaluate.d.ts +7 -0
  66. package/dist/document/expr/evaluate.d.ts.map +1 -1
  67. package/dist/document/expr/evaluate.js +29 -2
  68. package/dist/document/expr/evaluate.js.map +1 -1
  69. package/dist/document/format-words.d.ts +1 -0
  70. package/dist/document/format-words.d.ts.map +1 -1
  71. package/dist/document/format-words.js +2 -2
  72. package/dist/document/format-words.js.map +1 -1
  73. package/dist/document/kept-in-step-with.d.ts +20 -0
  74. package/dist/document/kept-in-step-with.d.ts.map +1 -0
  75. package/dist/document/kept-in-step-with.js +38 -0
  76. package/dist/document/kept-in-step-with.js.map +1 -0
  77. package/dist/document/kept-in-step.d.ts.map +1 -1
  78. package/dist/document/kept-in-step.js +3 -1
  79. package/dist/document/kept-in-step.js.map +1 -1
  80. package/dist/document/money.d.ts +10 -2
  81. package/dist/document/money.d.ts.map +1 -1
  82. package/dist/document/money.js +14 -5
  83. package/dist/document/money.js.map +1 -1
  84. package/dist/document/policy-words.d.ts +2 -0
  85. package/dist/document/policy-words.d.ts.map +1 -1
  86. package/dist/document/policy-words.js +11 -1
  87. package/dist/document/policy-words.js.map +1 -1
  88. package/dist/document/schema.d.ts +46 -1
  89. package/dist/document/schema.d.ts.map +1 -1
  90. package/dist/document/schema.js +48 -8
  91. package/dist/document/schema.js.map +1 -1
  92. package/dist/document/template-parse.d.ts.map +1 -1
  93. package/dist/document/template-parse.js +7 -1
  94. package/dist/document/template-parse.js.map +1 -1
  95. package/dist/document/template.d.ts +2 -0
  96. package/dist/document/template.d.ts.map +1 -1
  97. package/dist/document/template.js +10 -1
  98. package/dist/document/template.js.map +1 -1
  99. package/dist/document/to-document.js +2 -2
  100. package/dist/document/to-document.js.map +1 -1
  101. package/dist/document/workflow-spec.d.ts +72 -0
  102. package/dist/document/workflow-spec.d.ts.map +1 -0
  103. package/dist/document/workflow-spec.js +45 -0
  104. package/dist/document/workflow-spec.js.map +1 -0
  105. package/dist/document.schema.json +883 -4
  106. package/dist/head.d.ts +81 -0
  107. package/dist/head.d.ts.map +1 -0
  108. package/dist/head.js +123 -0
  109. package/dist/head.js.map +1 -0
  110. package/dist/history.d.ts.map +1 -1
  111. package/dist/history.js +2 -0
  112. package/dist/history.js.map +1 -1
  113. package/dist/index.d.ts +7 -5
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +4 -3
  116. package/dist/index.js.map +1 -1
  117. package/dist/installation.d.ts +1 -1
  118. package/dist/job-findings.d.ts +5 -0
  119. package/dist/job-findings.d.ts.map +1 -1
  120. package/dist/job-findings.js +9 -3
  121. package/dist/job-findings.js.map +1 -1
  122. package/dist/keep-linked.d.ts +45 -0
  123. package/dist/keep-linked.d.ts.map +1 -0
  124. package/dist/keep-linked.js +188 -0
  125. package/dist/keep-linked.js.map +1 -0
  126. package/dist/mutations/derive-edits.js +2 -2
  127. package/dist/mutations/propose-changes.d.ts +24 -0
  128. package/dist/mutations/propose-changes.d.ts.map +1 -0
  129. package/dist/mutations/propose-changes.js +30 -0
  130. package/dist/mutations/propose-changes.js.map +1 -0
  131. package/dist/mutations/vote-suggestion.d.ts +30 -0
  132. package/dist/mutations/vote-suggestion.d.ts.map +1 -1
  133. package/dist/mutations/vote-suggestion.js +23 -0
  134. package/dist/mutations/vote-suggestion.js.map +1 -1
  135. package/dist/mutations/words.js +3 -3
  136. package/dist/ops/digest.d.ts +3 -1
  137. package/dist/ops/digest.d.ts.map +1 -1
  138. package/dist/ops/digest.js +7 -2
  139. package/dist/ops/digest.js.map +1 -1
  140. package/dist/ops/lineage.d.ts +19 -2
  141. package/dist/ops/lineage.d.ts.map +1 -1
  142. package/dist/ops/lineage.js +8 -4
  143. package/dist/ops/lineage.js.map +1 -1
  144. package/dist/ops/log.d.ts.map +1 -1
  145. package/dist/ops/log.js +13 -2
  146. package/dist/ops/log.js.map +1 -1
  147. package/dist/ops/suggestions.d.ts +47 -5
  148. package/dist/ops/suggestions.d.ts.map +1 -1
  149. package/dist/ops/suggestions.js +88 -13
  150. package/dist/ops/suggestions.js.map +1 -1
  151. package/dist/ops/tidying.d.ts.map +1 -1
  152. package/dist/ops/tidying.js +6 -1
  153. package/dist/ops/tidying.js.map +1 -1
  154. package/dist/ops/types.d.ts +28 -0
  155. package/dist/ops/types.d.ts.map +1 -1
  156. package/dist/ops/undo.d.ts.map +1 -1
  157. package/dist/ops/undo.js +17 -0
  158. package/dist/ops/undo.js.map +1 -1
  159. package/dist/permissions/policy.d.ts +7 -0
  160. package/dist/permissions/policy.d.ts.map +1 -1
  161. package/dist/permissions/policy.js +12 -0
  162. package/dist/permissions/policy.js.map +1 -1
  163. package/dist/permissions/types.d.ts +27 -0
  164. package/dist/permissions/types.d.ts.map +1 -1
  165. package/dist/permissions/types.js.map +1 -1
  166. package/dist/places.d.ts +10 -0
  167. package/dist/places.d.ts.map +1 -1
  168. package/dist/places.js +35 -1
  169. package/dist/places.js.map +1 -1
  170. package/dist/scene.d.ts +2 -0
  171. package/dist/scene.d.ts.map +1 -1
  172. package/dist/scene.js +2 -0
  173. package/dist/scene.js.map +1 -1
  174. package/dist/schema/amount-mark.d.ts +2 -0
  175. package/dist/schema/amount-mark.d.ts.map +1 -1
  176. package/dist/schema/amount-mark.js.map +1 -1
  177. package/dist/schema/amount-words.js +2 -2
  178. package/dist/schema/amount-words.js.map +1 -1
  179. package/dist/schema/choice-words.js +2 -2
  180. package/dist/schema/define-node.d.ts +8 -2
  181. package/dist/schema/define-node.d.ts.map +1 -1
  182. package/dist/schema/define-node.js +2 -2
  183. package/dist/schema/define-node.js.map +1 -1
  184. package/dist/schema/humanize.d.ts +17 -0
  185. package/dist/schema/humanize.d.ts.map +1 -1
  186. package/dist/schema/humanize.js +28 -1
  187. package/dist/schema/humanize.js.map +1 -1
  188. package/dist/schema/reading.d.ts +8 -0
  189. package/dist/schema/reading.d.ts.map +1 -1
  190. package/dist/schema/reading.js +16 -2
  191. package/dist/schema/reading.js.map +1 -1
  192. package/dist/schema/types.d.ts +8 -0
  193. package/dist/schema/types.d.ts.map +1 -1
  194. package/dist/schema/value-words.js +2 -2
  195. package/dist/schema/value-words.js.map +1 -1
  196. package/dist/starting-document.d.ts +16 -3
  197. package/dist/starting-document.d.ts.map +1 -1
  198. package/dist/starting-document.js +32 -5
  199. package/dist/starting-document.js.map +1 -1
  200. package/dist/store.d.ts +47 -2
  201. package/dist/store.d.ts.map +1 -1
  202. package/dist/store.js +176 -13
  203. package/dist/store.js.map +1 -1
  204. package/dist/sync/connection.d.ts +24 -5
  205. package/dist/sync/connection.d.ts.map +1 -1
  206. package/dist/sync/connection.js +168 -31
  207. package/dist/sync/connection.js.map +1 -1
  208. package/dist/sync/engine.d.ts +97 -0
  209. package/dist/sync/engine.d.ts.map +1 -1
  210. package/dist/sync/engine.js +159 -1
  211. package/dist/sync/engine.js.map +1 -1
  212. package/dist/sync/google-calendar.d.ts.map +1 -1
  213. package/dist/sync/google-calendar.js +18 -1
  214. package/dist/sync/google-calendar.js.map +1 -1
  215. package/dist/sync/guest-words.d.ts +9 -0
  216. package/dist/sync/guest-words.d.ts.map +1 -0
  217. package/dist/sync/guest-words.js +41 -0
  218. package/dist/sync/guest-words.js.map +1 -0
  219. package/dist/sync/index.d.ts +8 -5
  220. package/dist/sync/index.d.ts.map +1 -1
  221. package/dist/sync/index.js +2 -0
  222. package/dist/sync/index.js.map +1 -1
  223. package/dist/sync/notion-stub.d.ts +4 -1
  224. package/dist/sync/notion-stub.d.ts.map +1 -1
  225. package/dist/sync/notion-stub.js +40 -0
  226. package/dist/sync/notion-stub.js.map +1 -1
  227. package/dist/sync/notion.d.ts +44 -2
  228. package/dist/sync/notion.d.ts.map +1 -1
  229. package/dist/sync/notion.js +289 -143
  230. package/dist/sync/notion.js.map +1 -1
  231. package/dist/sync/status.d.ts +43 -0
  232. package/dist/sync/status.d.ts.map +1 -0
  233. package/dist/sync/status.js +46 -0
  234. package/dist/sync/status.js.map +1 -0
  235. package/dist/sync/types.d.ts +60 -0
  236. package/dist/sync/types.d.ts.map +1 -1
  237. package/dist/sync/types.js.map +1 -1
  238. package/dist/theme/marks.d.ts +12 -0
  239. package/dist/theme/marks.d.ts.map +1 -1
  240. package/dist/theme/marks.js +5 -0
  241. package/dist/theme/marks.js.map +1 -1
  242. package/dist/tidying.d.ts +53 -4
  243. package/dist/tidying.d.ts.map +1 -1
  244. package/dist/tidying.js +110 -8
  245. package/dist/tidying.js.map +1 -1
  246. package/dist/validate-graph.d.ts +2 -1
  247. package/dist/validate-graph.d.ts.map +1 -1
  248. package/dist/validate-graph.js +32 -1
  249. package/dist/validate-graph.js.map +1 -1
  250. package/dist/version.d.ts +1 -1
  251. package/dist/version.js +1 -1
  252. package/dist/words.d.ts +4 -1
  253. package/dist/words.d.ts.map +1 -1
  254. package/dist/words.js +4 -1
  255. package/dist/words.js.map +1 -1
  256. package/dist/workflow-shape.d.ts +223 -0
  257. package/dist/workflow-shape.d.ts.map +1 -0
  258. package/dist/workflow-shape.js +31 -0
  259. package/dist/workflow-shape.js.map +1 -0
  260. package/dist/workflows/findings.d.ts +13 -0
  261. package/dist/workflows/findings.d.ts.map +1 -0
  262. package/dist/workflows/findings.js +157 -0
  263. package/dist/workflows/findings.js.map +1 -0
  264. package/dist/workflows/generate-words.d.ts +12 -0
  265. package/dist/workflows/generate-words.d.ts.map +1 -0
  266. package/dist/workflows/generate-words.js +25 -0
  267. package/dist/workflows/generate-words.js.map +1 -0
  268. package/dist/workflows/generate.d.ts +59 -0
  269. package/dist/workflows/generate.d.ts.map +1 -0
  270. package/dist/workflows/generate.js +165 -0
  271. package/dist/workflows/generate.js.map +1 -0
  272. package/dist/workflows/judging.d.ts +17 -0
  273. package/dist/workflows/judging.d.ts.map +1 -0
  274. package/dist/workflows/judging.js +11 -0
  275. package/dist/workflows/judging.js.map +1 -0
  276. package/dist/workflows/next.d.ts +18 -0
  277. package/dist/workflows/next.d.ts.map +1 -0
  278. package/dist/workflows/next.js +75 -0
  279. package/dist/workflows/next.js.map +1 -0
  280. package/dist/workflows/on-the-map.d.ts +79 -0
  281. package/dist/workflows/on-the-map.d.ts.map +1 -0
  282. package/dist/workflows/on-the-map.js +96 -0
  283. package/dist/workflows/on-the-map.js.map +1 -0
  284. package/dist/workflows/policies.d.ts +39 -0
  285. package/dist/workflows/policies.d.ts.map +1 -0
  286. package/dist/workflows/policies.js +95 -0
  287. package/dist/workflows/policies.js.map +1 -0
  288. package/dist/workflows/resolve.d.ts +55 -0
  289. package/dist/workflows/resolve.d.ts.map +1 -0
  290. package/dist/workflows/resolve.js +57 -0
  291. package/dist/workflows/resolve.js.map +1 -0
  292. package/dist/workflows/schedule.d.ts +28 -0
  293. package/dist/workflows/schedule.d.ts.map +1 -0
  294. package/dist/workflows/schedule.js +160 -0
  295. package/dist/workflows/schedule.js.map +1 -0
  296. package/dist/workflows/step-act.d.ts +6 -0
  297. package/dist/workflows/step-act.d.ts.map +1 -0
  298. package/dist/workflows/step-act.js +79 -0
  299. package/dist/workflows/step-act.js.map +1 -0
  300. package/dist/workflows/step-agent.d.ts +4 -0
  301. package/dist/workflows/step-agent.d.ts.map +1 -0
  302. package/dist/workflows/step-agent.js +55 -0
  303. package/dist/workflows/step-agent.js.map +1 -0
  304. package/dist/workflows/step-archive.d.ts +8 -0
  305. package/dist/workflows/step-archive.d.ts.map +1 -0
  306. package/dist/workflows/step-archive.js +74 -0
  307. package/dist/workflows/step-archive.js.map +1 -0
  308. package/dist/workflows/step-classify.d.ts +25 -0
  309. package/dist/workflows/step-classify.d.ts.map +1 -0
  310. package/dist/workflows/step-classify.js +160 -0
  311. package/dist/workflows/step-classify.js.map +1 -0
  312. package/dist/workflows/step-generate.d.ts +7 -0
  313. package/dist/workflows/step-generate.d.ts.map +1 -0
  314. package/dist/workflows/step-generate.js +185 -0
  315. package/dist/workflows/step-generate.js.map +1 -0
  316. package/dist/workflows/steps.d.ts +75 -0
  317. package/dist/workflows/steps.d.ts.map +1 -0
  318. package/dist/workflows/steps.js +40 -0
  319. package/dist/workflows/steps.js.map +1 -0
  320. package/dist/workflows/triggered.d.ts +35 -0
  321. package/dist/workflows/triggered.d.ts.map +1 -0
  322. package/dist/workflows/triggered.js +69 -0
  323. package/dist/workflows/triggered.js.map +1 -0
  324. package/dist/workflows/values.d.ts +28 -0
  325. package/dist/workflows/values.d.ts.map +1 -0
  326. package/dist/workflows/values.js +75 -0
  327. package/dist/workflows/values.js.map +1 -0
  328. package/dist/workflows/words.d.ts +10 -0
  329. package/dist/workflows/words.d.ts.map +1 -0
  330. package/dist/workflows/words.js +30 -0
  331. package/dist/workflows/words.js.map +1 -0
  332. package/dist/workflows/world.d.ts +23 -0
  333. package/dist/workflows/world.d.ts.map +1 -0
  334. package/dist/workflows/world.js +50 -0
  335. package/dist/workflows/world.js.map +1 -0
  336. package/dist/workflows.d.ts +35 -0
  337. package/dist/workflows.d.ts.map +1 -0
  338. package/dist/workflows.js +20 -0
  339. package/dist/workflows.js.map +1 -0
  340. package/package.json +12 -2
@@ -16,13 +16,14 @@ const refusedFormat = (format, type) => {
16
16
  import { farEnd } from "./far-end.js";
17
17
  import { jobFindings, jobWorldOfDocument } from "../job-findings.js";
18
18
  import { JobSpec } from "./purpose-spec.js";
19
+ import { WORKFLOW_JUDGING } from "../workflows/judging.js";
19
20
  import { aliasWord, documentAliasFindings } from "../alias-findings.js";
20
21
  import { humanizeField, withArticle } from "../schema/define-node.js";
21
- import { ConnectionSpec } from "./connection-spec.js";
22
+ import { ConnectionSpec, connectionKinds } from "./connection-spec.js";
22
23
  import { linkedRelationFindings } from "./linked-relation-findings.js";
23
24
  import { linkedRelationOfSpec, reachesAnotherApp } from "../schema/linked.js";
24
- import { ActSpec, ALIAS, MOST_ALIASES, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
25
- import { SHIPPED_LENSES, isShippedLens, SHIPPED_LENS_NAMES, unknownLensOption, viewReference } from "../places.js";
25
+ import { ActSpec, ALIAS, MOST_ALIASES, MOST_COMPUTED, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
26
+ import { SHIPPED_LENSES, isShippedLens, SHIPPED_LENS_NAMES, TIMELINE_AXIS_ROLES, timelineBindingsWords, unknownLensOption, viewReference } from "../places.js";
26
27
  import { LENSES_THAT_LIST, listsRecords } from "../lenses-that-list.js";
27
28
  import { placeSlug, VIEW_REFERENCE } from "../views/types.js";
28
29
  import { documentListFields, kindListFindings, kindListWords, listGroup, listSort, sortTypeOfDocumentField } from "../kind-list.js";
@@ -36,13 +37,15 @@ import { canonicalize } from "./canonical.js";
36
37
  import { quotedAtTheEnd } from "./quote.js";
37
38
  import { isRecord as isObject, respellDocument } from "./respell.js";
38
39
  import { refName } from "./compiled.js";
39
- import { localeSentence, timeZoneSentence } from "./diff.js";
40
+ import { derivesSentence, localeSentence, timeZoneSentence } from "./diff.js";
40
41
  import { accentProblem } from "../theme/accent.js";
41
42
  import { darkPageFor, groundProblem } from "../theme/ground-check.js";
42
43
  import { pageFieldFindings, pageNames, renamedOnPage, withoutOnPage } from "./page-fields.js";
44
+ /** Whether a field or a computed field is said as money: its format, or a unit that is a currency. */
45
+ const isMoney = (f) => f["format"] === "money" || (f["format"] === undefined && typeof f["unit"] === "string" && /^[A-Z]{3}$/.test(f["unit"]));
43
46
  import { capitalize, listed as listOf } from "../capital.js";
44
47
  import { quoted } from "./quote.js";
45
- import { FIRST_GRANT, FIRST_SIGHT, grantBody, grantGone, grantSentence, NO_GRANT_LEFT, NO_SIGHT_LEFT, peopleKinds, peopleSentence, archivesSentence, tidyingSentence, nounWords, pluralWords as pluralOf, sightGone, sightSentence, sightWords, whoWords } from "./policy-words.js";
48
+ import { FIRST_GRANT, FIRST_SIGHT, grantBody, grantGone, grantSentence, NO_GRANT_LEFT, NO_SIGHT_LEFT, peopleKinds, peopleSentence, appTidyingSentence, archivesSentence, tidyingSentence, nounWords, pluralWords as pluralOf, sightGone, sightSentence, sightWords, whoWords } from "./policy-words.js";
46
49
  /** The ops, for tool schemas and the guide. */
47
50
  export const EDIT_OPS = [
48
51
  "add-kind",
@@ -84,6 +87,8 @@ export const EDIT_OPS = [
84
87
  "set-people",
85
88
  "set-archives",
86
89
  "set-tidying",
90
+ "set-app-tidying",
91
+ "set-derives",
87
92
  "add-grant",
88
93
  "remove-grant",
89
94
  "add-sight",
@@ -95,6 +100,11 @@ export const EDIT_OPS = [
95
100
  "add-alias",
96
101
  "set-aliases",
97
102
  "set-connection",
103
+ "add-workflow",
104
+ "set-workflow",
105
+ "remove-workflow",
106
+ "pause-workflow",
107
+ "resume-workflow",
98
108
  ];
99
109
  /** Words the rule language keeps: a field or relation renamed to one could not be named in a rule. */
100
110
  const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
@@ -126,7 +136,7 @@ function buildShapes() {
126
136
  "rename-field": z.object({ op: z.literal("rename-field"), kind: kindName, field: fieldName, to: fieldName }).strict(),
127
137
  "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(), multiple: z.boolean().optional(), of: z.enum(["string", "number", "date", "image", "video", "file"]).optional(), format: z.enum(["money", "percent", "duration", "markdown"]).optional() }).strict(),
128
138
  "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(), labels: z.record(z.string().min(1).max(80), z.union([z.string().min(1).max(80), z.null()])).optional() }).strict(),
129
- "set-format": z.object({ op: z.literal("set-format"), kind: kindName, field: fieldName, format: z.union([z.enum(["money", "percent", "duration", "markdown"]), z.null()]).optional(), unit: z.union([z.string().min(1).max(12), z.null()]).optional() }).strict(),
139
+ "set-format": z.object({ op: z.literal("set-format"), kind: kindName, field: fieldName, format: z.union([z.enum(["money", "percent", "duration", "markdown"]), z.null()]).optional(), unit: z.union([z.string().min(1).max(12), z.null()]).optional(), places: z.union([z.literal(0), z.null()]).optional() }).strict(),
130
140
  "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(),
131
141
  "set-required": z.object({ op: z.literal("set-required"), kind: kindName, field: fieldName, required: z.boolean() }).strict(),
132
142
  "set-default": z.object({ op: z.literal("set-default"), kind: kindName, field: fieldName, default: z.unknown() }).strict(),
@@ -168,7 +178,7 @@ function buildShapes() {
168
178
  // Whether the home draws the description as the line under the name (Roadmap f-home-subtitle-separate): false keeps it off, true or null draws it again.
169
179
  subtitle: z.union([z.boolean(), z.null()]).optional(),
170
180
  // The opening screen (Roadmap f-splash-in-document), whole; null for Graview's own.
171
- splash: z.union([z.object({ style: z.string(), image: z.string().min(1).optional(), title: z.string().min(1).max(80).optional(), kicker: z.string().min(1).max(80).optional(), shader: z.string().min(1).max(40).optional() }).strict(), z.null()]).optional(),
181
+ splash: z.union([z.object({ style: z.string(), image: z.string().min(1).optional(), title: z.string().min(1).max(80).optional(), kicker: z.string().min(1).max(80).optional(), shader: z.string().min(1).max(40).optional(), scheme: z.enum(["light", "dark", "device"]).optional() }).strict(), z.null()]).optional(),
172
182
  // What every worker view inherits (Roadmap f-app-wide-view-theme): a stylesheet drawn before each view's own (its length the sanitizer's to refuse, in `brandFindings`), and a fade between records; null clears either, or all of it.
173
183
  views: z
174
184
  .union([
@@ -254,6 +264,10 @@ function buildShapes() {
254
264
  "set-archives": z.object({ op: z.literal("set-archives"), kind: kindName, archives: z.boolean() }).strict(),
255
265
  // What the app's own actor may do on its own to a kind, or to one relation of it (`@graview/core/tidying`).
256
266
  "set-tidying": z.object({ op: z.literal("set-tidying"), kind: kindName, relation: z.optional(z.string()), tidying: z.enum(["never", "no-links", "allowed"]) }).strict(),
267
+ // What the app says of suggestions as a whole (`tidying` at the top of the document): the votes at which the app's own actor makes one; null takes it away.
268
+ "set-app-tidying": z.object({ op: z.literal("set-app-tidying"), autoAcceptAt: z.union([z.number().int().min(1).max(10_000), z.null()]) }).strict(),
269
+ // What a kind follows (Roadmap f-keep-linked): the kind, the relation joining them and the fields worked out from it; null, it follows nothing.
270
+ "set-derives": z.object({ op: z.literal("set-derives"), kind: kindName, derives: z.union([z.object({ from: kindName, via: z.string(), set: z.record(z.string(), z.unknown()) }).strict(), z.null()]) }).strict(),
257
271
  "add-grant": z.object({ op: z.literal("add-grant") }).passthrough(),
258
272
  "remove-grant": z.object({ op: z.literal("remove-grant") }).passthrough(),
259
273
  "add-sight": z.object({ op: z.literal("add-sight") }).passthrough(),
@@ -282,6 +296,12 @@ function buildShapes() {
282
296
  "set-aliases": z.object({ op: z.literal("set-aliases"), kind: kindName, field: fieldName.optional(), aliases: z.union([z.array(z.string().min(1).max(40)).max(MOST_ALIASES), z.null()]) }).strict(),
283
297
  // A connection declared whole, by its name: set where there was none, or replaced (FR-229); null takes it away.
284
298
  "set-connection": z.object({ op: z.literal("set-connection"), name: kindName, connection: z.union([z.record(z.string(), z.unknown()), z.null()]) }).strict(),
299
+ // A workflow by its name (Roadmap f-jobs-declared): added where there was none, replaced whole, removed, paused or resumed.
300
+ "add-workflow": z.object({ op: z.literal("add-workflow"), name: kindName, workflow: z.record(z.string(), z.unknown()) }).strict(),
301
+ "set-workflow": z.object({ op: z.literal("set-workflow"), name: kindName, workflow: z.record(z.string(), z.unknown()) }).strict(),
302
+ "remove-workflow": z.object({ op: z.literal("remove-workflow"), name: kindName }).strict(),
303
+ "pause-workflow": z.object({ op: z.literal("pause-workflow"), name: kindName }).strict(),
304
+ "resume-workflow": z.object({ op: z.literal("resume-workflow"), name: kindName }).strict(),
285
305
  };
286
306
  }
287
307
  let shapes;
@@ -1045,8 +1065,12 @@ class Editor {
1045
1065
  const fields = e.fields;
1046
1066
  const seen = new Set();
1047
1067
  for (const [n, field] of fields.entries()) {
1048
- if (!spec.fields[field])
1049
- return this.fail(i, `fields.${n}`, `a glance at ${e.kind} is to say "${field}", and ${e.kind} has no field called that`, `use one of: ${Object.keys(spec.fields).join(", ")}`);
1068
+ // A computed field may be glanced at (FR-83): worked out for each card, as its page works it out — unless it is kept off what a record says of itself.
1069
+ const worked = spec.computed?.[field];
1070
+ if (!spec.fields[field] && worked === undefined)
1071
+ return this.fail(i, `fields.${n}`, `a glance at ${e.kind} is to say "${field}", and ${e.kind} has no field or computed field called that`, `use one of: ${[...Object.keys(spec.fields), ...Object.keys(spec.computed ?? {})].join(", ")}`);
1072
+ if (!spec.fields[field] && isObject(worked) && worked["hidden"] === true)
1073
+ return this.fail(i, `fields.${n}`, `${e.kind}'s "${field}" is worked out and kept off its card, page and CSV, so a glance cannot say it`, `show it first: set-computed with "hidden": false`);
1050
1074
  if (seen.has(field))
1051
1075
  return this.fail(i, `fields.${n}`, `"${field}" is in the glance twice; name each field once`);
1052
1076
  seen.add(field);
@@ -1085,6 +1109,10 @@ class Editor {
1085
1109
  return this.setArchives(i, e);
1086
1110
  case "set-tidying":
1087
1111
  return this.setTidying(i, e);
1112
+ case "set-app-tidying":
1113
+ return this.setAppTidying(e);
1114
+ case "set-derives":
1115
+ return this.setDerives(i, e);
1088
1116
  case "add-grant":
1089
1117
  case "remove-grant":
1090
1118
  case "add-sight":
@@ -1104,6 +1132,15 @@ class Editor {
1104
1132
  return this.setAliases(i, e.kind, e.field, e.aliases ?? []);
1105
1133
  case "set-connection":
1106
1134
  return this.setConnection(i, e.name, e.connection);
1135
+ case "add-workflow":
1136
+ return this.placeWorkflow(i, e.name, e.workflow, "add");
1137
+ case "set-workflow":
1138
+ return this.placeWorkflow(i, e.name, e.workflow, "set");
1139
+ case "remove-workflow":
1140
+ return this.removeWorkflow(i, e.name);
1141
+ case "pause-workflow":
1142
+ case "resume-workflow":
1143
+ return this.setWorkflowStatus(i, e.name, e.op === "pause-workflow" ? "paused" : "active");
1107
1144
  }
1108
1145
  }
1109
1146
  // ── a connection, declared whole (FR-229) ──
@@ -1131,7 +1168,9 @@ class Editor {
1131
1168
  const was = this.doc.connections?.[name] !== undefined;
1132
1169
  (this.doc.connections ??= {})[name] = parsed.data;
1133
1170
  const system = parsed.data.system === "notion" ? "a Notion database" : "a Google calendar";
1134
- this.said.push(`The connection ${name} ${was ? "now keeps" : "keeps"} ${pluralOf(this.doc.kinds, parsed.data.kind)} in step with ${system}${parsed.data.direction === "read" ? ", which owns them" : ", both ways"}.`);
1171
+ const kinds = connectionKinds(parsed.data).map((kind) => pluralOf(this.doc.kinds, kind));
1172
+ const said = kinds.length <= 1 ? kinds.join("") : `${kinds.slice(0, -1).join(", ")} and ${kinds.at(-1)}`;
1173
+ this.said.push(`The connection ${name} ${was ? "now keeps" : "keeps"} ${said} in step with ${system}${parsed.data.direction === "read" ? ", which owns them" : ", both ways"}.`);
1135
1174
  }
1136
1175
  // ── other words for a kind or a field (Roadmap f-learned-suggestions) ──
1137
1176
  aliasesOf(kind, field) {
@@ -1258,6 +1297,58 @@ class Editor {
1258
1297
  this.keepPurpose({ ...this.doc.purpose });
1259
1298
  this.said.push(`The job "${job.name}" (${quotedAtTheEnd(job.ask)}) is removed.`);
1260
1299
  }
1300
+ // ── what the app does on its own, and when (Roadmap f-jobs-declared) ──
1301
+ /** The workflow a name names, or undefined having said why. */
1302
+ workflowNamed(i, name) {
1303
+ const workflows = this.doc.workflows ?? {};
1304
+ if (Object.hasOwn(workflows, name))
1305
+ return workflows[name];
1306
+ const names = Object.keys(workflows);
1307
+ this.fail(i, "name", `there is no workflow "${name}"${names.length > 0 ? `; the workflows are ${names.map((one) => `"${one}"`).join(", ")}` : ""}`);
1308
+ return undefined;
1309
+ }
1310
+ /** A workflow added or replaced whole, judged as `graview check` judges it (`workflow-*`): kept only when nothing is wrong with it. */
1311
+ placeWorkflow(i, name, workflow, how) {
1312
+ const workflows = { ...(this.doc.workflows ?? {}) };
1313
+ if (how === "add" && Object.hasOwn(workflows, name))
1314
+ return this.fail(i, "name", `there is already a workflow "${name}"`, `Change it with {"op": "set-workflow", "name": "${name}", …}, or name this one otherwise.`);
1315
+ if (how === "set" && !Object.hasOwn(workflows, name) && !this.workflowNamed(i, name))
1316
+ return;
1317
+ const shaped = typeof workflow.title === "string" && Array.isArray(workflow.when) && workflow.when.length > 0 && Array.isArray(workflow.steps) && workflow.steps.length > 0;
1318
+ if (!shaped)
1319
+ return this.fail(i, "workflow", "a workflow is { title, when: [...], steps: [...] }, each list with at least one", `{"op": "${how}-workflow", "name": "${name}", "workflow": {"title": "…", "when": [{"every": "day 03:00"}], "steps": [{"act": "…"}]}}`);
1320
+ // Judged as `graview check` judges it, where the checker is loaded; else kept as shaped, and the check says the rest.
1321
+ const judged = WORKFLOW_JUDGING.judge?.(name, workflow, { ...this.doc, workflows });
1322
+ for (const finding of judged?.errors ?? [])
1323
+ this.failAs(finding.code, i, `workflow${finding.path ? `.${finding.path}` : ""}`, finding.message, finding.fix);
1324
+ if (judged && judged.errors.length > 0)
1325
+ return;
1326
+ workflows[name] = workflow;
1327
+ this.doc.workflows = workflows;
1328
+ const steps = workflow.steps.length;
1329
+ this.said.push(`${how === "add" ? "A new workflow" : "The workflow"} “${workflow.title}”${how === "add" ? "" : " is now"}: ${judged?.said ?? `${steps === 1 ? "one step" : `${steps} steps`}${workflow.status === "paused" ? ", paused" : ""}`}.`);
1330
+ for (const warning of judged?.warnings ?? [])
1331
+ this.said.push(warning);
1332
+ }
1333
+ removeWorkflow(i, name) {
1334
+ const workflow = this.workflowNamed(i, name);
1335
+ if (!workflow)
1336
+ return;
1337
+ const { [name]: _gone, ...rest } = this.doc.workflows;
1338
+ if (Object.keys(rest).length === 0)
1339
+ delete this.doc.workflows;
1340
+ else
1341
+ this.doc.workflows = rest;
1342
+ this.said.push(`The workflow “${workflow.title ?? name}” is removed.`);
1343
+ }
1344
+ setWorkflowStatus(i, name, status) {
1345
+ const workflow = this.workflowNamed(i, name);
1346
+ if (!workflow)
1347
+ return;
1348
+ const { status: _was, ...rest } = workflow;
1349
+ this.doc.workflows = { ...this.doc.workflows, [name]: status === "paused" ? { ...rest, status } : rest };
1350
+ this.said.push(status === "paused" ? `The workflow “${workflow.title ?? name}” is paused: it runs on no clock and no change until it is resumed.` : `The workflow “${workflow.title ?? name}” runs again.`);
1351
+ }
1261
1352
  /** How a record's page orders and groups its facts (FR-148): judged as the compile judges `kinds.<kind>.page`. */
1262
1353
  setPageFields(i, e) {
1263
1354
  const spec = this.kind(i, e.kind);
@@ -1401,6 +1492,8 @@ class Editor {
1401
1492
  }
1402
1493
  if (name === "columns" && !(this.columnsHold(i, e.bindings) && this.boardOptionsHold(i, e.bindings, e.options)))
1403
1494
  return;
1495
+ if (name === "timeline" && !this.timelineHolds(i, e.bindings, e.options))
1496
+ return;
1404
1497
  if (was)
1405
1498
  lenses[at] = lens;
1406
1499
  else
@@ -1473,6 +1566,44 @@ class Editor {
1473
1566
  }
1474
1567
  return true;
1475
1568
  }
1569
+ /**
1570
+ * A TIMELINE THAT WOULD DRAW, held before it is added: a timeline lays out
1571
+ * spans by number across columns, so each kind it binds is a kind, its
1572
+ * `start` and `end` are number fields, and its columns are named in the
1573
+ * options or read from a choice field bound to `column`. A timeline over a
1574
+ * project's start and end dates was accepted, said nothing and drew
1575
+ * nothing — never a place; it is refused here in the words the checker
1576
+ * uses (`lens-cannot-draw`), pointing spans of dates at the calendar.
1577
+ */
1578
+ timelineHolds(i, bindings, options) {
1579
+ const refuse = (path, message, kind, start, end) => (this.failAs("lens-cannot-draw", i, path, `a timeline here would not draw: ${message}`, timelineBindingsWords(kind, typeof start === "string" ? start : "start", typeof end === "string" ? end : "end")), false);
1580
+ if (!isObject(bindings) || Object.keys(bindings).length === 0)
1581
+ return refuse("bindings", "it binds no kind", "<kind>");
1582
+ let columns = Array.isArray(options?.["columns"]) && options["columns"].length > 0;
1583
+ for (const [kind, roles] of Object.entries(bindings)) {
1584
+ if (!isObject(this.doc.kinds[kind]))
1585
+ return (this.fail(i, `bindings.${kind}`, `"${kind}" is not a kind this app has; it has ${Object.keys(this.doc.kinds).join(", ")}`), false);
1586
+ if (!isObject(roles))
1587
+ return refuse(`bindings.${kind}`, `it does not say which fields of ${kind} it lays out`, kind);
1588
+ const fields = this.doc.kinds[kind].fields;
1589
+ for (const role of TIMELINE_AXIS_ROLES) {
1590
+ const field = roles[role];
1591
+ if (typeof field !== "string")
1592
+ return refuse(`bindings.${kind}.${role}`, `it does not say which field of ${kind} is its ${role}`, kind, roles["start"], roles["end"]);
1593
+ if (!(field in fields))
1594
+ return (this.fail(i, `bindings.${kind}.${role}`, `${kind} has no field "${field}"; it has ${Object.keys(fields).join(", ")}`), false);
1595
+ const type = fields[field]["type"];
1596
+ if (type !== "number" && type !== "integer" && type !== "duration")
1597
+ return refuse(`bindings.${kind}.${role}`, `${kind}'s ${field} is ${withArticle(String(type ?? "text"))} field, and a timeline's axis is a number`, kind, roles["start"], roles["end"]);
1598
+ }
1599
+ const column = typeof roles["column"] === "string" ? roles["column"] : typeof roles["columns"] === "string" ? roles["columns"] : undefined;
1600
+ if (column !== undefined && fields[column]?.["type"] === "enum")
1601
+ columns = true;
1602
+ }
1603
+ if (!columns)
1604
+ return refuse("options.columns", "it has no columns — no option names them and no `column` role is bound to a choice field", Object.keys(bindings)[0]);
1605
+ return true;
1606
+ }
1476
1607
  /**
1477
1608
  * WHAT A BOARD SHOWS OF ITSELF (Roadmap f-lens-search-filter): fields of
1478
1609
  * its kind for Find and the chips, columns of its field to open shut or
@@ -2035,8 +2166,8 @@ class Editor {
2035
2166
  return this.fail(i, "name", `${e.kind} already has a ${spec.fields[name] ? "field" : "relation"} called "${name}"`);
2036
2167
  if (KEYWORDS.has(name))
2037
2168
  return this.fail(i, "name", `"${name}" is a word the rule language keeps; pick another name`);
2038
- if (Object.keys(spec.computed ?? {}).length >= 20)
2039
- return this.fail(i, "name", "a kind works out at most 20 computed fields");
2169
+ if (Object.keys(spec.computed ?? {}).length >= MOST_COMPUTED)
2170
+ return this.fail(i, "name", `a kind works out at most ${MOST_COMPUTED} computed fields`);
2040
2171
  }
2041
2172
  const was = existing === undefined ? {} : typeof existing === "string" ? { expr: existing } : { ...existing };
2042
2173
  const next = { ...was };
@@ -2128,6 +2259,86 @@ class Editor {
2128
2259
  * never tidied is given no leave-alone-<kind> or allow-tidying-<kind>,
2129
2260
  * which a grant may not then name.
2130
2261
  */
2262
+ /**
2263
+ * WHAT A KIND FOLLOWS (Roadmap f-keep-linked): each record of it linked
2264
+ * through `via` to a record of `from` takes the fields `set` works out
2265
+ * from it, kept in step by the store in the same change; null, it
2266
+ * follows nothing and its records keep the values they hold.
2267
+ */
2268
+ setDerives(i, e) {
2269
+ const spec = this.kind(i, e.kind);
2270
+ if (!spec)
2271
+ return;
2272
+ if (e.derives === null) {
2273
+ const was = spec.derives?.from;
2274
+ delete spec.derives;
2275
+ this.said.push(was ? derivesSentence(e.kind, undefined) : `${e.kind} follows nothing, as before.`);
2276
+ return;
2277
+ }
2278
+ const { from, via, set } = e.derives;
2279
+ if (!this.doc.kinds[from])
2280
+ return this.fail(i, "derives.from", `"${from}" is not a kind this app has`);
2281
+ const joins = (spec.edges?.[via] && (spec.edges[via].to === "*" || spec.edges[via].to.includes(from))) || (this.doc.kinds[from].edges?.[via] && (this.doc.kinds[from].edges[via].to === "*" || this.doc.kinds[from].edges[via].to.includes(e.kind)));
2282
+ if (!joins)
2283
+ return this.fail(i, "derives.via", `"${via}" does not join ${words(e.kind)} to ${words(from)}`, `name a relation of ${e.kind} to ${from}, or of ${from} to ${e.kind} (add one with add-relation in the same change)`);
2284
+ spec.derives = { from, via, set };
2285
+ this.said.push(derivesSentence(e.kind, spec.derives));
2286
+ }
2287
+ /**
2288
+ * WHAT A KIND FOLLOWS, held to a rename or a removal (Roadmap
2289
+ * f-keep-linked): the names it says follow a rename; a value reading
2290
+ * what is removed goes, and a `derives` left setting nothing, or
2291
+ * following a kind or relation that is gone, goes with it.
2292
+ */
2293
+ followDerives(r, removing) {
2294
+ const touched = [];
2295
+ for (const [kind, spec] of Object.entries(this.doc.kinds)) {
2296
+ const d = spec.derives;
2297
+ if (!d)
2298
+ continue;
2299
+ const before = JSON.stringify(d);
2300
+ const ctx = new Set([d.from]);
2301
+ if ((r.t === "kind" && d.from === r.from) || (r.t === "edge" && d.via === r.from)) {
2302
+ if (removing) {
2303
+ delete spec.derives;
2304
+ touched.push(`what ${kind} follows`);
2305
+ continue;
2306
+ }
2307
+ }
2308
+ const set = {};
2309
+ for (const [field, value] of Object.entries(d.set ?? {})) {
2310
+ let key = field;
2311
+ if (r.t === "field" && r.kind === kind && field === r.from) {
2312
+ if (removing)
2313
+ continue;
2314
+ key = r.to;
2315
+ }
2316
+ if (isObject(value) && typeof value.expr === "string") {
2317
+ const source = value.expr;
2318
+ if (removing) {
2319
+ if (!exprMentions(this.doc, source, ctx, r))
2320
+ set[key] = value;
2321
+ }
2322
+ else
2323
+ set[key] = { expr: rewriteExpr(this.doc, source, ctx, r) };
2324
+ }
2325
+ else
2326
+ set[key] = value;
2327
+ }
2328
+ if (!removing && r.t === "kind" && d.from === r.from)
2329
+ d.from = r.to;
2330
+ if (!removing && r.t === "edge" && d.via === r.from)
2331
+ d.via = r.to;
2332
+ d.set = set;
2333
+ if (Object.keys(set).length === 0) {
2334
+ delete spec.derives;
2335
+ touched.push(`what ${kind} follows`);
2336
+ }
2337
+ else if (JSON.stringify(d) !== before)
2338
+ touched.push(`what ${kind} follows`);
2339
+ }
2340
+ return touched;
2341
+ }
2131
2342
  setTidying(i, e) {
2132
2343
  const spec = this.kind(i, e.kind);
2133
2344
  if (!spec)
@@ -2166,6 +2377,26 @@ class Editor {
2166
2377
  this.toolsMoved.add(`leave-alone-${e.kind} and allow-tidying-${e.kind} ${tidying === "never" ? "are gone" : "are offered"}`);
2167
2378
  this.said.push(tidyingSentence(this.doc.kinds, e.kind, undefined, tidying, already));
2168
2379
  }
2380
+ /**
2381
+ * WHAT THE APP SAYS OF SUGGESTIONS AS A WHOLE (Roadmap f-suggestion-votes):
2382
+ * `tidying.autoAcceptAt`, the votes for a suggestion at which the app's
2383
+ * own actor makes it; null takes it away, and a suggestion waits for
2384
+ * somebody who may decide. A `tidying` left saying nothing goes.
2385
+ */
2386
+ setAppTidying(e) {
2387
+ const autoAcceptAt = e.autoAcceptAt;
2388
+ const was = this.doc.tidying?.autoAcceptAt;
2389
+ const tidying = { ...(this.doc.tidying ?? {}) };
2390
+ if (autoAcceptAt === null)
2391
+ delete tidying.autoAcceptAt;
2392
+ else
2393
+ tidying.autoAcceptAt = autoAcceptAt;
2394
+ if (Object.keys(tidying).length === 0)
2395
+ delete this.doc.tidying;
2396
+ else
2397
+ this.doc.tidying = tidying;
2398
+ this.said.push(appTidyingSentence(autoAcceptAt ?? undefined, was === (autoAcceptAt ?? undefined)));
2399
+ }
2169
2400
  /** A grant or a sight as the document holds one, or undefined having said why not. */
2170
2401
  policyPart(i, e, sight) {
2171
2402
  const { op: _op, ...rest } = e;
@@ -2198,13 +2429,15 @@ class Editor {
2198
2429
  return this.failAs("sight-field", i, `${key}.${n}`, `"${field}" is not a field of ${kinds.join(" or ")}`, `one of ${[...new Set(kinds.flatMap((kind) => Object.keys(this.doc.kinds[kind]?.fields ?? {})))].join(", ")}`);
2199
2430
  }
2200
2431
  }
2201
- if (!sight && part.mutations !== "*") {
2202
- for (const [n, act] of part.mutations.entries()) {
2432
+ for (const key of sight ? [] : ["mutations", "propose"]) {
2433
+ if (part[key] === undefined || part[key] === "*")
2434
+ continue;
2435
+ for (const [n, act] of part[key].entries()) {
2203
2436
  const derived = derivedActOf(act);
2204
2437
  if (this.doc.acts?.[act] || (derived && this.doc.kinds[derived.kind] && (this.doc.kinds[derived.kind].archives !== false || givenWhenNeverArchived(derived.act)) && (this.doc.kinds[derived.kind].tidying !== "never" || givenWhenNeverTidied(derived.act))) || givesRelationAct(act, this.doc.kinds))
2205
2438
  continue;
2206
2439
  const relationActs = relationActsOf(documentRelations(this.doc.kinds)).filter((name) => givesRelationAct(name, this.doc.kinds));
2207
- return this.failAs("grant-act", i, `mutations.${n}`, `"${act}" is not an act this app has`, `one of ${[...Object.keys(this.doc.acts ?? {}), ...Object.keys(this.doc.kinds).flatMap(derivedActsOf), ...relationActs].join(", ")}`);
2440
+ return this.failAs("grant-act", i, `${key}.${n}`, `"${act}" is not an act this app has`, `one of ${[...Object.keys(this.doc.acts ?? {}), ...Object.keys(this.doc.kinds).flatMap(derivedActsOf), ...relationActs].join(", ")}`);
2208
2441
  }
2209
2442
  }
2210
2443
  const said = [];
@@ -2245,10 +2478,10 @@ class Editor {
2245
2478
  removePolicyPart(i, part, sight) {
2246
2479
  const list = (sight ? this.doc.policy?.sees : this.doc.policy?.grants) ?? [];
2247
2480
  const sameSet = (a, b) => a === b || (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x) => b.includes(x)));
2248
- const keys = sight ? ["own", "via"] : ["kinds", "self", "ownEnd", "describe"];
2481
+ const keys = sight ? ["own", "via"] : ["kinds", "self", "ownEnd", "propose", "describe"];
2249
2482
  const matches = list.filter((one) => sameSet(one.roles, part.roles) &&
2250
2483
  (sight ? sameSet(one.kinds, part.kinds) : sameSet(one.mutations, part.mutations)) &&
2251
- keys.every((key) => part[key] === undefined || (typeof part[key] === "boolean" ? (one[key] ?? false) === part[key] : key === "kinds" ? sameSet(one[key] ?? "*", part[key]) : one[key] === part[key])));
2484
+ keys.every((key) => part[key] === undefined || (typeof part[key] === "boolean" ? (one[key] ?? false) === part[key] : key === "kinds" ? sameSet(one[key] ?? "*", part[key]) : key === "propose" ? sameSet(one[key], part[key]) : one[key] === part[key])));
2252
2485
  const what = sight ? "sight" : "grant";
2253
2486
  const say = (one) => (sight ? `${whoWords(one.roles)} ${sightWords(this.doc, one)}` : grantBody(this.doc, one));
2254
2487
  if (matches.length === 0) {
@@ -2477,6 +2710,7 @@ class Editor {
2477
2710
  * kind, or drew by it, goes, and `pages.first` with it when it named it.
2478
2711
  */
2479
2712
  const swept = { t: "kind", from: kind, to: "\u0000" };
2713
+ gone.push(...this.followDerives(swept, true));
2480
2714
  for (const [other, spec] of Object.entries(this.doc.kinds)) {
2481
2715
  for (const [name, c] of Object.entries(spec.computed ?? {})) {
2482
2716
  if (exprMentions(this.doc, typeof c === "string" ? c : c.expr, new Set([other]), swept))
@@ -2676,8 +2910,8 @@ class Editor {
2676
2910
  const f = this.field(i, e.kind, e.field);
2677
2911
  if (!f)
2678
2912
  return;
2679
- if (e.format === undefined && e.unit === undefined)
2680
- return this.fail(i, "format", 'say a "format" ("money", "percent" or "duration" for a number, "markdown" for a text field, or null for neither), a "unit" ("EUR", "kg", or null), or both');
2913
+ if (e.format === undefined && e.unit === undefined && e.places === undefined)
2914
+ return this.fail(i, "format", 'say a "format" ("money", "percent" or "duration" for a number, "markdown" for a text field, or null for neither), a "unit" ("EUR", "kg", or null), "places" (0 for money in whole units, or null), or some of them');
2681
2915
  if (f.type === "text") {
2682
2916
  if (e.unit !== undefined && e.unit !== null)
2683
2917
  return this.fail(i, "unit", refusedFormat("a unit", f.type));
@@ -2693,12 +2927,14 @@ class Editor {
2693
2927
  if (!formatFits(typeof e.format === "string" ? e.format : "money", f.type))
2694
2928
  return this.fail(i, "field", refusedFormat(typeof e.format === "string" ? e.format : "a unit", f.type));
2695
2929
  const before = formatWords(f, this.doc.brand);
2696
- for (const key of ["format", "unit"]) {
2930
+ for (const key of ["format", "unit", "places"]) {
2697
2931
  if (e[key] === null)
2698
2932
  delete f[key];
2699
2933
  else if (e[key] !== undefined)
2700
2934
  f[key] = e[key];
2701
2935
  }
2936
+ if (f.places !== undefined && !isMoney(f))
2937
+ return this.fail(i, "places", `"places" is said of money, and ${e.kind}'s ${e.field} is ${formatWords(f, this.doc.brand) ?? "a plain number"}`, 'say "format": "money" with it, or leave "places" out');
2702
2938
  const now = formatWords(f, this.doc.brand);
2703
2939
  this.said.push(`${e.kind}'s ${e.field} is said as ${now ?? "a plain number"}${before ? `, where it was ${before.replace(/ — .*/, "")}` : ""}; its values are kept.`);
2704
2940
  }
@@ -2710,19 +2946,21 @@ class Editor {
2710
2946
  * its expression is left.
2711
2947
  */
2712
2948
  setComputedFormat(i, e, worked) {
2713
- if (e.format === undefined && e.unit === undefined)
2714
- return this.fail(i, "format", 'say a "format" ("money", "percent" or "duration", or null for none), a "unit" ("EUR", "kg", or null), or both');
2949
+ if (e.format === undefined && e.unit === undefined && e.places === undefined)
2950
+ return this.fail(i, "format", 'say a "format" ("money", "percent" or "duration", or null for none), a "unit" ("EUR", "kg", or null), "places" (0 for money in whole units, or null), or some of them');
2715
2951
  if (e.format === "markdown")
2716
2952
  return this.fail(i, "format", `${e.field} is worked out, not written, so it cannot be Markdown; a computed field is said as "money", "percent" or "duration"`);
2717
2953
  const spec = this.doc.kinds[e.kind];
2718
2954
  const next = typeof worked === "string" ? { expr: worked } : { ...worked };
2719
2955
  const before = formatWords(next, this.doc.brand);
2720
- for (const key of ["format", "unit"]) {
2956
+ for (const key of ["format", "unit", "places"]) {
2721
2957
  if (e[key] === null)
2722
2958
  delete next[key];
2723
2959
  else if (e[key] !== undefined)
2724
2960
  next[key] = e[key];
2725
2961
  }
2962
+ if (next.places !== undefined && !isMoney(next))
2963
+ return this.fail(i, "places", `"places" is said of money, and ${e.kind}'s ${e.field} is ${formatWords(next, this.doc.brand) ?? "a plain number"}`, 'say "format": "money" with it, or leave "places" out');
2726
2964
  spec.computed[e.field] = Object.keys(next).length === 1 ? next.expr : next;
2727
2965
  const now = formatWords(next, this.doc.brand);
2728
2966
  this.said.push(`${e.kind}'s ${e.field} is worked out as before and said as ${now ?? "a plain number"}${before ? `, where it was ${before.replace(/ — .*/, "")}` : ""}.`);
@@ -3065,6 +3303,8 @@ class Editor {
3065
3303
  const prose = r.t === "field" ? proseSwap(r.from, r.to) : r.t === "kind" ? proseSwap(r.from, r.to) : undefined;
3066
3304
  const exprAt = (source, ctx) => (source === undefined ? undefined : rewriteExpr(doc, source, ctx, r));
3067
3305
  const tmplAt = (source, ctx, words) => (source === undefined ? undefined : rewriteTemplate(doc, source, ctx, r, words));
3306
+ // What a kind follows (Roadmap f-keep-linked): its kind, relation and values follow the rename.
3307
+ touched.push(...this.followDerives(r, false));
3068
3308
  // Kinds: templates, the lifecycle, where relations lead.
3069
3309
  for (const [kind, spec] of Object.entries(doc.kinds)) {
3070
3310
  const here = new Set([kind]);
@@ -3373,6 +3613,7 @@ class Editor {
3373
3613
  // A kind's list ordered or grouped by it lets that go (a stored field removed, a computed one no longer worked out).
3374
3614
  if (r.t === "field")
3375
3615
  gone.push(...this.listsFollow(r.kind, r.from, undefined));
3616
+ gone.push(...this.followDerives(r, true));
3376
3617
  for (const [kind, spec] of Object.entries(doc.kinds)) {
3377
3618
  for (const key of ["label", "describe"]) {
3378
3619
  if (templateMentions(doc, spec[key], new Set([kind]), r)) {