@turri-dev/cli 0.1.27 → 0.1.28-next.d5f2680
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +70 -14
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -13
- package/dist/regenerate/conformance.js +119 -82
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +62 -43
- package/dist/regenerate/loop.js +472 -341
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +71 -28
- package/dist/regenerate/test-kinds.js +110 -63
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +87 -52
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +23 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { advisorModel, configForSpec, readProjectConfig, stackOf, testDirOf } from "../config.js";
|
|
4
|
+
import { plainFileParser } from "../plain/parser.js";
|
|
5
|
+
import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS } from "../plain/spec.js";
|
|
6
|
+
import { tipsAbove } from "../plain/check.js";
|
|
7
|
+
import { conformanceTestsOnIn } from "../conformance-switch.js";
|
|
8
|
+
import { isStdlibModule } from "../plain/stdlib.js";
|
|
9
|
+
import { outsideJobs, withStopSignal } from "./activity.js";
|
|
10
|
+
import { advisorEntry, isCurrent, markDelivered, pendingEntries, putAdvisorEntry, removeAdvisorEntry, textHash, undeliveredEntries, } from "./advisor-records.js";
|
|
11
|
+
import { firstLine, record } from "./ledger.js";
|
|
12
|
+
import { projectDirOf, resolveTemplateDirs } from "./paths.js";
|
|
13
|
+
import { advise as adviseByModel } from "./test-advisor.js";
|
|
14
|
+
/**
|
|
15
|
+
* When the test advisor runs, and on what (docs/test-advisor.md). It watches what the work loop looks at - every
|
|
16
|
+
* ***plain change the loop sees, as it sees it - and takes each functional spec that is new or whose text changed since
|
|
17
|
+
* its code was built (its code record), has no scenarios, and has no entry in the advisor's file for that text. A
|
|
18
|
+
* project already built, with no advisor file, is looked at not at all: only what changes from now on is.
|
|
19
|
+
*
|
|
20
|
+
* Each is marked `pending` in the advisor's file then and there, and judged in the background - two at a time at most,
|
|
21
|
+
* outside `parallel-jobs` and the lanes: no pulse waits for it, and it never steers or stops one. Its work is clear of
|
|
22
|
+
* any pulse: no panel, no log, no stop of a pulse's reaches it. A functional spec whose text changes while it is judged
|
|
23
|
+
* has that work thrown away - whoever changed it - and the new text starts again. What `pending` is left as turri quits,
|
|
24
|
+
* the next start in the chat or a host picks up.
|
|
25
|
+
*
|
|
26
|
+
* The advisor decides and writes nothing in the ***plain: what it made of a functional spec is kept in its file and
|
|
27
|
+
* handed on (`onVerdict`) to the chat the person looks at, which writes the scenarios on a yes. A yes not yet handed to a
|
|
28
|
+
* chat stays marked undelivered, and is handed on again at the next start.
|
|
29
|
+
*
|
|
30
|
+
* It is made only where the session starts in the chat or a host (extension.ts) - never for a headless command, nor
|
|
31
|
+
* pi's print, json and rpc modes - and does nothing in a project whose `test-advisor:` is false.
|
|
32
|
+
*/
|
|
33
|
+
/** How many functional specs the advisor works on at once. */
|
|
34
|
+
export const ADVISOR_PARALLEL = 2;
|
|
35
|
+
/** The functional spec's own place in a module, as its own .plain file has it now - or nothing. */
|
|
36
|
+
function currentItem(plainFile, funcId) {
|
|
37
|
+
try {
|
|
38
|
+
const [, tree] = plainFileParser(basename(plainFile), resolveTemplateDirs(plainFile), { allowNoFunctionalities: true, testDir: testDirOf(plainFile) });
|
|
39
|
+
return (tree[FUNCTIONAL_REQUIREMENTS] ?? []).find((item) => item.id === funcId);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Whether a functional spec has scenarios of any kind: then it is tested as the person wants, and today's review covers it. */
|
|
46
|
+
export function hasScenarios(item) {
|
|
47
|
+
const of = (key) => (item?.[key] ?? []).length;
|
|
48
|
+
return of(CONFORMANCE_TESTS) + of(ACCEPTANCE_TESTS) > 0;
|
|
49
|
+
}
|
|
50
|
+
/** The project's AGENTS.md, whole: its own words on what matters. */
|
|
51
|
+
function agentsText(project) {
|
|
52
|
+
const file = join(project, "AGENTS.md");
|
|
53
|
+
try {
|
|
54
|
+
return existsSync(file) ? [readFileSync(file, "utf8")] : [];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Whether a project wants the advisor: it conformance tests (`conformance-tests: true`), and its `test-advisor:` is not false. */
|
|
61
|
+
function wanted(project) {
|
|
62
|
+
try {
|
|
63
|
+
return conformanceTestsOnIn(project) && readProjectConfig(project, stackOf(project)).testAdvisor !== false;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function createTestAdvisor(options = {}) {
|
|
70
|
+
const enabled = options.enabled ?? wanted;
|
|
71
|
+
const advise = options.advise ?? adviseByModel;
|
|
72
|
+
const parallel = options.parallel ?? ADVISOR_PARALLEL;
|
|
73
|
+
const queue = [];
|
|
74
|
+
const running = new Map();
|
|
75
|
+
let idleWaiters = [];
|
|
76
|
+
let stopped = false;
|
|
77
|
+
const keyOf = (project, module, funcId) => `${project}\0${module}:${funcId}`;
|
|
78
|
+
const settleIdle = () => {
|
|
79
|
+
if (queue.length > 0 || running.size > 0)
|
|
80
|
+
return;
|
|
81
|
+
const waiters = idleWaiters;
|
|
82
|
+
idleWaiters = [];
|
|
83
|
+
for (const resolveIdle of waiters)
|
|
84
|
+
resolveIdle();
|
|
85
|
+
};
|
|
86
|
+
/** The work on a functional spec thrown away: its text is not what it was judging any more. */
|
|
87
|
+
const drop = (key, hash) => {
|
|
88
|
+
const at = queue.findIndex((work) => work.key === key && work.hash !== hash);
|
|
89
|
+
if (at !== -1)
|
|
90
|
+
queue.splice(at, 1);
|
|
91
|
+
const work = running.get(key);
|
|
92
|
+
if (work !== undefined && work.hash !== hash && work.dropped !== true) {
|
|
93
|
+
work.dropped = true;
|
|
94
|
+
work.controller.abort();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Take a functional spec, marked pending at once - unless the same text is taken already. Whether it was taken. */
|
|
98
|
+
const take = (given) => {
|
|
99
|
+
const key = keyOf(given.project, given.module, given.funcId);
|
|
100
|
+
drop(key, given.hash);
|
|
101
|
+
if (queue.some((work) => work.key === key) || running.get(key)?.hash === given.hash)
|
|
102
|
+
return false;
|
|
103
|
+
putAdvisorEntry(given.project, given.module, relative(given.project, given.plainFile), {
|
|
104
|
+
funcId: given.funcId,
|
|
105
|
+
hash: given.hash,
|
|
106
|
+
state: "pending",
|
|
107
|
+
tip: relative(given.project, given.tip),
|
|
108
|
+
...(given.asked !== undefined ? { asked: given.asked } : {}),
|
|
109
|
+
});
|
|
110
|
+
queue.push({ ...given, key, controller: new AbortController() });
|
|
111
|
+
pump();
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
const pump = () => {
|
|
115
|
+
if (stopped)
|
|
116
|
+
return;
|
|
117
|
+
while (running.size < parallel) {
|
|
118
|
+
// A new text waits for the work on its old one to unwind; the rest go on past it.
|
|
119
|
+
const at = queue.findIndex((work) => !running.has(work.key));
|
|
120
|
+
if (at === -1)
|
|
121
|
+
break;
|
|
122
|
+
const work = queue.splice(at, 1)[0];
|
|
123
|
+
running.set(work.key, work);
|
|
124
|
+
// Clear of any pulse the loop was looking for, and stopped by its own signal alone.
|
|
125
|
+
void outsideJobs(() => withStopSignal(work.controller.signal, () => run(work))).finally(() => {
|
|
126
|
+
if (running.get(work.key) === work)
|
|
127
|
+
running.delete(work.key);
|
|
128
|
+
pump();
|
|
129
|
+
settleIdle();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
settleIdle();
|
|
133
|
+
};
|
|
134
|
+
const spec = (work) => `${work.module}:${work.funcId}`;
|
|
135
|
+
const place = (work) => ({ project: work.project, module: work.module });
|
|
136
|
+
/** A verdict handed on to the chat: a yes kept undelivered in the advisor's file until a chat has it. */
|
|
137
|
+
const handOn = (given) => {
|
|
138
|
+
if (options.onVerdict === undefined)
|
|
139
|
+
return;
|
|
140
|
+
const { project, module, funcId, plainFile, hash } = given;
|
|
141
|
+
try {
|
|
142
|
+
options.onVerdict({
|
|
143
|
+
...given,
|
|
144
|
+
testFile: relative(project, join(testDirOf(plainFile), module, `${funcId}.plain`)),
|
|
145
|
+
current: () => {
|
|
146
|
+
const item = currentItem(plainFile, funcId);
|
|
147
|
+
return item !== undefined && textHash(item.markdown) === hash && !hasScenarios(item) && advisorEntry(project, module, funcId)?.hash === hash;
|
|
148
|
+
},
|
|
149
|
+
delivered: () => markDelivered(project, module, funcId, hash),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* a chat that throws as it is told: the yes stays undelivered, for the next start */
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const run = async (work) => {
|
|
157
|
+
const config = (() => {
|
|
158
|
+
try {
|
|
159
|
+
return configForSpec(work.plainFile);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
const model = advisorModel(config);
|
|
166
|
+
const entryOf = (state, more = {}) => {
|
|
167
|
+
putAdvisorEntry(work.project, work.module, relative(work.project, work.plainFile), {
|
|
168
|
+
funcId: work.funcId,
|
|
169
|
+
hash: work.hash,
|
|
170
|
+
state,
|
|
171
|
+
tip: relative(work.project, work.tip),
|
|
172
|
+
...(model !== undefined ? { model } : {}),
|
|
173
|
+
...more,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
// The setting turns off only what a change starts: the chat's asking stands.
|
|
178
|
+
if (work.asked === undefined && !enabled(work.project))
|
|
179
|
+
return;
|
|
180
|
+
const subject = {
|
|
181
|
+
module: work.module,
|
|
182
|
+
funcId: work.funcId,
|
|
183
|
+
text: work.text,
|
|
184
|
+
moduleText: readFileSync(work.plainFile, "utf8"),
|
|
185
|
+
...(resolve(work.tip) !== resolve(work.plainFile) && existsSync(work.tip) ? { testedIn: { module: basename(work.tip).replace(/\.plain$/, ""), text: readFileSync(work.tip, "utf8") } } : {}),
|
|
186
|
+
context: [...agentsText(work.project), ...(options.context?.(work.plainFile) ?? [])],
|
|
187
|
+
...(work.asked?.reason !== undefined ? { reason: work.asked.reason } : {}),
|
|
188
|
+
};
|
|
189
|
+
const report = await advise(subject, { cwd: work.project, ...(model !== undefined ? { model } : {}) });
|
|
190
|
+
if (work.controller.signal.aborted)
|
|
191
|
+
throw new Error("stopped");
|
|
192
|
+
record({ kind: "advisor.classified", spec: spec(work), verdict: report.verdict, questions: report.questions.map((answered) => answered.question), asked: work.asked !== undefined }, place(work));
|
|
193
|
+
// A yes is kept undelivered until a chat has it; a no is only a line, and nothing is lost when it is not drawn.
|
|
194
|
+
entryOf(report.verdict, { report, ...(report.verdict === "yes" && options.onVerdict !== undefined ? { undelivered: true } : {}) });
|
|
195
|
+
handOn({ project: work.project, module: work.module, funcId: work.funcId, plainFile: work.plainFile, tip: work.tip, hash: work.hash, report });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (work.dropped === true) {
|
|
199
|
+
record({ kind: "advisor.dropped", spec: spec(work) }, place(work));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Stopped with the session: left pending, for the next start.
|
|
203
|
+
if (stopped || work.controller.signal.aborted)
|
|
204
|
+
return;
|
|
205
|
+
const reason = firstLine(error instanceof Error ? error.message : String(error), 300);
|
|
206
|
+
entryOf("failed", { reason });
|
|
207
|
+
record({ kind: "advisor.failed", spec: spec(work), reason }, place(work));
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const advisor = {
|
|
211
|
+
seen(look) {
|
|
212
|
+
if (stopped)
|
|
213
|
+
return;
|
|
214
|
+
const tip = look.chain.at(-1);
|
|
215
|
+
if (tip === undefined)
|
|
216
|
+
return;
|
|
217
|
+
const project = projectDirOf(tip.plainFile);
|
|
218
|
+
if (!enabled(project))
|
|
219
|
+
return;
|
|
220
|
+
const built = new Map();
|
|
221
|
+
const builtItems = (section) => (look.built[section] ?? []);
|
|
222
|
+
for (const item of builtItems(FUNCTIONAL_REQUIREMENTS))
|
|
223
|
+
built.set(`${tip.moduleName}:${item.id}`, (item.markdown ?? "").trim());
|
|
224
|
+
for (const item of builtItems(REQUIRED_FUNCTIONAL_SPECS))
|
|
225
|
+
built.set(`${item.module}:${item.id}`, (item.markdown ?? "").trim());
|
|
226
|
+
const items = (section, own) => (look.tree[section] ?? []).map((item) => ({ item, module: own ? tip.moduleName : (item.module ?? "") }));
|
|
227
|
+
for (const { item, module } of [...items(REQUIRED_FUNCTIONAL_SPECS, false), ...items(FUNCTIONAL_REQUIREMENTS, true)]) {
|
|
228
|
+
if (module === "" || isStdlibModule(module))
|
|
229
|
+
continue;
|
|
230
|
+
const entry = look.chain.find((link) => link.moduleName === module);
|
|
231
|
+
if (entry === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const text = item.markdown.trim();
|
|
234
|
+
const hash = textHash(text);
|
|
235
|
+
const key = keyOf(project, module, item.id);
|
|
236
|
+
// Work on a text that is no more is thrown away, wherever the change came from.
|
|
237
|
+
drop(key, hash);
|
|
238
|
+
// Built as it is now: neither new nor changed.
|
|
239
|
+
if (built.get(`${module}:${item.id}`) === text)
|
|
240
|
+
continue;
|
|
241
|
+
// Scenarios of any kind: tested as the person wants, and today's review covers it.
|
|
242
|
+
if (hasScenarios(item))
|
|
243
|
+
continue;
|
|
244
|
+
// Judged already, for this text - or being judged now.
|
|
245
|
+
const known = advisorEntry(project, module, item.id);
|
|
246
|
+
if (isCurrent(known, hash) && (known.state !== "pending" || running.has(key) || queue.some((work) => work.key === key)))
|
|
247
|
+
continue;
|
|
248
|
+
take({ project, module, funcId: item.id, plainFile: resolve(entry.plainFile), tip: resolve(tip.plainFile), text, hash });
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
resume(project) {
|
|
252
|
+
if (stopped)
|
|
253
|
+
return;
|
|
254
|
+
// Conformance tests off: the advisor is off with them - nothing pending is picked up, not even what the chat asked for.
|
|
255
|
+
if (options.enabled === undefined && !conformanceTestsOnIn(project))
|
|
256
|
+
return;
|
|
257
|
+
// With the setting off, only what the chat asked for is picked up.
|
|
258
|
+
const wants = enabled(project);
|
|
259
|
+
for (const pending of pendingEntries(project)) {
|
|
260
|
+
if (!wants && pending.entry.asked === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
const plainFile = resolve(project, pending.plainFile);
|
|
263
|
+
const item = currentItem(plainFile, pending.entry.funcId);
|
|
264
|
+
// Gone, or given scenarios meanwhile: nothing is left to pick up.
|
|
265
|
+
if (item === undefined || hasScenarios(item)) {
|
|
266
|
+
removeAdvisorEntry(project, pending.module, pending.entry.funcId);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const text = item.markdown.trim();
|
|
270
|
+
take({
|
|
271
|
+
project,
|
|
272
|
+
module: pending.module,
|
|
273
|
+
funcId: item.id,
|
|
274
|
+
plainFile,
|
|
275
|
+
tip: resolve(project, pending.entry.tip ?? pending.plainFile),
|
|
276
|
+
text,
|
|
277
|
+
hash: textHash(text),
|
|
278
|
+
...(pending.entry.asked !== undefined ? { asked: pending.entry.asked } : {}),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// A yes no chat had when turri quit: handed on now, while it is still about the functional spec as it is.
|
|
282
|
+
for (const kept of undeliveredEntries(project)) {
|
|
283
|
+
const { entry } = kept;
|
|
284
|
+
const plainFile = resolve(project, kept.plainFile);
|
|
285
|
+
const item = currentItem(plainFile, entry.funcId);
|
|
286
|
+
if (entry.report === undefined || item === undefined || textHash(item.markdown) !== entry.hash || hasScenarios(item)) {
|
|
287
|
+
markDelivered(project, kept.module, entry.funcId, entry.hash);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
handOn({ project, module: kept.module, funcId: entry.funcId, plainFile, tip: resolve(project, entry.tip ?? kept.plainFile), hash: entry.hash, report: entry.report });
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
idle() {
|
|
294
|
+
if (queue.length === 0 && running.size === 0)
|
|
295
|
+
return Promise.resolve();
|
|
296
|
+
return new Promise((resolveIdle) => idleWaiters.push(resolveIdle));
|
|
297
|
+
},
|
|
298
|
+
taking(project) {
|
|
299
|
+
const mine = (work) => project === undefined || resolve(work.project) === resolve(project);
|
|
300
|
+
return [...new Set([...running.values(), ...queue].filter(mine).map((work) => `${work.module}:${work.funcId}`))];
|
|
301
|
+
},
|
|
302
|
+
evaluate(asked) {
|
|
303
|
+
const plainFile = resolve(asked.plainFile);
|
|
304
|
+
const project = projectDirOf(plainFile);
|
|
305
|
+
const item = currentItem(plainFile, asked.funcId);
|
|
306
|
+
if (item === undefined)
|
|
307
|
+
return "missing";
|
|
308
|
+
if (hasScenarios(item))
|
|
309
|
+
return "scenarios";
|
|
310
|
+
const text = item.markdown.trim();
|
|
311
|
+
const hash = textHash(text);
|
|
312
|
+
const known = advisorEntry(project, asked.module, asked.funcId);
|
|
313
|
+
// The tip its scenarios are tested in: given, else known, else the module at the top of a chain it is in - itself when none is.
|
|
314
|
+
const tip = asked.tip ?? (known?.tip !== undefined ? resolve(project, known.tip) : (tipsAbove(asked.module, project)[0]?.file ?? plainFile));
|
|
315
|
+
const reason = asked.reason?.trim();
|
|
316
|
+
const taken = take({ project, module: asked.module, funcId: asked.funcId, plainFile, tip: resolve(tip), text, hash, asked: reason !== undefined && reason !== "" ? { reason } : {} });
|
|
317
|
+
return taken ? "started" : "already";
|
|
318
|
+
},
|
|
319
|
+
stop() {
|
|
320
|
+
stopped = true;
|
|
321
|
+
queue.length = 0;
|
|
322
|
+
for (const work of running.values())
|
|
323
|
+
work.controller.abort();
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
return advisor;
|
|
327
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { BASE_RECORD_FILE } from "./paths.ts";
|
|
2
|
+
export { BASE_RECORD_FILE };
|
|
3
|
+
/**
|
|
4
|
+
* A base folder (`base-folder:` in config.turri.yaml, a stack's own in its file): what a codebase that requires no other
|
|
5
|
+
* module starts from instead of nothing - an existing codebase to build on, files to use (images, animations, fonts), or
|
|
6
|
+
* both. It is copied in once, before the codebase's first pulse, and never written to; what was copied is recorded beside
|
|
7
|
+
* the code record, and goes up the chain with the code, so every coding agent of the chain is told where its codebase
|
|
8
|
+
* started (prompt.ts). It is not kept in step: a change to it is said (`baseFolderDrift`), and a codebase regenerated from
|
|
9
|
+
* scratch starts from it again. The ***plain is untouched by it: it may refer to the folder in prose, and link into it.
|
|
10
|
+
*/
|
|
11
|
+
export interface BaseRecord {
|
|
12
|
+
/** The folder as `base-folder:` named it. */
|
|
13
|
+
folder: string;
|
|
14
|
+
/** The content of what was copied: its files' paths and bytes. */
|
|
15
|
+
hash: string;
|
|
16
|
+
/** The files copied, by path in the codebase. */
|
|
17
|
+
files: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Raised before a pulse starts when the base folder its codebase is to start from is not there. */
|
|
20
|
+
export declare class MissingBaseFolderError extends Error {
|
|
21
|
+
readonly moduleName: string;
|
|
22
|
+
constructor(moduleName: string, named: string, path: string);
|
|
23
|
+
}
|
|
24
|
+
/** The base folder a module's codebase starts from - its stack's over the project's - as named and as a path; none when the config names none. */
|
|
25
|
+
export declare function baseFolderOf(plainFile: string): {
|
|
26
|
+
named: string;
|
|
27
|
+
path: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The files of a base folder, by path in it, sorted: in a git work tree what git sees - tracked, and untracked that it
|
|
31
|
+
* does not ignore - so no dependencies or build output; otherwise every file but those of the folders never copied.
|
|
32
|
+
*/
|
|
33
|
+
export declare function baseFiles(folder: string): string[];
|
|
34
|
+
/** What a base folder holds now, as one hash: each file's path and bytes. */
|
|
35
|
+
export declare function hashBase(folder: string, files?: string[]): string;
|
|
36
|
+
/** What a codebase started from, when it started from a base folder. */
|
|
37
|
+
export declare function readBaseRecord(buildDir: string): BaseRecord | undefined;
|
|
38
|
+
/** Carry what a codebase started from up to one built on it, with its code (requires.ts copyRequiredCode). */
|
|
39
|
+
export declare function carryBaseRecord(from: string, to: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Start an empty codebase that requires no other module from its base folder, when the config names one: the folder's
|
|
42
|
+
* files copied in, what was copied recorded, and committed. Nothing when the config names none. A base folder that is not
|
|
43
|
+
* there stops the pulse before it starts.
|
|
44
|
+
*/
|
|
45
|
+
export declare function seedFromBaseFolder(buildDir: string, plainFile: string, moduleName: string): BaseRecord | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* How a codebase's base folder stands against what it started from: undefined when they agree, or it started from none
|
|
48
|
+
* and the config names none; otherwise a line saying what moved - the folder named now another, its files changed, or
|
|
49
|
+
* it gone - and that regenerating from scratch starts again from it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function baseFolderDrift(buildDir: string, plainFile: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* What a codebase started from, as its coding agents are told: the base folder as named, and the files that came from
|
|
54
|
+
* it that are in the codebase still - a module built on another is told those of its own. None when it started from
|
|
55
|
+
* no base folder.
|
|
56
|
+
*/
|
|
57
|
+
export declare function baseOfCodebase(buildDir: string): {
|
|
58
|
+
folder: string;
|
|
59
|
+
files: string[];
|
|
60
|
+
} | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Where a file the ***plain links is in the codebase, when it is one the base folder held and the codebase has still:
|
|
63
|
+
* its path there - the agent uses it from there, and never copies it. The base folder is the one the codebase started
|
|
64
|
+
* from, by its record, from the project's folder.
|
|
65
|
+
*/
|
|
66
|
+
export declare function inCodebaseOf(file: string, buildDir: string, plainFile: string): string | undefined;
|
|
67
|
+
/** A base folder named for a module and not there: what `parse` and `doctor` say of it. None when it is there, or none is named. */
|
|
68
|
+
export declare function missingBaseFolder(plainFile: string): string | undefined;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { CONFIG_KEYS, configForSpec } from "../config.js";
|
|
6
|
+
import { recordHistory } from "./lineage.js";
|
|
7
|
+
import { BASE_RECORD_FILE, projectDirOf } from "./paths.js";
|
|
8
|
+
export { BASE_RECORD_FILE };
|
|
9
|
+
/** Raised before a pulse starts when the base folder its codebase is to start from is not there. */
|
|
10
|
+
export class MissingBaseFolderError extends Error {
|
|
11
|
+
moduleName;
|
|
12
|
+
constructor(moduleName, named, path) {
|
|
13
|
+
super([
|
|
14
|
+
`Stopped before building ${moduleName}: its codebase is to start from the base folder ${named} (${CONFIG_KEYS.baseFolder}: in config.turri.yaml), ` +
|
|
15
|
+
`and ${existsSync(path) ? "that is not a folder" : "there is no such folder"} - ${path}.`,
|
|
16
|
+
"",
|
|
17
|
+
`Make the folder, or name the right one in ${CONFIG_KEYS.baseFolder}:, by path from the project's folder - or take the key out to start from nothing. Then build again.`,
|
|
18
|
+
].join("\n"));
|
|
19
|
+
this.name = "MissingBaseFolderError";
|
|
20
|
+
this.moduleName = moduleName;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The base folder a module's codebase starts from - its stack's over the project's - as named and as a path; none when the config names none. */
|
|
24
|
+
export function baseFolderOf(plainFile) {
|
|
25
|
+
const named = configForSpec(plainFile).baseFolder;
|
|
26
|
+
if (named === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
return { named, path: resolve(projectDirOf(resolve(plainFile)), named) };
|
|
29
|
+
}
|
|
30
|
+
/** Folders never copied from a base folder that git does not keep: its history, installed dependencies, caches. */
|
|
31
|
+
const NOT_COPIED = new Set([".git", "node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".turri", "plain_modules"]);
|
|
32
|
+
/**
|
|
33
|
+
* The files of a base folder, by path in it, sorted: in a git work tree what git sees - tracked, and untracked that it
|
|
34
|
+
* does not ignore - so no dependencies or build output; otherwise every file but those of the folders never copied.
|
|
35
|
+
*/
|
|
36
|
+
export function baseFiles(folder) {
|
|
37
|
+
const inGit = spawnSync("git", ["-C", folder, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
|
|
38
|
+
if (inGit.status === 0 && inGit.stdout.trim() === "true") {
|
|
39
|
+
const listed = spawnSync("git", ["-C", folder, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
40
|
+
if (listed.status === 0) {
|
|
41
|
+
// A tracked file deleted in the work tree is listed, and not there to copy.
|
|
42
|
+
return [...new Set(listed.stdout.split("\0").filter((path) => path !== ""))].filter((path) => isFile(join(folder, path))).sort();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const found = [];
|
|
46
|
+
const walk = (dir) => {
|
|
47
|
+
for (const entry of readdirSync(dir)) {
|
|
48
|
+
if (NOT_COPIED.has(entry))
|
|
49
|
+
continue;
|
|
50
|
+
const full = join(dir, entry);
|
|
51
|
+
if (statSync(full).isDirectory())
|
|
52
|
+
walk(full);
|
|
53
|
+
else
|
|
54
|
+
found.push(relative(folder, full).split(sep).join("/"));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
walk(folder);
|
|
58
|
+
return found.sort();
|
|
59
|
+
}
|
|
60
|
+
const isFile = (path) => {
|
|
61
|
+
try {
|
|
62
|
+
return statSync(path).isFile();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** What a base folder holds now, as one hash: each file's path and bytes. */
|
|
69
|
+
export function hashBase(folder, files = baseFiles(folder)) {
|
|
70
|
+
const hash = createHash("sha256");
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
hash.update(file).update("\0");
|
|
73
|
+
hash.update(readFileSync(join(folder, file))).update("\0");
|
|
74
|
+
}
|
|
75
|
+
return hash.digest("hex");
|
|
76
|
+
}
|
|
77
|
+
/** What a codebase started from, when it started from a base folder. */
|
|
78
|
+
export function readBaseRecord(buildDir) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(readFileSync(join(buildDir, BASE_RECORD_FILE), "utf8"));
|
|
81
|
+
return typeof parsed.folder === "string" && typeof parsed.hash === "string" && Array.isArray(parsed.files) ? parsed : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Carry what a codebase started from up to one built on it, with its code (requires.ts copyRequiredCode). */
|
|
88
|
+
export function carryBaseRecord(from, to) {
|
|
89
|
+
if (existsSync(join(from, BASE_RECORD_FILE)) && !existsSync(join(to, BASE_RECORD_FILE)))
|
|
90
|
+
copyFileSync(join(from, BASE_RECORD_FILE), join(to, BASE_RECORD_FILE));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Start an empty codebase that requires no other module from its base folder, when the config names one: the folder's
|
|
94
|
+
* files copied in, what was copied recorded, and committed. Nothing when the config names none. A base folder that is not
|
|
95
|
+
* there stops the pulse before it starts.
|
|
96
|
+
*/
|
|
97
|
+
export function seedFromBaseFolder(buildDir, plainFile, moduleName) {
|
|
98
|
+
const base = baseFolderOf(plainFile);
|
|
99
|
+
if (base === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
if (!existsSync(base.path) || !statSync(base.path).isDirectory())
|
|
102
|
+
throw new MissingBaseFolderError(moduleName, base.named, base.path);
|
|
103
|
+
const files = baseFiles(base.path);
|
|
104
|
+
mkdirSync(buildDir, { recursive: true });
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const target = join(buildDir, file);
|
|
107
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
108
|
+
copyFileSync(join(base.path, file), target);
|
|
109
|
+
}
|
|
110
|
+
const record = { folder: base.named, hash: hashBase(base.path, files), files };
|
|
111
|
+
writeFileSync(join(buildDir, BASE_RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`);
|
|
112
|
+
recordHistory(buildDir, `start from the base folder ${base.named}`, `${files.length} file${files.length === 1 ? "" : "s"} copied in`);
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* How a codebase's base folder stands against what it started from: undefined when they agree, or it started from none
|
|
117
|
+
* and the config names none; otherwise a line saying what moved - the folder named now another, its files changed, or
|
|
118
|
+
* it gone - and that regenerating from scratch starts again from it.
|
|
119
|
+
*/
|
|
120
|
+
export function baseFolderDrift(buildDir, plainFile) {
|
|
121
|
+
const record = readBaseRecord(buildDir);
|
|
122
|
+
const base = baseFolderOf(plainFile);
|
|
123
|
+
const again = "regenerate it from scratch to start again from it";
|
|
124
|
+
if (record === undefined) {
|
|
125
|
+
if (base === undefined)
|
|
126
|
+
return undefined;
|
|
127
|
+
if (existsSync(buildDir) && readdirSync(buildDir).length > 0)
|
|
128
|
+
return `its base folder is ${base.named}, but its codebase did not start from one - ${again}`;
|
|
129
|
+
return existsSync(base.path) && statSync(base.path).isDirectory() ? undefined : `its base folder ${base.named} is not there - its first pulse stops until it is`;
|
|
130
|
+
}
|
|
131
|
+
if (base === undefined)
|
|
132
|
+
return `its codebase started from the base folder ${record.folder}, which ${CONFIG_KEYS.baseFolder}: no longer names - it is built on what was copied`;
|
|
133
|
+
if (base.named !== record.folder)
|
|
134
|
+
return `its base folder is now ${base.named}, but its codebase started from ${record.folder} - ${again}`;
|
|
135
|
+
if (!existsSync(base.path) || !statSync(base.path).isDirectory())
|
|
136
|
+
return `its base folder ${base.named} is gone - its codebase is built on what was copied from it`;
|
|
137
|
+
return hashBase(base.path) !== record.hash ? `its base folder ${base.named} changed since its codebase started from it - ${again}` : undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a codebase started from, as its coding agents are told: the base folder as named, and the files that came from
|
|
141
|
+
* it that are in the codebase still - a module built on another is told those of its own. None when it started from
|
|
142
|
+
* no base folder.
|
|
143
|
+
*/
|
|
144
|
+
export function baseOfCodebase(buildDir) {
|
|
145
|
+
const record = readBaseRecord(buildDir);
|
|
146
|
+
if (record === undefined)
|
|
147
|
+
return undefined;
|
|
148
|
+
return { folder: record.folder, files: record.files.filter((file) => isFile(join(buildDir, file))) };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Where a file the ***plain links is in the codebase, when it is one the base folder held and the codebase has still:
|
|
152
|
+
* its path there - the agent uses it from there, and never copies it. The base folder is the one the codebase started
|
|
153
|
+
* from, by its record, from the project's folder.
|
|
154
|
+
*/
|
|
155
|
+
export function inCodebaseOf(file, buildDir, plainFile) {
|
|
156
|
+
const base = baseOfCodebase(buildDir);
|
|
157
|
+
if (base === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
const at = relative(resolve(projectDirOf(resolve(plainFile)), base.folder), resolve(file)).split(sep).join("/");
|
|
160
|
+
return !at.startsWith("../") && at !== ".." && base.files.includes(at) ? at : undefined;
|
|
161
|
+
}
|
|
162
|
+
/** A base folder named for a module and not there: what `parse` and `doctor` say of it. None when it is there, or none is named. */
|
|
163
|
+
export function missingBaseFolder(plainFile) {
|
|
164
|
+
const base = baseFolderOf(plainFile);
|
|
165
|
+
if (base === undefined || (existsSync(base.path) && statSync(base.path).isDirectory()))
|
|
166
|
+
return undefined;
|
|
167
|
+
return `its base folder ${base.named} (${CONFIG_KEYS.baseFolder}:) is ${existsSync(base.path) ? "not a folder" : "not there"} - its codebase's first pulse stops until it is`;
|
|
168
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { JobCases } from "./drift.ts";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* What the coding agent does, case by case. `fromBase`: the codebase started from a base folder - nothing is built from
|
|
4
|
+
* the specification, but the folder is not empty, and a project in it is built on, never set up anew.
|
|
5
|
+
*/
|
|
6
|
+
export declare function codeCasesBlock(cases: JobCases, fromBase?: boolean): string[];
|
|
4
7
|
/** What the unit reviewer holds the work to, case by case - in place of its checks, when the job is mechanical. */
|
|
5
8
|
export declare function codeReviewCasesBlock(cases: JobCases): string[];
|
|
6
|
-
/** What the conformance agent does, case by case. */
|
|
7
|
-
export declare function conformanceCasesBlock(cases: JobCases): string[];
|
|
8
|
-
/** What the conformance test reviewer holds the tests to, when the job is mechanical
|
|
9
|
-
export declare function conformanceReviewCasesBlock(cases: JobCases): string[];
|
|
9
|
+
/** What the conformance agent does, case by case - scenario by scenario, when the job has them. */
|
|
10
|
+
export declare function conformanceCasesBlock(cases: JobCases, module?: string): string[];
|
|
11
|
+
/** What the conformance test reviewer holds the tests to: scenario by scenario - or, when the job is mechanical, in place of its checks. */
|
|
12
|
+
export declare function conformanceReviewCasesBlock(cases: JobCases, module?: string): string[];
|