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