@graview/core 0.1.60 → 0.1.61

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 (134) hide show
  1. package/README.md +10 -0
  2. package/dist/arrangement.d.ts +2 -0
  3. package/dist/arrangement.d.ts.map +1 -1
  4. package/dist/arrangement.js +14 -0
  5. package/dist/arrangement.js.map +1 -1
  6. package/dist/capabilities.js +1 -1
  7. package/dist/capabilities.js.map +1 -1
  8. package/dist/catalog.d.ts.map +1 -1
  9. package/dist/catalog.js +2 -1
  10. package/dist/catalog.js.map +1 -1
  11. package/dist/cli/check/context.d.ts +5 -0
  12. package/dist/cli/check/context.d.ts.map +1 -1
  13. package/dist/cli/check/context.js.map +1 -1
  14. package/dist/cli/check/face.d.ts +14 -0
  15. package/dist/cli/check/face.d.ts.map +1 -0
  16. package/dist/cli/check/face.js +161 -0
  17. package/dist/cli/check/face.js.map +1 -0
  18. package/dist/cli/check.d.ts.map +1 -1
  19. package/dist/cli/check.js +2 -3
  20. package/dist/cli/check.js.map +1 -1
  21. package/dist/cli/docs.d.ts.map +1 -1
  22. package/dist/cli/docs.js +5 -3
  23. package/dist/cli/docs.js.map +1 -1
  24. package/dist/cli/document-schema.d.ts.map +1 -1
  25. package/dist/cli/document-schema.js +2 -0
  26. package/dist/cli/document-schema.js.map +1 -1
  27. package/dist/cli/index.d.ts +2 -1
  28. package/dist/cli/index.d.ts.map +1 -1
  29. package/dist/cli/index.js +14 -3
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/types.d.ts.map +1 -1
  32. package/dist/cli/types.js +2 -1
  33. package/dist/cli/types.js.map +1 -1
  34. package/dist/compose.d.ts +6 -0
  35. package/dist/compose.d.ts.map +1 -1
  36. package/dist/compose.js +6 -1
  37. package/dist/compose.js.map +1 -1
  38. package/dist/document/blocks.d.ts.map +1 -1
  39. package/dist/document/blocks.js +2 -1
  40. package/dist/document/blocks.js.map +1 -1
  41. package/dist/document/compile-checked.js +1 -1
  42. package/dist/document/compile-checked.js.map +1 -1
  43. package/dist/document/compile.d.ts.map +1 -1
  44. package/dist/document/compile.js +6 -2
  45. package/dist/document/compile.js.map +1 -1
  46. package/dist/document/compiled.d.ts.map +1 -1
  47. package/dist/document/compiled.js +7 -2
  48. package/dist/document/compiled.js.map +1 -1
  49. package/dist/document/connection-findings.d.ts.map +1 -1
  50. package/dist/document/connection-findings.js +134 -12
  51. package/dist/document/connection-findings.js.map +1 -1
  52. package/dist/document/connection-spec.d.ts +1 -0
  53. package/dist/document/connection-spec.d.ts.map +1 -1
  54. package/dist/document/connection-spec.js +8 -2
  55. package/dist/document/connection-spec.js.map +1 -1
  56. package/dist/document/edit.d.ts +1 -1
  57. package/dist/document/edit.d.ts.map +1 -1
  58. package/dist/document/edit.js +83 -11
  59. package/dist/document/edit.js.map +1 -1
  60. package/dist/document/field-spec.d.ts.map +1 -1
  61. package/dist/document/field-spec.js +5 -0
  62. package/dist/document/field-spec.js.map +1 -1
  63. package/dist/document/findings.d.ts +33 -2
  64. package/dist/document/findings.d.ts.map +1 -1
  65. package/dist/document/findings.js +14 -17
  66. package/dist/document/findings.js.map +1 -1
  67. package/dist/document/migrate.d.ts.map +1 -1
  68. package/dist/document/migrate.js +10 -4
  69. package/dist/document/migrate.js.map +1 -1
  70. package/dist/document/schema.d.ts +5 -0
  71. package/dist/document/schema.d.ts.map +1 -1
  72. package/dist/document/schema.js +8 -0
  73. package/dist/document/schema.js.map +1 -1
  74. package/dist/document.schema.json +44 -2
  75. package/dist/formats.d.ts +21 -13
  76. package/dist/formats.d.ts.map +1 -1
  77. package/dist/formats.js +21 -13
  78. package/dist/formats.js.map +1 -1
  79. package/dist/import/coerce.d.ts.map +1 -1
  80. package/dist/import/coerce.js +16 -4
  81. package/dist/import/coerce.js.map +1 -1
  82. package/dist/kind-list.d.ts +1 -0
  83. package/dist/kind-list.d.ts.map +1 -1
  84. package/dist/kind-list.js +2 -1
  85. package/dist/kind-list.js.map +1 -1
  86. package/dist/mutations/form.d.ts +2 -0
  87. package/dist/mutations/form.d.ts.map +1 -1
  88. package/dist/mutations/form.js +6 -3
  89. package/dist/mutations/form.js.map +1 -1
  90. package/dist/scaffold/agents.d.ts +9 -1
  91. package/dist/scaffold/agents.d.ts.map +1 -1
  92. package/dist/scaffold/agents.js +33 -2
  93. package/dist/scaffold/agents.js.map +1 -1
  94. package/dist/scaffold/project.js +1 -1
  95. package/dist/schema/choice-words.d.ts.map +1 -1
  96. package/dist/schema/choice-words.js +3 -0
  97. package/dist/schema/choice-words.js.map +1 -1
  98. package/dist/starting-document.d.ts +41 -0
  99. package/dist/starting-document.d.ts.map +1 -0
  100. package/dist/starting-document.js +614 -0
  101. package/dist/starting-document.js.map +1 -0
  102. package/dist/store.d.ts +21 -0
  103. package/dist/store.d.ts.map +1 -1
  104. package/dist/store.js +33 -6
  105. package/dist/store.js.map +1 -1
  106. package/dist/sync/connection.d.ts +10 -1
  107. package/dist/sync/connection.d.ts.map +1 -1
  108. package/dist/sync/connection.js +109 -17
  109. package/dist/sync/connection.js.map +1 -1
  110. package/dist/sync/engine.d.ts +13 -4
  111. package/dist/sync/engine.d.ts.map +1 -1
  112. package/dist/sync/engine.js +5 -3
  113. package/dist/sync/engine.js.map +1 -1
  114. package/dist/sync/google-calendar.d.ts.map +1 -1
  115. package/dist/sync/google-calendar.js +4 -2
  116. package/dist/sync/google-calendar.js.map +1 -1
  117. package/dist/sync/notion-types.d.ts +7 -0
  118. package/dist/sync/notion-types.d.ts.map +1 -1
  119. package/dist/sync/notion-types.js +6 -1
  120. package/dist/sync/notion-types.js.map +1 -1
  121. package/dist/sync/recurrence.d.ts +41 -0
  122. package/dist/sync/recurrence.d.ts.map +1 -0
  123. package/dist/sync/recurrence.js +192 -0
  124. package/dist/sync/recurrence.js.map +1 -0
  125. package/dist/traits.d.ts +72 -0
  126. package/dist/traits.d.ts.map +1 -0
  127. package/dist/traits.js +447 -0
  128. package/dist/traits.js.map +1 -0
  129. package/dist/typed.d.ts +4 -0
  130. package/dist/typed.d.ts.map +1 -1
  131. package/dist/typed.js.map +1 -1
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/package.json +1 -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";
@@ -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(),
@@ -269,6 +271,8 @@ function buildShapes() {
269
271
  "add-alias": z.object({ op: z.literal("add-alias"), kind: kindName, field: fieldName.optional(), alias: z.string().min(1).max(40) }).strict(),
270
272
  // All of a kind's or a field's other words at once; null or [] takes them away.
271
273
  "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(),
274
+ // A connection declared whole, by its name: set where there was none, or replaced (FR-229); null takes it away.
275
+ "set-connection": z.object({ op: z.literal("set-connection"), name: kindName, connection: z.union([z.record(z.string(), z.unknown()), z.null()]) }).strict(),
272
276
  };
273
277
  }
274
278
  let shapes;
@@ -276,6 +280,13 @@ const opShapes = () => (shapes ??= buildShapes());
276
280
  const NONE = new Set();
277
281
  const clone = (v) => structuredClone(v);
278
282
  const asArray = (v) => [v ?? []].flat();
283
+ /** 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. */
284
+ function defaultOutsideOptions(f, given) {
285
+ const options = f.options ?? [];
286
+ if (!f.multiple)
287
+ return Array.isArray(given) || !options.includes(String(given)) ? given : undefined;
288
+ return (Array.isArray(given) ? given : [given]).find((one) => !options.includes(String(one)));
289
+ }
279
290
  const kebab = (camel) => camel.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
280
291
  const words = (name) => kebab(name).replace(/-/g, " ");
281
292
  /** 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. */
@@ -860,8 +871,8 @@ class Editor {
860
871
  this.said.push(`${e.kind}'s ${e.field} has no default now.`);
861
872
  }
862
873
  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(", ")})`);
874
+ if (f.type === "enum" && defaultOutsideOptions(f, e.default) !== undefined)
875
+ return this.fail(i, "default", `"${String(defaultOutsideOptions(f, e.default))}" is not one of ${e.field}'s options (${(f.options ?? []).join(", ")})`);
865
876
  const outside = outsideRange(e.default, f, e.field);
866
877
  if (outside)
867
878
  return this.fail(i, "default", outside);
@@ -1030,7 +1041,36 @@ class Editor {
1030
1041
  return this.setAliases(i, e.kind, e.field, [...this.aliasesOf(e.kind, e.field), e.alias], e.alias);
1031
1042
  case "set-aliases":
1032
1043
  return this.setAliases(i, e.kind, e.field, e.aliases ?? []);
1044
+ case "set-connection":
1045
+ return this.setConnection(i, e.name, e.connection);
1046
+ }
1047
+ }
1048
+ // ── a connection, declared whole (FR-229) ──
1049
+ /**
1050
+ * A CONNECTION SET BY NAME: its shape is judged here, as the document's
1051
+ * is; what it says of the kind and the fields it maps, and the acts an
1052
+ * inbound change lands through, the checker judges of the document it
1053
+ * leaves (`connection-findings.ts`). Null takes the connection away.
1054
+ */
1055
+ setConnection(i, name, connection) {
1056
+ if (connection === null) {
1057
+ if (!this.doc.connections?.[name])
1058
+ 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(", ")}` : ""}`);
1059
+ delete this.doc.connections[name];
1060
+ if (Object.keys(this.doc.connections).length === 0)
1061
+ delete this.doc.connections;
1062
+ this.said.push(`The connection ${name} is gone: nothing keeps its records in step now.`);
1063
+ return;
1033
1064
  }
1065
+ const parsed = ConnectionSpec.safeParse(connection);
1066
+ if (!parsed.success) {
1067
+ this.refuse(i, parsed.error.issues.map((issue) => ({ ...issue, path: ["connection", ...issue.path] })), "a connection");
1068
+ return;
1069
+ }
1070
+ const was = this.doc.connections?.[name] !== undefined;
1071
+ (this.doc.connections ??= {})[name] = parsed.data;
1072
+ const system = parsed.data.system === "notion" ? "a Notion database" : "a Google calendar";
1073
+ 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"}.`);
1034
1074
  }
1035
1075
  // ── other words for a kind or a field (Roadmap f-learned-suggestions) ──
1036
1076
  aliasesOf(kind, field) {
@@ -1361,9 +1401,10 @@ class Editor {
1361
1401
  return (this.fail(i, `bindings.${kind}.column`, `the board does not say which field of ${kind} its columns are`, example), false);
1362
1402
  if (!(field in fields))
1363
1403
  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);
1404
+ // A choice that takes several of its options (FR-233) has no columns: a record would stand in several.
1405
+ if (fields[field]["type"] !== "enum" || fields[field]["multiple"] === true) {
1406
+ const choices = Object.keys(fields).filter((one) => fields[one]["type"] === "enum" && fields[one]["multiple"] !== true);
1407
+ 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
1408
  }
1368
1409
  const others = Object.keys(roles).filter((role) => role !== "column");
1369
1410
  if (others.length > 0)
@@ -2237,6 +2278,12 @@ class Editor {
2237
2278
  spec.plural = e.plural;
2238
2279
  else if (spec.plural && (spec.plural === `${words(from)}s` || spec.plural === `${from}s`))
2239
2280
  spec.plural = `${words(to)}s`;
2281
+ // Its hue follows it.
2282
+ if (isObject(this.doc.brand?.accents) && from in this.doc.brand.accents && !(to in this.doc.brand.accents)) {
2283
+ this.doc.brand.accents[to] = this.doc.brand.accents[from];
2284
+ delete this.doc.brand.accents[from];
2285
+ touched.push("its hue");
2286
+ }
2240
2287
  // Its list's filters, order and grouping follow it.
2241
2288
  const lists = this.doc.pages?.lists;
2242
2289
  if (lists?.[from] !== undefined && lists[to] === undefined) {
@@ -2288,6 +2335,7 @@ class Editor {
2288
2335
  gone.push(what);
2289
2336
  }
2290
2337
  const removedPlural = this.doc.kinds[kind].plural ?? `${words(kind)}s`;
2338
+ const people = this.doc.kinds[kind].people === true;
2291
2339
  // Its own relations' connect and disconnect go with it.
2292
2340
  const ownRelationActs = relationActsOf(documentRelations({ [kind]: this.doc.kinds[kind] }));
2293
2341
  delete this.doc.kinds[kind];
@@ -2306,15 +2354,25 @@ class Editor {
2306
2354
  }
2307
2355
  }
2308
2356
  }
2357
+ // With the people kind gone, an act on the acting person's own record (`$me`) has no record to reach: it goes too.
2309
2358
  for (const [name, act] of Object.entries(this.doc.acts ?? {})) {
2310
2359
  const on = asArray(act.on);
2311
- if (act.creates === kind || (act.effects ?? []).some((x) => x.create === kind) || (on.includes(kind) && on.length === 1)) {
2360
+ if (act.creates === kind || (act.effects ?? []).some((x) => x.create === kind) || (on.includes(kind) && on.length === 1) || (people && JSON.stringify(act).includes('"$me"'))) {
2312
2361
  gone.push(titled("act", name, act));
2313
2362
  this.removeActQuietly(name);
2314
2363
  }
2315
2364
  else if (on.includes(kind))
2316
2365
  act.on = on.filter((k) => k !== kind);
2317
2366
  }
2367
+ // Its hue goes with it: an accent for a kind nobody declares is refused by the checker.
2368
+ if (isObject(this.doc.brand?.accents) && kind in this.doc.brand.accents) {
2369
+ delete this.doc.brand.accents[kind];
2370
+ if (Object.keys(this.doc.brand.accents).length === 0)
2371
+ delete this.doc.brand.accents;
2372
+ if (Object.keys(this.doc.brand).length === 0)
2373
+ delete this.doc.brand;
2374
+ gone.push("its hue");
2375
+ }
2318
2376
  for (const [name, rule] of Object.entries(this.doc.rules ?? {})) {
2319
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" })) {
2320
2378
  delete this.doc.rules[name];
@@ -2342,7 +2400,12 @@ class Editor {
2342
2400
  });
2343
2401
  }
2344
2402
  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);
2403
+ // A sight of other kinds through a relation that went with the kind would show nothing: it goes too.
2404
+ const declared = new Set(Object.values(this.doc.kinds).flatMap((spec) => Object.keys(spec.edges ?? {})));
2405
+ const through = this.doc.policy.sees.filter((s) => typeof s.via === "string" && !declared.has(s.via) && s.kinds.some((k) => k !== kind));
2406
+ if (through.length > 0)
2407
+ gone.push(`${through.length === 1 ? "the sight" : "the sights"} through ${listOf([...new Set(through.map((s) => `"${String(s.via)}"`))])}`);
2408
+ 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
2409
  if (this.doc.policy.sees.length === 0)
2347
2410
  delete this.doc.policy.sees;
2348
2411
  }
@@ -2397,8 +2460,8 @@ class Editor {
2397
2460
  }
2398
2461
  const f = { ...parsed.data };
2399
2462
  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`);
2463
+ if (f.type === "enum" && f.default !== undefined && defaultOutsideOptions(f, f.default) !== undefined)
2464
+ return this.fail(i, "default", `"${String(defaultOutsideOptions(f, f.default))}" is not one of the options`);
2402
2465
  if (f.format && !formatFits(f.format, f.type))
2403
2466
  return this.fail(i, "format", refusedFormat(f.format, f.type));
2404
2467
  spec.fields[field] = f;
@@ -2489,11 +2552,18 @@ class Editor {
2489
2552
  if (e.format && !formatFits(e.format, e.type))
2490
2553
  return this.fail(i, "format", refusedFormat(e.format, e.type));
2491
2554
  const was = f.type;
2555
+ const wasSeveral = f.multiple === true;
2492
2556
  f.type = e.type;
2493
2557
  if (e.type === "enum")
2494
2558
  f.options = e.options ?? f.options;
2495
2559
  else
2496
2560
  delete f.options;
2561
+ // A choice that takes several of its options (FR-233), or one again: only an enum says it, and a retype says it anew.
2562
+ if (e.type === "enum" && e.multiple)
2563
+ f.multiple = true;
2564
+ else
2565
+ delete f.multiple;
2566
+ const several = f.multiple === true;
2497
2567
  // An option's label goes with the option.
2498
2568
  if (f.optionLabels) {
2499
2569
  f.optionLabels = Object.fromEntries(Object.entries(f.optionLabels).filter(([o]) => (f.options ?? []).includes(o)));
@@ -2523,9 +2593,11 @@ class Editor {
2523
2593
  // A list grouped by a choice that is no longer one opens ungrouped, and says so.
2524
2594
  const held = this.listsHold(e.kind);
2525
2595
  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}`}.`);
2596
+ 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
2597
  if (was === e.type && e.type === "text")
2528
2598
  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.`);
2599
+ else if (was === e.type && e.type === "enum" && wasSeveral !== several)
2600
+ 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
2601
  else
2530
2602
  this.said.push(`${e.kind}'s ${e.field} changes from ${was} to ${e.type}${said ? `, said as ${said}` : ""}; values that convert are kept.`);
2531
2603
  }