@turri-dev/cli 0.0.0-stage → 0.1.1
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/.pi/themes/turri-dark.json +80 -0
- package/.pi/themes/turri-light.json +82 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +252 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
- package/bin/turri.mjs +29 -0
- package/dist/ask.js +149 -0
- package/dist/brand.js +50 -0
- package/dist/btw.js +174 -0
- package/dist/chat-models.js +37 -0
- package/dist/chat-skills.js +18 -0
- package/dist/cli.js +178 -0
- package/dist/config-command.js +197 -0
- package/dist/config-edit.js +367 -0
- package/dist/config.js +489 -0
- package/dist/conform-cli.js +29 -0
- package/dist/cultivate-cli.js +110 -0
- package/dist/drift-cli.js +25 -0
- package/dist/environment.js +70 -0
- package/dist/extension.js +2372 -0
- package/dist/genome.js +301 -0
- package/dist/graph.js +246 -0
- package/dist/identity.js +35 -0
- package/dist/init-config.js +511 -0
- package/dist/judge.js +62 -0
- package/dist/lineage-cli.js +13 -0
- package/dist/loop-cli.js +132 -0
- package/dist/loop-tools.js +225 -0
- package/dist/mature-tools.js +193 -0
- package/dist/model-check.js +117 -0
- package/dist/module-report.js +9 -0
- package/dist/pi-launch.js +31 -0
- package/dist/pi.js +813 -0
- package/dist/plain/check.js +76 -0
- package/dist/plain/concepts.js +213 -0
- package/dist/plain/errors.js +43 -0
- package/dist/plain/files.js +57 -0
- package/dist/plain/highlight.js +195 -0
- package/dist/plain/index.js +15 -0
- package/dist/plain/liquid.js +129 -0
- package/dist/plain/markdown.js +208 -0
- package/dist/plain/parser.js +752 -0
- package/dist/plain/plain.tmLanguage.json +124 -0
- package/dist/plain/spec.js +291 -0
- package/dist/plain/stdlib.js +44 -0
- package/dist/question-dialog.js +125 -0
- package/dist/questioning.js +109 -0
- package/dist/quiet.js +125 -0
- package/dist/regenerate/activity.js +187 -0
- package/dist/regenerate/agent-env.js +190 -0
- package/dist/regenerate/agent-shell.js +114 -0
- package/dist/regenerate/arbiter.js +133 -0
- package/dist/regenerate/audit.js +203 -0
- package/dist/regenerate/cases.js +83 -0
- package/dist/regenerate/change-view.js +102 -0
- package/dist/regenerate/choose-conformance.js +117 -0
- package/dist/regenerate/chunk-plan.js +625 -0
- package/dist/regenerate/codebase.js +75 -0
- package/dist/regenerate/command-check.js +312 -0
- package/dist/regenerate/compaction.js +20 -0
- package/dist/regenerate/confine.js +310 -0
- package/dist/regenerate/conform.js +133 -0
- package/dist/regenerate/conformance-agents.js +955 -0
- package/dist/regenerate/conformance.js +936 -0
- package/dist/regenerate/conversation-writes.js +100 -0
- package/dist/regenerate/credentials.js +88 -0
- package/dist/regenerate/debug-log.js +228 -0
- package/dist/regenerate/debugger.js +90 -0
- package/dist/regenerate/delivery.js +19 -0
- package/dist/regenerate/diagnose-job.js +40 -0
- package/dist/regenerate/diagnose.js +81 -0
- package/dist/regenerate/diff-view.js +240 -0
- package/dist/regenerate/drift.js +194 -0
- package/dist/regenerate/engine.js +118 -0
- package/dist/regenerate/environment-error.js +220 -0
- package/dist/regenerate/job-log.js +102 -0
- package/dist/regenerate/job-panel.js +336 -0
- package/dist/regenerate/jobs-tree.js +300 -0
- package/dist/regenerate/jobs.js +427 -0
- package/dist/regenerate/lanes.js +32 -0
- package/dist/regenerate/lineage.js +384 -0
- package/dist/regenerate/loop.js +1435 -0
- package/dist/regenerate/masking.js +95 -0
- package/dist/regenerate/notes.js +397 -0
- package/dist/regenerate/os-sandbox.js +243 -0
- package/dist/regenerate/paths.js +165 -0
- package/dist/regenerate/plain-palette.js +83 -0
- package/dist/regenerate/prompt.js +704 -0
- package/dist/regenerate/records.js +294 -0
- package/dist/regenerate/regenerate.js +598 -0
- package/dist/regenerate/regression.js +181 -0
- package/dist/regenerate/release.js +262 -0
- package/dist/regenerate/requires.js +241 -0
- package/dist/regenerate/resources.js +164 -0
- package/dist/regenerate/self-check.js +111 -0
- package/dist/regenerate/services.js +155 -0
- package/dist/regenerate/shell.js +319 -0
- package/dist/regenerate/side-panel.js +278 -0
- package/dist/regenerate/spec-map-view.js +574 -0
- package/dist/regenerate/spec-map.js +561 -0
- package/dist/regenerate/spec-steps.js +527 -0
- package/dist/regenerate/state.js +10 -0
- package/dist/regenerate/steer.js +91 -0
- package/dist/regenerate/steering.js +119 -0
- package/dist/regenerate/stop-report.js +161 -0
- package/dist/regenerate/submit-work.js +71 -0
- package/dist/regenerate/test-kinds.js +124 -0
- package/dist/regenerate/test-scripts.js +695 -0
- package/dist/regenerate/testing.js +406 -0
- package/dist/regenerate/transcript-view.js +1 -0
- package/dist/regenerate/unit-gate.js +20 -0
- package/dist/regenerate/unit-review.js +347 -0
- package/dist/regenerate/web.js +256 -0
- package/dist/regenerate/workspace.js +99 -0
- package/dist/regenerate-cli.js +68 -0
- package/dist/release-agent.js +114 -0
- package/dist/release-cli.js +131 -0
- package/dist/release-tools.js +148 -0
- package/dist/review/live.js +165 -0
- package/dist/rules.js +47 -0
- package/dist/self-invocation.js +110 -0
- package/dist/setup.js +245 -0
- package/dist/skill-progress.js +78 -0
- package/dist/skill.js +149 -0
- package/dist/skills.js +74 -0
- package/dist/spec-diff-cli.js +37 -0
- package/dist/spec-tools.js +155 -0
- package/dist/stack.js +158 -0
- package/dist/stacks.js +97 -0
- package/dist/stdlib.js +100 -0
- package/dist/terminal-images.js +46 -0
- package/dist/tools.js +242 -0
- package/dist/validate-cli.js +52 -0
- package/docs/README.md +58 -0
- package/docs/authoring-plain.md +245 -0
- package/docs/config.md +353 -0
- package/docs/deploy.md +109 -0
- package/docs/development.md +75 -0
- package/docs/headless.md +148 -0
- package/docs/images/how-turri-works.svg +50 -0
- package/docs/images/turri-banner.png +0 -0
- package/docs/installation.md +118 -0
- package/docs/parser.md +108 -0
- package/docs/regenerating.md +1116 -0
- package/docs/sandbox.md +233 -0
- package/docs/skills-and-gates.md +254 -0
- package/docs/theming.md +76 -0
- package/docs/using.md +454 -0
- package/genome/rules/bullet-continuation.md +69 -0
- package/genome/rules/concise-specs-examples.md +431 -0
- package/genome/rules/concise-specs-integration-examples.md +270 -0
- package/genome/rules/concise-specs.md +80 -0
- package/genome/rules/definitions.md +107 -0
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
- package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
- package/genome/rules/exported-concepts.md +39 -0
- package/genome/rules/func-specs.md +129 -0
- package/genome/rules/impl-reqs.md +68 -0
- package/genome/rules/import-modules.md +48 -0
- package/genome/rules/integration-embedded-testing.md +276 -0
- package/genome/rules/integration-embedded.md +171 -0
- package/genome/rules/integration-standalone.md +135 -0
- package/genome/rules/integrations.md +137 -0
- package/genome/rules/linked-resources.md +69 -0
- package/genome/rules/module-structure.md +105 -0
- package/genome/rules/project-model.md +88 -0
- package/genome/rules/required-concepts.md +44 -0
- package/genome/rules/requires-modules.md +80 -0
- package/genome/rules/stdlib.md +73 -0
- package/genome/rules/test-reqs.md +57 -0
- package/genome/skills/add-acceptance-test/SKILL.md +100 -0
- package/genome/skills/add-concept/SKILL.md +71 -0
- package/genome/skills/add-functional-spec/SKILL.md +84 -0
- package/genome/skills/add-functional-specs/SKILL.md +118 -0
- package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
- package/genome/skills/add-resource/SKILL.md +105 -0
- package/genome/skills/add-template/SKILL.md +64 -0
- package/genome/skills/add-test-requirement/SKILL.md +73 -0
- package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
- package/genome/skills/analyze-func-specs/SKILL.md +123 -0
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
- package/genome/skills/break-down-func-spec/SKILL.md +205 -0
- package/genome/skills/brood/SKILL.md +229 -0
- package/genome/skills/brood/references/intent.md +35 -0
- package/genome/skills/brood/references/product.md +101 -0
- package/genome/skills/brood/references/stack.md +61 -0
- package/genome/skills/brood/references/testing.md +56 -0
- package/genome/skills/bud/SKILL.md +148 -0
- package/genome/skills/bud/references/checklist.md +44 -0
- package/genome/skills/bud/references/intent.md +30 -0
- package/genome/skills/check-plain-env/SKILL.md +285 -0
- package/genome/skills/consolidate-concepts/SKILL.md +192 -0
- package/genome/skills/create-import-module/SKILL.md +104 -0
- package/genome/skills/create-requires-module/SKILL.md +109 -0
- package/genome/skills/debug-specs/SKILL.md +188 -0
- package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
- package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
- package/genome/skills/init-plain-project/SKILL.md +117 -0
- package/genome/skills/init-plain-project/references/checklist.md +30 -0
- package/genome/skills/mature/SKILL.md +126 -0
- package/genome/skills/refactor-module/SKILL.md +196 -0
- package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
- package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
- package/genome/skills/spawn/SKILL.md +163 -0
- package/package.json +68 -4
- package/stdlib/backend/common.plain +25 -0
- package/stdlib/backend/conformance/common.plain +12 -0
- package/stdlib/backend/conformance/go-gin.plain +9 -0
- package/stdlib/backend/conformance/go-nethttp.plain +9 -0
- package/stdlib/backend/conformance/java-springboot.plain +9 -0
- package/stdlib/backend/conformance/python-django.plain +9 -0
- package/stdlib/backend/conformance/python-fastapi.plain +9 -0
- package/stdlib/backend/conformance/python-flask.plain +9 -0
- package/stdlib/backend/conformance/rust-axum.plain +9 -0
- package/stdlib/backend/conformance/typescript-express.plain +9 -0
- package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
- package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
- package/stdlib/backend/go-gin.plain +18 -0
- package/stdlib/backend/go-nethttp.plain +18 -0
- package/stdlib/backend/java-springboot.plain +19 -0
- package/stdlib/backend/python-django.plain +19 -0
- package/stdlib/backend/python-fastapi.plain +23 -0
- package/stdlib/backend/python-flask.plain +23 -0
- package/stdlib/backend/rust-axum.plain +17 -0
- package/stdlib/backend/typescript-express.plain +19 -0
- package/stdlib/backend/typescript-fastify.plain +19 -0
- package/stdlib/backend/typescript-nestjs.plain +19 -0
- package/stdlib/cli/common.plain +17 -0
- package/stdlib/cli/conformance/common.plain +11 -0
- package/stdlib/cli/conformance/go-cobra.plain +9 -0
- package/stdlib/cli/conformance/go-flag.plain +9 -0
- package/stdlib/cli/conformance/java-picocli.plain +9 -0
- package/stdlib/cli/conformance/python-argparse.plain +9 -0
- package/stdlib/cli/conformance/python-click.plain +9 -0
- package/stdlib/cli/conformance/python-typer.plain +9 -0
- package/stdlib/cli/conformance/rust-clap.plain +9 -0
- package/stdlib/cli/conformance/typescript-commander.plain +9 -0
- package/stdlib/cli/go-cobra.plain +14 -0
- package/stdlib/cli/go-flag.plain +14 -0
- package/stdlib/cli/java-picocli.plain +15 -0
- package/stdlib/cli/python-argparse.plain +18 -0
- package/stdlib/cli/python-click.plain +16 -0
- package/stdlib/cli/python-typer.plain +16 -0
- package/stdlib/cli/rust-clap.plain +14 -0
- package/stdlib/cli/typescript-commander.plain +17 -0
- package/stdlib/frontend/angular.plain +17 -0
- package/stdlib/frontend/common.plain +17 -0
- package/stdlib/frontend/conformance/angular.plain +5 -0
- package/stdlib/frontend/conformance/common.plain +11 -0
- package/stdlib/frontend/conformance/react-vite.plain +5 -0
- package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
- package/stdlib/frontend/conformance/vue-vite.plain +5 -0
- package/stdlib/frontend/react-vite.plain +16 -0
- package/stdlib/frontend/sveltekit-spa.plain +16 -0
- package/stdlib/frontend/vue-vite.plain +16 -0
- package/stdlib/lang/go.plain +13 -0
- package/stdlib/lang/java.plain +12 -0
- package/stdlib/lang/python.plain +18 -0
- package/stdlib/lang/rust.plain +12 -0
- package/stdlib/lang/typescript.plain +13 -0
- package/stdlib/library/common.plain +15 -0
- package/stdlib/library/conformance/common.plain +9 -0
- package/stdlib/library/conformance/go.plain +9 -0
- package/stdlib/library/conformance/java-gradle.plain +9 -0
- package/stdlib/library/conformance/java-maven.plain +9 -0
- package/stdlib/library/conformance/python.plain +9 -0
- package/stdlib/library/conformance/rust.plain +9 -0
- package/stdlib/library/conformance/typescript.plain +9 -0
- package/stdlib/library/go.plain +12 -0
- package/stdlib/library/java-gradle.plain +13 -0
- package/stdlib/library/java-maven.plain +13 -0
- package/stdlib/library/python.plain +13 -0
- package/stdlib/library/rust.plain +12 -0
- package/stdlib/library/typescript.plain +15 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { DEFAULT_CONCEPTS, extractConceptsFromDefinition, extractConceptsFromSpecText } from "../plain/concepts.js";
|
|
4
|
+
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
5
|
+
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
6
|
+
import { reportActivity, reportStage } from "./activity.js";
|
|
7
|
+
import { steeringBlock } from "./steer.js";
|
|
8
|
+
import { marksRegenerateAgain } from "./records.js";
|
|
9
|
+
import { validateConcepts } from "../plain/concepts.js";
|
|
10
|
+
import { applyChanges, changesBetween, inconsistencies, layoutOf, makeStep, parseTree, serializeTree } from "./spec-steps.js";
|
|
11
|
+
/** The work the queue does: every change from the base to the spec as it is now, numbered. */
|
|
12
|
+
export function netChanges(base, current) {
|
|
13
|
+
return changesBetween(serializeTree(base), serializeTree(current));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What is wrong with a reworked queue, or nothing: every change in exactly one job, no job without
|
|
17
|
+
* one, and each job, applied after the ones before it, leaving a spec that holds together - the last
|
|
18
|
+
* exactly the spec as it is now.
|
|
19
|
+
*/
|
|
20
|
+
export function checkDraft(input, draft) {
|
|
21
|
+
const net = netChanges(input.base, input.current);
|
|
22
|
+
const byId = new Map(net.map((change) => [change.id, change]));
|
|
23
|
+
const problems = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const job of draft) {
|
|
26
|
+
if (job.changes.length === 0)
|
|
27
|
+
problems.push(`"${job.title}" makes no change - delete it, or give it changes.`);
|
|
28
|
+
for (const id of job.changes) {
|
|
29
|
+
if (!byId.has(id))
|
|
30
|
+
problems.push(`"${job.title}" names ${id}, which is not one of the changes.`);
|
|
31
|
+
else if (seen.has(id))
|
|
32
|
+
problems.push(`${id} is in more than one job - "${job.title}" and an earlier one.`);
|
|
33
|
+
seen.add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const missing = net.filter((change) => !seen.has(change.id)).map((change) => change.id);
|
|
37
|
+
if (missing.length > 0)
|
|
38
|
+
problems.push(`No job makes ${missing.join(", ")} - every change has to be made, or the queue does not end on the spec as it is now.`);
|
|
39
|
+
if (problems.length > 0)
|
|
40
|
+
return problems;
|
|
41
|
+
const base = serializeTree(input.base);
|
|
42
|
+
const endpoints = new Set([...validateConcepts(input.base), ...validateConcepts(input.current)]);
|
|
43
|
+
const sofar = [];
|
|
44
|
+
for (const job of draft) {
|
|
45
|
+
sofar.push(...job.changes.map((id) => byId.get(id)));
|
|
46
|
+
const wrong = inconsistencies(parseTree(applyChanges(base, sofar)), endpoints);
|
|
47
|
+
if (wrong.length > 0) {
|
|
48
|
+
problems.push(`After "${job.title}" the spec does not hold together - a later job defines what it uses:\n${wrong.map((error) => ` - ${error}`).join("\n")}`);
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (applyChanges(base, sofar) !== serializeTree(input.current)) {
|
|
53
|
+
problems.push("Applied in order, the jobs do not end exactly on the spec as it is now.");
|
|
54
|
+
}
|
|
55
|
+
return problems;
|
|
56
|
+
}
|
|
57
|
+
/** A checked draft, as the queue runs it: each job's diff and the spec it leaves. */
|
|
58
|
+
export function stepsOf(input, draft) {
|
|
59
|
+
const byId = new Map(netChanges(input.base, input.current).map((change) => [change.id, change]));
|
|
60
|
+
const layout = layoutOf(input.specText);
|
|
61
|
+
let before = serializeTree(input.base);
|
|
62
|
+
const sofar = [];
|
|
63
|
+
return draft.map((job) => {
|
|
64
|
+
const changes = job.changes.map((id) => byId.get(id));
|
|
65
|
+
sofar.push(...changes);
|
|
66
|
+
const after = applyChanges(serializeTree(input.base), sofar);
|
|
67
|
+
const step = makeStep(job.title, job.reason, changes, before, after, input.specFile, layout);
|
|
68
|
+
before = after;
|
|
69
|
+
return { ...job, step };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export const MAX_SPECS_PER_JOB = 3;
|
|
73
|
+
export const MAX_LINES_PER_JOB = 120;
|
|
74
|
+
/** How often an answer that does not hold goes back to its agent before the plan falls back. */
|
|
75
|
+
const RETRIES = 2;
|
|
76
|
+
/** An item's text, from its serialized line: its markdown, and its acceptance tests. */
|
|
77
|
+
function textOf(change) {
|
|
78
|
+
const line = change.after ?? change.before ?? "";
|
|
79
|
+
try {
|
|
80
|
+
const item = JSON.parse(line);
|
|
81
|
+
return [item.markdown ?? "", ...(item[ACCEPTANCE_TESTS] ?? []).map((test) => test.markdown ?? "")].filter(Boolean).join("\n");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** How many lines a change reads as: what a job's size is counted in. */
|
|
88
|
+
function linesOf(change) {
|
|
89
|
+
return textOf(change).split("\n").length;
|
|
90
|
+
}
|
|
91
|
+
/** The concepts a text names, reserved ones left out. */
|
|
92
|
+
function namedIn(text) {
|
|
93
|
+
return extractConceptsFromSpecText(text).filter((concept) => !DEFAULT_CONCEPTS.has(concept));
|
|
94
|
+
}
|
|
95
|
+
/** The concept a definition defines, from its text. */
|
|
96
|
+
function definedBy(text) {
|
|
97
|
+
return extractConceptsFromDefinition(text)[0];
|
|
98
|
+
}
|
|
99
|
+
/** What is wrong with a grouping: every new functional spec once, in order, next to each other, none too big. */
|
|
100
|
+
export function groupProblems(specs, answer, maxSpecs = MAX_SPECS_PER_JOB) {
|
|
101
|
+
const problems = [];
|
|
102
|
+
const numbers = specs.map((spec) => spec.number);
|
|
103
|
+
const given = answer.groups.flatMap((group) => group.specs);
|
|
104
|
+
if (given.join(",") !== numbers.join(","))
|
|
105
|
+
problems.push(`The groups must hold every new functional spec once, in the order the spec has them: ${numbers.join(", ")}. They hold ${given.join(", ") || "none"}.`);
|
|
106
|
+
// Next to each other: in the same module, one numbered after the other there.
|
|
107
|
+
const byNumber = new Map(specs.map((spec) => [spec.number, spec]));
|
|
108
|
+
const nextTo = (a, b) => {
|
|
109
|
+
const first = byNumber.get(a);
|
|
110
|
+
const second = byNumber.get(b);
|
|
111
|
+
if (first === undefined || second === undefined)
|
|
112
|
+
return b === a + 1;
|
|
113
|
+
return first.module === second.module && Number(second.id ?? second.number) === Number(first.id ?? first.number) + 1;
|
|
114
|
+
};
|
|
115
|
+
for (const group of answer.groups) {
|
|
116
|
+
if (group.specs.length === 0)
|
|
117
|
+
problems.push(`"${group.title}" has no functional spec.`);
|
|
118
|
+
if (group.specs.length > maxSpecs)
|
|
119
|
+
problems.push(`"${group.title}" has ${group.specs.length} functional specs - at most ${maxSpecs}.`);
|
|
120
|
+
for (let i = 1; i < group.specs.length; i += 1) {
|
|
121
|
+
if (!nextTo(group.specs[i - 1], group.specs[i]))
|
|
122
|
+
problems.push(`"${group.title}" groups ${group.specs.join(", ")}: only functional specs next to each other in the same module go together.`);
|
|
123
|
+
}
|
|
124
|
+
if (group.title.trim() === "")
|
|
125
|
+
problems.push("Every group needs a title.");
|
|
126
|
+
}
|
|
127
|
+
return problems;
|
|
128
|
+
}
|
|
129
|
+
/** What is wrong with a placement: every new req once, each where there is such a job. */
|
|
130
|
+
export function placeProblems(input, answer) {
|
|
131
|
+
const problems = [];
|
|
132
|
+
const valid = (to) => to === "all" || (to === "built" ? input.built : Number.isInteger(to) && to >= 1 && to <= input.groups.length);
|
|
133
|
+
for (const [kind, reqs, placed] of [
|
|
134
|
+
["implementation req", input.implementationReqs, answer.implementationReqs],
|
|
135
|
+
["test req", input.testReqs, answer.testReqs],
|
|
136
|
+
]) {
|
|
137
|
+
const numbers = reqs.map((req) => req.number);
|
|
138
|
+
const seen = new Map();
|
|
139
|
+
for (const entry of placed) {
|
|
140
|
+
if (!numbers.includes(entry.req))
|
|
141
|
+
problems.push(`There is no ${kind} ${entry.req}.`);
|
|
142
|
+
seen.set(entry.req, (seen.get(entry.req) ?? 0) + 1);
|
|
143
|
+
if (!valid(entry.to))
|
|
144
|
+
problems.push(`${kind} ${entry.req} goes to ${entry.to}, which is no job: a group's number, "all"${input.built ? ', or "built"' : ""}.`);
|
|
145
|
+
}
|
|
146
|
+
for (const number of numbers) {
|
|
147
|
+
const times = seen.get(number) ?? 0;
|
|
148
|
+
if (times === 0)
|
|
149
|
+
problems.push(`${kind} ${number} is not placed: every one goes somewhere.`);
|
|
150
|
+
if (times > 1)
|
|
151
|
+
problems.push(`${kind} ${number} is placed ${times} times: once.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return problems;
|
|
155
|
+
}
|
|
156
|
+
/** Whether a change is to a functional spec marked to regenerate again: built, and regenerated again - cut into jobs as new work is. */
|
|
157
|
+
function regeneratesAgain(change) {
|
|
158
|
+
if ((change.section !== FUNCTIONAL_REQUIREMENTS && change.section !== REQUIRED_FUNCTIONAL_SPECS) || change.kind !== "change" || change.before === undefined)
|
|
159
|
+
return false;
|
|
160
|
+
try {
|
|
161
|
+
return marksRegenerateAgain(JSON.parse(change.before));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** The functional specs added at the end: new work. One added anywhere else changes what is built on. */
|
|
168
|
+
function newSpecsOf(net, current) {
|
|
169
|
+
const count = (current[FUNCTIONAL_REQUIREMENTS] ?? []).length;
|
|
170
|
+
const added = net.filter((change) => change.section === FUNCTIONAL_REQUIREMENTS && change.kind === "add").sort((a, b) => a.position - b.position);
|
|
171
|
+
const tail = [];
|
|
172
|
+
for (let at = count; at >= 1; at -= 1) {
|
|
173
|
+
const change = added.find((candidate) => candidate.position === at);
|
|
174
|
+
if (change === undefined)
|
|
175
|
+
break;
|
|
176
|
+
tail.unshift(change);
|
|
177
|
+
}
|
|
178
|
+
return tail;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The definitions each chunk uses, in order: the concepts its items name, and those its definitions name in turn -
|
|
182
|
+
* each definition with the first chunk that uses it. Those nothing uses go with the last.
|
|
183
|
+
*/
|
|
184
|
+
function placeDefinitions(chunks, definitions) {
|
|
185
|
+
const byConcept = new Map();
|
|
186
|
+
for (const definition of definitions)
|
|
187
|
+
for (const concept of definedBy(textOf(definition)))
|
|
188
|
+
byConcept.set(concept, definition);
|
|
189
|
+
const placed = new Set();
|
|
190
|
+
for (const chunk of chunks) {
|
|
191
|
+
chunk.definitions = [];
|
|
192
|
+
const queue = [...chunk.specs, ...chunk.reqs, ...(chunk.built ?? [])].flatMap((change) => namedIn(textOf(change)));
|
|
193
|
+
for (let concept = queue.shift(); concept !== undefined; concept = queue.shift()) {
|
|
194
|
+
const definition = byConcept.get(concept);
|
|
195
|
+
if (definition === undefined || placed.has(definition))
|
|
196
|
+
continue;
|
|
197
|
+
placed.add(definition);
|
|
198
|
+
chunk.definitions.push(definition);
|
|
199
|
+
queue.push(...namedIn(textOf(definition)));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const left = definitions.filter((definition) => !placed.has(definition));
|
|
203
|
+
if (left.length > 0 && chunks.length > 0)
|
|
204
|
+
chunks.at(-1).definitions.push(...left);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The lines of a chunk that a split can move to a later job: its functional specs and the definitions they bring. Its
|
|
208
|
+
* reqs stay with the first job of the split whatever happens, so they never decide one.
|
|
209
|
+
*/
|
|
210
|
+
function movableSize(chunk) {
|
|
211
|
+
return [...chunk.specs, ...chunk.definitions].reduce((total, change) => total + linesOf(change), 0);
|
|
212
|
+
}
|
|
213
|
+
function sizeOf(chunk) {
|
|
214
|
+
return [...chunk.specs, ...chunk.definitions, ...chunk.reqs, ...(chunk.built ?? [])].reduce((total, change) => total + linesOf(change), 0);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The draft a chunking is: a job per chunk, its changes in the order the net has them. A job runs as the queued job
|
|
218
|
+
* whose own change it carries - the first of them not taken by an earlier one - so a job keeps its id, its notes and
|
|
219
|
+
* its conformance job where the plan leaves its change where it was.
|
|
220
|
+
*/
|
|
221
|
+
function draftOf(chunks, net) {
|
|
222
|
+
return chunks
|
|
223
|
+
.map((chunk) => {
|
|
224
|
+
const ids = new Set([...chunk.specs, ...chunk.definitions, ...chunk.reqs, ...(chunk.built ?? [])].map((change) => change.id));
|
|
225
|
+
return { chunk, changes: net.filter((change) => ids.has(change.id)).map((change) => change.id) };
|
|
226
|
+
})
|
|
227
|
+
.filter((entry) => entry.changes.length > 0)
|
|
228
|
+
.map((entry) => ({
|
|
229
|
+
title: entry.chunk.title,
|
|
230
|
+
reason: entry.chunk.built !== undefined
|
|
231
|
+
? "what is built already, changed"
|
|
232
|
+
: entry.chunk.specs.length > 0 && entry.chunk.specs.every(regeneratesAgain)
|
|
233
|
+
? "functional specs regenerated again"
|
|
234
|
+
: "new work, cut from its functional specs",
|
|
235
|
+
changes: entry.changes,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
/** A spec's first line, as a group's title when no agent gave one. */
|
|
239
|
+
function titleOf(change) {
|
|
240
|
+
return textOf(change).split("\n")[0].replace(/^-\s*/, "").slice(0, 80);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cut the queue's work - from `input.base` to `input.current` - into jobs, as the three steps say. Returns the draft
|
|
244
|
+
* the work loop runs: checked (checkDraft), or, when nothing holds, the whole work as one job. `notes` is given what
|
|
245
|
+
* the plan did not do as asked - the person's wishes an agent did not follow and why, an answer that did not hold, a
|
|
246
|
+
* group split for its size - for the conversation to say, rather than guess.
|
|
247
|
+
*/
|
|
248
|
+
export async function planChunks(input, agents, notes) {
|
|
249
|
+
if (input.track !== "conformance")
|
|
250
|
+
return cut(input, agents, notes);
|
|
251
|
+
// The conformance tests are grouped by the harness, as one job as far as the guardrail lets: no agent is asked.
|
|
252
|
+
return anchored(await cut(input), netChanges(input.base, input.current));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* A conformance plan, every job of it testing a functional spec: a job that touches none - reqs or definitions alone -
|
|
256
|
+
* goes into the next job that does, or, with none after it, the last before it. Work that touches no functional spec
|
|
257
|
+
* at all - test reqs changed, say - stays as it is: it is mechanical, and nothing else.
|
|
258
|
+
*/
|
|
259
|
+
function anchored(draft, net) {
|
|
260
|
+
const sectionOf = new Map(net.map((change) => [change.id, change.section]));
|
|
261
|
+
const testsASpec = (job) => job.changes.some((id) => sectionOf.get(id) === FUNCTIONAL_REQUIREMENTS);
|
|
262
|
+
if (!draft.some(testsASpec))
|
|
263
|
+
return draft;
|
|
264
|
+
const jobs = [];
|
|
265
|
+
let carried = [];
|
|
266
|
+
for (const job of draft) {
|
|
267
|
+
if (!testsASpec(job)) {
|
|
268
|
+
carried.push(...job.changes);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
jobs.push({ ...job, changes: [...carried, ...job.changes] });
|
|
272
|
+
carried = [];
|
|
273
|
+
}
|
|
274
|
+
if (carried.length > 0)
|
|
275
|
+
jobs[jobs.length - 1] = { ...jobs.at(-1), changes: [...jobs.at(-1).changes, ...carried] };
|
|
276
|
+
return jobs;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* How the conformance tests are cut, with no agent: every functional spec tested in one job - those of one module
|
|
280
|
+
* together - a new job begun where the next functional spec was asked for in another group (`groups`: conform called for
|
|
281
|
+
* 1-3, then for 4-6), or would take the job's functional specs and the definitions they bring past `maxLines`; or,
|
|
282
|
+
* `separately`, a job a functional spec, as the person asked.
|
|
283
|
+
* `size`: the movable lines of the last group of a grouping, its definitions placed.
|
|
284
|
+
*/
|
|
285
|
+
function conformanceGroups(specs, separately, maxLines, size, titleOfSpec,
|
|
286
|
+
/** The group a functional spec was asked for in - none, -1: a job never takes two. */
|
|
287
|
+
askedIn = () => -1) {
|
|
288
|
+
const groups = [];
|
|
289
|
+
for (const spec of specs) {
|
|
290
|
+
const last = groups.at(-1);
|
|
291
|
+
const first = last !== undefined ? specs.find((other) => other.number === last.specs[0]) : undefined;
|
|
292
|
+
const sameModule = first !== undefined && first.module === spec.module;
|
|
293
|
+
const sameAsk = first !== undefined && askedIn(first) === askedIn(spec);
|
|
294
|
+
if (!separately && last !== undefined && sameModule && sameAsk) {
|
|
295
|
+
const trial = [...groups.slice(0, -1), { specs: [...last.specs, spec.number] }].map((group) => ({ ...group, title: "" }));
|
|
296
|
+
if (size(trial) <= maxLines) {
|
|
297
|
+
last.specs.push(spec.number);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
groups.push({ specs: [spec.number] });
|
|
302
|
+
}
|
|
303
|
+
return groups.map((group) => ({ specs: group.specs, title: group.specs.map(titleOfSpec).join("; ") }));
|
|
304
|
+
}
|
|
305
|
+
/** A functional spec's id, as the tree it is in has it - a conformance item's `frid` - or nothing when it says none. */
|
|
306
|
+
function fridOf(change) {
|
|
307
|
+
for (const side of [change.after, change.before]) {
|
|
308
|
+
if (side === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
try {
|
|
311
|
+
const frid = JSON.parse(side).frid;
|
|
312
|
+
if (typeof frid === "string" && frid !== "")
|
|
313
|
+
return frid;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Not an item with an id.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
async function cut(input, agents, notes) {
|
|
322
|
+
// How big a pulse may be: the config's, else the defaults.
|
|
323
|
+
const { specs: maxSpecs, lines: maxLines } = input.limits ?? { specs: MAX_SPECS_PER_JOB, lines: MAX_LINES_PER_JOB };
|
|
324
|
+
/** What the plan did not do as asked: in the activity, and in the notes. */
|
|
325
|
+
const say = (text) => {
|
|
326
|
+
reportActivity(` ${text}`);
|
|
327
|
+
notes?.push(text);
|
|
328
|
+
};
|
|
329
|
+
const net = netChanges(input.base, input.current);
|
|
330
|
+
if (net.length === 0)
|
|
331
|
+
return [];
|
|
332
|
+
// What is grouped into jobs: the functional specs regenerated again - the modules' below first, as the codebase is
|
|
333
|
+
// built on them - and the codebase's own, regenerated again or new at the end, in the spec's order.
|
|
334
|
+
const below = net.filter((change) => regeneratesAgain(change) && change.section === REQUIRED_FUNCTIONAL_SPECS).sort((a, b) => a.position - b.position);
|
|
335
|
+
// The conformance tests are written on the code as built: every functional spec added or changed is tested alike, wherever it is.
|
|
336
|
+
const own = input.track === "conformance"
|
|
337
|
+
? net.filter((change) => change.section === FUNCTIONAL_REQUIREMENTS && change.kind !== "remove").sort((a, b) => a.position - b.position)
|
|
338
|
+
: [...net.filter((change) => regeneratesAgain(change) && change.section === FUNCTIONAL_REQUIREMENTS), ...newSpecsOf(net, input.current)].sort((a, b) => a.position - b.position);
|
|
339
|
+
const specs = [...below, ...own];
|
|
340
|
+
const inSpecs = new Set(specs);
|
|
341
|
+
const isNew = (change, section) => change.section === section && change.kind === "add";
|
|
342
|
+
const definitions = net.filter((change) => isNew(change, DEFINITIONS));
|
|
343
|
+
const implementationReqs = net.filter((change) => isNew(change, NON_FUNCTIONAL_REQUIREMENTS));
|
|
344
|
+
const testReqs = net.filter((change) => isNew(change, TEST_REQUIREMENTS));
|
|
345
|
+
const placedByKind = new Set([...definitions, ...implementationReqs, ...testReqs]);
|
|
346
|
+
// Added in another section - the required modules' functional specs of a first build: with the first job of the new work.
|
|
347
|
+
const otherAdds = net.filter((change) => change.kind === "add" && !inSpecs.has(change) && !placedByKind.has(change) && change.section !== FUNCTIONAL_REQUIREMENTS);
|
|
348
|
+
// Anything else changes what is built on - an edit, a removal, a functional spec built changed or put in among them.
|
|
349
|
+
const builtChanges = net.filter((change) => !inSpecs.has(change) && !placedByKind.has(change) && !otherAdds.includes(change));
|
|
350
|
+
const hasBuilt = builtChanges.length > 0;
|
|
351
|
+
// Named as the person asks for it: "shorty.plain regenerate".
|
|
352
|
+
const whole = [{ title: "regenerate", reason: "the whole of the work, in one job", changes: net.map((change) => change.id) }];
|
|
353
|
+
if (specs.length === 0)
|
|
354
|
+
return checkDraft(input, whole).length === 0 ? whole : [];
|
|
355
|
+
// 1. Which functional specs go together.
|
|
356
|
+
reportStage(`planning the queue: which of the ${specs.length} functional spec${specs.length === 1 ? "" : "s"} to build go together`);
|
|
357
|
+
const numbered = specs.map((change, index) => {
|
|
358
|
+
const required = change.section === REQUIRED_FUNCTIONAL_SPECS ? JSON.parse(change.after) : undefined;
|
|
359
|
+
return {
|
|
360
|
+
number: index + 1,
|
|
361
|
+
text: textOf(change),
|
|
362
|
+
id: required?.frid ?? String(change.position),
|
|
363
|
+
...(required?.module !== undefined ? { module: required.module } : input.moduleName !== undefined ? { module: input.moduleName } : {}),
|
|
364
|
+
...(regeneratesAgain(change) ? { again: true } : {}),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
const bySpec = new Map(specs.map((change, index) => [index + 1, change]));
|
|
368
|
+
const chunksFor = (list) => [
|
|
369
|
+
...(hasBuilt ? [{ title: "Changes to what is built", specs: [], definitions: [], reqs: [], built: builtChanges }] : []),
|
|
370
|
+
...list.map((group) => ({ title: group.title, specs: group.specs.map((number) => bySpec.get(number)), definitions: [], reqs: [] })),
|
|
371
|
+
];
|
|
372
|
+
const conformance = input.track === "conformance";
|
|
373
|
+
const grouped = conformance ? undefined : ((await agents?.group(numbered, (answer) => groupProblems(numbered, answer, maxSpecs), input.steering, maxSpecs).catch(() => undefined)) ?? undefined);
|
|
374
|
+
const groups = conformance
|
|
375
|
+
? conformanceGroups(numbered, input.separately === true, maxLines, (list) => {
|
|
376
|
+
const trial = chunksFor(list);
|
|
377
|
+
placeDefinitions(trial, definitions);
|
|
378
|
+
return movableSize(trial.at(-1));
|
|
379
|
+
}, (number) => titleOf(bySpec.get(number)), (spec) => {
|
|
380
|
+
const frid = fridOf(bySpec.get(spec.number));
|
|
381
|
+
return frid === undefined ? -1 : (input.groups ?? []).findIndex((group) => group.includes(frid));
|
|
382
|
+
})
|
|
383
|
+
: grouped !== undefined && groupProblems(numbered, grouped, maxSpecs).length === 0
|
|
384
|
+
? grouped.groups
|
|
385
|
+
: numbered.map((spec, index) => ({ specs: [spec.number], title: titleOf(specs[index]) }));
|
|
386
|
+
if (grouped?.notes !== undefined && grouped.notes.trim() !== "")
|
|
387
|
+
say(`the grouping: ${grouped.notes.trim()}`);
|
|
388
|
+
if (!conformance && agents !== undefined) {
|
|
389
|
+
if (grouped === undefined)
|
|
390
|
+
say("the grouping was not given - a functional spec a pulse");
|
|
391
|
+
else if (groupProblems(numbered, grouped, maxSpecs).length > 0)
|
|
392
|
+
say(`the grouping did not hold (${groupProblems(numbered, grouped, maxSpecs).join(" ")}) - a functional spec a pulse`);
|
|
393
|
+
}
|
|
394
|
+
let chunks = chunksFor(groups);
|
|
395
|
+
// 2. The definitions each group uses.
|
|
396
|
+
placeDefinitions(chunks, definitions);
|
|
397
|
+
// 3. Where each implementation req and test req goes.
|
|
398
|
+
const newChunks = () => chunks.filter((chunk) => chunk.built === undefined);
|
|
399
|
+
const builtChunk = () => chunks.find((chunk) => chunk.built !== undefined);
|
|
400
|
+
let placement;
|
|
401
|
+
if (implementationReqs.length + testReqs.length > 0) {
|
|
402
|
+
reportStage(`planning the queue: where each of ${implementationReqs.length + testReqs.length} new req${implementationReqs.length + testReqs.length === 1 ? "" : "s"} goes`);
|
|
403
|
+
const placeInput = {
|
|
404
|
+
...(input.steering !== undefined ? { steering: input.steering } : {}),
|
|
405
|
+
groups: newChunks().map((chunk, index) => ({ number: index + 1, title: chunk.title, specs: chunk.specs.map(textOf), definitions: chunk.definitions.map(textOf) })),
|
|
406
|
+
built: hasBuilt,
|
|
407
|
+
implementationReqs: implementationReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
408
|
+
testReqs: testReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
409
|
+
};
|
|
410
|
+
const answer = (await agents?.place(placeInput, (given) => placeProblems(placeInput, given)).catch(() => undefined)) ?? undefined;
|
|
411
|
+
if (answer?.notes !== undefined && answer.notes.trim() !== "")
|
|
412
|
+
say(`the placement: ${answer.notes.trim()}`);
|
|
413
|
+
placement = answer !== undefined && placeProblems(placeInput, answer).length === 0 ? answer : undefined;
|
|
414
|
+
if (placement === undefined && agents !== undefined)
|
|
415
|
+
say("the placement did not hold - every implementation req with the first pulse, every test req with the first conformance pulse");
|
|
416
|
+
}
|
|
417
|
+
/** A req's chunk: its group, the built job, or the first job of the new work for "all". */
|
|
418
|
+
const chunkOf = (to) => (to === "built" ? builtChunk() : to === "all" ? newChunks()[0] : newChunks()[to - 1]);
|
|
419
|
+
const place = () => {
|
|
420
|
+
for (const chunk of chunks)
|
|
421
|
+
chunk.reqs = [];
|
|
422
|
+
implementationReqs.forEach((change, index) => chunkOf(placement?.implementationReqs.find((entry) => entry.req === index + 1)?.to ?? "all").reqs.push(change));
|
|
423
|
+
testReqs.forEach((change, index) => chunkOf(placement?.testReqs.find((entry) => entry.req === index + 1)?.to ?? 1).reqs.push(change));
|
|
424
|
+
newChunks()[0]?.reqs.push(...otherAdds);
|
|
425
|
+
// What the reqs name is defined with them, or before.
|
|
426
|
+
placeDefinitions(chunks, definitions);
|
|
427
|
+
};
|
|
428
|
+
place();
|
|
429
|
+
// A group too big to render well is split - into as few pulses as fit, its functional specs kept together in runs as
|
|
430
|
+
// long as the limit lets, rather than a functional spec a pulse: most of the grouping holds. Its reqs go with the first.
|
|
431
|
+
if (newChunks().some((chunk) => chunk.specs.length > 1 && movableSize(chunk) > maxLines)) {
|
|
432
|
+
/** The size of the group starting at `first` in a trial grouping, its definitions placed as they would be. */
|
|
433
|
+
const sizeIn = (list, first) => {
|
|
434
|
+
const trial = chunksFor(list);
|
|
435
|
+
placeDefinitions(trial, definitions);
|
|
436
|
+
const chunk = trial.find((candidate) => candidate.specs[0] === bySpec.get(first));
|
|
437
|
+
return chunk !== undefined ? movableSize(chunk) : 0;
|
|
438
|
+
};
|
|
439
|
+
const split = [];
|
|
440
|
+
groups.forEach((group, at) => {
|
|
441
|
+
const chunk = newChunks().find((candidate) => candidate.specs[0] === bySpec.get(group.specs[0]));
|
|
442
|
+
if (chunk === undefined || chunk.specs.length < 2 || movableSize(chunk) <= maxLines) {
|
|
443
|
+
split.push(group);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const after = groups.slice(at + 1);
|
|
447
|
+
const runs = [];
|
|
448
|
+
let run = [group.specs[0]];
|
|
449
|
+
for (const next of group.specs.slice(1)) {
|
|
450
|
+
const longer = [...run, next];
|
|
451
|
+
const trial = [...split, ...runs.map((specs) => ({ specs, title: "" })), { specs: longer, title: "" }, ...after];
|
|
452
|
+
if (sizeIn(trial, longer[0]) <= maxLines)
|
|
453
|
+
run = longer;
|
|
454
|
+
else {
|
|
455
|
+
runs.push(run);
|
|
456
|
+
run = [next];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
runs.push(run);
|
|
460
|
+
say(`"${group.title}" is past ${maxLines} lines with its definitions and reqs - split into ${runs.length} pulses`);
|
|
461
|
+
runs.forEach((specs, index) => split.push({ specs, title: specs.length === 1 ? titleOf(bySpec.get(specs[0])) : `${group.title} (${index + 1} of ${runs.length})` }));
|
|
462
|
+
});
|
|
463
|
+
const before = placement;
|
|
464
|
+
// A req placed with a group that split goes with the first functional spec of it.
|
|
465
|
+
const groupOfSpec = new Map(split.map((group, index) => [group.specs[0], index + 1]));
|
|
466
|
+
const remap = (to) => (typeof to === "number" ? (groupOfSpec.get(groups[to - 1].specs[0]) ?? to) : to);
|
|
467
|
+
placement = before !== undefined ? { implementationReqs: before.implementationReqs.map((entry) => ({ ...entry, to: remap(entry.to) })), testReqs: before.testReqs.map((entry) => ({ ...entry, to: remap(entry.to) })) } : undefined;
|
|
468
|
+
groups.splice(0, groups.length, ...split);
|
|
469
|
+
chunks = chunksFor(groups);
|
|
470
|
+
place();
|
|
471
|
+
}
|
|
472
|
+
// What is about everything, the first group cannot carry within the limit: a job of its own ahead of it, when it holds on its own.
|
|
473
|
+
const first = newChunks()[0];
|
|
474
|
+
if (first !== undefined && sizeOf(first) > maxLines) {
|
|
475
|
+
const everything = implementationReqs.filter((change, index) => (placement?.implementationReqs.find((entry) => entry.req === index + 1)?.to ?? "all") === "all");
|
|
476
|
+
const standalone = everything.filter((change) => namedIn(textOf(change)).every((concept) => definitions.every((definition) => !definedBy(textOf(definition)).includes(concept))));
|
|
477
|
+
if (standalone.length > 0) {
|
|
478
|
+
first.reqs = first.reqs.filter((change) => !standalone.includes(change));
|
|
479
|
+
chunks.splice(chunks.indexOf(first), 0, { title: "What the whole of it is built with", specs: [], definitions: [], reqs: standalone });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const draft = draftOf(chunks, net);
|
|
483
|
+
if (checkDraft(input, draft).length === 0)
|
|
484
|
+
return draft;
|
|
485
|
+
// It does not hold together: a functional spec a job, every req with the first.
|
|
486
|
+
say(`the plan did not hold together (${checkDraft(input, draft)[0]?.slice(0, 120)}) - a functional spec a pulse`);
|
|
487
|
+
placement = undefined;
|
|
488
|
+
groups.splice(0, groups.length, ...numbered.map((spec, index) => ({ specs: [spec.number], title: titleOf(specs[index]) })));
|
|
489
|
+
chunks = chunksFor(groups);
|
|
490
|
+
place();
|
|
491
|
+
const simple = draftOf(chunks, net);
|
|
492
|
+
if (checkDraft(input, simple).length === 0)
|
|
493
|
+
return simple;
|
|
494
|
+
return checkDraft(input, whole).length === 0 ? whole : [];
|
|
495
|
+
}
|
|
496
|
+
// ---------------------------------------------------------------------------------------------
|
|
497
|
+
// The agents.
|
|
498
|
+
export function groupPrompt(specs, steering, maxSpecs = MAX_SPECS_PER_JOB) {
|
|
499
|
+
return [
|
|
500
|
+
"You plan how new work on a codebase is built: which of its new functional specs are built together, in one job.",
|
|
501
|
+
"",
|
|
502
|
+
"Each job is rendered by a coding agent, reviewed and tested, then the next - fewer, fuller jobs: each job costs a",
|
|
503
|
+
"coding session, a test run and a review. So group by default: what builds one feature goes together - the feature,",
|
|
504
|
+
`its edge cases, and the functional specs that build on it - up to ${maxSpecs}: a component and the parts only it uses`,
|
|
505
|
+
"(a timeout handler, a retry handler and the executor that runs a function through both), an endpoint and the one",
|
|
506
|
+
"that undoes it, a record, the list of it and its filters. Keep a functional spec alone only when it is truly",
|
|
507
|
+
"independent - nothing next to it builds on it or is part of the same thing - or when it is very large on its own.",
|
|
508
|
+
"Never group specs only because they are of the same kind.",
|
|
509
|
+
"",
|
|
510
|
+
"Only functional specs next to each other in the same module go together, and they stay in the order given: the",
|
|
511
|
+
"order is what each is built on. Give each group a short title that says what it builds.",
|
|
512
|
+
"",
|
|
513
|
+
"The specification is split into modules - a ***plain matter, not how the code is laid out. Each numbers its",
|
|
514
|
+
"functional specs from 1: each below is given with its module and its id there, and a number of its own to answer with.",
|
|
515
|
+
"",
|
|
516
|
+
...steeringBlock(steering),
|
|
517
|
+
...(specs.some((spec) => spec.again === true)
|
|
518
|
+
? [
|
|
519
|
+
"Those marked again=\"true\" are built already, and are regenerated again - rewritten, doing what they say. Group them as",
|
|
520
|
+
"you would new ones, by what they build.",
|
|
521
|
+
"",
|
|
522
|
+
]
|
|
523
|
+
: []),
|
|
524
|
+
"The functional specs to build:",
|
|
525
|
+
"",
|
|
526
|
+
...specs.map((spec) => `<functional spec number="${spec.number}"${spec.module !== undefined ? ` module="${spec.module}"` : ""} id="${spec.id ?? spec.number}"${spec.again === true ? ' again="true"' : ""}>\n${spec.text.trim()}\n</functional spec>`),
|
|
527
|
+
"",
|
|
528
|
+
"Call submit_groups once, with every one of them in a group.",
|
|
529
|
+
].join("\n");
|
|
530
|
+
}
|
|
531
|
+
export function placePrompt(input) {
|
|
532
|
+
return [
|
|
533
|
+
"You plan how new work on a codebase is built. Its new functional specs are in groups, each a job, built in order,",
|
|
534
|
+
"each with the definitions it uses. Place each new implementation req and test req in the job it belongs to.",
|
|
535
|
+
"",
|
|
536
|
+
"- An implementation req goes with the group it is about - how that part is built, stored, secured. One about",
|
|
537
|
+
' everything - the language, the framework, how every part is laid out - goes to "all": with the first job,',
|
|
538
|
+
" where the codebase starts.",
|
|
539
|
+
"- A test req goes with the first group whose conformance tests need it; one about all the tests to \"all\".",
|
|
540
|
+
...(input.built ? ['- "built" is the job that changes what is built already, and goes first: a req about the code there already goes there.'] : []),
|
|
541
|
+
"",
|
|
542
|
+
...steeringBlock(input.steering),
|
|
543
|
+
...input.groups.map((group) => [
|
|
544
|
+
// A title that is only its first spec's first line is not said twice.
|
|
545
|
+
group.specs[0] !== undefined && group.specs[0].includes(group.title.trim().replace(/\.$/, "")) ? `<group number="${group.number}">` : `<group number="${group.number}" title="${group.title}">`,
|
|
546
|
+
...group.specs.map((spec) => `<functional spec>\n${spec.trim()}\n</functional spec>`),
|
|
547
|
+
...group.definitions.map((definition) => `<definition>\n${definition.trim()}\n</definition>`),
|
|
548
|
+
"</group>",
|
|
549
|
+
].join("\n")),
|
|
550
|
+
"",
|
|
551
|
+
...(input.implementationReqs.length > 0 ? ["The new implementation reqs:", ...input.implementationReqs.map((req) => `<implementation req number="${req.number}">\n${req.text.trim()}\n</implementation req>`), ""] : []),
|
|
552
|
+
...(input.testReqs.length > 0 ? ["The new test reqs:", ...input.testReqs.map((req) => `<test req number="${req.number}">\n${req.text.trim()}\n</test req>`), ""] : []),
|
|
553
|
+
"Call submit_placement once, with every one of them placed once.",
|
|
554
|
+
].join("\n");
|
|
555
|
+
}
|
|
556
|
+
const TargetSchema = Type.Union([Type.Number(), Type.Literal("all"), Type.Literal("built")]);
|
|
557
|
+
const NOTES = "Each of the person's wishes for how the queue is planned that you did not follow, and why - a limit it would break, say. Leave it out when you followed them all, or there are none.";
|
|
558
|
+
/** Ask one step of an agent, and ask again with what was wrong, until it holds or the retries are spent. */
|
|
559
|
+
async function askStep(options) {
|
|
560
|
+
let answer;
|
|
561
|
+
const tool = options.tool((given) => (answer = given));
|
|
562
|
+
const session = await createExecutor({ cwd: options.cwd, customTools: [tool], tools: [tool.name], model: options.model, label: options.label, role: "planner", task: options.task, budget: { calls: 4, finish: [tool.name] } });
|
|
563
|
+
try {
|
|
564
|
+
await ask(session, BEGIN);
|
|
565
|
+
for (let again = 0; again < RETRIES; again += 1) {
|
|
566
|
+
if (answer !== undefined && options.check(answer).length === 0)
|
|
567
|
+
return answer;
|
|
568
|
+
const problems = answer === undefined ? [`You did not call ${tool.name}.`] : options.check(answer);
|
|
569
|
+
answer = undefined;
|
|
570
|
+
await ask(session, `That does not hold:\n- ${problems.join("\n- ")}\n\nCall ${tool.name} again, with it fixed.`);
|
|
571
|
+
}
|
|
572
|
+
return answer;
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
session.dispose();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** The chunking's two agents, on a model - fresh for every plan. */
|
|
579
|
+
export function llmChunkAgents(options) {
|
|
580
|
+
const label = options.label ?? "queue planner";
|
|
581
|
+
return {
|
|
582
|
+
group: (specs, check, steering, maxSpecs) => askStep({
|
|
583
|
+
cwd: options.cwd,
|
|
584
|
+
model: options.model,
|
|
585
|
+
label: `${label} grouping`,
|
|
586
|
+
task: groupPrompt(specs, steering, maxSpecs),
|
|
587
|
+
check,
|
|
588
|
+
tool: (take) => defineTool({
|
|
589
|
+
name: "submit_groups",
|
|
590
|
+
label: "Submit the groups",
|
|
591
|
+
description: "The new functional specs in groups, in order: each group's spec numbers and a short title.",
|
|
592
|
+
parameters: Type.Object({
|
|
593
|
+
groups: Type.Array(Type.Object({ specs: Type.Array(Type.Number()), title: Type.String() })),
|
|
594
|
+
notes: Type.Optional(Type.String({ description: NOTES })),
|
|
595
|
+
}),
|
|
596
|
+
async execute(_id, params) {
|
|
597
|
+
take(params);
|
|
598
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
599
|
+
},
|
|
600
|
+
}),
|
|
601
|
+
}),
|
|
602
|
+
place: (input, check) => askStep({
|
|
603
|
+
cwd: options.cwd,
|
|
604
|
+
model: options.model,
|
|
605
|
+
label: `${label} placement`,
|
|
606
|
+
task: placePrompt(input),
|
|
607
|
+
check,
|
|
608
|
+
tool: (take) => defineTool({
|
|
609
|
+
name: "submit_placement",
|
|
610
|
+
label: "Submit the placement",
|
|
611
|
+
description: 'Where each new req goes: a group\'s number, "all", or "built".',
|
|
612
|
+
parameters: Type.Object({
|
|
613
|
+
implementation_reqs: Type.Array(Type.Object({ req: Type.Number(), to: TargetSchema })),
|
|
614
|
+
test_reqs: Type.Array(Type.Object({ req: Type.Number(), to: TargetSchema })),
|
|
615
|
+
notes: Type.Optional(Type.String({ description: NOTES })),
|
|
616
|
+
}),
|
|
617
|
+
async execute(_id, params) {
|
|
618
|
+
const given = params;
|
|
619
|
+
take({ implementationReqs: given.implementation_reqs, testReqs: given.test_reqs, ...(given.notes !== undefined ? { notes: given.notes } : {}) });
|
|
620
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
621
|
+
},
|
|
622
|
+
}),
|
|
623
|
+
}),
|
|
624
|
+
};
|
|
625
|
+
}
|