@turri-dev/cli 0.1.27-next.763d091 → 0.1.28-next.d5f2680

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 (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +71 -15
  86. package/dist/regenerate/change-view.d.ts +8 -3
  87. package/dist/regenerate/change-view.js +71 -22
  88. package/dist/regenerate/chunk-plan.d.ts +36 -15
  89. package/dist/regenerate/chunk-plan.js +165 -59
  90. package/dist/regenerate/conform.d.ts +3 -0
  91. package/dist/regenerate/conform.js +6 -5
  92. package/dist/regenerate/conformance-agents.d.ts +18 -39
  93. package/dist/regenerate/conformance-agents.js +60 -88
  94. package/dist/regenerate/conformance.d.ts +23 -20
  95. package/dist/regenerate/conformance.js +118 -121
  96. package/dist/regenerate/diff-view.d.ts +1 -1
  97. package/dist/regenerate/diff-view.js +20 -4
  98. package/dist/regenerate/drift.d.ts +47 -15
  99. package/dist/regenerate/drift.js +101 -30
  100. package/dist/regenerate/jobs.d.ts +2 -2
  101. package/dist/regenerate/jobs.js +1 -1
  102. package/dist/regenerate/ledger.d.ts +19 -0
  103. package/dist/regenerate/loop.d.ts +61 -49
  104. package/dist/regenerate/loop.js +473 -340
  105. package/dist/regenerate/paths.d.ts +4 -0
  106. package/dist/regenerate/paths.js +7 -1
  107. package/dist/regenerate/plain-palette.js +2 -0
  108. package/dist/regenerate/prompt.d.ts +57 -19
  109. package/dist/regenerate/prompt.js +157 -122
  110. package/dist/regenerate/records.d.ts +43 -12
  111. package/dist/regenerate/records.js +102 -88
  112. package/dist/regenerate/regenerate.js +29 -11
  113. package/dist/regenerate/regression.d.ts +4 -4
  114. package/dist/regenerate/regression.js +14 -10
  115. package/dist/regenerate/renames.d.ts +42 -0
  116. package/dist/regenerate/renames.js +185 -0
  117. package/dist/regenerate/requires.d.ts +1 -1
  118. package/dist/regenerate/requires.js +10 -3
  119. package/dist/regenerate/resources.d.ts +5 -3
  120. package/dist/regenerate/resources.js +4 -4
  121. package/dist/regenerate/scripts-being-written.d.ts +19 -0
  122. package/dist/regenerate/scripts-being-written.js +30 -0
  123. package/dist/regenerate/spec-map-view.d.ts +2 -2
  124. package/dist/regenerate/spec-map-view.js +46 -15
  125. package/dist/regenerate/spec-map.d.ts +27 -5
  126. package/dist/regenerate/spec-map.js +92 -60
  127. package/dist/regenerate/spec-steps.d.ts +48 -5
  128. package/dist/regenerate/spec-steps.js +213 -28
  129. package/dist/regenerate/state.d.ts +4 -3
  130. package/dist/regenerate/stop-report.d.ts +1 -1
  131. package/dist/regenerate/stop-report.js +10 -10
  132. package/dist/regenerate/test-advisor.d.ts +101 -0
  133. package/dist/regenerate/test-advisor.js +379 -0
  134. package/dist/regenerate/test-kinds.d.ts +69 -31
  135. package/dist/regenerate/test-kinds.js +109 -80
  136. package/dist/regenerate/test-scripts.d.ts +2 -0
  137. package/dist/regenerate/test-scripts.js +4 -0
  138. package/dist/regenerate/testing-steps.d.ts +41 -0
  139. package/dist/regenerate/testing-steps.js +110 -0
  140. package/dist/regenerate/testing.d.ts +1 -1
  141. package/dist/regenerate/testing.js +5 -4
  142. package/dist/regenerate/unit-review.d.ts +1 -1
  143. package/dist/regenerate/unit-review.js +3 -3
  144. package/dist/regenerate-cli.js +11 -12
  145. package/dist/release-agent.js +4 -0
  146. package/dist/release-cli.js +2 -0
  147. package/dist/release-tools.js +2 -0
  148. package/dist/run-report.d.ts +2 -2
  149. package/dist/run-report.js +4 -3
  150. package/dist/self-invocation.d.ts +1 -1
  151. package/dist/self-invocation.js +1 -1
  152. package/dist/sentinel.js +1 -1
  153. package/dist/setup.d.ts +10 -0
  154. package/dist/setup.js +52 -4
  155. package/dist/skill.d.ts +10 -0
  156. package/dist/skill.js +6 -0
  157. package/dist/skills.js +3 -1
  158. package/dist/spec-tools.js +21 -31
  159. package/dist/test-plain-files.d.ts +41 -0
  160. package/dist/test-plain-files.js +177 -0
  161. package/docs/README.md +3 -1
  162. package/docs/authoring-plain.md +46 -13
  163. package/docs/chats.md +3 -1
  164. package/docs/config.md +20 -15
  165. package/docs/deploy.md +1 -1
  166. package/docs/development.md +2 -1
  167. package/docs/headless.md +15 -18
  168. package/docs/hosting.md +28 -12
  169. package/docs/installation.md +1 -0
  170. package/docs/parser.md +88 -27
  171. package/docs/regenerating.md +89 -54
  172. package/docs/sentinel.md +1 -1
  173. package/docs/skills-and-gates.md +3 -1
  174. package/docs/test-advisor.md +110 -0
  175. package/docs/theming.md +1 -1
  176. package/docs/using.md +17 -15
  177. package/genome/rules/bullet-continuation.md +8 -8
  178. package/genome/rules/concise-specs-examples.md +111 -80
  179. package/genome/rules/concise-specs-integration-examples.md +56 -41
  180. package/genome/rules/concise-specs.md +17 -16
  181. package/genome/rules/definitions.md +5 -5
  182. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
  183. package/genome/rules/func-specs.md +69 -74
  184. package/genome/rules/impl-reqs.md +4 -4
  185. package/genome/rules/integration-embedded-testing.md +20 -20
  186. package/genome/rules/integration-embedded.md +31 -31
  187. package/genome/rules/integration-standalone.md +9 -9
  188. package/genome/rules/integrations.md +18 -18
  189. package/genome/rules/linked-resources.md +19 -16
  190. package/genome/rules/module-structure.md +21 -14
  191. package/genome/rules/project-model.md +18 -17
  192. package/genome/rules/required-concepts.md +1 -1
  193. package/genome/rules/requires-modules.md +6 -5
  194. package/genome/rules/semantic-ids.md +49 -0
  195. package/genome/rules/stdlib.md +4 -5
  196. package/genome/rules/test-plain-files.md +130 -0
  197. package/genome/rules/test-reqs.md +8 -8
  198. package/genome/rules/turri-regeneration.md +24 -8
  199. package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
  200. package/genome/skills/analyze-func-specs/SKILL.md +46 -32
  201. package/genome/skills/break-down-func-spec/SKILL.md +91 -67
  202. package/genome/skills/consolidate-concepts/SKILL.md +10 -10
  203. package/genome/skills/convert-old-syntax/SKILL.md +68 -0
  204. package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
  205. package/genome/skills/debug-specs/SKILL.md +62 -56
  206. package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
  207. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
  208. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
  209. package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
  210. package/genome/skills/refactor-module/SKILL.md +35 -35
  211. package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
  212. package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
  213. package/package.json +7 -6
  214. package/dist/regenerate/choose-conformance.d.ts +0 -32
  215. package/dist/regenerate/choose-conformance.js +0 -117
  216. /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
@@ -1,8 +1,9 @@
1
- import { existsSync, readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
2
2
  import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import { plainFileParser, withParseCache } from "../plain/parser.js";
4
4
  import { STDLIB_ROOT } from "../plain/stdlib.js";
5
- import { ACCEPTANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
5
+ import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_PLAIN_FILES, TEST_REQUIREMENTS } from "../plain/spec.js";
6
+ import { testPlainFilesOf } from "../test-plain-files.js";
6
7
  import { reportActivity, reportStage } from "./activity.js";
7
8
  import { llmChunkAgents, MAX_LINES_PER_JOB, MAX_SPECS_PER_JOB, planChunks, stepsOf } from "./chunk-plan.js";
8
9
  import { commitNotes, landWork, recordHistory, resetToLastCommit } from "./lineage.js";
@@ -10,11 +11,11 @@ import { firstLine, record } from "./ledger.js";
10
11
  import { NOTES_FILE, recordPulse } from "./notes.js";
11
12
  import { createJobRegistry, formatDuration } from "./jobs.js";
12
13
  import { moduleLanes } from "./lanes.js";
13
- import { adoptDigests, chainTree, codeTree, conformanceTarget, marksRegenerateAgain, REGENERATE_AGAIN, inheritedRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, } from "./records.js";
14
+ import { adoptDigests, chainTree, codeTree, conformanceTarget, marksRegenerateAgain, REGENERATE_AGAIN, inheritedRecord, isOldRecord, notBuilt, readRecord, RECORD_FILE, withResourceDigests, writeCodeRecord, writeTestsRecord, hasScenarios, identityOf, testedFuncIds, } from "./records.js";
14
15
  import { describeSpecDiffConformance, renderSpecDiffConformance } from "./conform.js";
15
16
  import { conformanceDirFor } from "./conformance.js";
16
17
  import { modulesRoot, projectRootOf } from "./paths.js";
17
- import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn, stackForModule, stackPinned, withStack } from "../config.js";
18
+ import { configForSpec, stackForModule, stackPinned, testDirOf, withStack } from "../config.js";
18
19
  import { describeSpecChange } from "./prompt.js";
19
20
  import { resolveTemplateDirs } from "./paths.js";
20
21
  import { builtTips, copyRequiredCode, importingModules, isImportModule, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
@@ -25,7 +26,14 @@ import { createSteering, MAX_STEERS, markTold, steer, waitingSteer } from "./ste
25
26
  import { projectDirOf } from "./paths.js";
26
27
  import { changesBetween, parseTree, serializeTree } from "./spec-steps.js";
27
28
  import { listFiles } from "./testing.js";
29
+ import { makeRenames, pureRenames, renameText } from "./renames.js";
28
30
  import { casesOf, codeWork, stillWanted, whyNotWanted } from "./drift.js";
31
+ import { testingGap } from "./testing-steps.js";
32
+ import { baseFolderDrift, baseOfCodebase, seedFromBaseFolder } from "./base-folder.js";
33
+ import { BASE_RECORD_FILE } from "./paths.js";
34
+ import { onScriptRunEnded, scriptBeingWritten } from "./scripts-being-written.js";
35
+ import { conformanceTestsOn } from "../conformance-switch.js";
36
+ import { findTestScripts, projectRootFor } from "./test-scripts.js";
29
37
  /** What is left to regenerate, as the conversation and the command line read it. */
30
38
  export function describeDrift(reports) {
31
39
  if (reports.length === 0)
@@ -35,8 +43,8 @@ export function describeDrift(reports) {
35
43
  // the spec as it is, nothing regenerating or waiting, and nothing holding the codebase back. A pulse that did not
36
44
  // land leaves its work undone, so it shows as drift.
37
45
  // conformance: null is no drift, undefined none written here - either is regenerated; a case list is work left.
38
- const regenerated = reports.every((report) => report.code === null && (report.conformance === null || report.conformance === undefined));
39
- const settled = reports.every((report) => report.running === undefined && report.why === undefined && report.startOver === undefined && report.scheduled.length === 0);
46
+ const regenerated = reports.every((report) => report.code === null && (report.conformance === null || report.conformance === undefined) && (report.renames ?? []).length === 0);
47
+ const settled = reports.every((report) => report.running === undefined && report.why === undefined && report.startOver === undefined);
40
48
  const verdict = regenerated && settled ? "\nFully regenerated: every codebase is regenerated from the spec as it is, and nothing is regenerating." : "";
41
49
  return reports
42
50
  .map((report) => {
@@ -47,23 +55,40 @@ export function describeDrift(reports) {
47
55
  lines.push(` code: drifted from the spec - to regenerate: ${cases(report.code)}`);
48
56
  if (report.conformance === null)
49
57
  lines.push(" conformance tests: written for what is built - no drift");
50
- else if (report.conformance !== undefined)
51
- lines.push(` conformance tests: to write or change - ${cases(report.conformance)}`);
58
+ else if (report.conformance !== undefined) {
59
+ lines.push(report.conformanceOnly !== undefined
60
+ ? ` conformance tests: to write or change - conform asked for ${report.conformanceOnly.join(", ")} alone: the others wait for conform with no names, or the module asked for again`
61
+ : " conformance tests: to write or change - written once the code is built, or now with conform", ...conformanceLines(report.module, report.conformance));
62
+ }
52
63
  else
53
- lines.push(" conformance tests: none written here");
54
- for (const group of report.scheduled)
55
- lines.push(` conformance tests ahead of their code: fs ${group.frids.join(", fs ")} - once ${group.unbuilt.map((frid) => `fs ${frid}`).join(", ")} ${group.unbuilt.length === 1 ? "is" : "are"} regenerated`);
64
+ lines.push(report.conformanceOff === true ? " conformance tests: off (conformance-tests: false) - none written or run" : " conformance tests: none written here");
65
+ for (const rename of report.renames ?? [])
66
+ lines.push(` renames ${rename} - the records and the tests' labels only, nothing regenerated`);
67
+ if (report.baseFolder !== undefined)
68
+ lines.push(` ${report.baseFolder}`);
56
69
  if (report.running !== undefined)
57
70
  lines.push(` regenerating now: pulse ${report.running.jobId} - ${report.running.title}`);
58
71
  if (report.why !== undefined)
59
72
  lines.push(` ${report.why}`);
60
- if (report.startOver !== undefined)
61
- lines.push(` it has to be regenerated from scratch on ${report.startOver.requires.join(", ")} before anything more is regenerated there`);
73
+ if (report.startOver !== undefined) {
74
+ lines.push(report.startOver.old === true
75
+ ? ` it has to be regenerated from scratch, with its whole chain, before anything more is regenerated there: ${OLD_RECORDS_WHY(report.startOver.codebases)}`
76
+ : ` it has to be regenerated from scratch on ${report.startOver.requires.join(", ")} before anything more is regenerated there`);
77
+ }
62
78
  return lines.join("\n");
63
79
  })
64
80
  .join("\n") + verdict;
65
81
  }
66
82
  const tipOf = (chain) => chain.at(-1);
83
+ /** When a file was last written, as a key - none when it is not there. */
84
+ const modifiedAt = (path) => {
85
+ try {
86
+ return String(statSync(path).mtimeMs);
87
+ }
88
+ catch {
89
+ return "-";
90
+ }
91
+ };
67
92
  /** Whether a module's codebase has been started: code in it, or a record. */
68
93
  const started = (entry) => listFiles(entry.buildDir).length > 0 || readRecord(entry.buildDir) !== null;
69
94
  /** The spec a codebase is built from, as the .plain files have it now: its whole requires chain's. */
@@ -71,7 +96,8 @@ export function chainSpec(chain) {
71
96
  // Each module's linked resources hashed where its links point: what a file holds is part of the spec.
72
97
  const trees = chain.map((entry) => {
73
98
  const dirs = resolveTemplateDirs(entry.plainFile);
74
- return { module: entry.moduleName, tree: withResourceDigests(plainFileParser(basename(entry.plainFile), dirs, { allowNoFunctionalities: true })[1], dirs) };
99
+ // Its test plain files bound to it: each functional spec with its scenarios, and every one of them with a test plain file.
100
+ return { module: entry.moduleName, tree: withResourceDigests(plainFileParser(basename(entry.plainFile), dirs, { allowNoFunctionalities: true, testDir: testDirOf(entry.plainFile) })[1], dirs) };
75
101
  });
76
102
  const tip = tipOf(chain);
77
103
  return { moduleName: tip.moduleName, plainFile: tip.plainFile, tree: chainTree(trees), chain };
@@ -121,7 +147,24 @@ export function functionalityCompletion(plainFile) {
121
147
  if (readRecord(entry.buildDir) !== null)
122
148
  delivered = index;
123
149
  });
124
- const counts = chain.map((entry) => ({ module: entry.moduleName, specified: entry.frids.length, completed: 0, conformanceTested: 0, tested: [], incomplete: [...entry.frids] }));
150
+ // Of each module's functional specs, those with scenarios: what "tested of specified" counts over.
151
+ let whole;
152
+ try {
153
+ whole = chainSpec(chain).tree;
154
+ }
155
+ catch {
156
+ whole = undefined;
157
+ }
158
+ const ownItems = (tree, module, own) => (tree?.[own ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS] ?? []).filter((item) => own || item.module === module);
159
+ const counts = chain.map((entry, index) => ({
160
+ module: entry.moduleName,
161
+ specified: entry.funcIds.length,
162
+ withScenarios: ownItems(whole, entry.moduleName, index === chain.length - 1).filter(hasScenarios).length,
163
+ completed: 0,
164
+ conformanceTested: 0,
165
+ tested: [],
166
+ incomplete: [...entry.funcIds],
167
+ }));
125
168
  if (delivered === -1)
126
169
  return { codebase: tipOf(chain).buildDir, modules: counts };
127
170
  const codebase = chain.slice(0, delivered + 1);
@@ -131,34 +174,39 @@ export function functionalityCompletion(plainFile) {
131
174
  const changes = codeWork(record, spec);
132
175
  // Its own: what the work loop would regenerate - new, changed, a concept it uses changed, or marked again.
133
176
  const cases = casesOf(codeTree(record ?? {}), changes, { module: tip.moduleName });
134
- const touched = new Set([...cases.add, ...cases.patch, ...cases.again].map((spec) => `${spec.module ?? tip.moduleName}:${spec.frid}`));
177
+ const touched = new Set([...cases.add, ...cases.patch, ...cases.again].map((spec) => `${spec.module ?? tip.moduleName}:${spec.funcId}`));
135
178
  // The modules' below it: an item the record does not have as the spec has it.
136
179
  for (const change of changes) {
137
180
  if (change.section !== REQUIRED_FUNCTIONAL_SPECS || change.after === undefined)
138
181
  continue;
139
182
  try {
140
183
  const item = JSON.parse(change.after);
141
- if (item.module !== undefined && item.frid !== undefined)
142
- touched.add(`${item.module}:${item.frid}`);
184
+ if (item.module !== undefined && item.id !== undefined)
185
+ touched.add(`${item.module}:${item.id}`);
143
186
  }
144
187
  catch {
145
188
  /* not an item */
146
189
  }
147
190
  }
148
- const specText = (module, frid) => {
149
- const own = module === tip.moduleName;
150
- const items = spec[own ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS] ?? [];
151
- return (own ? items[Number(frid) - 1] : items.find((item) => item.module === module && item.frid === frid))?.markdown.trim();
191
+ // A functional spec as the tests are written for it: its text and its scenarios.
192
+ const reads = (item) => {
193
+ if (item === undefined)
194
+ return undefined;
195
+ const scenarios = (key) => (item[key] ?? []).map((scenario) => [scenario.id, scenario.text]);
196
+ return JSON.stringify([String(item.markdown ?? "").trim(), scenarios(CONFORMANCE_TESTS), scenarios(ACCEPTANCE_TESTS)]);
152
197
  };
198
+ const specText = (module, funcId) => reads(ownItems(spec, module, module === tip.moduleName).find((item) => item.id === funcId));
199
+ // What the conformance tests are written for, in the same codebase: its suite holds the chain's.
200
+ const testsRecord = readRecord(conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile))));
153
201
  codebase.forEach((entry, index) => {
154
202
  const count = counts[index];
155
- const completed = entry.frids.filter((frid) => !touched.has(`${entry.moduleName}:${frid}`));
203
+ const completed = entry.funcIds.filter((funcId) => !touched.has(`${entry.moduleName}:${funcId}`));
156
204
  // Tests marked to regenerate again were written against code since replaced: not conformance tested until rewritten.
157
- const tested = (readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDirOf(entry.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).filter((item) => item.frid !== undefined && !marksRegenerateAgain(item) && item.markdown.trim() === specText(entry.moduleName, item.frid));
205
+ const tested = ownItems(testsRecord ?? undefined, entry.moduleName, entry.moduleName === tip.moduleName).filter((item) => item.id !== undefined && !marksRegenerateAgain(item) && reads(item) === specText(entry.moduleName, item.id));
158
206
  count.completed = completed.length;
159
- count.tested = completed.filter((frid) => tested.some((item) => item.frid === frid));
207
+ count.tested = completed.filter((funcId) => tested.some((item) => item.id === funcId));
160
208
  count.conformanceTested = count.tested.length;
161
- count.incomplete = entry.frids.filter((frid) => !completed.includes(frid));
209
+ count.incomplete = entry.funcIds.filter((funcId) => !completed.includes(funcId));
162
210
  });
163
211
  return { codebase: tip.buildDir, modules: counts };
164
212
  }));
@@ -177,17 +225,6 @@ export function createWorkLoop(options) {
177
225
  const order = [];
178
226
  const run = options.run ?? defaultRunner;
179
227
  const runConformance = options.runConformance ?? defaultConformanceRunner;
180
- // Off unless the config turns it on: all (true), or chosen.
181
- const configConformance = options.conformanceOn ?? ((plainFile) => conformanceSwitchedOn(configForSpec(plainFile).conformanceTests));
182
- /** When the chooser decides: after each code pulse (chosen), once the module is regenerated (module), or never. */
183
- const chooseWhen = (plainFile) => options.conformanceChoosing !== undefined
184
- ? options.conformanceChoosing(plainFile)
185
- : options.conformanceChosen !== undefined
186
- ? options.conformanceChosen(plainFile)
187
- ? "pulse"
188
- : undefined
189
- : choosingWhen(configForSpec(plainFile).conformanceTests);
190
- const configChosen = (plainFile) => chooseWhen(plainFile) !== undefined;
191
228
  // One tick at a time: each reads the records and the spec, and decides.
192
229
  let ticking = Promise.resolve();
193
230
  const tipEntry = (state) => tipOf(state.chain);
@@ -219,159 +256,123 @@ export function createWorkLoop(options) {
219
256
  /** What the code is built from, as a key: a conformance plan made against other code is made again. */
220
257
  const codeKeyOf = (state) => serializeTree(readRecord(tipEntry(state).buildDir) ?? {});
221
258
  /**
222
- * What the conformance tests are to be written for now: of what the code is built from, what is chosen, with the test
223
- * reqs as the spec has them. `from`: the tests the work starts from - what a running pulse will land, when the work
259
+ * What the conformance tests are to be written for now: of what the code is built from, the functional specs with
260
+ * scenarios, with the test reqs as the spec has them. `from`: the tests the work starts from - what a running pulse will land, when the work
224
261
  * after it is planned ahead - else the tests record as it is. Read from the record alone, a pulse still writing a
225
262
  * functional spec's tests would not count them: one whose code is behind the spec would be planned out, its tests
226
263
  * taken out again once they landed.
227
264
  */
228
- const conformanceOf = (state, spec, from) => {
265
+ const conformanceOf = (state, spec, from, narrowed = true) => {
229
266
  const tests = from ?? readRecord(testsDirOf(state));
230
- const target = conformanceTarget(readRecord(tipEntry(state).buildDir), tests, spec.tree, chosenOf(state, spec, tests));
267
+ const whole = conformanceTarget(readRecord(tipEntry(state).buildDir), tests, spec.tree);
268
+ const target = narrowed ? onlyNamed(state, whole, tests, spec.tree) : whole;
231
269
  // No functional spec to test, and none tested: nothing to write - the reqs alone are no conformance job.
232
- const specsIn = (tree) => (tree?.[FUNCTIONAL_REQUIREMENTS] ?? []).length;
270
+ const specsIn = (tree) => (tree?.[FUNCTIONAL_REQUIREMENTS] ?? []).length + (tree?.[REQUIRED_FUNCTIONAL_SPECS] ?? []).length;
233
271
  return specsIn(target) === 0 && specsIn(tests) === 0 ? (tests ?? {}) : target;
234
272
  };
235
273
  /**
236
- * Which functional specs the conformance tests are for: every one - unless the config has them for the chosen ones,
237
- * or the conversation named some over a config that has them off. Then: those named, those with acceptance tests
238
- * when the config chooses, and those tested already - a choice is kept, and tests are never taken out because
239
- * other functional specs were named.
274
+ * Whether a codebase's conformance tests are written: with test reqs to write them by, and code to test. What they are
275
+ * written for is the functional specs with scenarios (conformanceOf): a scenario added, changed or taken out, the
276
+ * functional spec's text changed, regenerated again or taken out - the tests of each are behind, and are written.
240
277
  */
241
- const chosenOf = (state, spec, tests) => {
242
- const named = state.conformance?.chosen;
243
- const choosing = configChosen(tipEntry(state).plainFile);
244
- if (!choosing && (state.conformance === undefined || named === undefined || configConformance(tipEntry(state).plainFile)))
245
- return undefined;
246
- if (choosing && state.conformance !== undefined && named === undefined)
247
- return undefined;
248
- const tested = new Set((tests?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
249
- const specs = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
250
- const accepted = (frid) => (specs[Number(frid) - 1]?.[ACCEPTANCE_TESTS] ?? []).length > 0;
251
- return (frid) => tested.has(frid) || named?.has(frid) === true || (choosing && accepted(frid));
252
- };
278
+ const conformanceWanted = (state, spec) => conformanceTestsOn(tipEntry(state).plainFile) &&
279
+ (spec.tree[TEST_REQUIREMENTS] ?? []).length > 0 && readRecord(tipEntry(state).buildDir) !== null && gapOf(state, spec) === undefined;
253
280
  /**
254
- * Under `conformance-tests: chosen`: the functional specs a code job built or changed that no conformance test is
255
- * for or is to be for - for the conversation to choose from.
281
+ * The step of adding tests a codebase skipped - its functional specs have scenarios, and it has no test reqs or no
282
+ * conformance testing script for its stack: no conformance pulse of it starts, in the loop or from conform, and it
283
+ * waits on that step until the spec or the scripts change.
256
284
  */
257
- const toChoose = (state, planned, spec) => {
258
- if (planned.track !== "code" || !configChosen(tipEntry(state).plainFile))
259
- return [];
260
- const chosen = chosenOf(state, spec, readRecord(testsDirOf(state)));
261
- if (chosen === undefined)
262
- return [];
263
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
264
- return [...planned.cases.add, ...planned.cases.patch].filter((touched) => !chosen(touched.frid) && !scheduled.has(touched.frid));
285
+ const gapOf = (state, spec) => {
286
+ const tip = tipEntry(state);
287
+ return testingGap({ moduleName: tip.moduleName, plainFile: tip.plainFile, tree: spec.tree, buildDir: tip.buildDir });
265
288
  };
266
289
  /**
267
- * Under `conformance-budget:`: how many more of the module's functional specs may be chosen - the budget less every
268
- * one chosen for it already, tested or scheduled, whenever that was; those with acceptance tests do not count. Undefined:
269
- * no budget.
290
+ * The functional specs conform is asked for, as the spec has them now - refused when one is not in the spec, or has no
291
+ * scenarios to test.
270
292
  */
271
- const budgetLeft = (state, spec) => {
272
- const budget = configForSpec(tipEntry(state).plainFile).conformanceBudget;
273
- if (budget === undefined)
274
- return undefined;
275
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
276
- const tests = readRecord(testsDirOf(state));
277
- const tested = new Set((tests?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
278
- const named = state.conformance?.chosen ?? new Set();
279
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
280
- const used = items.filter((item, index) => {
281
- const frid = String(index + 1);
282
- return (item[ACCEPTANCE_TESTS] ?? []).length === 0 && (tested.has(frid) || named.has(frid) || scheduled.has(frid));
283
- }).length;
284
- return Math.max(0, budgetFor(budget, items.length) - used);
293
+ const askedSpecs = (moduleName, spec, funcIds) => {
294
+ const items = spec[FUNCTIONAL_REQUIREMENTS] ?? [];
295
+ const unknown = funcIds.filter((funcId) => !items.some((item) => item.id === funcId));
296
+ if (unknown.length > 0)
297
+ throw new Error(`${moduleName} has no functional spec ${namesOf(moduleName, unknown)}. Nothing was started.`);
298
+ const untested = funcIds.filter((funcId) => !hasScenarios(items.find((item) => item.id === funcId)));
299
+ if (untested.length > 0) {
300
+ throw new Error(`${namesOf(moduleName, untested)} ${untested.length === 1 ? "has" : "have"} no scenarios - no test plain file - so there is nothing to test: ` +
301
+ "write the conformance or acceptance scenarios first, with change_plain, and their tests are written once the code is built. Nothing was started.");
302
+ }
285
303
  };
286
304
  /**
287
- * Under `conformance-tests: chosen`: every functional spec of the module that is built and that no conformance test
288
- * is for, chosen or scheduled - the module's whole choice, once its code is built.
305
+ * Whether a codebase lacks a testing script of this kind that is being written in the background now: it waits for it,
306
+ * rather than fail for want of it, and is looked at again once the run ends.
289
307
  */
290
- const moduleToChoose = (state, spec) => {
291
- if (!configChosen(tipEntry(state).plainFile))
292
- return [];
293
- const chosen = chosenOf(state, spec, readRecord(testsDirOf(state)));
294
- if (chosen === undefined)
295
- return [];
296
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
297
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
298
- const frids = items.map((_, index) => String(index + 1));
299
- const unbuilt = new Set(notBuilt(readRecord(tipEntry(state).buildDir), spec.tree, frids));
300
- return frids
301
- .filter((frid) => !unbuilt.has(frid) && !chosen(frid) && !scheduled.has(frid))
302
- .map((frid) => ({ frid, text: items[Number(frid) - 1].markdown.trim().split("\n")[0].replace(/^-\s*/, "").trim() }));
308
+ const awaitsScript = (state, kind) => {
309
+ const tip = tipEntry(state);
310
+ const root = projectRootFor(tip.plainFile, tip.buildDir);
311
+ return scriptBeingWritten(root, kind, stackForModule(tip.plainFile)) && findTestScripts(root, tip.plainFile, tip.buildDir)[kind] === undefined;
303
312
  };
304
313
  /**
305
- * The groups scheduled whose functional specs are all built, as the spec has them: chosen, so their tests are
306
- * written. A functional spec taken out of the spec is dropped from its group.
314
+ * The base folder a codebase started from, as its status says it. Counting the files still there looks at each, and the
315
+ * status is read often: so it is read again only when the base record or the code record moved - a pulse landed.
307
316
  */
308
- const promote = (state, spec) => {
309
- const chosen = state.conformance?.chosen;
310
- if (state.scheduled === undefined || chosen === undefined)
311
- return;
312
- const count = (spec.tree[FUNCTIONAL_REQUIREMENTS] ?? []).length;
313
- const code = readRecord(tipEntry(state).buildDir);
314
- state.scheduled = state.scheduled.flatMap((group) => {
315
- const specs = group.specs.filter((touched) => Number(touched.frid) <= count);
316
- const unbuilt = notBuilt(code, spec.tree, specs.map((touched) => touched.frid));
317
- if (specs.length === 0)
318
- return [];
319
- if (unbuilt.length > 0)
320
- return [{ specs, unbuilt }];
321
- for (const touched of specs)
322
- chosen.add(touched.frid);
323
- state.together = [...(state.together ?? []), specs];
324
- // What the tests are to be written for changed: planned again - after the running job, when one runs.
325
- state.conformancePlan = undefined;
326
- return [];
327
- });
317
+ const baseOfState = (state) => {
318
+ const dir = tipEntry(state).buildDir;
319
+ const at = [BASE_RECORD_FILE, RECORD_FILE].map((name) => modifiedAt(join(dir, name))).join(":");
320
+ if (state.base?.at !== at) {
321
+ const started = baseOfCodebase(dir);
322
+ state.base = { at, base: started !== undefined ? { folder: started.folder, files: started.files.length } : undefined };
323
+ }
324
+ return state.base.base;
328
325
  };
329
- /** Whether a codebase's conformance tests are written: asked for, or on in the config - with test reqs to write them from, and code to test. */
330
- const conformanceWanted = (state, spec) => (state.conformance !== undefined || configConformance(tipEntry(state).plainFile)) &&
331
- (spec.tree[TEST_REQUIREMENTS] ?? []).length > 0 &&
332
- readRecord(tipEntry(state).buildDir) !== null;
333
326
  /**
334
- * The functional specs a conformance job is scheduled for, as the spec has them now - refused when one is not in
335
- * the spec, or the module has no test reqs to write tests from: no code job would build it, or no test be written.
327
+ * What the conformance tests are to be written for, as conform narrowed it to the functional specs it named: every other
328
+ * functional spec as the tests are written for it now - its tests neither written, changed nor taken out. One whose text
329
+ * or scenarios changed since joins those named: a change to it is work again.
336
330
  */
337
- const scheduledSpecs = (state, frids) => {
338
- const tip = tipEntry(state);
339
- const spec = chainSpec(state.chain);
340
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
341
- const unknown = frids.filter((frid) => !/^[1-9]\d*$/.test(frid) || Number(frid) > items.length);
342
- if (unknown.length > 0) {
343
- throw new Error(`${tip.moduleName} has ${items.length} functional spec${items.length === 1 ? "" : "s"}, and ${unknown.map((frid) => `fs ${frid}`).join(", ")} ` +
344
- `${unknown.length === 1 ? "is not one of them" : "are not among them"}: no code pulse builds ${unknown.length === 1 ? "it" : "them"}, so no conformance ` +
345
- "test could ever be written for it. Nothing was scheduled.");
331
+ const onlyNamed = (state, target, tests, tree) => {
332
+ const only = state.conformanceOnly;
333
+ if (only === undefined)
334
+ return target;
335
+ for (const section of [FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS]) {
336
+ for (const item of tree[section] ?? []) {
337
+ const key = identityOf(item);
338
+ if (!only.specs.has(key) && only.read.get(key) !== JSON.stringify(item))
339
+ only.specs.add(key);
340
+ }
346
341
  }
347
- if ((spec.tree[TEST_REQUIREMENTS] ?? []).length === 0) {
348
- throw new Error(`${tip.moduleName} has no ***test reqs***: conformance tests are written from them, so none can be. Nothing was scheduled.`);
342
+ const narrowed = { ...target };
343
+ for (const section of [FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS]) {
344
+ const tested = new Map(((tests?.[section] ?? [])).map((item) => [identityOf(item), item]));
345
+ const wanted = (target[section] ?? []).flatMap((item) => {
346
+ if (only.specs.has(identityOf(item)))
347
+ return [item];
348
+ // Not named: as its tests are written for it now, or - none written - not at all.
349
+ const kept = tested.get(identityOf(item));
350
+ return kept !== undefined ? [kept] : [];
351
+ });
352
+ // Taken out of the target and not named: its tests are kept, as they are.
353
+ const inTarget = new Set(wanted.map(identityOf));
354
+ for (const [key, item] of tested)
355
+ if (!only.specs.has(key) && !inTarget.has(key))
356
+ wanted.push(item);
357
+ if (wanted.length > 0)
358
+ narrowed[section] = wanted;
359
+ else
360
+ delete narrowed[section];
349
361
  }
350
- return frids.map((frid) => ({ frid, text: items[Number(frid) - 1].markdown.trim().split("\n")[0].replace(/^-\s*/, "").trim() }));
362
+ return narrowed;
351
363
  };
352
- /** Functional specs of a module scheduled for conformance tests, beside those chosen before - as conform does for the module's own - in the chooser's pulses, when it planned them. */
353
- const schedule = (state, frids, groups) => {
354
- const before = state.conformance;
355
- // Every one is tested already: nothing to add.
356
- if (before !== undefined && before.chosen === undefined)
357
- return;
358
- let specs;
359
- try {
360
- specs = scheduledSpecs(state, frids);
361
- }
362
- catch (error) {
363
- reportActivity(` ${tipEntry(state).moduleName}: the conformance tests chosen were not scheduled - ${error instanceof Error ? error.message : String(error)}`);
364
- return;
365
- }
366
- state.conformance = { chosen: before?.chosen ?? new Set() };
367
- state.scheduled = [...(state.scheduled ?? []), { specs, unbuilt: specs.map((touched) => touched.frid) }];
368
- if (groups !== undefined && groups.length > 0) {
369
- state.conformanceGroups = [...(state.conformanceGroups ?? []), ...groups];
370
- state.conformancePlan = undefined;
364
+ /** The functional specs conform named - its own module's func-ids - and every functional spec as it reads now, to tell a change to one after. */
365
+ const namedOnly = (state, funcIds) => {
366
+ const tree = chainSpec(state.chain).tree;
367
+ const read = new Map();
368
+ for (const section of [FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS]) {
369
+ for (const item of tree[section] ?? [])
370
+ read.set(identityOf(item), JSON.stringify(item));
371
371
  }
372
+ return { specs: new Set(funcIds.map((id) => identityOf({ id }))), read };
372
373
  };
373
- /** The work of a track, from where it is built from to what it is to be. */
374
- const workOf = (track, from, state, spec) => track === "code" ? codeWork(from, spec.tree) : changesBetween(serializeTree(from), serializeTree(conformanceOf(state, spec, from)));
374
+ /** The work of a track, from where it is built from to what it is to be - for the conformance tests, as conform narrowed them unless `narrowed` is false. */
375
+ const workOf = (track, from, state, spec, narrowed = true) => track === "code" ? codeWork(from, spec.tree) : changesBetween(serializeTree(from), serializeTree(conformanceOf(state, spec, from, narrowed)));
375
376
  /** The codebases brought up in this look: one left behind is never waited for. */
376
377
  let looking = new Set();
377
378
  /**
@@ -394,8 +395,6 @@ export function createWorkLoop(options) {
394
395
  return waiting("the code it is built on is not built yet");
395
396
  if (below.conformanceHalted !== undefined)
396
397
  return waiting(`its conformance tests did not pass - ${below.conformanceHalted.why}`);
397
- if ((below.scheduled ?? []).length > 0)
398
- return waiting("the conformance tests chosen for it are not written yet");
399
398
  if (conformanceWanted(below, spec) && workOf("conformance", testedOf(below), below, spec).length > 0)
400
399
  return waiting("its conformance tests are not written yet");
401
400
  }
@@ -462,9 +461,11 @@ export function createWorkLoop(options) {
462
461
  // How big a pulse may be: the module's config, else the defaults.
463
462
  const config = configForSpec(tip.plainFile);
464
463
  const limits = { specs: config.maxSpecsPerPulse ?? MAX_SPECS_PER_JOB, lines: config.maxLinesPerPulse ?? MAX_LINES_PER_JOB };
465
- // The conformance tests the person asked for in groups of their own - 1-3, then 4-6: a group never shares a job with another.
466
- const groups = track === "conformance" && state.conformanceGroups !== undefined && state.conformanceGroups.length > 0 ? state.conformanceGroups : undefined;
467
- const input = { base: from, current, specFile, specText, steering, track, moduleName: tip.moduleName, limits, ...(separately ? { separately } : {}), ...(groups !== undefined ? { groups } : {}) };
464
+ const testPlainPath = track === "conformance" ? testPlainPathOf(spec.tree, tip.moduleName, tip.plainFile, dirname(specFile)) : undefined;
465
+ // A pure rename is turri's to make before the next pulse, not a delete and an add: the work is planned from the code as renamed.
466
+ const renamed = track === "code" ? pureRenames(from, readRecord(testsDirOf(state)), spec.tree, tip.moduleName) : undefined;
467
+ const base = renamed !== undefined && renamed.renames.length > 0 && renamed.code !== null ? codeTree(renamed.code) : from;
468
+ const input = { base, current, specFile, specText, steering, track, moduleName: tip.moduleName, limits, ...(separately ? { separately } : {}), ...(testPlainPath !== undefined ? { testPlainPath } : {}) };
468
469
  const model = typeof options.plannerModel === "function" ? options.plannerModel() : options.plannerModel;
469
470
  const agents = options.planner === false ? undefined : (options.planner ?? llmChunkAgents({ cwd: options.cwd, model: model ?? state.model, label: `${basename(tip.plainFile)} planner` }));
470
471
  reportStage(`${tip.moduleName}: planning the ${track === "code" ? "work" : "conformance tests"} into pulses`);
@@ -510,8 +511,10 @@ export function createWorkLoop(options) {
510
511
  const conformance = planned.track === "conformance";
511
512
  // A code job can be steered while it regenerates: its target moves with the spec, inside what it regenerates.
512
513
  const steering = conformance ? undefined : createSteering(planned.step.tree, tip.moduleName);
513
- // Shown as its agent is given it: the job's own items, from whichever file they are written in - its imports too.
514
- const shown = changeDiffInFiles(agentChanges(planned.step.changes, planned.track), relative(projectDirOf(tip.plainFile), tip.plainFile) || basename(tip.plainFile), (diff) => whereInFiles(diff, state.chain));
514
+ // Shown as its agent is given it: the job's own items, from whichever file they are written in - its imports too, and
515
+ // for the conformance tests the test plain files of the functional specs whose scenarios it carries out.
516
+ const moduleFile = relative(projectDirOf(tip.plainFile), tip.plainFile) || basename(tip.plainFile);
517
+ const shown = changeDiffInFiles(agentChanges(planned.step.changes, planned.track), moduleFile, (diff) => whereInFiles(diff, state.chain), conformance ? { module: tip.moduleName, pathOf: testPlainPathOf(spec.tree, tip.moduleName, tip.plainFile, dirname(moduleFile)) } : undefined);
515
518
  const job = options.registry.start({
516
519
  kind: conformance ? "conformance" : "regenerate",
517
520
  summary: `${basename(tip.plainFile)} ${conformance ? "conformance: " : ""}${planned.title}`,
@@ -530,13 +533,13 @@ export function createWorkLoop(options) {
530
533
  run: async (onDelta) => {
531
534
  // The guardrail: a conformance job tests only code that is built - the code record, read as the job starts, has every functional spec it tests as it tests it.
532
535
  if (conformance) {
533
- const testing = [...planned.cases.add, ...planned.cases.patch, ...planned.cases.again].map((touched) => touched.frid);
534
- const missing = notBuilt(readRecord(tip.buildDir), planned.step.tree, testing);
536
+ const testing = [...planned.cases.add, ...planned.cases.patch, ...planned.cases.again];
537
+ const missing = notBuilt(readRecord(tip.buildDir), planned.step.tree, testing, tip.moduleName);
535
538
  if (missing.length > 0) {
536
539
  const record = relative(options.cwd, join(tip.buildDir, RECORD_FILE));
537
- const notBuiltText = `${missing.map((frid) => `fs ${frid}`).join(", ")} ${missing.length === 1 ? "is" : "are"} not built`;
540
+ const notBuiltText = `${refsOf(tip.moduleName, missing)} ${missing.length === 1 ? "is" : "are"} not built`;
538
541
  return {
539
- report: `Not started: this conformance pulse is to test ${testing.map((frid) => `fs ${frid}`).join(", ")}, and ${notBuiltText} - ` +
542
+ report: `Not started: this conformance pulse is to test ${refsOf(tip.moduleName, testing)}, and ${notBuiltText} - ` +
540
543
  `${record} does not have ${missing.length === 1 ? "it" : "them"} as the spec has ${missing.length === 1 ? "it" : "them"}. ` +
541
544
  "No conformance test is written for code that is not built. Tell the user, and see with drift which code pulse regenerates it.",
542
545
  details: { passed: false, landed: false, notBuilt: notBuiltText },
@@ -548,13 +551,14 @@ export function createWorkLoop(options) {
548
551
  const below = state.chain.find((entry) => entry.moduleName === tip.requires);
549
552
  const source = below === undefined ? null : readRecord(below.buildDir);
550
553
  if (below !== undefined && source !== null) {
554
+ // Its conformance tests come with the code, and what they are written for, as what it inherits.
551
555
  copyRequiredCode(below.buildDir, tip.buildDir, tip.moduleName, below.moduleName);
552
- // Its conformance tests come with the code, the ones below's: none of its own is written yet.
553
- if (existsSync(join(testsDirOf(state), RECORD_FILE)))
554
- writeTestsRecord(testsDirOf(state), {});
555
556
  }
556
557
  }
557
- // A conformance job is shown the spec whole - its test reqs and acceptance tests are its to write from.
558
+ // Nothing here yet, and nothing below: the codebase starts from its base folder, when the config names one.
559
+ if (tip.requires === null && !started(tip))
560
+ inStack(state, () => seedFromBaseFolder(tip.buildDir, tip.plainFile, tip.moduleName));
561
+ // A conformance job is shown the spec whole - its test reqs and scenarios are its to write from.
558
562
  const result = await (conformance ? runConformance : run)({
559
563
  plainFile: tip.plainFile,
560
564
  moduleName: tip.moduleName,
@@ -565,35 +569,7 @@ export function createWorkLoop(options) {
565
569
  cwd: options.cwd,
566
570
  ...(steering !== undefined ? { steering } : {}),
567
571
  }, onDelta);
568
- // Under conformance-tests: chosen or module, the chooser decides as the pulse's last stage - before it ends, so the
569
- // choice is made, and its conformance pulse scheduled, before anything can be built on the module. Under chosen, of
570
- // what this pulse regenerated; under module, once the module is regenerated, of all of it - and in which pulses.
571
- let chose;
572
- const when = chooseWhen(tip.plainFile);
573
- if (result.landed && planned.track === "code" && when !== undefined && options.onChoose !== undefined) {
574
- const now = chainSpec(state.chain);
575
- const candidates = when === "pulse" ? toChoose(state, planned, now) : codeWork(builtOf(state), now.tree).length === 0 ? moduleToChoose(state, now) : [];
576
- const left = budgetLeft(state, now);
577
- if (candidates.length > 0 && left === 0)
578
- chose = `conformance: the module's conformance budget is spent - none of fs ${candidates.map((touched) => touched.frid).join(", ")} is chosen`;
579
- else if (candidates.length > 0) {
580
- reportStage(`module ${tip.moduleName}: choosing its conformance tests`);
581
- const picked = await options.onChoose(tip.plainFile, candidates, left, when);
582
- // Within the budget, whatever was answered: the first that fit.
583
- const frids = picked === undefined ? [] : left !== undefined ? picked.frids.slice(0, left) : picked.frids;
584
- const groups = picked?.groups?.map((group) => group.filter((frid) => frids.includes(frid))).filter((group) => group.length > 0);
585
- if (frids.length > 0)
586
- schedule(state, frids, groups);
587
- const why = picked?.why !== undefined ? ` - ${picked.why.replace(/\s+/g, " ").trim()}` : "";
588
- chose =
589
- frids.length > 0
590
- ? `conformance: fs ${frids.join(", ")} chosen${groups !== undefined && groups.length > 1 ? `, in ${groups.length} pulses (${groups.map((group) => group.join(", ")).join(" / ")})` : ""}${why}`
591
- : `conformance: none of fs ${candidates.map((touched) => touched.frid).join(", ")} chosen${why}`;
592
- }
593
- if (chose !== undefined)
594
- reportActivity(` ${tip.moduleName}: ${chose}`);
595
- }
596
- const report = chose !== undefined ? `${result.report}\n\n${chose}` : result.report;
572
+ const report = result.report;
597
573
  // Released as its last stage, when the config says so: the module's next pulse starts on what is released.
598
574
  let released;
599
575
  if (result.landed && options.release !== undefined && configForSpec(tip.plainFile).releaseAfterPulse === true) {
@@ -647,7 +623,6 @@ export function createWorkLoop(options) {
647
623
  return;
648
624
  state.plan = undefined;
649
625
  state.conformancePlan = undefined;
650
- // What the person asked of it - conformance tests chosen or scheduled - stays: it is asked of the code regenerated.
651
626
  state.halted = undefined;
652
627
  state.conformanceHalted = undefined;
653
628
  state.startOver = undefined;
@@ -780,6 +755,11 @@ export function createWorkLoop(options) {
780
755
  .catch((error) => reportActivity(` the work loop: ${error instanceof Error ? error.message : String(error)}`));
781
756
  return ticking;
782
757
  };
758
+ // A testing script written in the background, or not: what waited for it is looked at again - started, or failed for want of it.
759
+ onScriptRunEnded(() => {
760
+ if (asked.size > 0 && options.dryRun !== true)
761
+ void tick();
762
+ });
783
763
  /** The files asked for, each with how: what the loop brings up, worked out again each look. */
784
764
  const asked = new Map();
785
765
  /**
@@ -840,19 +820,28 @@ export function createWorkLoop(options) {
840
820
  }
841
821
  const now = active();
842
822
  looking = now;
843
- for (const key of order) {
844
- const state = modules.get(key);
845
- // A codebase left behind - built on since - is not looked at, unless a job of it is still running; what it had
846
- // planned, or waited for, is no longer shown.
847
- if (now.has(state) || state.running !== undefined)
848
- await inStack(state, () => look(state));
849
- else {
850
- state.why = "left behind: the code is built on further up the chain, where changes to it are made";
851
- state.waitsFor = undefined;
852
- state.plan = undefined;
853
- state.conformancePlan = undefined;
823
+ // Why each codebase stands as it does, before this look: one that changed is told, as a plan that changed is - a
824
+ // module that starts or stops waiting moves no job, and would otherwise go unseen until something else did.
825
+ const whyBefore = new Map([...modules.values()].map((state) => [state, state.why]));
826
+ try {
827
+ for (const key of order) {
828
+ const state = modules.get(key);
829
+ // A codebase left behind - built on since - is not looked at, unless a job of it is still running; what it had
830
+ // planned, or waited for, is no longer shown.
831
+ if (now.has(state) || state.running !== undefined)
832
+ await inStack(state, () => look(state));
833
+ else {
834
+ state.why = "left behind: the code is built on further up the chain, where changes to it are made";
835
+ state.waitsFor = undefined;
836
+ state.plan = undefined;
837
+ state.conformancePlan = undefined;
838
+ }
854
839
  }
855
840
  }
841
+ finally {
842
+ if ([...modules.values()].some((state) => state.why !== whyBefore.get(state)))
843
+ options.registry.notify?.();
844
+ }
856
845
  };
857
846
  const look = async (state) => {
858
847
  const tip = tipEntry(state);
@@ -872,7 +861,14 @@ export function createWorkLoop(options) {
872
861
  }
873
862
  const specKey = keyOf(spec);
874
863
  state.seen = spec;
875
- promote(state, spec);
864
+ if (options.onSpec !== undefined) {
865
+ try {
866
+ options.onSpec({ chain: state.chain, tree: spec.tree, built: builtOf(state) });
867
+ }
868
+ catch {
869
+ /* what watches the spec never stops the loop */
870
+ }
871
+ }
876
872
  if (state.running !== undefined) {
877
873
  if (state.running.stopping === true)
878
874
  return;
@@ -933,8 +929,15 @@ export function createWorkLoop(options) {
933
929
  }
934
930
  state.halted = undefined;
935
931
  }
936
- const fresh = newlyRequired(state.chain);
937
932
  state.startOver = undefined;
933
+ // Built when functional specs were numbered, before func-ids: nothing is built on it - its chain starts over.
934
+ const old = oldCodebases(state.chain);
935
+ if (old.length > 0) {
936
+ state.startOver = { requires: [], codebases: old.map((entry) => entry.moduleName), old: true };
937
+ state.why = OLD_RECORDS_WHY(old.map((entry) => entry.moduleName));
938
+ return;
939
+ }
940
+ const fresh = newlyRequired(state.chain);
938
941
  if (fresh !== undefined && fresh.rebuild.some((entry) => entry.buildDir === tip.buildDir)) {
939
942
  const codebases = fresh.rebuild.map((entry) => entry.moduleName);
940
943
  state.startOver = { requires: fresh.modules, codebases };
@@ -949,6 +952,19 @@ export function createWorkLoop(options) {
949
952
  state.why = below.why;
950
953
  return;
951
954
  }
955
+ // A func-id or a scenario-id renamed, and nothing else with it: turri renames it in the records and relabels the tests
956
+ // itself, as one unit of work - no pulse, the code untouched. Failing, it is rolled back whole, and the codebase halts
957
+ // rather than build the functional spec anew.
958
+ const renamed = options.dryRun === true || readRecord(tip.buildDir) === null ? undefined : pureRenames(readRecord(tip.buildDir), readRecord(testsDirOf(state)), spec.tree, tip.moduleName);
959
+ if (renamed !== undefined && renamed.renames.length > 0) {
960
+ const made = makeRenames({ buildDir: tip.buildDir, testsDir: testsDirOf(state), moduleName: tip.moduleName, renamed, place: { project: projectRootOf(tip.buildDir), module: tip.moduleName } });
961
+ if (!made) {
962
+ state.halted = { why: `renaming ${renamed.renames.map(renameText).join(", ")} failed, and was rolled back - it is tried again once the spec changes, or the module is asked for again`, specKey };
963
+ state.why = state.halted.why;
964
+ record({ kind: "loop.halted", why: firstLine(state.halted.why, 300), track: "code" }, { project: projectRootOf(tip.buildDir), module: tip.moduleName });
965
+ return;
966
+ }
967
+ }
952
968
  // The module's code first, every job of it; then its conformance tests - once, for the module as built, with one
953
969
  // explorer and one run of the whole suite - and only then is anything built on it (waitingOn).
954
970
  const from = builtOf(state);
@@ -974,13 +990,19 @@ export function createWorkLoop(options) {
974
990
  }
975
991
  else {
976
992
  state.conformancePlan = undefined;
977
- state.together = undefined;
978
- state.conformanceGroups = undefined;
979
993
  }
980
994
  }
981
995
  if (!codeLeft) {
982
996
  state.plan = undefined;
983
- state.why = state.conformanceHalted?.why ?? UP_TO_DATE;
997
+ // Its scenarios untested for a step of adding tests it skipped: no conformance pulse starts - it waits on that step.
998
+ const gap = state.conformanceHalted === undefined && readRecord(tip.buildDir) !== null ? gapOf(state, spec) : undefined;
999
+ state.why =
1000
+ state.conformanceHalted?.why ??
1001
+ (gap === undefined
1002
+ ? UP_TO_DATE
1003
+ : gap.step === "conformance testing script" && awaitsScript(state, "conformance")
1004
+ ? SCRIPT_BEING_WRITTEN("conformance testing script")
1005
+ : `waiting for its ${gap.step}: ${gap.text}`);
984
1006
  return;
985
1007
  }
986
1008
  const fromKey = serializeTree(from);
@@ -993,6 +1015,10 @@ export function createWorkLoop(options) {
993
1015
  return;
994
1016
  }
995
1017
  }
1018
+ if (awaitsScript(state, "unit")) {
1019
+ state.why = SCRIPT_BEING_WRITTEN("unit testing script");
1020
+ return;
1021
+ }
996
1022
  state.why = undefined;
997
1023
  startJob(state, spec, from, state.plan.jobs[0]);
998
1024
  };
@@ -1024,43 +1050,48 @@ export function createWorkLoop(options) {
1024
1050
  ...(replanning === "code" ? [] : own(state.plan?.jobs)),
1025
1051
  ...(replanning === "conformance" ? [] : own(state.conformancePlan?.jobs)),
1026
1052
  ];
1027
- // A scheduled conformance job comes after the module's last code job - last, when none is planned.
1028
1053
  const lastCode = jobs.findLastIndex((job) => job.track === "code");
1029
- const after = (state.scheduled ?? []).map(() => lastCode);
1030
- const scheduledAt = (index) => (state.scheduled ?? []).flatMap((group, at) => after[at] !== index
1031
- ? []
1032
- : [{ title: group.specs.map((touched) => `fs ${touched.frid}`).join(", "), cases: { fresh: false, add: group.specs, patch: [], again: [], delete: [], mechanical: false }, track: "conformance", waitsFor: group.unbuilt }]);
1033
- // Being planned, nothing running: what is next - for the conformance tests, the groups chosen that it plans - not nothing.
1034
- const together = (state.together ?? []).flat().map((touched) => `fs ${touched.frid}`);
1035
1054
  const planningNow = planningTrack !== undefined
1036
1055
  ? [
1037
1056
  {
1038
- title: planningTrack === "conformance" ? (together.length > 0 ? together.join(", ") : "the conformance tests") : "the work",
1057
+ title: planningTrack === "conformance" ? "the conformance tests" : "the work",
1039
1058
  cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false },
1040
1059
  track: planningTrack,
1041
1060
  planning: true,
1042
1061
  },
1043
1062
  ]
1044
1063
  : [];
1045
- // The module's conformance tests, wanted for all of it while code is still to build: planned once it is built, shown now.
1064
+ // The conformance tests that follow the code still to build - of its functional specs with scenarios whose tests
1065
+ // are not written for them as they read now: planned once the code is built, shown now.
1046
1066
  const codeToBuild = lastCode !== -1 || planningTrack === "code";
1047
- const testReqs = (state.seen?.tree[TEST_REQUIREMENTS] ?? []).length > 0;
1048
- const plainFile = tipEntry(state).plainFile;
1049
- const wholeModule = (state.conformance !== undefined && state.conformance.chosen === undefined) || (configConformance(plainFile) && !configChosen(plainFile));
1050
- const afterCode = codeToBuild && testReqs && wholeModule && state.conformanceHalted === undefined && !jobs.some((job) => job.track === "conformance")
1051
- ? [{ title: "conformance tests", cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false }, track: "conformance", afterCode: true }]
1067
+ const seen = state.seen?.tree;
1068
+ // Not with a step of adding tests skipped: they are not planned until it is taken.
1069
+ const testReqs = (seen?.[TEST_REQUIREMENTS] ?? []).length > 0 &&
1070
+ codeToBuild &&
1071
+ testingGap({ moduleName: tipEntry(state).moduleName, plainFile: tipEntry(state).plainFile, tree: seen, buildDir: tipEntry(state).buildDir }) === undefined;
1072
+ // Those of the modules below too: the tip's suite holds the chain's.
1073
+ const tested = testedOf(state);
1074
+ const sections = [REQUIRED_FUNCTIONAL_SPECS, FUNCTIONAL_REQUIREMENTS];
1075
+ const written = new Set(sections.flatMap((section) => (tested[section] ?? []).map((item) => JSON.stringify(item))));
1076
+ const behind = sections.flatMap((section) => (seen?.[section] ?? []).filter((item) => hasScenarios(item) && !written.has(JSON.stringify(item))));
1077
+ const afterCode = codeToBuild && testReqs && behind.length > 0 && state.conformanceHalted === undefined && !jobs.some((job) => job.track === "conformance")
1078
+ ? [{ title: "conformance tests", cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false }, track: "conformance", afterCode: true, specs: behind.map((item) => `${item.module ?? tipEntry(state).moduleName}:${item.id}`) }]
1052
1079
  : [];
1053
1080
  const listed = [
1054
1081
  ...planningNow.map((entry) => ({ entry })),
1055
- ...jobs.flatMap((job, index) => [{ entry: { title: job.title, cases: job.cases, track: job.track }, job }, ...scheduledAt(index).map((entry) => ({ entry }))]),
1056
- ...scheduledAt(-1).map((entry) => ({ entry })),
1082
+ ...jobs.map((job) => ({ entry: { title: job.title, cases: job.cases, track: job.track }, job })),
1057
1083
  ...afterCode.map((entry) => ({ entry })),
1058
1084
  ];
1059
1085
  const planned = listed.map((item) => item.entry);
1060
1086
  const tip = tipEntry(state);
1087
+ // While a pulse runs, the renames made once it ends: shown as renames, never as a functional spec taken out and one added.
1088
+ const renames = state.running !== undefined && state.running.planned.track === "code" && seen !== undefined ? pureRenames(readRecord(tip.buildDir), readRecord(testsDirOf(state)), seen, tip.moduleName).renames : [];
1089
+ const base = baseOfState(state);
1061
1090
  const status = {
1062
1091
  module: tip.moduleName,
1063
1092
  plainFile: tip.plainFile,
1093
+ ...(base !== undefined ? { base } : {}),
1094
+ ...(renames.length > 0 ? { renames: renames.map(renameText) } : {}),
1064
1095
  ...(state.stack?.name !== undefined ? { stack: state.stack.name } : {}),
1065
1096
  ...(state.running !== undefined
1066
1097
  ? {
@@ -1076,7 +1107,8 @@ export function createWorkLoop(options) {
1076
1107
  ...(state.running === undefined && state.why !== undefined ? { why: state.why } : {}),
1077
1108
  ...(state.running === undefined && state.startOver !== undefined ? { startOver: state.startOver } : {}),
1078
1109
  };
1079
- return { status, jobs: (state.running !== undefined ? listed.slice(1) : listed).map((item) => item.job), scheduled: (state.running !== undefined ? listed.slice(1) : listed).map((item) => (item.entry.waitsFor !== undefined ? { title: item.entry.title, specs: item.entry.cases.add.map((touched) => touched.text) } : undefined)) };
1110
+ const shown = state.running !== undefined ? listed.slice(1) : listed;
1111
+ return { status, jobs: shown.map((item) => item.job), ahead: shown.map((item) => (item.entry.afterCode === true ? { title: item.entry.title, specs: item.entry.specs ?? [] } : undefined)) };
1080
1112
  };
1081
1113
  /**
1082
1114
  * Look after a codebase - its chain up to it - from now on; says its state. `pin`: its stack is worked out again - its
@@ -1125,6 +1157,9 @@ export function createWorkLoop(options) {
1125
1157
  state.halted = undefined;
1126
1158
  if (specChange !== true)
1127
1159
  state.held = undefined;
1160
+ // Asked for again: its conformance tests are written for every functional spec with scenarios, not only those conform named.
1161
+ if (specChange !== true)
1162
+ state.conformanceOnly = undefined;
1128
1163
  }
1129
1164
  await tick();
1130
1165
  return [...new Set(states)].map(statusOf);
@@ -1134,10 +1169,10 @@ export function createWorkLoop(options) {
1134
1169
  },
1135
1170
  async regenerateAgain(plainFile, given, request = {}) {
1136
1171
  return preparingFor(plainFile, async () => {
1137
- const frids = [...new Set(Array.isArray(given) ? given : [given])];
1138
1172
  const asked = resolve(options.cwd, plainFile);
1139
1173
  const own = resolveRequiresChain(asked, [], undefined, { allowNoFunctionalities: true });
1140
1174
  const module = tipOf(own).moduleName;
1175
+ const funcIds = funcIdsNamed(module, [...new Set(Array.isArray(given) ? given : [given])]);
1141
1176
  const chains = chainsFor(asked);
1142
1177
  for (const chain of chains) {
1143
1178
  const tip = tipOf(chain);
@@ -1152,33 +1187,32 @@ export function createWorkLoop(options) {
1152
1187
  continue;
1153
1188
  const section = tip.moduleName === module ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS;
1154
1189
  const items = record[section] ?? [];
1155
- const named = (item, index) => section === FUNCTIONAL_REQUIREMENTS ? String(index + 1) : item.module === module ? (item.frid ?? "") : "";
1156
- const taken = items.flatMap((item, index) => (frids.includes(named(item, index)) ? [named(item, index)] : []));
1190
+ const named = (item) => (section === FUNCTIONAL_REQUIREMENTS || item.module === module ? (item.id ?? "") : "");
1191
+ const taken = items.flatMap((item) => (funcIds.includes(named(item)) ? [named(item)] : []));
1157
1192
  if (taken.length === 0)
1158
1193
  continue;
1159
- for (const frid of taken)
1160
- found.add(frid);
1161
- const marked = items.map((item, index) => (frids.includes(named(item, index)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1194
+ for (const funcId of taken)
1195
+ found.add(funcId);
1196
+ const marked = items.map((item) => (funcIds.includes(named(item)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1162
1197
  writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
1163
- // Its conformance tests were written against the code being replaced: marked with it, they are written again -
1164
- // checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
1165
- if (section === FUNCTIONAL_REQUIREMENTS) {
1166
- const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
1167
- const tests = readRecord(testsDir);
1168
- const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
1169
- if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
1170
- const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1171
- writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
1172
- }
1198
+ // Its conformance tests were written against the code being replaced: marked with it, in the tests record of the
1199
+ // same codebase - the tip's suite holds the chain's - they are written again, checked against the new code, and
1200
+ // rewritten where they relied on what the old code chose, once it lands.
1201
+ const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
1202
+ const tests = readRecord(testsDir);
1203
+ const tested = tests?.[section] ?? [];
1204
+ if (tested.some((item) => taken.includes(named(item)))) {
1205
+ const again = tested.map((item) => (taken.includes(named(item)) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1206
+ writeTestsRecord(testsDir, { ...tests, [section]: again });
1173
1207
  }
1174
- recordHistory(tip.buildDir, `${module} functional spec${taken.length === 1 ? "" : "s"} ${taken.join(", ")} to regenerate again`);
1208
+ recordHistory(tip.buildDir, `${namesOf(module, taken)} to regenerate again`);
1175
1209
  }
1176
- const missing = frids.filter((frid) => !found.has(frid));
1210
+ const missing = funcIds.filter((funcId) => !found.has(funcId));
1177
1211
  if (found.size === 0)
1178
- throw new Error(`${module} has no functional spec ${missing.join(", ")} built anywhere, so there is nothing to regenerate again: regenerate builds what is not built.`);
1212
+ throw new Error(`${namesOf(module, missing)} ${missing.length === 1 ? "is" : "are"} not built anywhere, so there is nothing to regenerate again: regenerate builds what is not built.`);
1179
1213
  const statuses = await loop.bringUp(plainFile, request);
1180
1214
  if (missing.length > 0)
1181
- reportActivity(` ${module}: functional spec ${missing.join(", ")} is not built anywhere - nothing to regenerate again there`);
1215
+ reportActivity(` ${namesOf(module, missing)} ${missing.length === 1 ? "is" : "are"} not built anywhere - nothing to regenerate again there`);
1182
1216
  return statuses;
1183
1217
  });
1184
1218
  },
@@ -1190,7 +1224,7 @@ export function createWorkLoop(options) {
1190
1224
  // The codebase as the loop knows it, or - never asked for - as its records have it.
1191
1225
  const state = modules.get(tip.buildDir) ?? { chain: codebase, askedFile: file, guidance: [] };
1192
1226
  return inStack(state, () => {
1193
- const report = { module: tip.moduleName, plainFile: tip.plainFile, code: undefined, conformance: undefined, scheduled: [] };
1227
+ const report = { module: tip.moduleName, plainFile: tip.plainFile, code: undefined, conformance: undefined, ...(conformanceTestsOn(tip.plainFile) ? {} : { conformanceOff: true }) };
1194
1228
  let spec;
1195
1229
  try {
1196
1230
  spec = chainSpec(codebase);
@@ -1198,23 +1232,47 @@ export function createWorkLoop(options) {
1198
1232
  catch (error) {
1199
1233
  return { ...report, why: `its spec does not parse: ${error instanceof Error ? error.message : String(error)}` };
1200
1234
  }
1201
- const from = builtOf(state);
1235
+ // A pure rename is no work: shown as one, and the work worked out as though it were made.
1236
+ const record = readRecord(tip.buildDir);
1237
+ const renamed = record === null ? undefined : pureRenames(record, readRecord(testsDirOf(state)), spec.tree, tip.moduleName);
1238
+ if (renamed !== undefined && renamed.renames.length > 0)
1239
+ report.renames = renamed.renames.map(renameText);
1240
+ const from = renamed?.code !== undefined && renamed.code !== null ? codeTree(renamed.code) : builtOf(state);
1202
1241
  const fresh = tip.requires === null && !started(tip);
1242
+ // A codebase that requires nothing starts from its base folder: one changed since, or missing, is said.
1243
+ const base = tip.requires === null ? baseFolderDrift(tip.buildDir, tip.plainFile) : undefined;
1244
+ if (base !== undefined)
1245
+ report.baseFolder = base;
1203
1246
  const code = codeWork(from, spec.tree);
1204
1247
  report.code = code.length === 0 ? null : casesOf(from, code, { fresh, module: tip.moduleName });
1205
1248
  if (conformanceWanted(state, spec)) {
1206
- const tested = testedOf(state);
1207
- const tests = workOf("conformance", tested, state, spec);
1249
+ const tested = renamed?.tests ?? testedOf(state);
1250
+ // Against the code record as renamed, when a rename is to be made: the tests follow it.
1251
+ const tests = renamed !== undefined && renamed.renames.length > 0
1252
+ ? changesBetween(serializeTree(tested), serializeTree(conformanceTarget(renamed.code, tested, spec.tree)))
1253
+ : workOf("conformance", tested, state, spec, false);
1208
1254
  report.conformance = tests.length === 0 ? null : casesOf(tested, tests, { module: tip.moduleName });
1255
+ // Narrowed by conform to the functional specs it named: the others wait, and why is said.
1256
+ const only = state.conformanceOnly;
1257
+ if (only !== undefined && tests.length > 0)
1258
+ report.conformanceOnly = [...only.specs].map((key) => (key.startsWith(":") ? `${tip.moduleName}${key}` : key));
1209
1259
  }
1210
- report.scheduled = (state.scheduled ?? []).map((group) => ({ frids: group.specs.map((touched) => touched.frid), unbuilt: group.unbuilt }));
1211
1260
  if (state.running !== undefined)
1212
1261
  report.running = { jobId: state.running.jobId, title: state.running.planned.title };
1213
1262
  const halted = state.held?.why ?? state.halted?.why ?? state.conformanceHalted?.why;
1214
1263
  if (halted !== undefined)
1215
1264
  report.why = halted;
1265
+ else {
1266
+ // Its scenarios untested for a step of adding tests it skipped: said, as the loop says it.
1267
+ const gap = gapOf(state, spec);
1268
+ if (gap !== undefined)
1269
+ report.why = `waiting for its ${gap.step}: ${gap.text}`;
1270
+ }
1216
1271
  if (state.startOver !== undefined)
1217
1272
  report.startOver = state.startOver;
1273
+ const old = oldCodebases(codebase);
1274
+ if (old.length > 0)
1275
+ report.startOver = { requires: [], codebases: old.map((entry) => entry.moduleName), old: true };
1218
1276
  return report;
1219
1277
  });
1220
1278
  })));
@@ -1228,10 +1286,26 @@ export function createWorkLoop(options) {
1228
1286
  // a codebase built on it - by a module above - is disposable, and is deleted, a pulse running there stopped first;
1229
1287
  // the module above is regenerated from it once they pass. Only when there are tests to write: asked again for tests
1230
1288
  // it has, nothing is touched.
1231
- const wanted = new Set([...(request.frids ?? []), ...(request.groups ?? []).flat()]);
1232
- const tested = new Set((readRecord(conformanceDirFor(self.moduleName, modulesRoot(projectDirOf(self.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
1233
- const ownSpecs = (chainSpec(own).tree[FUNCTIONAL_REQUIREMENTS] ?? []).map((_, index) => String(index + 1));
1234
- const toWrite = (wanted.size > 0 ? [...wanted] : ownSpecs).some((frid) => !tested.has(frid));
1289
+ if (!conformanceTestsOn(self.plainFile)) {
1290
+ throw new Error(`Conformance tests are off for ${self.moduleName} (conformance-tests: false, or unset): nothing is conformance tested. ` +
1291
+ "They are turned on in the chat - by asking for them - or with conformance-tests: true in config.turri.yaml. Nothing was started.");
1292
+ }
1293
+ const named = request.funcIds === undefined ? undefined : funcIdsNamed(self.moduleName, [...new Set(request.funcIds)]);
1294
+ const ownSpec = chainSpec(own).tree;
1295
+ const withScenarios = testedFuncIds(ownSpec);
1296
+ // Refused before anything is touched: nothing to test, a functional spec not there, or one with no scenarios.
1297
+ if (withScenarios.length === 0) {
1298
+ throw new Error(`No functional spec of ${self.moduleName} has scenarios - none has a test plain file - so there is nothing to test: ` +
1299
+ "write the conformance or acceptance scenarios first, with change_plain, and their tests are written once the code is built. Nothing was started.");
1300
+ }
1301
+ if (named !== undefined && named.length > 0)
1302
+ askedSpecs(self.moduleName, ownSpec, named);
1303
+ // A step of adding tests skipped - its test reqs, or the conformance testing script for its stack: refused, naming it.
1304
+ const gap = testingGap({ moduleName: self.moduleName, plainFile: self.plainFile, tree: ownSpec, buildDir: self.buildDir });
1305
+ if (gap !== undefined)
1306
+ throw new Error(`${gap.text} Nothing was started.`);
1307
+ const tested = new Set((readRecord(conformanceDirFor(self.moduleName, modulesRoot(projectDirOf(self.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.id));
1308
+ const toWrite = (named !== undefined && named.length > 0 ? named : withScenarios).some((funcId) => !tested.has(funcId));
1235
1309
  let wentBack = false;
1236
1310
  if (readRecord(self.buildDir) !== null && toWrite) {
1237
1311
  const why = `${self.moduleName}'s conformance tests were asked for, and they come before anything is built on it`;
@@ -1262,40 +1336,20 @@ export function createWorkLoop(options) {
1262
1336
  const asking = tracked.find((state) => tipEntry(state).buildDir === self.buildDir);
1263
1337
  const states = asking !== undefined ? tracked.filter((state) => own.some((entry) => entry.buildDir === tipEntry(state).buildDir)) : tracked;
1264
1338
  const tip = asking ?? states.at(-1);
1265
- // In groups of the person's own - 1-3 and 4-6: every functional spec of them asked for, each group a pulse of its own.
1266
- const groups = request.groups?.map((group) => [...new Set(group)]).filter((group) => group.length > 0);
1267
- const named = [...(request.frids ?? []), ...(groups ?? []).flat()];
1268
- const asks = named.length > 0 ? [...new Set(named)] : undefined;
1339
+ const asks = named !== undefined && named.length > 0 ? named : undefined;
1269
1340
  // Its tests are written already, and a codebase built on it holds its code: nothing to write, nothing touched.
1270
1341
  if (asking === undefined && asks !== undefined && tip !== undefined) {
1271
- throw new Error(`${self.moduleName}'s conformance tests for fs ${asks.join(", ")} are written already. Nothing was scheduled.`);
1342
+ throw new Error(`${self.moduleName}'s conformance tests for ${namesOf(self.moduleName, asks)} are written already. Nothing was started.`);
1272
1343
  }
1273
- // Refused before anything is chosen: a functional spec the spec does not have is built by no code job, so its tests could never be written.
1274
- const specs = tip !== undefined && asks !== undefined ? scheduledSpecs(tip, asks) : undefined;
1275
- // On for the whole module already - asked for before, or by the config: asked again, nothing changes, and it is said.
1276
- const already = new Set(states.filter((state) => {
1277
- const file = tipEntry(state).plainFile;
1278
- return (state.conformance !== undefined && state.conformance.chosen === undefined) || inStack(state, () => configConformance(file) && !configChosen(file));
1279
- }));
1280
- // Only the module asked for: the modules below it keep their conformance tests as their config and choices have them.
1344
+ // Only the module asked for: one job for all of its tests, or one a functional spec, as this request asks; a halt cleared.
1281
1345
  for (const state of tip !== undefined ? [tip] : []) {
1282
- // The functional specs named are the module's own: they are scheduled in its codebase, beside those chosen before.
1283
- const named = specs;
1284
- const before = state.conformance;
1285
- // Asked for every one before, or now: every one.
1286
- const all = (before !== undefined && before.chosen === undefined) || named === undefined;
1287
- state.conformance = all ? {} : { chosen: before?.chosen ?? new Set() };
1288
- // Scheduled now, chosen once all of them are built - at once, those that are.
1289
- state.scheduled = all ? undefined : [...(state.scheduled ?? []), { specs: named, unbuilt: named.map((touched) => touched.frid) }];
1290
- // One job for all of them, or one a functional spec: as this request asks. Changed, the plan is made again.
1291
1346
  const separately = request.separately === true;
1292
1347
  if ((state.conformanceSeparately === true) !== separately)
1293
1348
  state.conformancePlan = undefined;
1294
1349
  state.conformanceSeparately = separately;
1295
- if (state === tip && groups !== undefined && groups.length > 0) {
1296
- state.conformanceGroups = [...(state.conformanceGroups ?? []), ...groups];
1297
- state.conformancePlan = undefined;
1298
- }
1350
+ // Named: their tests alone - the others left as they are until the module is asked for again, or conform names none.
1351
+ state.conformanceOnly = asks !== undefined ? namedOnly(state, asks) : undefined;
1352
+ state.conformancePlan = undefined;
1299
1353
  state.conformanceHalted = undefined;
1300
1354
  state.halted = undefined;
1301
1355
  state.held = undefined;
@@ -1303,10 +1357,15 @@ export function createWorkLoop(options) {
1303
1357
  await tick();
1304
1358
  return [...new Set(states)].map((state) => {
1305
1359
  const status = statusOf(state);
1306
- if (!already.has(state))
1360
+ const writing = status.running !== undefined || status.planned.some((job) => job.track === "conformance");
1361
+ if (state !== tip || writing)
1307
1362
  return status;
1308
- const built = status.planned.some((job) => job.afterCode === true) ? "they are planned into pulses once its code is built" : "every functional spec of it is tested";
1309
- return { ...status, conformanceNote: `conformance tests were on for the whole of ${status.module} already: ${built}` };
1363
+ return {
1364
+ ...status,
1365
+ conformanceNote: asks !== undefined
1366
+ ? `the conformance tests of ${namesOf(status.module, asks)} are written for ${asks.length === 1 ? "it" : "them"} as built: there is nothing to write`
1367
+ : `the conformance tests of ${status.module} are written for every functional spec with scenarios, as built: there is nothing to write`,
1368
+ };
1310
1369
  });
1311
1370
  },
1312
1371
  async startOver(plainFile, request = {}) {
@@ -1318,8 +1377,12 @@ export function createWorkLoop(options) {
1318
1377
  for (const entry of fresh?.rebuild ?? [])
1319
1378
  rebuild.set(entry.buildDir, { entry, requires: fresh.modules });
1320
1379
  }
1321
- if (rebuild.size === 0)
1380
+ if (rebuild.size === 0) {
1381
+ // Built when functional specs were numbered: the whole chain is regenerated from scratch.
1382
+ if ([...chainsFor(file), own].some((chain) => oldCodebases(chain).length > 0))
1383
+ return loop.fromScratch(plainFile, { ...request, chain: true });
1322
1384
  throw new Error(`Nothing of ${tipOf(own).moduleName} has to be started over: its code is built on every module it requires.`);
1385
+ }
1323
1386
  for (const { entry } of rebuild.values()) {
1324
1387
  if (modules.get(entry.buildDir)?.running !== undefined)
1325
1388
  throw new Error(`${entry.moduleName} has a pulse running: ask again once it is done.`);
@@ -1453,13 +1516,11 @@ export function createWorkLoop(options) {
1453
1516
  ];
1454
1517
  }
1455
1518
  return status.planned.map((job, index) => {
1456
- const planned = row(`planned-${tip.moduleName}-${index + 1}`, `${basename(tip.plainFile)} ${job.track === "conformance" ? "conformance: " : ""}${job.title}`, job.waitsFor !== undefined
1457
- ? `Scheduled, not started: the conformance tests of ${job.title}, once ${waitsText(job.waitsFor)} built - the code record has to have them first.`
1458
- : job.afterCode === true
1459
- ? "Not planned yet: the module's conformance tests are planned into pulses once its code is built."
1460
- : job.planning === true
1461
- ? `Being planned into pulses now: ${job.title}. The first starts as soon as the plan is made.`
1462
- : `Planned, not started: ${casesText(job.cases)}. It is planned again whenever the spec changes or a pulse before it ends.`, before !== undefined ? [before] : [], job.track === "conformance");
1519
+ const planned = row(`planned-${tip.moduleName}-${index + 1}`, `${basename(tip.plainFile)} ${job.track === "conformance" ? "conformance: " : ""}${job.title}`, job.afterCode === true
1520
+ ? `Not planned yet: the conformance tests${job.specs !== undefined && job.specs.length > 0 ? ` of ${job.specs.join(", ")}` : ""} are planned into pulses once the module's code is built.`
1521
+ : job.planning === true
1522
+ ? `Being planned into pulses now: ${job.title}. The first starts as soon as the plan is made.`
1523
+ : `Planned, not started: ${casesText(job.cases)}. It is planned again whenever the spec changes or a pulse before it ends.`, before !== undefined ? [before] : [], job.track === "conformance");
1463
1524
  if (job.planning === true)
1464
1525
  planned.planning = { since: state.planningSince ?? preparing.get(tip.moduleName)?.since ?? now };
1465
1526
  before = planned.id;
@@ -1481,7 +1542,7 @@ export function createWorkLoop(options) {
1481
1542
  codebases: order.map((key) => {
1482
1543
  const state = modules.get(key);
1483
1544
  const tip = tipEntry(state);
1484
- const { jobs, scheduled } = describe(state);
1545
+ const { jobs, ahead } = describe(state);
1485
1546
  const running = state.running;
1486
1547
  let next = running?.number ?? state.pulses ?? 0;
1487
1548
  const pulses = [
@@ -1490,10 +1551,10 @@ export function createWorkLoop(options) {
1490
1551
  : []),
1491
1552
  ...jobs.flatMap((job, index) => {
1492
1553
  const rowId = `planned-${tip.moduleName}-${index + 1}`;
1493
- // Scheduled ahead of its code: numbered among the rest, its functional specs by their text.
1494
- const ahead = scheduled[index];
1495
- if (ahead !== undefined)
1496
- return [{ number: (next += 1), title: ahead.title, track: "conformance", running: false, rowId, before: "", after: "", specs: ahead.specs }];
1554
+ // The conformance tests that follow the code still to build: numbered among the rest, their functional specs by name.
1555
+ const following = ahead[index];
1556
+ if (following !== undefined)
1557
+ return [{ number: (next += 1), title: following.title, track: "conformance", running: false, rowId, before: "", after: "", specs: following.specs }];
1497
1558
  return job === undefined ? [] : [{ number: (next += 1), title: job.title, track: job.track, running: false, rowId, before: job.step.before, after: job.step.after }];
1498
1559
  }),
1499
1560
  ];
@@ -1504,7 +1565,7 @@ export function createWorkLoop(options) {
1504
1565
  plainFile: tip.plainFile,
1505
1566
  buildDir: tip.buildDir,
1506
1567
  pulses,
1507
- conformance: state.conformance !== undefined || inStack(state, () => configConformance(tip.plainFile)),
1568
+ conformance: testedFuncIds(state.seen?.tree).length > 0,
1508
1569
  ...(state.planning !== undefined ? { planning: state.planning } : {}),
1509
1570
  ...(running === undefined && state.waitsFor !== undefined ? { waitsFor: state.waitsFor } : {}),
1510
1571
  ...(running === undefined && state.why !== undefined ? { why: state.why } : {}),
@@ -1531,14 +1592,36 @@ export function createWorkLoop(options) {
1531
1592
  };
1532
1593
  return loop;
1533
1594
  }
1595
+ /** The codebases of a chain built when functional specs were numbered, before func-ids: their code or tests record is old. */
1596
+ function oldCodebases(chain) {
1597
+ return chain.filter((entry) => isOldRecord(readRecord(entry.buildDir)) || isOldRecord(readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDirOf(entry.plainFile))))));
1598
+ }
1599
+ /** Why nothing is built on codebases with old records. */
1600
+ const OLD_RECORDS_WHY = (codebases) => `${codebases.join(", ")} ${codebases.length === 1 ? "was" : "were"} built when functional specs were numbered, before they had func-ids: nothing more is built on ${codebases.length === 1 ? "it" : "them"} - the whole chain is regenerated from scratch once you agree`;
1534
1601
  /** Why nothing runs in a codebase whose code - and conformance tests, where they are wanted - is built from its spec as it is. */
1535
1602
  export const UP_TO_DATE = "built from the ***plain as it is";
1536
1603
  /** Why a codebase with work left has no plan: the planner found none. */
1604
+ /** Why a codebase waits for a testing script being written in the background. */
1605
+ const SCRIPT_BEING_WRITTEN = (what) => `waiting for its ${what}, which is being written`;
1537
1606
  const NO_PLAN = "the planner found no way to cut the work into pulses that hold together";
1607
+ /**
1608
+ * Where the functional specs' test plain files of a module's chain are, by func-id and module - the module's own when none
1609
+ * is named - under `base`, the folder the module's file is named in: as the parse bound them, else where one is to be,
1610
+ * `<test-dir>/<module>/<func-id>.plain`.
1611
+ */
1612
+ function testPlainPathOf(tree, moduleName, plainFile, base) {
1613
+ const bound = Object.entries(tree[TEST_PLAIN_FILES] ?? {});
1614
+ const fallback = relative(dirname(plainFile), testDirOf(plainFile));
1615
+ return (funcId, module = moduleName) => {
1616
+ const name = bound.find(([, binding]) => binding.module === module && binding.func_id === funcId)?.[0] ?? join(fallback, module, `${funcId}.plain`);
1617
+ return join(base, name);
1618
+ };
1619
+ }
1538
1620
  /** A diff's lines placed in the .plain files of a chain - the tip's first - named as they are from the project. */
1539
1621
  function whereInFiles(diff, chain) {
1540
1622
  try {
1541
- const files = [...chain].reverse().flatMap((entry) => specFilesOf(entry.plainFile));
1623
+ // The test plain files too: a conformance pulse's diff has their lines.
1624
+ const files = [...chain].reverse().flatMap((entry) => [...specFilesOf(entry.plainFile), ...testPlainFilesOf(entry.plainFile)]);
1542
1625
  // A standard library template by its import path, not by where turri happens to be.
1543
1626
  const nameOf = (path) => (path.startsWith(`${STDLIB_ROOT}/stdlib/`) ? relative(STDLIB_ROOT, path) : relative(projectDirOf(chain.at(-1).plainFile), path) || basename(path));
1544
1627
  const unique = [...new Set(files)].map((path) => ({ name: nameOf(path), text: readFileSync(path, "utf8") }));
@@ -1548,18 +1631,69 @@ function whereInFiles(diff, chain) {
1548
1631
  return {};
1549
1632
  }
1550
1633
  }
1551
- /** The functional specs a scheduled conformance job waits for: "fs 2 is", "fs 2, fs 3 are". */
1552
- function waitsText(frids) {
1553
- return `${frids.map((frid) => `fs ${frid}`).join(", ")} ${frids.length === 1 ? "is" : "are"}`;
1634
+ /**
1635
+ * The conformance tests' work, a line a functional spec, each with its scenarios counted by case and why else its tests
1636
+ * are behind: "todo:add-task 1 added, 1 changed". What a case is follows from the tests record, by scenario-id: a
1637
+ * scenario added there is new to the tests, one changed reads otherwise, one removed is taken out - and a functional spec
1638
+ * whose code changed under scenarios unchanged has their tests run on the new code.
1639
+ */
1640
+ function conformanceLines(module, cases) {
1641
+ const name = (touched) => `${touched.module ?? module}:${touched.funcId}`;
1642
+ const why = { text: "its code changed", again: "its code was regenerated again", concept: "a concept it uses changed" };
1643
+ const counted = (spec) => {
1644
+ const parts = [
1645
+ ...(spec.added.length > 0 ? [`${spec.added.length} added`] : []),
1646
+ ...(spec.changed.length > 0 ? [`${spec.changed.length} changed`] : []),
1647
+ ...(spec.removed.length > 0 ? [`${spec.removed.length} removed`] : []),
1648
+ ...(spec.codeChanged !== undefined ? [`${why[spec.codeChanged]} - ${spec.unchanged.length} to run on it`] : []),
1649
+ ];
1650
+ return parts.length > 0 ? parts.join(", ") : "its text changed";
1651
+ };
1652
+ const rows = cases.scenarios !== undefined
1653
+ ? cases.scenarios.map((spec) => [name(spec), cases.delete.some((gone) => name(gone) === name(spec)) ? `taken out - its tests go (${spec.removed.length} removed)` : counted(spec)])
1654
+ : [
1655
+ ...cases.add.map((touched) => [name(touched), "scenarios to write tests for"]),
1656
+ ...cases.patch.map((touched) => [name(touched), "its text or its scenarios changed"]),
1657
+ ...cases.again.map((touched) => [name(touched), "its code was regenerated again"]),
1658
+ ...cases.delete.map((touched) => [name(touched), "taken out - its tests go"]),
1659
+ ];
1660
+ if (rows.length === 0)
1661
+ return [" the test reqs or what they are written against changed"];
1662
+ const width = Math.max(...rows.map(([name]) => name.length));
1663
+ return rows.map(([name, why]) => ` ${name.padEnd(width)} ${why}`);
1664
+ }
1665
+ /** Functional specs as a person names them: `module:func-id`, each. */
1666
+ export function namesOf(module, funcIds) {
1667
+ return funcIds.map((funcId) => `${module}:${funcId}`).join(", ");
1668
+ }
1669
+ /** Functional specs of a chain as a person names them: `module:func-id`, each - the codebase's own with `module`. */
1670
+ function refsOf(module, specs) {
1671
+ return specs.map((spec) => `${spec.module ?? module}:${spec.funcId}`).join(", ");
1672
+ }
1673
+ /**
1674
+ * The func-ids a person names in a module, each as `module:func-id` - a bare func-id, or another module's, is refused:
1675
+ * a func-id is unique in its module only.
1676
+ */
1677
+ export function funcIdsNamed(module, names) {
1678
+ return names.map((name) => {
1679
+ const at = name.lastIndexOf(":");
1680
+ if (at === -1)
1681
+ throw new Error(`Name the functional spec as ${module}:${name} - a func-id with its module. Nothing was done.`);
1682
+ const [named, funcId] = [name.slice(0, at), name.slice(at + 1)];
1683
+ if (named !== module)
1684
+ throw new Error(`${name} is not a functional spec of ${module}: name ${named}'s .plain file for it. Nothing was done.`);
1685
+ return funcId;
1686
+ });
1554
1687
  }
1555
- /** What a job does, in a few words: its cases. */
1688
+ /** What a job does, in a few words: its cases, each functional spec by its func-id - with its module, a module below's. */
1556
1689
  function casesText(cases) {
1690
+ const named = (specs) => specs.map((spec) => (spec.module !== undefined ? `${spec.module}:${spec.funcId}` : spec.funcId)).join(", ");
1557
1691
  const parts = [
1558
1692
  ...(cases.fresh ? ["sets the project up"] : []),
1559
- ...(cases.add.length > 0 ? [`adds ${cases.add.map((spec) => `fs ${spec.frid}`).join(", ")}`] : []),
1560
- ...(cases.patch.length > 0 ? [`changes ${cases.patch.map((spec) => `fs ${spec.frid}`).join(", ")}`] : []),
1561
- ...(cases.again.length > 0 ? [`regenerates ${cases.again.map((spec) => `fs ${spec.frid}`).join(", ")} again`] : []),
1562
- ...(cases.delete.length > 0 ? [`takes out ${cases.delete.map((spec) => `fs ${spec.frid}`).join(", ")}`] : []),
1693
+ ...(cases.add.length > 0 ? [`adds ${named(cases.add)}`] : []),
1694
+ ...(cases.patch.length > 0 ? [`changes ${named(cases.patch)}`] : []),
1695
+ ...(cases.again.length > 0 ? [`regenerates ${named(cases.again)} again`] : []),
1696
+ ...(cases.delete.length > 0 ? [`takes out ${named(cases.delete)}`] : []),
1563
1697
  ];
1564
1698
  return parts.length > 0 ? parts.join(", ") : "no functional spec - reqs or definitions only";
1565
1699
  }
@@ -1622,6 +1756,16 @@ export function describeModules(statuses, options = {}) {
1622
1756
  if (status.startOver === undefined)
1623
1757
  return [];
1624
1758
  const { requires, codebases } = status.startOver;
1759
+ if (status.startOver.old === true) {
1760
+ return options.for === "command line"
1761
+ ? [` turri regenerate ${basename(status.plainFile)} --start-over regenerates it from scratch, with its whole chain`]
1762
+ : [
1763
+ ` Ask the user whether to regenerate ${status.module} from scratch, with its whole chain, and say why: ${codebases.join(", ")} ` +
1764
+ `${codebases.length === 1 ? "was" : "were"} built when functional specs were numbered, before they had func-ids, so nothing more ` +
1765
+ "can be built on it - its code and conformance tests are taken out, kept in its history, and every module of the chain " +
1766
+ `is built again from the bottom. Only if they agree, call regenerate with ${basename(status.plainFile)} and from_scratch: "chain".`,
1767
+ ];
1768
+ }
1625
1769
  return options.for === "command line"
1626
1770
  ? [` turri regenerate ${basename(status.plainFile)} --start-over starts ${codebases.join(", ")} over`]
1627
1771
  : [
@@ -1643,13 +1787,13 @@ export function describeModules(statuses, options = {}) {
1643
1787
  for (const why of status.running.steered ?? [])
1644
1788
  lines.push(` ⇢ steered: ${why} - its agent was told, and it lands the ***plain as it is now there`);
1645
1789
  }
1790
+ for (const rename of status.renames ?? [])
1791
+ lines.push(` renames ${rename} once the running pulse ends - the records and the tests' labels, nothing regenerated`);
1646
1792
  status.planned.forEach((job, index) => lines.push(job.afterCode === true
1647
- ? ` ${index + 1}. conformance tests - planned once the code is built`
1648
- : job.waitsFor !== undefined
1649
- ? ` ${index + 1}. conformance tests: ${job.title} - scheduled, starts once ${waitsText(job.waitsFor)} built`
1650
- : job.planning === true
1651
- ? ` ${index + 1}. ${job.track === "conformance" ? "conformance tests: " : ""}${job.title} - being planned into pulses`
1652
- : ` ${index + 1}. ${job.track === "conformance" ? "conformance tests: " : ""}${job.title} - ${casesText(job.cases)}`));
1793
+ ? ` ${index + 1}. conformance tests${job.specs !== undefined && job.specs.length > 0 ? ` of ${job.specs.join(", ")}` : ""} - planned once the code is built`
1794
+ : job.planning === true
1795
+ ? ` ${index + 1}. ${job.track === "conformance" ? "conformance tests: " : ""}${job.title} - being planned into pulses`
1796
+ : ` ${index + 1}. ${job.track === "conformance" ? "conformance tests: " : ""}${job.title} - ${casesText(job.cases)}`));
1653
1797
  if (status.planNotes !== undefined)
1654
1798
  lines.push(" what the plan did not do as asked:", ...status.planNotes.map((note) => ` - ${note}`));
1655
1799
  if (status.why !== undefined)
@@ -1675,32 +1819,21 @@ const defaultConformanceRunner = async (job, onDelta) => {
1675
1819
  applied: "already applied",
1676
1820
  spec: job.spec,
1677
1821
  cases: job.planned.cases,
1822
+ recorded: job.planned.step.tree,
1678
1823
  ...(job.model !== undefined ? { model: job.model } : {}),
1679
1824
  onDelta,
1680
1825
  });
1681
1826
  if (result.passed) {
1682
- writeTestsRecord(result.conformanceDir, keepAgainMarks(job.planned.step.tree, job.planned.cases, job.moduleName, result.conformance.rewritten ?? []));
1827
+ writeTestsRecord(result.conformanceDir, job.planned.step.tree);
1683
1828
  landWork(job.buildDir, `conformance tests: ${job.planned.title}`);
1684
1829
  }
1685
1830
  return { landed: result.passed, report: describeSpecDiffConformance(result), ...(result.specGaps !== undefined ? { specGaps: result.specGaps } : {}) };
1686
1831
  };
1687
1832
  /** The functional specs a pulse covers, by case, each with the module whose spec it is: a host's to light and mark. */
1688
1833
  export function pulseCases(cases, moduleName) {
1689
- const of = (specs) => specs.map((spec) => ({ module: spec.module ?? moduleName, frid: spec.frid }));
1834
+ const of = (specs) => specs.map((spec) => ({ module: spec.module ?? moduleName, funcId: spec.funcId }));
1690
1835
  return { add: of(cases.add), patch: of(cases.patch), again: of(cases.again), delete: of(cases.delete) };
1691
1836
  }
1692
- /**
1693
- * What a conformance job records its tests are written for: its step's tree, but a functional spec of the module it
1694
- * regenerates again keeps its mark unless the run wrote its tests - tests that passed as they were are still the old
1695
- * code's, and still conformance work.
1696
- */
1697
- export function keepAgainMarks(tree, cases, moduleName, rewritten) {
1698
- const stale = new Set(cases.again.filter((spec) => (spec.module ?? moduleName) === moduleName && !rewritten.includes(spec.frid)).map((spec) => spec.frid));
1699
- if (stale.size === 0)
1700
- return tree;
1701
- const items = tree[FUNCTIONAL_REQUIREMENTS] ?? [];
1702
- return { ...tree, [FUNCTIONAL_REQUIREMENTS]: items.map((item) => (item.frid !== undefined && stale.has(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item)) };
1703
- }
1704
1837
  /**
1705
1838
  * A module's first pulse when it builds one functional spec - the project set up and the smallest thing that runs: it lands on its
1706
1839
  * unit tests alone, with no review. A reviewer, and an arbiter after it, on a scaffold is time spent before anything can