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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +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
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { basename, extname, join, relative, resolve } from "node:path";
3
3
  import { ask, BEGIN, createExecutor, refuseIfModelIsBlind } from "../pi.js";
4
4
  import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js";
5
- import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
5
+ import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
6
6
  import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
7
7
  import { confinedTools } from "./confine.js";
8
8
  import { abandonWork, diffSinceSnapshot, landWork, moduleDirOf, startWork, workDiff, workSnapshot } from "./lineage.js";
@@ -10,7 +10,7 @@ import { firstLine, record } from "./ledger.js";
10
10
  import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
11
11
  import { CONFORMANCE_COMPACTION } from "./compaction.js";
12
12
  import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
13
- import { acceptanceLabel, acceptanceText, labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
13
+ import { labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels, orphanedLabels, orphansPrompt, testedSpecsOf } from "./test-kinds.js";
14
14
  import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
15
15
  import { createWebTools, WEB_TOOLS } from "./web.js";
16
16
  import { createMyDiffTool } from "./self-check.js";
@@ -24,9 +24,9 @@ import { reportSpecConflict } from "./activity.js";
24
24
  import { ATTEMPTS } from "./stop-report.js";
25
25
  import { ARBITER_AFTER, arbitrateReview, dismissedNote, REVIEW_LIMIT } from "./arbiter.js";
26
26
  import { sectionsOf } from "./state.js";
27
- import { configForSpec, inheritModel, modelOnly, modelSpec } from "../config.js";
27
+ import { configForSpec, inheritModel, modelOnly, modelSpec, testDirOf } from "../config.js";
28
28
  import { defaultBuildDir, modulesRoot, testsDirUnder, projectDirOf } from "./paths.js";
29
- import { buildConformancePrompt, functionalitiesByModule, moduleSection, section, sectionModules, specChangeBlock } from "./prompt.js";
29
+ import { buildConformancePrompt, functionalitiesByModule, moduleSection, scenariosBlock, section, sectionModules, specChangeBlock } from "./prompt.js";
30
30
  import { diffFiles, digestFiles } from "./codebase.js";
31
31
  import { createCopyResourceTool, loadLinkedResources, resourceImages } from "./resources.js";
32
32
  import { resolveTemplateDirs } from "./paths.js";
@@ -53,16 +53,16 @@ export async function renderConformanceTests(options) {
53
53
  const templateDirs = resolveTemplateDirs(options.plainFile, options.templateDirs);
54
54
  const [moduleName, tree] = options.spec
55
55
  ? [options.spec.moduleName, options.spec.tree]
56
- : plainFileParser(basename(options.plainFile), templateDirs);
56
+ : plainFileParser(basename(options.plainFile), templateDirs, { testDir: testDirOf(options.plainFile) });
57
57
  // For a spec change, the tests follow the change, whatever it touches - a definition, a req -
58
58
  // with or without a functional spec yet; for one functionality, they are that functionality's.
59
59
  const forChange = options.diff !== undefined;
60
- const frids = [...getFrids(tree)];
61
- const frid = options.frid ?? (forChange ? frids.at(-1) : frids[0]);
62
- if (frid === undefined && !forChange)
60
+ const funcIds = [...getFrids(tree)];
61
+ const funcId = options.funcId ?? (forChange ? funcIds.at(-1) : funcIds[0]);
62
+ if (funcId === undefined && !forChange)
63
63
  throw new Error(`Module '${moduleName}' has no functionalities.`);
64
- if (frid !== undefined && !frids.includes(frid)) {
65
- throw new Error(`Functionality ${frid} does not exist. Available: ${frids.join(", ")}.`);
64
+ if (funcId !== undefined && !funcIds.includes(funcId)) {
65
+ throw new Error(`Functionality ${funcId} does not exist. Available: ${funcIds.join(", ")}.`);
66
66
  }
67
67
  const buildDir = resolve(options.buildDir ?? defaultBuildDir(moduleName, projectDirOf(options.plainFile)));
68
68
  // The agents that give a verdict - the judge, the test review - may run on a stronger model than the rest.
@@ -70,11 +70,11 @@ export async function renderConformanceTests(options) {
70
70
  const conformanceDir = resolve(options.conformanceDir ?? conformanceDirFor(moduleName, modulesRoot(projectDirOf(options.plainFile))));
71
71
  const stage = (label) => {
72
72
  if (!options.dryRun)
73
- reportStage(`${forChange ? unitOf(moduleName) : unitOf(moduleName, frid)}: ${label}`);
73
+ reportStage(`${forChange ? unitOf(moduleName) : unitOf(moduleName, funcId)}: ${label}`);
74
74
  };
75
- stage(options.diff !== undefined ? "conformance tests for the spec change" : `conformance tests for functionality ${frid}`);
75
+ stage(options.diff !== undefined ? "conformance tests for the spec change" : `conformance tests for functionality ${funcId}`);
76
76
  if (!options.dryRun && !forChange)
77
- reportSpec(buildingSpec(moduleName, frid, frids.length, getSpecificationsForFrid(tree, frid)[0]));
77
+ reportSpec(buildingSpec(moduleName, funcId, funcIds.length, getSpecificationsForFrid(tree, funcId)[0]));
78
78
  // From the last known good state, unless this step continues work not yet committed.
79
79
  // On its own it is one unit - its tests, and any change to the code - landed whole or undone
80
80
  // whole. Continuing work already under way, it is part of that work's unit instead.
@@ -90,9 +90,9 @@ export async function renderConformanceTests(options) {
90
90
  }
91
91
  // The spec as it stands: every section, and the functional specs - none, for a change made
92
92
  // before any was written.
93
- const specifications = frid !== undefined
94
- ? getSpecificationsForFrid(tree, frid)[0]
95
- : { ...sectionsOf(tree), [FUNCTIONAL_REQUIREMENTS]: [], [ACCEPTANCE_TESTS]: [] };
93
+ const specifications = funcId !== undefined
94
+ ? getSpecificationsForFrid(tree, funcId)[0]
95
+ : { ...sectionsOf(tree), [FUNCTIONAL_REQUIREMENTS]: [], [CONFORMANCE_TESTS]: [], [ACCEPTANCE_TESTS]: [] };
96
96
  // Conformance tests are written from the test reqs: they say how the suite is built and
97
97
  // run. Without them there is nothing to write tests against, and an agent asked anyway
98
98
  // would invent a convention that nothing else in the project shares.
@@ -101,6 +101,11 @@ export async function renderConformanceTests(options) {
101
101
  `Add a ${TEST_REQUIREMENTS} section saying how conformance tests are written and run, or render ` +
102
102
  "with conformance turned off.");
103
103
  }
104
+ // A functional spec's tests are its scenarios': one with none - no test plain file - has nothing to test.
105
+ if (!forChange && (specifications[CONFORMANCE_TESTS] ?? []).length + (specifications[ACCEPTANCE_TESTS] ?? []).length === 0) {
106
+ throw new Error(`${moduleName}:${funcId} has no scenarios - no test plain file - so there is nothing to test: write its conformance or ` +
107
+ "acceptance scenarios first.");
108
+ }
104
109
  // Whether the tests pass is the project's conformance testing script - and, when the code is
105
110
  // changed, its unit testing script: without them, nothing is written.
106
111
  stage("finding the project's testing scripts");
@@ -108,7 +113,7 @@ export async function renderConformanceTests(options) {
108
113
  const scripts = findTestScripts(root, options.plainFile, buildDir);
109
114
  if (!options.dryRun)
110
115
  requireTestScripts(moduleName, scripts, { conformance: true });
111
- const linkedResources = loadLinkedResources(tree, forChange ? null : frid, templateDirs);
116
+ const linkedResources = loadLinkedResources(tree, forChange ? null : funcId, templateDirs);
112
117
  // The spec's images need a model that reads them: refused before any of its agents starts.
113
118
  if (!options.dryRun)
114
119
  await refuseIfModelIsBlind(options.model, resourceImages(linkedResources).length);
@@ -116,8 +121,9 @@ export async function renderConformanceTests(options) {
116
121
  const prompt = withTestingScripts(buildConformancePrompt({
117
122
  ...(options.cases !== undefined ? { cases: options.cases } : {}),
118
123
  moduleName,
119
- frid,
124
+ funcId,
120
125
  specifications,
126
+ funcIds,
121
127
  modules: sectionModules(tree, moduleName),
122
128
  linkedResources,
123
129
  manifest,
@@ -129,7 +135,7 @@ export async function renderConformanceTests(options) {
129
135
  if (options.dryRun) {
130
136
  return {
131
137
  moduleName,
132
- frid: frid ?? "",
138
+ funcId: funcId ?? "",
133
139
  buildDir,
134
140
  conformanceDir,
135
141
  prompt,
@@ -157,18 +163,18 @@ export async function renderConformanceTests(options) {
157
163
  // The code and its tests are rendered apart: the tests in their own folder, by an agent that
158
164
  // may read the code but writes only tests. An explorer reads the code first and writes down how the
159
165
  // software is invoked, which is what the tests are written against.
160
- const label = `${moduleName} ${options.diff !== undefined ? "spec change" : frid}`;
161
- // For a spec change: the functionalities it touches, whose acceptance tests are labelled, checked and reviewed.
162
- const touched = forChange ? touchedFunctionalities(tree, frids, options.diff) : [];
163
- // The acceptance tests the run's tests carry out: every functionality the change touches, or the one under test.
164
- const carriedOut = forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }];
165
- const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, carriedOut, options.change ?? options.diff, sectionModules(tree, moduleName)) };
166
+ const label = `${moduleName} ${options.diff !== undefined ? "spec change" : funcId}`;
167
+ // For a spec change: the functionalities it touches, whose scenarios are carried out, labelled, checked and reviewed.
168
+ const touched = forChange ? touchedFunctionalities(tree, funcIds, options.diff, moduleName, options.cases) : [];
169
+ // The scenarios the run's tests carry out: those of every functionality the change touches, or of the one under test.
170
+ const carriedOut = forChange ? touched : [testedSpecOf(specifications, funcId)];
171
+ const specContext = { moduleName, text: specText(specifications, moduleName, funcIds, required, carriedOut, options.change ?? options.diff, sectionModules(tree, moduleName)) };
166
172
  // Any functionality of the codebase - this module's, and those of the modules it requires. A
167
173
  // functionality's run may raise a spec conflict only with that functionality; a spec diff's, between any two.
168
- const own = (forChange ? frids : frids.slice(0, frids.indexOf(frid) + 1)).map((id) => ({ module: moduleName, frid: id }));
174
+ const own = (forChange ? funcIds : funcIds.slice(0, funcIds.indexOf(funcId) + 1)).map((id) => ({ module: moduleName, funcId: id }));
169
175
  const scope = {
170
- functionalities: [...required.map((spec) => ({ module: spec.module, frid: spec.frid })), ...own],
171
- ...(forChange ? {} : { tested: { module: moduleName, frid: frid } }),
176
+ functionalities: [...required.map((spec) => ({ module: spec.module, funcId: spec.funcId })), ...own],
177
+ ...(forChange ? { touchedModules: [...new Set(touched.map((item) => item.module ?? moduleName))] } : { tested: { module: moduleName, funcId: funcId } }),
172
178
  };
173
179
  /** How many times each spec conflict has been raised in this run, by the two functionalities it names. */
174
180
  const conflicts = new Map();
@@ -232,17 +238,14 @@ export async function renderConformanceTests(options) {
232
238
  : []),
233
239
  { path: relative(root, scripts.conformance), runs: "<script> <copy of the build> <copy of the whole conformance suite>", content: readFileSync(scripts.conformance, "utf8") },
234
240
  ];
235
- // The acceptance tests the explorer's plan has to carry out, step by step.
236
- const toCarryOut = carriedOut
237
- .map((item) => ({ module: moduleName, frid: item.frid, count: item.acceptanceTests.length }))
238
- .filter((item) => item.count > 0);
239
- let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, acceptance: toCarryOut, label, model: options.model, onDelta: options.onDelta });
241
+ let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, label, model: options.model, onDelta: options.onDelta });
240
242
  reportActivity(` how to invoke it: ${invocation.split("\n")[0].slice(0, 120)}`);
241
243
  const agentPrompt = withTestingScripts(buildConformancePrompt({
242
244
  ...(options.cases !== undefined ? { cases: options.cases } : {}),
243
245
  moduleName,
244
- frid,
246
+ funcId,
245
247
  specifications,
248
+ funcIds,
246
249
  modules: sectionModules(tree, moduleName),
247
250
  linkedResources,
248
251
  manifest,
@@ -337,13 +340,10 @@ export async function renderConformanceTests(options) {
337
340
  // What the tests were when this run began: what it adds or changes is what the review gate reads.
338
341
  const testsAtStart = digestFiles(conformanceDir);
339
342
  let unitInventory = inventoryTests(buildDir);
340
- // Acceptance tests only when the spec has them for this functionality.
341
- const acceptance = options.diff === undefined ? (specifications[ACCEPTANCE_TESTS] ?? []).length : 0;
342
- stage(acceptance > 0
343
- ? `turri is writing conformance tests from the spec, and its ${acceptance} acceptance test${acceptance === 1 ? "" : "s"} apart from them`
344
- : options.diff !== undefined
345
- ? "turri is bringing the conformance tests in line with the spec change"
346
- : "turri is writing conformance tests from the spec");
343
+ const scenarios = carriedOut.reduce((count, item) => count + item.conformance.length + item.acceptance.length, 0);
344
+ stage(options.diff !== undefined
345
+ ? "turri is bringing the conformance tests in line with the spec change"
346
+ : `turri is writing the conformance tests of its ${scenarios} scenario${scenarios === 1 ? "" : "s"}`);
347
347
  let response = await askConformance(BEGIN, options.onDelta);
348
348
  const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
349
349
  let verified = false;
@@ -370,10 +370,9 @@ export async function renderConformanceTests(options) {
370
370
  await askFixer(removalChallengePrompt(unitRemovals, "unit tests"));
371
371
  continue;
372
372
  }
373
- // The conformance tests and each acceptance test have to be there, and labelled as what
374
- // they are, before anything else counts.
373
+ // A test for each scenario has to be there, and labelled as the scenario it carries out, before anything else counts.
375
374
  if (options.diff === undefined) {
376
- const missing = missingTestLabels(conformanceDir, frid, specifications[ACCEPTANCE_TESTS] ?? []);
375
+ const missing = missingTestLabels(conformanceDir, moduleName, carriedOut[0]);
377
376
  if (missing.length > 0) {
378
377
  if (labelAsks >= LABEL_ASKS) {
379
378
  stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
@@ -381,16 +380,19 @@ export async function renderConformanceTests(options) {
381
380
  break;
382
381
  }
383
382
  labelAsks += 1;
384
- stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the conformance and acceptance tests");
385
- response = await askConformance(labelsPrompt(frid, missing, listFiles(conformanceDir).length === 0, (specifications[ACCEPTANCE_TESTS] ?? []).length > 0), options.onDelta);
383
+ stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the tests of the scenarios");
384
+ response = await askConformance(labelsPrompt(moduleName, funcId, missing, listFiles(conformanceDir).length === 0), options.onDelta);
386
385
  continue;
387
386
  }
388
387
  }
389
388
  else {
390
389
  // A spec change: every functionality it touches, as a functionality's own run would have it - every one missing
391
390
  // asked for in one request, checked whole again after each answer, so none is asked for in place of another.
392
- const pending = touched
393
- .map((item) => ({ frid: item.frid, missing: missingTestLabels(conformanceDir, item.frid, item.acceptanceTests) }))
391
+ // And every other functional spec the tests are written for: each of its scenarios has its labelled test too.
392
+ const touchedKeys = new Set(touched.map((item) => `${item.module ?? moduleName}:${item.funcId}`));
393
+ const others = testedSpecsOf(options.recorded).filter((item) => !touchedKeys.has(`${item.module ?? moduleName}:${item.funcId}`));
394
+ const pending = [...touched, ...others]
395
+ .map((item) => ({ funcId: item.funcId, ...(item.module !== undefined ? { module: item.module } : {}), missing: missingTestLabels(conformanceDir, moduleName, item) }))
394
396
  .filter((item) => item.missing.length > 0);
395
397
  if (pending.length > 0) {
396
398
  if (labelAsks >= LABEL_ASKS) {
@@ -399,9 +401,22 @@ export async function renderConformanceTests(options) {
399
401
  break;
400
402
  }
401
403
  labelAsks += 1;
402
- stage(`labelling the conformance and acceptance tests of functionalit${pending.length === 1 ? "y" : "ies"} ${pending.map((item) => item.frid).join(", ")}, which the change touches`);
403
- const acceptance = touched.some((item) => item.acceptanceTests.length > 0);
404
- response = await askConformance(labelsPromptFor(pending, listFiles(conformanceDir).length === 0, acceptance), options.onDelta);
404
+ stage(`labelling the tests of the scenarios of ${pending.map((item) => `${item.module ?? moduleName}:${item.funcId}`).join(", ")}, which the change touches`);
405
+ response = await askConformance(labelsPromptFor(moduleName, pending, listFiles(conformanceDir).length === 0), options.onDelta);
406
+ continue;
407
+ }
408
+ // A label that names nothing the tests are written for - a scenario or a functional spec taken out, a label
409
+ // mistyped, a test invented beside the scenarios: sent back to be removed or relabelled.
410
+ const orphans = options.recorded !== undefined ? orphanedLabels(conformanceDir, moduleName, testedSpecsOf(options.recorded)) : [];
411
+ if (orphans.length > 0) {
412
+ if (labelAsks >= LABEL_ASKS) {
413
+ stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
414
+ unlabelled = orphans.map((orphan) => `${orphan.file} is labelled "${orphan.label}", which names nothing the tests are written for.`);
415
+ break;
416
+ }
417
+ labelAsks += 1;
418
+ stage(`${orphans.length} label${orphans.length === 1 ? "" : "s"} in the tests name${orphans.length === 1 ? "s" : ""} nothing they are written for - the conformance agent removes or relabels them`);
419
+ response = await askConformance(orphansPrompt(orphans), options.onDelta);
405
420
  continue;
406
421
  }
407
422
  }
@@ -462,7 +477,6 @@ export async function renderConformanceTests(options) {
462
477
  scope,
463
478
  testingScripts,
464
479
  question: { failure: shownOutput(buildDir, "conformance-stuck", recheck.output), account: invocation },
465
- acceptance: toCarryOut,
466
480
  label,
467
481
  model: options.model,
468
482
  onDelta: options.onDelta,
@@ -598,8 +612,8 @@ export async function renderConformanceTests(options) {
598
612
  ...ref,
599
613
  plainFile: files.get(ref.module) ?? options.plainFile,
600
614
  text: ref.module === moduleName
601
- ? (ownSpecs[frids.indexOf(ref.frid)] ?? "")
602
- : (required.find((spec) => spec.module === ref.module && spec.frid === ref.frid)?.spec ?? ""),
615
+ ? (ownSpecs[funcIds.indexOf(ref.funcId)] ?? "")
616
+ : (required.find((spec) => spec.module === ref.module && spec.funcId === ref.funcId)?.spec ?? ""),
603
617
  })),
604
618
  reasons: said,
605
619
  });
@@ -696,7 +710,7 @@ export async function renderConformanceTests(options) {
696
710
  // After the last review's problems were worked on, the tests go on once they pass: not reviewed again. With
697
711
  // the review off in the config, they go on once they pass from the first.
698
712
  if (reviewOn && changedTests.length > 0 && failedReviews < REVIEW_LIMIT) {
699
- stage(`optimising the tests (review ${failedReviews + 1} of at most ${REVIEW_LIMIT}): black box, and the acceptance tests doing what the spec says`);
713
+ stage(`optimising the tests (review ${failedReviews + 1} of at most ${REVIEW_LIMIT}): black box, and every scenario carried out`);
700
714
  // One reviewer for the run, made the first time the tests are reviewed.
701
715
  reviewer ??= await createTestReviewer({
702
716
  ...(options.cases !== undefined ? { cases: options.cases } : {}),
@@ -730,11 +744,11 @@ export async function renderConformanceTests(options) {
730
744
  if (failedReviews > REVIEW_ROUNDS) {
731
745
  stage(`the review sent the tests back ${failedReviews === 1 ? "once" : `${failedReviews} times`} - the arbiter weighs what it found`);
732
746
  const ruling = await arbitrateReview({
733
- subject: `the conformance tests of ${forChange ? `the spec change in module \`${moduleName}\`` : `functionality ${frid} of module \`${moduleName}\``}`,
747
+ subject: `the conformance tests of ${forChange ? `the spec change in module \`${moduleName}\`` : `functional spec ${moduleName}:${funcId}`}`,
734
748
  workdir: conformanceDir,
735
749
  read: [conformanceDir, buildDir],
736
750
  uphold: [
737
- "- uphold it when because of it the functionality is not tested black box - a test or an acceptance test",
751
+ "- uphold it when because of it the functionality is not tested black box - a scenario's test",
738
752
  " the specification states missing, cut down or skipping what it checks, the software mocked or not reached",
739
753
  " from outside, a test that cannot fail. Then the reviewer is right, and the conformance agent fixes it.",
740
754
  ],
@@ -802,20 +816,20 @@ export async function renderConformanceTests(options) {
802
816
  const changed = codeChanged();
803
817
  if (verified) {
804
818
  // The tests, and the code when it changed: one commit if they share a module folder.
805
- const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${frid}`}${changed ? " (code changed)" : ""}`;
819
+ const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${funcId}`}${changed ? " (code changed)" : ""}`;
806
820
  stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
807
821
  landWork(conformanceDir, subject);
808
822
  if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
809
823
  landWork(buildDir, subject);
810
824
  }
811
825
  else if (start !== undefined) {
812
- const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${frid}`, unlabelled);
826
+ const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${funcId}`, unlabelled);
813
827
  stage(`${why} - undoing them`);
814
828
  abandonWork(start, moduleName, why);
815
829
  }
816
830
  return {
817
831
  moduleName,
818
- frid: frid ?? "",
832
+ funcId: funcId ?? "",
819
833
  buildDir,
820
834
  conformanceDir,
821
835
  prompt: agentPrompt,
@@ -861,23 +875,41 @@ function plainFilesOf(plainFile, templateDirs) {
861
875
  }
862
876
  }
863
877
  /**
864
- * The functionalities a spec change touches - its functional spec, or one of its acceptance tests,
865
- * has a line the change adds or removes - with their acceptance tests: what a spec diff's
878
+ * The functionalities with scenarios a spec change touches - by the job's cases, else those whose functional spec, or one of
879
+ * whose scenarios, has a line the change adds or removes - with their scenarios: what a spec diff's
866
880
  * conformance run labels, checks and reviews, as a functionality's run does for its one.
867
881
  */
868
- export function touchedFunctionalities(tree, frids, diff) {
882
+ export function touchedFunctionalities(tree, funcIds, diff, moduleName, cases) {
883
+ const tested = (funcId) => {
884
+ const spec = testedSpecOf(getSpecificationsForFrid(tree, funcId)[0], funcId);
885
+ return spec.conformance.length + spec.acceptance.length > 0 ? spec : undefined;
886
+ };
887
+ // The job's cases say which: what it adds, changes or regenerates again - the modules' below first, in the chain's
888
+ // order, as the tip's suite holds their tests too, then the module's own.
889
+ if (cases !== undefined) {
890
+ const own = (touched) => touched.module === undefined || touched.module === moduleName;
891
+ const touched = [...cases.add, ...cases.patch, ...cases.again];
892
+ const named = new Set(touched.filter(own).map((spec) => spec.funcId));
893
+ const below = new Set(touched.filter((spec) => !own(spec)).map((spec) => `${spec.module}:${spec.funcId}`));
894
+ const inherited = getInheritedFunctionalSpecs(tree)
895
+ .filter((spec) => below.has(`${spec.module}:${spec.funcId}`) && spec.conformanceTests.length + spec.acceptanceTests.length > 0)
896
+ .map((spec) => ({ funcId: spec.funcId, module: spec.module, conformance: spec.conformanceTests, acceptance: spec.acceptanceTests }));
897
+ return [...inherited, ...funcIds.filter((funcId) => named.has(funcId)).flatMap((funcId) => tested(funcId) ?? [])];
898
+ }
899
+ // Else what the diff's lines touch: a functional spec's text, or one of its scenarios'.
869
900
  const bare = (line) => line.trim().replace(/^-\s+/, "").replace(/\s+/g, " ");
870
901
  const changed = new Set(diff
871
902
  .split("\n")
872
903
  .filter((line) => /^[+-]/.test(line) && !/^(\+\+\+|---)/.test(line))
873
904
  .map((line) => bare(line.slice(1)))
874
905
  .filter((line) => line.length > 12));
875
- return frids.flatMap((frid) => {
876
- const specifications = getSpecificationsForFrid(tree, frid)[0];
877
- const spec = specifications[FUNCTIONAL_REQUIREMENTS].at(-1) ?? "";
878
- const acceptanceTests = specifications[ACCEPTANCE_TESTS] ?? [];
879
- const lines = [spec, ...acceptanceTests].flatMap((text) => text.split("\n")).map(bare);
880
- return lines.some((line) => changed.has(line)) ? [{ frid, acceptanceTests }] : [];
906
+ return funcIds.flatMap((funcId) => {
907
+ const spec = tested(funcId);
908
+ if (spec === undefined)
909
+ return [];
910
+ const text = getSpecificationsForFrid(tree, funcId)[0][FUNCTIONAL_REQUIREMENTS].at(-1) ?? "";
911
+ const lines = [text, ...spec.conformance.map((scenario) => scenario.text), ...spec.acceptance.map((scenario) => scenario.text)].flatMap((item) => item.split("\n")).map(bare);
912
+ return lines.some((line) => changed.has(line)) ? [spec] : [];
881
913
  });
882
914
  }
883
915
  /** From the how-manieth review that sends the tests back the arbiter weighs each one (see arbiter.ts). */
@@ -962,14 +994,14 @@ export function failureLine(run) {
962
994
  }
963
995
  /**
964
996
  * The spec as the explorer, the judge and the coding agent are shown it - its functional specs
965
- * numbered with their ids, so a spec conflict can name them, and the acceptance tests the run carries
997
+ * named by their func-ids, so a spec conflict can name them, and the scenarios the run carries
966
998
  * out labelled as their tests are, so a verdict can quote them.
967
999
  */
968
- function specText(specifications, moduleName, frids, required, acceptance, diff, modules) {
1000
+ function specText(specifications, moduleName, funcIds, required, carriedOut, diff, modules) {
969
1001
  // Every functionality in the codebase, each with its module: the required modules' first, then this one's.
970
1002
  const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
971
1003
  ? [
972
- functionalitiesByModule([...required, ...items.map((item, index) => ({ module: moduleName, frid: frids[index] ?? String(index + 1), spec: item }))], moduleName),
1004
+ functionalitiesByModule([...required, ...items.map((item, index) => ({ module: moduleName, funcId: funcIds[index] ?? String(index + 1), spec: item }))], moduleName),
973
1005
  ]
974
1006
  : items;
975
1007
  // Each section in its tag, as every agent is shown it (see specPrimer).
@@ -977,22 +1009,27 @@ function specText(specifications, moduleName, frids, required, acceptance, diff,
977
1009
  .map((key) => [key, specifications[key] ?? []])
978
1010
  .filter(([, items]) => items.length > 0)
979
1011
  .map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
980
- // Not only the last functionality's: a spec change's run carries out the acceptance tests of every one it touches.
981
- const tests = acceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `${acceptanceLabel(item.frid, index + 1)}: ${acceptanceText(test)}`));
982
- if (tests.length > 0)
983
- sections.push(section(ACCEPTANCE_TESTS, tests));
1012
+ // Not only the last functionality's: a spec change's run carries out the scenarios of every one it touches.
1013
+ const scenarios = scenariosBlock(moduleName, carriedOut);
1014
+ if (scenarios !== null)
1015
+ sections.push(scenarios);
984
1016
  if (diff !== undefined)
985
- sections.push(specChangeBlock(diff));
1017
+ sections.push(specChangeBlock(diff, true));
986
1018
  return sections.join("\n\n");
987
1019
  }
1020
+ /** A functional spec's scenarios, as its specifications carry them: those its conformance tests carry out. */
1021
+ function testedSpecOf(specifications, funcId) {
1022
+ return { funcId, conformance: specifications[CONFORMANCE_TESTS] ?? [], acceptance: specifications[ACCEPTANCE_TESTS] ?? [] };
1023
+ }
988
1024
  /**
989
1025
  * The functional spec a conformance run writes tests for, for the person watching: which one of how
990
- * many, and its text as the spec has it, with its acceptance tests after it.
1026
+ * many, and its text as the spec has it, with its scenarios after it.
991
1027
  */
992
- export function buildingSpec(moduleName, frid, total, specifications) {
993
- const acceptance = specifications[ACCEPTANCE_TESTS] ?? [];
1028
+ export function buildingSpec(moduleName, funcId, total, specifications) {
1029
+ const kind = (name, scenarios) => (scenarios ?? []).length > 0 ? ["", `${name}:`, ...(scenarios ?? []).map((scenario) => `# ${scenario.id}\n${scenario.text.trim()}`)] : [];
994
1030
  return {
995
- label: `${moduleName} functionality ${frid} of ${total}`,
996
- text: [(specifications["functional specs"].at(-1) ?? "").trim(), ...(acceptance.length > 0 ? ["", "acceptance tests:", ...acceptance.map((test) => test.trim())] : [])].join("\n"),
1031
+ spec: { module: moduleName, funcId },
1032
+ label: `${moduleName}:${funcId}, functional spec of ${total}`,
1033
+ text: [(specifications["functional specs"].at(-1) ?? "").trim(), ...kind("conformance tests", specifications[CONFORMANCE_TESTS]), ...kind("acceptance tests", specifications[ACCEPTANCE_TESTS])].join("\n"),
997
1034
  };
998
1035
  }
@@ -1,6 +1,6 @@
1
1
  import { type PlainRole } from "../plain/highlight.ts";
2
2
  /**
3
- * A spec diff as the panels show it: each line's ***plain highlighted as plyn highlights it
3
+ * A spec diff as the panels show it: each line's ***plain highlighted by turri's own grammar
4
4
  * (`plain/highlight.ts`), an added line on a green band and a removed one on a red, the band the width
5
5
  * of the panel - as an editor shows a diff. Each file is named once, quietly, above its hunks - not
6
6
  * git's headers - and a hunk header is a quiet rule between hunks.
@@ -36,6 +36,10 @@ export function locateDiffLines(diff, files) {
36
36
  });
37
37
  const used = new Set();
38
38
  let last;
39
+ // A test plain file's part of the diff is that file's alone: its `---` and its section headers are in every one of
40
+ // them, and in the module's frontmatter - found by text anywhere, they would be placed in the wrong file.
41
+ const testPlainFile = texts.map((file) => /^---\n(?:.*\n)*?func_id:/.test(file.written.join("\n")));
42
+ let only;
39
43
  const take = (file, line, index) => {
40
44
  used.add(`${file}:${line}`);
41
45
  const section = sections[file][line];
@@ -46,14 +50,23 @@ export function locateDiffLines(diff, files) {
46
50
  .replace(/\n+$/, "")
47
51
  .split("\n")
48
52
  .forEach((line, index) => {
49
- if (HEADER.test(line) || line.startsWith("+++ ") || line.startsWith("@@") || line.startsWith("\\"))
53
+ const named = /^\+\+\+ (?:b\/)?(.+)$/.exec(line);
54
+ if (named !== null) {
55
+ const file = texts.findIndex((candidate, at) => testPlainFile[at] === true && (candidate.name === named[1] || candidate.name.endsWith(`/${named[1]}`) || named[1].endsWith(`/${candidate.name}`)));
56
+ only = file === -1 ? undefined : file;
57
+ return;
58
+ }
59
+ if (line.startsWith("diff --git "))
60
+ only = undefined;
61
+ if (HEADER.test(line) || line.startsWith("@@") || line.startsWith("\\"))
50
62
  return;
51
63
  if (line.startsWith("-")) {
52
64
  // A removed line still written in a file - taken out of what is compared, not out of the spec - is that file's.
53
65
  const removed = asRead(line.slice(1).trimEnd());
54
66
  if (removed.trim() === "" || SECTION.test(removed.trim()))
55
67
  return;
56
- const file = texts.findIndex((candidate) => candidate.lines.includes(removed));
68
+ // In a test plain file a removed line is gone: no line of it is that line any more.
69
+ const file = only !== undefined ? -1 : texts.findIndex((candidate) => candidate.lines.includes(removed));
57
70
  if (file !== -1) {
58
71
  const at = texts[file].lines.indexOf(removed);
59
72
  const section = sections[file][at];
@@ -64,11 +77,11 @@ export function locateDiffLines(diff, files) {
64
77
  const text = (line.startsWith("+") || line.startsWith(" ") ? line.slice(1) : line).trimEnd();
65
78
  if (text === "") {
66
79
  // A blank line: the one right after the line before it, when that is blank.
67
- if (last !== undefined && texts[last.file].lines[last.line + 1] === "" && !used.has(`${last.file}:${last.line + 1}`))
80
+ if (last !== undefined && (only === undefined || last.file === only) && texts[last.file].lines[last.line + 1] === "" && !used.has(`${last.file}:${last.line + 1}`))
68
81
  take(last.file, last.line + 1, index);
69
82
  return;
70
83
  }
71
- const order = last === undefined ? texts.map((_, file) => file) : [last.file, ...texts.map((_, file) => file).filter((file) => file !== last.file)];
84
+ const order = only !== undefined ? [only] : last === undefined ? texts.map((_, file) => file) : [last.file, ...texts.map((_, file) => file).filter((file) => file !== last.file)];
72
85
  for (const file of order) {
73
86
  const lines = texts[file].lines;
74
87
  const from = last !== undefined && file === last.file ? last.line + 1 : 0;
@@ -112,6 +125,9 @@ export function diffFiles(diff, width, paint = PLAIN_PAINT, where) {
112
125
  rendered ??= headerFile;
113
126
  before = [];
114
127
  after = [];
128
+ // A new file starts in no section: a test plain file never has the module's.
129
+ sectionBefore = undefined;
130
+ sectionAfter = undefined;
115
131
  continue;
116
132
  }
117
133
  if (HEADER.test(line) || line.startsWith("@@") || line.startsWith("\\"))
@@ -1,5 +1,4 @@
1
1
  import { type MarshalledPlainSource } from "../plain/spec.ts";
2
- import { type Chosen } from "./records.ts";
3
2
  import { type SpecChange } from "./spec-steps.ts";
4
3
  /**
5
4
  * The work, derived from the records - see docs/regenerating.md. Nothing here is stored: what is left
@@ -8,7 +7,7 @@ import { type SpecChange } from "./spec-steps.ts";
8
7
  *
9
8
  * - Code work: from the code record to the spec as the code sees it.
10
9
  * - Conformance work: from the tests record to what the conformance tests are to be written for now -
11
- * the chosen functional specs the code is built from.
10
+ * the functional specs the code is built from that have scenarios.
12
11
  *
13
12
  * A job is a set of those changes. What it tells its agents follows from the functional specs it
14
13
  * touches - its cases - and a running job is stopped once the spec no longer wants what it makes.
@@ -16,13 +15,13 @@ import { type SpecChange } from "./spec-steps.ts";
16
15
  /** Everything left to build in the code: from what the code record says it is built from, to the spec now. */
17
16
  export declare function codeWork(record: MarshalledPlainSource | null, spec: MarshalledPlainSource): SpecChange[];
18
17
  /** Everything left to do in the conformance tests: from what the tests record says they are written for, to what they are to be now. */
19
- export declare function conformanceWork(tests: MarshalledPlainSource | null, code: MarshalledPlainSource | null, spec: MarshalledPlainSource, chosen?: Chosen): SpecChange[];
18
+ export declare function conformanceWork(tests: MarshalledPlainSource | null, code: MarshalledPlainSource | null, spec: MarshalledPlainSource): SpecChange[];
20
19
  /**
21
- * A functional spec a job touches: its number - in the spec after the job, or, taken out, before it - its first line,
22
- * and the module it belongs to: each module numbers its own from 1, so a number alone can mean two.
20
+ * A functional spec a job touches: its func-id, its first line, and the module it belongs to - a func-id is unique in its
21
+ * module, so a func-id alone can mean two.
23
22
  */
24
23
  export interface TouchedSpec {
25
- frid: string;
24
+ funcId: string;
26
25
  text: string;
27
26
  module?: string;
28
27
  }
@@ -30,17 +29,19 @@ export interface TouchedSpec {
30
29
  * What a job does to the functional specs, and so what its agents are told:
31
30
  *
32
31
  * - fresh: the module requires none and nothing is built - the project is set up first.
33
- * - add: functional specs that are new.
34
- * - patch: functional specs changed - their text, their acceptance tests, or a concept they use: a
35
- * change to what is built, never built again from scratch.
32
+ * - add: functional specs that are new - a func-id the spec did not have.
33
+ * - patch: functional specs changed - their text, their scenarios, or a concept they use: a
34
+ * change to what is built, never built again from scratch. A functional spec moved, and nothing else, is none.
36
35
  * - again: functional specs marked to regenerate again (`regenerate` with functionalities named): their code is built, and is regenerated again - rewritten
37
- * or started over, doing what the spec says now - with what it replaces taken out. A functional spec of a module
38
- * this one requires, regenerated again here, is one too.
39
- * - delete: functional specs taken out.
40
- * - mechanical: none of those - only reqs, or definitions no functional spec uses. Existing tests may
41
- * change only mechanically, and no new ones are written.
36
+ * or started over, doing what the spec says now - with what it replaces taken out.
37
+ * - delete: functional specs taken out - a func-id the spec no longer has. One renamed with any other change is a delete
38
+ * and an add; renamed and nothing else, turri renames it itself, and it is no job's (renames.ts).
39
+ * - mechanical: none of those - no functional spec of any module of the chain touched: only reqs, or definitions no
40
+ * functional spec uses. Existing tests may change only mechanically, and no new ones are written.
42
41
  *
43
- * A job can hold several of add, patch and delete at once: its prompt has a section for each.
42
+ * The functional specs of the modules below are cased as the codebase's own are, each with its module: the codebase at
43
+ * the tip of a chain holds their code. A job can hold several of add, patch and delete at once: its prompt has a section
44
+ * for each.
44
45
  */
45
46
  export interface JobCases {
46
47
  fresh: boolean;
@@ -49,6 +50,37 @@ export interface JobCases {
49
50
  again: TouchedSpec[];
50
51
  delete: TouchedSpec[];
51
52
  mechanical: boolean;
53
+ /**
54
+ * A conformance job's: each functional spec it touches, its scenarios by case - in the order of add, patch, again and
55
+ * delete. None for a code job: the code record holds no scenario.
56
+ */
57
+ scenarios?: ScenarioCases[];
58
+ }
59
+ /** A scenario as a conformance job names it: its kind, its scenario-id, its text. */
60
+ export interface ScenarioRef {
61
+ kind: "conformance" | "acceptance";
62
+ id: string;
63
+ text: string;
64
+ }
65
+ /**
66
+ * What a conformance job does to the tests of one functional spec, scenario by scenario - compared with the tests record
67
+ * by kind and scenario-id, as functional specs are by func-id:
68
+ *
69
+ * - added: write its tests; changed: rewrite them to the scenario as it reads now; removed: delete the tests labelled
70
+ * with it; unchanged: its tests stay as they are.
71
+ * - A scenario-id renamed with any other change, or a scenario moved between conformance and acceptance - its label
72
+ * changes - is one removed and one added; one renamed and nothing else turri relabels itself (renames.ts). One moved
73
+ * between test plain files of the same functional spec is nothing.
74
+ * - `codeChanged`: the functional spec's code moved under its unchanged scenarios - its text or a file it links changed
75
+ * (`text`), it was regenerated again (`again`), or a concept it uses changed (`concept`): their tests are run on the
76
+ * new code, and those that fail are fixed against the scenario.
77
+ */
78
+ export interface ScenarioCases extends TouchedSpec {
79
+ added: ScenarioRef[];
80
+ changed: ScenarioRef[];
81
+ removed: ScenarioRef[];
82
+ unchanged: ScenarioRef[];
83
+ codeChanged?: "text" | "again" | "concept";
52
84
  }
53
85
  /**
54
86
  * Every concept a functional spec uses: those its text names, and those their definitions name in