@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,240 @@
|
|
|
1
|
+
import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import { highlightLine, roleOf } from "../plain/highlight.js";
|
|
3
|
+
import { RESOURCE_MARKER } from "../plain/markdown.js";
|
|
4
|
+
/** Painted with nothing: the text as it is, for where there are no colours. */
|
|
5
|
+
export const PLAIN_PAINT = {
|
|
6
|
+
syntax: (_colour, text) => text,
|
|
7
|
+
added: (text) => text,
|
|
8
|
+
removed: (text) => text,
|
|
9
|
+
addedBand: (text) => text,
|
|
10
|
+
removedBand: (text) => text,
|
|
11
|
+
muted: (text) => text,
|
|
12
|
+
};
|
|
13
|
+
/** A section's header: `***functional specs***`. */
|
|
14
|
+
const SECTION = /^\*\*\*[^*].*\*\*\*$/;
|
|
15
|
+
/** A line of a .plain file as the spec reads it: its links as the parser renders them (markdown.ts). */
|
|
16
|
+
function asRead(line) {
|
|
17
|
+
return line.replace(/\[([^\]]*)\]\([^)]*\)/g, (_, text) => `\`${RESOURCE_MARKER}${text}\``);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Place a spec diff's lines in the .plain files they come from - `files`, the module's and every one it
|
|
21
|
+
* reads, named as they are to be shown. A spec diff is of the spec as read, the files it imports laid into
|
|
22
|
+
* it, so its own line numbers are not the file's: each line is found where it is written, the file a line
|
|
23
|
+
* before it was found in first, from there on, and each place once.
|
|
24
|
+
*/
|
|
25
|
+
export function locateDiffLines(diff, files) {
|
|
26
|
+
const where = {};
|
|
27
|
+
// Each line as the parser renders it: a link `[text](path)` is the span `` `[resource]text` `` in the spec as read.
|
|
28
|
+
const texts = files.map((file) => ({ name: file.name, lines: file.text.split("\n").map((line) => asRead(line.trimEnd())) }));
|
|
29
|
+
// The section each line of a file is in: the nearest `***...***` header above it, or on it.
|
|
30
|
+
const sections = texts.map((file) => {
|
|
31
|
+
let current;
|
|
32
|
+
return file.lines.map((line) => (SECTION.test(line.trim()) ? (current = line.trim()) : current));
|
|
33
|
+
});
|
|
34
|
+
const used = new Set();
|
|
35
|
+
let last;
|
|
36
|
+
const take = (file, line, index) => {
|
|
37
|
+
used.add(`${file}:${line}`);
|
|
38
|
+
const section = sections[file][line];
|
|
39
|
+
where[index] = { file: texts[file].name, line: line + 1, ...(section !== undefined ? { section } : {}) };
|
|
40
|
+
last = { file, line };
|
|
41
|
+
};
|
|
42
|
+
diff
|
|
43
|
+
.replace(/\n+$/, "")
|
|
44
|
+
.split("\n")
|
|
45
|
+
.forEach((line, index) => {
|
|
46
|
+
if (HEADER.test(line) || line.startsWith("+++ ") || line.startsWith("@@") || line.startsWith("\\"))
|
|
47
|
+
return;
|
|
48
|
+
if (line.startsWith("-")) {
|
|
49
|
+
// A removed line still written in a file - taken out of what is compared, not out of the spec - is that file's.
|
|
50
|
+
const removed = asRead(line.slice(1).trimEnd());
|
|
51
|
+
if (removed.trim() === "" || SECTION.test(removed.trim()))
|
|
52
|
+
return;
|
|
53
|
+
const file = texts.findIndex((candidate) => candidate.lines.includes(removed));
|
|
54
|
+
if (file !== -1) {
|
|
55
|
+
const at = texts[file].lines.indexOf(removed);
|
|
56
|
+
const section = sections[file][at];
|
|
57
|
+
where[index] = { file: texts[file].name, line: at + 1, ...(section !== undefined ? { section } : {}) };
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const text = (line.startsWith("+") || line.startsWith(" ") ? line.slice(1) : line).trimEnd();
|
|
62
|
+
if (text === "") {
|
|
63
|
+
// A blank line: the one right after the line before it, when that is blank.
|
|
64
|
+
if (last !== undefined && texts[last.file].lines[last.line + 1] === "" && !used.has(`${last.file}:${last.line + 1}`))
|
|
65
|
+
take(last.file, last.line + 1, index);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const order = last === undefined ? texts.map((_, file) => file) : [last.file, ...texts.map((_, file) => file).filter((file) => file !== last.file)];
|
|
69
|
+
for (const file of order) {
|
|
70
|
+
const lines = texts[file].lines;
|
|
71
|
+
const from = last !== undefined && file === last.file ? last.line + 1 : 0;
|
|
72
|
+
const found = [...lines.keys()].slice(from).concat([...lines.keys()].slice(0, from)).find((at) => lines[at] === text && !used.has(`${file}:${at}`));
|
|
73
|
+
if (found !== undefined)
|
|
74
|
+
return take(file, found, index);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return where;
|
|
78
|
+
}
|
|
79
|
+
const HEADER = /^(diff --git |index |--- |new file mode |deleted file mode )/;
|
|
80
|
+
/**
|
|
81
|
+
* A spec diff, file by file: each file's lines together, however the diff has them - a spec diff is of the
|
|
82
|
+
* spec as read, so what one file holds can come in more than one place - the file being rendered first,
|
|
83
|
+
* the rest by name. Each line is in the file it is placed in (`where`); a line placed nowhere - a removed
|
|
84
|
+
* one, or one written other than it reads - goes with the line placed before it, else the one after it,
|
|
85
|
+
* else the file the diff's own header names. The file being rendered is the one the diff's header names.
|
|
86
|
+
*/
|
|
87
|
+
export function diffFiles(diff, width, paint = PLAIN_PAINT, where) {
|
|
88
|
+
const inner = Math.max(10, width);
|
|
89
|
+
const lines = diff.replace(/\n+$/, "").split("\n");
|
|
90
|
+
// The gutter: as wide as the largest line number there is. None known, there is no gutter.
|
|
91
|
+
const digits = where === undefined || Object.keys(where).length === 0 ? 0 : String(Math.max(...Object.values(where).map((at) => at.line))).length;
|
|
92
|
+
// Every line highlighted in the diff's order, so front matter one opens is carried to the lines after it on its side.
|
|
93
|
+
let before = [];
|
|
94
|
+
let after = [];
|
|
95
|
+
let headerFile;
|
|
96
|
+
let removedFrom;
|
|
97
|
+
let rendered;
|
|
98
|
+
// The section the diff is in on each side, from the headers it shows: for lines placed nowhere.
|
|
99
|
+
let sectionBefore;
|
|
100
|
+
let sectionAfter;
|
|
101
|
+
const read = [];
|
|
102
|
+
for (const [index, line] of lines.entries()) {
|
|
103
|
+
if (line.startsWith("--- "))
|
|
104
|
+
removedFrom = line.slice(4).replace(/^a\//, "");
|
|
105
|
+
if (line.startsWith("+++ ")) {
|
|
106
|
+
// The file, by the name it has after the change - or had, when the change removes it.
|
|
107
|
+
const named = line.slice(4).replace(/^b\//, "");
|
|
108
|
+
headerFile = named === "/dev/null" ? (removedFrom ?? named) : named;
|
|
109
|
+
rendered ??= headerFile;
|
|
110
|
+
before = [];
|
|
111
|
+
after = [];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (HEADER.test(line) || line.startsWith("@@") || line.startsWith("\\"))
|
|
115
|
+
continue;
|
|
116
|
+
const sign = line[0] === "+" || line[0] === "-" ? line[0] : " ";
|
|
117
|
+
const text = sign === " " && !line.startsWith(" ") ? line : line.slice(1);
|
|
118
|
+
let tokens;
|
|
119
|
+
if (sign === "-")
|
|
120
|
+
({ tokens, state: before } = highlightLine(text, before));
|
|
121
|
+
else if (sign === "+")
|
|
122
|
+
({ tokens, state: after } = highlightLine(text, after));
|
|
123
|
+
else {
|
|
124
|
+
// On both sides: each carries what it has open on.
|
|
125
|
+
({ tokens, state: before } = highlightLine(text, before));
|
|
126
|
+
after = highlightLine(text, after).state;
|
|
127
|
+
}
|
|
128
|
+
const coloured = tokens
|
|
129
|
+
.map((token) => {
|
|
130
|
+
const role = roleOf(token.scopes);
|
|
131
|
+
return role === undefined ? token.text : paint.syntax(role, token.text);
|
|
132
|
+
})
|
|
133
|
+
.join("");
|
|
134
|
+
const header = SECTION.test(text.trim());
|
|
135
|
+
if (header && sign !== "+")
|
|
136
|
+
sectionBefore = text.trim();
|
|
137
|
+
if (header && sign !== "-")
|
|
138
|
+
sectionAfter = text.trim();
|
|
139
|
+
const at = where?.[index];
|
|
140
|
+
const section = at?.section ?? (sign === "-" ? sectionBefore : sectionAfter);
|
|
141
|
+
read.push({ index, sign, coloured, file: at?.file ?? headerFile ?? "", header, ...(headerFile !== undefined ? { under: headerFile } : {}), ...(at !== undefined ? { at } : {}), ...(section !== undefined ? { section } : {}) });
|
|
142
|
+
}
|
|
143
|
+
// A line placed nowhere is never put in a file on a guess. A removed one - in no file now - is in the file of the
|
|
144
|
+
// change it is part of: the lines placed in its own run of removed and added lines, or else the line right beside
|
|
145
|
+
// it. Any other goes with the lines found in no file, named as such.
|
|
146
|
+
// With nothing placed at all - a diff of the files themselves - its own headers say which file each line is in.
|
|
147
|
+
for (const [at, line] of read.entries()) {
|
|
148
|
+
if (line.at !== undefined || where === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
// Under a header of another file than the one rendered - a linked resource whose content changed - it is that file's.
|
|
151
|
+
if (line.under !== undefined && line.under !== rendered) {
|
|
152
|
+
line.file = line.under;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
line.file = NOT_FOUND;
|
|
156
|
+
// A blank line says nothing of where it is: it goes with the line before it in the diff, or else the one after it.
|
|
157
|
+
// Never a line of its own under "not found": beside nothing placed, it goes with the file being rendered.
|
|
158
|
+
if (visibleWidth(line.coloured.trim()) === 0) {
|
|
159
|
+
const candidates = [read[at - 1]?.file, read[at + 1]?.at?.file, headerFile];
|
|
160
|
+
line.file = candidates.find((file) => file !== undefined && file !== NOT_FOUND) ?? line.file;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (line.sign !== "-")
|
|
164
|
+
continue;
|
|
165
|
+
const run = [];
|
|
166
|
+
for (let i = at; i >= 0 && read[i].sign !== " " && (i === at || read[i + 1].index - read[i].index === 1); i -= 1)
|
|
167
|
+
run.unshift(read[i]);
|
|
168
|
+
for (let i = at + 1; i < read.length && read[i].sign !== " " && read[i].index - read[i - 1].index === 1; i += 1)
|
|
169
|
+
run.push(read[i]);
|
|
170
|
+
const placed = run.find((other) => other.sign === "+" && other.at !== undefined) ?? run.find((other) => other.at !== undefined);
|
|
171
|
+
// A section's header beside it says nothing of its file: every spec has one.
|
|
172
|
+
const beside = [read[at - 1], read[at + run.length - run.indexOf(line)]].find((other) => other !== undefined && other.at !== undefined && !other.header && Math.abs(other.index - line.index) <= run.length);
|
|
173
|
+
// Beside nothing placed, it left the spec being shown.
|
|
174
|
+
const file = placed?.at?.file ?? beside?.at?.file ?? rendered;
|
|
175
|
+
if (file !== undefined)
|
|
176
|
+
line.file = file;
|
|
177
|
+
}
|
|
178
|
+
// The file being rendered first - the diff names it by its path from where it was written - then the rest by name.
|
|
179
|
+
const names = [...new Set(read.map((line) => line.file))];
|
|
180
|
+
const isRendered = (name) => rendered !== undefined && (name === rendered || rendered.endsWith(`/${name}`) || name.endsWith(`/${rendered}`));
|
|
181
|
+
// The lines found in no file last.
|
|
182
|
+
const rank = (name) => (name === NOT_FOUND ? 2 : isRendered(name) ? 0 : 1);
|
|
183
|
+
names.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
184
|
+
// Only the files the diff changes: one whose lines are all context around a change elsewhere is not shown, and nor is
|
|
185
|
+
// one whose only change is a blank line - spacing, not the spec.
|
|
186
|
+
const changed = names.filter((file) => read.some((line) => line.file === file && line.sign !== " " && visibleWidth(line.coloured.trim()) > 0));
|
|
187
|
+
return changed.map((file) => {
|
|
188
|
+
const own = read.filter((line) => line.file === file);
|
|
189
|
+
const rows = [];
|
|
190
|
+
let previous;
|
|
191
|
+
// Lines of the file not in the diff, as GitHub marks them: above the first block, and between two.
|
|
192
|
+
// Its section beside it, as GitHub names a hunk's function - once: not when the block starts with the section's
|
|
193
|
+
// header itself, nor when the lines above it are in that section already, its name shown there.
|
|
194
|
+
const hidden = (arrow, count, next, above) => {
|
|
195
|
+
const lines = paint.muted(`${" ".repeat(digits)}${digits > 0 ? " " : ""}${arrow}${count !== undefined && count > 0 ? ` ${count} line${count === 1 ? "" : "s"}` : ""}`);
|
|
196
|
+
const named = next?.section !== undefined && !next.header && above?.section !== next.section;
|
|
197
|
+
return named ? `${lines}${paint.muted(" · ")}${paint.syntax("section", next.section)}` : lines;
|
|
198
|
+
};
|
|
199
|
+
const firstAt = own.find((line) => line.at !== undefined)?.at?.line;
|
|
200
|
+
if (own[0] !== undefined) {
|
|
201
|
+
// Above the first block: how many lines are hidden there, when that is known - and its section, unless it starts with it.
|
|
202
|
+
const count = own[0].at !== undefined && firstAt !== undefined ? firstAt - 1 : undefined;
|
|
203
|
+
if ((count !== undefined && count > 0) || (own[0].section !== undefined && !own[0].header))
|
|
204
|
+
rows.push(hidden("↑", count, own[0]));
|
|
205
|
+
}
|
|
206
|
+
for (const line of own) {
|
|
207
|
+
// Where its lines do not follow on from each other - another hunk, or another place in the file - the lines between are hidden.
|
|
208
|
+
if (previous !== undefined) {
|
|
209
|
+
const gap = line.at !== undefined && previous.at !== undefined ? line.at.line - previous.at.line - 1 : undefined;
|
|
210
|
+
const follows = gap !== undefined ? gap <= 0 : line.index - previous.index <= 1;
|
|
211
|
+
if (!follows)
|
|
212
|
+
rows.push(hidden("↕", gap, line, previous));
|
|
213
|
+
}
|
|
214
|
+
previous = line;
|
|
215
|
+
const mark = line.sign === "+" ? paint.added("+") : line.sign === "-" ? paint.removed("-") : " ";
|
|
216
|
+
const gutter = line.at !== undefined ? paint.muted(String(line.at.line).padStart(digits)) : " ".repeat(digits);
|
|
217
|
+
const lead = digits > 0 ? `${gutter} ` : "";
|
|
218
|
+
const blank = " ".repeat(digits > 0 ? digits + 1 : 0);
|
|
219
|
+
const [first = "", ...rest] = line.coloured === "" ? [""] : wrapTextWithAnsi(line.coloured, inner - 2 - blank.length);
|
|
220
|
+
// Continuations under the text, not the number or the sign: those columns stay clear.
|
|
221
|
+
for (const row of [`${lead}${mark} ${first}`, ...rest.map((part) => `${blank} ${part}`)]) {
|
|
222
|
+
const padded = `${row}${" ".repeat(Math.max(0, inner - visibleWidth(row)))}`;
|
|
223
|
+
rows.push(line.sign === "+" ? paint.addedBand(padded) : line.sign === "-" ? paint.removedBand(padded) : row);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { file, added: own.filter((line) => line.sign === "+").length, removed: own.filter((line) => line.sign === "-").length, rows };
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** A file's header: its name, and what it adds and takes out. `open`, with the mark of a group that opens and closes. */
|
|
230
|
+
export function fileHeader(file, paint, open) {
|
|
231
|
+
const mark = open === undefined ? "" : open ? "▾ " : "▸ ";
|
|
232
|
+
const counts = [file.added > 0 ? paint.added(`+${file.added}`) : "", file.removed > 0 ? paint.removed(`−${file.removed}`) : ""].filter(Boolean).join(" ");
|
|
233
|
+
return `${paint.muted(`${mark}${file.file === "" ? "(the spec)" : file.file}`)}${counts !== "" ? ` ${counts}` : ""}`;
|
|
234
|
+
}
|
|
235
|
+
/** Where a spec diff's lines go that are in none of the spec's files as they are written. */
|
|
236
|
+
export const NOT_FOUND = "not found in the spec's files";
|
|
237
|
+
/** The diff's lines, at most `width` wide: every file, each under its header - for a view that opens none and closes none. */
|
|
238
|
+
export function diffView(diff, width, paint = PLAIN_PAINT, where) {
|
|
239
|
+
return diffFiles(diff, width, paint, where).flatMap((file) => [fileHeader(file, paint), ...file.rows]);
|
|
240
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { DEFAULT_CONCEPTS, extractConceptsFromDefinition, extractConceptsFromSpecText } from "../plain/concepts.js";
|
|
2
|
+
import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS } from "../plain/spec.js";
|
|
3
|
+
import { codeTree, conformanceTarget, marksRegenerateAgain } from "./records.js";
|
|
4
|
+
import { applyChanges, changesBetween, parseTree, resourcesChanged, serializeTree } from "./spec-steps.js";
|
|
5
|
+
/**
|
|
6
|
+
* The work, derived from the records - see docs/regenerating.md. Nothing here is stored: what is left
|
|
7
|
+
* to build is the difference between what a record says is built and what the spec says now, worked
|
|
8
|
+
* out again whenever either changes.
|
|
9
|
+
*
|
|
10
|
+
* - Code work: from the code record to the spec as the code sees it.
|
|
11
|
+
* - Conformance work: from the tests record to what the conformance tests are to be written for now -
|
|
12
|
+
* the chosen functional specs the code is built from.
|
|
13
|
+
*
|
|
14
|
+
* A job is a set of those changes. What it tells its agents follows from the functional specs it
|
|
15
|
+
* touches - its cases - and a running job is stopped once the spec no longer wants what it makes.
|
|
16
|
+
*/
|
|
17
|
+
/** Everything left to build in the code: from what the code record says it is built from, to the spec now. */
|
|
18
|
+
export function codeWork(record, spec) {
|
|
19
|
+
return changesBetween(serializeTree(codeTree(record ?? {})), serializeTree(codeTree(spec)));
|
|
20
|
+
}
|
|
21
|
+
/** Everything left to do in the conformance tests: from what the tests record says they are written for, to what they are to be now. */
|
|
22
|
+
export function conformanceWork(tests, code, spec, chosen) {
|
|
23
|
+
return changesBetween(serializeTree(tests ?? {}), serializeTree(conformanceTarget(code, tests, spec, chosen)));
|
|
24
|
+
}
|
|
25
|
+
const functionalSpecs = (tree) => tree[FUNCTIONAL_REQUIREMENTS] ?? [];
|
|
26
|
+
const firstLine = (markdown) => markdown.trim().split("\n")[0].replace(/^-\s*/, "").trim();
|
|
27
|
+
/** An item of a serialized tree, read back. */
|
|
28
|
+
const itemOfLine = (line) => {
|
|
29
|
+
if (line === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(line);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/** The concept a definition defines, from its text. */
|
|
39
|
+
const definedBy = (markdown) => extractConceptsFromDefinition(markdown)[0];
|
|
40
|
+
/** The concepts a text names, the predefined ones left out. */
|
|
41
|
+
const namedIn = (text) => extractConceptsFromSpecText(text).filter((concept) => !DEFAULT_CONCEPTS.has(concept));
|
|
42
|
+
/**
|
|
43
|
+
* Every concept a functional spec uses: those its text names, and those their definitions name in
|
|
44
|
+
* turn, as far as it goes.
|
|
45
|
+
*/
|
|
46
|
+
export function conceptsUsed(markdown, definitions) {
|
|
47
|
+
const byConcept = new Map();
|
|
48
|
+
for (const definition of definitions)
|
|
49
|
+
for (const concept of definedBy(definition.markdown))
|
|
50
|
+
byConcept.set(concept, definition.markdown);
|
|
51
|
+
const used = new Set();
|
|
52
|
+
const visit = (text) => {
|
|
53
|
+
for (const concept of namedIn(text)) {
|
|
54
|
+
if (used.has(concept))
|
|
55
|
+
continue;
|
|
56
|
+
used.add(concept);
|
|
57
|
+
const definition = byConcept.get(concept);
|
|
58
|
+
if (definition !== undefined)
|
|
59
|
+
visit(definition);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
visit(markdown);
|
|
63
|
+
return used;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The cases of a job: `before` is the tree it starts from - the record, or what the jobs ahead of it
|
|
67
|
+
* leave - and `changes` the changes it makes, found from `before`. `fresh`: the module requires none
|
|
68
|
+
* and nothing is built.
|
|
69
|
+
*
|
|
70
|
+
* A functional spec is patched when its item changed, or when a concept it uses - as the spec after
|
|
71
|
+
* the job defines it, or as it was defined before - is defined anew, changed or taken out by the job.
|
|
72
|
+
* `module`: the module whose codebase it is - its own functional specs are that module's.
|
|
73
|
+
*/
|
|
74
|
+
export function casesOf(before, changes, options = {}) {
|
|
75
|
+
const beforeText = serializeTree(before);
|
|
76
|
+
const after = parseTree(applyChanges(beforeText, changes));
|
|
77
|
+
const numberIn = (tree, item, position) => {
|
|
78
|
+
if (item?.frid !== undefined)
|
|
79
|
+
return item.frid;
|
|
80
|
+
const specs = functionalSpecs(tree);
|
|
81
|
+
const at = item === undefined ? -1 : specs.findIndex((spec) => spec.markdown === item.markdown);
|
|
82
|
+
return String(at === -1 ? position : at + 1);
|
|
83
|
+
};
|
|
84
|
+
const own = options.module !== undefined ? { module: options.module } : {};
|
|
85
|
+
const add = [];
|
|
86
|
+
const patch = [];
|
|
87
|
+
const again = [];
|
|
88
|
+
const removed = [];
|
|
89
|
+
for (const change of changes) {
|
|
90
|
+
const was = itemOfLine(change.before);
|
|
91
|
+
const is = itemOfLine(change.after);
|
|
92
|
+
// Marked to regenerate again: regenerated again, whatever else changed in its text - its own or, required, another module's.
|
|
93
|
+
if (change.kind === "change" && is !== undefined && marksRegenerateAgain(was)) {
|
|
94
|
+
const required = change.section === REQUIRED_FUNCTIONAL_SPECS ? is : undefined;
|
|
95
|
+
if (change.section === FUNCTIONAL_REQUIREMENTS)
|
|
96
|
+
again.push({ frid: numberIn(after, is, change.position), text: firstLine(is.markdown), ...own });
|
|
97
|
+
else if (required !== undefined)
|
|
98
|
+
again.push({ frid: `${required.frid ?? change.position}`, text: firstLine(is.markdown), ...(required.module !== undefined ? { module: required.module } : {}) });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (change.section !== FUNCTIONAL_REQUIREMENTS)
|
|
102
|
+
continue;
|
|
103
|
+
if (change.kind === "add" && is !== undefined)
|
|
104
|
+
add.push({ frid: numberIn(after, is, change.position), text: firstLine(is.markdown), ...own });
|
|
105
|
+
else if (change.kind === "remove" && was !== undefined)
|
|
106
|
+
removed.push({ frid: numberIn(before, was, change.position), text: firstLine(was.markdown), ...own });
|
|
107
|
+
else if (change.kind === "change" && is !== undefined)
|
|
108
|
+
patch.push({ frid: numberIn(after, is, change.position), text: firstLine(is.markdown), ...own });
|
|
109
|
+
}
|
|
110
|
+
// The concepts the job defines anew, changes or takes out.
|
|
111
|
+
const redefined = new Set();
|
|
112
|
+
for (const change of changes) {
|
|
113
|
+
if (change.section !== DEFINITIONS)
|
|
114
|
+
continue;
|
|
115
|
+
for (const line of [change.before, change.after]) {
|
|
116
|
+
const item = itemOfLine(line);
|
|
117
|
+
if (item !== undefined)
|
|
118
|
+
for (const concept of definedBy(item.markdown))
|
|
119
|
+
redefined.add(concept);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (redefined.size > 0) {
|
|
123
|
+
const touched = new Set([...add, ...patch, ...again].filter((spec) => spec.module === options.module).map((spec) => spec.frid));
|
|
124
|
+
const definitionsAfter = after[DEFINITIONS] ?? [];
|
|
125
|
+
const definitionsBefore = before[DEFINITIONS] ?? [];
|
|
126
|
+
functionalSpecs(after).forEach((item, index) => {
|
|
127
|
+
const frid = item.frid ?? String(index + 1);
|
|
128
|
+
if (touched.has(frid))
|
|
129
|
+
return;
|
|
130
|
+
const uses = new Set([...conceptsUsed(item.markdown, definitionsAfter), ...conceptsUsed(item.markdown, definitionsBefore)]);
|
|
131
|
+
if ([...redefined].some((concept) => uses.has(concept)))
|
|
132
|
+
patch.push({ frid, text: firstLine(item.markdown), ...own });
|
|
133
|
+
});
|
|
134
|
+
patch.sort((a, b) => Number(a.frid) - Number(b.frid));
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
fresh: options.fresh === true,
|
|
138
|
+
add,
|
|
139
|
+
patch,
|
|
140
|
+
again,
|
|
141
|
+
delete: removed,
|
|
142
|
+
mechanical: add.length === 0 && patch.length === 0 && again.length === 0 && removed.length === 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Two changes are the same change when they do the same to the same item. */
|
|
146
|
+
function sameChange(a, b) {
|
|
147
|
+
return a.section === b.section && a.kind === b.kind && (a.before ?? "") === (b.before ?? "") && (a.after ?? "") === (b.after ?? "");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether a running job may finish: every change it makes is still part of the work as it is now -
|
|
151
|
+
* both measured from the same record, which nothing changes while the job runs. When one is not, the
|
|
152
|
+
* job is making something the spec no longer wants, and it is stopped and rolled back.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Why a running job is no longer wanted, in a line: the files it links that hold something else now - the commonest
|
|
156
|
+
* cause, a resource edited while the job regenerated from it - else the items whose text changed under it, by section
|
|
157
|
+
* and place.
|
|
158
|
+
*/
|
|
159
|
+
export function whyNotWanted(running, work) {
|
|
160
|
+
const taken = new Set();
|
|
161
|
+
const gone = running.filter((mine) => {
|
|
162
|
+
const at = work.findIndex((change, index) => !taken.has(index) && sameChange(mine, change));
|
|
163
|
+
if (at === -1)
|
|
164
|
+
return true;
|
|
165
|
+
taken.add(at);
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
const files = new Set();
|
|
169
|
+
const items = [];
|
|
170
|
+
for (const mine of gone) {
|
|
171
|
+
// The same item as the spec has it now: by section and place.
|
|
172
|
+
const now = work.find((change) => change.section === mine.section && change.position === mine.position);
|
|
173
|
+
const changedFiles = now !== undefined ? resourcesChanged(mine.after, now.after).map((resource) => resource.target) : [];
|
|
174
|
+
if (changedFiles.length > 0)
|
|
175
|
+
changedFiles.forEach((file) => files.add(file));
|
|
176
|
+
else
|
|
177
|
+
items.push(`${mine.section} ${mine.position}`);
|
|
178
|
+
}
|
|
179
|
+
const parts = [
|
|
180
|
+
...(files.size > 0 ? [`${[...files].join(", ")} ${files.size === 1 ? "holds" : "hold"} something else now`] : []),
|
|
181
|
+
...(items.length > 0 ? [`${items.join(", ")} ${items.length === 1 ? "reads" : "read"} otherwise now`] : []),
|
|
182
|
+
];
|
|
183
|
+
return parts.length > 0 ? parts.join("; ") : "the spec it was building from changed";
|
|
184
|
+
}
|
|
185
|
+
export function stillWanted(running, work) {
|
|
186
|
+
const taken = new Set();
|
|
187
|
+
return running.every((mine) => {
|
|
188
|
+
const at = work.findIndex((change, index) => !taken.has(index) && sameChange(mine, change));
|
|
189
|
+
if (at === -1)
|
|
190
|
+
return false;
|
|
191
|
+
taken.add(at);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model the agents of a job run on.
|
|
3
|
+
*
|
|
4
|
+
* By default, whatever the conversation is using. The person can say otherwise - "render spec
|
|
5
|
+
* changes with Gemini Flash" - for one kind of work or for all of it, and that holds for the
|
|
6
|
+
* rest of the session, for jobs started from a tool call and for jobs a skill queues alike. A
|
|
7
|
+
* single tool call can also name a model for itself alone.
|
|
8
|
+
*
|
|
9
|
+
* A name is resolved strictly. pi's own pattern matching picks its best guess and says nothing
|
|
10
|
+
* - "gpt-5" becomes a Bedrock model with no credentials, "flash" a GLM one - and a job on a
|
|
11
|
+
* guessed model is worse than a question. So an exact id is taken as it is, a looser name is
|
|
12
|
+
* taken only when it fits exactly one model that has credentials, and anything else comes back
|
|
13
|
+
* as a short list to ask the person about.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_CANDIDATES = 8;
|
|
16
|
+
function label(model) {
|
|
17
|
+
return `${model.provider}/${model.id}`;
|
|
18
|
+
}
|
|
19
|
+
function describe(model, catalog) {
|
|
20
|
+
const name = model.name && model.name !== model.id ? ` (${model.name})` : "";
|
|
21
|
+
return `${label(model)}${name}${catalog.hasConfiguredAuth(model) ? "" : " - no credentials"}`;
|
|
22
|
+
}
|
|
23
|
+
/** The words of a model name, lowercased: "Gemini Flash 3.1" and "gemini-3.1-flash" alike. */
|
|
24
|
+
function words(text) {
|
|
25
|
+
return text
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.split(/[\s/_:-]+|(?<=[a-z])(?=\d)|(?<=\d)(?=[a-z])/)
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
function noCredentials(model) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
message: `${label(model)} is known, but ${model.provider} has no credentials here. ` +
|
|
34
|
+
"Log in with /login, or set the provider's API key, and try again.",
|
|
35
|
+
candidates: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function found(model) {
|
|
39
|
+
return { ok: true, model: label(model), name: model.name ?? model.id };
|
|
40
|
+
}
|
|
41
|
+
/** Resolve what the person called a model to one this machine can run, or say why not. */
|
|
42
|
+
export function resolveModel(query, catalog) {
|
|
43
|
+
const wanted = query.trim();
|
|
44
|
+
const all = catalog.getAll();
|
|
45
|
+
const usable = (model) => catalog.hasConfiguredAuth(model);
|
|
46
|
+
// An exact reference: provider/id, or a bare id.
|
|
47
|
+
const exact = all.filter((model) => label(model).toLowerCase() === wanted.toLowerCase());
|
|
48
|
+
if (exact.length > 0)
|
|
49
|
+
return usable(exact[0]) ? found(exact[0]) : noCredentials(exact[0]);
|
|
50
|
+
const byId = all.filter((model) => model.id.toLowerCase() === wanted.toLowerCase());
|
|
51
|
+
const byIdUsable = byId.filter(usable);
|
|
52
|
+
if (byIdUsable.length === 1)
|
|
53
|
+
return found(byIdUsable[0]);
|
|
54
|
+
if (byId.length === 1)
|
|
55
|
+
return noCredentials(byId[0]);
|
|
56
|
+
// A looser name: every word of it has to be in the model's provider, id or name.
|
|
57
|
+
const asked = words(wanted);
|
|
58
|
+
const matches = (byId.length > 1 ? byId : all).filter((model) => {
|
|
59
|
+
const have = new Set(words(`${model.provider} ${model.id} ${model.name ?? ""}`));
|
|
60
|
+
return asked.length > 0 && asked.every((word) => have.has(word));
|
|
61
|
+
});
|
|
62
|
+
const matchesUsable = matches.filter(usable);
|
|
63
|
+
if (matchesUsable.length === 1)
|
|
64
|
+
return found(matchesUsable[0]);
|
|
65
|
+
// Several fit, but one is exactly what was said - "gpt 5.5" is gpt-5.5, not gpt-5.5-pro.
|
|
66
|
+
const same = (model) => {
|
|
67
|
+
const id = words(model.id).filter((word) => !words(model.provider).includes(word));
|
|
68
|
+
const said = asked.filter((word) => !words(model.provider).includes(word));
|
|
69
|
+
return id.length === said.length && said.every((word) => id.includes(word));
|
|
70
|
+
};
|
|
71
|
+
const exactly = matchesUsable.filter(same);
|
|
72
|
+
if (exactly.length === 1)
|
|
73
|
+
return found(exactly[0]);
|
|
74
|
+
const shown = [...matchesUsable, ...matches.filter((model) => !usable(model))].slice(0, MAX_CANDIDATES);
|
|
75
|
+
if (shown.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: `No model matches '${wanted}'. Give its provider/id - pi's /model lists the ones configured here.`,
|
|
79
|
+
candidates: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const more = matches.length > shown.length ? ` (and ${matches.length - shown.length} more)` : "";
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
message: matchesUsable.length === 0
|
|
86
|
+
? `'${wanted}' matches no model that has credentials here. The closest${more}:`
|
|
87
|
+
: `'${wanted}' fits more than one model. Ask which one is meant${more}:`,
|
|
88
|
+
candidates: shown.map((model) => describe(model, catalog)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** The resolution as text for the agent: the model, or why not and what to ask. */
|
|
92
|
+
export function describeResolution(resolution) {
|
|
93
|
+
if (resolution.ok)
|
|
94
|
+
return `${resolution.model} (${resolution.name})`;
|
|
95
|
+
return [resolution.message, ...resolution.candidates.map((candidate) => `- ${candidate}`)].join("\n");
|
|
96
|
+
}
|
|
97
|
+
export const WORK_LABEL = {
|
|
98
|
+
spec_changes: "spec changes",
|
|
99
|
+
functionalities: "functionalities",
|
|
100
|
+
all: "everything",
|
|
101
|
+
};
|
|
102
|
+
/** The model a job of this kind runs on: chosen for it, chosen for everything, or the chat's. */
|
|
103
|
+
export function modelFor(choices, work, chat) {
|
|
104
|
+
return choices[work] ?? choices.all ?? chat;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Where each kind of work runs now, in a line each: chosen in this session, or the default coding
|
|
108
|
+
* model - from `--coding-model` or config.turri.yaml, said which - or the conversation's.
|
|
109
|
+
*/
|
|
110
|
+
export function describeChoices(choices, chat, project) {
|
|
111
|
+
const fallback = project !== undefined ? `${project.model} (${project.from})` : `${chat ?? "pi's default model"} (the chat's model)`;
|
|
112
|
+
return ["spec_changes", "functionalities"]
|
|
113
|
+
.map((work) => {
|
|
114
|
+
const chosen = choices[work] ?? choices.all;
|
|
115
|
+
return `- ${WORK_LABEL[work]}: ${chosen ?? fallback}`;
|
|
116
|
+
})
|
|
117
|
+
.join("\n");
|
|
118
|
+
}
|