@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc
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 +320 -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 +88 -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 +119 -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,955 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
6
|
+
import { confinedTools } from "./confine.js";
|
|
7
|
+
import { ensureInModulesScratch, modulesScratchDir } from "./paths.js";
|
|
8
|
+
import { workspaceBlock } from "./workspace.js";
|
|
9
|
+
import { reportActivity } from "./activity.js";
|
|
10
|
+
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
11
|
+
import { diffFiles, digestFiles } from "./codebase.js";
|
|
12
|
+
import { ERROR_CAUSE_RULES, fixMethod, INTEGRITY_RULES, specPrimer } from "./prompt.js";
|
|
13
|
+
import { conformanceReviewCasesBlock } from "./cases.js";
|
|
14
|
+
import { REVIEW_LESSON } from "./notes.js";
|
|
15
|
+
import { CODER_COMPACTION, REVIEWER_COMPACTION } from "./compaction.js";
|
|
16
|
+
import { SUBMIT_AFTER_REVIEW } from "./submit-work.js";
|
|
17
|
+
/** An exploration as the test author, and the judge, read it. */
|
|
18
|
+
export function explorationText(exploration, replies = []) {
|
|
19
|
+
const sections = [
|
|
20
|
+
`## How to build and run it\n\n${exploration.buildAndRun.trim()}`,
|
|
21
|
+
`## How the conformance tests reach it, run by the testing script\n\n${exploration.testsReachIt.trim()}`,
|
|
22
|
+
`## How each functionality is reached\n\n${exploration.interfaces.map((item) => `### ${item.module} ${item.frid}\n\n${item.how.trim()}`).join("\n\n")}`,
|
|
23
|
+
`## What it needs around it\n\n${exploration.environment.trim()}`,
|
|
24
|
+
`## Plan for the conformance tests\n\n${exploration.plan.map((item) => `### ${item.module} ${item.frid}\n\n${item.tests.trim()}`).join("\n\n")}`,
|
|
25
|
+
...(exploration.other?.trim() ? [`## Anything else\n\n${exploration.other.trim()}`] : []),
|
|
26
|
+
// An account it could not complete is passed on with what it wrote, so nothing it found is lost.
|
|
27
|
+
...(replies.length > 0 ? [`## What the explorer wrote as it worked\n\n${replies.map((reply) => reply.trim()).join("\n\n")}`] : []),
|
|
28
|
+
];
|
|
29
|
+
return sections.join("\n\n");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What an exploration lacks for the conformance tests to be written from it alone, or nothing when
|
|
33
|
+
* it is whole: how to build and run it, and - for every functionality the tests are for - how it
|
|
34
|
+
* is reached and what to test.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* How many tool calls the agents with one thing to decide may make before they are told to decide:
|
|
38
|
+
* the explorer its account, the reviewer its review, the judge its verdict. Past twice that, the turn
|
|
39
|
+
* is stopped. A judge that reads 40 files has read enough; one that has not decided by then is going
|
|
40
|
+
* round in a circle.
|
|
41
|
+
*/
|
|
42
|
+
export const EXPLORER_CALLS = 40;
|
|
43
|
+
export const REVIEWER_CALLS = 40;
|
|
44
|
+
export const JUDGE_CALLS = 30;
|
|
45
|
+
/** How long an exploration may be: the interface, not the code. Longer is sent back to be cut down. */
|
|
46
|
+
export const MAX_EXPLORATION_CHARS = 12_000;
|
|
47
|
+
/** How many times the explorer may hand an exploration over: each ends its turn, and is checked. */
|
|
48
|
+
export const EXPLORATION_SUBMISSIONS = 3;
|
|
49
|
+
export function explorationProblems(exploration, scope, moduleName,
|
|
50
|
+
/** The functionalities whose acceptance tests the plan has to carry out, and how many each has. */
|
|
51
|
+
acceptance = []) {
|
|
52
|
+
if (exploration === undefined)
|
|
53
|
+
return ["You have not called submit_exploration."];
|
|
54
|
+
const problems = [];
|
|
55
|
+
const size = explorationText(exploration).length;
|
|
56
|
+
if (size > MAX_EXPLORATION_CHARS) {
|
|
57
|
+
problems.push(`It is ${size} characters - keep it under ${MAX_EXPLORATION_CHARS}. Hand over the interface, not the code: the routes, ` +
|
|
58
|
+
"commands, fields and their types, and the setup - no code listings, no files pasted whole.");
|
|
59
|
+
}
|
|
60
|
+
const unknown = [...exploration.interfaces, ...exploration.plan].filter((item) => item.frid !== "" && !scope.functionalities.some((ref) => ref.module === item.module && ref.frid === item.frid));
|
|
61
|
+
if (unknown.length > 0 && scope.tested === undefined) {
|
|
62
|
+
const names = [...new Set(unknown.map((item) => `${item.module} ${item.frid}`))];
|
|
63
|
+
problems.push(`${names.join(", ")} ${names.length === 1 ? "is not a functionality" : "are not functionalities"}: give module and id as ` +
|
|
64
|
+
`the specification numbers them - ${scope.functionalities.filter((ref) => ref.module === moduleName).map(refLabel).join(", ") || "none yet"}.`);
|
|
65
|
+
}
|
|
66
|
+
const short = (text) => (text ?? "").trim().length < 40;
|
|
67
|
+
if (short(exploration.buildAndRun))
|
|
68
|
+
problems.push("build_and_run says too little: what to install and build, the exact command that starts or runs it, where it listens and how to tell it is up.");
|
|
69
|
+
if (short(exploration.testsReachIt)) {
|
|
70
|
+
problems.push("tests_reach_it says too little: read the conformance testing script - what it builds or installs, and where - and say exactly " +
|
|
71
|
+
"how the tests it runs reach the software: the package, module, entry point, path or address they depend on or start, " +
|
|
72
|
+
"with its exact name and version, and whether they start it themselves and how.");
|
|
73
|
+
}
|
|
74
|
+
if (short(exploration.environment))
|
|
75
|
+
problems.push("environment says too little: every variable, configuration file, service, database and fixture it needs, and how each test gets a fresh one - or say plainly it needs none.");
|
|
76
|
+
const reached = exploration.interfaces.filter((item) => !short(item.how));
|
|
77
|
+
const planned = exploration.plan.filter((item) => !short(item.tests));
|
|
78
|
+
if (scope.tested !== undefined) {
|
|
79
|
+
// A functionality's run: the one under test, reached and planned for.
|
|
80
|
+
const tested = scope.tested;
|
|
81
|
+
const has = (list) => list.some((item) => item.module === tested.module && item.frid === tested.frid);
|
|
82
|
+
if (!has(reached))
|
|
83
|
+
problems.push(`interfaces does not say how ${refLabel(tested)} is reached: the command and its arguments, or the route, method, headers and the request and response fields.`);
|
|
84
|
+
if (!has(planned))
|
|
85
|
+
problems.push(`plan has no tests for ${refLabel(tested)}: the cases to write, how each is set up and invoked, and what it checks as the specification says.`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// A spec change's: what it touches is for the explorer to say, but some of this module has to be reached and planned.
|
|
89
|
+
if (!reached.some((item) => item.module === moduleName))
|
|
90
|
+
problems.push(`interfaces says nothing of how \`${moduleName}\`'s functionalities the change touches are reached - one entry each.`);
|
|
91
|
+
if (!planned.some((item) => item.module === moduleName))
|
|
92
|
+
problems.push(`plan has no tests for \`${moduleName}\`: the cases the change adds, changes or removes, for each functionality it touches.`);
|
|
93
|
+
}
|
|
94
|
+
// Every acceptance test is carried out, step by step, from outside: a plan that leaves one out
|
|
95
|
+
// leaves its author to write a placeholder.
|
|
96
|
+
for (const item of acceptance) {
|
|
97
|
+
const text = exploration.plan
|
|
98
|
+
.filter((entry) => entry.module === item.module && entry.frid === item.frid)
|
|
99
|
+
.map((entry) => entry.tests)
|
|
100
|
+
.join("\n");
|
|
101
|
+
const named = new Set([...text.matchAll(/acceptance test\s*(\d+)/gi)].map((match) => Number(match[1])));
|
|
102
|
+
const missing = Array.from({ length: item.count }, (_, index) => index + 1).filter((n) => !named.has(n));
|
|
103
|
+
if (missing.length > 0) {
|
|
104
|
+
problems.push(`plan does not carry out ${missing.map((n) => `acceptance test ${n}`).join(", ")} of ${item.module} ${item.frid}: name each ` +
|
|
105
|
+
'"acceptance test N" as the specification numbers them, and say how each of its steps is done from outside - through ' +
|
|
106
|
+
"which endpoint, or which class and method where the test reqs let the tests call a component directly - with what, " +
|
|
107
|
+
"and what it asserts. A long acceptance test is planned whole, never cut down.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return problems;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* How the built software is invoked from outside, and a plan for its conformance tests, as an agent
|
|
114
|
+
* that explored the code writes it - everything the tests' author works
|
|
115
|
+
* from. Checked before it is used: it has to cover every functionality the tests are for, and is sent
|
|
116
|
+
* back until it does. What the explorer wrote in its replies goes with an account it never completed.
|
|
117
|
+
*/
|
|
118
|
+
export async function exploreInvocation(options) {
|
|
119
|
+
// The codebase's account from an earlier run, and what of the code changed since: the explorer starts from it.
|
|
120
|
+
const previous = options.question === undefined ? previousExploration(options.buildDir) : undefined;
|
|
121
|
+
let exploration;
|
|
122
|
+
let submissions = 0;
|
|
123
|
+
const target = options.scope.tested;
|
|
124
|
+
const ref = {
|
|
125
|
+
module: Type.Optional(Type.String({ description: "The module the functionality is in" })),
|
|
126
|
+
id: Type.Optional(Type.String({
|
|
127
|
+
description: target !== undefined
|
|
128
|
+
? `The functionality's id as the specification numbers it, e.g. "${target.frid}" - leave both out for ${refLabel(target)}, the one under test`
|
|
129
|
+
: 'The functionality\'s id as the specification numbers it, e.g. "3" - never a name of your own',
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
/** An entry's functionality: as given when it is one, otherwise - in a functionality's run - the one under test. */
|
|
133
|
+
const owner = (item) => {
|
|
134
|
+
const given = { module: (item.module ?? "").trim() || options.spec.moduleName, frid: (item.id ?? "").trim() };
|
|
135
|
+
const known = options.scope.functionalities.some((known) => known.module === given.module && known.frid === given.frid);
|
|
136
|
+
return known || target === undefined ? given : { ...target };
|
|
137
|
+
};
|
|
138
|
+
const submit = defineTool({
|
|
139
|
+
name: "submit_exploration",
|
|
140
|
+
label: "Submit the exploration",
|
|
141
|
+
description: "Hand over everything the author of the conformance tests needs - they do not see your replies, only this. Call it once you are done; call it again to replace what you handed over.",
|
|
142
|
+
parameters: Type.Object({
|
|
143
|
+
build_and_run: Type.String({
|
|
144
|
+
description: "What to install and build, the exact command that starts or runs it, where it listens and how to tell it is up",
|
|
145
|
+
}),
|
|
146
|
+
tests_reach_it: Type.String({
|
|
147
|
+
description: "How the conformance tests, run by the conformance testing script, reach it: what the script builds or installs and where, the " +
|
|
148
|
+
"package, module, entry point, path or address the tests depend on or start, with its exact name and version, and " +
|
|
149
|
+
"whether the tests start it themselves and how",
|
|
150
|
+
}),
|
|
151
|
+
interfaces: Type.Array(Type.Object({
|
|
152
|
+
...ref,
|
|
153
|
+
how: Type.String({
|
|
154
|
+
description: "How this functionality is reached from outside: the command and its arguments, or the route, method and headers, and the request and response fields and their types",
|
|
155
|
+
}),
|
|
156
|
+
})),
|
|
157
|
+
environment: Type.String({
|
|
158
|
+
description: "What it needs around it: environment variables, configuration, services, databases, files, fixtures - and how each test gets a fresh one",
|
|
159
|
+
}),
|
|
160
|
+
plan: Type.Array(Type.Object({
|
|
161
|
+
...ref,
|
|
162
|
+
tests: Type.String({
|
|
163
|
+
description: "The conformance tests to write for it, from the specification and its acceptance tests: each case, how it is set up and invoked, what it checks as the specification says, and where its files go as the test reqs say - and every acceptance test, named \"acceptance test N\", with how each of its steps is done from outside and what it asserts",
|
|
164
|
+
}),
|
|
165
|
+
})),
|
|
166
|
+
other: Type.Optional(Type.String({ description: "Anything else the test author needs" })),
|
|
167
|
+
}),
|
|
168
|
+
async execute(_id, params) {
|
|
169
|
+
const given = params;
|
|
170
|
+
submissions += 1;
|
|
171
|
+
exploration = {
|
|
172
|
+
buildAndRun: given.build_and_run,
|
|
173
|
+
testsReachIt: given.tests_reach_it ?? "",
|
|
174
|
+
interfaces: given.interfaces.map((item) => ({ ...owner(item), how: item.how })),
|
|
175
|
+
environment: given.environment,
|
|
176
|
+
plan: given.plan.map((item) => ({ ...owner(item), tests: item.tests })),
|
|
177
|
+
other: given.other,
|
|
178
|
+
};
|
|
179
|
+
// Every hand-over ends the turn and is checked: the harness says what is missing, a few times at most.
|
|
180
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
// It reads the code - and runs it, to see - but changes nothing.
|
|
184
|
+
// The explorer only reads: the file tools, no shell - it scours the code, it runs nothing.
|
|
185
|
+
const fence = confinedTools({ workdir: options.buildDir, read: [options.buildDir], write: [], noBash: true });
|
|
186
|
+
const session = await createExecutor({
|
|
187
|
+
cwd: options.buildDir,
|
|
188
|
+
customTools: [submit, ...fence.tools],
|
|
189
|
+
tools: [...fence.names, "submit_exploration"],
|
|
190
|
+
model: options.model,
|
|
191
|
+
label: `${options.label} explorer`,
|
|
192
|
+
role: "explorer",
|
|
193
|
+
system: fence.note,
|
|
194
|
+
task: explorePrompt(options.buildDir, options.spec, options.scope, options.testingScripts, options.question, previous),
|
|
195
|
+
budget: { calls: EXPLORER_CALLS, finish: ["submit_exploration"] },
|
|
196
|
+
});
|
|
197
|
+
const replies = [];
|
|
198
|
+
try {
|
|
199
|
+
replies.push(await ask(session, BEGIN, options.onDelta));
|
|
200
|
+
for (let again = 0; again < EXPLORATION_SUBMISSIONS; again += 1) {
|
|
201
|
+
const problems = explorationProblems(exploration, options.scope, options.spec.moduleName, options.acceptance);
|
|
202
|
+
if (problems.length === 0 || submissions >= EXPLORATION_SUBMISSIONS)
|
|
203
|
+
break;
|
|
204
|
+
reportActivity(` the exploration is not complete yet: ${problems[0].slice(0, 120)}`);
|
|
205
|
+
replies.push(await ask(session, `What you handed over is not complete - the test author gets nothing else:\n- ${problems.join("\n- ")}\n\nCall submit_exploration with the whole of it: everything you found, not a summary.`, options.onDelta));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
session.dispose();
|
|
210
|
+
}
|
|
211
|
+
const complete = explorationProblems(exploration, options.scope, options.spec.moduleName, options.acceptance).length === 0;
|
|
212
|
+
const found = replies.filter((reply) => reply.trim() !== "");
|
|
213
|
+
if (exploration === undefined) {
|
|
214
|
+
return found.length > 0
|
|
215
|
+
? `The explorer did not hand over a complete account. What it wrote as it worked:\n\n${found.join("\n\n")}`
|
|
216
|
+
: "The explorer could not say how the software is invoked. Work it out from the test reqs and the files in the build.";
|
|
217
|
+
}
|
|
218
|
+
const account = explorationText(exploration, complete ? [] : found);
|
|
219
|
+
// Kept for the codebase's next conformance run: its explorer starts from it, and from what changed since.
|
|
220
|
+
if (complete)
|
|
221
|
+
keepExploration(options.buildDir, account);
|
|
222
|
+
return account;
|
|
223
|
+
}
|
|
224
|
+
/** Where a codebase's last complete exploration is kept: its module's scratch, beside the code - never in it. */
|
|
225
|
+
function explorationFile(buildDir) {
|
|
226
|
+
return join(modulesScratchDir(buildDir), "explorations", `${basename(dirname(resolve(buildDir)))}.json`);
|
|
227
|
+
}
|
|
228
|
+
/** Keep an account, with the code as it was when it was handed over, file by file. */
|
|
229
|
+
function keepExploration(buildDir, account) {
|
|
230
|
+
try {
|
|
231
|
+
const file = explorationFile(buildDir);
|
|
232
|
+
ensureInModulesScratch(buildDir, dirname(file));
|
|
233
|
+
writeFileSync(file, JSON.stringify({ account, code: Object.fromEntries(digestFiles(buildDir)) }));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* only a head start: without it, the next explorer starts from the code */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** The codebase's last complete exploration, and the files of the code changed since - added, changed or removed. */
|
|
240
|
+
export function previousExploration(buildDir) {
|
|
241
|
+
try {
|
|
242
|
+
const kept = JSON.parse(readFileSync(explorationFile(buildDir), "utf8"));
|
|
243
|
+
if (typeof kept.account !== "string" || kept.code === undefined)
|
|
244
|
+
return undefined;
|
|
245
|
+
const now = digestFiles(buildDir);
|
|
246
|
+
const changed = new Set();
|
|
247
|
+
for (const [file, digest] of now)
|
|
248
|
+
if (kept.code[file] !== digest)
|
|
249
|
+
changed.add(file);
|
|
250
|
+
for (const file of Object.keys(kept.code))
|
|
251
|
+
if (!now.has(file))
|
|
252
|
+
changed.add(file);
|
|
253
|
+
return { account: kept.account, changed: [...changed].sort() };
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** How many questions the test author may put to the explorer in one conformance run, and how long an answer may be. */
|
|
260
|
+
export const EXPLORER_QUESTIONS = 3;
|
|
261
|
+
const ANSWER_CALLS = 20;
|
|
262
|
+
const MAX_ANSWER_CHARS = 4_000;
|
|
263
|
+
/**
|
|
264
|
+
* The test author's question to the explorer, answered: a fresh explorer, in the code's folder - it reads the
|
|
265
|
+
* code and runs it, and changes nothing - given what was handed over so far and the question, answering in words.
|
|
266
|
+
*/
|
|
267
|
+
export async function answerAsExplorer(options) {
|
|
268
|
+
let answer;
|
|
269
|
+
const submit = defineTool({
|
|
270
|
+
name: "submit_answer",
|
|
271
|
+
label: "Submit the answer",
|
|
272
|
+
description: "Hand the answer to the test author. Call this once.",
|
|
273
|
+
parameters: Type.Object({ answer: Type.String({ description: `The answer, in words, with the exact names - under ${MAX_ANSWER_CHARS} characters` }) }),
|
|
274
|
+
async execute(_id, params) {
|
|
275
|
+
answer = params.answer;
|
|
276
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
// The explorer only reads: the file tools, no shell - it scours the code, it runs nothing.
|
|
280
|
+
const fence = confinedTools({ workdir: options.buildDir, read: [options.buildDir], write: [], noBash: true });
|
|
281
|
+
const session = await createExecutor({
|
|
282
|
+
cwd: options.buildDir,
|
|
283
|
+
customTools: [submit, ...fence.tools],
|
|
284
|
+
tools: [...fence.names, "submit_answer"],
|
|
285
|
+
model: options.model,
|
|
286
|
+
label: `${options.label} explorer`,
|
|
287
|
+
role: "explorer",
|
|
288
|
+
system: fence.note,
|
|
289
|
+
task: [
|
|
290
|
+
`You explored the built code of the \`${options.spec.moduleName}\` specification, in ${options.buildDir}, for the`,
|
|
291
|
+
"person writing its black-box conformance tests, and handed over the account below. They are writing the tests from it",
|
|
292
|
+
"now and have a question. Answer it from the code: read it - you have no shell, and run nothing. Answer only what is",
|
|
293
|
+
"asked, in words, with the exact names - routes, fields, commands, paths, versions - as",
|
|
294
|
+
`the code has them, never guessed. Under ${MAX_ANSWER_CHARS} characters. Call submit_answer once.`,
|
|
295
|
+
"",
|
|
296
|
+
`<your account>\n${options.account.trim()}\n</your account>`,
|
|
297
|
+
"",
|
|
298
|
+
`<question>\n${options.question.trim()}\n</question>`,
|
|
299
|
+
].join("\n"),
|
|
300
|
+
budget: { calls: ANSWER_CALLS, finish: ["submit_answer"] },
|
|
301
|
+
});
|
|
302
|
+
let reply = "";
|
|
303
|
+
try {
|
|
304
|
+
reply = await ask(session, BEGIN, options.onDelta);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
session.dispose();
|
|
308
|
+
}
|
|
309
|
+
const given = (answer ?? reply).trim();
|
|
310
|
+
return given === "" ? "The explorer could not answer. Work it out from the account and the code, which you can read." : given.slice(0, MAX_ANSWER_CHARS * 2);
|
|
311
|
+
}
|
|
312
|
+
/** ask_explorer, for the conformance agent: a question about the software, answered by the explorer. */
|
|
313
|
+
export function createAskExplorerTool(options) {
|
|
314
|
+
let left = options.questions ?? EXPLORER_QUESTIONS;
|
|
315
|
+
return defineTool({
|
|
316
|
+
name: "ask_explorer",
|
|
317
|
+
label: "Ask the explorer",
|
|
318
|
+
description: "Ask the explorer - who read and ran the code, and wrote the account of it you were given - something the account " +
|
|
319
|
+
"does not say: how a functionality is reached, what a field is called, what the software needs around it. It reads " +
|
|
320
|
+
`and runs the code to answer. ${options.questions ?? EXPLORER_QUESTIONS} questions a run: ask what reading the code yourself would take long to find.`,
|
|
321
|
+
promptSnippet: "ask_explorer: a question about how the software is reached, answered by the explorer from the code",
|
|
322
|
+
parameters: Type.Object({ question: Type.String({ description: "What you need to know, exactly" }) }),
|
|
323
|
+
async execute(_id, params) {
|
|
324
|
+
if (left <= 0)
|
|
325
|
+
return { content: [{ type: "text", text: "No questions left in this run: work it out from the account and the code." }], details: {} };
|
|
326
|
+
left -= 1;
|
|
327
|
+
const answer = await options.ask(params.question);
|
|
328
|
+
return { content: [{ type: "text", text: `${answer}\n\n(${left} question${left === 1 ? "" : "s"} left)` }], details: {} };
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export function explorePrompt(buildDir, spec, scope, testingScripts, question, previous) {
|
|
333
|
+
const target = scope.tested !== undefined
|
|
334
|
+
? `functionality ${refLabel(scope.tested)}`
|
|
335
|
+
: `the spec change - every functionality of \`${spec.moduleName}\` it can reach`;
|
|
336
|
+
return [
|
|
337
|
+
specPrimer({ testReqs: true }),
|
|
338
|
+
`You are exploring the built code of the \`${spec.moduleName}\` specification, in ${buildDir}, for the conformance tests of ${target}.`,
|
|
339
|
+
"",
|
|
340
|
+
"Someone else will write those tests: black-box tests that invoke the built software from the outside and",
|
|
341
|
+
"check it does what the specification says. They will not see your replies: what you hand over with",
|
|
342
|
+
"submit_exploration is what they start from. So hand over everything they need of",
|
|
343
|
+
`it - in full, but only what they need: the interface to ${target}, not the codebase. Be focused: find its`,
|
|
344
|
+
"entry point and what it needs around it; do not survey everything else. Describe the routes, commands,",
|
|
345
|
+
`fields and their types in words - no code listings, no files pasted whole, under ${MAX_EXPLORATION_CHARS} characters in all. In its parts:`,
|
|
346
|
+
"",
|
|
347
|
+
"- build_and_run: what to install and build, the exact command that starts or runs it, where it listens",
|
|
348
|
+
" and how to tell it is up - as its build file, its entry point and its configuration say;",
|
|
349
|
+
"- tests_reach_it: how the conformance tests reach it, run as the conformance testing script below runs",
|
|
350
|
+
" them. Read the script: what it builds or installs, and where, before the tests run; then say exactly what",
|
|
351
|
+
" the tests depend on or start - a package, module, entry point, path or address, with its exact name and",
|
|
352
|
+
" version - and whether they start the software themselves, and how. Copy names and versions from the",
|
|
353
|
+
" code's build file itself, and name the file: never",
|
|
354
|
+
" infer them from a folder or module name. A test that cannot reach the software tests nothing, so this is",
|
|
355
|
+
" the part that matters most;",
|
|
356
|
+
`- interfaces: for ${target} - and any other functionality its tests need to set up state - how it is`,
|
|
357
|
+
" reached from outside: the command and its arguments, or the route, method and headers, and the fields",
|
|
358
|
+
" of its request and response and their types, as the code takes them;",
|
|
359
|
+
"- environment: environment variables, configuration, services and databases it talks to, files and",
|
|
360
|
+
" fixtures - and how each test gets a fresh one;",
|
|
361
|
+
`- plan: the conformance tests to write for ${target}, from the specification and its acceptance tests:`,
|
|
362
|
+
" each case, how it is set up and invoked, what it checks as the specification says, and where the files",
|
|
363
|
+
" go as the test reqs say. Every acceptance test, named \"acceptance test N\" as the specification numbers",
|
|
364
|
+
" them, with how each of its steps is done from outside - through which endpoint, or which class and",
|
|
365
|
+
" method where the test reqs let the tests call a component directly - and what it asserts. A long one is",
|
|
366
|
+
" planned whole, never cut down;",
|
|
367
|
+
"",
|
|
368
|
+
"Describe how to call it, never what it answers: what it returns in each case is for the tests to check",
|
|
369
|
+
"against the specification, not to copy from the code - so the plan's expectations come from the",
|
|
370
|
+
"specification, and the code only tells you how to reach it. Read the code - its build files, its entry points, its",
|
|
371
|
+
"configuration - with the file tools: you have no shell, and run nothing. Change nothing. Then call submit_exploration - once, with the whole of it: it ends your turn, and",
|
|
372
|
+
`you are told if anything is missing. You can hand it over ${EXPLORATION_SUBMISSIONS} times at most.`,
|
|
373
|
+
"",
|
|
374
|
+
"<specification>",
|
|
375
|
+
spec.text.trim(),
|
|
376
|
+
"</specification>",
|
|
377
|
+
...(testingScripts !== undefined && testingScripts.length > 0
|
|
378
|
+
? [
|
|
379
|
+
"",
|
|
380
|
+
"This is the project's conformance testing script, which the harness runs to decide whether the",
|
|
381
|
+
"tests pass - read it for what it builds or installs before the tests run, and so what the tests can depend on:",
|
|
382
|
+
...testingScripts.flatMap((script) => ["", `<script path="${script.path}" run-as="${script.runs}">`, script.content.trimEnd(), "</script>"]),
|
|
383
|
+
]
|
|
384
|
+
: []),
|
|
385
|
+
...(previous !== undefined && question === undefined
|
|
386
|
+
? [
|
|
387
|
+
"",
|
|
388
|
+
"This codebase was explored before, for an earlier conformance run, and the account below was handed over then.",
|
|
389
|
+
"Start from it - do not explore again what it already says. Check what it says against the files that changed",
|
|
390
|
+
"since, listed below, and read further only for what it does not cover yet: how the functionality under test",
|
|
391
|
+
"now is reached, and the plan for its tests. Then hand over the whole account, updated - what still holds kept,",
|
|
392
|
+
"what changed corrected, what is new added.",
|
|
393
|
+
"",
|
|
394
|
+
`<the account handed over before>
|
|
395
|
+
${previous.account.trim()}
|
|
396
|
+
</the account handed over before>`,
|
|
397
|
+
"",
|
|
398
|
+
previous.changed.length > 0
|
|
399
|
+
? `<files changed since>\n${previous.changed.slice(0, 200).join("\n")}${previous.changed.length > 200 ? `\n... and ${previous.changed.length - 200} more` : ""}\n</files changed since>`
|
|
400
|
+
: "No file of the code changed since.",
|
|
401
|
+
]
|
|
402
|
+
: []),
|
|
403
|
+
...(question !== undefined
|
|
404
|
+
? [
|
|
405
|
+
"",
|
|
406
|
+
"You are asked again. The conformance tests were written from your last account, and they keep failing the",
|
|
407
|
+
"same way - most likely for something they need of the software that the account left out: how they reach",
|
|
408
|
+
"it, what to depend on or start, what it needs around it. Look at the code again for exactly that, and hand",
|
|
409
|
+
"over the whole account again, with what was missing.",
|
|
410
|
+
"",
|
|
411
|
+
`<your last account>\n${question.account.trim()}\n</your last account>`,
|
|
412
|
+
"",
|
|
413
|
+
`<the failure that keeps coming back>\n${question.failure.trimEnd()}\n</the failure that keeps coming back>`,
|
|
414
|
+
]
|
|
415
|
+
: []),
|
|
416
|
+
].join("\n");
|
|
417
|
+
}
|
|
418
|
+
/** A review's word for a problem, as one of the two: "missing" (and anything starting so), else "bad". */
|
|
419
|
+
export function reviewKind(word) {
|
|
420
|
+
return /^\s*missing/i.test(word) ? "missing" : "bad";
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The reviewer, one session for the whole conformance run: a review after the first starts from what it already
|
|
424
|
+
* read and raised. Its task is its instructions; each review is shown the diff of the tests - the whole of what the
|
|
425
|
+
* run wrote first, what changed since after - and the conformance agent's account of it.
|
|
426
|
+
*/
|
|
427
|
+
export async function createTestReviewer(options) {
|
|
428
|
+
let verdict;
|
|
429
|
+
const submit = defineTool({
|
|
430
|
+
name: "submit_review",
|
|
431
|
+
label: "Submit the review",
|
|
432
|
+
description: "Say whether the tests pass the review, and if not, every problem with them. Call this once, when you are done.",
|
|
433
|
+
parameters: Type.Object({
|
|
434
|
+
pass: Type.Boolean({ description: "True only when nothing is missing and nothing is bad" }),
|
|
435
|
+
problems: Type.Array(Type.Object({
|
|
436
|
+
file: Type.String({ description: "The test file, relative to the tests folder" }),
|
|
437
|
+
test: Type.Optional(Type.String({ description: "The test in it, when it is one test" })),
|
|
438
|
+
kind: Type.String({
|
|
439
|
+
description: '"missing" when a test the specification calls for is not there at all; "bad" when a test is there but weak, wrong or ' +
|
|
440
|
+
"not black box. Any other word is read as one of the two - one starting with \"missing\" as missing, the rest as bad",
|
|
441
|
+
}),
|
|
442
|
+
what: Type.String({ description: "Exactly what is missing or bad, and what the specification says instead" }),
|
|
443
|
+
blocking: Type.Boolean({
|
|
444
|
+
description: "True when the functionality is not yet tested black box because of it: a test or an acceptance test missing, cut down or " +
|
|
445
|
+
"skipping what it checks, the software mocked or not reached from outside, a test that cannot fail. False for a way to make " +
|
|
446
|
+
"tests that already do that better",
|
|
447
|
+
}),
|
|
448
|
+
})),
|
|
449
|
+
}),
|
|
450
|
+
async execute(_id, params) {
|
|
451
|
+
// "missing_test", "weak", "not black box": the verdict is what counts, not the exact word - one bad word never throws a review away.
|
|
452
|
+
const given = { ...params };
|
|
453
|
+
given.problems = given.problems.map((problem) => ({ ...problem, kind: reviewKind(problem.kind) }));
|
|
454
|
+
// A pass with problems, or a fail with none, is not a verdict.
|
|
455
|
+
if (given.pass === given.problems.length > 0) {
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: given.pass ? "A pass lists no problems - either drop them or fail it." : "A fail has to say what is wrong: list every problem." }],
|
|
458
|
+
details: {},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
verdict = given;
|
|
462
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
const fence = confinedTools({ workdir: options.conformanceDir, read: [options.conformanceDir, options.buildDir], write: [], noBash: true });
|
|
466
|
+
const session = await createExecutor({
|
|
467
|
+
cwd: options.conformanceDir,
|
|
468
|
+
customTools: [submit, ...fence.tools],
|
|
469
|
+
tools: [...fence.names, "submit_review"],
|
|
470
|
+
model: options.model,
|
|
471
|
+
label: `${options.label} test reviewer`, role: "test-reviewer",
|
|
472
|
+
system: fence.note,
|
|
473
|
+
task: testReviewTask(options),
|
|
474
|
+
compaction: { instructions: REVIEWER_COMPACTION },
|
|
475
|
+
budget: { calls: REVIEWER_CALLS, finish: ["submit_review"] },
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
async review(request) {
|
|
479
|
+
verdict = undefined;
|
|
480
|
+
await ask(session, request, options.onDelta);
|
|
481
|
+
if (verdict === undefined)
|
|
482
|
+
await ask(session, "You have not called submit_review. Call it now: pass, or every problem.", options.onDelta);
|
|
483
|
+
return (verdict ?? {
|
|
484
|
+
pass: false,
|
|
485
|
+
problems: [{ file: "(all)", kind: "bad", what: "The review gave no verdict. Check every test you added or changed against the rules above yourself." }],
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
dispose: () => session.dispose(),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function problemText(problem) {
|
|
492
|
+
return `${problem.blocking === true ? "BLOCKING · " : ""}${problem.kind === "missing" ? "MISSING" : "BAD"} · ${problem.file}${problem.test ? ` · ${problem.test}` : ""}: ${problem.what.trim()}`;
|
|
493
|
+
}
|
|
494
|
+
/** The reviewer's task, for its instructions: word for word through every review of the run. */
|
|
495
|
+
export function testReviewTask(options) {
|
|
496
|
+
const withAcceptance = options.functionalities.filter((item) => item.acceptanceTests.length > 0);
|
|
497
|
+
const whose = options.forChange
|
|
498
|
+
? `the functionalities of module \`${options.spec.moduleName}\` the change touches (${options.functionalities.map((item) => item.frid).join(", ") || "none"})`
|
|
499
|
+
: `functionality ${options.functionalities[0]?.frid ?? ""} of module \`${options.spec.moduleName}\``;
|
|
500
|
+
return [
|
|
501
|
+
specPrimer({ testReqs: true }),
|
|
502
|
+
`You review the conformance tests of the \`${options.spec.moduleName}\` specification a conformance run adds or changes, each time`,
|
|
503
|
+
"they pass - and the unit tests with them. Passing tests prove nothing when they assert nothing, mock the software",
|
|
504
|
+
"they test, or leave out what the specification asks for.",
|
|
505
|
+
"",
|
|
506
|
+
"You are the exacting reviewer: opinionated, thorough, and hard to satisfy. Raise every problem you find with",
|
|
507
|
+
"these tests, however small - a senior engineer weighs your findings after you, and lets go what is not worth",
|
|
508
|
+
"the time. Missing one is worse than raising one too many.",
|
|
509
|
+
"",
|
|
510
|
+
`The tests are in ${options.conformanceDir}; the code is in ${options.buildDir}. Read both; change neither.`,
|
|
511
|
+
"",
|
|
512
|
+
"You review the same tests more than once. The first time you are shown everything the run wrote in the tests",
|
|
513
|
+
"folder; each time after, what changed since your last review, and what the conformance agent says it did about",
|
|
514
|
+
"each problem you raised. The diff is the harness's, not the agent's; <the conformance agent's account> is a claim -",
|
|
515
|
+
"check it against the diff and the files. A problem it says is fixed that the tests do not fix is still a problem;",
|
|
516
|
+
"one it declined, weigh its reason - it may be right.",
|
|
517
|
+
"",
|
|
518
|
+
...(options.cases !== undefined && options.cases.mechanical ? [...conformanceReviewCasesBlock(options.cases), "", "And, of every test the run changed:"] : ["Check every test the run added or changed:"]),
|
|
519
|
+
"- Black box: it reaches the software only the way the test reqs have it - its API, its command, its public",
|
|
520
|
+
" classes where the test reqs say so - and never mocks, stubs or replaces any part of the software under test:",
|
|
521
|
+
" no mocked beans, services or clients of its own. A real external service the test reqs say not to mock is",
|
|
522
|
+
" not mocked either.",
|
|
523
|
+
"- It tests something: every test asserts what the specification says, and fails when that does not hold. No",
|
|
524
|
+
" placeholder, no `assertTrue(true)`, no empty or skipped test, no failure caught and swallowed, no assertion",
|
|
525
|
+
" so loose any answer passes.",
|
|
526
|
+
...(withAcceptance.length > 0
|
|
527
|
+
? [
|
|
528
|
+
`- The acceptance tests of ${whose}, each implemented as a test of its own, doing every step it lists, in`,
|
|
529
|
+
" order, with what it names, and asserting every expectation it states. A step left out, replaced by a",
|
|
530
|
+
" stand-in, or said to be impossible to run is missing, not done:",
|
|
531
|
+
"",
|
|
532
|
+
...withAcceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `<acceptance test ${index + 1} of functionality ${item.frid}>\n${test.trim()}\n</acceptance test>`)),
|
|
533
|
+
"",
|
|
534
|
+
]
|
|
535
|
+
: []),
|
|
536
|
+
`- The conformance tests cover what the specification says ${options.forChange ? "the change does" : `${whose} does`} - its behaviour, its`,
|
|
537
|
+
" edge cases, its errors - each checked against the specification, not against what the code happens to do.",
|
|
538
|
+
" Only what can be seen from outside: where the specification says one part calls or uses another, the test",
|
|
539
|
+
" checks the outcome that follows - what comes back, what is stored, what the other service received - never",
|
|
540
|
+
" that the call was made. Asking for a test of how the code works inside is not a problem to raise.",
|
|
541
|
+
"",
|
|
542
|
+
"The first review finds every problem, in that one review - the tests go back once with all of them. A review after",
|
|
543
|
+
"it checks each problem you raised last first - fixed, or still there - and raises a new one only when it is one of",
|
|
544
|
+
"the above - a test that cannot fail, mocks the software, or leaves out or misdoes what the specification or an",
|
|
545
|
+
"acceptance test asks for. Not a matter of style or thoroughness the last review let pass: the review is not done",
|
|
546
|
+
"again from the start.",
|
|
547
|
+
"",
|
|
548
|
+
"Each review ends with submit_review, once: pass when nothing is missing and nothing is bad; otherwise every problem",
|
|
549
|
+
'- the file, the test, its kind - "missing" for a test that is not there, "bad" for one that is weak, wrong or not',
|
|
550
|
+
"black box - and exactly what, with what the specification says instead. Be specific: the agent that fixes them gets",
|
|
551
|
+
"your list and nothing else. Mark a problem blocking when because of it the functionality is not yet tested black",
|
|
552
|
+
"box - a test or an acceptance test missing, cut down or skipping what it checks, the software mocked or not reached",
|
|
553
|
+
"from outside, a test that cannot fail. Those are worked on first; the rest - ways to make tests that already do",
|
|
554
|
+
"that better - are let go after a few reviews.",
|
|
555
|
+
"",
|
|
556
|
+
"<specification>",
|
|
557
|
+
options.spec.text.trim(),
|
|
558
|
+
"</specification>",
|
|
559
|
+
"",
|
|
560
|
+
`<how to invoke it>\n${options.invocation.trim()}\n</how to invoke it>`,
|
|
561
|
+
].join("\n");
|
|
562
|
+
}
|
|
563
|
+
/** What one review of the tests is shown: the whole of what the run wrote first; after that, what changed since. */
|
|
564
|
+
export function testReviewRequest(options) {
|
|
565
|
+
const diff = options.diff.trim();
|
|
566
|
+
const again = options.earlier !== undefined;
|
|
567
|
+
return [
|
|
568
|
+
again
|
|
569
|
+
? "The conformance suite and the unit tests pass again, after the conformance agent worked on the problems your last review raised. Review the tests again."
|
|
570
|
+
: "The conformance suite and the unit tests pass. Review the tests the run added or changed.",
|
|
571
|
+
"",
|
|
572
|
+
again ? "<what changed since your last review - a diff of the tests folder>" : "<the tests this run wrote - a diff of the tests folder from where the run started>",
|
|
573
|
+
diff !== "" ? diff : again ? "(nothing changed since your last review)" : "(no diff - read the files listed below)",
|
|
574
|
+
again ? "</what changed since your last review>" : "</the tests this run wrote>",
|
|
575
|
+
"",
|
|
576
|
+
"<tests added or changed by the run>",
|
|
577
|
+
options.changed.join("\n"),
|
|
578
|
+
"</tests added or changed by the run>",
|
|
579
|
+
"",
|
|
580
|
+
"<the conformance agent's account>",
|
|
581
|
+
options.account,
|
|
582
|
+
"</the conformance agent's account>",
|
|
583
|
+
...(again && options.earlier.length > 0
|
|
584
|
+
? ["", "The problems of your last review the conformance agent was given:", ...options.earlier.map((problem, index) => `${index + 1}. ${problemText(problem)}`)]
|
|
585
|
+
: []),
|
|
586
|
+
...(options.letGo !== undefined && options.letGo.length > 0
|
|
587
|
+
? ["", "The arbiter weighed your last review and let these go - they are settled, do not raise them again:", ...options.letGo.map((problem) => `- ${problemText(problem)}`)]
|
|
588
|
+
: []),
|
|
589
|
+
"",
|
|
590
|
+
"Then call submit_review.",
|
|
591
|
+
].join("\n");
|
|
592
|
+
}
|
|
593
|
+
/** The review's problems, for the conformance agent to fix. */
|
|
594
|
+
export function reviewFeedbackPrompt(problems) {
|
|
595
|
+
return [
|
|
596
|
+
"The conformance suite and the unit tests pass. A review of the tests found how to make them better - each, exactly:",
|
|
597
|
+
"",
|
|
598
|
+
...problems.map((problem, index) => `${index + 1}. ${problemText(problem)}`),
|
|
599
|
+
"",
|
|
600
|
+
"Fix every one: implement what is missing, and make what is bad test the software black box and as the",
|
|
601
|
+
"specification says. Then run the suite. One you are sure the review is wrong about, decline, and say why.",
|
|
602
|
+
...SUBMIT_AFTER_REVIEW,
|
|
603
|
+
...REVIEW_LESSON,
|
|
604
|
+
].join("\n");
|
|
605
|
+
}
|
|
606
|
+
/** "billing 4": a functionality as the judge and the person read it. */
|
|
607
|
+
export function refLabel(ref) {
|
|
608
|
+
return `${ref.module} ${ref.frid}`;
|
|
609
|
+
}
|
|
610
|
+
const same = (a, b) => a.module === b.module && a.frid === b.frid;
|
|
611
|
+
/** A conflict's functionalities as the judge gave them, tidied: trimmed, and each once. */
|
|
612
|
+
export function conflictRefs(given) {
|
|
613
|
+
const refs = [];
|
|
614
|
+
for (const item of given ?? []) {
|
|
615
|
+
const ref = { module: (item.module ?? "").trim(), frid: (item.frid ?? item.id ?? "").trim() };
|
|
616
|
+
if (ref.module !== "" && ref.frid !== "" && !refs.some((known) => same(known, ref)))
|
|
617
|
+
refs.push(ref);
|
|
618
|
+
}
|
|
619
|
+
return refs;
|
|
620
|
+
}
|
|
621
|
+
/** Why a spec conflict cannot be raised as given, or nothing when it can. */
|
|
622
|
+
export function specConflictProblem(functionalities, scope) {
|
|
623
|
+
const refs = conflictRefs(functionalities);
|
|
624
|
+
if (refs.length !== 2) {
|
|
625
|
+
return `A spec conflict names exactly two functionalities, each by its module and id - it named ${refs.length === 0 ? "none" : refs.map(refLabel).join(", ")}.`;
|
|
626
|
+
}
|
|
627
|
+
const unknown = refs.filter((ref) => !scope.functionalities.some((known) => same(known, ref)));
|
|
628
|
+
if (unknown.length > 0) {
|
|
629
|
+
return `There is no functionality ${unknown.map(refLabel).join(" or ")}: the functionalities are ${scope.functionalities.map(refLabel).join(", ")}.`;
|
|
630
|
+
}
|
|
631
|
+
if (scope.tested !== undefined && !refs.some((ref) => same(ref, scope.tested))) {
|
|
632
|
+
return (`Only a conflict with functionality ${refLabel(scope.tested)}, the one under test, can be raised now. One between ` +
|
|
633
|
+
`${refs.map(refLabel).join(" and ")} would have come up when the later of them was tested.`);
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
/** The two functionalities of a spec conflict, in a fixed order: the same conflict, however it was named. */
|
|
638
|
+
export function conflictKey(functionalities) {
|
|
639
|
+
return sortedRefs(functionalities).map(refLabel).join(" & ");
|
|
640
|
+
}
|
|
641
|
+
export function sortedRefs(functionalities) {
|
|
642
|
+
return conflictRefs(functionalities).sort((a, b) => a.module.localeCompare(b.module) || a.frid.localeCompare(b.frid, undefined, { numeric: true }));
|
|
643
|
+
}
|
|
644
|
+
/** How many times a spec conflict between the same two functionalities is raised before it counts. */
|
|
645
|
+
export const SPEC_CONFLICT_RAISES = 3;
|
|
646
|
+
/**
|
|
647
|
+
* How many times, one after the other, the run's environment is judged what is wrong before it counts: once more
|
|
648
|
+
* than once, so a second judge of a new run - the tests checked in between - has to agree.
|
|
649
|
+
*/
|
|
650
|
+
export const ENVIRONMENT_RAISES = 2;
|
|
651
|
+
/** Which is wrong when the conformance suite fails - the code, the tests, or the spec itself - read against the spec. */
|
|
652
|
+
/** Text as it is compared for a quote: lower case, words only. */
|
|
653
|
+
function words(text) {
|
|
654
|
+
return ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Why a verdict that the code is wrong cannot be taken as it is, or nothing when it can. The code is
|
|
658
|
+
* wrong only against what the specification says - so the judge quotes it, and the quote has to be
|
|
659
|
+
* in the specification - or when it hides why the software failed. A test asserting what the
|
|
660
|
+
* specification leaves open is the test's mistake, however plausible the assertion.
|
|
661
|
+
*/
|
|
662
|
+
export function unfoundedCodeVerdict(verdict, specText) {
|
|
663
|
+
if (verdict.causeHidden === true)
|
|
664
|
+
return undefined;
|
|
665
|
+
const quote = verdict.specSays?.trim() ?? "";
|
|
666
|
+
if (quote === "") {
|
|
667
|
+
return ("Your verdict blames the code, but quotes no words of the specification it does not meet. Call submit_verdict " +
|
|
668
|
+
"again: for code, put the specification's own words the code does not meet in spec_says, copied exactly from " +
|
|
669
|
+
"<specification>; or set cause_hidden when the output does not show why the software failed. If the specification " +
|
|
670
|
+
"does not say what the failing test asserts, the test is what is wrong: culprit tests.");
|
|
671
|
+
}
|
|
672
|
+
const quoted = words(quote);
|
|
673
|
+
if (quoted.trim().split(" ").length < 4 || !words(specText).includes(quoted)) {
|
|
674
|
+
return (`Your verdict blames the code, but "${quote}" is not in the specification. Call submit_verdict again: for code, ` +
|
|
675
|
+
"copy into spec_says the specification's own words the code does not meet, exactly as they are in <specification>. " +
|
|
676
|
+
"If no words of it say what the failing test asserts, the test is what is wrong: culprit tests.");
|
|
677
|
+
}
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
export async function judgeFailure(options) {
|
|
681
|
+
let verdict;
|
|
682
|
+
const submit = defineTool({
|
|
683
|
+
name: "submit_verdict",
|
|
684
|
+
label: "Submit verdict",
|
|
685
|
+
description: "Say what is wrong - the code, the tests, the specification itself, or the run's environment - and why. Call this exactly once.",
|
|
686
|
+
parameters: Type.Object({
|
|
687
|
+
culprit: Type.Union([Type.Literal("code"), Type.Literal("tests"), Type.Literal("spec"), Type.Literal("environment")], {
|
|
688
|
+
description: "What has to change to meet the specification - or spec, when the specification cannot be met at all, or environment, when only the person can fix what the run is given",
|
|
689
|
+
}),
|
|
690
|
+
reason: Type.String({ description: "Why, against the specification: which test, which behaviour, what the spec says" }),
|
|
691
|
+
spec_says: Type.Optional(Type.String({
|
|
692
|
+
description: "For code: the specification's own words the code does not meet, copied exactly from <specification> - the sentence, or the part of it that says what the test checks",
|
|
693
|
+
})),
|
|
694
|
+
cause_hidden: Type.Optional(Type.Boolean({ description: "For code: true when it is wrong because the output does not show why the software failed - it hides the cause" })),
|
|
695
|
+
functionalities: Type.Optional(Type.Array(Type.Object({
|
|
696
|
+
module: Type.String({ description: "The module the functionality is in" }),
|
|
697
|
+
id: Type.String({ description: "Its id in that module" }),
|
|
698
|
+
}), { description: 'For spec: the two functionalities that cannot both hold, e.g. [{"module": "billing", "id": "4"}, {"module": "accounts", "id": "1"}]' })),
|
|
699
|
+
}),
|
|
700
|
+
async execute(_id, params) {
|
|
701
|
+
const given = params;
|
|
702
|
+
verdict = {
|
|
703
|
+
culprit: given.culprit,
|
|
704
|
+
reason: given.reason,
|
|
705
|
+
...(given.spec_says?.trim() ? { specSays: given.spec_says.trim() } : {}),
|
|
706
|
+
...(given.cause_hidden === true ? { causeHidden: true } : {}),
|
|
707
|
+
...(given.functionalities ? { functionalities: conflictRefs(given.functionalities) } : {}),
|
|
708
|
+
};
|
|
709
|
+
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
// It reads both, and changes neither: the file tools and the web - to look up what a service it calls answers -
|
|
713
|
+
// and no shell. It judges from the code, the tests, the spec and the failing run's output, which it is given.
|
|
714
|
+
const fence = confinedTools({ workdir: options.conformanceDir, read: [options.buildDir, options.conformanceDir], write: [], noBash: true });
|
|
715
|
+
const before = { code: digestFiles(options.buildDir), tests: digestFiles(options.conformanceDir) };
|
|
716
|
+
const session = await createExecutor({
|
|
717
|
+
cwd: options.conformanceDir,
|
|
718
|
+
customTools: [submit, ...createWebTools(), ...fence.tools],
|
|
719
|
+
tools: [...fence.names, ...WEB_TOOLS, "submit_verdict"],
|
|
720
|
+
model: options.model,
|
|
721
|
+
label: `${options.label} judge`, role: "judge",
|
|
722
|
+
system: fence.note,
|
|
723
|
+
task: judgePrompt(options),
|
|
724
|
+
budget: { calls: JUDGE_CALLS, finish: ["submit_verdict"] },
|
|
725
|
+
});
|
|
726
|
+
try {
|
|
727
|
+
await ask(session, BEGIN, options.onDelta);
|
|
728
|
+
for (let again = 0; again < 2; again += 1) {
|
|
729
|
+
if (verdict === undefined) {
|
|
730
|
+
await ask(session, "You have not called submit_verdict. Call it now: code, tests, spec or environment, and why.", options.onDelta);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (verdict.culprit === "code") {
|
|
734
|
+
const unfounded = unfoundedCodeVerdict(verdict, options.spec.text);
|
|
735
|
+
if (unfounded === undefined)
|
|
736
|
+
break;
|
|
737
|
+
verdict = undefined;
|
|
738
|
+
await ask(session, unfounded, options.onDelta);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (verdict.culprit !== "spec")
|
|
742
|
+
break;
|
|
743
|
+
const problem = specConflictProblem(verdict.functionalities, options.scope);
|
|
744
|
+
if (problem === undefined)
|
|
745
|
+
break;
|
|
746
|
+
verdict = undefined;
|
|
747
|
+
await ask(session, `That spec conflict cannot be raised: ${problem} Call submit_verdict again - code, tests, or a spec conflict that can be.`, options.onDelta);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
session.dispose();
|
|
752
|
+
}
|
|
753
|
+
// A judge changes nothing: should anything have changed anyway, it is said, for whoever looks at the run.
|
|
754
|
+
for (const [what, dir, digest] of [["code", options.buildDir, before.code], ["tests", options.conformanceDir, before.tests]]) {
|
|
755
|
+
const changed = diffFiles(digest, digestFiles(dir));
|
|
756
|
+
const files = [...changed.added, ...changed.changed, ...changed.removed];
|
|
757
|
+
if (files.length > 0)
|
|
758
|
+
reportActivity(` the judge changed the ${what}, which it must not: ${files.slice(0, 5).join(", ")}`);
|
|
759
|
+
}
|
|
760
|
+
if (verdict?.culprit === "spec" && specConflictProblem(verdict.functionalities, options.scope) !== undefined)
|
|
761
|
+
verdict = undefined;
|
|
762
|
+
if (verdict?.culprit === "code" && unfoundedCodeVerdict(verdict, options.spec.text) !== undefined) {
|
|
763
|
+
// Blaming the code takes the specification's own words; without them, the test asserts what the spec does not say.
|
|
764
|
+
return { culprit: "tests", reason: `${verdict.reason}\n\nBut no words of the specification say so: a test that asserts what the specification does not say is what is wrong. Check each failing assertion against the specification, and assert only what it says.` };
|
|
765
|
+
}
|
|
766
|
+
if (verdict?.culprit === "code" && verdict.specSays !== undefined) {
|
|
767
|
+
verdict = { ...verdict, reason: `${verdict.reason}\n\nThe specification says: "${verdict.specSays}"` };
|
|
768
|
+
}
|
|
769
|
+
// No verdict is no evidence the code is wrong: the tests are the newer work, and looked at first.
|
|
770
|
+
return verdict ?? { culprit: "tests", reason: "The judge gave no verdict that could be used; check the failing tests against the specification first." };
|
|
771
|
+
}
|
|
772
|
+
export function judgePrompt(options) {
|
|
773
|
+
const tested = options.scope.tested;
|
|
774
|
+
return [
|
|
775
|
+
specPrimer({ testReqs: true }),
|
|
776
|
+
`The conformance tests of the \`${options.spec.moduleName}\` specification failed. Decide what is wrong: the code, the tests, the specification itself, or the run's environment.`,
|
|
777
|
+
"",
|
|
778
|
+
`The code is in ${options.buildDir}; the conformance tests are in ${options.conformanceDir}. Read both.`,
|
|
779
|
+
"",
|
|
780
|
+
"The specification is the judge, never either of them:",
|
|
781
|
+
"- tests: a test asserts something the specification does not say, misreads it, or reaches the software the",
|
|
782
|
+
" wrong way - a wrong command, route or setup (see <how to invoke it>).",
|
|
783
|
+
"- code: the code does not do what the specification says, and the test checks exactly that.",
|
|
784
|
+
" Only what the specification says counts: anything it leaves open - a name, coordinates, a version, a path,",
|
|
785
|
+
" a package, a port, a file layout - is what the code already has. The tests follow it, and it is never",
|
|
786
|
+
" code - changing the code to fit a test's guess moves what every other test points at.",
|
|
787
|
+
"- spec: two functionalities of the specification cannot both hold - whatever the code does, one of them fails.",
|
|
788
|
+
" Only when the code and the tests both follow the specification, and the specification itself cannot be met.",
|
|
789
|
+
" They can be in different modules: this one's, and those of the modules it requires - each is listed with its",
|
|
790
|
+
" module. Name the two in `functionalities`, each by its module and id" +
|
|
791
|
+
(tested !== undefined
|
|
792
|
+
? `. One of them must be ${refLabel(tested)}, the one under test: a conflict between two earlier ones would`
|
|
793
|
+
: "."),
|
|
794
|
+
...(tested !== undefined ? [" have come up when the later of them was tested."] : []),
|
|
795
|
+
` A spec conflict counts only once it has been raised ${SPEC_CONFLICT_RAISES} times, for the same two functionalities;`,
|
|
796
|
+
" until then it is recorded, and the tests are checked against the specification again.",
|
|
797
|
+
"- environment: neither the code nor the tests can fix it - only the person can, in what the run is given: a",
|
|
798
|
+
" credential the run reads is missing, empty or rejected (a 401 or 403 from a provider for a token the tests did not",
|
|
799
|
+
" make up), an account, plan or quota a service refuses, a service the specification names that cannot be reached",
|
|
800
|
+
" at all. Say what the person has to provide, and where the run read it from - the file, the variable. Not for what",
|
|
801
|
+
" the tests set up themselves - a credential, a variable or a fixture a test sets, overrides or names wrong: that is",
|
|
802
|
+
" tests; nor for a toolchain or package missing where the tests run: the agents install those. The run stops on it",
|
|
803
|
+
` once it has been judged so ${ENVIRONMENT_RAISES} times in a row; until then the tests are checked for whether they cause it.`,
|
|
804
|
+
"",
|
|
805
|
+
"When the output does not show why the software failed - it answered with an error that names no cause, and",
|
|
806
|
+
"logged nothing about it - the code is what is wrong first: it hides the cause, and nobody can fix what they",
|
|
807
|
+
"cannot see. Judge code with cause_hidden, say that the cause has to be made to show, and name where the error is caught.",
|
|
808
|
+
"",
|
|
809
|
+
"To blame the code, quote in spec_says the specification's own words it does not meet, copied exactly - a",
|
|
810
|
+
"verdict of code without them is not taken. What the specification leaves open is never the code's fault: a test",
|
|
811
|
+
"that asserts it - a status code, a message, a format the specification does not name - is what is wrong.",
|
|
812
|
+
"",
|
|
813
|
+
`You have ${JUDGE_CALLS} tool calls to decide in: read what the failure points at, not the whole codebase.`,
|
|
814
|
+
"",
|
|
815
|
+
"When the code and the tests could both be what is wrong, choose tests. When both are wrong, say which to fix",
|
|
816
|
+
"first: the one that makes the failure you see. Then call submit_verdict once, with which and why - name the",
|
|
817
|
+
"failing test, what it expected, what the specification says, and what the code does.",
|
|
818
|
+
"",
|
|
819
|
+
...(options.notes !== ""
|
|
820
|
+
? [
|
|
821
|
+
options.notes,
|
|
822
|
+
"",
|
|
823
|
+
"Earlier attempts at this failure are in the pitfalls, each stamped `conformance attempt <n>`: what was judged",
|
|
824
|
+
"wrong, and what was done about it - which did not make the suite pass. Do not send it the way a failed",
|
|
825
|
+
"attempt did unless you have a new reason; look at the other side.",
|
|
826
|
+
"",
|
|
827
|
+
]
|
|
828
|
+
: []),
|
|
829
|
+
"<specification>",
|
|
830
|
+
options.spec.text.trim(),
|
|
831
|
+
"</specification>",
|
|
832
|
+
"",
|
|
833
|
+
`<how to invoke it>\n${options.invocation.trim()}\n</how to invoke it>`,
|
|
834
|
+
"",
|
|
835
|
+
`<failing run exit="${options.run.exitCode}" command="${options.run.command}">`,
|
|
836
|
+
options.run.output.trimEnd(),
|
|
837
|
+
"</failing run>",
|
|
838
|
+
].join("\n");
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* The coding agent a conformance run sends the code to, when the judge says the code is wrong: it
|
|
842
|
+
* works in the code folder, may read the tests but never changes them, and keeps the unit
|
|
843
|
+
* tests passing.
|
|
844
|
+
* Made the first time it is needed, and kept for the rest of the run: each fix after the first starts from what the
|
|
845
|
+
* ones before it tried. Its task is its instructions; its work is compacted as it gets longer.
|
|
846
|
+
*/
|
|
847
|
+
export async function createCodeFixer(options) {
|
|
848
|
+
const fence = confinedTools({
|
|
849
|
+
workdir: options.buildDir,
|
|
850
|
+
read: [options.buildDir, ...(options.conformanceDir !== undefined ? [options.conformanceDir] : [])],
|
|
851
|
+
write: [options.buildDir],
|
|
852
|
+
reportsEnvironment: true,
|
|
853
|
+
});
|
|
854
|
+
return createExecutor({
|
|
855
|
+
cwd: options.buildDir,
|
|
856
|
+
customTools: [...options.tools, ...createWebTools(), ...fence.tools],
|
|
857
|
+
tools: [...fence.names, ...options.toolNames, ...WEB_TOOLS],
|
|
858
|
+
model: options.model,
|
|
859
|
+
label: `${options.label} coding agent`, role: "coding",
|
|
860
|
+
system: `${fence.note}\n\n${workspaceBlock({ dir: options.buildDir, hasMyDiff: options.toolNames.includes("my_diff") })}`,
|
|
861
|
+
task: codeFixerTask(options),
|
|
862
|
+
compaction: { instructions: CODER_COMPACTION },
|
|
863
|
+
...(options.checkEnvironment !== undefined ? { checkEnvironment: options.checkEnvironment } : {}),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The code fixer's task, for its instructions: whose code it is, the specification, and how a fix is made. It stays
|
|
868
|
+
* word for word through every fix of the run; each failure comes as a message (`codeFixPrompt`).
|
|
869
|
+
*/
|
|
870
|
+
export function codeFixerTask(options) {
|
|
871
|
+
return [
|
|
872
|
+
specPrimer({ testReqs: false }),
|
|
873
|
+
`You are the coding agent of the \`${options.spec.moduleName}\` specification. Its code is in ${options.buildDir}. When its`,
|
|
874
|
+
"conformance tests fail and the code is what is wrong, you are sent the failure, and fix the code.",
|
|
875
|
+
"",
|
|
876
|
+
"<specification>",
|
|
877
|
+
options.spec.text.trim(),
|
|
878
|
+
"</specification>",
|
|
879
|
+
"",
|
|
880
|
+
...fixMethod({ what: "conformance tests", fixes: "code", runTool: "run_conformance_tests" }),
|
|
881
|
+
"",
|
|
882
|
+
"run_conformance_tests runs the project's conformance testing script on the code as it now is, with the whole",
|
|
883
|
+
"suite, and you get its output. Make the failure show in your own unit tests too where you can - for software that",
|
|
884
|
+
"will not start, a unit test that starts it - so run_unit_tests catches it from now on.",
|
|
885
|
+
"",
|
|
886
|
+
"If the output does not show why the software failed, make it show first (the rule on errors below), run",
|
|
887
|
+
"run_conformance_tests to read the cause, then fix that. Keep the logging.",
|
|
888
|
+
"",
|
|
889
|
+
...(options.conformanceDir !== undefined
|
|
890
|
+
? [
|
|
891
|
+
`You have read access to the conformance tests, in ${options.conformanceDir}, with your file tools - read, grep,`,
|
|
892
|
+
"find, ls - but not write access: read them to see what a failing test does and expects; never change them.",
|
|
893
|
+
"",
|
|
894
|
+
]
|
|
895
|
+
: []),
|
|
896
|
+
"Fix the code so it does what the specification says. The tests are not yours: you never change them, and",
|
|
897
|
+
"they are not the judge - the specification is. Keep the unit tests passing: call run_unit_tests before",
|
|
898
|
+
"you finish.",
|
|
899
|
+
"Keep the build's identity as it is - its name, coordinates, version, package and main class - unless the",
|
|
900
|
+
"specification asks for another: the tests and other builds point at them. Leave in your notes what holds about",
|
|
901
|
+
"the code for the next agent, not an account of this fix: the harness records the attempts itself.",
|
|
902
|
+
...INTEGRITY_RULES,
|
|
903
|
+
...ERROR_CAUSE_RULES,
|
|
904
|
+
].join("\n");
|
|
905
|
+
}
|
|
906
|
+
/** One failure for the code fixer: the run, and why the code is what is wrong. */
|
|
907
|
+
export function codeFixPrompt(options) {
|
|
908
|
+
return [
|
|
909
|
+
`The conformance tests failed, and the code is what is wrong (exit ${options.run.exitCode}):`,
|
|
910
|
+
"",
|
|
911
|
+
`<why the code>\n${options.reason.trim()}\n</why the code>`,
|
|
912
|
+
"",
|
|
913
|
+
`<failing run command="${options.run.command}">`,
|
|
914
|
+
options.run.output.trimEnd(),
|
|
915
|
+
"</failing run>",
|
|
916
|
+
"",
|
|
917
|
+
"Fix the code, run the conformance tests and the unit tests, and finish.",
|
|
918
|
+
].join("\n");
|
|
919
|
+
}
|
|
920
|
+
/** The run's environment judged what is wrong: the conformance agent checks the tests do not cause it, and changes nothing when they do not. */
|
|
921
|
+
export function environmentCheckPrompt(options) {
|
|
922
|
+
return [
|
|
923
|
+
`The harness ran the conformance testing script, \`${options.run.command}\`, on the whole suite, and it failed (exit ${options.run.exitCode}).`,
|
|
924
|
+
"It was checked against the specification, and what is wrong is judged to be the run's environment - something only",
|
|
925
|
+
"the person can fix, such as a credential the run is given:",
|
|
926
|
+
"",
|
|
927
|
+
`<why the environment>\n${options.reason.trim()}\n</why the environment>`,
|
|
928
|
+
"",
|
|
929
|
+
"<failing run>",
|
|
930
|
+
options.run.output.trimEnd().slice(-6000),
|
|
931
|
+
"</failing run>",
|
|
932
|
+
"",
|
|
933
|
+
"Before that is taken as the answer, check that the tests do not cause it themselves: a credential, an environment",
|
|
934
|
+
"variable or a fixture a test sets, overrides, leaves behind for the next test, or reads under the wrong name. If",
|
|
935
|
+
"they do, fix the tests and run them. If they do not, change nothing - never make up a credential, write a .env",
|
|
936
|
+
"file, or weaken a test so it passes without what it needs - and say so in submit_work.",
|
|
937
|
+
].join("\n");
|
|
938
|
+
}
|
|
939
|
+
export function testsFixPrompt(options) {
|
|
940
|
+
return [
|
|
941
|
+
`The harness ran the conformance testing script, \`${options.run.command}\`, on the whole suite, and it failed (exit ${options.run.exitCode}).`,
|
|
942
|
+
"It was checked against the specification, and a test is what is wrong:",
|
|
943
|
+
"",
|
|
944
|
+
`<why the tests>\n${options.reason.trim()}\n</why the tests>`,
|
|
945
|
+
"",
|
|
946
|
+
"<failing run>",
|
|
947
|
+
options.run.output.trimEnd(),
|
|
948
|
+
"</failing run>",
|
|
949
|
+
"",
|
|
950
|
+
...fixMethod({ what: "conformance tests", fixes: "tests", runTool: "run_conformance_tests" }),
|
|
951
|
+
"",
|
|
952
|
+
"Correct the test to what the specification says. Never change a test so it agrees with code that",
|
|
953
|
+
"contradicts the specification.",
|
|
954
|
+
].join("\n");
|
|
955
|
+
}
|