@graview/core 0.1.60 → 0.1.62

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 (307) hide show
  1. package/README.md +23 -1
  2. package/dist/app-places.d.ts.map +1 -1
  3. package/dist/app-places.js +2 -2
  4. package/dist/app-places.js.map +1 -1
  5. package/dist/app.d.ts +10 -0
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js.map +1 -1
  8. package/dist/arrangement.d.ts +2 -0
  9. package/dist/arrangement.d.ts.map +1 -1
  10. package/dist/arrangement.js +14 -0
  11. package/dist/arrangement.js.map +1 -1
  12. package/dist/capabilities.js +1 -1
  13. package/dist/capabilities.js.map +1 -1
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +2 -1
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/cli/check/context.d.ts +5 -0
  18. package/dist/cli/check/context.d.ts.map +1 -1
  19. package/dist/cli/check/context.js.map +1 -1
  20. package/dist/cli/check/face.d.ts +14 -0
  21. package/dist/cli/check/face.d.ts.map +1 -0
  22. package/dist/cli/check/face.js +161 -0
  23. package/dist/cli/check/face.js.map +1 -0
  24. package/dist/cli/check/hidden-reads.d.ts.map +1 -1
  25. package/dist/cli/check/hidden-reads.js +26 -0
  26. package/dist/cli/check/hidden-reads.js.map +1 -1
  27. package/dist/cli/check/index.d.ts +1 -0
  28. package/dist/cli/check/index.d.ts.map +1 -1
  29. package/dist/cli/check/index.js +1 -0
  30. package/dist/cli/check/index.js.map +1 -1
  31. package/dist/cli/check/tidying.d.ts +14 -0
  32. package/dist/cli/check/tidying.d.ts.map +1 -0
  33. package/dist/cli/check/tidying.js +24 -0
  34. package/dist/cli/check/tidying.js.map +1 -0
  35. package/dist/cli/check.d.ts.map +1 -1
  36. package/dist/cli/check.js +4 -4
  37. package/dist/cli/check.js.map +1 -1
  38. package/dist/cli/describe.d.ts.map +1 -1
  39. package/dist/cli/describe.js +14 -1
  40. package/dist/cli/describe.js.map +1 -1
  41. package/dist/cli/docs.d.ts.map +1 -1
  42. package/dist/cli/docs.js +5 -3
  43. package/dist/cli/docs.js.map +1 -1
  44. package/dist/cli/document-schema.d.ts.map +1 -1
  45. package/dist/cli/document-schema.js +8 -2
  46. package/dist/cli/document-schema.js.map +1 -1
  47. package/dist/cli/index.d.ts +2 -1
  48. package/dist/cli/index.d.ts.map +1 -1
  49. package/dist/cli/index.js +15 -4
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/types.d.ts.map +1 -1
  52. package/dist/cli/types.js +2 -1
  53. package/dist/cli/types.js.map +1 -1
  54. package/dist/compose.d.ts +14 -0
  55. package/dist/compose.d.ts.map +1 -1
  56. package/dist/compose.js +16 -1
  57. package/dist/compose.js.map +1 -1
  58. package/dist/css/sanitize.d.ts +125 -0
  59. package/dist/css/sanitize.d.ts.map +1 -0
  60. package/dist/css/sanitize.js +989 -0
  61. package/dist/css/sanitize.js.map +1 -0
  62. package/dist/css/tables.d.ts +55 -0
  63. package/dist/css/tables.d.ts.map +1 -0
  64. package/dist/css/tables.js +137 -0
  65. package/dist/css/tables.js.map +1 -0
  66. package/dist/css.d.ts +20 -0
  67. package/dist/css.d.ts.map +1 -0
  68. package/dist/css.js +19 -0
  69. package/dist/css.js.map +1 -0
  70. package/dist/days.d.ts +15 -0
  71. package/dist/days.d.ts.map +1 -1
  72. package/dist/days.js +21 -0
  73. package/dist/days.js.map +1 -1
  74. package/dist/describe.d.ts +1 -1
  75. package/dist/describe.d.ts.map +1 -1
  76. package/dist/document/act-findings.d.ts.map +1 -1
  77. package/dist/document/act-findings.js +80 -1
  78. package/dist/document/act-findings.js.map +1 -1
  79. package/dist/document/blocks.d.ts.map +1 -1
  80. package/dist/document/blocks.js +2 -1
  81. package/dist/document/blocks.js.map +1 -1
  82. package/dist/document/brand-check.d.ts +15 -1
  83. package/dist/document/brand-check.d.ts.map +1 -1
  84. package/dist/document/brand-check.js +80 -0
  85. package/dist/document/brand-check.js.map +1 -1
  86. package/dist/document/brand.d.ts.map +1 -1
  87. package/dist/document/brand.js +2 -0
  88. package/dist/document/brand.js.map +1 -1
  89. package/dist/document/compile-checked.d.ts.map +1 -1
  90. package/dist/document/compile-checked.js +4 -3
  91. package/dist/document/compile-checked.js.map +1 -1
  92. package/dist/document/compile.d.ts.map +1 -1
  93. package/dist/document/compile.js +54 -9
  94. package/dist/document/compile.js.map +1 -1
  95. package/dist/document/compiled.d.ts +18 -3
  96. package/dist/document/compiled.d.ts.map +1 -1
  97. package/dist/document/compiled.js +91 -10
  98. package/dist/document/compiled.js.map +1 -1
  99. package/dist/document/connection-findings.d.ts.map +1 -1
  100. package/dist/document/connection-findings.js +219 -19
  101. package/dist/document/connection-findings.js.map +1 -1
  102. package/dist/document/connection-spec.d.ts +7 -0
  103. package/dist/document/connection-spec.d.ts.map +1 -1
  104. package/dist/document/connection-spec.js +26 -2
  105. package/dist/document/connection-spec.js.map +1 -1
  106. package/dist/document/covers.d.ts +48 -0
  107. package/dist/document/covers.d.ts.map +1 -0
  108. package/dist/document/covers.js +90 -0
  109. package/dist/document/covers.js.map +1 -0
  110. package/dist/document/describe-place.d.ts +3 -0
  111. package/dist/document/describe-place.d.ts.map +1 -1
  112. package/dist/document/describe-place.js +11 -0
  113. package/dist/document/describe-place.js.map +1 -1
  114. package/dist/document/described.d.ts +28 -0
  115. package/dist/document/described.d.ts.map +1 -1
  116. package/dist/document/diff.js +10 -1
  117. package/dist/document/diff.js.map +1 -1
  118. package/dist/document/edit.d.ts +1 -1
  119. package/dist/document/edit.d.ts.map +1 -1
  120. package/dist/document/edit.js +186 -18
  121. package/dist/document/edit.js.map +1 -1
  122. package/dist/document/expr/evaluate.d.ts +10 -1
  123. package/dist/document/expr/evaluate.d.ts.map +1 -1
  124. package/dist/document/expr/evaluate.js +110 -2
  125. package/dist/document/expr/evaluate.js.map +1 -1
  126. package/dist/document/expr/index.d.ts +1 -0
  127. package/dist/document/expr/index.d.ts.map +1 -1
  128. package/dist/document/expr/index.js +2 -0
  129. package/dist/document/expr/index.js.map +1 -1
  130. package/dist/document/expr/typing.d.ts +24 -0
  131. package/dist/document/expr/typing.d.ts.map +1 -0
  132. package/dist/document/expr/typing.js +219 -0
  133. package/dist/document/expr/typing.js.map +1 -0
  134. package/dist/document/expr/when.d.ts +52 -0
  135. package/dist/document/expr/when.d.ts.map +1 -0
  136. package/dist/document/expr/when.js +215 -0
  137. package/dist/document/expr/when.js.map +1 -0
  138. package/dist/document/expr-type-findings.d.ts +5 -0
  139. package/dist/document/expr-type-findings.d.ts.map +1 -0
  140. package/dist/document/expr-type-findings.js +90 -0
  141. package/dist/document/expr-type-findings.js.map +1 -0
  142. package/dist/document/field-spec.d.ts.map +1 -1
  143. package/dist/document/field-spec.js +5 -0
  144. package/dist/document/field-spec.js.map +1 -1
  145. package/dist/document/findings.d.ts +33 -2
  146. package/dist/document/findings.d.ts.map +1 -1
  147. package/dist/document/findings.js +14 -17
  148. package/dist/document/findings.js.map +1 -1
  149. package/dist/document/index.d.ts +6 -3
  150. package/dist/document/index.d.ts.map +1 -1
  151. package/dist/document/index.js +5 -2
  152. package/dist/document/index.js.map +1 -1
  153. package/dist/document/linked-findings.js +1 -1
  154. package/dist/document/linked-findings.js.map +1 -1
  155. package/dist/document/migrate.d.ts.map +1 -1
  156. package/dist/document/migrate.js +10 -4
  157. package/dist/document/migrate.js.map +1 -1
  158. package/dist/document/rule-line.d.ts +7 -3
  159. package/dist/document/rule-line.d.ts.map +1 -1
  160. package/dist/document/rule-line.js +17 -2
  161. package/dist/document/rule-line.js.map +1 -1
  162. package/dist/document/rule-reads.d.ts.map +1 -1
  163. package/dist/document/rule-reads.js +3 -1
  164. package/dist/document/rule-reads.js.map +1 -1
  165. package/dist/document/rules.d.ts +30 -0
  166. package/dist/document/rules.d.ts.map +1 -1
  167. package/dist/document/rules.js +74 -0
  168. package/dist/document/rules.js.map +1 -1
  169. package/dist/document/schema.d.ts +80 -2
  170. package/dist/document/schema.d.ts.map +1 -1
  171. package/dist/document/schema.js +90 -4
  172. package/dist/document/schema.js.map +1 -1
  173. package/dist/document/to-document.d.ts.map +1 -1
  174. package/dist/document/to-document.js +3 -1
  175. package/dist/document/to-document.js.map +1 -1
  176. package/dist/document.schema.json +304 -5
  177. package/dist/formats.d.ts +21 -13
  178. package/dist/formats.d.ts.map +1 -1
  179. package/dist/formats.js +21 -13
  180. package/dist/formats.js.map +1 -1
  181. package/dist/import/coerce.d.ts.map +1 -1
  182. package/dist/import/coerce.js +16 -4
  183. package/dist/import/coerce.js.map +1 -1
  184. package/dist/index.d.ts +6 -5
  185. package/dist/index.d.ts.map +1 -1
  186. package/dist/index.js +5 -3
  187. package/dist/index.js.map +1 -1
  188. package/dist/invariants/types.d.ts +7 -2
  189. package/dist/invariants/types.d.ts.map +1 -1
  190. package/dist/kind-list.d.ts +1 -0
  191. package/dist/kind-list.d.ts.map +1 -1
  192. package/dist/kind-list.js +2 -1
  193. package/dist/kind-list.js.map +1 -1
  194. package/dist/lines.d.ts.map +1 -1
  195. package/dist/lines.js +6 -2
  196. package/dist/lines.js.map +1 -1
  197. package/dist/mutations/form.d.ts +2 -0
  198. package/dist/mutations/form.d.ts.map +1 -1
  199. package/dist/mutations/form.js +6 -3
  200. package/dist/mutations/form.js.map +1 -1
  201. package/dist/mutations/types.d.ts +9 -0
  202. package/dist/mutations/types.d.ts.map +1 -1
  203. package/dist/mutations/vote-suggestion.d.ts +67 -0
  204. package/dist/mutations/vote-suggestion.d.ts.map +1 -0
  205. package/dist/mutations/vote-suggestion.js +36 -0
  206. package/dist/mutations/vote-suggestion.js.map +1 -0
  207. package/dist/navigate.d.ts +7 -3
  208. package/dist/navigate.d.ts.map +1 -1
  209. package/dist/navigate.js +7 -3
  210. package/dist/navigate.js.map +1 -1
  211. package/dist/ops/lineage.d.ts +11 -1
  212. package/dist/ops/lineage.d.ts.map +1 -1
  213. package/dist/ops/lineage.js +6 -2
  214. package/dist/ops/lineage.js.map +1 -1
  215. package/dist/ops/suggestions.d.ts +33 -1
  216. package/dist/ops/suggestions.d.ts.map +1 -1
  217. package/dist/ops/suggestions.js +44 -2
  218. package/dist/ops/suggestions.js.map +1 -1
  219. package/dist/ops/types.d.ts +13 -0
  220. package/dist/ops/types.d.ts.map +1 -1
  221. package/dist/ops/types.js.map +1 -1
  222. package/dist/place-names.d.ts +42 -0
  223. package/dist/place-names.d.ts.map +1 -0
  224. package/dist/place-names.js +43 -0
  225. package/dist/place-names.js.map +1 -0
  226. package/dist/places.d.ts +2 -0
  227. package/dist/places.d.ts.map +1 -1
  228. package/dist/places.js +2 -0
  229. package/dist/places.js.map +1 -1
  230. package/dist/scaffold/agents.d.ts +9 -1
  231. package/dist/scaffold/agents.d.ts.map +1 -1
  232. package/dist/scaffold/agents.js +33 -2
  233. package/dist/scaffold/agents.js.map +1 -1
  234. package/dist/scaffold/project.js +1 -1
  235. package/dist/schema/choice-words.d.ts.map +1 -1
  236. package/dist/schema/choice-words.js +3 -0
  237. package/dist/schema/choice-words.js.map +1 -1
  238. package/dist/starting-document.d.ts +73 -0
  239. package/dist/starting-document.d.ts.map +1 -0
  240. package/dist/starting-document.js +645 -0
  241. package/dist/starting-document.js.map +1 -0
  242. package/dist/store.d.ts +50 -0
  243. package/dist/store.d.ts.map +1 -1
  244. package/dist/store.js +167 -27
  245. package/dist/store.js.map +1 -1
  246. package/dist/sync/connection.d.ts +20 -1
  247. package/dist/sync/connection.d.ts.map +1 -1
  248. package/dist/sync/connection.js +198 -34
  249. package/dist/sync/connection.js.map +1 -1
  250. package/dist/sync/engine.d.ts +13 -4
  251. package/dist/sync/engine.d.ts.map +1 -1
  252. package/dist/sync/engine.js +10 -6
  253. package/dist/sync/engine.js.map +1 -1
  254. package/dist/sync/google-calendar.d.ts +8 -0
  255. package/dist/sync/google-calendar.d.ts.map +1 -1
  256. package/dist/sync/google-calendar.js +16 -4
  257. package/dist/sync/google-calendar.js.map +1 -1
  258. package/dist/sync/guests.d.ts +44 -0
  259. package/dist/sync/guests.d.ts.map +1 -0
  260. package/dist/sync/guests.js +57 -0
  261. package/dist/sync/guests.js.map +1 -0
  262. package/dist/sync/notion-types.d.ts +7 -0
  263. package/dist/sync/notion-types.d.ts.map +1 -1
  264. package/dist/sync/notion-types.js +6 -1
  265. package/dist/sync/notion-types.js.map +1 -1
  266. package/dist/sync/recurrence.d.ts +41 -0
  267. package/dist/sync/recurrence.d.ts.map +1 -0
  268. package/dist/sync/recurrence.js +192 -0
  269. package/dist/sync/recurrence.js.map +1 -0
  270. package/dist/sync/types.d.ts +9 -0
  271. package/dist/sync/types.d.ts.map +1 -1
  272. package/dist/sync/types.js.map +1 -1
  273. package/dist/testing.js +1 -1
  274. package/dist/testing.js.map +1 -1
  275. package/dist/theme/contrast.d.ts +2 -0
  276. package/dist/theme/contrast.d.ts.map +1 -1
  277. package/dist/theme/contrast.js +4 -0
  278. package/dist/theme/contrast.js.map +1 -1
  279. package/dist/theme/ground.d.ts +1 -2
  280. package/dist/theme/ground.d.ts.map +1 -1
  281. package/dist/theme/ground.js +2 -5
  282. package/dist/theme/ground.js.map +1 -1
  283. package/dist/theme/marks.d.ts +14 -2
  284. package/dist/theme/marks.d.ts.map +1 -1
  285. package/dist/theme/marks.js +20 -3
  286. package/dist/theme/marks.js.map +1 -1
  287. package/dist/theme/types.d.ts +22 -0
  288. package/dist/theme/types.d.ts.map +1 -1
  289. package/dist/theme/types.js.map +1 -1
  290. package/dist/tidying.d.ts +30 -4
  291. package/dist/tidying.d.ts.map +1 -1
  292. package/dist/tidying.js +110 -5
  293. package/dist/tidying.js.map +1 -1
  294. package/dist/trait-vocabulary.d.ts +17 -0
  295. package/dist/trait-vocabulary.d.ts.map +1 -0
  296. package/dist/trait-vocabulary.js +16 -0
  297. package/dist/trait-vocabulary.js.map +1 -0
  298. package/dist/traits.d.ts +79 -0
  299. package/dist/traits.d.ts.map +1 -0
  300. package/dist/traits.js +468 -0
  301. package/dist/traits.js.map +1 -0
  302. package/dist/typed.d.ts +4 -0
  303. package/dist/typed.d.ts.map +1 -1
  304. package/dist/typed.js.map +1 -1
  305. package/dist/version.d.ts +1 -1
  306. package/dist/version.js +1 -1
  307. package/package.json +11 -1
@@ -18,6 +18,7 @@ import { jobFindings, jobWorldOfDocument } from "../job-findings.js";
18
18
  import { JobSpec } from "./purpose-spec.js";
19
19
  import { aliasWord, documentAliasFindings } from "../alias-findings.js";
20
20
  import { humanizeField, withArticle } from "../schema/define-node.js";
21
+ import { ConnectionSpec } from "./connection-spec.js";
21
22
  import { linkedRelationFindings } from "./linked-relation-findings.js";
22
23
  import { linkedRelationOfSpec, reachesAnotherApp } from "../schema/linked.js";
23
24
  import { ActSpec, ALIAS, MOST_ALIASES, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
@@ -28,7 +29,7 @@ import { documentListFields, kindListFindings, kindListWords, listGroup, listSor
28
29
  import { computedOf, validateComputed } from "./computed.js";
29
30
  import { parseTemplate, templateBraces, TemplateError } from "./template-parse.js";
30
31
  import { validateViews, VIEW_SLOTS } from "./views.js";
31
- import { brandFindings, emojiIconSentence, groundWords, subtitleSentence } from "./brand-check.js";
32
+ import { brandFindings, emojiIconSentence, groundWords, splashSentence, subtitleSentence, viewsCssSentence, viewsMotionSentence } from "./brand-check.js";
32
33
  import { darkLogoSentence, logoParts } from "./logo.js";
33
34
  import { emojiOf } from "../theme/marks.js";
34
35
  import { canonicalize } from "./canonical.js";
@@ -93,6 +94,7 @@ export const EDIT_OPS = [
93
94
  "remove-job",
94
95
  "add-alias",
95
96
  "set-aliases",
97
+ "set-connection",
96
98
  ];
97
99
  /** Words the rule language keeps: a field or relation renamed to one could not be named in a rule. */
98
100
  const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
@@ -122,7 +124,7 @@ function buildShapes() {
122
124
  "remove-kind": z.object({ op: z.literal("remove-kind"), kind: kindName }).strict(),
123
125
  "add-field": z.object({ op: z.literal("add-field"), kind: kindName, field: fieldName, fill: z.unknown().optional(), spec: z.record(z.string(), z.unknown()).optional() }).passthrough(),
124
126
  "rename-field": z.object({ op: z.literal("rename-field"), kind: kindName, field: fieldName, to: fieldName }).strict(),
125
- "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", "image", "video", "file"]).optional(), format: z.enum(["money", "percent", "duration", "markdown"]).optional() }).strict(),
127
+ "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(),
126
128
  "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(),
127
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(),
128
130
  "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(),
@@ -165,6 +167,15 @@ function buildShapes() {
165
167
  scheme: z.union([z.enum(["light", "dark", "auto"]), z.null()]).optional(),
166
168
  // 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.
167
169
  subtitle: z.union([z.boolean(), z.null()]).optional(),
170
+ // 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(),
172
+ // 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
+ views: z
174
+ .union([
175
+ z.object({ css: z.union([z.string().min(1), z.null()]).optional(), motion: z.union([z.enum(["none", "fade"]), z.null()]).optional() }).strict(),
176
+ z.null(),
177
+ ])
178
+ .optional(),
168
179
  })
169
180
  .strict(),
170
181
  "set-name": z.object({ op: z.literal("set-name"), name: z.string().min(1).max(80) }).strict(),
@@ -269,6 +280,8 @@ function buildShapes() {
269
280
  "add-alias": z.object({ op: z.literal("add-alias"), kind: kindName, field: fieldName.optional(), alias: z.string().min(1).max(40) }).strict(),
270
281
  // All of a kind's or a field's other words at once; null or [] takes them away.
271
282
  "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
+ // A connection declared whole, by its name: set where there was none, or replaced (FR-229); null takes it away.
284
+ "set-connection": z.object({ op: z.literal("set-connection"), name: kindName, connection: z.union([z.record(z.string(), z.unknown()), z.null()]) }).strict(),
272
285
  };
273
286
  }
274
287
  let shapes;
@@ -276,6 +289,13 @@ const opShapes = () => (shapes ??= buildShapes());
276
289
  const NONE = new Set();
277
290
  const clone = (v) => structuredClone(v);
278
291
  const asArray = (v) => [v ?? []].flat();
292
+ /** The first of a choice's default that is none of its options — a list of them where the choice takes several (FR-233) — or undefined when every one is. */
293
+ function defaultOutsideOptions(f, given) {
294
+ const options = f.options ?? [];
295
+ if (!f.multiple)
296
+ return Array.isArray(given) || !options.includes(String(given)) ? given : undefined;
297
+ return (Array.isArray(given) ? given : [given]).find((one) => !options.includes(String(one)));
298
+ }
279
299
  const kebab = (camel) => camel.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
280
300
  const words = (name) => kebab(name).replace(/-/g, " ");
281
301
  /** Another app's kinds as people say them: "one house in the app @nick/house", "houses or flats in the app @nick/house". Their plural is that app's to say; this one says the plain one. */
@@ -539,6 +559,11 @@ function setTargets(act, doc) {
539
559
  const end = doc && relation ? farEnd(doc.kinds, relation, asArray(act.on)) : undefined;
540
560
  out.push({ set: act.setsOther, kinds: end && end.kinds !== "*" ? new Set(end.kinds) : "*", where: "setsOther" });
541
561
  }
562
+ // What it carries to the records linked to its subject, by relation: the kinds at each relation's far end.
563
+ for (const [relation, set] of Object.entries(act.setsLinked ?? {})) {
564
+ const end = doc ? farEnd(doc.kinds, relation, asArray(act.on)) : undefined;
565
+ out.push({ set, kinds: end && end.kinds !== "*" ? new Set(end.kinds) : "*", where: "setsLinked", relation });
566
+ }
542
567
  (act.effects ?? []).forEach((e, index) => {
543
568
  if (e.create)
544
569
  out.push({ set: e.set ?? {}, kinds: new Set([e.create]), where: "create", index });
@@ -591,10 +616,18 @@ class Editor {
591
616
  const named = Object.keys(e).filter((key) => key !== "op" && e[key] !== undefined);
592
617
  if (named.length === 0)
593
618
  return this.fail(i, "", "a set-brand edit names at least one key of the brand to set, or null to clear one", '{"op": "set-brand", "accent": "#0f6e5c"}');
594
- const emptyPart = ["typography", "shape", "accents"].find((key) => isObject(e[key]) && Object.keys(e[key]).length === 0);
619
+ const emptyPart = ["typography", "shape", "accents", "views"].find((key) => isObject(e[key]) && Object.keys(e[key]).length === 0);
595
620
  if (emptyPart)
596
621
  return this.fail(i, emptyPart, `${emptyPart} names no part to set; name one, or give null to clear all of it`);
597
- const judged = brandFindings({ ...(typeof e.timeZone === "string" ? { timeZone: e.timeZone } : {}), ...(typeof e.logo === "string" || isObject(e.logo) ? { logo: e.logo } : {}), ...(typeof e.favicon === "string" || isObject(e.favicon) ? { favicon: e.favicon } : {}), ...(isObject(e.typography) ? { typography: Object.fromEntries(Object.entries(e.typography).filter(([, v]) => typeof v === "string")) } : {}) }, { at, ...(this.options.fonts ? { fonts: this.options.fonts } : {}) });
622
+ const judged = brandFindings({
623
+ ...(typeof e.timeZone === "string" ? { timeZone: e.timeZone } : {}),
624
+ ...(isObject(e.splash) ? { splash: e.splash } : {}),
625
+ ...(typeof e.logo === "string" || isObject(e.logo) ? { logo: e.logo } : {}),
626
+ ...(typeof e.favicon === "string" || isObject(e.favicon) ? { favicon: e.favicon } : {}),
627
+ ...(isObject(e.typography) ? { typography: Object.fromEntries(Object.entries(e.typography).filter(([, v]) => typeof v === "string")) } : {}),
628
+ // The stylesheet every view gets is judged as a page draws it (Roadmap f-app-wide-view-theme): an edit that would leave part of it undrawn is refused, saying which part.
629
+ ...(isObject(e.views) && typeof e.views["css"] === "string" ? { views: { css: e.views["css"] } } : {}),
630
+ }, { at, ...(this.options.fonts ? { fonts: this.options.fonts } : {}) });
598
631
  if (judged.length > 0)
599
632
  return void this.findings.push(...judged);
600
633
  if (e.accent === null) {
@@ -768,6 +801,45 @@ class Editor {
768
801
  delete brand["subtitle"];
769
802
  this.said.push(subtitleSentence(e.subtitle !== false));
770
803
  }
804
+ // The opening screen is set whole or cleared: its keys are few, and half of one is nothing a page could draw.
805
+ if (e.splash !== undefined) {
806
+ if (e.splash === null)
807
+ delete brand["splash"];
808
+ else
809
+ brand["splash"] = e.splash;
810
+ if (!(e.splash === null && !isObject(this.doc.brand?.splash)))
811
+ this.said.push(splashSentence(e.splash));
812
+ }
813
+ /* What every view inherits (Roadmap f-app-wide-view-theme): the stylesheet and the fade, each set or cleared on its own, null clearing both. */
814
+ if (e.views === null) {
815
+ const had = brand["views"] ?? {};
816
+ if (had["css"] !== undefined || had["motion"] === undefined)
817
+ this.said.push(viewsCssSentence(false));
818
+ if (had["motion"] === "fade")
819
+ this.said.push(viewsMotionSentence(undefined));
820
+ delete brand["views"];
821
+ }
822
+ else if (isObject(e.views)) {
823
+ const into = { ...(brand["views"] ?? {}) };
824
+ if (e.views["css"] !== undefined) {
825
+ if (e.views["css"] === null)
826
+ delete into["css"];
827
+ else
828
+ into["css"] = e.views["css"];
829
+ this.said.push(viewsCssSentence(e.views["css"] !== null));
830
+ }
831
+ if (e.views["motion"] !== undefined) {
832
+ if (e.views["motion"] === "fade")
833
+ into["motion"] = "fade";
834
+ else
835
+ delete into["motion"];
836
+ this.said.push(viewsMotionSentence(e.views["motion"] === "fade" ? "fade" : undefined));
837
+ }
838
+ if (Object.keys(into).length === 0)
839
+ delete brand["views"];
840
+ else
841
+ brand["views"] = into;
842
+ }
771
843
  if (Object.keys(brand).length === 0)
772
844
  delete this.doc.brand;
773
845
  else
@@ -860,8 +932,8 @@ class Editor {
860
932
  this.said.push(`${e.kind}'s ${e.field} has no default now.`);
861
933
  }
862
934
  else {
863
- if (f.type === "enum" && !(f.options ?? []).includes(e.default))
864
- return this.fail(i, "default", `"${String(e.default)}" is not one of ${e.field}'s options (${(f.options ?? []).join(", ")})`);
935
+ if (f.type === "enum" && defaultOutsideOptions(f, e.default) !== undefined)
936
+ return this.fail(i, "default", `"${String(defaultOutsideOptions(f, e.default))}" is not one of ${e.field}'s options (${(f.options ?? []).join(", ")})`);
865
937
  const outside = outsideRange(e.default, f, e.field);
866
938
  if (outside)
867
939
  return this.fail(i, "default", outside);
@@ -1030,8 +1102,37 @@ class Editor {
1030
1102
  return this.setAliases(i, e.kind, e.field, [...this.aliasesOf(e.kind, e.field), e.alias], e.alias);
1031
1103
  case "set-aliases":
1032
1104
  return this.setAliases(i, e.kind, e.field, e.aliases ?? []);
1105
+ case "set-connection":
1106
+ return this.setConnection(i, e.name, e.connection);
1033
1107
  }
1034
1108
  }
1109
+ // ── a connection, declared whole (FR-229) ──
1110
+ /**
1111
+ * A CONNECTION SET BY NAME: its shape is judged here, as the document's
1112
+ * is; what it says of the kind and the fields it maps, and the acts an
1113
+ * inbound change lands through, the checker judges of the document it
1114
+ * leaves (`connection-findings.ts`). Null takes the connection away.
1115
+ */
1116
+ setConnection(i, name, connection) {
1117
+ if (connection === null) {
1118
+ if (!this.doc.connections?.[name])
1119
+ return this.fail(i, "name", `there is no connection called "${name}"${this.doc.connections ? `; there ${Object.keys(this.doc.connections).length === 1 ? "is" : "are"} ${Object.keys(this.doc.connections).join(", ")}` : ""}`);
1120
+ delete this.doc.connections[name];
1121
+ if (Object.keys(this.doc.connections).length === 0)
1122
+ delete this.doc.connections;
1123
+ this.said.push(`The connection ${name} is gone: nothing keeps its records in step now.`);
1124
+ return;
1125
+ }
1126
+ const parsed = ConnectionSpec.safeParse(connection);
1127
+ if (!parsed.success) {
1128
+ this.refuse(i, parsed.error.issues.map((issue) => ({ ...issue, path: ["connection", ...issue.path] })), "a connection");
1129
+ return;
1130
+ }
1131
+ const was = this.doc.connections?.[name] !== undefined;
1132
+ (this.doc.connections ??= {})[name] = parsed.data;
1133
+ 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"}.`);
1135
+ }
1035
1136
  // ── other words for a kind or a field (Roadmap f-learned-suggestions) ──
1036
1137
  aliasesOf(kind, field) {
1037
1138
  const spec = this.doc.kinds[kind];
@@ -1361,9 +1462,10 @@ class Editor {
1361
1462
  return (this.fail(i, `bindings.${kind}.column`, `the board does not say which field of ${kind} its columns are`, example), false);
1362
1463
  if (!(field in fields))
1363
1464
  return (this.fail(i, `bindings.${kind}.column`, `${kind} has no field "${field}"; it has ${Object.keys(fields).join(", ")}`), false);
1364
- if (fields[field]["type"] !== "enum") {
1365
- const choices = Object.keys(fields).filter((one) => fields[one]["type"] === "enum");
1366
- 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);
1465
+ // A choice that takes several of its options (FR-233) has no columns: a record would stand in several.
1466
+ if (fields[field]["type"] !== "enum" || fields[field]["multiple"] === true) {
1467
+ const choices = Object.keys(fields).filter((one) => fields[one]["type"] === "enum" && fields[one]["multiple"] !== true);
1468
+ return (this.fail(i, `bindings.${kind}.column`, `${kind}'s ${field} is ${fields[field]["multiple"] === true ? "a choice of several, so a record would stand in more than one column" : "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);
1367
1469
  }
1368
1470
  const others = Object.keys(roles).filter((role) => role !== "column");
1369
1471
  if (others.length > 0)
@@ -2237,6 +2339,12 @@ class Editor {
2237
2339
  spec.plural = e.plural;
2238
2340
  else if (spec.plural && (spec.plural === `${words(from)}s` || spec.plural === `${from}s`))
2239
2341
  spec.plural = `${words(to)}s`;
2342
+ // Its hue follows it.
2343
+ if (isObject(this.doc.brand?.accents) && from in this.doc.brand.accents && !(to in this.doc.brand.accents)) {
2344
+ this.doc.brand.accents[to] = this.doc.brand.accents[from];
2345
+ delete this.doc.brand.accents[from];
2346
+ touched.push("its hue");
2347
+ }
2240
2348
  // Its list's filters, order and grouping follow it.
2241
2349
  const lists = this.doc.pages?.lists;
2242
2350
  if (lists?.[from] !== undefined && lists[to] === undefined) {
@@ -2288,6 +2396,7 @@ class Editor {
2288
2396
  gone.push(what);
2289
2397
  }
2290
2398
  const removedPlural = this.doc.kinds[kind].plural ?? `${words(kind)}s`;
2399
+ const people = this.doc.kinds[kind].people === true;
2291
2400
  // Its own relations' connect and disconnect go with it.
2292
2401
  const ownRelationActs = relationActsOf(documentRelations({ [kind]: this.doc.kinds[kind] }));
2293
2402
  delete this.doc.kinds[kind];
@@ -2306,17 +2415,27 @@ class Editor {
2306
2415
  }
2307
2416
  }
2308
2417
  }
2418
+ // With the people kind gone, an act on the acting person's own record (`$me`) has no record to reach: it goes too.
2309
2419
  for (const [name, act] of Object.entries(this.doc.acts ?? {})) {
2310
2420
  const on = asArray(act.on);
2311
- if (act.creates === kind || (act.effects ?? []).some((x) => x.create === kind) || (on.includes(kind) && on.length === 1)) {
2421
+ if (act.creates === kind || (act.effects ?? []).some((x) => x.create === kind) || (on.includes(kind) && on.length === 1) || (people && JSON.stringify(act).includes('"$me"'))) {
2312
2422
  gone.push(titled("act", name, act));
2313
2423
  this.removeActQuietly(name);
2314
2424
  }
2315
2425
  else if (on.includes(kind))
2316
2426
  act.on = on.filter((k) => k !== kind);
2317
2427
  }
2428
+ // Its hue goes with it: an accent for a kind nobody declares is refused by the checker.
2429
+ if (isObject(this.doc.brand?.accents) && kind in this.doc.brand.accents) {
2430
+ delete this.doc.brand.accents[kind];
2431
+ if (Object.keys(this.doc.brand.accents).length === 0)
2432
+ delete this.doc.brand.accents;
2433
+ if (Object.keys(this.doc.brand).length === 0)
2434
+ delete this.doc.brand;
2435
+ gone.push("its hue");
2436
+ }
2318
2437
  for (const [name, rule] of Object.entries(this.doc.rules ?? {})) {
2319
- if (rule.over === kind || exprMentions(this.doc, rule.when, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" }) || exprMentions(this.doc, rule.require, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" })) {
2438
+ if (rule.over === kind || rule.covers?.every === kind || exprMentions(this.doc, rule.when, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" }) || exprMentions(this.doc, rule.require, rule.over === "graph" ? NONE : new Set([rule.over]), { t: "kind", from: kind, to: "\u0000" })) {
2320
2439
  delete this.doc.rules[name];
2321
2440
  gone.push(titled("rule", name, rule));
2322
2441
  }
@@ -2342,7 +2461,12 @@ class Editor {
2342
2461
  });
2343
2462
  }
2344
2463
  if (this.doc.policy?.sees) {
2345
- this.doc.policy.sees = this.doc.policy.sees.map((s) => ({ ...s, kinds: s.kinds.filter((k) => k !== kind) })).filter((s) => s.kinds.length > 0);
2464
+ // A sight of other kinds through a relation that went with the kind would show nothing: it goes too.
2465
+ const declared = new Set(Object.values(this.doc.kinds).flatMap((spec) => Object.keys(spec.edges ?? {})));
2466
+ const through = this.doc.policy.sees.filter((s) => typeof s.via === "string" && !declared.has(s.via) && s.kinds.some((k) => k !== kind));
2467
+ if (through.length > 0)
2468
+ gone.push(`${through.length === 1 ? "the sight" : "the sights"} through ${listOf([...new Set(through.map((s) => `"${String(s.via)}"`))])}`);
2469
+ this.doc.policy.sees = this.doc.policy.sees.filter((s) => !through.includes(s)).map((s) => ({ ...s, kinds: s.kinds.filter((k) => k !== kind) })).filter((s) => s.kinds.length > 0);
2346
2470
  if (this.doc.policy.sees.length === 0)
2347
2471
  delete this.doc.policy.sees;
2348
2472
  }
@@ -2397,8 +2521,8 @@ class Editor {
2397
2521
  }
2398
2522
  const f = { ...parsed.data };
2399
2523
  delete f.renamedFrom;
2400
- if (f.type === "enum" && f.default !== undefined && !f.options.includes(f.default))
2401
- return this.fail(i, "default", `"${String(f.default)}" is not one of the options`);
2524
+ if (f.type === "enum" && f.default !== undefined && defaultOutsideOptions(f, f.default) !== undefined)
2525
+ return this.fail(i, "default", `"${String(defaultOutsideOptions(f, f.default))}" is not one of the options`);
2402
2526
  if (f.format && !formatFits(f.format, f.type))
2403
2527
  return this.fail(i, "format", refusedFormat(f.format, f.type));
2404
2528
  spec.fields[field] = f;
@@ -2489,11 +2613,18 @@ class Editor {
2489
2613
  if (e.format && !formatFits(e.format, e.type))
2490
2614
  return this.fail(i, "format", refusedFormat(e.format, e.type));
2491
2615
  const was = f.type;
2616
+ const wasSeveral = f.multiple === true;
2492
2617
  f.type = e.type;
2493
2618
  if (e.type === "enum")
2494
2619
  f.options = e.options ?? f.options;
2495
2620
  else
2496
2621
  delete f.options;
2622
+ // A choice that takes several of its options (FR-233), or one again: only an enum says it, and a retype says it anew.
2623
+ if (e.type === "enum" && e.multiple)
2624
+ f.multiple = true;
2625
+ else
2626
+ delete f.multiple;
2627
+ const several = f.multiple === true;
2497
2628
  // An option's label goes with the option.
2498
2629
  if (f.optionLabels) {
2499
2630
  f.optionLabels = Object.fromEntries(Object.entries(f.optionLabels).filter(([o]) => (f.options ?? []).includes(o)));
@@ -2523,9 +2654,11 @@ class Editor {
2523
2654
  // A list grouped by a choice that is no longer one opens ungrouped, and says so.
2524
2655
  const held = this.listsHold(e.kind);
2525
2656
  if (held.length > 0)
2526
- this.said.push(`${capitalize(listOf(held))} ${held.length === 1 ? "goes" : "go"} back to the list's own, since ${e.field} is no longer ${was === "enum" ? "a choice" : `a ${was}`}.`);
2657
+ this.said.push(`${capitalize(listOf(held))} ${held.length === 1 ? "goes" : "go"} back to the list's own, since ${e.field} is no longer ${was === "enum" ? (several ? "a choice of one" : "a choice") : `a ${was}`}.`);
2527
2658
  if (was === e.type && e.type === "text")
2528
2659
  this.said.push(`${e.kind}'s ${e.field} ${e.format === "markdown" ? "is now written in Markdown, drawn as prose on the record's page" : "is drawn as written, not as Markdown"}; its values are kept.`);
2660
+ else if (was === e.type && e.type === "enum" && wasSeveral !== several)
2661
+ this.said.push(several ? `${e.kind}'s ${e.field} now takes several of its options at once; the one each record held is kept as a list of one.` : `${e.kind}'s ${e.field} takes one of its options again; the first each record held is kept.`);
2529
2662
  else
2530
2663
  this.said.push(`${e.kind}'s ${e.field} changes from ${was} to ${e.type}${said ? `, said as ${said}` : ""}; values that convert are kept.`);
2531
2664
  }
@@ -2986,6 +3119,12 @@ class Editor {
2986
3119
  act.severs = r.to;
2987
3120
  if (Array.isArray(act.replaces))
2988
3121
  act.replaces = act.replaces.map((e) => (e === r.from ? r.to : e));
3122
+ if (act.setsLinked?.[r.from] !== undefined) {
3123
+ const carried = {};
3124
+ for (const [k, v] of Object.entries(act.setsLinked))
3125
+ carried[k === r.from ? r.to : k] = v;
3126
+ act.setsLinked = carried;
3127
+ }
2989
3128
  for (const e of act.effects ?? []) {
2990
3129
  if (e.connect === r.from)
2991
3130
  e.connect = r.to;
@@ -3009,6 +3148,8 @@ class Editor {
3009
3148
  act.sets = renamed;
3010
3149
  else if (t.where === "setsOther")
3011
3150
  act.setsOther = renamed;
3151
+ else if (t.where === "setsLinked")
3152
+ act.setsLinked[t.relation] = renamed;
3012
3153
  else
3013
3154
  act.effects[t.index].set = renamed;
3014
3155
  }
@@ -3072,7 +3213,17 @@ class Editor {
3072
3213
  const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
3073
3214
  if (rule.when !== undefined)
3074
3215
  rule.when = exprAt(rule.when, ctx);
3075
- rule.require = exprAt(rule.require, ctx);
3216
+ if (rule.require !== undefined)
3217
+ rule.require = exprAt(rule.require, ctx);
3218
+ // A coverage rule names its kind and its relations outright: each follows its rename.
3219
+ if (rule.covers) {
3220
+ if (r.t === "kind" && rule.covers.every === r.from)
3221
+ rule.covers.every = r.to;
3222
+ if (r.t === "edge")
3223
+ for (const key of ["through", "via"])
3224
+ if (rule.covers[key] === r.from)
3225
+ rule.covers[key] = r.to;
3226
+ }
3076
3227
  const mentioned = JSON.stringify(rule) !== before;
3077
3228
  if (rule.says !== undefined)
3078
3229
  rule.says = tmplAt(rule.says, ctx, mentioned ? prose : undefined);
@@ -3253,6 +3404,15 @@ class Editor {
3253
3404
  let uses = exprMentions(doc, act.allowedWhen, on, r) || templateMentions(doc, act.refusal, on, r);
3254
3405
  if (r.t === "edge")
3255
3406
  uses ||= act.connects === r.from || act.severs === r.from || (act.effects ?? []).some((e) => e.connect === r.from || e.sever === r.from);
3407
+ // A relation it carried values through carries nothing now: the entry goes; an act that did nothing else goes with it.
3408
+ if (r.t === "edge" && !uses && act.setsLinked?.[r.from] !== undefined) {
3409
+ delete act.setsLinked[r.from];
3410
+ if (Object.keys(act.setsLinked).length === 0)
3411
+ delete act.setsLinked;
3412
+ const stillDoes = act.creates || act.writes || act.sets || act.setsLinked || act.connects || act.severs || act.removes || (act.effects ?? []).length > 0;
3413
+ if (!stillDoes)
3414
+ uses = true;
3415
+ }
3256
3416
  // A relation it only replaced goes from the list; one that replaced nothing else goes with it (FR-115).
3257
3417
  if (r.t === "edge" && !uses && Array.isArray(act.replaces) && act.replaces.includes(r.from)) {
3258
3418
  act.replaces = act.replaces.filter((e) => e !== r.from);
@@ -3283,7 +3443,13 @@ class Editor {
3283
3443
  delete act.sets;
3284
3444
  if (act.setsOther && Object.keys(act.setsOther).length === 0)
3285
3445
  delete act.setsOther;
3286
- 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));
3446
+ // A relation whose carried fields are all gone carries nothing: it goes from `setsLinked`, and an empty `setsLinked` goes with it.
3447
+ for (const [relation, set] of Object.entries(act.setsLinked ?? {}))
3448
+ if (Object.keys(set).length === 0)
3449
+ delete act.setsLinked[relation];
3450
+ if (act.setsLinked && Object.keys(act.setsLinked).length === 0)
3451
+ delete act.setsLinked;
3452
+ const doesSomething = act.creates || act.writes || act.sets || act.setsLinked || 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));
3287
3453
  if (!doesSomething)
3288
3454
  uses = true;
3289
3455
  else if (act.effects) {
@@ -3298,7 +3464,9 @@ class Editor {
3298
3464
  }
3299
3465
  for (const [name, rule] of Object.entries(doc.rules ?? {})) {
3300
3466
  const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
3301
- if (exprMentions(doc, rule.when, ctx, r) || exprMentions(doc, rule.require, ctx, r) || templateMentions(doc, rule.says, ctx, r)) {
3467
+ // A coverage rule goes with the kind it covers or a relation it walks.
3468
+ const walked = rule.covers !== undefined && ((r.t === "kind" && rule.covers.every === r.from) || (r.t === "edge" && (rule.covers.through === r.from || rule.covers.via === r.from)));
3469
+ if (walked || exprMentions(doc, rule.when, ctx, r) || exprMentions(doc, rule.require, ctx, r) || templateMentions(doc, rule.says, ctx, r)) {
3302
3470
  delete doc.rules[name];
3303
3471
  gone.push(titled("rule", name, rule));
3304
3472
  }