@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
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, extname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { ask, BEGIN, createExecutor, refuseIfModelIsBlind } from "../pi.js";
|
|
4
4
|
import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js";
|
|
5
|
-
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
5
|
+
import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
|
|
7
7
|
import { confinedTools } from "./confine.js";
|
|
8
8
|
import { abandonWork, diffSinceSnapshot, landWork, moduleDirOf, startWork, workDiff, workSnapshot } from "./lineage.js";
|
|
@@ -10,7 +10,7 @@ import { firstLine, record } from "./ledger.js";
|
|
|
10
10
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
11
11
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
12
12
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
13
|
-
import {
|
|
13
|
+
import { labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels, orphanedLabels, orphansPrompt, testedSpecsOf } from "./test-kinds.js";
|
|
14
14
|
import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
|
|
15
15
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
16
16
|
import { createMyDiffTool } from "./self-check.js";
|
|
@@ -24,9 +24,9 @@ import { reportSpecConflict } from "./activity.js";
|
|
|
24
24
|
import { ATTEMPTS } from "./stop-report.js";
|
|
25
25
|
import { ARBITER_AFTER, arbitrateReview, dismissedNote, REVIEW_LIMIT } from "./arbiter.js";
|
|
26
26
|
import { sectionsOf } from "./state.js";
|
|
27
|
-
import { configForSpec, inheritModel, modelOnly, modelSpec } from "../config.js";
|
|
27
|
+
import { configForSpec, inheritModel, modelOnly, modelSpec, testDirOf } from "../config.js";
|
|
28
28
|
import { defaultBuildDir, modulesRoot, testsDirUnder, projectDirOf } from "./paths.js";
|
|
29
|
-
import { buildConformancePrompt, functionalitiesByModule, moduleSection, section, sectionModules, specChangeBlock } from "./prompt.js";
|
|
29
|
+
import { buildConformancePrompt, functionalitiesByModule, moduleSection, scenariosBlock, section, sectionModules, specChangeBlock } from "./prompt.js";
|
|
30
30
|
import { diffFiles, digestFiles } from "./codebase.js";
|
|
31
31
|
import { createCopyResourceTool, loadLinkedResources, resourceImages } from "./resources.js";
|
|
32
32
|
import { resolveTemplateDirs } from "./paths.js";
|
|
@@ -53,16 +53,16 @@ export async function renderConformanceTests(options) {
|
|
|
53
53
|
const templateDirs = resolveTemplateDirs(options.plainFile, options.templateDirs);
|
|
54
54
|
const [moduleName, tree] = options.spec
|
|
55
55
|
? [options.spec.moduleName, options.spec.tree]
|
|
56
|
-
: plainFileParser(basename(options.plainFile), templateDirs);
|
|
56
|
+
: plainFileParser(basename(options.plainFile), templateDirs, { testDir: testDirOf(options.plainFile) });
|
|
57
57
|
// For a spec change, the tests follow the change, whatever it touches - a definition, a req -
|
|
58
58
|
// with or without a functional spec yet; for one functionality, they are that functionality's.
|
|
59
59
|
const forChange = options.diff !== undefined;
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
60
|
+
const funcIds = [...getFrids(tree)];
|
|
61
|
+
const funcId = options.funcId ?? (forChange ? funcIds.at(-1) : funcIds[0]);
|
|
62
|
+
if (funcId === undefined && !forChange)
|
|
63
63
|
throw new Error(`Module '${moduleName}' has no functionalities.`);
|
|
64
|
-
if (
|
|
65
|
-
throw new Error(`Functionality ${
|
|
64
|
+
if (funcId !== undefined && !funcIds.includes(funcId)) {
|
|
65
|
+
throw new Error(`Functionality ${funcId} does not exist. Available: ${funcIds.join(", ")}.`);
|
|
66
66
|
}
|
|
67
67
|
const buildDir = resolve(options.buildDir ?? defaultBuildDir(moduleName, projectDirOf(options.plainFile)));
|
|
68
68
|
// The agents that give a verdict - the judge, the test review - may run on a stronger model than the rest.
|
|
@@ -70,11 +70,11 @@ export async function renderConformanceTests(options) {
|
|
|
70
70
|
const conformanceDir = resolve(options.conformanceDir ?? conformanceDirFor(moduleName, modulesRoot(projectDirOf(options.plainFile))));
|
|
71
71
|
const stage = (label) => {
|
|
72
72
|
if (!options.dryRun)
|
|
73
|
-
reportStage(`${forChange ? unitOf(moduleName) : unitOf(moduleName,
|
|
73
|
+
reportStage(`${forChange ? unitOf(moduleName) : unitOf(moduleName, funcId)}: ${label}`);
|
|
74
74
|
};
|
|
75
|
-
stage(options.diff !== undefined ? "conformance tests for the spec change" : `conformance tests for functionality ${
|
|
75
|
+
stage(options.diff !== undefined ? "conformance tests for the spec change" : `conformance tests for functionality ${funcId}`);
|
|
76
76
|
if (!options.dryRun && !forChange)
|
|
77
|
-
reportSpec(buildingSpec(moduleName,
|
|
77
|
+
reportSpec(buildingSpec(moduleName, funcId, funcIds.length, getSpecificationsForFrid(tree, funcId)[0]));
|
|
78
78
|
// From the last known good state, unless this step continues work not yet committed.
|
|
79
79
|
// On its own it is one unit - its tests, and any change to the code - landed whole or undone
|
|
80
80
|
// whole. Continuing work already under way, it is part of that work's unit instead.
|
|
@@ -90,9 +90,9 @@ export async function renderConformanceTests(options) {
|
|
|
90
90
|
}
|
|
91
91
|
// The spec as it stands: every section, and the functional specs - none, for a change made
|
|
92
92
|
// before any was written.
|
|
93
|
-
const specifications =
|
|
94
|
-
? getSpecificationsForFrid(tree,
|
|
95
|
-
: { ...sectionsOf(tree), [FUNCTIONAL_REQUIREMENTS]: [], [ACCEPTANCE_TESTS]: [] };
|
|
93
|
+
const specifications = funcId !== undefined
|
|
94
|
+
? getSpecificationsForFrid(tree, funcId)[0]
|
|
95
|
+
: { ...sectionsOf(tree), [FUNCTIONAL_REQUIREMENTS]: [], [CONFORMANCE_TESTS]: [], [ACCEPTANCE_TESTS]: [] };
|
|
96
96
|
// Conformance tests are written from the test reqs: they say how the suite is built and
|
|
97
97
|
// run. Without them there is nothing to write tests against, and an agent asked anyway
|
|
98
98
|
// would invent a convention that nothing else in the project shares.
|
|
@@ -101,6 +101,11 @@ export async function renderConformanceTests(options) {
|
|
|
101
101
|
`Add a ${TEST_REQUIREMENTS} section saying how conformance tests are written and run, or render ` +
|
|
102
102
|
"with conformance turned off.");
|
|
103
103
|
}
|
|
104
|
+
// A functional spec's tests are its scenarios': one with none - no test plain file - has nothing to test.
|
|
105
|
+
if (!forChange && (specifications[CONFORMANCE_TESTS] ?? []).length + (specifications[ACCEPTANCE_TESTS] ?? []).length === 0) {
|
|
106
|
+
throw new Error(`${moduleName}:${funcId} has no scenarios - no test plain file - so there is nothing to test: write its conformance or ` +
|
|
107
|
+
"acceptance scenarios first.");
|
|
108
|
+
}
|
|
104
109
|
// Whether the tests pass is the project's conformance testing script - and, when the code is
|
|
105
110
|
// changed, its unit testing script: without them, nothing is written.
|
|
106
111
|
stage("finding the project's testing scripts");
|
|
@@ -108,7 +113,7 @@ export async function renderConformanceTests(options) {
|
|
|
108
113
|
const scripts = findTestScripts(root, options.plainFile, buildDir);
|
|
109
114
|
if (!options.dryRun)
|
|
110
115
|
requireTestScripts(moduleName, scripts, { conformance: true });
|
|
111
|
-
const linkedResources = loadLinkedResources(tree, forChange ? null :
|
|
116
|
+
const linkedResources = loadLinkedResources(tree, forChange ? null : funcId, templateDirs);
|
|
112
117
|
// The spec's images need a model that reads them: refused before any of its agents starts.
|
|
113
118
|
if (!options.dryRun)
|
|
114
119
|
await refuseIfModelIsBlind(options.model, resourceImages(linkedResources).length);
|
|
@@ -116,8 +121,9 @@ export async function renderConformanceTests(options) {
|
|
|
116
121
|
const prompt = withTestingScripts(buildConformancePrompt({
|
|
117
122
|
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
118
123
|
moduleName,
|
|
119
|
-
|
|
124
|
+
funcId,
|
|
120
125
|
specifications,
|
|
126
|
+
funcIds,
|
|
121
127
|
modules: sectionModules(tree, moduleName),
|
|
122
128
|
linkedResources,
|
|
123
129
|
manifest,
|
|
@@ -129,7 +135,7 @@ export async function renderConformanceTests(options) {
|
|
|
129
135
|
if (options.dryRun) {
|
|
130
136
|
return {
|
|
131
137
|
moduleName,
|
|
132
|
-
|
|
138
|
+
funcId: funcId ?? "",
|
|
133
139
|
buildDir,
|
|
134
140
|
conformanceDir,
|
|
135
141
|
prompt,
|
|
@@ -157,18 +163,18 @@ export async function renderConformanceTests(options) {
|
|
|
157
163
|
// The code and its tests are rendered apart: the tests in their own folder, by an agent that
|
|
158
164
|
// may read the code but writes only tests. An explorer reads the code first and writes down how the
|
|
159
165
|
// software is invoked, which is what the tests are written against.
|
|
160
|
-
const label = `${moduleName} ${options.diff !== undefined ? "spec change" :
|
|
161
|
-
// For a spec change: the functionalities it touches, whose
|
|
162
|
-
const touched = forChange ? touchedFunctionalities(tree,
|
|
163
|
-
// The
|
|
164
|
-
const carriedOut = forChange ? touched : [
|
|
165
|
-
const specContext = { moduleName, text: specText(specifications, moduleName,
|
|
166
|
+
const label = `${moduleName} ${options.diff !== undefined ? "spec change" : funcId}`;
|
|
167
|
+
// For a spec change: the functionalities it touches, whose scenarios are carried out, labelled, checked and reviewed.
|
|
168
|
+
const touched = forChange ? touchedFunctionalities(tree, funcIds, options.diff, moduleName, options.cases) : [];
|
|
169
|
+
// The scenarios the run's tests carry out: those of every functionality the change touches, or of the one under test.
|
|
170
|
+
const carriedOut = forChange ? touched : [testedSpecOf(specifications, funcId)];
|
|
171
|
+
const specContext = { moduleName, text: specText(specifications, moduleName, funcIds, required, carriedOut, options.change ?? options.diff, sectionModules(tree, moduleName)) };
|
|
166
172
|
// Any functionality of the codebase - this module's, and those of the modules it requires. A
|
|
167
173
|
// functionality's run may raise a spec conflict only with that functionality; a spec diff's, between any two.
|
|
168
|
-
const own = (forChange ?
|
|
174
|
+
const own = (forChange ? funcIds : funcIds.slice(0, funcIds.indexOf(funcId) + 1)).map((id) => ({ module: moduleName, funcId: id }));
|
|
169
175
|
const scope = {
|
|
170
|
-
functionalities: [...required.map((spec) => ({ module: spec.module,
|
|
171
|
-
...(forChange ? {} : { tested: { module: moduleName,
|
|
176
|
+
functionalities: [...required.map((spec) => ({ module: spec.module, funcId: spec.funcId })), ...own],
|
|
177
|
+
...(forChange ? { touchedModules: [...new Set(touched.map((item) => item.module ?? moduleName))] } : { tested: { module: moduleName, funcId: funcId } }),
|
|
172
178
|
};
|
|
173
179
|
/** How many times each spec conflict has been raised in this run, by the two functionalities it names. */
|
|
174
180
|
const conflicts = new Map();
|
|
@@ -232,17 +238,14 @@ export async function renderConformanceTests(options) {
|
|
|
232
238
|
: []),
|
|
233
239
|
{ path: relative(root, scripts.conformance), runs: "<script> <copy of the build> <copy of the whole conformance suite>", content: readFileSync(scripts.conformance, "utf8") },
|
|
234
240
|
];
|
|
235
|
-
|
|
236
|
-
const toCarryOut = carriedOut
|
|
237
|
-
.map((item) => ({ module: moduleName, frid: item.frid, count: item.acceptanceTests.length }))
|
|
238
|
-
.filter((item) => item.count > 0);
|
|
239
|
-
let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, acceptance: toCarryOut, label, model: options.model, onDelta: options.onDelta });
|
|
241
|
+
let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, label, model: options.model, onDelta: options.onDelta });
|
|
240
242
|
reportActivity(` how to invoke it: ${invocation.split("\n")[0].slice(0, 120)}`);
|
|
241
243
|
const agentPrompt = withTestingScripts(buildConformancePrompt({
|
|
242
244
|
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
243
245
|
moduleName,
|
|
244
|
-
|
|
246
|
+
funcId,
|
|
245
247
|
specifications,
|
|
248
|
+
funcIds,
|
|
246
249
|
modules: sectionModules(tree, moduleName),
|
|
247
250
|
linkedResources,
|
|
248
251
|
manifest,
|
|
@@ -337,27 +340,11 @@ export async function renderConformanceTests(options) {
|
|
|
337
340
|
// What the tests were when this run began: what it adds or changes is what the review gate reads.
|
|
338
341
|
const testsAtStart = digestFiles(conformanceDir);
|
|
339
342
|
let unitInventory = inventoryTests(buildDir);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
: options.diff !== undefined
|
|
345
|
-
? "turri is bringing the conformance tests in line with the spec change"
|
|
346
|
-
: "turri is writing conformance tests from the spec");
|
|
343
|
+
const scenarios = carriedOut.reduce((count, item) => count + item.conformance.length + item.acceptance.length, 0);
|
|
344
|
+
stage(options.diff !== undefined
|
|
345
|
+
? "turri is bringing the conformance tests in line with the spec change"
|
|
346
|
+
: `turri is writing the conformance tests of its ${scenarios} scenario${scenarios === 1 ? "" : "s"}`);
|
|
347
347
|
let response = await askConformance(BEGIN, options.onDelta);
|
|
348
|
-
// The functional specs of this module the job regenerates again, and of those, the ones whose tests this run has
|
|
349
|
-
// not written yet: no test file labelled with it added or changed since the run began.
|
|
350
|
-
const againFrids = (options.cases?.again ?? []).filter((spec) => spec.module === undefined || spec.module === moduleName).map((spec) => spec.frid);
|
|
351
|
-
const rewrittenAgain = () => {
|
|
352
|
-
const changes = diffFiles(testsAtStart, digestFiles(conformanceDir));
|
|
353
|
-
const touchedFiles = new Set([...changes.added, ...changes.changed]);
|
|
354
|
-
return againFrids.filter((id) => labelledFiles(conformanceDir, id).some((file) => touchedFiles.has(file)));
|
|
355
|
-
};
|
|
356
|
-
const staleAgain = () => {
|
|
357
|
-
const done = new Set(rewrittenAgain());
|
|
358
|
-
return againFrids.filter((id) => !done.has(id));
|
|
359
|
-
};
|
|
360
|
-
let againAsks = 0;
|
|
361
348
|
const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
362
349
|
let verified = false;
|
|
363
350
|
let rounds = 0;
|
|
@@ -383,10 +370,9 @@ export async function renderConformanceTests(options) {
|
|
|
383
370
|
await askFixer(removalChallengePrompt(unitRemovals, "unit tests"));
|
|
384
371
|
continue;
|
|
385
372
|
}
|
|
386
|
-
//
|
|
387
|
-
// they are, before anything else counts.
|
|
373
|
+
// A test for each scenario has to be there, and labelled as the scenario it carries out, before anything else counts.
|
|
388
374
|
if (options.diff === undefined) {
|
|
389
|
-
const missing = missingTestLabels(conformanceDir,
|
|
375
|
+
const missing = missingTestLabels(conformanceDir, moduleName, carriedOut[0]);
|
|
390
376
|
if (missing.length > 0) {
|
|
391
377
|
if (labelAsks >= LABEL_ASKS) {
|
|
392
378
|
stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
|
|
@@ -394,16 +380,19 @@ export async function renderConformanceTests(options) {
|
|
|
394
380
|
break;
|
|
395
381
|
}
|
|
396
382
|
labelAsks += 1;
|
|
397
|
-
stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the
|
|
398
|
-
response = await askConformance(labelsPrompt(
|
|
383
|
+
stage(listFiles(conformanceDir).length === 0 ? "no tests in the tests folder - the conformance agent writes them" : "labelling the tests of the scenarios");
|
|
384
|
+
response = await askConformance(labelsPrompt(moduleName, funcId, missing, listFiles(conformanceDir).length === 0), options.onDelta);
|
|
399
385
|
continue;
|
|
400
386
|
}
|
|
401
387
|
}
|
|
402
388
|
else {
|
|
403
389
|
// A spec change: every functionality it touches, as a functionality's own run would have it - every one missing
|
|
404
390
|
// asked for in one request, checked whole again after each answer, so none is asked for in place of another.
|
|
405
|
-
|
|
406
|
-
|
|
391
|
+
// And every other functional spec the tests are written for: each of its scenarios has its labelled test too.
|
|
392
|
+
const touchedKeys = new Set(touched.map((item) => `${item.module ?? moduleName}:${item.funcId}`));
|
|
393
|
+
const others = testedSpecsOf(options.recorded).filter((item) => !touchedKeys.has(`${item.module ?? moduleName}:${item.funcId}`));
|
|
394
|
+
const pending = [...touched, ...others]
|
|
395
|
+
.map((item) => ({ funcId: item.funcId, ...(item.module !== undefined ? { module: item.module } : {}), missing: missingTestLabels(conformanceDir, moduleName, item) }))
|
|
407
396
|
.filter((item) => item.missing.length > 0);
|
|
408
397
|
if (pending.length > 0) {
|
|
409
398
|
if (labelAsks >= LABEL_ASKS) {
|
|
@@ -412,25 +401,24 @@ export async function renderConformanceTests(options) {
|
|
|
412
401
|
break;
|
|
413
402
|
}
|
|
414
403
|
labelAsks += 1;
|
|
415
|
-
stage(`labelling the
|
|
416
|
-
|
|
417
|
-
response = await askConformance(labelsPromptFor(pending, listFiles(conformanceDir).length === 0, acceptance), options.onDelta);
|
|
404
|
+
stage(`labelling the tests of the scenarios of ${pending.map((item) => `${item.module ?? moduleName}:${item.funcId}`).join(", ")}, which the change touches`);
|
|
405
|
+
response = await askConformance(labelsPromptFor(moduleName, pending, listFiles(conformanceDir).length === 0), options.onDelta);
|
|
418
406
|
continue;
|
|
419
407
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
408
|
+
// A label that names nothing the tests are written for - a scenario or a functional spec taken out, a label
|
|
409
|
+
// mistyped, a test invented beside the scenarios: sent back to be removed or relabelled.
|
|
410
|
+
const orphans = options.recorded !== undefined ? orphanedLabels(conformanceDir, moduleName, testedSpecsOf(options.recorded)) : [];
|
|
411
|
+
if (orphans.length > 0) {
|
|
412
|
+
if (labelAsks >= LABEL_ASKS) {
|
|
413
|
+
stage(`the tests were sent back ${LABEL_ASKS} times to be written and labelled, and still are not - stopping`);
|
|
414
|
+
unlabelled = orphans.map((orphan) => `${orphan.file} is labelled "${orphan.label}", which names nothing the tests are written for.`);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
labelAsks += 1;
|
|
418
|
+
stage(`${orphans.length} label${orphans.length === 1 ? "" : "s"} in the tests name${orphans.length === 1 ? "s" : ""} nothing they are written for - the conformance agent removes or relabels them`);
|
|
419
|
+
response = await askConformance(orphansPrompt(orphans), options.onDelta);
|
|
420
|
+
continue;
|
|
429
421
|
}
|
|
430
|
-
againAsks += 1;
|
|
431
|
-
stage(`the tests of functionalit${stale.length === 1 ? "y" : "ies"} ${stale.join(", ")}, regenerated again, are as they were - the conformance agent writes them again`);
|
|
432
|
-
response = await askConformance(againPrompt(stale), options.onDelta);
|
|
433
|
-
continue;
|
|
434
422
|
}
|
|
435
423
|
// The suite is the tests folder: a conformance test in the code folder is in the wrong place,
|
|
436
424
|
// and not the coding agent's to write.
|
|
@@ -489,7 +477,6 @@ export async function renderConformanceTests(options) {
|
|
|
489
477
|
scope,
|
|
490
478
|
testingScripts,
|
|
491
479
|
question: { failure: shownOutput(buildDir, "conformance-stuck", recheck.output), account: invocation },
|
|
492
|
-
acceptance: toCarryOut,
|
|
493
480
|
label,
|
|
494
481
|
model: options.model,
|
|
495
482
|
onDelta: options.onDelta,
|
|
@@ -625,8 +612,8 @@ export async function renderConformanceTests(options) {
|
|
|
625
612
|
...ref,
|
|
626
613
|
plainFile: files.get(ref.module) ?? options.plainFile,
|
|
627
614
|
text: ref.module === moduleName
|
|
628
|
-
? (ownSpecs[
|
|
629
|
-
: (required.find((spec) => spec.module === ref.module && spec.
|
|
615
|
+
? (ownSpecs[funcIds.indexOf(ref.funcId)] ?? "")
|
|
616
|
+
: (required.find((spec) => spec.module === ref.module && spec.funcId === ref.funcId)?.spec ?? ""),
|
|
630
617
|
})),
|
|
631
618
|
reasons: said,
|
|
632
619
|
});
|
|
@@ -723,7 +710,7 @@ export async function renderConformanceTests(options) {
|
|
|
723
710
|
// After the last review's problems were worked on, the tests go on once they pass: not reviewed again. With
|
|
724
711
|
// the review off in the config, they go on once they pass from the first.
|
|
725
712
|
if (reviewOn && changedTests.length > 0 && failedReviews < REVIEW_LIMIT) {
|
|
726
|
-
stage(`optimising the tests (review ${failedReviews + 1} of at most ${REVIEW_LIMIT}): black box, and
|
|
713
|
+
stage(`optimising the tests (review ${failedReviews + 1} of at most ${REVIEW_LIMIT}): black box, and every scenario carried out`);
|
|
727
714
|
// One reviewer for the run, made the first time the tests are reviewed.
|
|
728
715
|
reviewer ??= await createTestReviewer({
|
|
729
716
|
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
@@ -757,11 +744,11 @@ export async function renderConformanceTests(options) {
|
|
|
757
744
|
if (failedReviews > REVIEW_ROUNDS) {
|
|
758
745
|
stage(`the review sent the tests back ${failedReviews === 1 ? "once" : `${failedReviews} times`} - the arbiter weighs what it found`);
|
|
759
746
|
const ruling = await arbitrateReview({
|
|
760
|
-
subject: `the conformance tests of ${forChange ? `the spec change in module \`${moduleName}\`` : `
|
|
747
|
+
subject: `the conformance tests of ${forChange ? `the spec change in module \`${moduleName}\`` : `functional spec ${moduleName}:${funcId}`}`,
|
|
761
748
|
workdir: conformanceDir,
|
|
762
749
|
read: [conformanceDir, buildDir],
|
|
763
750
|
uphold: [
|
|
764
|
-
"- uphold it when because of it the functionality is not tested black box - a
|
|
751
|
+
"- uphold it when because of it the functionality is not tested black box - a scenario's test",
|
|
765
752
|
" the specification states missing, cut down or skipping what it checks, the software mocked or not reached",
|
|
766
753
|
" from outside, a test that cannot fail. Then the reviewer is right, and the conformance agent fixes it.",
|
|
767
754
|
],
|
|
@@ -829,20 +816,20 @@ export async function renderConformanceTests(options) {
|
|
|
829
816
|
const changed = codeChanged();
|
|
830
817
|
if (verified) {
|
|
831
818
|
// The tests, and the code when it changed: one commit if they share a module folder.
|
|
832
|
-
const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${
|
|
819
|
+
const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${funcId}`}${changed ? " (code changed)" : ""}`;
|
|
833
820
|
stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
|
|
834
821
|
landWork(conformanceDir, subject);
|
|
835
822
|
if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
|
|
836
823
|
landWork(buildDir, subject);
|
|
837
824
|
}
|
|
838
825
|
else if (start !== undefined) {
|
|
839
|
-
const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${
|
|
826
|
+
const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${funcId}`, unlabelled);
|
|
840
827
|
stage(`${why} - undoing them`);
|
|
841
828
|
abandonWork(start, moduleName, why);
|
|
842
829
|
}
|
|
843
830
|
return {
|
|
844
831
|
moduleName,
|
|
845
|
-
|
|
832
|
+
funcId: funcId ?? "",
|
|
846
833
|
buildDir,
|
|
847
834
|
conformanceDir,
|
|
848
835
|
prompt: agentPrompt,
|
|
@@ -857,7 +844,6 @@ export async function renderConformanceTests(options) {
|
|
|
857
844
|
...(stuck !== undefined ? { stuck } : {}),
|
|
858
845
|
...(escalation !== undefined ? { escalation } : {}),
|
|
859
846
|
...(unlabelled !== undefined ? { unlabelled } : {}),
|
|
860
|
-
...(againFrids.length > 0 ? { rewritten: rewrittenAgain() } : {}),
|
|
861
847
|
...(reviewWaived !== undefined ? { reviewWaived } : {}),
|
|
862
848
|
...(verified && green !== undefined ? { green } : {}),
|
|
863
849
|
...(!verified && lastReview !== undefined ? { review: lastReview } : {}),
|
|
@@ -870,18 +856,6 @@ export async function renderConformanceTests(options) {
|
|
|
870
856
|
await fixerServices.stopAll();
|
|
871
857
|
}
|
|
872
858
|
}
|
|
873
|
-
/** Sent back when the tests of a functional spec regenerated again are as they were: they are to be written for the new code. */
|
|
874
|
-
export function againPrompt(frids) {
|
|
875
|
-
const which = frids.length === 1 ? `functionality ${frids[0]}` : `functionalities ${frids.join(", ")}`;
|
|
876
|
-
return [
|
|
877
|
-
`${which[0].toUpperCase()}${which.slice(1)} ${frids.length === 1 ? "is" : "are"} regenerated again: ${frids.length === 1 ? "its" : "their"} code was built anew, and no test file labelled with ${frids.length === 1 ? "it" : "them"} has changed.`,
|
|
878
|
-
"Tests that pass prove nothing here: they were written against the code it replaced, and pass on what only the old",
|
|
879
|
-
"code did as readily as on what the specification says. Read the new code, and write the tests again for it: what",
|
|
880
|
-
"the specification says, checked as the new code is reached; what it leaves open - a path, a field, a message, a",
|
|
881
|
-
"format the old code chose - checked as the new code does it, or taken out. Keep each label. The tests of",
|
|
882
|
-
"everything else stay as they are.",
|
|
883
|
-
].join("\n");
|
|
884
|
-
}
|
|
885
859
|
/**
|
|
886
860
|
* Why conformance tests did not land, in a line. Tests sent back to be labelled and never labelled may well pass: the
|
|
887
861
|
* line says so, rather than that they failed.
|
|
@@ -901,23 +875,41 @@ function plainFilesOf(plainFile, templateDirs) {
|
|
|
901
875
|
}
|
|
902
876
|
}
|
|
903
877
|
/**
|
|
904
|
-
* The functionalities a spec change touches -
|
|
905
|
-
* has a line the change adds or removes - with their
|
|
878
|
+
* The functionalities with scenarios a spec change touches - by the job's cases, else those whose functional spec, or one of
|
|
879
|
+
* whose scenarios, has a line the change adds or removes - with their scenarios: what a spec diff's
|
|
906
880
|
* conformance run labels, checks and reviews, as a functionality's run does for its one.
|
|
907
881
|
*/
|
|
908
|
-
export function touchedFunctionalities(tree,
|
|
882
|
+
export function touchedFunctionalities(tree, funcIds, diff, moduleName, cases) {
|
|
883
|
+
const tested = (funcId) => {
|
|
884
|
+
const spec = testedSpecOf(getSpecificationsForFrid(tree, funcId)[0], funcId);
|
|
885
|
+
return spec.conformance.length + spec.acceptance.length > 0 ? spec : undefined;
|
|
886
|
+
};
|
|
887
|
+
// The job's cases say which: what it adds, changes or regenerates again - the modules' below first, in the chain's
|
|
888
|
+
// order, as the tip's suite holds their tests too, then the module's own.
|
|
889
|
+
if (cases !== undefined) {
|
|
890
|
+
const own = (touched) => touched.module === undefined || touched.module === moduleName;
|
|
891
|
+
const touched = [...cases.add, ...cases.patch, ...cases.again];
|
|
892
|
+
const named = new Set(touched.filter(own).map((spec) => spec.funcId));
|
|
893
|
+
const below = new Set(touched.filter((spec) => !own(spec)).map((spec) => `${spec.module}:${spec.funcId}`));
|
|
894
|
+
const inherited = getInheritedFunctionalSpecs(tree)
|
|
895
|
+
.filter((spec) => below.has(`${spec.module}:${spec.funcId}`) && spec.conformanceTests.length + spec.acceptanceTests.length > 0)
|
|
896
|
+
.map((spec) => ({ funcId: spec.funcId, module: spec.module, conformance: spec.conformanceTests, acceptance: spec.acceptanceTests }));
|
|
897
|
+
return [...inherited, ...funcIds.filter((funcId) => named.has(funcId)).flatMap((funcId) => tested(funcId) ?? [])];
|
|
898
|
+
}
|
|
899
|
+
// Else what the diff's lines touch: a functional spec's text, or one of its scenarios'.
|
|
909
900
|
const bare = (line) => line.trim().replace(/^-\s+/, "").replace(/\s+/g, " ");
|
|
910
901
|
const changed = new Set(diff
|
|
911
902
|
.split("\n")
|
|
912
903
|
.filter((line) => /^[+-]/.test(line) && !/^(\+\+\+|---)/.test(line))
|
|
913
904
|
.map((line) => bare(line.slice(1)))
|
|
914
905
|
.filter((line) => line.length > 12));
|
|
915
|
-
return
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
|
|
906
|
+
return funcIds.flatMap((funcId) => {
|
|
907
|
+
const spec = tested(funcId);
|
|
908
|
+
if (spec === undefined)
|
|
909
|
+
return [];
|
|
910
|
+
const text = getSpecificationsForFrid(tree, funcId)[0][FUNCTIONAL_REQUIREMENTS].at(-1) ?? "";
|
|
911
|
+
const lines = [text, ...spec.conformance.map((scenario) => scenario.text), ...spec.acceptance.map((scenario) => scenario.text)].flatMap((item) => item.split("\n")).map(bare);
|
|
912
|
+
return lines.some((line) => changed.has(line)) ? [spec] : [];
|
|
921
913
|
});
|
|
922
914
|
}
|
|
923
915
|
/** From the how-manieth review that sends the tests back the arbiter weighs each one (see arbiter.ts). */
|
|
@@ -1002,14 +994,14 @@ export function failureLine(run) {
|
|
|
1002
994
|
}
|
|
1003
995
|
/**
|
|
1004
996
|
* The spec as the explorer, the judge and the coding agent are shown it - its functional specs
|
|
1005
|
-
*
|
|
997
|
+
* named by their func-ids, so a spec conflict can name them, and the scenarios the run carries
|
|
1006
998
|
* out labelled as their tests are, so a verdict can quote them.
|
|
1007
999
|
*/
|
|
1008
|
-
function specText(specifications, moduleName,
|
|
1000
|
+
function specText(specifications, moduleName, funcIds, required, carriedOut, diff, modules) {
|
|
1009
1001
|
// Every functionality in the codebase, each with its module: the required modules' first, then this one's.
|
|
1010
1002
|
const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
|
|
1011
1003
|
? [
|
|
1012
|
-
functionalitiesByModule([...required, ...items.map((item, index) => ({ module: moduleName,
|
|
1004
|
+
functionalitiesByModule([...required, ...items.map((item, index) => ({ module: moduleName, funcId: funcIds[index] ?? String(index + 1), spec: item }))], moduleName),
|
|
1013
1005
|
]
|
|
1014
1006
|
: items;
|
|
1015
1007
|
// Each section in its tag, as every agent is shown it (see specPrimer).
|
|
@@ -1017,22 +1009,27 @@ function specText(specifications, moduleName, frids, required, acceptance, diff,
|
|
|
1017
1009
|
.map((key) => [key, specifications[key] ?? []])
|
|
1018
1010
|
.filter(([, items]) => items.length > 0)
|
|
1019
1011
|
.map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
|
|
1020
|
-
// Not only the last functionality's: a spec change's run carries out the
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1023
|
-
sections.push(
|
|
1012
|
+
// Not only the last functionality's: a spec change's run carries out the scenarios of every one it touches.
|
|
1013
|
+
const scenarios = scenariosBlock(moduleName, carriedOut);
|
|
1014
|
+
if (scenarios !== null)
|
|
1015
|
+
sections.push(scenarios);
|
|
1024
1016
|
if (diff !== undefined)
|
|
1025
|
-
sections.push(specChangeBlock(diff));
|
|
1017
|
+
sections.push(specChangeBlock(diff, true));
|
|
1026
1018
|
return sections.join("\n\n");
|
|
1027
1019
|
}
|
|
1020
|
+
/** A functional spec's scenarios, as its specifications carry them: those its conformance tests carry out. */
|
|
1021
|
+
function testedSpecOf(specifications, funcId) {
|
|
1022
|
+
return { funcId, conformance: specifications[CONFORMANCE_TESTS] ?? [], acceptance: specifications[ACCEPTANCE_TESTS] ?? [] };
|
|
1023
|
+
}
|
|
1028
1024
|
/**
|
|
1029
1025
|
* The functional spec a conformance run writes tests for, for the person watching: which one of how
|
|
1030
|
-
* many, and its text as the spec has it, with its
|
|
1026
|
+
* many, and its text as the spec has it, with its scenarios after it.
|
|
1031
1027
|
*/
|
|
1032
|
-
export function buildingSpec(moduleName,
|
|
1033
|
-
const
|
|
1028
|
+
export function buildingSpec(moduleName, funcId, total, specifications) {
|
|
1029
|
+
const kind = (name, scenarios) => (scenarios ?? []).length > 0 ? ["", `${name}:`, ...(scenarios ?? []).map((scenario) => `# ${scenario.id}\n${scenario.text.trim()}`)] : [];
|
|
1034
1030
|
return {
|
|
1035
|
-
|
|
1036
|
-
|
|
1031
|
+
spec: { module: moduleName, funcId },
|
|
1032
|
+
label: `${moduleName}:${funcId}, functional spec of ${total}`,
|
|
1033
|
+
text: [(specifications["functional specs"].at(-1) ?? "").trim(), ...kind("conformance tests", specifications[CONFORMANCE_TESTS]), ...kind("acceptance tests", specifications[ACCEPTANCE_TESTS])].join("\n"),
|
|
1037
1034
|
};
|
|
1038
1035
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PlainRole } from "../plain/highlight.ts";
|
|
2
2
|
/**
|
|
3
|
-
* A spec diff as the panels show it: each line's ***plain highlighted
|
|
3
|
+
* A spec diff as the panels show it: each line's ***plain highlighted by turri's own grammar
|
|
4
4
|
* (`plain/highlight.ts`), an added line on a green band and a removed one on a red, the band the width
|
|
5
5
|
* of the panel - as an editor shows a diff. Each file is named once, quietly, above its hunks - not
|
|
6
6
|
* git's headers - and a hunk header is a quiet rule between hunks.
|
|
@@ -36,6 +36,10 @@ export function locateDiffLines(diff, files) {
|
|
|
36
36
|
});
|
|
37
37
|
const used = new Set();
|
|
38
38
|
let last;
|
|
39
|
+
// A test plain file's part of the diff is that file's alone: its `---` and its section headers are in every one of
|
|
40
|
+
// them, and in the module's frontmatter - found by text anywhere, they would be placed in the wrong file.
|
|
41
|
+
const testPlainFile = texts.map((file) => /^---\n(?:.*\n)*?func_id:/.test(file.written.join("\n")));
|
|
42
|
+
let only;
|
|
39
43
|
const take = (file, line, index) => {
|
|
40
44
|
used.add(`${file}:${line}`);
|
|
41
45
|
const section = sections[file][line];
|
|
@@ -46,14 +50,23 @@ export function locateDiffLines(diff, files) {
|
|
|
46
50
|
.replace(/\n+$/, "")
|
|
47
51
|
.split("\n")
|
|
48
52
|
.forEach((line, index) => {
|
|
49
|
-
|
|
53
|
+
const named = /^\+\+\+ (?:b\/)?(.+)$/.exec(line);
|
|
54
|
+
if (named !== null) {
|
|
55
|
+
const file = texts.findIndex((candidate, at) => testPlainFile[at] === true && (candidate.name === named[1] || candidate.name.endsWith(`/${named[1]}`) || named[1].endsWith(`/${candidate.name}`)));
|
|
56
|
+
only = file === -1 ? undefined : file;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith("diff --git "))
|
|
60
|
+
only = undefined;
|
|
61
|
+
if (HEADER.test(line) || line.startsWith("@@") || line.startsWith("\\"))
|
|
50
62
|
return;
|
|
51
63
|
if (line.startsWith("-")) {
|
|
52
64
|
// A removed line still written in a file - taken out of what is compared, not out of the spec - is that file's.
|
|
53
65
|
const removed = asRead(line.slice(1).trimEnd());
|
|
54
66
|
if (removed.trim() === "" || SECTION.test(removed.trim()))
|
|
55
67
|
return;
|
|
56
|
-
|
|
68
|
+
// In a test plain file a removed line is gone: no line of it is that line any more.
|
|
69
|
+
const file = only !== undefined ? -1 : texts.findIndex((candidate) => candidate.lines.includes(removed));
|
|
57
70
|
if (file !== -1) {
|
|
58
71
|
const at = texts[file].lines.indexOf(removed);
|
|
59
72
|
const section = sections[file][at];
|
|
@@ -64,11 +77,11 @@ export function locateDiffLines(diff, files) {
|
|
|
64
77
|
const text = (line.startsWith("+") || line.startsWith(" ") ? line.slice(1) : line).trimEnd();
|
|
65
78
|
if (text === "") {
|
|
66
79
|
// A blank line: the one right after the line before it, when that is blank.
|
|
67
|
-
if (last !== undefined && texts[last.file].lines[last.line + 1] === "" && !used.has(`${last.file}:${last.line + 1}`))
|
|
80
|
+
if (last !== undefined && (only === undefined || last.file === only) && texts[last.file].lines[last.line + 1] === "" && !used.has(`${last.file}:${last.line + 1}`))
|
|
68
81
|
take(last.file, last.line + 1, index);
|
|
69
82
|
return;
|
|
70
83
|
}
|
|
71
|
-
const order = last === undefined ? texts.map((_, file) => file) : [last.file, ...texts.map((_, file) => file).filter((file) => file !== last.file)];
|
|
84
|
+
const order = only !== undefined ? [only] : last === undefined ? texts.map((_, file) => file) : [last.file, ...texts.map((_, file) => file).filter((file) => file !== last.file)];
|
|
72
85
|
for (const file of order) {
|
|
73
86
|
const lines = texts[file].lines;
|
|
74
87
|
const from = last !== undefined && file === last.file ? last.line + 1 : 0;
|
|
@@ -112,6 +125,9 @@ export function diffFiles(diff, width, paint = PLAIN_PAINT, where) {
|
|
|
112
125
|
rendered ??= headerFile;
|
|
113
126
|
before = [];
|
|
114
127
|
after = [];
|
|
128
|
+
// A new file starts in no section: a test plain file never has the module's.
|
|
129
|
+
sectionBefore = undefined;
|
|
130
|
+
sectionAfter = undefined;
|
|
115
131
|
continue;
|
|
116
132
|
}
|
|
117
133
|
if (HEADER.test(line) || line.startsWith("@@") || line.startsWith("\\"))
|