@turri-dev/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +319 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +75 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +118 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
@@ -0,0 +1,114 @@
1
+ import { dirname, join, resolve, sep } from "node:path";
2
+ import { readProjectConfig, stackOf } from "../config.js";
3
+ import { reportNote } from "./activity.js";
4
+ import { agentEnvironment, DARWIN_TEMP_SUFFIX, packageCacheDir, TOOLCHAIN_VARIABLES } from "./agent-env.js";
5
+ import { afterCommand, auditMessage, beforeCommand } from "./audit.js";
6
+ import { checkCommand, machineReadable } from "./command-check.js";
7
+ import { darwinUserDirs, machineFor, sandboxAvailable, sandboxWrap } from "./os-sandbox.js";
8
+ import { MODULES_DIR, projectRootOf } from "./paths.js";
9
+ import { RECORD_FILE } from "./records.js";
10
+ import { runCommand } from "./shell.js";
11
+ /**
12
+ * What a module's shell writes beside its roots: its home, its TMPDIR, the package caches - and on macOS its own folder
13
+ * in the per-user temporary folder, where its JVMs keep the attach sockets Mockito's inline mocks and jcmd go through.
14
+ */
15
+ export function writableFor(env, moduleDir) {
16
+ const darwinTemp = darwinUserDirs().temp;
17
+ const suffix = env[DARWIN_TEMP_SUFFIX];
18
+ return [env.HOME, env.TMPDIR, packageCacheDir(moduleDir), ...(darwinTemp !== undefined && suffix !== undefined ? [join(darwinTemp, suffix)] : [])];
19
+ }
20
+ /** The module a folder is part of: `plain_modules/<module>`, or - a build folder elsewhere - its parent. */
21
+ function moduleOf(dir) {
22
+ const parts = resolve(dir).split(sep);
23
+ const index = parts.lastIndexOf(MODULES_DIR);
24
+ return index >= 0 && parts.length > index + 1 ? parts.slice(0, index + 2).join(sep) : dirname(resolve(dir));
25
+ }
26
+ export function agentShell(role, options = {}) {
27
+ const workdir = resolve(role.workdir);
28
+ const read = (role.read ?? [workdir]).map((dir) => resolve(dir));
29
+ const write = (role.write ?? [workdir]).map((dir) => resolve(dir));
30
+ const moduleDir = moduleOf(workdir);
31
+ const project = projectRootOf(workdir);
32
+ const { env } = agentEnvironment(moduleDir);
33
+ const root = projectRootOf(moduleDir);
34
+ const toolchains = readProjectConfig(root, stackOf(root)).toolchains ?? [];
35
+ const machine = machineFor(env, TOOLCHAIN_VARIABLES, toolchains);
36
+ const writable = writableFor(env, moduleDir);
37
+ const commandFence = {
38
+ cwd: workdir,
39
+ read,
40
+ write,
41
+ home: env.HOME,
42
+ tmp: env.TMPDIR,
43
+ writable,
44
+ readable: [...machineReadable(), ...machine],
45
+ platform: process.platform === "win32" ? "win32" : "posix",
46
+ };
47
+ const sandboxFence = {
48
+ cwd: workdir,
49
+ read: read.filter((dir) => !write.includes(dir)),
50
+ write,
51
+ writable,
52
+ machine,
53
+ deny: ["code", "tests"].map((part) => join(moduleDir, part, RECORD_FILE)),
54
+ };
55
+ const wall = options.wall ?? sandboxAvailable();
56
+ const wrap = sandboxWrap(wall, sandboxFence);
57
+ const auditScope = { project, module: moduleDir, write, readOnly: read.filter((dir) => !write.includes(dir)), ...(options.busy !== undefined ? { busy: options.busy } : {}) };
58
+ const audited = async (work) => {
59
+ const before = beforeCommand(auditScope);
60
+ let value;
61
+ try {
62
+ value = await work();
63
+ }
64
+ catch (error) {
65
+ // Stopped or failed: what it did is put back all the same.
66
+ afterCommand(before);
67
+ throw error;
68
+ }
69
+ const result = afterCommand(before);
70
+ if (result.putBack.length > 0)
71
+ reportNote(` an agent's command changed ${result.putBack.join(", ")} - put back`);
72
+ if (result.left.length > 0)
73
+ reportNote(` an agent's command changed ${result.left.join(", ")}, where another pulse is at work - left`);
74
+ if (result.lost.length > 0)
75
+ reportNote(` an agent's command changed ${result.lost.join(", ")}, and it could not be put back`);
76
+ if (result.conversation.length > 0)
77
+ reportNote(` the conversation changed ${result.conversation.join(", ")} while an agent's command ran - left as it is`);
78
+ const note = auditMessage(result);
79
+ return { value, ...(note !== undefined ? { note } : {}) };
80
+ };
81
+ const description = wall.kind === "none"
82
+ ? `commands on this machine behind the command check and the audit - no OS sandbox: ${wall.why}`
83
+ : `commands on this machine behind the command check, ${wall.kind === "seatbelt" ? "Seatbelt" : "bubblewrap"} and the audit`;
84
+ return {
85
+ wall,
86
+ env,
87
+ description,
88
+ check: (command) => checkCommand(command, commandFence),
89
+ wrap,
90
+ async run(command, runOptions = {}) {
91
+ const checked = checkCommand(command, commandFence);
92
+ if (!checked.ok) {
93
+ reportNote(` refused an agent's command (${checked.rule}): ${command.length > 120 ? `${command.slice(0, 120)}...` : command}`);
94
+ const output = `Not run: ${checked.reason}`;
95
+ runOptions.onData?.(Buffer.from(output));
96
+ return { exitCode: 126, output };
97
+ }
98
+ const { value, note } = await audited(() => runCommand(command, {
99
+ cwd: workdir,
100
+ env,
101
+ wrap,
102
+ ...(runOptions.timeoutMs !== undefined ? { timeoutMs: runOptions.timeoutMs } : {}),
103
+ ...(runOptions.signal !== undefined ? { signal: runOptions.signal } : {}),
104
+ ...(runOptions.onData !== undefined ? { onData: runOptions.onData } : {}),
105
+ ...(runOptions.timeoutNote !== undefined ? { timeoutNote: runOptions.timeoutNote } : {}),
106
+ }));
107
+ if (note === undefined)
108
+ return value;
109
+ runOptions.onData?.(Buffer.from(`\n${note}\n`));
110
+ return { ...value, output: `${value.output}\n${note}\n` };
111
+ },
112
+ audited,
113
+ };
114
+ }
@@ -0,0 +1,133 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { ask, BEGIN, createExecutor } from "../pi.js";
4
+ import { confinedTools } from "./confine.js";
5
+ import { REVIEWER_CALLS } from "./conformance-agents.js";
6
+ /**
7
+ * The arbiter: the third voice in a review. The coding agent writes the tests; the reviewer - exacting,
8
+ * opinionated, hard to satisfy - sends them back with everything it finds; after the first review that sends
9
+ * them back, the arbiter weighs what each one found before any of it goes back. It is the distinguished engineer
10
+ * who balances the trade-offs: the tests have to be good, and time lost chasing perfect ones is lost too. It
11
+ * upholds a problem when the reviewer is right - a behaviour the spec states is untested, or a test cannot
12
+ * fail - and dismisses it when it is out of scope or a nitpick. Only what it upholds goes back; when it upholds
13
+ * nothing, the tests are approved as they are. Read-only, like the reviewer.
14
+ */
15
+ /**
16
+ * How many reviews that send the work back go to the agent as they are: every one after them the arbiter weighs
17
+ * first, and only what it upholds goes back.
18
+ */
19
+ export const ARBITER_AFTER = 1;
20
+ /**
21
+ * How many reviews at most. What the last one sends back - weighed by the arbiter - goes to the agent once more, and
22
+ * then the work goes on without another review, once its tests pass.
23
+ */
24
+ export const REVIEW_LIMIT = 2;
25
+ export function problemLine(problem) {
26
+ return `${problem.blocking === true ? "BLOCKING · " : ""}${problem.kind.toUpperCase()} · ${problem.file}${problem.test ? ` · ${problem.test}` : ""}: ${problem.what.trim()}`;
27
+ }
28
+ /** The arbiter's prompt: the spec, the reviews so far, and the latest one's problems, numbered. */
29
+ export function arbiterPrompt(options) {
30
+ return [
31
+ `You are the arbiter of ${options.subject}: the distinguished engineer who has the last word on their review.`,
32
+ `The tests pass. A reviewer - exacting, opinionated, hard to satisfy - has ${options.history.length === 0 ? "sent the work back" : `sent the work back ${options.history.length === 1 ? "once" : `${options.history.length} times`}, and has sent it back again`}. Weigh what it found, as the engineer who balances`,
33
+ "the trade-offs: the work has to be right and its tests good, and time lost chasing perfect ones is lost too.",
34
+ "",
35
+ `The tests and the code are in ${options.read.join(" and ")}.`,
36
+ "",
37
+ "For each problem the reviewer raised this time, rule:",
38
+ ...options.uphold,
39
+ "- dismiss it when it is out of scope - something the specification does not state for this work, a page or a",
40
+ " component only a definition names, how the code works inside rather than what it does - or a nitpick: a",
41
+ " test that already tests the behaviour, made a little stronger, another case of one already covered, a",
42
+ " matter of style, code written another way than the reviewer would have that does what the specification",
43
+ " says. Dismissed, it is let go.",
44
+ "When you uphold nothing, the work is approved as it is.",
45
+ "",
46
+ ...(options.history.length > 0
47
+ ? [
48
+ "<the reviews before, oldest first - what each sent back, and the tests were worked on since>",
49
+ ...options.history.map((problems, index) => `review ${index + 1}:\n${problems.map((problem) => ` - ${problemLine(problem)}`).join("\n")}`),
50
+ "</the reviews before>",
51
+ "",
52
+ ]
53
+ : []),
54
+ "<the problems it raised now>",
55
+ ...options.problems.map((problem, index) => `${index + 1}. ${problemLine(problem)}`),
56
+ "</the problems it raised now>",
57
+ "",
58
+ "<tests added or changed>",
59
+ options.changed.length > 0 ? options.changed.join("\n") : "(none)",
60
+ "</tests added or changed>",
61
+ "",
62
+ options.spec.trim(),
63
+ "",
64
+ ...(options.code !== undefined
65
+ ? [
66
+ "<the code this work wrote - a diff from where the work started, new files whole>",
67
+ options.code.trim() !== "" ? options.code.trim() : "(nothing)",
68
+ "</the code this work wrote>",
69
+ "",
70
+ ]
71
+ : []),
72
+ "Then call submit_ruling once, with every problem by its number - upheld, or dismissed - and in a line, why.",
73
+ ].join("\n");
74
+ }
75
+ /** The arbiter's ruling on the latest review - or nothing, when it gave none: the review then stands. */
76
+ export async function arbitrateReview(options) {
77
+ let ruling;
78
+ const submit = defineTool({
79
+ name: "submit_ruling",
80
+ label: "Submit the ruling",
81
+ description: "Rule on every problem the reviewer raised, by its number: upheld, or dismissed, each with why. Call this once.",
82
+ parameters: Type.Object({
83
+ rulings: Type.Array(Type.Object({
84
+ problem: Type.Number({ description: "The problem's number" }),
85
+ uphold: Type.Boolean({ description: "True: the reviewer is right, the tests are fixed for it. False: dismissed, let go" }),
86
+ why: Type.String({ description: "In one line, why" }),
87
+ })),
88
+ }),
89
+ async execute(_id, params) {
90
+ const given = params.rulings;
91
+ const numbers = new Set(given.map((entry) => entry.problem));
92
+ const missing = options.problems.map((_, index) => index + 1).filter((number) => !numbers.has(number));
93
+ if (missing.length > 0) {
94
+ return { content: [{ type: "text", text: `Rule on every problem: ${missing.join(", ")} ${missing.length === 1 ? "has" : "have"} no ruling.` }], details: {} };
95
+ }
96
+ const ruled = given
97
+ .filter((entry) => options.problems[entry.problem - 1] !== undefined)
98
+ .map((entry) => ({ problem: options.problems[entry.problem - 1], why: entry.why, uphold: entry.uphold }));
99
+ ruling = {
100
+ upheld: ruled.filter((entry) => entry.uphold).map(({ problem, why }) => ({ problem, why })),
101
+ dismissed: ruled.filter((entry) => !entry.uphold).map(({ problem, why }) => ({ problem, why })),
102
+ };
103
+ return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
104
+ },
105
+ });
106
+ const fence = confinedTools({ workdir: options.workdir, read: options.read, write: [], noBash: true });
107
+ const session = await createExecutor({
108
+ cwd: options.workdir,
109
+ customTools: [submit, ...fence.tools],
110
+ tools: [...fence.names, "submit_ruling"],
111
+ model: options.model,
112
+ label: `${options.label} arbiter`, role: "arbiter",
113
+ system: fence.note,
114
+ task: arbiterPrompt(options),
115
+ budget: { calls: REVIEWER_CALLS, finish: ["submit_ruling"] },
116
+ });
117
+ try {
118
+ await ask(session, BEGIN, options.onDelta);
119
+ if (ruling === undefined)
120
+ await ask(session, "You have not called submit_ruling. Call it now, with every problem by its number.", options.onDelta);
121
+ }
122
+ finally {
123
+ session.dispose();
124
+ }
125
+ return ruling;
126
+ }
127
+ /** What the arbiter let go, for the working notes' pitfalls. */
128
+ export function dismissedNote(ruling, what) {
129
+ return `the arbiter let go what the ${what} review raised: ${ruling.dismissed
130
+ .map((entry) => `${entry.problem.file}${entry.problem.test ? ` ${entry.problem.test}` : ""}: ${entry.problem.what.trim()} - ${entry.why.trim()}`)
131
+ .join("; ")
132
+ .slice(0, 600)}`;
133
+ }
@@ -0,0 +1,203 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, relative, sep } from "node:path";
4
+ import { writtenByConversation } from "./conversation-writes.js";
5
+ import { isCredentialFile } from "./credentials.js";
6
+ import { RECORD_FILE } from "./records.js";
7
+ /** Folders never looked in: dependencies, build output, caches, histories, turri's scratch. */
8
+ const SKIPPED = new Set([
9
+ "node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", "target", "build",
10
+ "dist", ".gradle", ".m2", ".cargo", ".next", "coverage", ".git", ".turri", ".plaingit", ".idea", ".vscode",
11
+ ]);
12
+ /** A file kept whole before the command - at most this big. */
13
+ const MAX_KEPT = 1024 * 1024;
14
+ function stat(path) {
15
+ try {
16
+ const found = statSync(path);
17
+ return found.isFile() ? { size: found.size, mtimeMs: found.mtimeMs } : undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ /** Every file under `root` that `wanted` takes, skipping the folders that are never looked in. */
24
+ function walk(root, wanted, skip = () => false) {
25
+ const files = new Map();
26
+ const visit = (dir) => {
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(dir);
30
+ }
31
+ catch {
32
+ return;
33
+ }
34
+ for (const name of entries) {
35
+ const path = join(dir, name);
36
+ let found;
37
+ try {
38
+ found = statSync(path);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ if (found.isDirectory()) {
44
+ if (!SKIPPED.has(name) && !skip(path))
45
+ visit(path);
46
+ }
47
+ else if (found.isFile() && wanted(path)) {
48
+ files.set(path, { size: found.size, mtimeMs: found.mtimeMs });
49
+ }
50
+ }
51
+ };
52
+ if (existsSync(root))
53
+ visit(root);
54
+ return files;
55
+ }
56
+ function keep(files) {
57
+ const kept = new Map();
58
+ for (const [path, seen] of files) {
59
+ if (seen.size > MAX_KEPT)
60
+ continue;
61
+ try {
62
+ kept.set(path, readFileSync(path));
63
+ }
64
+ catch {
65
+ /* unreadable: nothing to put back from */
66
+ }
67
+ }
68
+ return kept;
69
+ }
70
+ function within(path, root) {
71
+ return path === root || path.startsWith(root + sep);
72
+ }
73
+ /** What the project's own files are that no agent changes: specs, config, testing scripts, credentials. */
74
+ function projectFile(project, path) {
75
+ const name = basename(path);
76
+ if (isCredentialFile(path))
77
+ return true;
78
+ if (name.endsWith(".plain"))
79
+ return true;
80
+ if (dirname(path) === project && /^config(\.[^.]+)?\.ya?ml$/.test(name))
81
+ return true;
82
+ if (relative(project, path).split(sep)[0] === "test_scripts")
83
+ return true;
84
+ return dirname(path) === project && /^(run_|prepare_environment).*\.sh$/.test(name);
85
+ }
86
+ /** Take what the audit compares against, before a command runs. */
87
+ export function beforeCommand(scope) {
88
+ const at = Date.now();
89
+ const modulesRoot = join(scope.project, "plain_modules");
90
+ const trees = [];
91
+ // The project's own files, outside plain_modules.
92
+ const own = walk(scope.project, (path) => projectFile(scope.project, path), (dir) => dir === modulesRoot);
93
+ trees.push({ root: scope.project, files: own, kept: keep(own), from: "copy" });
94
+ // In what the role writes: only the record and credential files are watched.
95
+ for (const root of scope.write) {
96
+ const watched = walk(root, (path) => basename(path) === RECORD_FILE || isCredentialFile(path));
97
+ trees.push({ root, files: watched, kept: keep(watched), from: "copy", watchOnly: true });
98
+ }
99
+ // What the role only reads, and the parts of its own module it does not write at all: all of it, from a copy - they may
100
+ // hold this pulse's work, not yet committed.
101
+ const ownParts = ["code", "tests"].map((part) => join(scope.module, part)).filter((part) => existsSync(part) && !scope.write.includes(part) && !scope.readOnly.includes(part));
102
+ for (const root of [...scope.readOnly, ...ownParts]) {
103
+ const all = walk(root, () => true);
104
+ trees.push({ root, files: all, kept: keep(all), from: "copy" });
105
+ }
106
+ // The module's lineage: its refs and settings - the objects are only ever added to.
107
+ const lineage = join(scope.module, ".plaingit");
108
+ const refs = walk(lineage, (path) => !within(path, join(lineage, "objects")) && !within(path, join(lineage, "logs")));
109
+ trees.push({ root: lineage, files: refs, kept: keep(refs), from: "copy" });
110
+ // The other modules, put back from their last commit.
111
+ if (existsSync(modulesRoot)) {
112
+ for (const name of readdirSync(modulesRoot)) {
113
+ const other = join(modulesRoot, name);
114
+ if (name.startsWith(".") || other === scope.module || !existsSync(join(other, ".plaingit")))
115
+ continue;
116
+ const files = walk(other, () => true);
117
+ trees.push({ root: other, files, from: { lineage: other }, busy: scope.busy?.(other) === true });
118
+ }
119
+ }
120
+ return { scope, trees, at };
121
+ }
122
+ /** The file's last committed content in a module's lineage, or nothing when it was not committed. */
123
+ function committed(moduleDir, path) {
124
+ const rel = relative(moduleDir, path).split(sep).join("/");
125
+ const run = spawnSync("git", ["--git-dir", join(moduleDir, ".plaingit"), "show", `HEAD:${rel}`], { maxBuffer: 256 * 1024 * 1024 });
126
+ return run.status === 0 ? run.stdout : undefined;
127
+ }
128
+ function put(path, content) {
129
+ mkdirSync(dirname(path), { recursive: true });
130
+ writeFileSync(path, content);
131
+ }
132
+ /** After the command: put back what it changed that its role may not, and say what. */
133
+ export function afterCommand(snapshot) {
134
+ const { scope } = snapshot;
135
+ const result = { putBack: [], left: [], lost: [], conversation: [] };
136
+ const shown = (path) => relative(scope.project, path).split(sep).join("/");
137
+ const modulesRoot = join(scope.project, "plain_modules");
138
+ for (const tree of snapshot.trees) {
139
+ const isLineage = tree.root === join(scope.module, ".plaingit");
140
+ const after = tree.root === scope.project
141
+ ? walk(scope.project, (path) => projectFile(scope.project, path), (dir) => dir === modulesRoot)
142
+ : tree.watchOnly === true
143
+ ? walk(tree.root, (path) => basename(path) === RECORD_FILE || isCredentialFile(path))
144
+ : isLineage
145
+ ? walk(tree.root, (path) => !within(path, join(tree.root, "objects")) && !within(path, join(tree.root, "logs")))
146
+ : walk(tree.root, () => true);
147
+ const changed = [];
148
+ for (const [path, seen] of after) {
149
+ const was = tree.files.get(path);
150
+ if (was === undefined)
151
+ changed.push(path);
152
+ else if (was.size !== seen.size || was.mtimeMs !== seen.mtimeMs) {
153
+ const kept = tree.kept?.get(path);
154
+ // Touched but the same: nothing to put back.
155
+ if (kept !== undefined && readFileSync(path).equals(kept))
156
+ continue;
157
+ changed.push(path);
158
+ }
159
+ }
160
+ for (const path of tree.files.keys())
161
+ if (!after.has(path))
162
+ changed.push(path);
163
+ // What the conversation wrote meanwhile is its own: left as it is.
164
+ const mine = changed.filter((path) => writtenByConversation(path, snapshot.at));
165
+ result.conversation.push(...mine.map(shown));
166
+ changed.splice(0, changed.length, ...changed.filter((path) => !mine.includes(path)));
167
+ if (changed.length === 0)
168
+ continue;
169
+ if (tree.busy === true) {
170
+ result.left.push(...changed.map(shown));
171
+ continue;
172
+ }
173
+ for (const path of changed) {
174
+ const existed = tree.files.has(path);
175
+ if (!existed) {
176
+ rmSync(path, { force: true });
177
+ result.putBack.push(shown(path));
178
+ continue;
179
+ }
180
+ const content = tree.from === "copy" ? tree.kept?.get(path) : committed(tree.from.lineage, path);
181
+ if (content === undefined) {
182
+ result.lost.push(shown(path));
183
+ continue;
184
+ }
185
+ put(path, content);
186
+ result.putBack.push(shown(path));
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+ /** What the agent is told in its command's result, or nothing when the audit put nothing back. */
192
+ export function auditMessage(result) {
193
+ if (result.putBack.length === 0 && result.lost.length === 0)
194
+ return undefined;
195
+ const list = (paths) => (paths.length > 10 ? `${paths.slice(0, 10).join(", ")} and ${paths.length - 10} more` : paths.join(", "));
196
+ const parts = [];
197
+ if (result.putBack.length > 0)
198
+ parts.push(`turri put back ${list(result.putBack)} as it was before this command`);
199
+ if (result.lost.length > 0)
200
+ parts.push(`${list(result.lost)} changed and could not be put back`);
201
+ return (`[${parts.join("; ")}: outside what you may change. Your own work is kept. What you need from there is in your ` +
202
+ "prompt; leave it as it is.]");
203
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * What a job's agents are told to do, by its cases - see docs/regenerating.md. A job's functional specs each fall in
3
+ * one case - fresh, added, changed (patched), regenerated again, taken out - or, with none touched, the job is
4
+ * mechanical. The coding agent and the conformance agent are each told, case by case, exactly what to do with the
5
+ * functional specs of that case; their reviewers, what to hold each to. A job holding several cases gets a section
6
+ * for each.
7
+ */
8
+ /** Functional specs, each by its number and - each module numbering its own from 1 - the module it belongs to. */
9
+ const named = (specs) => specs.map((spec) => `fs ${spec.frid}${spec.module !== undefined ? ` of module \`${spec.module}\`` : ""} (${spec.text})`).join("; ");
10
+ /** What the coding agent does, case by case. */
11
+ export function codeCasesBlock(cases) {
12
+ const lines = [];
13
+ if (cases.fresh) {
14
+ lines.push("- Nothing is built yet: set the project up first - its layout, its dependency manifest, and its unit tests", " where the unit testing script runs them - then build what follows, with its unit tests.");
15
+ }
16
+ if (cases.add.length > 0)
17
+ lines.push(`- New - build each, and write its unit tests: ${named(cases.add)}.`);
18
+ if (cases.patch.length > 0) {
19
+ lines.push(`- Changed - its text, or a concept it uses: ${named(cases.patch)}.`, " Its code and unit tests exist: change them to match what it says now. Do not build it again from scratch.");
20
+ }
21
+ if (cases.again.length > 0) {
22
+ lines.push(`- Regenerated again - its code and unit tests are built, and are to be regenerated again: ${named(cases.again)}.`, " Replace its implementation: rewrite it, or take it out and build it anew - made better, and doing exactly what", " the specification says now. Take out what the new code replaces: no second implementation beside the old one,", " nothing of the old left unused. What the rest of the code uses from it keeps working. Its unit tests may be", " rewritten, to test what the specification says.");
23
+ }
24
+ if (cases.delete.length > 0) {
25
+ lines.push(`- Taken out - remove its code, and the unit tests only it had, where nothing that remains needs them: ${named(cases.delete)}.`);
26
+ }
27
+ if (cases.mechanical) {
28
+ lines.push("- No functional spec is touched: the change is to implementation reqs, or definitions no functional spec uses.", " Adapt the code to it. Write no new tests. An existing test may change only mechanically - an import, a", " fixture, its setup - never what it asserts. Every test must still pass.");
29
+ }
30
+ return lines;
31
+ }
32
+ /** What the unit reviewer holds the work to, case by case - in place of its checks, when the job is mechanical. */
33
+ export function codeReviewCasesBlock(cases) {
34
+ if (cases.mechanical) {
35
+ return [
36
+ "This work touches no functional spec - only implementation reqs, or definitions none uses. Review only that",
37
+ "the code keeps to them and still does what it did, and that no test was removed or weakened: an assertion",
38
+ "loosened or taken out, a case dropped, a test skipped or deleted. A test may change mechanically - an import, a",
39
+ "fixture, its setup. Raise no missing test: this work adds no behaviour to test.",
40
+ ];
41
+ }
42
+ return [
43
+ "What this work does, by functional spec - hold each part to its own:",
44
+ ...(cases.add.length > 0 ? [`- New: ${named(cases.add)} - implemented, and tested as below.`] : []),
45
+ ...(cases.patch.length > 0 ? [`- Changed: ${named(cases.patch)} - the code and its tests changed to match it now, not left as they were.`] : []),
46
+ ...(cases.again.length > 0
47
+ ? [
48
+ `- Regenerated again: ${named(cases.again)} - its implementation replaced, not patched beside the old one: nothing of what it`,
49
+ " replaced left behind or duplicated, it does what the specification says, what the rest of the code uses from it",
50
+ " still works, and its unit tests test the specification.",
51
+ ]
52
+ : []),
53
+ ...(cases.delete.length > 0 ? [`- Taken out: ${named(cases.delete)} - its code and the tests only it had removed, and nothing else.`] : []),
54
+ ];
55
+ }
56
+ /** What the conformance agent does, case by case. */
57
+ export function conformanceCasesBlock(cases) {
58
+ const lines = [];
59
+ if (cases.fresh) {
60
+ lines.push("- There are no conformance tests yet: set the tests folder up first - the tests' own build and layout, so the", " conformance testing script, which brings the tests together with the code, runs them - then write what follows.");
61
+ }
62
+ if (cases.add.length > 0)
63
+ lines.push(`- New - write its conformance tests: ${named(cases.add)}.`);
64
+ if (cases.patch.length > 0) {
65
+ lines.push(`- Changed - its text, its acceptance tests, or a concept it uses: ${named(cases.patch)}.`, " Its conformance tests exist: rewrite them to what it says now. The tests of everything else stay as they are.");
66
+ }
67
+ if (cases.delete.length > 0)
68
+ lines.push(`- Taken out - remove its conformance tests: ${named(cases.delete)}.`);
69
+ if (cases.mechanical) {
70
+ lines.push("- No functional spec is touched: the change is to the test reqs, or to definitions or implementation reqs alone.", " Adapt the existing tests to it. Write no new tests. A test may change only mechanically - how it is built,", " started or reaches the software - never what it asserts. Every test must still pass.");
71
+ }
72
+ return lines;
73
+ }
74
+ /** What the conformance test reviewer holds the tests to, when the job is mechanical: in place of its checks. */
75
+ export function conformanceReviewCasesBlock(cases) {
76
+ if (!cases.mechanical)
77
+ return [];
78
+ return [
79
+ "This work touches no functional spec - only the test reqs, or definitions or implementation reqs alone. Review",
80
+ "only that the tests follow them, and that no test was removed or weakened: an assertion loosened or taken out, a",
81
+ "case dropped, a test skipped or deleted. A test may change mechanically. Raise no missing test.",
82
+ ];
83
+ }
@@ -0,0 +1,102 @@
1
+ import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS } from "../plain/spec.js";
2
+ import { resourceDiff } from "./spec-steps.js";
3
+ /**
4
+ * A job's change as its agent is given it, for the panels: the same items `describeSpecChange` writes into
5
+ * `<spec change>` - what the job's functional specs use, from the module's own file and from the files it
6
+ * imports and requires alike - laid out as a diff the panels already show (`diff-view.ts`). Each section is
7
+ * a block under its header, each item as its file writes it - its markdown, its acceptance tests indented
8
+ * under it - so placed in the .plain files (`locateDiffLines`) every line is shown in the file it is written
9
+ * in, with its line there: split by file and by block. A text diff of the module's file would show other
10
+ * things - the lines around a change, the items no job takes yet - and leave out what an import adds.
11
+ */
12
+ /** The sections in the order the agent is given them, then any other. */
13
+ const ORDER = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS];
14
+ /** An item's lines as a .plain file writes them: its markdown, then its acceptance tests indented under it. */
15
+ function itemLines(serialized) {
16
+ if (serialized === undefined)
17
+ return [];
18
+ let item;
19
+ try {
20
+ item = JSON.parse(serialized);
21
+ }
22
+ catch {
23
+ return serialized.split("\n");
24
+ }
25
+ if (typeof item === "string")
26
+ return item.split("\n");
27
+ const { markdown, [ACCEPTANCE_TESTS]: tests } = (item ?? {});
28
+ const lines = (markdown ?? JSON.stringify(item)).split("\n");
29
+ if (Array.isArray(tests) && tests.length > 0) {
30
+ lines.push("", " ***acceptance tests***", "");
31
+ for (const test of tests) {
32
+ const text = typeof test === "string" ? test : (test.markdown ?? JSON.stringify(test));
33
+ lines.push(...text.split("\n").map((line) => ` ${line}`));
34
+ }
35
+ }
36
+ return lines;
37
+ }
38
+ /**
39
+ * `changes` as a diff of `file` - the module's .plain file, which the diff names - section by section: a removed
40
+ * item's lines taken out, an added one's put in, a changed one's both. A change to what a linked file holds
41
+ * follows, as the step's own diff has it.
42
+ */
43
+ export function changeDiff(changes, file) {
44
+ return laidOut(changes, file).diff;
45
+ }
46
+ /**
47
+ * `changeDiff`, each section's items in the order they are written - by file, then by line - rather than in the
48
+ * order the spec is read in, which puts a concept after those it uses: placed by `locate`, as the panels place it.
49
+ */
50
+ export function changeDiffInFiles(changes, file, locate) {
51
+ const first = laidOut(changes, file);
52
+ const where = locate(first.diff);
53
+ const placeOf = (change) => {
54
+ const at = where[first.starts.get(change) ?? -1];
55
+ return at === undefined ? undefined : { file: at.file, line: at.line };
56
+ };
57
+ const ordered = [...changes].sort((a, b) => {
58
+ // Each section is laid out on its own: between two, any order that holds will do.
59
+ if (a.section !== b.section)
60
+ return a.section < b.section ? -1 : 1;
61
+ const [x, y] = [placeOf(a), placeOf(b)];
62
+ if (x === undefined || y === undefined)
63
+ return a.position - b.position;
64
+ return x.file === y.file ? x.line - y.line : (x.file === file ? -1 : y.file === file ? 1 : x.file.localeCompare(y.file));
65
+ });
66
+ const diff = laidOut(ordered, file, true).diff;
67
+ return { diff, where: locate(diff) };
68
+ }
69
+ /** The diff, and the line each change starts on in it. `asGiven`: each section's items in the order given. */
70
+ function laidOut(changes, file, asGiven = false) {
71
+ const starts = new Map();
72
+ const sections = [...new Set(changes.map((change) => change.section))].sort((a, b) => (ORDER.indexOf(a) === -1 ? ORDER.length : ORDER.indexOf(a)) - (ORDER.indexOf(b) === -1 ? ORDER.length : ORDER.indexOf(b)));
73
+ const lines = [`diff --git a/${file} b/${file}`, `--- a/${file}`, `+++ b/${file}`];
74
+ for (const section of sections) {
75
+ const own = changes.filter((change) => change.section === section);
76
+ if (!asGiven)
77
+ own.sort((a, b) => a.position - b.position);
78
+ const body = [];
79
+ // Where this section's lines start in the diff: after the lines so far, its hunk header and its section header.
80
+ const offset = lines.length + 2;
81
+ for (const change of own) {
82
+ const before = change.kind === "add" ? [] : itemLines(change.before);
83
+ const after = change.kind === "remove" ? [] : itemLines(change.after);
84
+ // Where it is found: its first line now, or - removed - its first line as it was.
85
+ starts.set(change, offset + body.length + (change.kind === "change" && after.length > 0 ? before.length : 0));
86
+ // Only what a file it links holds changed: the item as it is, the file's change after the spec's.
87
+ if (change.kind === "change" && before.join("\n") === after.join("\n")) {
88
+ body.push(...after.map((line) => ` ${line}`));
89
+ continue;
90
+ }
91
+ body.push(...before.map((line) => `-${line}`), ...after.map((line) => `+${line}`));
92
+ }
93
+ if (body.length === 0)
94
+ continue;
95
+ lines.push("@@ @@", ` ***${section}***`, ...body);
96
+ }
97
+ return { diff: `${lines.join("\n")}\n${resourceDiff(changes, file)}`, starts };
98
+ }
99
+ /** The changes a job's agent works from: a code job's leave out the test reqs, which are the conformance job's. */
100
+ export function agentChanges(changes, track) {
101
+ return track === "conformance" ? changes : changes.filter((change) => change.section !== TEST_REQUIREMENTS);
102
+ }