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