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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +71 -15
  86. package/dist/regenerate/change-view.d.ts +8 -3
  87. package/dist/regenerate/change-view.js +71 -22
  88. package/dist/regenerate/chunk-plan.d.ts +36 -15
  89. package/dist/regenerate/chunk-plan.js +165 -59
  90. package/dist/regenerate/conform.d.ts +3 -0
  91. package/dist/regenerate/conform.js +6 -5
  92. package/dist/regenerate/conformance-agents.d.ts +18 -39
  93. package/dist/regenerate/conformance-agents.js +60 -88
  94. package/dist/regenerate/conformance.d.ts +23 -20
  95. package/dist/regenerate/conformance.js +118 -121
  96. package/dist/regenerate/diff-view.d.ts +1 -1
  97. package/dist/regenerate/diff-view.js +20 -4
  98. package/dist/regenerate/drift.d.ts +47 -15
  99. package/dist/regenerate/drift.js +101 -30
  100. package/dist/regenerate/jobs.d.ts +2 -2
  101. package/dist/regenerate/jobs.js +1 -1
  102. package/dist/regenerate/ledger.d.ts +19 -0
  103. package/dist/regenerate/loop.d.ts +61 -49
  104. package/dist/regenerate/loop.js +473 -340
  105. package/dist/regenerate/paths.d.ts +4 -0
  106. package/dist/regenerate/paths.js +7 -1
  107. package/dist/regenerate/plain-palette.js +2 -0
  108. package/dist/regenerate/prompt.d.ts +57 -19
  109. package/dist/regenerate/prompt.js +157 -122
  110. package/dist/regenerate/records.d.ts +43 -12
  111. package/dist/regenerate/records.js +102 -88
  112. package/dist/regenerate/regenerate.js +29 -11
  113. package/dist/regenerate/regression.d.ts +4 -4
  114. package/dist/regenerate/regression.js +14 -10
  115. package/dist/regenerate/renames.d.ts +42 -0
  116. package/dist/regenerate/renames.js +185 -0
  117. package/dist/regenerate/requires.d.ts +1 -1
  118. package/dist/regenerate/requires.js +10 -3
  119. package/dist/regenerate/resources.d.ts +5 -3
  120. package/dist/regenerate/resources.js +4 -4
  121. package/dist/regenerate/scripts-being-written.d.ts +19 -0
  122. package/dist/regenerate/scripts-being-written.js +30 -0
  123. package/dist/regenerate/spec-map-view.d.ts +2 -2
  124. package/dist/regenerate/spec-map-view.js +46 -15
  125. package/dist/regenerate/spec-map.d.ts +27 -5
  126. package/dist/regenerate/spec-map.js +92 -60
  127. package/dist/regenerate/spec-steps.d.ts +48 -5
  128. package/dist/regenerate/spec-steps.js +213 -28
  129. package/dist/regenerate/state.d.ts +4 -3
  130. package/dist/regenerate/stop-report.d.ts +1 -1
  131. package/dist/regenerate/stop-report.js +10 -10
  132. package/dist/regenerate/test-advisor.d.ts +101 -0
  133. package/dist/regenerate/test-advisor.js +379 -0
  134. package/dist/regenerate/test-kinds.d.ts +69 -31
  135. package/dist/regenerate/test-kinds.js +109 -80
  136. package/dist/regenerate/test-scripts.d.ts +2 -0
  137. package/dist/regenerate/test-scripts.js +4 -0
  138. package/dist/regenerate/testing-steps.d.ts +41 -0
  139. package/dist/regenerate/testing-steps.js +110 -0
  140. package/dist/regenerate/testing.d.ts +1 -1
  141. package/dist/regenerate/testing.js +5 -4
  142. package/dist/regenerate/unit-review.d.ts +1 -1
  143. package/dist/regenerate/unit-review.js +3 -3
  144. package/dist/regenerate-cli.js +11 -12
  145. package/dist/release-agent.js +4 -0
  146. package/dist/release-cli.js +2 -0
  147. package/dist/release-tools.js +2 -0
  148. package/dist/run-report.d.ts +2 -2
  149. package/dist/run-report.js +4 -3
  150. package/dist/self-invocation.d.ts +1 -1
  151. package/dist/self-invocation.js +1 -1
  152. package/dist/sentinel.js +1 -1
  153. package/dist/setup.d.ts +10 -0
  154. package/dist/setup.js +52 -4
  155. package/dist/skill.d.ts +10 -0
  156. package/dist/skill.js +6 -0
  157. package/dist/skills.js +3 -1
  158. package/dist/spec-tools.js +21 -31
  159. package/dist/test-plain-files.d.ts +41 -0
  160. package/dist/test-plain-files.js +177 -0
  161. package/docs/README.md +3 -1
  162. package/docs/authoring-plain.md +46 -13
  163. package/docs/chats.md +3 -1
  164. package/docs/config.md +20 -15
  165. package/docs/deploy.md +1 -1
  166. package/docs/development.md +2 -1
  167. package/docs/headless.md +15 -18
  168. package/docs/hosting.md +28 -12
  169. package/docs/installation.md +1 -0
  170. package/docs/parser.md +88 -27
  171. package/docs/regenerating.md +89 -54
  172. package/docs/sentinel.md +1 -1
  173. package/docs/skills-and-gates.md +3 -1
  174. package/docs/test-advisor.md +110 -0
  175. package/docs/theming.md +1 -1
  176. package/docs/using.md +17 -15
  177. package/genome/rules/bullet-continuation.md +8 -8
  178. package/genome/rules/concise-specs-examples.md +111 -80
  179. package/genome/rules/concise-specs-integration-examples.md +56 -41
  180. package/genome/rules/concise-specs.md +17 -16
  181. package/genome/rules/definitions.md +5 -5
  182. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
  183. package/genome/rules/func-specs.md +69 -74
  184. package/genome/rules/impl-reqs.md +4 -4
  185. package/genome/rules/integration-embedded-testing.md +20 -20
  186. package/genome/rules/integration-embedded.md +31 -31
  187. package/genome/rules/integration-standalone.md +9 -9
  188. package/genome/rules/integrations.md +18 -18
  189. package/genome/rules/linked-resources.md +19 -16
  190. package/genome/rules/module-structure.md +21 -14
  191. package/genome/rules/project-model.md +18 -17
  192. package/genome/rules/required-concepts.md +1 -1
  193. package/genome/rules/requires-modules.md +6 -5
  194. package/genome/rules/semantic-ids.md +49 -0
  195. package/genome/rules/stdlib.md +4 -5
  196. package/genome/rules/test-plain-files.md +130 -0
  197. package/genome/rules/test-reqs.md +8 -8
  198. package/genome/rules/turri-regeneration.md +24 -8
  199. package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
  200. package/genome/skills/analyze-func-specs/SKILL.md +46 -32
  201. package/genome/skills/break-down-func-spec/SKILL.md +91 -67
  202. package/genome/skills/consolidate-concepts/SKILL.md +10 -10
  203. package/genome/skills/convert-old-syntax/SKILL.md +68 -0
  204. package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
  205. package/genome/skills/debug-specs/SKILL.md +62 -56
  206. package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
  207. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
  208. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
  209. package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
  210. package/genome/skills/refactor-module/SKILL.md +35 -35
  211. package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
  212. package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
  213. package/package.json +7 -6
  214. package/dist/regenerate/choose-conformance.d.ts +0 -32
  215. package/dist/regenerate/choose-conformance.js +0 -117
  216. /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
@@ -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, labelledFiles, 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,27 +340,11 @@ 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
- // The functional specs of this module the job regenerates again, and of those, the ones whose tests this run has
349
- // not written yet: no test file labelled with it added or changed since the run began.
350
- const againFrids = (options.cases?.again ?? []).filter((spec) => spec.module === undefined || spec.module === moduleName).map((spec) => spec.frid);
351
- const rewrittenAgain = () => {
352
- const changes = diffFiles(testsAtStart, digestFiles(conformanceDir));
353
- const touchedFiles = new Set([...changes.added, ...changes.changed]);
354
- return againFrids.filter((id) => labelledFiles(conformanceDir, id).some((file) => touchedFiles.has(file)));
355
- };
356
- const staleAgain = () => {
357
- const done = new Set(rewrittenAgain());
358
- return againFrids.filter((id) => !done.has(id));
359
- };
360
- let againAsks = 0;
361
348
  const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
362
349
  let verified = false;
363
350
  let rounds = 0;
@@ -383,10 +370,9 @@ export async function renderConformanceTests(options) {
383
370
  await askFixer(removalChallengePrompt(unitRemovals, "unit tests"));
384
371
  continue;
385
372
  }
386
- // The conformance tests and each acceptance test have to be there, and labelled as what
387
- // 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.
388
374
  if (options.diff === undefined) {
389
- const missing = missingTestLabels(conformanceDir, frid, specifications[ACCEPTANCE_TESTS] ?? []);
375
+ const missing = missingTestLabels(conformanceDir, moduleName, carriedOut[0]);
390
376
  if (missing.length > 0) {
391
377
  if (labelAsks >= LABEL_ASKS) {
392
378
  stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
@@ -394,16 +380,19 @@ export async function renderConformanceTests(options) {
394
380
  break;
395
381
  }
396
382
  labelAsks += 1;
397
- stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the conformance and acceptance tests");
398
- 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);
399
385
  continue;
400
386
  }
401
387
  }
402
388
  else {
403
389
  // A spec change: every functionality it touches, as a functionality's own run would have it - every one missing
404
390
  // asked for in one request, checked whole again after each answer, so none is asked for in place of another.
405
- const pending = touched
406
- .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) }))
407
396
  .filter((item) => item.missing.length > 0);
408
397
  if (pending.length > 0) {
409
398
  if (labelAsks >= LABEL_ASKS) {
@@ -412,25 +401,24 @@ export async function renderConformanceTests(options) {
412
401
  break;
413
402
  }
414
403
  labelAsks += 1;
415
- stage(`labelling the conformance and acceptance tests of functionalit${pending.length === 1 ? "y" : "ies"} ${pending.map((item) => item.frid).join(", ")}, which the change touches`);
416
- const acceptance = touched.some((item) => item.acceptanceTests.length > 0);
417
- 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);
418
406
  continue;
419
407
  }
420
- }
421
- // Regenerated again: its tests were written against the code it replaced, and their passing says nothing of the new
422
- // code - they pass on what only the old code did as readily. Its tests have to be written again in this run.
423
- const stale = staleAgain();
424
- if (stale.length > 0) {
425
- if (againAsks >= LABEL_ASKS) {
426
- stage(`the tests of functionalit${stale.length === 1 ? "y" : "ies"} ${stale.join(", ")}, regenerated again, were sent back ${LABEL_ASKS} times to be written again, and still are not - stopping`);
427
- unlabelled = stale.map((id) => `The tests of functionality ${id}, regenerated again, were not written again for the new code: no test file labelled with it changed.`);
428
- break;
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);
420
+ continue;
429
421
  }
430
- againAsks += 1;
431
- stage(`the tests of functionalit${stale.length === 1 ? "y" : "ies"} ${stale.join(", ")}, regenerated again, are as they were - the conformance agent writes them again`);
432
- response = await askConformance(againPrompt(stale), options.onDelta);
433
- continue;
434
422
  }
435
423
  // The suite is the tests folder: a conformance test in the code folder is in the wrong place,
436
424
  // and not the coding agent's to write.
@@ -489,7 +477,6 @@ export async function renderConformanceTests(options) {
489
477
  scope,
490
478
  testingScripts,
491
479
  question: { failure: shownOutput(buildDir, "conformance-stuck", recheck.output), account: invocation },
492
- acceptance: toCarryOut,
493
480
  label,
494
481
  model: options.model,
495
482
  onDelta: options.onDelta,
@@ -625,8 +612,8 @@ export async function renderConformanceTests(options) {
625
612
  ...ref,
626
613
  plainFile: files.get(ref.module) ?? options.plainFile,
627
614
  text: ref.module === moduleName
628
- ? (ownSpecs[frids.indexOf(ref.frid)] ?? "")
629
- : (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 ?? ""),
630
617
  })),
631
618
  reasons: said,
632
619
  });
@@ -723,7 +710,7 @@ export async function renderConformanceTests(options) {
723
710
  // After the last review's problems were worked on, the tests go on once they pass: not reviewed again. With
724
711
  // the review off in the config, they go on once they pass from the first.
725
712
  if (reviewOn && changedTests.length > 0 && failedReviews < REVIEW_LIMIT) {
726
- 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`);
727
714
  // One reviewer for the run, made the first time the tests are reviewed.
728
715
  reviewer ??= await createTestReviewer({
729
716
  ...(options.cases !== undefined ? { cases: options.cases } : {}),
@@ -757,11 +744,11 @@ export async function renderConformanceTests(options) {
757
744
  if (failedReviews > REVIEW_ROUNDS) {
758
745
  stage(`the review sent the tests back ${failedReviews === 1 ? "once" : `${failedReviews} times`} - the arbiter weighs what it found`);
759
746
  const ruling = await arbitrateReview({
760
- 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}`}`,
761
748
  workdir: conformanceDir,
762
749
  read: [conformanceDir, buildDir],
763
750
  uphold: [
764
- "- 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",
765
752
  " the specification states missing, cut down or skipping what it checks, the software mocked or not reached",
766
753
  " from outside, a test that cannot fail. Then the reviewer is right, and the conformance agent fixes it.",
767
754
  ],
@@ -829,20 +816,20 @@ export async function renderConformanceTests(options) {
829
816
  const changed = codeChanged();
830
817
  if (verified) {
831
818
  // The tests, and the code when it changed: one commit if they share a module folder.
832
- 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)" : ""}`;
833
820
  stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
834
821
  landWork(conformanceDir, subject);
835
822
  if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
836
823
  landWork(buildDir, subject);
837
824
  }
838
825
  else if (start !== undefined) {
839
- 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);
840
827
  stage(`${why} - undoing them`);
841
828
  abandonWork(start, moduleName, why);
842
829
  }
843
830
  return {
844
831
  moduleName,
845
- frid: frid ?? "",
832
+ funcId: funcId ?? "",
846
833
  buildDir,
847
834
  conformanceDir,
848
835
  prompt: agentPrompt,
@@ -857,7 +844,6 @@ export async function renderConformanceTests(options) {
857
844
  ...(stuck !== undefined ? { stuck } : {}),
858
845
  ...(escalation !== undefined ? { escalation } : {}),
859
846
  ...(unlabelled !== undefined ? { unlabelled } : {}),
860
- ...(againFrids.length > 0 ? { rewritten: rewrittenAgain() } : {}),
861
847
  ...(reviewWaived !== undefined ? { reviewWaived } : {}),
862
848
  ...(verified && green !== undefined ? { green } : {}),
863
849
  ...(!verified && lastReview !== undefined ? { review: lastReview } : {}),
@@ -870,18 +856,6 @@ export async function renderConformanceTests(options) {
870
856
  await fixerServices.stopAll();
871
857
  }
872
858
  }
873
- /** Sent back when the tests of a functional spec regenerated again are as they were: they are to be written for the new code. */
874
- export function againPrompt(frids) {
875
- const which = frids.length === 1 ? `functionality ${frids[0]}` : `functionalities ${frids.join(", ")}`;
876
- return [
877
- `${which[0].toUpperCase()}${which.slice(1)} ${frids.length === 1 ? "is" : "are"} regenerated again: ${frids.length === 1 ? "its" : "their"} code was built anew, and no test file labelled with ${frids.length === 1 ? "it" : "them"} has changed.`,
878
- "Tests that pass prove nothing here: they were written against the code it replaced, and pass on what only the old",
879
- "code did as readily as on what the specification says. Read the new code, and write the tests again for it: what",
880
- "the specification says, checked as the new code is reached; what it leaves open - a path, a field, a message, a",
881
- "format the old code chose - checked as the new code does it, or taken out. Keep each label. The tests of",
882
- "everything else stay as they are.",
883
- ].join("\n");
884
- }
885
859
  /**
886
860
  * Why conformance tests did not land, in a line. Tests sent back to be labelled and never labelled may well pass: the
887
861
  * line says so, rather than that they failed.
@@ -901,23 +875,41 @@ function plainFilesOf(plainFile, templateDirs) {
901
875
  }
902
876
  }
903
877
  /**
904
- * The functionalities a spec change touches - its functional spec, or one of its acceptance tests,
905
- * 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
906
880
  * conformance run labels, checks and reviews, as a functionality's run does for its one.
907
881
  */
908
- 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'.
909
900
  const bare = (line) => line.trim().replace(/^-\s+/, "").replace(/\s+/g, " ");
910
901
  const changed = new Set(diff
911
902
  .split("\n")
912
903
  .filter((line) => /^[+-]/.test(line) && !/^(\+\+\+|---)/.test(line))
913
904
  .map((line) => bare(line.slice(1)))
914
905
  .filter((line) => line.length > 12));
915
- return frids.flatMap((frid) => {
916
- const specifications = getSpecificationsForFrid(tree, frid)[0];
917
- const spec = specifications[FUNCTIONAL_REQUIREMENTS].at(-1) ?? "";
918
- const acceptanceTests = specifications[ACCEPTANCE_TESTS] ?? [];
919
- const lines = [spec, ...acceptanceTests].flatMap((text) => text.split("\n")).map(bare);
920
- 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] : [];
921
913
  });
922
914
  }
923
915
  /** From the how-manieth review that sends the tests back the arbiter weighs each one (see arbiter.ts). */
@@ -1002,14 +994,14 @@ export function failureLine(run) {
1002
994
  }
1003
995
  /**
1004
996
  * The spec as the explorer, the judge and the coding agent are shown it - its functional specs
1005
- * 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
1006
998
  * out labelled as their tests are, so a verdict can quote them.
1007
999
  */
1008
- function specText(specifications, moduleName, frids, required, acceptance, diff, modules) {
1000
+ function specText(specifications, moduleName, funcIds, required, carriedOut, diff, modules) {
1009
1001
  // Every functionality in the codebase, each with its module: the required modules' first, then this one's.
1010
1002
  const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
1011
1003
  ? [
1012
- 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),
1013
1005
  ]
1014
1006
  : items;
1015
1007
  // Each section in its tag, as every agent is shown it (see specPrimer).
@@ -1017,22 +1009,27 @@ function specText(specifications, moduleName, frids, required, acceptance, diff,
1017
1009
  .map((key) => [key, specifications[key] ?? []])
1018
1010
  .filter(([, items]) => items.length > 0)
1019
1011
  .map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
1020
- // Not only the last functionality's: a spec change's run carries out the acceptance tests of every one it touches.
1021
- const tests = acceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `${acceptanceLabel(item.frid, index + 1)}: ${acceptanceText(test)}`));
1022
- if (tests.length > 0)
1023
- 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);
1024
1016
  if (diff !== undefined)
1025
- sections.push(specChangeBlock(diff));
1017
+ sections.push(specChangeBlock(diff, true));
1026
1018
  return sections.join("\n\n");
1027
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
+ }
1028
1024
  /**
1029
1025
  * The functional spec a conformance run writes tests for, for the person watching: which one of how
1030
- * 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.
1031
1027
  */
1032
- export function buildingSpec(moduleName, frid, total, specifications) {
1033
- 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()}`)] : [];
1034
1030
  return {
1035
- label: `${moduleName} functionality ${frid} of ${total}`,
1036
- 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"),
1037
1034
  };
1038
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("\\"))