@turri-dev/cli 0.1.27 → 0.2.0

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 +70 -14
  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 -13
  95. package/dist/regenerate/conformance.js +119 -82
  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 +62 -43
  104. package/dist/regenerate/loop.js +472 -341
  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 +71 -28
  135. package/dist/regenerate/test-kinds.js +110 -63
  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 +87 -52
  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 +23 -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,173 +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. Off in the config and not asked for: those tested already alone - their tests
240
- * kept up to date, and none written for any other.
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.
241
277
  */
242
- const chosenOf = (state, spec, tests) => {
243
- const named = state.conformance?.chosen;
244
- const choosing = configChosen(tipEntry(state).plainFile);
245
- const tested = new Set((tests?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
246
- if (state.conformance === undefined && !configConformance(tipEntry(state).plainFile))
247
- return (frid) => tested.has(frid);
248
- if (!choosing && (state.conformance === undefined || named === undefined || configConformance(tipEntry(state).plainFile)))
249
- return undefined;
250
- if (choosing && state.conformance !== undefined && named === undefined)
251
- return undefined;
252
- const specs = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
253
- const accepted = (frid) => (specs[Number(frid) - 1]?.[ACCEPTANCE_TESTS] ?? []).length > 0;
254
- return (frid) => tested.has(frid) || named?.has(frid) === true || (choosing && accepted(frid));
255
- };
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;
256
280
  /**
257
- * Under `conformance-tests: chosen`: the functional specs a code job built or changed that no conformance test is
258
- * 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.
259
284
  */
260
- const toChoose = (state, planned, spec) => {
261
- if (planned.track !== "code" || !configChosen(tipEntry(state).plainFile))
262
- return [];
263
- const chosen = chosenOf(state, spec, readRecord(testsDirOf(state)));
264
- if (chosen === undefined)
265
- return [];
266
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
267
- return [...planned.cases.add, ...planned.cases.patch].filter((touched) => !chosen(touched.frid) && !scheduled.has(touched.frid));
268
- };
269
- /**
270
- * Under `conformance-budget:`: how many more of the module's functional specs may be chosen - the budget less every
271
- * one chosen for it already, tested or scheduled, whenever that was; those with acceptance tests do not count. Undefined:
272
- * no budget.
273
- */
274
- const budgetLeft = (state, spec) => {
275
- const budget = configForSpec(tipEntry(state).plainFile).conformanceBudget;
276
- if (budget === undefined)
277
- return undefined;
278
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
279
- const tests = readRecord(testsDirOf(state));
280
- const tested = new Set((tests?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
281
- const named = state.conformance?.chosen ?? new Set();
282
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
283
- const used = items.filter((item, index) => {
284
- const frid = String(index + 1);
285
- return (item[ACCEPTANCE_TESTS] ?? []).length === 0 && (tested.has(frid) || named.has(frid) || scheduled.has(frid));
286
- }).length;
287
- return Math.max(0, budgetFor(budget, items.length) - used);
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 });
288
288
  };
289
289
  /**
290
- * Under `conformance-tests: chosen`: every functional spec of the module that is built and that no conformance test
291
- * is for, chosen or scheduled - the module's whole choice, once its code is built.
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.
292
292
  */
293
- const moduleToChoose = (state, spec) => {
294
- if (!configChosen(tipEntry(state).plainFile))
295
- return [];
296
- const chosen = chosenOf(state, spec, readRecord(testsDirOf(state)));
297
- if (chosen === undefined)
298
- return [];
299
- const scheduled = new Set((state.scheduled ?? []).flatMap((group) => group.specs.map((touched) => touched.frid)));
300
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
301
- const frids = items.map((_, index) => String(index + 1));
302
- const unbuilt = new Set(notBuilt(readRecord(tipEntry(state).buildDir), spec.tree, frids));
303
- return frids
304
- .filter((frid) => !unbuilt.has(frid) && !chosen(frid) && !scheduled.has(frid))
305
- .map((frid) => ({ frid, text: items[Number(frid) - 1].markdown.trim().split("\n")[0].replace(/^-\s*/, "").trim() }));
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
+ }
306
303
  };
307
304
  /**
308
- * The groups scheduled whose functional specs are all built, as the spec has them: chosen, so their tests are
309
- * written. A functional spec taken out of the spec is dropped from its group.
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.
310
307
  */
311
- const promote = (state, spec) => {
312
- const chosen = state.conformance?.chosen;
313
- if (state.scheduled === undefined || chosen === undefined)
314
- return;
315
- const count = (spec.tree[FUNCTIONAL_REQUIREMENTS] ?? []).length;
316
- const code = readRecord(tipEntry(state).buildDir);
317
- state.scheduled = state.scheduled.flatMap((group) => {
318
- const specs = group.specs.filter((touched) => Number(touched.frid) <= count);
319
- const unbuilt = notBuilt(code, spec.tree, specs.map((touched) => touched.frid));
320
- if (specs.length === 0)
321
- return [];
322
- if (unbuilt.length > 0)
323
- return [{ specs, unbuilt }];
324
- for (const touched of specs)
325
- chosen.add(touched.frid);
326
- state.together = [...(state.together ?? []), specs];
327
- // What the tests are to be written for changed: planned again - after the running job, when one runs.
328
- state.conformancePlan = undefined;
329
- return [];
330
- });
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;
331
312
  };
332
313
  /**
333
- * Whether a codebase's conformance tests are written: asked for, or on in the config - with test reqs to write them
334
- * from, and code to test. Off and not asked for, the tests there are are still kept up to date: wanted when a
335
- * functional spec that has conformance tests was added, changed, regenerated again or taken out in the code - the
336
- * work then is for those alone (chosenOf). Work for the test reqs or definitions alone does not start it.
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.
337
316
  */
338
- const conformanceWanted = (state, spec) => {
339
- if ((spec.tree[TEST_REQUIREMENTS] ?? []).length === 0 || readRecord(tipEntry(state).buildDir) === null)
340
- return false;
341
- if (state.conformance !== undefined || configConformance(tipEntry(state).plainFile))
342
- return true;
343
- const tested = testedOf(state);
344
- const cases = casesOf(tested, workOf("conformance", tested, state, spec), { module: tipEntry(state).moduleName });
345
- return cases.add.length + cases.patch.length + cases.again.length + cases.delete.length > 0;
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;
346
325
  };
347
326
  /**
348
- * The functional specs a conformance job is scheduled for, as the spec has them now - refused when one is not in
349
- * 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.
350
330
  */
351
- const scheduledSpecs = (state, frids) => {
352
- const tip = tipEntry(state);
353
- const spec = chainSpec(state.chain);
354
- const items = spec.tree[FUNCTIONAL_REQUIREMENTS] ?? [];
355
- const unknown = frids.filter((frid) => !/^[1-9]\d*$/.test(frid) || Number(frid) > items.length);
356
- if (unknown.length > 0) {
357
- throw new Error(`${tip.moduleName} has ${items.length} functional spec${items.length === 1 ? "" : "s"}, and ${unknown.map((frid) => `fs ${frid}`).join(", ")} ` +
358
- `${unknown.length === 1 ? "is not one of them" : "are not among them"}: no code pulse builds ${unknown.length === 1 ? "it" : "them"}, so no conformance ` +
359
- "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
+ }
360
341
  }
361
- if ((spec.tree[TEST_REQUIREMENTS] ?? []).length === 0) {
362
- 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];
363
361
  }
364
- return frids.map((frid) => ({ frid, text: items[Number(frid) - 1].markdown.trim().split("\n")[0].replace(/^-\s*/, "").trim() }));
362
+ return narrowed;
365
363
  };
366
- /** 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. */
367
- const schedule = (state, frids, groups) => {
368
- const before = state.conformance;
369
- // Every one is tested already: nothing to add.
370
- if (before !== undefined && before.chosen === undefined)
371
- return;
372
- let specs;
373
- try {
374
- specs = scheduledSpecs(state, frids);
375
- }
376
- catch (error) {
377
- reportActivity(` ${tipEntry(state).moduleName}: the conformance tests chosen were not scheduled - ${error instanceof Error ? error.message : String(error)}`);
378
- return;
379
- }
380
- state.conformance = { chosen: before?.chosen ?? new Set() };
381
- state.scheduled = [...(state.scheduled ?? []), { specs, unbuilt: specs.map((touched) => touched.frid) }];
382
- if (groups !== undefined && groups.length > 0) {
383
- state.conformanceGroups = [...(state.conformanceGroups ?? []), ...groups];
384
- 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));
385
371
  }
372
+ return { specs: new Set(funcIds.map((id) => identityOf({ id }))), read };
386
373
  };
387
- /** The work of a track, from where it is built from to what it is to be. */
388
- 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)));
389
376
  /** The codebases brought up in this look: one left behind is never waited for. */
390
377
  let looking = new Set();
391
378
  /**
@@ -408,8 +395,6 @@ export function createWorkLoop(options) {
408
395
  return waiting("the code it is built on is not built yet");
409
396
  if (below.conformanceHalted !== undefined)
410
397
  return waiting(`its conformance tests did not pass - ${below.conformanceHalted.why}`);
411
- if ((below.scheduled ?? []).length > 0)
412
- return waiting("the conformance tests chosen for it are not written yet");
413
398
  if (conformanceWanted(below, spec) && workOf("conformance", testedOf(below), below, spec).length > 0)
414
399
  return waiting("its conformance tests are not written yet");
415
400
  }
@@ -476,9 +461,11 @@ export function createWorkLoop(options) {
476
461
  // How big a pulse may be: the module's config, else the defaults.
477
462
  const config = configForSpec(tip.plainFile);
478
463
  const limits = { specs: config.maxSpecsPerPulse ?? MAX_SPECS_PER_JOB, lines: config.maxLinesPerPulse ?? MAX_LINES_PER_JOB };
479
- // 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.
480
- const groups = track === "conformance" && state.conformanceGroups !== undefined && state.conformanceGroups.length > 0 ? state.conformanceGroups : undefined;
481
- 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 } : {}) };
482
469
  const model = typeof options.plannerModel === "function" ? options.plannerModel() : options.plannerModel;
483
470
  const agents = options.planner === false ? undefined : (options.planner ?? llmChunkAgents({ cwd: options.cwd, model: model ?? state.model, label: `${basename(tip.plainFile)} planner` }));
484
471
  reportStage(`${tip.moduleName}: planning the ${track === "code" ? "work" : "conformance tests"} into pulses`);
@@ -524,8 +511,10 @@ export function createWorkLoop(options) {
524
511
  const conformance = planned.track === "conformance";
525
512
  // A code job can be steered while it regenerates: its target moves with the spec, inside what it regenerates.
526
513
  const steering = conformance ? undefined : createSteering(planned.step.tree, tip.moduleName);
527
- // Shown as its agent is given it: the job's own items, from whichever file they are written in - its imports too.
528
- 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);
529
518
  const job = options.registry.start({
530
519
  kind: conformance ? "conformance" : "regenerate",
531
520
  summary: `${basename(tip.plainFile)} ${conformance ? "conformance: " : ""}${planned.title}`,
@@ -544,13 +533,13 @@ export function createWorkLoop(options) {
544
533
  run: async (onDelta) => {
545
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.
546
535
  if (conformance) {
547
- const testing = [...planned.cases.add, ...planned.cases.patch, ...planned.cases.again].map((touched) => touched.frid);
548
- 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);
549
538
  if (missing.length > 0) {
550
539
  const record = relative(options.cwd, join(tip.buildDir, RECORD_FILE));
551
- 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`;
552
541
  return {
553
- 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} - ` +
554
543
  `${record} does not have ${missing.length === 1 ? "it" : "them"} as the spec has ${missing.length === 1 ? "it" : "them"}. ` +
555
544
  "No conformance test is written for code that is not built. Tell the user, and see with drift which code pulse regenerates it.",
556
545
  details: { passed: false, landed: false, notBuilt: notBuiltText },
@@ -562,13 +551,14 @@ export function createWorkLoop(options) {
562
551
  const below = state.chain.find((entry) => entry.moduleName === tip.requires);
563
552
  const source = below === undefined ? null : readRecord(below.buildDir);
564
553
  if (below !== undefined && source !== null) {
554
+ // Its conformance tests come with the code, and what they are written for, as what it inherits.
565
555
  copyRequiredCode(below.buildDir, tip.buildDir, tip.moduleName, below.moduleName);
566
- // Its conformance tests come with the code, the ones below's: none of its own is written yet.
567
- if (existsSync(join(testsDirOf(state), RECORD_FILE)))
568
- writeTestsRecord(testsDirOf(state), {});
569
556
  }
570
557
  }
571
- // 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.
572
562
  const result = await (conformance ? runConformance : run)({
573
563
  plainFile: tip.plainFile,
574
564
  moduleName: tip.moduleName,
@@ -579,35 +569,7 @@ export function createWorkLoop(options) {
579
569
  cwd: options.cwd,
580
570
  ...(steering !== undefined ? { steering } : {}),
581
571
  }, onDelta);
582
- // Under conformance-tests: chosen or module, the chooser decides as the pulse's last stage - before it ends, so the
583
- // choice is made, and its conformance pulse scheduled, before anything can be built on the module. Under chosen, of
584
- // what this pulse regenerated; under module, once the module is regenerated, of all of it - and in which pulses.
585
- let chose;
586
- const when = chooseWhen(tip.plainFile);
587
- if (result.landed && planned.track === "code" && when !== undefined && options.onChoose !== undefined) {
588
- const now = chainSpec(state.chain);
589
- const candidates = when === "pulse" ? toChoose(state, planned, now) : codeWork(builtOf(state), now.tree).length === 0 ? moduleToChoose(state, now) : [];
590
- const left = budgetLeft(state, now);
591
- if (candidates.length > 0 && left === 0)
592
- chose = `conformance: the module's conformance budget is spent - none of fs ${candidates.map((touched) => touched.frid).join(", ")} is chosen`;
593
- else if (candidates.length > 0) {
594
- reportStage(`module ${tip.moduleName}: choosing its conformance tests`);
595
- const picked = await options.onChoose(tip.plainFile, candidates, left, when);
596
- // Within the budget, whatever was answered: the first that fit.
597
- const frids = picked === undefined ? [] : left !== undefined ? picked.frids.slice(0, left) : picked.frids;
598
- const groups = picked?.groups?.map((group) => group.filter((frid) => frids.includes(frid))).filter((group) => group.length > 0);
599
- if (frids.length > 0)
600
- schedule(state, frids, groups);
601
- const why = picked?.why !== undefined ? ` - ${picked.why.replace(/\s+/g, " ").trim()}` : "";
602
- chose =
603
- frids.length > 0
604
- ? `conformance: fs ${frids.join(", ")} chosen${groups !== undefined && groups.length > 1 ? `, in ${groups.length} pulses (${groups.map((group) => group.join(", ")).join(" / ")})` : ""}${why}`
605
- : `conformance: none of fs ${candidates.map((touched) => touched.frid).join(", ")} chosen${why}`;
606
- }
607
- if (chose !== undefined)
608
- reportActivity(` ${tip.moduleName}: ${chose}`);
609
- }
610
- const report = chose !== undefined ? `${result.report}\n\n${chose}` : result.report;
572
+ const report = result.report;
611
573
  // Released as its last stage, when the config says so: the module's next pulse starts on what is released.
612
574
  let released;
613
575
  if (result.landed && options.release !== undefined && configForSpec(tip.plainFile).releaseAfterPulse === true) {
@@ -661,7 +623,6 @@ export function createWorkLoop(options) {
661
623
  return;
662
624
  state.plan = undefined;
663
625
  state.conformancePlan = undefined;
664
- // What the person asked of it - conformance tests chosen or scheduled - stays: it is asked of the code regenerated.
665
626
  state.halted = undefined;
666
627
  state.conformanceHalted = undefined;
667
628
  state.startOver = undefined;
@@ -794,6 +755,11 @@ export function createWorkLoop(options) {
794
755
  .catch((error) => reportActivity(` the work loop: ${error instanceof Error ? error.message : String(error)}`));
795
756
  return ticking;
796
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
+ });
797
763
  /** The files asked for, each with how: what the loop brings up, worked out again each look. */
798
764
  const asked = new Map();
799
765
  /**
@@ -854,19 +820,28 @@ export function createWorkLoop(options) {
854
820
  }
855
821
  const now = active();
856
822
  looking = now;
857
- for (const key of order) {
858
- const state = modules.get(key);
859
- // A codebase left behind - built on since - is not looked at, unless a job of it is still running; what it had
860
- // planned, or waited for, is no longer shown.
861
- if (now.has(state) || state.running !== undefined)
862
- await inStack(state, () => look(state));
863
- else {
864
- state.why = "left behind: the code is built on further up the chain, where changes to it are made";
865
- state.waitsFor = undefined;
866
- state.plan = undefined;
867
- 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
+ }
868
839
  }
869
840
  }
841
+ finally {
842
+ if ([...modules.values()].some((state) => state.why !== whyBefore.get(state)))
843
+ options.registry.notify?.();
844
+ }
870
845
  };
871
846
  const look = async (state) => {
872
847
  const tip = tipEntry(state);
@@ -886,7 +861,14 @@ export function createWorkLoop(options) {
886
861
  }
887
862
  const specKey = keyOf(spec);
888
863
  state.seen = spec;
889
- 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
+ }
890
872
  if (state.running !== undefined) {
891
873
  if (state.running.stopping === true)
892
874
  return;
@@ -947,8 +929,15 @@ export function createWorkLoop(options) {
947
929
  }
948
930
  state.halted = undefined;
949
931
  }
950
- const fresh = newlyRequired(state.chain);
951
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);
952
941
  if (fresh !== undefined && fresh.rebuild.some((entry) => entry.buildDir === tip.buildDir)) {
953
942
  const codebases = fresh.rebuild.map((entry) => entry.moduleName);
954
943
  state.startOver = { requires: fresh.modules, codebases };
@@ -963,6 +952,19 @@ export function createWorkLoop(options) {
963
952
  state.why = below.why;
964
953
  return;
965
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
+ }
966
968
  // The module's code first, every job of it; then its conformance tests - once, for the module as built, with one
967
969
  // explorer and one run of the whole suite - and only then is anything built on it (waitingOn).
968
970
  const from = builtOf(state);
@@ -988,13 +990,19 @@ export function createWorkLoop(options) {
988
990
  }
989
991
  else {
990
992
  state.conformancePlan = undefined;
991
- state.together = undefined;
992
- state.conformanceGroups = undefined;
993
993
  }
994
994
  }
995
995
  if (!codeLeft) {
996
996
  state.plan = undefined;
997
- 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}`);
998
1006
  return;
999
1007
  }
1000
1008
  const fromKey = serializeTree(from);
@@ -1007,6 +1015,10 @@ export function createWorkLoop(options) {
1007
1015
  return;
1008
1016
  }
1009
1017
  }
1018
+ if (awaitsScript(state, "unit")) {
1019
+ state.why = SCRIPT_BEING_WRITTEN("unit testing script");
1020
+ return;
1021
+ }
1010
1022
  state.why = undefined;
1011
1023
  startJob(state, spec, from, state.plan.jobs[0]);
1012
1024
  };
@@ -1038,43 +1050,48 @@ export function createWorkLoop(options) {
1038
1050
  ...(replanning === "code" ? [] : own(state.plan?.jobs)),
1039
1051
  ...(replanning === "conformance" ? [] : own(state.conformancePlan?.jobs)),
1040
1052
  ];
1041
- // A scheduled conformance job comes after the module's last code job - last, when none is planned.
1042
1053
  const lastCode = jobs.findLastIndex((job) => job.track === "code");
1043
- const after = (state.scheduled ?? []).map(() => lastCode);
1044
- const scheduledAt = (index) => (state.scheduled ?? []).flatMap((group, at) => after[at] !== index
1045
- ? []
1046
- : [{ 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 }]);
1047
- // Being planned, nothing running: what is next - for the conformance tests, the groups chosen that it plans - not nothing.
1048
- const together = (state.together ?? []).flat().map((touched) => `fs ${touched.frid}`);
1049
1054
  const planningNow = planningTrack !== undefined
1050
1055
  ? [
1051
1056
  {
1052
- title: planningTrack === "conformance" ? (together.length > 0 ? together.join(", ") : "the conformance tests") : "the work",
1057
+ title: planningTrack === "conformance" ? "the conformance tests" : "the work",
1053
1058
  cases: { fresh: false, add: [], patch: [], again: [], delete: [], mechanical: false },
1054
1059
  track: planningTrack,
1055
1060
  planning: true,
1056
1061
  },
1057
1062
  ]
1058
1063
  : [];
1059
- // 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.
1060
1066
  const codeToBuild = lastCode !== -1 || planningTrack === "code";
1061
- const testReqs = (state.seen?.tree[TEST_REQUIREMENTS] ?? []).length > 0;
1062
- const plainFile = tipEntry(state).plainFile;
1063
- const wholeModule = (state.conformance !== undefined && state.conformance.chosen === undefined) || (configConformance(plainFile) && !configChosen(plainFile));
1064
- const afterCode = codeToBuild && testReqs && wholeModule && state.conformanceHalted === undefined && !jobs.some((job) => job.track === "conformance")
1065
- ? [{ 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}`) }]
1066
1079
  : [];
1067
1080
  const listed = [
1068
1081
  ...planningNow.map((entry) => ({ entry })),
1069
- ...jobs.flatMap((job, index) => [{ entry: { title: job.title, cases: job.cases, track: job.track }, job }, ...scheduledAt(index).map((entry) => ({ entry }))]),
1070
- ...scheduledAt(-1).map((entry) => ({ entry })),
1082
+ ...jobs.map((job) => ({ entry: { title: job.title, cases: job.cases, track: job.track }, job })),
1071
1083
  ...afterCode.map((entry) => ({ entry })),
1072
1084
  ];
1073
1085
  const planned = listed.map((item) => item.entry);
1074
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);
1075
1090
  const status = {
1076
1091
  module: tip.moduleName,
1077
1092
  plainFile: tip.plainFile,
1093
+ ...(base !== undefined ? { base } : {}),
1094
+ ...(renames.length > 0 ? { renames: renames.map(renameText) } : {}),
1078
1095
  ...(state.stack?.name !== undefined ? { stack: state.stack.name } : {}),
1079
1096
  ...(state.running !== undefined
1080
1097
  ? {
@@ -1090,7 +1107,8 @@ export function createWorkLoop(options) {
1090
1107
  ...(state.running === undefined && state.why !== undefined ? { why: state.why } : {}),
1091
1108
  ...(state.running === undefined && state.startOver !== undefined ? { startOver: state.startOver } : {}),
1092
1109
  };
1093
- 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)) };
1094
1112
  };
1095
1113
  /**
1096
1114
  * Look after a codebase - its chain up to it - from now on; says its state. `pin`: its stack is worked out again - its
@@ -1139,6 +1157,9 @@ export function createWorkLoop(options) {
1139
1157
  state.halted = undefined;
1140
1158
  if (specChange !== true)
1141
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;
1142
1163
  }
1143
1164
  await tick();
1144
1165
  return [...new Set(states)].map(statusOf);
@@ -1148,10 +1169,10 @@ export function createWorkLoop(options) {
1148
1169
  },
1149
1170
  async regenerateAgain(plainFile, given, request = {}) {
1150
1171
  return preparingFor(plainFile, async () => {
1151
- const frids = [...new Set(Array.isArray(given) ? given : [given])];
1152
1172
  const asked = resolve(options.cwd, plainFile);
1153
1173
  const own = resolveRequiresChain(asked, [], undefined, { allowNoFunctionalities: true });
1154
1174
  const module = tipOf(own).moduleName;
1175
+ const funcIds = funcIdsNamed(module, [...new Set(Array.isArray(given) ? given : [given])]);
1155
1176
  const chains = chainsFor(asked);
1156
1177
  for (const chain of chains) {
1157
1178
  const tip = tipOf(chain);
@@ -1166,33 +1187,32 @@ export function createWorkLoop(options) {
1166
1187
  continue;
1167
1188
  const section = tip.moduleName === module ? FUNCTIONAL_REQUIREMENTS : REQUIRED_FUNCTIONAL_SPECS;
1168
1189
  const items = record[section] ?? [];
1169
- const named = (item, index) => section === FUNCTIONAL_REQUIREMENTS ? String(index + 1) : item.module === module ? (item.frid ?? "") : "";
1170
- 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)] : []));
1171
1192
  if (taken.length === 0)
1172
1193
  continue;
1173
- for (const frid of taken)
1174
- found.add(frid);
1175
- 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));
1176
1197
  writeCodeRecord(tip.buildDir, { ...record, [section]: marked });
1177
- // Its conformance tests were written against the code being replaced: marked with it, they are written again -
1178
- // checked against the new code, and rewritten where they relied on what the old code chose - once it lands.
1179
- if (section === FUNCTIONAL_REQUIREMENTS) {
1180
- const testsDir = conformanceDirFor(tip.moduleName, modulesRoot(projectDirOf(tip.plainFile)));
1181
- const tests = readRecord(testsDir);
1182
- const tested = tests?.[FUNCTIONAL_REQUIREMENTS] ?? [];
1183
- if (tested.some((item) => item.frid !== undefined && taken.includes(item.frid))) {
1184
- const again = tested.map((item) => (item.frid !== undefined && taken.includes(item.frid) ? { ...item, [REGENERATE_AGAIN]: true } : item));
1185
- writeTestsRecord(testsDir, { ...tests, [FUNCTIONAL_REQUIREMENTS]: again });
1186
- }
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 });
1187
1207
  }
1188
- 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`);
1189
1209
  }
1190
- const missing = frids.filter((frid) => !found.has(frid));
1210
+ const missing = funcIds.filter((funcId) => !found.has(funcId));
1191
1211
  if (found.size === 0)
1192
- 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.`);
1193
1213
  const statuses = await loop.bringUp(plainFile, request);
1194
1214
  if (missing.length > 0)
1195
- 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`);
1196
1216
  return statuses;
1197
1217
  });
1198
1218
  },
@@ -1204,7 +1224,7 @@ export function createWorkLoop(options) {
1204
1224
  // The codebase as the loop knows it, or - never asked for - as its records have it.
1205
1225
  const state = modules.get(tip.buildDir) ?? { chain: codebase, askedFile: file, guidance: [] };
1206
1226
  return inStack(state, () => {
1207
- 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 }) };
1208
1228
  let spec;
1209
1229
  try {
1210
1230
  spec = chainSpec(codebase);
@@ -1212,23 +1232,47 @@ export function createWorkLoop(options) {
1212
1232
  catch (error) {
1213
1233
  return { ...report, why: `its spec does not parse: ${error instanceof Error ? error.message : String(error)}` };
1214
1234
  }
1215
- 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);
1216
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;
1217
1246
  const code = codeWork(from, spec.tree);
1218
1247
  report.code = code.length === 0 ? null : casesOf(from, code, { fresh, module: tip.moduleName });
1219
1248
  if (conformanceWanted(state, spec)) {
1220
- const tested = testedOf(state);
1221
- 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);
1222
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));
1223
1259
  }
1224
- report.scheduled = (state.scheduled ?? []).map((group) => ({ frids: group.specs.map((touched) => touched.frid), unbuilt: group.unbuilt }));
1225
1260
  if (state.running !== undefined)
1226
1261
  report.running = { jobId: state.running.jobId, title: state.running.planned.title };
1227
1262
  const halted = state.held?.why ?? state.halted?.why ?? state.conformanceHalted?.why;
1228
1263
  if (halted !== undefined)
1229
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
+ }
1230
1271
  if (state.startOver !== undefined)
1231
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 };
1232
1276
  return report;
1233
1277
  });
1234
1278
  })));
@@ -1242,10 +1286,26 @@ export function createWorkLoop(options) {
1242
1286
  // a codebase built on it - by a module above - is disposable, and is deleted, a pulse running there stopped first;
1243
1287
  // the module above is regenerated from it once they pass. Only when there are tests to write: asked again for tests
1244
1288
  // it has, nothing is touched.
1245
- const wanted = new Set([...(request.frids ?? []), ...(request.groups ?? []).flat()]);
1246
- const tested = new Set((readRecord(conformanceDirFor(self.moduleName, modulesRoot(projectDirOf(self.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).map((item) => item.frid));
1247
- const ownSpecs = (chainSpec(own).tree[FUNCTIONAL_REQUIREMENTS] ?? []).map((_, index) => String(index + 1));
1248
- 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));
1249
1309
  let wentBack = false;
1250
1310
  if (readRecord(self.buildDir) !== null && toWrite) {
1251
1311
  const why = `${self.moduleName}'s conformance tests were asked for, and they come before anything is built on it`;
@@ -1276,40 +1336,20 @@ export function createWorkLoop(options) {
1276
1336
  const asking = tracked.find((state) => tipEntry(state).buildDir === self.buildDir);
1277
1337
  const states = asking !== undefined ? tracked.filter((state) => own.some((entry) => entry.buildDir === tipEntry(state).buildDir)) : tracked;
1278
1338
  const tip = asking ?? states.at(-1);
1279
- // 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.
1280
- const groups = request.groups?.map((group) => [...new Set(group)]).filter((group) => group.length > 0);
1281
- const named = [...(request.frids ?? []), ...(groups ?? []).flat()];
1282
- const asks = named.length > 0 ? [...new Set(named)] : undefined;
1339
+ const asks = named !== undefined && named.length > 0 ? named : undefined;
1283
1340
  // Its tests are written already, and a codebase built on it holds its code: nothing to write, nothing touched.
1284
1341
  if (asking === undefined && asks !== undefined && tip !== undefined) {
1285
- 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.`);
1286
1343
  }
1287
- // 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.
1288
- const specs = tip !== undefined && asks !== undefined ? scheduledSpecs(tip, asks) : undefined;
1289
- // On for the whole module already - asked for before, or by the config: asked again, nothing changes, and it is said.
1290
- const already = new Set(states.filter((state) => {
1291
- const file = tipEntry(state).plainFile;
1292
- return (state.conformance !== undefined && state.conformance.chosen === undefined) || inStack(state, () => configConformance(file) && !configChosen(file));
1293
- }));
1294
- // 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.
1295
1345
  for (const state of tip !== undefined ? [tip] : []) {
1296
- // The functional specs named are the module's own: they are scheduled in its codebase, beside those chosen before.
1297
- const named = specs;
1298
- const before = state.conformance;
1299
- // Asked for every one before, or now: every one.
1300
- const all = (before !== undefined && before.chosen === undefined) || named === undefined;
1301
- state.conformance = all ? {} : { chosen: before?.chosen ?? new Set() };
1302
- // Scheduled now, chosen once all of them are built - at once, those that are.
1303
- state.scheduled = all ? undefined : [...(state.scheduled ?? []), { specs: named, unbuilt: named.map((touched) => touched.frid) }];
1304
- // One job for all of them, or one a functional spec: as this request asks. Changed, the plan is made again.
1305
1346
  const separately = request.separately === true;
1306
1347
  if ((state.conformanceSeparately === true) !== separately)
1307
1348
  state.conformancePlan = undefined;
1308
1349
  state.conformanceSeparately = separately;
1309
- if (state === tip && groups !== undefined && groups.length > 0) {
1310
- state.conformanceGroups = [...(state.conformanceGroups ?? []), ...groups];
1311
- state.conformancePlan = undefined;
1312
- }
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;
1313
1353
  state.conformanceHalted = undefined;
1314
1354
  state.halted = undefined;
1315
1355
  state.held = undefined;
@@ -1317,10 +1357,15 @@ export function createWorkLoop(options) {
1317
1357
  await tick();
1318
1358
  return [...new Set(states)].map((state) => {
1319
1359
  const status = statusOf(state);
1320
- if (!already.has(state))
1360
+ const writing = status.running !== undefined || status.planned.some((job) => job.track === "conformance");
1361
+ if (state !== tip || writing)
1321
1362
  return status;
1322
- 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";
1323
- 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
+ };
1324
1369
  });
1325
1370
  },
1326
1371
  async startOver(plainFile, request = {}) {
@@ -1332,8 +1377,12 @@ export function createWorkLoop(options) {
1332
1377
  for (const entry of fresh?.rebuild ?? [])
1333
1378
  rebuild.set(entry.buildDir, { entry, requires: fresh.modules });
1334
1379
  }
1335
- 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 });
1336
1384
  throw new Error(`Nothing of ${tipOf(own).moduleName} has to be started over: its code is built on every module it requires.`);
1385
+ }
1337
1386
  for (const { entry } of rebuild.values()) {
1338
1387
  if (modules.get(entry.buildDir)?.running !== undefined)
1339
1388
  throw new Error(`${entry.moduleName} has a pulse running: ask again once it is done.`);
@@ -1467,13 +1516,11 @@ export function createWorkLoop(options) {
1467
1516
  ];
1468
1517
  }
1469
1518
  return status.planned.map((job, index) => {
1470
- const planned = row(`planned-${tip.moduleName}-${index + 1}`, `${basename(tip.plainFile)} ${job.track === "conformance" ? "conformance: " : ""}${job.title}`, job.waitsFor !== undefined
1471
- ? `Scheduled, not started: the conformance tests of ${job.title}, once ${waitsText(job.waitsFor)} built - the code record has to have them first.`
1472
- : job.afterCode === true
1473
- ? "Not planned yet: the module's conformance tests are planned into pulses once its code is built."
1474
- : job.planning === true
1475
- ? `Being planned into pulses now: ${job.title}. The first starts as soon as the plan is made.`
1476
- : `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");
1477
1524
  if (job.planning === true)
1478
1525
  planned.planning = { since: state.planningSince ?? preparing.get(tip.moduleName)?.since ?? now };
1479
1526
  before = planned.id;
@@ -1495,7 +1542,7 @@ export function createWorkLoop(options) {
1495
1542
  codebases: order.map((key) => {
1496
1543
  const state = modules.get(key);
1497
1544
  const tip = tipEntry(state);
1498
- const { jobs, scheduled } = describe(state);
1545
+ const { jobs, ahead } = describe(state);
1499
1546
  const running = state.running;
1500
1547
  let next = running?.number ?? state.pulses ?? 0;
1501
1548
  const pulses = [
@@ -1504,10 +1551,10 @@ export function createWorkLoop(options) {
1504
1551
  : []),
1505
1552
  ...jobs.flatMap((job, index) => {
1506
1553
  const rowId = `planned-${tip.moduleName}-${index + 1}`;
1507
- // Scheduled ahead of its code: numbered among the rest, its functional specs by their text.
1508
- const ahead = scheduled[index];
1509
- if (ahead !== undefined)
1510
- 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 }];
1511
1558
  return job === undefined ? [] : [{ number: (next += 1), title: job.title, track: job.track, running: false, rowId, before: job.step.before, after: job.step.after }];
1512
1559
  }),
1513
1560
  ];
@@ -1518,7 +1565,7 @@ export function createWorkLoop(options) {
1518
1565
  plainFile: tip.plainFile,
1519
1566
  buildDir: tip.buildDir,
1520
1567
  pulses,
1521
- conformance: state.conformance !== undefined || inStack(state, () => configConformance(tip.plainFile)),
1568
+ conformance: testedFuncIds(state.seen?.tree).length > 0,
1522
1569
  ...(state.planning !== undefined ? { planning: state.planning } : {}),
1523
1570
  ...(running === undefined && state.waitsFor !== undefined ? { waitsFor: state.waitsFor } : {}),
1524
1571
  ...(running === undefined && state.why !== undefined ? { why: state.why } : {}),
@@ -1545,14 +1592,36 @@ export function createWorkLoop(options) {
1545
1592
  };
1546
1593
  return loop;
1547
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`;
1548
1601
  /** Why nothing runs in a codebase whose code - and conformance tests, where they are wanted - is built from its spec as it is. */
1549
1602
  export const UP_TO_DATE = "built from the ***plain as it is";
1550
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`;
1551
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
+ }
1552
1620
  /** A diff's lines placed in the .plain files of a chain - the tip's first - named as they are from the project. */
1553
1621
  function whereInFiles(diff, chain) {
1554
1622
  try {
1555
- 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)]);
1556
1625
  // A standard library template by its import path, not by where turri happens to be.
1557
1626
  const nameOf = (path) => (path.startsWith(`${STDLIB_ROOT}/stdlib/`) ? relative(STDLIB_ROOT, path) : relative(projectDirOf(chain.at(-1).plainFile), path) || basename(path));
1558
1627
  const unique = [...new Set(files)].map((path) => ({ name: nameOf(path), text: readFileSync(path, "utf8") }));
@@ -1562,18 +1631,69 @@ function whereInFiles(diff, chain) {
1562
1631
  return {};
1563
1632
  }
1564
1633
  }
1565
- /** The functional specs a scheduled conformance job waits for: "fs 2 is", "fs 2, fs 3 are". */
1566
- function waitsText(frids) {
1567
- 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(", ");
1568
1668
  }
1569
- /** What a job does, in a few words: its cases. */
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
+ });
1687
+ }
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. */
1570
1689
  function casesText(cases) {
1690
+ const named = (specs) => specs.map((spec) => (spec.module !== undefined ? `${spec.module}:${spec.funcId}` : spec.funcId)).join(", ");
1571
1691
  const parts = [
1572
1692
  ...(cases.fresh ? ["sets the project up"] : []),
1573
- ...(cases.add.length > 0 ? [`adds ${cases.add.map((spec) => `fs ${spec.frid}`).join(", ")}`] : []),
1574
- ...(cases.patch.length > 0 ? [`changes ${cases.patch.map((spec) => `fs ${spec.frid}`).join(", ")}`] : []),
1575
- ...(cases.again.length > 0 ? [`regenerates ${cases.again.map((spec) => `fs ${spec.frid}`).join(", ")} again`] : []),
1576
- ...(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)}`] : []),
1577
1697
  ];
1578
1698
  return parts.length > 0 ? parts.join(", ") : "no functional spec - reqs or definitions only";
1579
1699
  }
@@ -1636,6 +1756,16 @@ export function describeModules(statuses, options = {}) {
1636
1756
  if (status.startOver === undefined)
1637
1757
  return [];
1638
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
+ }
1639
1769
  return options.for === "command line"
1640
1770
  ? [` turri regenerate ${basename(status.plainFile)} --start-over starts ${codebases.join(", ")} over`]
1641
1771
  : [
@@ -1657,13 +1787,13 @@ export function describeModules(statuses, options = {}) {
1657
1787
  for (const why of status.running.steered ?? [])
1658
1788
  lines.push(` ⇢ steered: ${why} - its agent was told, and it lands the ***plain as it is now there`);
1659
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`);
1660
1792
  status.planned.forEach((job, index) => lines.push(job.afterCode === true
1661
- ? ` ${index + 1}. conformance tests - planned once the code is built`
1662
- : job.waitsFor !== undefined
1663
- ? ` ${index + 1}. conformance tests: ${job.title} - scheduled, starts once ${waitsText(job.waitsFor)} built`
1664
- : job.planning === true
1665
- ? ` ${index + 1}. ${job.track === "conformance" ? "conformance tests: " : ""}${job.title} - being planned into pulses`
1666
- : ` ${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)}`));
1667
1797
  if (status.planNotes !== undefined)
1668
1798
  lines.push(" what the plan did not do as asked:", ...status.planNotes.map((note) => ` - ${note}`));
1669
1799
  if (status.why !== undefined)
@@ -1689,6 +1819,7 @@ const defaultConformanceRunner = async (job, onDelta) => {
1689
1819
  applied: "already applied",
1690
1820
  spec: job.spec,
1691
1821
  cases: job.planned.cases,
1822
+ recorded: job.planned.step.tree,
1692
1823
  ...(job.model !== undefined ? { model: job.model } : {}),
1693
1824
  onDelta,
1694
1825
  });
@@ -1700,7 +1831,7 @@ const defaultConformanceRunner = async (job, onDelta) => {
1700
1831
  };
1701
1832
  /** The functional specs a pulse covers, by case, each with the module whose spec it is: a host's to light and mark. */
1702
1833
  export function pulseCases(cases, moduleName) {
1703
- 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 }));
1704
1835
  return { add: of(cases.add), patch: of(cases.patch), again: of(cases.again), delete: of(cases.delete) };
1705
1836
  }
1706
1837
  /**