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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +70 -14
  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 -13
  95. package/dist/regenerate/conformance.js +119 -82
  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 +62 -43
  104. package/dist/regenerate/loop.js +472 -341
  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 +71 -28
  135. package/dist/regenerate/test-kinds.js +110 -63
  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 +87 -52
  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 +23 -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
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
- import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS, getInheritedFunctionalSpecs, } from "../plain/spec.js";
5
+ import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS, getInheritedFunctionalSpecs, } from "../plain/spec.js";
6
6
  import { validateConcepts } from "../plain/concepts.js";
7
7
  import { sectionTag } from "./prompt.js";
8
8
  /**
@@ -11,9 +11,10 @@ import { sectionTag } from "./prompt.js";
11
11
  *
12
12
  * Both specs are marshalled trees, written out as text with one JSON value per line - a
13
13
  * section's name, then each of its items on a line of its own - and compared with git. So an
14
- * item is the unit of change: a functional spec with its acceptance tests and linked resources,
14
+ * item is the unit of change: a functional spec with its scenarios and linked resources,
15
15
  * a definition, a req. Git lines them up, so a spec inserted in the middle is one addition and
16
- * the specs after it are untouched, though their numbers moved.
16
+ * the specs after it are untouched. A functional spec is known by its func-id: one taken out and
17
+ * one put in with the same func-id are the same functional spec, changed - or, the same text, moved.
17
18
  *
18
19
  * The planner (`chunk-plan.ts`) cuts the changes into steps - which go together, which go first. It
19
20
  * picks changes by id; it never writes a diff. `makeStep` applies what it picked, and `inconsistencies`
@@ -72,8 +73,9 @@ export function sameSpec(before, after) {
72
73
  const had = modulesOf(before);
73
74
  if ([...modulesOf(after)].some((module) => !had.has(module)))
74
75
  return false;
76
+ // A functional spec by its func-id and what it says, wherever it is written: moved, it is the same spec.
75
77
  const text = (item) => {
76
- const { module: _module, frid: _frid, ...rest } = item;
78
+ const { module: _module, ...rest } = item;
77
79
  return JSON.stringify(sortKeys(rest));
78
80
  };
79
81
  const list = (tree, heading) => (tree[heading] ?? []).map(text);
@@ -84,7 +86,7 @@ export function sameSpec(before, after) {
84
86
  .map((key) => [key, sortKeys(tree[key])]);
85
87
  return JSON.stringify({
86
88
  sections: [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS].map((heading) => list(tree, heading).sort()),
87
- specs: [...list(tree, REQUIRED_FUNCTIONAL_SPECS), ...list(tree, FUNCTIONAL_REQUIREMENTS)],
89
+ specs: [...list(tree, REQUIRED_FUNCTIONAL_SPECS), ...list(tree, FUNCTIONAL_REQUIREMENTS)].sort(),
88
90
  others,
89
91
  });
90
92
  };
@@ -245,7 +247,51 @@ export function changesBetween(before, after) {
245
247
  });
246
248
  });
247
249
  }
248
- return changes;
250
+ return mergeMoves(changes);
251
+ }
252
+ /** The sections whose items are functional specs, each known by its func-id. */
253
+ const FUNCTIONAL_SECTIONS = [FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS];
254
+ /** A functional spec's identity - its func-id, and its module for one a module below has - or undefined for any other line. */
255
+ function identityOfLine(line) {
256
+ if (line === undefined || headerOf(line) !== undefined)
257
+ return undefined;
258
+ try {
259
+ const item = JSON.parse(line);
260
+ return typeof item.id === "string" ? `${typeof item.module === "string" ? item.module : ""}:${item.id}` : undefined;
261
+ }
262
+ catch {
263
+ return undefined;
264
+ }
265
+ }
266
+ /**
267
+ * A functional spec taken out in one place and put in at another - the same func-id - is one change, not two: moved,
268
+ * and changed where its text differs. git lines up a move as a removal and an addition, in hunks of their own; the change
269
+ * takes the line out where it was and puts it in where it now is.
270
+ */
271
+ function mergeMoves(changes) {
272
+ // Paired first, whichever comes first: a spec moved up is added before it is removed.
273
+ const pairs = new Map();
274
+ const used = new Set();
275
+ for (const change of changes) {
276
+ const identity = change.kind === "remove" && FUNCTIONAL_SECTIONS.includes(change.section) ? identityOfLine(change.before) : undefined;
277
+ const added = identity === undefined ? undefined : changes.find((other) => !used.has(other) && other.kind === "add" && other.section === change.section && identityOfLine(other.after) === identity);
278
+ if (added === undefined)
279
+ continue;
280
+ used.add(added);
281
+ pairs.set(change, added);
282
+ }
283
+ const merged = [];
284
+ for (const change of changes) {
285
+ if (used.has(change))
286
+ continue;
287
+ const added = pairs.get(change);
288
+ merged.push(added === undefined ? change : { ...change, kind: "change", after: added.after, position: added.position, inserts: added.inserts });
289
+ }
290
+ return merged.map((change, index) => ({ ...change, id: `c${index + 1}` }));
291
+ }
292
+ /** Whether a change moves a functional spec and leaves what it says as it was. */
293
+ export function isMove(change) {
294
+ return change.kind === "change" && change.before === change.after;
249
295
  }
250
296
  /** The words of an item's text, for telling a changed item from a different one. */
251
297
  function wordsOf(line) {
@@ -268,12 +314,18 @@ export function similarity(a, b) {
268
314
  const SAME_ITEM = 0.5;
269
315
  /**
270
316
  * Which removed item became which added one, in order on both sides: the pairing that matches
271
- * the most alike items. Only items at least SAME_ITEM alike are paired.
317
+ * the most alike items. A functional spec pairs only with the one of its func-id; any other item only with one at
318
+ * least SAME_ITEM alike.
272
319
  */
273
320
  function pairItems(removed, added) {
274
- const alike = (r, a) => headerOf(removed[r]) === undefined && headerOf(added[a]) === undefined && similarity(removed[r], added[a]) >= SAME_ITEM
275
- ? similarity(removed[r], added[a])
276
- : 0;
321
+ const alike = (r, a) => {
322
+ if (headerOf(removed[r]) !== undefined || headerOf(added[a]) !== undefined)
323
+ return 0;
324
+ const identity = identityOfLine(removed[r]);
325
+ if (identity !== undefined || identityOfLine(added[a]) !== undefined)
326
+ return identity === identityOfLine(added[a]) ? 1 : 0;
327
+ return similarity(removed[r], added[a]) >= SAME_ITEM ? similarity(removed[r], added[a]) : 0;
328
+ };
277
329
  // Best total likeness over order-keeping pairings - a longest common subsequence, weighted.
278
330
  const best = Array.from({ length: removed.length + 1 }, () => new Array(added.length + 1).fill(0));
279
331
  for (let r = removed.length - 1; r >= 0; r -= 1) {
@@ -314,15 +366,82 @@ export function applyChanges(before, changes) {
314
366
  }
315
367
  return `${out.join("\n")}\n`;
316
368
  }
317
- /** An item as a person reads it: its text, and its acceptance tests under it. */
369
+ /** A functional spec's scenarios of one kind, as a person reads them: each under its scenario-id, in its section's tag. */
370
+ export function scenarioLines(tag, scenarios) {
371
+ if (scenarios === undefined || scenarios.length === 0)
372
+ return [];
373
+ const indent = (text) => text.split("\n").map((line) => (line === "" ? "" : ` ${line}`)).join("\n");
374
+ return [` <${tag}>`, ...scenarios.map((scenario) => indent(`# ${scenario.id ?? ""}\n${scenario.text ?? ""}`)), ` </${tag}>`];
375
+ }
376
+ /**
377
+ * An item as a person reads it: a functional spec under its `# <func-id>` heading, as its module's file writes it,
378
+ * with its conformance and acceptance scenarios under it where the tree has them; anything else as it reads.
379
+ */
318
380
  function itemText(line) {
319
381
  const item = JSON.parse(line);
320
- const tests = item[ACCEPTANCE_TESTS] ?? [];
321
382
  return [
322
- item.markdown ?? line,
323
- ...(tests.length > 0 ? [" <acceptance tests>", ...tests.map((test) => ` ${(test.markdown ?? "").replace(/\n/g, "\n ")}`), " </acceptance tests>"] : []),
383
+ item.id !== undefined ? `# ${item.id}\n${item.markdown ?? ""}` : (item.markdown ?? line),
384
+ ...scenarioLines("conformance tests", item[CONFORMANCE_TESTS]),
385
+ ...scenarioLines("acceptance tests", item[ACCEPTANCE_TESTS]),
324
386
  ].join("\n");
325
387
  }
388
+ /** An item as its module's file writes it: a functional spec under its `# <func-id>` heading - its scenarios are in its test plain file. */
389
+ function fileItemText(line) {
390
+ const item = JSON.parse(line);
391
+ return item.id !== undefined ? `# ${item.id}\n${item.markdown ?? ""}` : (item.markdown ?? line);
392
+ }
393
+ /** A functional spec's test plain file, as it would read: its binding, then each section that has scenarios, each scenario under its id. */
394
+ export function testPlainFileText(module, item) {
395
+ // A section with no scenarios is left out: it is a syntax error.
396
+ const section = (title, scenarios) => (scenarios ?? []).length === 0 ? [] : [`${title}\n\n${(scenarios ?? []).map((scenario) => `# ${scenario.id ?? ""}\n${(scenario.text ?? "").trim()}`).join("\n\n")}`];
397
+ const sections = [...section("***conformance tests***", item[CONFORMANCE_TESTS]), ...section("***acceptance tests***", item[ACCEPTANCE_TESTS])];
398
+ return `---\nmodule: ${module}\nfunc_id: ${item.id ?? ""}\n---\n\n${sections.join("\n\n")}\n`;
399
+ }
400
+ /** One file's two texts as git diffs them, named `path` on both sides: an empty side is the file added or taken out whole. */
401
+ export function diffFileTexts(before, after, path) {
402
+ return before === after ? "" : gitDiff(before, after, 3, [path, path]);
403
+ }
404
+ /**
405
+ * The functional specs' test plain files a change between two serialized trees touches, each as a diff of its own,
406
+ * rebuilt from the trees as the module's file is: only the trees that carry scenarios - the conformance tests' - have
407
+ * any. A functional spec that had no scenarios has no test plain file before, so its file shows as added whole; one whose
408
+ * scenarios all went, as deleted whole - an empty one is a syntax error.
409
+ */
410
+ export function diffTestPlainFiles(before, after, module, pathOf) {
411
+ const carriers = (serialized) => {
412
+ const found = new Map();
413
+ let section;
414
+ for (const line of serialized.split("\n")) {
415
+ if (line === "")
416
+ continue;
417
+ const header = headerOf(line);
418
+ if (header !== undefined) {
419
+ section = header.key;
420
+ continue;
421
+ }
422
+ // A module below's functional specs too, by module and func-id: the tip's conformance suite holds the chain's.
423
+ if (section === undefined || !FUNCTIONAL_SECTIONS.includes(section))
424
+ continue;
425
+ const item = JSON.parse(line);
426
+ // A functional spec with no scenarios - none listed, or the lists empty - has no test plain file.
427
+ if (item.id !== undefined && (item[CONFORMANCE_TESTS] ?? []).length + (item[ACCEPTANCE_TESTS] ?? []).length > 0)
428
+ found.set(`${item.module ?? ""}:${item.id}`, item);
429
+ }
430
+ return found;
431
+ };
432
+ const was = carriers(before);
433
+ const now = carriers(after);
434
+ return [...new Set([...now.keys(), ...was.keys()])]
435
+ .map((key) => {
436
+ const item = now.get(key) ?? was.get(key);
437
+ const owner = item.module ?? module;
438
+ const text = (found) => (found === undefined ? "" : testPlainFileText(owner, found));
439
+ const [from, to] = [text(was.get(key)), text(now.get(key))];
440
+ const path = pathOf(item.id ?? "", owner);
441
+ return from === to ? "" : gitDiff(from, to, 3, [path, path]);
442
+ })
443
+ .join("");
444
+ }
326
445
  /** A serialized tree as a person reads it, section by section. */
327
446
  export function treeText(serialized) {
328
447
  const out = [];
@@ -345,25 +464,52 @@ export function treeText(serialized) {
345
464
  out.push(`</${open}>`);
346
465
  return out.join("\n").trim();
347
466
  }
348
- /** What of an item a change touches: its text, its acceptance tests, its linked resources. */
467
+ /** What of an item a change touches: its text, its scenarios, its linked resources. */
349
468
  function changedParts(before, after) {
350
469
  if (headerOf(before) !== undefined || headerOf(after) !== undefined)
351
470
  return "the text";
352
471
  const x = JSON.parse(before);
353
472
  const y = JSON.parse(after);
354
- const names = { markdown: "the text", [ACCEPTANCE_TESTS]: "the acceptance tests", linked_resources: "the linked resources", code_variables: "the code variables" };
473
+ const names = {
474
+ markdown: "the text",
475
+ [CONFORMANCE_TESTS]: "the conformance scenarios",
476
+ [ACCEPTANCE_TESTS]: "the acceptance scenarios",
477
+ linked_resources: "the linked resources",
478
+ code_variables: "the code variables",
479
+ };
355
480
  const parts = [...new Set([...Object.keys(x), ...Object.keys(y)])]
356
481
  .filter((key) => JSON.stringify(x[key]) !== JSON.stringify(y[key]))
357
482
  .map((key) => names[key] ?? key);
358
483
  return parts.length === 0 ? "the text" : parts.join(" and ");
359
484
  }
485
+ /** A functional spec a change is to, by its func-id - `module:func-id` for one of a module below - or nothing for another item. */
486
+ function funcSpecOf(change) {
487
+ if (!FUNCTIONAL_SECTIONS.includes(change.section))
488
+ return undefined;
489
+ try {
490
+ const item = JSON.parse(change.after ?? change.before ?? "");
491
+ if (item.id === undefined)
492
+ return undefined;
493
+ return `functional spec \`${item.module !== undefined ? `${item.module}:` : ""}${item.id}\``;
494
+ }
495
+ catch {
496
+ return undefined;
497
+ }
498
+ }
360
499
  /** A change as the planning agent is shown it. */
361
500
  export function describeChange(change) {
362
- const what = change.kind === "add"
363
- ? `add to ${change.section}, as item ${change.position}`
364
- : change.kind === "remove"
365
- ? `remove item ${change.position} of ${change.section}`
366
- : `change ${changedParts(change.before, change.after)} of item ${change.position} of ${change.section}`;
501
+ const spec = funcSpecOf(change);
502
+ const what = spec !== undefined
503
+ ? change.kind === "add"
504
+ ? `add ${spec} to ${change.section}, as item ${change.position}`
505
+ : change.kind === "remove"
506
+ ? `remove ${spec} from ${change.section}`
507
+ : `change ${changedParts(change.before, change.after)} of ${spec}`
508
+ : change.kind === "add"
509
+ ? `add to ${change.section}, as item ${change.position}`
510
+ : change.kind === "remove"
511
+ ? `remove item ${change.position} of ${change.section}`
512
+ : `change ${changedParts(change.before, change.after)} of item ${change.position} of ${change.section}`;
367
513
  const header = (line) => (line !== undefined && headerOf(line) !== undefined ? `the section ${headerOf(line).key}` : undefined);
368
514
  const body = [
369
515
  ...(change.before !== undefined ? [" before:", ` ${(header(change.before) ?? itemText(change.before)).replace(/\n/g, "\n ")}`] : []),
@@ -397,7 +543,7 @@ export function layoutOf(text) {
397
543
  continue;
398
544
  }
399
545
  // An item straight after another's last line, with no blank line: the section is written tight.
400
- if (section !== undefined && line.startsWith("- ") && previous.trim() !== "" && !SECTION_HEADING.test(previous))
546
+ if (section !== undefined && section !== FUNCTIONAL_REQUIREMENTS && line.startsWith("- ") && previous.trim() !== "" && !SECTION_HEADING.test(previous))
401
547
  tight.add(section);
402
548
  previous = line;
403
549
  const concept = CONCEPT.exec(line);
@@ -423,7 +569,7 @@ export function plainText(serialized, layout) {
423
569
  if (header !== undefined)
424
570
  sections.push({ key: header.key, items: [] });
425
571
  else
426
- sections.at(-1)?.items.push(itemText(line));
572
+ sections.at(-1)?.items.push(fileItemText(line));
427
573
  }
428
574
  const own = sections.filter((section) => section.key !== REQUIRED_FUNCTIONAL_SPECS && section.items.length > 0);
429
575
  if (layout === undefined)
@@ -450,6 +596,41 @@ export function plainText(serialized, layout) {
450
596
  export function diffSpecText(before, after, file = "spec.plain", layout) {
451
597
  return gitDiff(`${plainText(before, layout)}\n`, `${plainText(after, layout)}\n`, 3, [file, file]);
452
598
  }
599
+ /**
600
+ * The functional specs of the modules below that a change between two serialized trees touches, each module's as a diff
601
+ * of its own file's `***functional specs***`: the tip is built with them, but its own file does not hold them.
602
+ */
603
+ export function diffRequiredSpecs(before, after, file = "spec.plain") {
604
+ const byModule = (serialized) => {
605
+ const found = new Map();
606
+ let section;
607
+ for (const line of serialized.split("\n")) {
608
+ if (line === "")
609
+ continue;
610
+ const header = headerOf(line);
611
+ if (header !== undefined) {
612
+ section = header.key;
613
+ continue;
614
+ }
615
+ if (section !== REQUIRED_FUNCTIONAL_SPECS)
616
+ continue;
617
+ const module = JSON.parse(line).module ?? "";
618
+ found.set(module, [...(found.get(module) ?? []), fileItemText(line)]);
619
+ }
620
+ return found;
621
+ };
622
+ const was = byModule(before);
623
+ const now = byModule(after);
624
+ const folder = dirname(file);
625
+ return [...new Set([...was.keys(), ...now.keys()])]
626
+ .map((module) => {
627
+ const text = (items) => (items === undefined ? "" : `***${FUNCTIONAL_REQUIREMENTS}***\n\n${items.join("\n\n")}\n`);
628
+ const [from, to] = [text(was.get(module)), text(now.get(module))];
629
+ const path = folder === "." ? `${module}.plain` : `${folder}/${module}.plain`;
630
+ return from === to ? "" : gitDiff(from, to, 3, [path, path]);
631
+ })
632
+ .join("");
633
+ }
453
634
  /** The linked resources of a changed item whose content is different now - each hashed on its own: by their path, with both hashes. */
454
635
  export function resourcesChanged(before, after) {
455
636
  const resources = (line) => {
@@ -494,7 +675,7 @@ export function resourceDiff(changes, file = "spec.plain") {
494
675
  ].join("\n"))
495
676
  .join("");
496
677
  }
497
- /** The item a change is to, as the person finds it: its concept or its number, in the spec file it is in - a module below's by its name. */
678
+ /** The item a change is to, as the person finds it: its concept, its func-id or its number, in the spec file it is in - a module below's by its name. */
498
679
  function linkedBy(change, file) {
499
680
  let item = {};
500
681
  try {
@@ -506,12 +687,16 @@ function linkedBy(change, file) {
506
687
  const concept = /^-?\s*(:[^:\s]+:)/.exec(item.markdown?.trim() ?? "")?.[1];
507
688
  const what = change.section === DEFINITIONS && concept !== undefined
508
689
  ? concept
509
- : change.section === FUNCTIONAL_REQUIREMENTS
510
- ? `functional spec ${change.position}`
690
+ : FUNCTIONAL_SECTIONS.includes(change.section) && item.id !== undefined
691
+ ? `functional spec ${item.id}`
511
692
  : `${sectionTag(change.section).replace(/s$/, "")} ${change.position}`;
512
693
  return `${what} in ${item.module !== undefined ? `${item.module}.plain` : file}`;
513
694
  }
514
- export function makeStep(title, reason, changes, before, after, file, layout) {
695
+ /**
696
+ * A step of the work. `testPlain`: for the conformance tests' steps, the module and where its functional specs' test plain
697
+ * files are - a scenario changed shows as a diff of its test plain file, after the module's.
698
+ */
699
+ export function makeStep(title, reason, changes, before, after, file, layout, testPlain) {
515
700
  return {
516
701
  title,
517
702
  reason,
@@ -521,7 +706,7 @@ export function makeStep(title, reason, changes, before, after, file, layout) {
521
706
  tree: parseTree(after),
522
707
  diff: diffTrees(before, after),
523
708
  // What a linked resource holds changed shows in the step's diff, after the spec's own.
524
- textDiff: `${diffSpecText(before, after, file, layout)}${resourceDiff(changes, file)}`,
709
+ textDiff: `${diffSpecText(before, after, file, layout)}${diffRequiredSpecs(before, after, file)}${testPlain !== undefined ? diffTestPlainFiles(before, after, testPlain.module, testPlain.pathOf) : ""}${resourceDiff(changes, file)}`,
525
710
  sections: [...new Set(changes.map((change) => change.section))],
526
711
  };
527
712
  }
@@ -1,4 +1,4 @@
1
- import { DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, type MarshalledPlainSource } from "../plain/spec.ts";
1
+ import { DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, type MarshalledPlainSource, type ScenarioText } from "../plain/spec.ts";
2
2
  /**
3
3
  * A module's spec as the prompts read it: the sections every functional spec shares, and a
4
4
  * functional spec a module inherits. What the code and the tests are built from is in `records.ts`.
@@ -12,9 +12,10 @@ export interface SharedSections {
12
12
  /** A functional spec of a module this one requires: documented, not built here. */
13
13
  export interface RequiredSpec {
14
14
  module: string;
15
- frid: string;
15
+ funcId: string;
16
16
  spec: string;
17
- acceptanceTests?: string[];
17
+ conformanceTests?: ScenarioText[];
18
+ acceptanceTests?: ScenarioText[];
18
19
  }
19
20
  /** A module's shared sections as they stand, with or without a functional spec to read them through. */
20
21
  export declare function sectionsOf(tree: MarshalledPlainSource): SharedSections;
@@ -15,7 +15,7 @@ export declare const ATTEMPTS: Record<StopPoint, number>;
15
15
  export interface StopFacts {
16
16
  moduleName: string;
17
17
  /** The functionality it stopped at; none for a spec change. */
18
- frid?: string;
18
+ funcId?: string;
19
19
  at: StopPoint;
20
20
  /** The unit test runs, the last of which is the failing one. */
21
21
  runs?: TestRun[];
@@ -11,7 +11,7 @@ export const ATTEMPTS = {
11
11
  * offered here is a change to a `.plain` file, never to code, tests or the machine.
12
12
  */
13
13
  export function describeStop(facts) {
14
- const what = facts.frid !== undefined ? `${facts.moduleName} functionality ${facts.frid}` : `the spec change in ${facts.moduleName}`;
14
+ const what = facts.funcId !== undefined ? `functional spec ${facts.moduleName}:${facts.funcId}` : `the spec change in ${facts.moduleName}`;
15
15
  // What actually happened, not the most it could have been.
16
16
  const tries = facts.at === "unit tests" ? (facts.attempts ?? ATTEMPTS[facts.at]) : facts.at === "conformance" ? (facts.conformance?.rounds ?? ATTEMPTS[facts.at]) : ATTEMPTS[facts.at];
17
17
  let why;
@@ -50,12 +50,12 @@ export function describeStop(facts) {
50
50
  }
51
51
  else if (facts.at === "conformance" && facts.conformance?.specConflict !== undefined) {
52
52
  const { functionalities, reason } = facts.conformance.specConflict;
53
- const [one, two] = functionalities.map((ref) => `${ref.module} ${ref.frid}`);
53
+ const [one, two] = functionalities.map((ref) => `${ref.module}:${ref.funcId}`);
54
54
  why = `its spec contradicts itself: functionalities ${one} and ${two} cannot both hold. ${reason.trim()}`;
55
55
  todo = [
56
56
  `Compare functional specs ${one} and ${two} (\`analyze-2-func-specs\`, \`resolve-spec-conflict\`) and change whichever is wrong - ` +
57
57
  "or both, so they say the same thing about where they meet.",
58
- "If one is meant to change what the other does, say so in the one that changes it, and update its acceptance tests to match.",
58
+ "If one is meant to change what the other does, say so in the one that changes it, and update its scenarios to match.",
59
59
  ];
60
60
  }
61
61
  else if (facts.at === "conformance" && facts.conformance?.review !== undefined && facts.conformance.stuck === undefined) {
@@ -64,14 +64,14 @@ export function describeStop(facts) {
64
64
  `its conformance tests pass, but never passed review in ${tries} round${tries === 1 ? "" : "s"}: they do not yet test what the spec says, ` +
65
65
  `black box. What the review last found:\n${problems.slice(0, 8).map((problem) => ` - ${problem.kind === "missing" ? "missing" : "bad"} · ${problem.file}${problem.test ? ` · ${problem.test}` : ""}: ${problem.what.trim().replace(/\s+/g, " ")}`).join("\n")}`;
66
66
  todo = [
67
- "If an acceptance test asks for something the tests cannot do - a service they cannot reach, data they cannot set up - say in the test " +
68
- "reqs how they do it, or change the acceptance test to what can be checked.",
67
+ "If a scenario asks for something the tests cannot do - a service they cannot reach, data they cannot set up - say in the test " +
68
+ "reqs how they do it, or change the scenario to what can be checked.",
69
69
  "If the test reqs leave open how the software is reached or what may be mocked, say so exactly.",
70
70
  ];
71
71
  }
72
72
  else if (facts.at === "conformance" && facts.conformance?.unlabelled !== undefined) {
73
73
  why =
74
- "its conformance tests were never written and labelled as the two kinds - conformance tests and each acceptance test - " +
74
+ "its conformance tests were never written and labelled as the scenarios they carry out - each conformance and acceptance scenario - " +
75
75
  `though they were sent back for it again and again. Still missing:\n${facts.conformance.unlabelled.map((line) => ` - ${line}`).join("\n")}`;
76
76
  todo = ["If the test reqs leave open where the tests go or how they are built, say so exactly - the folder, the files, the build."];
77
77
  }
@@ -109,7 +109,7 @@ export function describeStop(facts) {
109
109
  why = `after ${tries} round${tries === 1 ? "" : "s"}, its conformance tests and its code still disagree about what the spec means.`;
110
110
  todo = [
111
111
  "The functional spec can be read more than one way. Say exactly what should happen - the inputs, the outputs, " +
112
- "the edge cases - and add acceptance tests that pin down the expected result.",
112
+ "the edge cases - and add scenarios to its test plain file that pin down the expected result.",
113
113
  "If the conformance tests are meant to be written or run differently - framework, command, setup - change the test reqs.",
114
114
  ];
115
115
  }
@@ -121,7 +121,7 @@ export function describeStop(facts) {
121
121
  "This most often means its spec contradicts an earlier one. Compare them (`analyze-func-specs`, " +
122
122
  "`resolve-spec-conflict`) and change whichever is wrong.",
123
123
  "If the new behaviour is meant to change what an earlier functionality does, change that earlier spec and its " +
124
- "acceptance tests to say so.",
124
+ "scenarios to say so.",
125
125
  ];
126
126
  }
127
127
  return [
@@ -140,13 +140,13 @@ export function describeStop(facts) {
140
140
  * spec is all they work on, and the fix is theirs to choose.
141
141
  */
142
142
  export function describeSpecConflict(conflict) {
143
- const name = (spec) => `functional spec ${spec.frid} of ${spec.module}`;
143
+ const name = (spec) => `functional spec ${spec.module}:${spec.funcId}`;
144
144
  const [one, two] = conflict.specs;
145
145
  return [
146
146
  `The spec contradicts itself: ${name(one)} and ${name(two)} cannot both hold.`,
147
147
  "",
148
148
  ...conflict.specs.flatMap((spec) => [
149
- `Functional spec ${spec.frid} of ${spec.module} (${basename(spec.plainFile)}):`,
149
+ `Functional spec ${spec.module}:${spec.funcId} (${basename(spec.plainFile)}):`,
150
150
  ...spec.text.trim().split("\n").map((line) => ` ${line}`),
151
151
  "",
152
152
  ]),
@@ -0,0 +1,101 @@
1
+ import type { AdvisorAnswer, AdvisorInput, AdvisorReport } from "./advisor-records.ts";
2
+ /**
3
+ * The test advisor's judgement of one functional spec: does it need explicit testing scenarios, and if so, which inputs
4
+ * should they cover (docs/test-advisor.md). Every step is a plain model call on `models.advisor`, answered through one
5
+ * tool and checked in code, as the planner's grouping agent is - none is a skill, and none can touch the project.
6
+ *
7
+ * - **Classify:** four questions, each yes carrying its evidence - a wrong output that looks right (silent failure), an
8
+ * input two reasonable readings answer differently (divergence), what would change (revisited), the consequence of a
9
+ * bug (cost). A yes whose evidence is empty or malformed counts as no. Silent failure, divergence or cost make the
10
+ * verdict yes; revisited alone does not - the unit tests are the regression protection.
11
+ * - **On a yes, pick the inputs:** one call writes 1-10 boundary probes (and the divergence's input), readers that
12
+ * differ only by their system prompt each predict every probe's output, code compares them, and any disagreement is a
13
+ * candidate - the union, never a majority. The third reader is spared when the first two already disagree. One last
14
+ * call keeps, of each candidate, the readings a reasonable person could hold.
15
+ */
16
+ /** The functional spec the advisor looks at, and what it reads it with. */
17
+ export interface AdvisorSubject {
18
+ module: string;
19
+ funcId: string;
20
+ /** The functional spec's text. */
21
+ text: string;
22
+ /** The module's .plain file, whole: its definitions, its reqs, the functional specs around it. */
23
+ moduleText: string;
24
+ /** The project's own words on what matters: its AGENTS.md, the steering given in the conversation. */
25
+ context: string[];
26
+ /** A module below's: the tip its scenarios are tested in - built on it, its own functional specs beside it. */
27
+ testedIn?: {
28
+ module: string;
29
+ text: string;
30
+ };
31
+ /** Why the chat asked for the evaluation (evaluate_tests) - "the coding agent failed three times to implement it" - for the classifier, as context. */
32
+ reason?: string;
33
+ }
34
+ export interface AdvisorCallOptions {
35
+ /** Where the calls run: the project. */
36
+ cwd: string;
37
+ /** The advisor's model (advisorModel) - the `advisor` role's config is laid over it as for any agent. */
38
+ model?: string;
39
+ }
40
+ /** The classification: the verdict, the questions that were a yes with their evidence, and the divergence's input. */
41
+ export interface Classification {
42
+ verdict: "yes" | "no";
43
+ questions: AdvisorAnswer[];
44
+ divergence?: {
45
+ input: string;
46
+ readings: [string, string];
47
+ };
48
+ }
49
+ /** The fewest and most probes a functional spec is given. */
50
+ export declare const MIN_PROBES = 1;
51
+ export declare const MAX_PROBES = 10;
52
+ /** What a reader answers for an input the functional spec does not settle: a scenario on it would pin a guess. */
53
+ export declare const UNSPECIFIED = "unspecified";
54
+ /** The readers: the same task, decorrelated by their system prompts alone. */
55
+ export declare const READERS: ReadonlyArray<{
56
+ name: string;
57
+ system: string;
58
+ }>;
59
+ /** Text compared as the readers' outputs are: whitespace collapsed, case left aside. */
60
+ export declare function normalised(text: string): string;
61
+ type ClassificationAnswer = {
62
+ silent_failure: {
63
+ answer: boolean;
64
+ wrong_output?: string;
65
+ };
66
+ divergence: {
67
+ answer: boolean;
68
+ input?: string;
69
+ reading_a?: string;
70
+ reading_b?: string;
71
+ };
72
+ revisited: {
73
+ answer: boolean;
74
+ would_change?: string;
75
+ };
76
+ cost: {
77
+ answer: boolean;
78
+ consequence?: string;
79
+ };
80
+ };
81
+ /**
82
+ * The classification, from the call's answer: each yes kept only with its evidence - a yes without it, or with it
83
+ * malformed (two readings that are the same), is a no. The verdict is yes on silent failure, divergence or cost.
84
+ */
85
+ export declare function classificationOf(answer: ClassificationAnswer): Classification;
86
+ /** Classify a functional spec: one call, four evidence-backed questions. Throws when the call gives no answer. */
87
+ export declare function classify(subject: AdvisorSubject, options: AdvisorCallOptions): Promise<Classification>;
88
+ /**
89
+ * The inputs the readers split on - every probe where any two predictions differ once normalised: the union, never a
90
+ * majority - each with its distinct readings, in the order the readers gave them.
91
+ */
92
+ export declare function splitInputs(readings: string[][]): Map<number, string[]>;
93
+ /**
94
+ * Pick the inputs a functional spec's scenarios should cover: the probes, the readers' predictions of them, the
95
+ * disagreements in code, and the verifier's word on each. An input the readers agree on comes with their output; one
96
+ * they split on, with every reading the verifier kept - a single reading left is an agreed one, and none left drops it.
97
+ */
98
+ export declare function pickInputs(subject: AdvisorSubject, classification: Classification, options: AdvisorCallOptions): Promise<AdvisorInput[]>;
99
+ /** The whole judgement: classify, and on a yes pick the inputs. Throws when a call that cannot be done without fails. */
100
+ export declare function advise(subject: AdvisorSubject, options: AdvisorCallOptions): Promise<AdvisorReport>;
101
+ export {};