@graview/core 0.1.61 → 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 (235) hide show
  1. package/README.md +13 -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/capabilities.js +1 -1
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/cli/check/hidden-reads.d.ts.map +1 -1
  11. package/dist/cli/check/hidden-reads.js +26 -0
  12. package/dist/cli/check/hidden-reads.js.map +1 -1
  13. package/dist/cli/check/index.d.ts +1 -0
  14. package/dist/cli/check/index.d.ts.map +1 -1
  15. package/dist/cli/check/index.js +1 -0
  16. package/dist/cli/check/index.js.map +1 -1
  17. package/dist/cli/check/tidying.d.ts +14 -0
  18. package/dist/cli/check/tidying.d.ts.map +1 -0
  19. package/dist/cli/check/tidying.js +24 -0
  20. package/dist/cli/check/tidying.js.map +1 -0
  21. package/dist/cli/check.d.ts.map +1 -1
  22. package/dist/cli/check.js +2 -1
  23. package/dist/cli/check.js.map +1 -1
  24. package/dist/cli/describe.d.ts.map +1 -1
  25. package/dist/cli/describe.js +14 -1
  26. package/dist/cli/describe.js.map +1 -1
  27. package/dist/cli/document-schema.d.ts.map +1 -1
  28. package/dist/cli/document-schema.js +6 -2
  29. package/dist/cli/document-schema.js.map +1 -1
  30. package/dist/cli/index.js +1 -1
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/compose.d.ts +11 -3
  33. package/dist/compose.d.ts.map +1 -1
  34. package/dist/compose.js +12 -2
  35. package/dist/compose.js.map +1 -1
  36. package/dist/css/sanitize.d.ts +125 -0
  37. package/dist/css/sanitize.d.ts.map +1 -0
  38. package/dist/css/sanitize.js +989 -0
  39. package/dist/css/sanitize.js.map +1 -0
  40. package/dist/css/tables.d.ts +55 -0
  41. package/dist/css/tables.d.ts.map +1 -0
  42. package/dist/css/tables.js +137 -0
  43. package/dist/css/tables.js.map +1 -0
  44. package/dist/css.d.ts +20 -0
  45. package/dist/css.d.ts.map +1 -0
  46. package/dist/css.js +19 -0
  47. package/dist/css.js.map +1 -0
  48. package/dist/days.d.ts +15 -0
  49. package/dist/days.d.ts.map +1 -1
  50. package/dist/days.js +21 -0
  51. package/dist/days.js.map +1 -1
  52. package/dist/describe.d.ts +1 -1
  53. package/dist/describe.d.ts.map +1 -1
  54. package/dist/document/act-findings.d.ts.map +1 -1
  55. package/dist/document/act-findings.js +80 -1
  56. package/dist/document/act-findings.js.map +1 -1
  57. package/dist/document/brand-check.d.ts +15 -1
  58. package/dist/document/brand-check.d.ts.map +1 -1
  59. package/dist/document/brand-check.js +80 -0
  60. package/dist/document/brand-check.js.map +1 -1
  61. package/dist/document/brand.d.ts.map +1 -1
  62. package/dist/document/brand.js +2 -0
  63. package/dist/document/brand.js.map +1 -1
  64. package/dist/document/compile-checked.d.ts.map +1 -1
  65. package/dist/document/compile-checked.js +3 -2
  66. package/dist/document/compile-checked.js.map +1 -1
  67. package/dist/document/compile.d.ts.map +1 -1
  68. package/dist/document/compile.js +48 -7
  69. package/dist/document/compile.js.map +1 -1
  70. package/dist/document/compiled.d.ts +18 -3
  71. package/dist/document/compiled.d.ts.map +1 -1
  72. package/dist/document/compiled.js +84 -8
  73. package/dist/document/compiled.js.map +1 -1
  74. package/dist/document/connection-findings.js +85 -7
  75. package/dist/document/connection-findings.js.map +1 -1
  76. package/dist/document/connection-spec.d.ts +6 -0
  77. package/dist/document/connection-spec.d.ts.map +1 -1
  78. package/dist/document/connection-spec.js +19 -1
  79. package/dist/document/connection-spec.js.map +1 -1
  80. package/dist/document/covers.d.ts +48 -0
  81. package/dist/document/covers.d.ts.map +1 -0
  82. package/dist/document/covers.js +90 -0
  83. package/dist/document/covers.js.map +1 -0
  84. package/dist/document/describe-place.d.ts +3 -0
  85. package/dist/document/describe-place.d.ts.map +1 -1
  86. package/dist/document/describe-place.js +11 -0
  87. package/dist/document/describe-place.js.map +1 -1
  88. package/dist/document/described.d.ts +28 -0
  89. package/dist/document/described.d.ts.map +1 -1
  90. package/dist/document/diff.js +10 -1
  91. package/dist/document/diff.js.map +1 -1
  92. package/dist/document/edit.d.ts.map +1 -1
  93. package/dist/document/edit.js +103 -7
  94. package/dist/document/edit.js.map +1 -1
  95. package/dist/document/expr/evaluate.d.ts +10 -1
  96. package/dist/document/expr/evaluate.d.ts.map +1 -1
  97. package/dist/document/expr/evaluate.js +110 -2
  98. package/dist/document/expr/evaluate.js.map +1 -1
  99. package/dist/document/expr/index.d.ts +1 -0
  100. package/dist/document/expr/index.d.ts.map +1 -1
  101. package/dist/document/expr/index.js +2 -0
  102. package/dist/document/expr/index.js.map +1 -1
  103. package/dist/document/expr/typing.d.ts +24 -0
  104. package/dist/document/expr/typing.d.ts.map +1 -0
  105. package/dist/document/expr/typing.js +219 -0
  106. package/dist/document/expr/typing.js.map +1 -0
  107. package/dist/document/expr/when.d.ts +52 -0
  108. package/dist/document/expr/when.d.ts.map +1 -0
  109. package/dist/document/expr/when.js +215 -0
  110. package/dist/document/expr/when.js.map +1 -0
  111. package/dist/document/expr-type-findings.d.ts +5 -0
  112. package/dist/document/expr-type-findings.d.ts.map +1 -0
  113. package/dist/document/expr-type-findings.js +90 -0
  114. package/dist/document/expr-type-findings.js.map +1 -0
  115. package/dist/document/index.d.ts +6 -3
  116. package/dist/document/index.d.ts.map +1 -1
  117. package/dist/document/index.js +5 -2
  118. package/dist/document/index.js.map +1 -1
  119. package/dist/document/linked-findings.js +1 -1
  120. package/dist/document/linked-findings.js.map +1 -1
  121. package/dist/document/rule-line.d.ts +7 -3
  122. package/dist/document/rule-line.d.ts.map +1 -1
  123. package/dist/document/rule-line.js +17 -2
  124. package/dist/document/rule-line.js.map +1 -1
  125. package/dist/document/rule-reads.d.ts.map +1 -1
  126. package/dist/document/rule-reads.js +3 -1
  127. package/dist/document/rule-reads.js.map +1 -1
  128. package/dist/document/rules.d.ts +30 -0
  129. package/dist/document/rules.d.ts.map +1 -1
  130. package/dist/document/rules.js +74 -0
  131. package/dist/document/rules.js.map +1 -1
  132. package/dist/document/schema.d.ts +75 -2
  133. package/dist/document/schema.d.ts.map +1 -1
  134. package/dist/document/schema.js +82 -4
  135. package/dist/document/schema.js.map +1 -1
  136. package/dist/document/to-document.d.ts.map +1 -1
  137. package/dist/document/to-document.js +3 -1
  138. package/dist/document/to-document.js.map +1 -1
  139. package/dist/document.schema.json +262 -5
  140. package/dist/index.d.ts +6 -5
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +5 -3
  143. package/dist/index.js.map +1 -1
  144. package/dist/invariants/types.d.ts +7 -2
  145. package/dist/invariants/types.d.ts.map +1 -1
  146. package/dist/lines.d.ts.map +1 -1
  147. package/dist/lines.js +6 -2
  148. package/dist/lines.js.map +1 -1
  149. package/dist/mutations/types.d.ts +9 -0
  150. package/dist/mutations/types.d.ts.map +1 -1
  151. package/dist/mutations/vote-suggestion.d.ts +67 -0
  152. package/dist/mutations/vote-suggestion.d.ts.map +1 -0
  153. package/dist/mutations/vote-suggestion.js +36 -0
  154. package/dist/mutations/vote-suggestion.js.map +1 -0
  155. package/dist/navigate.d.ts +7 -3
  156. package/dist/navigate.d.ts.map +1 -1
  157. package/dist/navigate.js +7 -3
  158. package/dist/navigate.js.map +1 -1
  159. package/dist/ops/lineage.d.ts +11 -1
  160. package/dist/ops/lineage.d.ts.map +1 -1
  161. package/dist/ops/lineage.js +6 -2
  162. package/dist/ops/lineage.js.map +1 -1
  163. package/dist/ops/suggestions.d.ts +33 -1
  164. package/dist/ops/suggestions.d.ts.map +1 -1
  165. package/dist/ops/suggestions.js +44 -2
  166. package/dist/ops/suggestions.js.map +1 -1
  167. package/dist/ops/types.d.ts +13 -0
  168. package/dist/ops/types.d.ts.map +1 -1
  169. package/dist/ops/types.js.map +1 -1
  170. package/dist/place-names.d.ts +42 -0
  171. package/dist/place-names.d.ts.map +1 -0
  172. package/dist/place-names.js +43 -0
  173. package/dist/place-names.js.map +1 -0
  174. package/dist/places.d.ts +2 -0
  175. package/dist/places.d.ts.map +1 -1
  176. package/dist/places.js +2 -0
  177. package/dist/places.js.map +1 -1
  178. package/dist/starting-document.d.ts +32 -0
  179. package/dist/starting-document.d.ts.map +1 -1
  180. package/dist/starting-document.js +35 -4
  181. package/dist/starting-document.js.map +1 -1
  182. package/dist/store.d.ts +29 -0
  183. package/dist/store.d.ts.map +1 -1
  184. package/dist/store.js +134 -21
  185. package/dist/store.js.map +1 -1
  186. package/dist/sync/connection.d.ts +11 -1
  187. package/dist/sync/connection.d.ts.map +1 -1
  188. package/dist/sync/connection.js +94 -22
  189. package/dist/sync/connection.js.map +1 -1
  190. package/dist/sync/engine.d.ts.map +1 -1
  191. package/dist/sync/engine.js +5 -3
  192. package/dist/sync/engine.js.map +1 -1
  193. package/dist/sync/google-calendar.d.ts +8 -0
  194. package/dist/sync/google-calendar.d.ts.map +1 -1
  195. package/dist/sync/google-calendar.js +12 -2
  196. package/dist/sync/google-calendar.js.map +1 -1
  197. package/dist/sync/guests.d.ts +44 -0
  198. package/dist/sync/guests.d.ts.map +1 -0
  199. package/dist/sync/guests.js +57 -0
  200. package/dist/sync/guests.js.map +1 -0
  201. package/dist/sync/types.d.ts +9 -0
  202. package/dist/sync/types.d.ts.map +1 -1
  203. package/dist/sync/types.js.map +1 -1
  204. package/dist/testing.js +1 -1
  205. package/dist/testing.js.map +1 -1
  206. package/dist/theme/contrast.d.ts +2 -0
  207. package/dist/theme/contrast.d.ts.map +1 -1
  208. package/dist/theme/contrast.js +4 -0
  209. package/dist/theme/contrast.js.map +1 -1
  210. package/dist/theme/ground.d.ts +1 -2
  211. package/dist/theme/ground.d.ts.map +1 -1
  212. package/dist/theme/ground.js +2 -5
  213. package/dist/theme/ground.js.map +1 -1
  214. package/dist/theme/marks.d.ts +14 -2
  215. package/dist/theme/marks.d.ts.map +1 -1
  216. package/dist/theme/marks.js +20 -3
  217. package/dist/theme/marks.js.map +1 -1
  218. package/dist/theme/types.d.ts +22 -0
  219. package/dist/theme/types.d.ts.map +1 -1
  220. package/dist/theme/types.js.map +1 -1
  221. package/dist/tidying.d.ts +30 -4
  222. package/dist/tidying.d.ts.map +1 -1
  223. package/dist/tidying.js +110 -5
  224. package/dist/tidying.js.map +1 -1
  225. package/dist/trait-vocabulary.d.ts +17 -0
  226. package/dist/trait-vocabulary.d.ts.map +1 -0
  227. package/dist/trait-vocabulary.js +16 -0
  228. package/dist/trait-vocabulary.js.map +1 -0
  229. package/dist/traits.d.ts +14 -7
  230. package/dist/traits.d.ts.map +1 -1
  231. package/dist/traits.js +33 -12
  232. package/dist/traits.js.map +1 -1
  233. package/dist/version.d.ts +1 -1
  234. package/dist/version.js +1 -1
  235. package/package.json +11 -1
@@ -29,7 +29,7 @@ import { documentListFields, kindListFindings, kindListWords, listGroup, listSor
29
29
  import { computedOf, validateComputed } from "./computed.js";
30
30
  import { parseTemplate, templateBraces, TemplateError } from "./template-parse.js";
31
31
  import { validateViews, VIEW_SLOTS } from "./views.js";
32
- import { brandFindings, emojiIconSentence, groundWords, subtitleSentence } from "./brand-check.js";
32
+ import { brandFindings, emojiIconSentence, groundWords, splashSentence, subtitleSentence, viewsCssSentence, viewsMotionSentence } from "./brand-check.js";
33
33
  import { darkLogoSentence, logoParts } from "./logo.js";
34
34
  import { emojiOf } from "../theme/marks.js";
35
35
  import { canonicalize } from "./canonical.js";
@@ -167,6 +167,15 @@ function buildShapes() {
167
167
  scheme: z.union([z.enum(["light", "dark", "auto"]), z.null()]).optional(),
168
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.
169
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(),
170
179
  })
171
180
  .strict(),
172
181
  "set-name": z.object({ op: z.literal("set-name"), name: z.string().min(1).max(80) }).strict(),
@@ -550,6 +559,11 @@ function setTargets(act, doc) {
550
559
  const end = doc && relation ? farEnd(doc.kinds, relation, asArray(act.on)) : undefined;
551
560
  out.push({ set: act.setsOther, kinds: end && end.kinds !== "*" ? new Set(end.kinds) : "*", where: "setsOther" });
552
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
+ }
553
567
  (act.effects ?? []).forEach((e, index) => {
554
568
  if (e.create)
555
569
  out.push({ set: e.set ?? {}, kinds: new Set([e.create]), where: "create", index });
@@ -602,10 +616,18 @@ class Editor {
602
616
  const named = Object.keys(e).filter((key) => key !== "op" && e[key] !== undefined);
603
617
  if (named.length === 0)
604
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"}');
605
- 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);
606
620
  if (emptyPart)
607
621
  return this.fail(i, emptyPart, `${emptyPart} names no part to set; name one, or give null to clear all of it`);
608
- 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 } : {}) });
609
631
  if (judged.length > 0)
610
632
  return void this.findings.push(...judged);
611
633
  if (e.accent === null) {
@@ -779,6 +801,45 @@ class Editor {
779
801
  delete brand["subtitle"];
780
802
  this.said.push(subtitleSentence(e.subtitle !== false));
781
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
+ }
782
843
  if (Object.keys(brand).length === 0)
783
844
  delete this.doc.brand;
784
845
  else
@@ -2374,7 +2435,7 @@ class Editor {
2374
2435
  gone.push("its hue");
2375
2436
  }
2376
2437
  for (const [name, rule] of Object.entries(this.doc.rules ?? {})) {
2377
- 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" })) {
2378
2439
  delete this.doc.rules[name];
2379
2440
  gone.push(titled("rule", name, rule));
2380
2441
  }
@@ -3058,6 +3119,12 @@ class Editor {
3058
3119
  act.severs = r.to;
3059
3120
  if (Array.isArray(act.replaces))
3060
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
+ }
3061
3128
  for (const e of act.effects ?? []) {
3062
3129
  if (e.connect === r.from)
3063
3130
  e.connect = r.to;
@@ -3081,6 +3148,8 @@ class Editor {
3081
3148
  act.sets = renamed;
3082
3149
  else if (t.where === "setsOther")
3083
3150
  act.setsOther = renamed;
3151
+ else if (t.where === "setsLinked")
3152
+ act.setsLinked[t.relation] = renamed;
3084
3153
  else
3085
3154
  act.effects[t.index].set = renamed;
3086
3155
  }
@@ -3144,7 +3213,17 @@ class Editor {
3144
3213
  const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
3145
3214
  if (rule.when !== undefined)
3146
3215
  rule.when = exprAt(rule.when, ctx);
3147
- 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
+ }
3148
3227
  const mentioned = JSON.stringify(rule) !== before;
3149
3228
  if (rule.says !== undefined)
3150
3229
  rule.says = tmplAt(rule.says, ctx, mentioned ? prose : undefined);
@@ -3325,6 +3404,15 @@ class Editor {
3325
3404
  let uses = exprMentions(doc, act.allowedWhen, on, r) || templateMentions(doc, act.refusal, on, r);
3326
3405
  if (r.t === "edge")
3327
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
+ }
3328
3416
  // A relation it only replaced goes from the list; one that replaced nothing else goes with it (FR-115).
3329
3417
  if (r.t === "edge" && !uses && Array.isArray(act.replaces) && act.replaces.includes(r.from)) {
3330
3418
  act.replaces = act.replaces.filter((e) => e !== r.from);
@@ -3355,7 +3443,13 @@ class Editor {
3355
3443
  delete act.sets;
3356
3444
  if (act.setsOther && Object.keys(act.setsOther).length === 0)
3357
3445
  delete act.setsOther;
3358
- 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));
3359
3453
  if (!doesSomething)
3360
3454
  uses = true;
3361
3455
  else if (act.effects) {
@@ -3370,7 +3464,9 @@ class Editor {
3370
3464
  }
3371
3465
  for (const [name, rule] of Object.entries(doc.rules ?? {})) {
3372
3466
  const ctx = rule.over === "graph" ? NONE : new Set([rule.over]);
3373
- 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)) {
3374
3470
  delete doc.rules[name];
3375
3471
  gone.push(titled("rule", name, rule));
3376
3472
  }