@turri-dev/cli 0.1.27-next.763d091 → 0.1.28-next.d5f2680

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +71 -15
  86. package/dist/regenerate/change-view.d.ts +8 -3
  87. package/dist/regenerate/change-view.js +71 -22
  88. package/dist/regenerate/chunk-plan.d.ts +36 -15
  89. package/dist/regenerate/chunk-plan.js +165 -59
  90. package/dist/regenerate/conform.d.ts +3 -0
  91. package/dist/regenerate/conform.js +6 -5
  92. package/dist/regenerate/conformance-agents.d.ts +18 -39
  93. package/dist/regenerate/conformance-agents.js +60 -88
  94. package/dist/regenerate/conformance.d.ts +23 -20
  95. package/dist/regenerate/conformance.js +118 -121
  96. package/dist/regenerate/diff-view.d.ts +1 -1
  97. package/dist/regenerate/diff-view.js +20 -4
  98. package/dist/regenerate/drift.d.ts +47 -15
  99. package/dist/regenerate/drift.js +101 -30
  100. package/dist/regenerate/jobs.d.ts +2 -2
  101. package/dist/regenerate/jobs.js +1 -1
  102. package/dist/regenerate/ledger.d.ts +19 -0
  103. package/dist/regenerate/loop.d.ts +61 -49
  104. package/dist/regenerate/loop.js +473 -340
  105. package/dist/regenerate/paths.d.ts +4 -0
  106. package/dist/regenerate/paths.js +7 -1
  107. package/dist/regenerate/plain-palette.js +2 -0
  108. package/dist/regenerate/prompt.d.ts +57 -19
  109. package/dist/regenerate/prompt.js +157 -122
  110. package/dist/regenerate/records.d.ts +43 -12
  111. package/dist/regenerate/records.js +102 -88
  112. package/dist/regenerate/regenerate.js +29 -11
  113. package/dist/regenerate/regression.d.ts +4 -4
  114. package/dist/regenerate/regression.js +14 -10
  115. package/dist/regenerate/renames.d.ts +42 -0
  116. package/dist/regenerate/renames.js +185 -0
  117. package/dist/regenerate/requires.d.ts +1 -1
  118. package/dist/regenerate/requires.js +10 -3
  119. package/dist/regenerate/resources.d.ts +5 -3
  120. package/dist/regenerate/resources.js +4 -4
  121. package/dist/regenerate/scripts-being-written.d.ts +19 -0
  122. package/dist/regenerate/scripts-being-written.js +30 -0
  123. package/dist/regenerate/spec-map-view.d.ts +2 -2
  124. package/dist/regenerate/spec-map-view.js +46 -15
  125. package/dist/regenerate/spec-map.d.ts +27 -5
  126. package/dist/regenerate/spec-map.js +92 -60
  127. package/dist/regenerate/spec-steps.d.ts +48 -5
  128. package/dist/regenerate/spec-steps.js +213 -28
  129. package/dist/regenerate/state.d.ts +4 -3
  130. package/dist/regenerate/stop-report.d.ts +1 -1
  131. package/dist/regenerate/stop-report.js +10 -10
  132. package/dist/regenerate/test-advisor.d.ts +101 -0
  133. package/dist/regenerate/test-advisor.js +379 -0
  134. package/dist/regenerate/test-kinds.d.ts +69 -31
  135. package/dist/regenerate/test-kinds.js +109 -80
  136. package/dist/regenerate/test-scripts.d.ts +2 -0
  137. package/dist/regenerate/test-scripts.js +4 -0
  138. package/dist/regenerate/testing-steps.d.ts +41 -0
  139. package/dist/regenerate/testing-steps.js +110 -0
  140. package/dist/regenerate/testing.d.ts +1 -1
  141. package/dist/regenerate/testing.js +5 -4
  142. package/dist/regenerate/unit-review.d.ts +1 -1
  143. package/dist/regenerate/unit-review.js +3 -3
  144. package/dist/regenerate-cli.js +11 -12
  145. package/dist/release-agent.js +4 -0
  146. package/dist/release-cli.js +2 -0
  147. package/dist/release-tools.js +2 -0
  148. package/dist/run-report.d.ts +2 -2
  149. package/dist/run-report.js +4 -3
  150. package/dist/self-invocation.d.ts +1 -1
  151. package/dist/self-invocation.js +1 -1
  152. package/dist/sentinel.js +1 -1
  153. package/dist/setup.d.ts +10 -0
  154. package/dist/setup.js +52 -4
  155. package/dist/skill.d.ts +10 -0
  156. package/dist/skill.js +6 -0
  157. package/dist/skills.js +3 -1
  158. package/dist/spec-tools.js +21 -31
  159. package/dist/test-plain-files.d.ts +41 -0
  160. package/dist/test-plain-files.js +177 -0
  161. package/docs/README.md +3 -1
  162. package/docs/authoring-plain.md +46 -13
  163. package/docs/chats.md +3 -1
  164. package/docs/config.md +20 -15
  165. package/docs/deploy.md +1 -1
  166. package/docs/development.md +2 -1
  167. package/docs/headless.md +15 -18
  168. package/docs/hosting.md +28 -12
  169. package/docs/installation.md +1 -0
  170. package/docs/parser.md +88 -27
  171. package/docs/regenerating.md +89 -54
  172. package/docs/sentinel.md +1 -1
  173. package/docs/skills-and-gates.md +3 -1
  174. package/docs/test-advisor.md +110 -0
  175. package/docs/theming.md +1 -1
  176. package/docs/using.md +17 -15
  177. package/genome/rules/bullet-continuation.md +8 -8
  178. package/genome/rules/concise-specs-examples.md +111 -80
  179. package/genome/rules/concise-specs-integration-examples.md +56 -41
  180. package/genome/rules/concise-specs.md +17 -16
  181. package/genome/rules/definitions.md +5 -5
  182. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
  183. package/genome/rules/func-specs.md +69 -74
  184. package/genome/rules/impl-reqs.md +4 -4
  185. package/genome/rules/integration-embedded-testing.md +20 -20
  186. package/genome/rules/integration-embedded.md +31 -31
  187. package/genome/rules/integration-standalone.md +9 -9
  188. package/genome/rules/integrations.md +18 -18
  189. package/genome/rules/linked-resources.md +19 -16
  190. package/genome/rules/module-structure.md +21 -14
  191. package/genome/rules/project-model.md +18 -17
  192. package/genome/rules/required-concepts.md +1 -1
  193. package/genome/rules/requires-modules.md +6 -5
  194. package/genome/rules/semantic-ids.md +49 -0
  195. package/genome/rules/stdlib.md +4 -5
  196. package/genome/rules/test-plain-files.md +130 -0
  197. package/genome/rules/test-reqs.md +8 -8
  198. package/genome/rules/turri-regeneration.md +24 -8
  199. package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
  200. package/genome/skills/analyze-func-specs/SKILL.md +46 -32
  201. package/genome/skills/break-down-func-spec/SKILL.md +91 -67
  202. package/genome/skills/consolidate-concepts/SKILL.md +10 -10
  203. package/genome/skills/convert-old-syntax/SKILL.md +68 -0
  204. package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
  205. package/genome/skills/debug-specs/SKILL.md +62 -56
  206. package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
  207. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
  208. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
  209. package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
  210. package/genome/skills/refactor-module/SKILL.md +35 -35
  211. package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
  212. package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
  213. package/package.json +7 -6
  214. package/dist/regenerate/choose-conformance.d.ts +0 -32
  215. package/dist/regenerate/choose-conformance.js +0 -117
  216. /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
@@ -0,0 +1,379 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { ask, BEGIN, createExecutor } from "../pi.js";
4
+ /** How many times a call that did not answer, or answered what does not hold, is asked again. */
5
+ const RETRIES = 2;
6
+ /** The fewest and most probes a functional spec is given. */
7
+ export const MIN_PROBES = 1;
8
+ export const MAX_PROBES = 10;
9
+ /** What a reader answers for an input the functional spec does not settle: a scenario on it would pin a guess. */
10
+ export const UNSPECIFIED = "unspecified";
11
+ /** The readers: the same task, decorrelated by their system prompts alone. */
12
+ export const READERS = [
13
+ {
14
+ name: "literal reader",
15
+ system: "You read a functional spec literally: you predict exactly what its words say, and nothing they do not. Where the words are silent, you take the narrowest reading they allow.",
16
+ },
17
+ {
18
+ name: "edge-case hunter",
19
+ system: "You read a functional spec as a tester hunting for edge cases: empty and huge values, limits, duplicates, order, rounding, time zones, missing data. You predict what a careful implementation would do at each edge.",
20
+ },
21
+ {
22
+ name: "another domain's reader",
23
+ system: "You read a functional spec as someone from another domain would - a different field, a different country, a different habit of mind - bringing the conventions that domain takes for granted. You predict what you would expect the output to be.",
24
+ },
25
+ ];
26
+ /** Ask one call, and ask again with what was wrong, until it holds or the retries are spent. */
27
+ async function askAdvisor(options) {
28
+ let answer;
29
+ const tool = options.tool((given) => (answer = given));
30
+ const session = await createExecutor({
31
+ cwd: options.cwd,
32
+ customTools: [tool],
33
+ tools: [tool.name],
34
+ model: options.model,
35
+ label: options.label,
36
+ role: "advisor",
37
+ task: options.task,
38
+ ...(options.system !== undefined ? { system: options.system } : {}),
39
+ budget: { calls: 4, finish: [tool.name] },
40
+ });
41
+ try {
42
+ await ask(session, BEGIN);
43
+ for (let again = 0; again < RETRIES; again += 1) {
44
+ if (answer !== undefined && options.check(answer).length === 0)
45
+ return answer;
46
+ const problems = answer === undefined ? [`You did not call ${tool.name}.`] : options.check(answer);
47
+ answer = undefined;
48
+ await ask(session, `That does not hold:\n- ${problems.join("\n- ")}\n\nCall ${tool.name} again, with it fixed.`);
49
+ }
50
+ return answer !== undefined && options.check(answer).length === 0 ? answer : undefined;
51
+ }
52
+ finally {
53
+ session.dispose();
54
+ }
55
+ }
56
+ /** Text compared as the readers' outputs are: whitespace collapsed, case left aside. */
57
+ export function normalised(text) {
58
+ return text.trim().replace(/\s+/g, " ").toLowerCase();
59
+ }
60
+ /** What every call is given: the functional spec, its module, and the project's own words on what matters. */
61
+ function subjectBlock(subject) {
62
+ return [
63
+ `<functional_spec module="${subject.module}" func_id="${subject.funcId}">`,
64
+ subject.text.trim(),
65
+ "</functional_spec>",
66
+ "",
67
+ `<module name="${subject.module}">`,
68
+ subject.moduleText.trim(),
69
+ "</module>",
70
+ ...(subject.testedIn !== undefined
71
+ ? [
72
+ "",
73
+ `Its scenarios are tested against the code of ${subject.testedIn.module}, which is built on ${subject.module}: what a command`,
74
+ `or a listing shows there includes ${subject.testedIn.module}'s own functional specs.`,
75
+ `<module name="${subject.testedIn.module}">`,
76
+ subject.testedIn.text.trim(),
77
+ "</module>",
78
+ ]
79
+ : []),
80
+ ...(subject.context.length > 0 ? ["", "<the project's own words on what matters>", ...subject.context.map((text) => text.trim()), "</the project's own words on what matters>"] : []),
81
+ ];
82
+ }
83
+ const stringOf = (value) => (typeof value === "string" ? value.trim() : "");
84
+ /**
85
+ * The classification, from the call's answer: each yes kept only with its evidence - a yes without it, or with it
86
+ * malformed (two readings that are the same), is a no. The verdict is yes on silent failure, divergence or cost.
87
+ */
88
+ export function classificationOf(answer) {
89
+ const questions = [];
90
+ const add = (question, yes, evidence) => {
91
+ if (yes && evidence !== "")
92
+ questions.push({ question, evidence });
93
+ };
94
+ add("silent-failure", answer.silent_failure?.answer === true, stringOf(answer.silent_failure?.wrong_output));
95
+ const input = stringOf(answer.divergence?.input);
96
+ const a = stringOf(answer.divergence?.reading_a);
97
+ const b = stringOf(answer.divergence?.reading_b);
98
+ const diverges = answer.divergence?.answer === true && input !== "" && a !== "" && b !== "" && normalised(a) !== normalised(b);
99
+ add("divergence", diverges, diverges ? `${input} - one reading: ${a}; the other: ${b}` : "");
100
+ add("revisited", answer.revisited?.answer === true, stringOf(answer.revisited?.would_change));
101
+ add("cost", answer.cost?.answer === true, stringOf(answer.cost?.consequence));
102
+ const decisive = questions.some((answered) => answered.question !== "revisited");
103
+ return { verdict: decisive ? "yes" : "no", questions, ...(diverges ? { divergence: { input, readings: [a, b] } } : {}) };
104
+ }
105
+ function classifyPrompt(subject) {
106
+ return [
107
+ "You decide whether one functional spec of a ***plain module needs explicit testing scenarios: black-box tests written",
108
+ "from scenarios, beside the unit tests its coding agent writes anyway. Not everything should be tested this way - only",
109
+ "functionality where a wrong implementation would go unnoticed, where readers would disagree, or where a bug is costly.",
110
+ "",
111
+ ...subjectBlock(subject),
112
+ ...(subject.reason !== undefined && subject.reason.trim() !== ""
113
+ ? ["", "<why it was asked for>", subject.reason.trim(), "</why it was asked for>", "This is context, not evidence: answer the questions from the functional spec, its module and the project."]
114
+ : []),
115
+ "",
116
+ "Answer four questions about the functional spec. Each yes needs concrete evidence; without it, answer no.",
117
+ "1. silent_failure: could an incorrect implementation give output that looks plausible but is wrong? Evidence: one such",
118
+ " wrong output, concretely - the input and the wrong output it would give.",
119
+ "2. divergence: is there an input for which two reasonable readings of the text give different outputs? Evidence: the",
120
+ " input, and the output under each reading.",
121
+ "3. revisited: is this functionality likely to be changed or built on later? Evidence: what would change.",
122
+ "4. cost: would a bug in it cause financial loss, security exposure, data loss, or harm to users' trust? Evidence: the",
123
+ " consequence.",
124
+ "",
125
+ "Judge only this functional spec, from what the module and the project say. Do not answer yes to be safe: a yes",
126
+ "starts work, and a functional spec with nothing at stake is better left to its unit tests.",
127
+ "Call submit_classification once.",
128
+ ].join("\n");
129
+ }
130
+ /** Classify a functional spec: one call, four evidence-backed questions. Throws when the call gives no answer. */
131
+ export async function classify(subject, options) {
132
+ const answer = await askAdvisor({
133
+ cwd: options.cwd,
134
+ model: options.model,
135
+ label: `${subject.module}:${subject.funcId} test advisor classifying`,
136
+ task: classifyPrompt(subject),
137
+ check: () => [],
138
+ tool: (take) => defineTool({
139
+ name: "submit_classification",
140
+ label: "Submit the classification",
141
+ description: "The four answers, each yes with its evidence.",
142
+ parameters: Type.Object({
143
+ silent_failure: Type.Object({ answer: Type.Boolean(), wrong_output: Type.Optional(Type.String({ description: "An input, and the plausible but wrong output an incorrect implementation would give." })) }),
144
+ divergence: Type.Object({
145
+ answer: Type.Boolean(),
146
+ input: Type.Optional(Type.String({ description: "The input two reasonable readings disagree on." })),
147
+ reading_a: Type.Optional(Type.String({ description: "The output under one reading." })),
148
+ reading_b: Type.Optional(Type.String({ description: "The output under the other." })),
149
+ }),
150
+ revisited: Type.Object({ answer: Type.Boolean(), would_change: Type.Optional(Type.String({ description: "What would change, or be built on it." })) }),
151
+ cost: Type.Object({ answer: Type.Boolean(), consequence: Type.Optional(Type.String({ description: "What a bug would cost, and whom." })) }),
152
+ }),
153
+ async execute(_id, params) {
154
+ take(params);
155
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
156
+ },
157
+ }),
158
+ });
159
+ if (answer === undefined)
160
+ throw new Error("the classification call gave no answer");
161
+ return classificationOf(answer);
162
+ }
163
+ function probesPrompt(subject, classification) {
164
+ return [
165
+ "This functional spec needs explicit testing scenarios. Pick the inputs they should cover.",
166
+ "",
167
+ ...subjectBlock(subject),
168
+ "",
169
+ "<why it needs them>",
170
+ ...classification.questions.map((answered) => `- ${answered.question}: ${answered.evidence}`),
171
+ "</why it needs them>",
172
+ "",
173
+ `Write the probes - as many as the functional spec has real boundaries, ${MIN_PROBES} to ${MAX_PROBES}, one when it has a single input:`,
174
+ "concrete inputs at the boundaries of what the functional spec says - limits, empty and missing values, duplicates,",
175
+ "order, the cases its wording leaves open - each one an input a black-box test can give through the interface the",
176
+ "functional spec names. Concrete values, never descriptions (\"a long name\").",
177
+ "Only inputs whose output the functional spec bears on: never a command, flag, alias or option it does not name, and",
178
+ "never one that exercises another functional spec of the module instead - a test of what it does not say would pin a guess.",
179
+ "Never pad to reach a number: an input that tests nothing new is noise in the scenarios. Do not say what the output",
180
+ "should be. Call submit_probes once.",
181
+ ].join("\n");
182
+ }
183
+ /** The probes: 1-10 boundary inputs, and the divergence's input when the classification found one. */
184
+ async function probes(subject, classification, options) {
185
+ const answer = await askAdvisor({
186
+ cwd: options.cwd,
187
+ model: options.model,
188
+ label: `${subject.module}:${subject.funcId} test advisor probes`,
189
+ task: probesPrompt(subject, classification),
190
+ check: (given) => {
191
+ const inputs = given.probes.map((probe) => stringOf(probe.input)).filter((input) => input !== "");
192
+ return inputs.length < MIN_PROBES || inputs.length > MAX_PROBES ? [`Give ${MIN_PROBES} to ${MAX_PROBES} probes, each a concrete input; you gave ${inputs.length}.`] : [];
193
+ },
194
+ tool: (take) => defineTool({
195
+ name: "submit_probes",
196
+ label: "Submit the probes",
197
+ description: `${MIN_PROBES} to ${MAX_PROBES} concrete boundary inputs.`,
198
+ parameters: Type.Object({ probes: Type.Array(Type.Object({ input: Type.String() })) }),
199
+ async execute(_id, params) {
200
+ take(params);
201
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
202
+ },
203
+ }),
204
+ });
205
+ if (answer === undefined)
206
+ throw new Error("the probes call gave no probes");
207
+ const inputs = [];
208
+ const add = (input) => {
209
+ if (input !== "" && !inputs.some((known) => normalised(known) === normalised(input)))
210
+ inputs.push(input);
211
+ };
212
+ for (const probe of answer.probes)
213
+ add(stringOf(probe.input));
214
+ if (classification.divergence !== undefined)
215
+ add(classification.divergence.input);
216
+ return inputs;
217
+ }
218
+ function readerPrompt(subject, inputs) {
219
+ return [
220
+ "Predict what an implementation of this functional spec outputs for each input below.",
221
+ "",
222
+ ...subjectBlock(subject),
223
+ "",
224
+ "<inputs>",
225
+ ...inputs.map((input, index) => `${index + 1}. ${input}`),
226
+ "</inputs>",
227
+ "",
228
+ "For every input, by its number, give the output you expect - concrete, as a test would check it, in a line. Where",
229
+ "the functional spec leaves it open, give the output your reading of it leads to. Where it says nothing that bears on",
230
+ `the input at all - a command or an option it does not name - answer exactly ${UNSPECIFIED}. Call submit_predictions once.`,
231
+ ].join("\n");
232
+ }
233
+ /** One reader's predictions, by probe - or nothing when it gave none that hold. */
234
+ async function predictions(subject, inputs, reader, options) {
235
+ const answer = await askAdvisor({
236
+ cwd: options.cwd,
237
+ model: options.model,
238
+ label: `${subject.module}:${subject.funcId} test advisor ${reader.name}`,
239
+ task: readerPrompt(subject, inputs),
240
+ system: reader.system,
241
+ check: (given) => {
242
+ const missing = inputs.flatMap((_input, index) => (given.predictions.filter((prediction) => prediction.input === index + 1 && stringOf(prediction.output) !== "").length === 1 ? [] : [index + 1]));
243
+ return missing.length > 0 ? [`Give exactly one output for every input; not for: ${missing.join(", ")}.`] : [];
244
+ },
245
+ tool: (take) => defineTool({
246
+ name: "submit_predictions",
247
+ label: "Submit the predictions",
248
+ description: "The output expected for every input, by its number.",
249
+ parameters: Type.Object({ predictions: Type.Array(Type.Object({ input: Type.Number(), output: Type.String() })) }),
250
+ async execute(_id, params) {
251
+ take(params);
252
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
253
+ },
254
+ }),
255
+ });
256
+ if (answer === undefined)
257
+ return undefined;
258
+ return inputs.map((_input, index) => stringOf(answer.predictions.find((prediction) => prediction.input === index + 1)?.output));
259
+ }
260
+ /**
261
+ * The inputs the readers split on - every probe where any two predictions differ once normalised: the union, never a
262
+ * majority - each with its distinct readings, in the order the readers gave them.
263
+ */
264
+ export function splitInputs(readings) {
265
+ const split = new Map();
266
+ const count = readings[0]?.length ?? 0;
267
+ for (let index = 0; index < count; index += 1) {
268
+ const distinct = [];
269
+ for (const reader of readings) {
270
+ const output = reader[index] ?? "";
271
+ if (!distinct.some((known) => normalised(known) === normalised(output)))
272
+ distinct.push(output);
273
+ }
274
+ if (distinct.length > 1)
275
+ split.set(index, distinct);
276
+ }
277
+ return split;
278
+ }
279
+ function verifyPrompt(subject, candidates) {
280
+ return [
281
+ "Readers of this functional spec disagreed on the outputs of some inputs. For each, say which readings a reasonable",
282
+ "person could hold, given the functional spec, its module and the project.",
283
+ "",
284
+ ...subjectBlock(subject),
285
+ "",
286
+ "<disagreements>",
287
+ ...candidates.flatMap((candidate, index) => [`${index + 1}. input: ${candidate.input}`, ...candidate.readings.map((reading, at) => ` reading ${at + 1}: ${reading}`)]),
288
+ "</disagreements>",
289
+ "",
290
+ "For every disagreement, by its number, list the numbers of the readings a reasonable person could hold. Leave out a",
291
+ `reading only a misreading or a mistake leads to. A reading of ${UNSPECIFIED} holds when the functional spec says nothing`,
292
+ "that settles the input. Call submit_verification once.",
293
+ ].join("\n");
294
+ }
295
+ /** Of each candidate, the readings a reasonable person could hold - or nothing when the call gave no answer that holds. */
296
+ async function verify(subject, candidates, options) {
297
+ const answer = await askAdvisor({
298
+ cwd: options.cwd,
299
+ model: options.model,
300
+ label: `${subject.module}:${subject.funcId} test advisor verifier`,
301
+ task: verifyPrompt(subject, candidates),
302
+ check: (given) => {
303
+ const problems = [];
304
+ candidates.forEach((candidate, index) => {
305
+ const said = given.inputs.filter((item) => item.input === index + 1);
306
+ if (said.length !== 1)
307
+ problems.push(`Say once which readings of disagreement ${index + 1} hold.`);
308
+ else if (said[0].reasonable.some((number) => !Number.isInteger(number) || number < 1 || number > candidate.readings.length))
309
+ problems.push(`Disagreement ${index + 1} has readings 1 to ${candidate.readings.length} only.`);
310
+ });
311
+ return problems;
312
+ },
313
+ tool: (take) => defineTool({
314
+ name: "submit_verification",
315
+ label: "Submit the verification",
316
+ description: "For every disagreement, by its number, the numbers of the readings a reasonable person could hold.",
317
+ parameters: Type.Object({ inputs: Type.Array(Type.Object({ input: Type.Number(), reasonable: Type.Array(Type.Number()) })) }),
318
+ async execute(_id, params) {
319
+ take(params);
320
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
321
+ },
322
+ }),
323
+ });
324
+ if (answer === undefined)
325
+ return undefined;
326
+ return candidates.map((candidate, index) => {
327
+ const reasonable = answer.inputs.find((item) => item.input === index + 1)?.reasonable ?? [];
328
+ return candidate.readings.filter((_reading, at) => reasonable.includes(at + 1));
329
+ });
330
+ }
331
+ /**
332
+ * Pick the inputs a functional spec's scenarios should cover: the probes, the readers' predictions of them, the
333
+ * disagreements in code, and the verifier's word on each. An input the readers agree on comes with their output; one
334
+ * they split on, with every reading the verifier kept - a single reading left is an agreed one, and none left drops it.
335
+ */
336
+ export async function pickInputs(subject, classification, options) {
337
+ const inputs = await probes(subject, classification, options);
338
+ // Two readers first; the third only when they agree on every probe - a disagreement already found is enough.
339
+ const first = await Promise.all(READERS.slice(0, 2).map((reader) => predictions(subject, inputs, reader, options)));
340
+ const readings = first.filter((reading) => reading !== undefined);
341
+ if (readings.length === 0 || splitInputs(readings).size === 0) {
342
+ for (const reader of READERS.slice(2)) {
343
+ const more = await predictions(subject, inputs, reader, options);
344
+ if (more !== undefined)
345
+ readings.push(more);
346
+ }
347
+ }
348
+ if (readings.length === 0)
349
+ throw new Error("no reader predicted the probes' outputs");
350
+ const split = splitInputs(readings);
351
+ const candidates = [...split].map(([index, distinct]) => ({ index, input: inputs[index], readings: distinct }));
352
+ // Without the verifier's word, every reading stands: a split left unsettled is the chat's to settle as it writes.
353
+ const kept = candidates.length > 0 ? ((await verify(subject, candidates, options)) ?? candidates.map((candidate) => candidate.readings)) : [];
354
+ const out = [];
355
+ inputs.forEach((input, index) => {
356
+ const at = candidates.findIndex((candidate) => candidate.index === index);
357
+ // An input the functional spec does not settle is left out: a scenario on it would pin a guess.
358
+ if (at === -1) {
359
+ if (normalised(readings[0][index]) !== UNSPECIFIED)
360
+ out.push({ input, agreed: readings[0][index] });
361
+ return;
362
+ }
363
+ const held = kept[at] ?? [];
364
+ if (held.some((reading) => normalised(reading) === UNSPECIFIED))
365
+ return;
366
+ if (held.length > 1)
367
+ out.push({ input, readings: held });
368
+ else if (held.length === 1)
369
+ out.push({ input, agreed: held[0] });
370
+ });
371
+ return out;
372
+ }
373
+ /** The whole judgement: classify, and on a yes pick the inputs. Throws when a call that cannot be done without fails. */
374
+ export async function advise(subject, options) {
375
+ const classification = await classify(subject, options);
376
+ if (classification.verdict === "no")
377
+ return { verdict: "no", questions: classification.questions, inputs: [] };
378
+ return { verdict: "yes", questions: classification.questions, inputs: await pickInputs(subject, classification, options) };
379
+ }
@@ -1,46 +1,84 @@
1
+ import { type MarshalledPlainSource } from "../plain/spec.ts";
1
2
  /**
2
- * The two kinds of test the conformance step writes, and how each is told apart in the code.
3
+ * How the conformance tests are labelled, and how the labels are checked.
3
4
  *
4
- * Conformance tests are written from the functional spec itself: what the functionality does,
5
- * its edge cases, its errors - as if no acceptance tests had been given. Acceptance tests are the
6
- * ones the spec's author wrote, each implemented as a test of its own, doing what it says. The
7
- * two are kept apart even when the acceptance tests would seem to cover the functionality: tests
8
- * derived only from the acceptance tests check what the author thought to check, and nothing
9
- * the spec says beyond it.
5
+ * A functional spec's conformance tests are its scenarios, and nothing else: each conformance scenario and each
6
+ * acceptance scenario of its test plain file is carried out by at least one test that does what it says. No test is
7
+ * invented beside them. A functional spec with no scenarios has no conformance tests.
10
8
  *
11
- * Each kind is labelled where anyone reading the tests can see it - in its file names, and in a
12
- * comment the harness looks for - so which test is which, and which acceptance test a test
13
- * implements, never has to be guessed.
9
+ * Each is labelled where anyone reading the tests can see it - a comment the harness looks for - so which functional
10
+ * spec a file tests, and which scenario a test carries out, never has to be guessed:
11
+ * - a file: `conformance tests of <module>:<func-id>`, one line for each functional spec it tests;
12
+ * - a test: `conformance scenario <scenario-id> of <module>:<func-id>` or `acceptance scenario <scenario-id> of
13
+ * <module>:<func-id>`, right above it.
14
14
  */
15
- export declare function conformanceLabel(frid: string): string;
16
- export declare function acceptanceLabel(frid: string, n: number): string;
17
- /** An acceptance test as the spec gives it, without its bullet. */
18
- export declare function acceptanceText(item: string): string;
15
+ /** A scenario as the conformance agent is given it: its id, and its text. */
16
+ export interface Scenario {
17
+ id: string;
18
+ text: string;
19
+ }
19
20
  /**
20
- * What is missing from the labelling of a functionality's tests: the conformance label, when no
21
- * test file carries it, and each acceptance test no test is labelled as implementing.
21
+ * A functional spec's scenarios: those its conformance tests carry out. `module`: a module below's - the tip's suite holds
22
+ * the tests of the whole chain - else the codebase's own.
22
23
  */
23
- export declare function missingTestLabels(testsDir: string, frid: string, acceptanceTests: string[]): string[];
24
+ export interface TestedSpec {
25
+ funcId: string;
26
+ module?: string;
27
+ conformance: Scenario[];
28
+ acceptance: Scenario[];
29
+ }
30
+ export type ScenarioKind = "conformance" | "acceptance";
31
+ /** The label of a file of a functional spec's conformance tests. */
32
+ export declare function conformanceLabel(module: string, funcId: string): string;
33
+ /** The label right above a test that carries out a scenario. */
34
+ export declare function scenarioLabel(kind: ScenarioKind, scenarioId: string, module: string, funcId: string): string;
35
+ /** Every scenario of a functional spec, each with its kind, in its file's order: the conformance ones, then the acceptance ones. */
36
+ export declare function scenariosOf(spec: TestedSpec): Array<Scenario & {
37
+ kind: ScenarioKind;
38
+ }>;
39
+ /** A scenario as the agents are given it: its label, and its text under it. */
40
+ export declare function scenarioText(module: string, funcId: string, scenario: Scenario & {
41
+ kind: ScenarioKind;
42
+ }): string;
24
43
  /**
25
- * The test files that carry a functionality's labels - its conformance tests' or any of its acceptance tests'. The
26
- * harness's own files beside them - the notes, the record - are not tests, whatever they mention.
44
+ * What is missing from the labelling of a functional spec's tests: the file label, when no test file carries it, and each
45
+ * scenario no test is labelled as carrying out.
27
46
  */
28
- export declare function labelledFiles(testsDir: string, frid: string): string[];
29
- /** Sent back when the labels are missing: which, and how to label them. */
30
- export declare function labelsPrompt(frid: string, missing: string[], empty?: boolean, hasAcceptance?: boolean): string;
47
+ export declare function missingTestLabels(testsDir: string, codebase: string, spec: TestedSpec): string[];
31
48
  /**
32
- * Sent back when the labels of several functionalities are missing - a spec change that touches them: every one that is
33
- * missing, in one request, so none is asked for in place of another.
49
+ * Sent back when the labels of functional specs are missing - one, or every one a spec change touches: every one that
50
+ * is missing, in one request, so none is asked for in place of another.
34
51
  */
35
- export declare function labelsPromptFor(items: Array<{
36
- frid: string;
52
+ export declare function labelsPromptFor(codebase: string, items: Array<{
53
+ funcId: string;
54
+ module?: string;
37
55
  missing: string[];
38
- }>, empty?: boolean, hasAcceptance?: boolean): string;
56
+ }>, empty?: boolean): string;
39
57
  /**
40
- * Conformance and acceptance tests in the code folder, where they do not belong: they live in the
41
- * module's tests folder, which the harness runs as the conformance suite. Found by their label, or
42
- * by a test file under a name or folder that says conformance or acceptance - one written without
43
- * a label is misplaced all the same.
58
+ * The functional specs of a tree of what conformance tests are written for, with their scenarios - those of the modules
59
+ * below with their module, the codebase's own after them.
60
+ */
61
+ export declare function testedSpecsOf(tree: MarshalledPlainSource | null | undefined): TestedSpec[];
62
+ /**
63
+ * The labels in the tests folder that name nothing the tests are written for: a functional spec, or a scenario of one,
64
+ * that `specs` does not have - left over from a scenario or a functional spec taken out or renamed, mistyped, or
65
+ * invented. Each with the file it is in.
66
+ */
67
+ export declare function orphanedLabels(testsDir: string, codebase: string, specs: TestedSpec[]): Array<{
68
+ file: string;
69
+ label: string;
70
+ }>;
71
+ /** Sent back when labels name nothing the tests are written for. */
72
+ export declare function orphansPrompt(orphans: Array<{
73
+ file: string;
74
+ label: string;
75
+ }>): string;
76
+ /** Sent back when the labels of one functional spec are missing. */
77
+ export declare function labelsPrompt(module: string, funcId: string, missing: string[], empty?: boolean): string;
78
+ /**
79
+ * Conformance tests in the code folder, where they do not belong: they live in the module's tests folder, which the
80
+ * harness runs as the conformance suite. Found by their label, or by a test file under a name or folder that says
81
+ * conformance or acceptance - one written without a label is misplaced all the same.
44
82
  */
45
83
  export declare function misplacedConformanceTests(codeDir: string): string[];
46
84
  /** Sent back when there are: to the agent writing the code, or to the one writing the tests. */