@graview/core 0.1.53 → 0.1.55

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 (156) hide show
  1. package/dist/alias-findings.d.ts +40 -0
  2. package/dist/alias-findings.d.ts.map +1 -0
  3. package/dist/alias-findings.js +95 -0
  4. package/dist/alias-findings.js.map +1 -0
  5. package/dist/app.d.ts +13 -0
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js.map +1 -1
  8. package/dist/archived-seen.d.ts +51 -15
  9. package/dist/archived-seen.d.ts.map +1 -1
  10. package/dist/archived-seen.js +123 -28
  11. package/dist/archived-seen.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 +32 -0
  15. package/dist/catalog.d.ts.map +1 -1
  16. package/dist/catalog.js +17 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/check.d.ts +2 -0
  19. package/dist/check.d.ts.map +1 -1
  20. package/dist/check.js +2 -0
  21. package/dist/check.js.map +1 -1
  22. package/dist/cli/check/aliases.d.ts +11 -0
  23. package/dist/cli/check/aliases.d.ts.map +1 -0
  24. package/dist/cli/check/aliases.js +17 -0
  25. package/dist/cli/check/aliases.js.map +1 -0
  26. package/dist/cli/check/context.d.ts +2 -0
  27. package/dist/cli/check/context.d.ts.map +1 -1
  28. package/dist/cli/check/context.js.map +1 -1
  29. package/dist/cli/check/index.d.ts +2 -0
  30. package/dist/cli/check/index.d.ts.map +1 -1
  31. package/dist/cli/check/index.js +2 -0
  32. package/dist/cli/check/index.js.map +1 -1
  33. package/dist/cli/check/jobs.d.ts +12 -0
  34. package/dist/cli/check/jobs.d.ts.map +1 -0
  35. package/dist/cli/check/jobs.js +25 -0
  36. package/dist/cli/check/jobs.js.map +1 -0
  37. package/dist/cli/check.d.ts +13 -0
  38. package/dist/cli/check.d.ts.map +1 -1
  39. package/dist/cli/check.js +25 -2
  40. package/dist/cli/check.js.map +1 -1
  41. package/dist/cli/describe.d.ts.map +1 -1
  42. package/dist/cli/describe.js +41 -0
  43. package/dist/cli/describe.js.map +1 -1
  44. package/dist/cli/document-schema.d.ts.map +1 -1
  45. package/dist/cli/document-schema.js +7 -0
  46. package/dist/cli/document-schema.js.map +1 -1
  47. package/dist/compose.d.ts +99 -0
  48. package/dist/compose.d.ts.map +1 -0
  49. package/dist/compose.js +245 -0
  50. package/dist/compose.js.map +1 -0
  51. package/dist/document/brand-check.d.ts +3 -0
  52. package/dist/document/brand-check.d.ts.map +1 -1
  53. package/dist/document/brand-check.js +21 -2
  54. package/dist/document/brand-check.js.map +1 -1
  55. package/dist/document/brand.d.ts.map +1 -1
  56. package/dist/document/brand.js +10 -2
  57. package/dist/document/brand.js.map +1 -1
  58. package/dist/document/compile-checked.d.ts.map +1 -1
  59. package/dist/document/compile-checked.js +13 -7
  60. package/dist/document/compile-checked.js.map +1 -1
  61. package/dist/document/compiled.d.ts.map +1 -1
  62. package/dist/document/compiled.js +6 -0
  63. package/dist/document/compiled.js.map +1 -1
  64. package/dist/document/design-findings.d.ts +32 -0
  65. package/dist/document/design-findings.d.ts.map +1 -0
  66. package/dist/document/design-findings.js +593 -0
  67. package/dist/document/design-findings.js.map +1 -0
  68. package/dist/document/diff.js +1 -1
  69. package/dist/document/diff.js.map +1 -1
  70. package/dist/document/edit.d.ts +1 -1
  71. package/dist/document/edit.d.ts.map +1 -1
  72. package/dist/document/edit.js +255 -4
  73. package/dist/document/edit.js.map +1 -1
  74. package/dist/document/findings.d.ts +8 -2
  75. package/dist/document/findings.d.ts.map +1 -1
  76. package/dist/document/findings.js +4 -3
  77. package/dist/document/findings.js.map +1 -1
  78. package/dist/document/graview-template.d.ts +3 -3
  79. package/dist/document/purpose-spec.d.ts +37 -0
  80. package/dist/document/purpose-spec.d.ts.map +1 -0
  81. package/dist/document/purpose-spec.js +41 -0
  82. package/dist/document/purpose-spec.js.map +1 -0
  83. package/dist/document/schema.d.ts +32 -11
  84. package/dist/document/schema.d.ts.map +1 -1
  85. package/dist/document/schema.js +30 -0
  86. package/dist/document/schema.js.map +1 -1
  87. package/dist/document/to-document.d.ts.map +1 -1
  88. package/dist/document/to-document.js +5 -1
  89. package/dist/document/to-document.js.map +1 -1
  90. package/dist/document.schema.json +142 -2
  91. package/dist/friction-record.d.ts +52 -0
  92. package/dist/friction-record.d.ts.map +1 -0
  93. package/dist/friction-record.js +36 -0
  94. package/dist/friction-record.js.map +1 -0
  95. package/dist/friction.d.ts +209 -0
  96. package/dist/friction.d.ts.map +1 -0
  97. package/dist/friction.js +302 -0
  98. package/dist/friction.js.map +1 -0
  99. package/dist/graph/graph.d.ts.map +1 -1
  100. package/dist/graph/graph.js +20 -0
  101. package/dist/graph/graph.js.map +1 -1
  102. package/dist/history.d.ts +32 -0
  103. package/dist/history.d.ts.map +1 -1
  104. package/dist/history.js.map +1 -1
  105. package/dist/index.d.ts +8 -1
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +5 -1
  108. package/dist/index.js.map +1 -1
  109. package/dist/job-findings.d.ts +39 -0
  110. package/dist/job-findings.d.ts.map +1 -0
  111. package/dist/job-findings.js +171 -0
  112. package/dist/job-findings.js.map +1 -0
  113. package/dist/job-shape.d.ts +57 -0
  114. package/dist/job-shape.d.ts.map +1 -0
  115. package/dist/job-shape.js +5 -0
  116. package/dist/job-shape.js.map +1 -0
  117. package/dist/jobs.d.ts +62 -0
  118. package/dist/jobs.d.ts.map +1 -0
  119. package/dist/jobs.js +112 -0
  120. package/dist/jobs.js.map +1 -0
  121. package/dist/learn.d.ts +151 -0
  122. package/dist/learn.d.ts.map +1 -0
  123. package/dist/learn.js +520 -0
  124. package/dist/learn.js.map +1 -0
  125. package/dist/past.d.ts +37 -2
  126. package/dist/past.d.ts.map +1 -1
  127. package/dist/past.js +73 -13
  128. package/dist/past.js.map +1 -1
  129. package/dist/schema/types.d.ts +9 -0
  130. package/dist/schema/types.d.ts.map +1 -1
  131. package/dist/search.d.ts.map +1 -1
  132. package/dist/search.js +2 -0
  133. package/dist/search.js.map +1 -1
  134. package/dist/seat.d.ts +3 -2
  135. package/dist/seat.d.ts.map +1 -1
  136. package/dist/seat.js +3 -2
  137. package/dist/seat.js.map +1 -1
  138. package/dist/subscriptions.d.ts +5 -0
  139. package/dist/subscriptions.d.ts.map +1 -1
  140. package/dist/subscriptions.js +46 -1
  141. package/dist/subscriptions.js.map +1 -1
  142. package/dist/theme/accent.d.ts +8 -6
  143. package/dist/theme/accent.d.ts.map +1 -1
  144. package/dist/theme/accent.js +29 -23
  145. package/dist/theme/accent.js.map +1 -1
  146. package/dist/theme/ground-check.d.ts +27 -0
  147. package/dist/theme/ground-check.d.ts.map +1 -0
  148. package/dist/theme/ground-check.js +81 -0
  149. package/dist/theme/ground-check.js.map +1 -0
  150. package/dist/theme/ground.d.ts +42 -0
  151. package/dist/theme/ground.d.ts.map +1 -0
  152. package/dist/theme/ground.js +101 -0
  153. package/dist/theme/ground.js.map +1 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/package.json +20 -2
@@ -14,9 +14,13 @@ const refusedFormat = (format, type) => {
14
14
  return `${sentence}; ${fix}`;
15
15
  };
16
16
  import { farEnd } from "./far-end.js";
17
+ import { jobFindings, jobWorldOfDocument } from "../job-findings.js";
18
+ import { JobSpec } from "./purpose-spec.js";
19
+ import { aliasWord, documentAliasFindings } from "../alias-findings.js";
20
+ import { withArticle } from "../schema/define-node.js";
17
21
  import { linkedRelationFindings } from "./linked-relation-findings.js";
18
22
  import { linkedRelationOfSpec, reachesAnotherApp } from "../schema/linked.js";
19
- import { ActSpec, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
23
+ import { ActSpec, ALIAS, MOST_ALIASES, EDGE_NAME, EdgeSpec, FIELD_NAME, FIELD_TYPES, FieldSpec, GrantSpec, KindSpec, NAME, RuleSpec, SightSpec, } from "./schema.js";
20
24
  import { SHIPPED_LENSES, isShippedLens, SHIPPED_LENS_NAMES, unknownLensOption, viewReference } from "../places.js";
21
25
  import { LENSES_THAT_LIST, listsRecords } from "../lenses-that-list.js";
22
26
  import { placeSlug, VIEW_REFERENCE } from "../views/types.js";
@@ -24,7 +28,7 @@ import { documentListFields, kindListFindings, kindListWords, listGroup, listSor
24
28
  import { computedOf, validateComputed } from "./computed.js";
25
29
  import { parseTemplate, templateBraces, TemplateError } from "./template-parse.js";
26
30
  import { validateViews, VIEW_SLOTS } from "./views.js";
27
- import { brandFindings, emojiIconSentence, subtitleSentence } from "./brand-check.js";
31
+ import { brandFindings, emojiIconSentence, groundWords, subtitleSentence } from "./brand-check.js";
28
32
  import { darkLogoSentence, logoParts } from "./logo.js";
29
33
  import { emojiOf } from "../theme/marks.js";
30
34
  import { canonicalize } from "./canonical.js";
@@ -33,6 +37,7 @@ import { isRecord as isObject, respellDocument } from "./respell.js";
33
37
  import { refName } from "./compiled.js";
34
38
  import { localeSentence, timeZoneSentence } from "./diff.js";
35
39
  import { accentProblem } from "../theme/accent.js";
40
+ import { groundProblem } from "../theme/ground-check.js";
36
41
  import { pageFieldFindings, pageNames, renamedOnPage, withoutOnPage } from "./page-fields.js";
37
42
  import { capitalize, listed as listOf } from "../capital.js";
38
43
  import { quoted } from "./quote.js";
@@ -81,6 +86,12 @@ export const EDIT_OPS = [
81
86
  "remove-grant",
82
87
  "add-sight",
83
88
  "remove-sight",
89
+ "set-purpose",
90
+ "add-job",
91
+ "edit-job",
92
+ "remove-job",
93
+ "add-alias",
94
+ "set-aliases",
84
95
  ];
85
96
  /** Words the rule language keeps: a field or relation renamed to one could not be named in a rule. */
86
97
  const KEYWORDS = new Set(["and", "or", "not", "in", "where", "true", "false", "null"]);
@@ -101,6 +112,7 @@ function buildShapes() {
101
112
  const edgeName = z.string().regex(EDGE_NAME, 'relation names are one camelCase word, like "tendedBy"');
102
113
  const actName = z.string().regex(NAME, 'act names are lower-case words joined by hyphens, like "mark-paid"');
103
114
  const lensTitle = z.string().min(1).max(80);
115
+ const jobName = z.string().regex(NAME, 'job names are lower-case words joined by hyphens, like "unbooked-soon"');
104
116
  return {
105
117
  "add-kind": z.object({ op: z.literal("add-kind"), kind: kindName, act: z.boolean().optional() }).passthrough(),
106
118
  "rename-kind": z.object({ op: z.literal("rename-kind"), kind: kindName, to: kindName, noun: z.string().min(1).max(40).optional(), plural: z.string().min(1).max(40).optional() }).strict(),
@@ -126,6 +138,8 @@ function buildShapes() {
126
138
  .object({
127
139
  op: z.literal("set-brand"),
128
140
  accent: z.union([z.string(), z.null()]).optional(),
141
+ // The page background (Roadmap f-brand-ground): one color, or one for each scheme; null for Graview's.
142
+ ground: z.union([z.string(), z.object({ light: z.string().optional(), dark: z.string().optional() }).strict(), z.null()]).optional(),
129
143
  name: z.union([z.string().min(1).max(60), z.null()]).optional(),
130
144
  currency: z.union([z.string(), z.null()]).optional(),
131
145
  locale: z.union([z.string(), z.null()]).optional(),
@@ -226,6 +240,28 @@ function buildShapes() {
226
240
  "remove-grant": z.object({ op: z.literal("remove-grant") }).passthrough(),
227
241
  "add-sight": z.object({ op: z.literal("add-sight") }).passthrough(),
228
242
  "remove-sight": z.object({ op: z.literal("remove-sight") }).passthrough(),
243
+ // Who the app is for, in words (Roadmap f-app-jobs); null takes the sentence away.
244
+ "set-purpose": z.object({ op: z.literal("set-purpose"), for: z.union([z.string().min(1).max(300), z.null()]) }).strict(),
245
+ // A job as the document holds one (`purpose.jobs[]`), placed at `at` or last.
246
+ "add-job": z.object({ op: z.literal("add-job"), job: z.record(z.string(), z.unknown()), at: z.number().int().min(0).optional() }).strict(),
247
+ // A job by its name, each key given set, null clearing an optional one.
248
+ "edit-job": z
249
+ .object({
250
+ op: z.literal("edit-job"),
251
+ name: jobName,
252
+ ask: z.string().min(1).max(200).optional(),
253
+ kind: kindName.optional(),
254
+ where: z.union([z.string().min(1).max(2000), z.null()]).optional(),
255
+ sort: z.union([listKey, z.object({ by: listKey, direction: listDirection }).strict(), z.null()]).optional(),
256
+ act: z.union([actName, z.null()]).optional(),
257
+ roles: z.union([z.array(z.string().regex(NAME)).min(1).max(40), z.null()]).optional(),
258
+ })
259
+ .strict(),
260
+ "remove-job": z.object({ op: z.literal("remove-job"), name: jobName }).strict(),
261
+ // Another word people use for a kind, or for one of its fields (Roadmap f-learned-suggestions).
262
+ "add-alias": z.object({ op: z.literal("add-alias"), kind: kindName, field: fieldName.optional(), alias: z.string().min(1).max(40) }).strict(),
263
+ // All of a kind's or a field's other words at once; null or [] takes them away.
264
+ "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(),
229
265
  };
230
266
  }
231
267
  let shapes;
@@ -538,7 +574,8 @@ class Editor {
538
574
  * set, null clears it, and what it does not name stays as it was. Each
539
575
  * is judged as `graview check` judges it — a mark that could act or load,
540
576
  * a face off the list — and an accent that does not read as given is
541
- * refused with the pair, the ratio and a shade that would (FR-126).
577
+ * refused with the pair, the ratio and a shade that would (FR-126), as
578
+ * is a ground that ink, muted ink or the accent would not read on.
542
579
  */
543
580
  setBrand(i, e) {
544
581
  const brand = clone(this.doc.brand ?? {});
@@ -566,6 +603,24 @@ class Editor {
566
603
  brand["accent"] = e.accent;
567
604
  this.said.push(`The app's accent color becomes ${e.accent}.`);
568
605
  }
606
+ if (e.ground === null) {
607
+ if (brand["ground"] !== undefined)
608
+ this.said.push("The app's page background goes back to Graview's.");
609
+ delete brand["ground"];
610
+ }
611
+ else if (e.ground !== undefined) {
612
+ const accent = brand["accent"];
613
+ const refused = groundProblem(e.ground, accent);
614
+ const fixed = (ground, also = "") => `{"op": "set-brand", "ground": ${JSON.stringify(ground).replace(/","/g, '", "').replace(/":"/g, '": "')}${also}}`;
615
+ if (refused)
616
+ return this.fail(i, "ground", refused.sentence, refused.suggestion ? fixed(typeof e.ground === "string" ? refused.suggestion : { ...e.ground, [refused.scheme]: refused.suggestion }) : "pick a lighter color for the light scheme, a darker one for the dark");
617
+ // The accent the app keeps is judged on the new ground as it was on the old (FR-126).
618
+ const unread = accent ? accentProblem(accent, e.ground) : null;
619
+ if (unread)
620
+ return this.fail(i, "ground", unread.sentence, unread.suggestion ? fixed(e.ground, `, "accent": "${unread.suggestion}"`) : "pick another ground, or another accent");
621
+ brand["ground"] = e.ground;
622
+ this.said.push(groundWords(e.ground));
623
+ }
569
624
  if (e.currency === null) {
570
625
  delete brand["currency"];
571
626
  this.said.push("Money is said with no currency.");
@@ -924,7 +979,144 @@ class Editor {
924
979
  case "add-sight":
925
980
  case "remove-sight":
926
981
  return this.policyEdit(i, e);
982
+ case "set-purpose":
983
+ return this.setPurpose(e);
984
+ case "add-job":
985
+ return this.addJob(i, e);
986
+ case "edit-job":
987
+ return this.editJob(i, e);
988
+ case "remove-job":
989
+ return this.removeJob(i, e.name);
990
+ case "add-alias":
991
+ return this.setAliases(i, e.kind, e.field, [...this.aliasesOf(e.kind, e.field), e.alias], e.alias);
992
+ case "set-aliases":
993
+ return this.setAliases(i, e.kind, e.field, e.aliases ?? []);
994
+ }
995
+ }
996
+ // ── other words for a kind or a field (Roadmap f-learned-suggestions) ──
997
+ aliasesOf(kind, field) {
998
+ const spec = this.doc.kinds[kind];
999
+ return (field ? spec?.fields?.[field]?.aliases : spec?.aliases) ?? [];
1000
+ }
1001
+ /**
1002
+ * A kind's or a field's aliases, as words people search with: lower case,
1003
+ * each once. Refused, in the checker's words, when one is not a word or is
1004
+ * what something else in the app is already called. `added` is the one
1005
+ * word `add-alias` gave, said alone.
1006
+ */
1007
+ setAliases(i, kind, field, given, added) {
1008
+ const spec = this.kind(i, kind);
1009
+ if (!spec)
1010
+ return;
1011
+ const target = field ? this.field(i, kind, field) : spec;
1012
+ if (!target)
1013
+ return;
1014
+ const said = [...new Set(given.map(aliasWord))];
1015
+ const at = added === undefined ? "aliases" : "alias";
1016
+ for (const [n, word] of said.entries())
1017
+ if (!ALIAS.test(word))
1018
+ return this.fail(i, added === undefined ? `aliases.${n}` : at, `"${word}" is not a word people search with: one to three words of letters and digits`, 'like "supplier" or "wedding venue"');
1019
+ if (said.length > MOST_ALIASES)
1020
+ return this.fail(i, at, `at most ${MOST_ALIASES} other words for one ${field ? "field" : "kind"}`);
1021
+ const before = target.aliases;
1022
+ if (said.length === 0)
1023
+ delete target.aliases;
1024
+ else
1025
+ target.aliases = said;
1026
+ const wrong = documentAliasFindings(this.doc).filter((finding) => finding.path.startsWith(`kinds.${kind}${field ? `.fields.${field}` : ""}.aliases`));
1027
+ if (wrong.length > 0) {
1028
+ if (before === undefined)
1029
+ delete target.aliases;
1030
+ else
1031
+ target.aliases = before;
1032
+ return this.fail(i, at, wrong[0].message, wrong[0].fix);
927
1033
  }
1034
+ const whose = field ? `${kind}'s ${field}` : withArticle(spec.noun ?? words(kind));
1035
+ if (added !== undefined)
1036
+ this.said.push(`Search and the seat take “${aliasWord(added)}” as another word for ${whose}.`);
1037
+ else
1038
+ this.said.push(said.length === 0 ? `${capitalize(whose)} has no other words now.` : `Other words for ${whose}: ${listOf(said.map((word) => `“${word}”`))}.`);
1039
+ }
1040
+ // ── who the app is for, and what they come to do (Roadmap f-app-jobs) ──
1041
+ setPurpose(e) {
1042
+ const purpose = { ...(this.doc.purpose ?? {}) };
1043
+ if (e.for === null)
1044
+ delete purpose.for;
1045
+ else
1046
+ purpose.for = e.for;
1047
+ this.keepPurpose(purpose);
1048
+ this.said.push(e.for === null ? "The app no longer says whom it is for." : `The app says it is for ${quotedAtTheEnd(e.for)}`);
1049
+ }
1050
+ /** The purpose as it now stands, or none when it says nothing. */
1051
+ keepPurpose(purpose) {
1052
+ if (Array.isArray(purpose.jobs) && purpose.jobs.length === 0)
1053
+ delete purpose.jobs;
1054
+ if (Object.keys(purpose).length === 0)
1055
+ delete this.doc.purpose;
1056
+ else
1057
+ this.doc.purpose = purpose;
1058
+ }
1059
+ /** The job a name names, or -1 having said why. */
1060
+ jobIndex(i, name) {
1061
+ const jobs = this.doc.purpose?.jobs ?? [];
1062
+ const index = jobs.findIndex((job) => job.name === name);
1063
+ if (index < 0)
1064
+ this.fail(i, "name", `there is no job "${name}"${jobs.length > 0 ? `; the jobs are ${jobs.map((job) => `"${job.name}"`).join(", ")}` : ""}`);
1065
+ return index;
1066
+ }
1067
+ /** A job placed among the others, judged as `graview check` judges it (`job-*`): kept only when nothing is wrong with it. */
1068
+ placeJob(i, job, at, replacing) {
1069
+ const jobs = [...(this.doc.purpose?.jobs ?? [])];
1070
+ if (replacing)
1071
+ jobs[at] = job;
1072
+ else
1073
+ jobs.splice(at, 0, job);
1074
+ const wrong = jobFindings({ ...this.doc.purpose, jobs }, jobWorldOfDocument(this.doc)).filter((finding) => finding.index === at || (finding.code === "jobs-too-many" && !replacing) || (finding.code === "job-name-taken" && jobs[finding.index ?? -1]?.name === job.name));
1075
+ for (const finding of wrong)
1076
+ this.failAs(finding.code, i, finding.code === "jobs-too-many" ? "" : `job.${finding.key ?? ""}`, finding.message, finding.fix);
1077
+ if (wrong.length > 0)
1078
+ return false;
1079
+ this.keepPurpose({ ...(this.doc.purpose ?? {}), jobs });
1080
+ return true;
1081
+ }
1082
+ addJob(i, e) {
1083
+ const parsed = JobSpec.safeParse(e.job);
1084
+ if (!parsed.success)
1085
+ return this.refuse(i, parsed.error.issues.map((issue) => ({ ...issue, path: ["job", ...issue.path] })), "a job");
1086
+ const job = parsed.data;
1087
+ const jobs = this.doc.purpose?.jobs ?? [];
1088
+ if (!this.placeJob(i, job, Math.min(e.at ?? jobs.length, jobs.length), false))
1089
+ return;
1090
+ this.said.push(`A new job, ${quoted(job.ask)}: ${jobWords(this.doc, job)}.`);
1091
+ }
1092
+ editJob(i, e) {
1093
+ const index = this.jobIndex(i, e.name);
1094
+ if (index < 0)
1095
+ return;
1096
+ const job = { ...this.doc.purpose.jobs[index] };
1097
+ const changed = ["ask", "kind", "where", "sort", "act", "roles"].filter((key) => e[key] !== undefined);
1098
+ if (changed.length === 0)
1099
+ return this.fail(i, "", "an edit-job names at least one of ask, kind, where, sort, act or roles to change", `{"op": "edit-job", "name": "${e.name}", "ask": "…"}`);
1100
+ for (const key of changed) {
1101
+ if (e[key] === null) {
1102
+ if (key === "ask" || key === "kind")
1103
+ return this.fail(i, key, `a job always has its ${key}`);
1104
+ delete job[key];
1105
+ }
1106
+ else
1107
+ job[key] = e[key];
1108
+ }
1109
+ if (!this.placeJob(i, job, index, true))
1110
+ return;
1111
+ this.said.push(`The job "${job.name}", ${quoted(job.ask)}, is now ${jobWords(this.doc, job)}.`);
1112
+ }
1113
+ removeJob(i, name) {
1114
+ const index = this.jobIndex(i, name);
1115
+ if (index < 0)
1116
+ return;
1117
+ const [job] = this.doc.purpose.jobs.splice(index, 1);
1118
+ this.keepPurpose({ ...this.doc.purpose });
1119
+ this.said.push(`The job "${job.name}" (${quotedAtTheEnd(job.ask)}) is removed.`);
928
1120
  }
929
1121
  /** How a record's page orders and groups its facts (FR-148): judged as the compile judges `kinds.<kind>.page`. */
930
1122
  setPageFields(i, e) {
@@ -2068,7 +2260,7 @@ class Editor {
2068
2260
  gone.push(...this.dropComputed(other, name));
2069
2261
  }
2070
2262
  }
2071
- gone.push(...this.pruneViews(swept), ...this.pruneLenses(swept));
2263
+ gone.push(...this.pruneViews(swept), ...this.pruneLenses(swept), ...this.pruneJobs(swept));
2072
2264
  // The arrangement names kinds (FR-80): a kind that is gone is no longer ordered or hidden, nor where the app opens.
2073
2265
  if (this.doc.pages) {
2074
2266
  for (const part of ["order", "hide", "primary"]) {
@@ -2534,6 +2726,13 @@ class Editor {
2534
2726
  else
2535
2727
  delete rule.repairs;
2536
2728
  }
2729
+ // A job asks the same question without it: only what people do next is let go.
2730
+ for (const job of (this.doc.purpose?.jobs ?? [])) {
2731
+ if (job.act !== act)
2732
+ continue;
2733
+ delete job.act;
2734
+ gone.push(`what the job "${job.name}" does next`);
2735
+ }
2537
2736
  return gone;
2538
2737
  }
2539
2738
  addRule(i, e) {
@@ -2563,6 +2762,9 @@ class Editor {
2563
2762
  for (const r of rule.repairs ?? [])
2564
2763
  if (r.act === from)
2565
2764
  r.act = to;
2765
+ for (const job of (this.doc.purpose?.jobs ?? []))
2766
+ if (job.act === from)
2767
+ job.act = to;
2566
2768
  }
2567
2769
  // ── every place a name is said ──
2568
2770
  /**
@@ -2814,8 +3016,46 @@ class Editor {
2814
3016
  if (JSON.stringify(doc.pages) !== before)
2815
3017
  touched.push("the pages");
2816
3018
  }
3019
+ // Jobs (Roadmap f-app-jobs): what each is answered by, what its where reads, what it sorts by and the act it names.
3020
+ for (const job of (doc.purpose?.jobs ?? [])) {
3021
+ const before = JSON.stringify(job);
3022
+ if (job.where !== undefined)
3023
+ job.where = exprAt(job.where, new Set([job.kind]));
3024
+ const by = typeof job.sort === "string" ? job.sort : job.sort?.by;
3025
+ if (r.t === "field" && job.kind === r.kind && by === r.from)
3026
+ job.sort = typeof job.sort === "string" ? r.to : { ...job.sort, by: r.to };
3027
+ if (r.t === "kind" && job.kind === r.from)
3028
+ job.kind = r.to;
3029
+ if (job.act !== undefined && r.t === "kind")
3030
+ job.act = renamedAct(job.act, r.from, r.to);
3031
+ if (job.act !== undefined && (r.t === "kind" || r.t === "edge"))
3032
+ job.act = renamedRelationAct(job.act, doc.kinds, r);
3033
+ if (JSON.stringify(job) !== before)
3034
+ touched.push(`the job "${job.name}"`);
3035
+ }
2817
3036
  return touched;
2818
3037
  }
3038
+ /** Jobs that can no longer be answered as asked go — a where that reads what is going, a kind that is gone — and an order by it is let go. */
3039
+ pruneJobs(r) {
3040
+ const purpose = this.doc.purpose;
3041
+ if (!purpose?.jobs)
3042
+ return [];
3043
+ const gone = [];
3044
+ purpose.jobs = purpose.jobs.filter((job) => {
3045
+ if ((r.t === "kind" && job.kind === r.from) || exprMentions(this.doc, job.where, new Set([job.kind]), r)) {
3046
+ gone.push(`the job "${job.name}"`);
3047
+ return false;
3048
+ }
3049
+ const by = typeof job.sort === "string" ? job.sort : job.sort?.by;
3050
+ if (r.t === "field" && job.kind === r.kind && by === r.from) {
3051
+ delete job.sort;
3052
+ gone.push(`the order of the job "${job.name}"`);
3053
+ }
3054
+ return true;
3055
+ });
3056
+ this.keepPurpose({ ...purpose });
3057
+ return gone;
3058
+ }
2819
3059
  /**
2820
3060
  * Remove what mentions a field or relation that is going: acts that write or walk
2821
3061
  * it, rules that judge it, view blocks that show it; templates that show it fall
@@ -2912,6 +3152,7 @@ class Editor {
2912
3152
  delete doc.rules;
2913
3153
  gone.push(...this.pruneViews(r));
2914
3154
  gone.push(...this.pruneLenses(r));
3155
+ gone.push(...this.pruneJobs(r));
2915
3156
  return gone;
2916
3157
  }
2917
3158
  /** Computed fields being dropped, so a cycle among them ends. */
@@ -3116,6 +3357,16 @@ class Editor {
3116
3357
  return said;
3117
3358
  }
3118
3359
  }
3360
+ /** How a job is answered, in words: "answered by the vendors where status != 'booked', sorted by due, then book". */
3361
+ function jobWords(doc, job) {
3362
+ const sort = typeof job.sort === "string" ? { by: job.sort, direction: "asc" } : job.sort;
3363
+ return [
3364
+ `answered by the ${pluralWords(doc, job.kind).toLowerCase()}${job.where ? ` where ${job.where}` : ""}`,
3365
+ ...(sort ? [`sorted by ${sort.by}${sort.direction === "desc" ? ", highest first" : ""}`] : []),
3366
+ ...(job.act ? [`then "${job.act}"`] : []),
3367
+ ...(job.roles ? [`for ${listOf(job.roles)}`] : []),
3368
+ ].join(", ");
3369
+ }
3119
3370
  /** What the people and the policy make together, as `graview check` judges it (`checkPeople`), said over the document. */
3120
3371
  function peopleProblems(doc, at) {
3121
3372
  const out = [];