@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +320 -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 +88 -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 +119 -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,561 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
+ import { withParseCache } from "../plain/parser.js";
4
+ import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS, } from "../plain/spec.js";
5
+ import { configForSpec, conformanceSwitchedOn, readProjectConfig } from "../config.js";
6
+ import { STDLIB_ROOT } from "../plain/stdlib.js";
7
+ import { conformanceDirFor } from "./conformance.js";
8
+ import { chainSpec } from "./loop.js";
9
+ import { modulesRoot, projectDirOf } from "./paths.js";
10
+ import { HISTORY_DIR } from "./lineage.js";
11
+ import { codeTree, readRecord, RECORD_FILE } from "./records.js";
12
+ import { specFilesOf } from "./regenerate.js";
13
+ import { builtTips, resolveRequiresChain } from "./requires.js";
14
+ import { changesBetween, layoutOf, serializeTree } from "./spec-steps.js";
15
+ const itemsOf = (tree, heading) => (tree?.[heading] ?? []).slice();
16
+ /** The sections an item of the code is in, as a page has them: a module below's functional specs among them. */
17
+ const CODE_SECTIONS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS];
18
+ /** A module's part of a codebase's tree: its own items untagged when it is the codebase, else those tagged with it. */
19
+ function moduleTree(tree, module, own) {
20
+ const mine = (item) => (own ? item.module === undefined : item.module === module);
21
+ return {
22
+ [DEFINITIONS]: itemsOf(tree, DEFINITIONS).filter(mine),
23
+ [NON_FUNCTIONAL_REQUIREMENTS]: itemsOf(tree, NON_FUNCTIONAL_REQUIREMENTS).filter(mine),
24
+ [FUNCTIONAL_REQUIREMENTS]: own ? itemsOf(tree, FUNCTIONAL_REQUIREMENTS) : itemsOf(tree, REQUIRED_FUNCTIONAL_SPECS).filter((item) => item.module === module),
25
+ };
26
+ }
27
+ /** The changes between two serialized trees - git runs for each, so the last ones are kept. */
28
+ const changesSeen = new Map();
29
+ function changesOf(before, after) {
30
+ const key = `${before}\u0000${after}`;
31
+ const seen = changesSeen.get(key);
32
+ if (seen !== undefined)
33
+ return seen;
34
+ const changes = changesBetween(before, after);
35
+ if (changesSeen.size > 64)
36
+ changesSeen.clear();
37
+ changesSeen.set(key, changes);
38
+ return changes;
39
+ }
40
+ const markdownOf = (line) => {
41
+ try {
42
+ return JSON.parse(line ?? "").markdown ?? "";
43
+ }
44
+ catch {
45
+ return "";
46
+ }
47
+ };
48
+ /**
49
+ * A module's items, from what its code is built from and what its spec says now: every item of the spec in its place,
50
+ * each built, new or changed, and between them the items taken out that the code still has.
51
+ *
52
+ * `spec` and `record` are the codebase's trees - the module's items untagged when `own`, tagged with it otherwise. `full`
53
+ * is the spec with its acceptance tests and test reqs.
54
+ */
55
+ export function pageItems(input) {
56
+ const now = moduleTree(input.spec, input.module, input.own);
57
+ const before = serializeTree(moduleTree(input.record === null ? null : codeTree(input.record), input.module, input.own));
58
+ const after = serializeTree(codeTree(now));
59
+ const changes = changesOf(before, after);
60
+ // Each item's acceptance tests, by its line in the code's tree.
61
+ const acceptance = new Map();
62
+ for (const item of itemsOf(now, FUNCTIONAL_REQUIREMENTS)) {
63
+ const line = serializeTree(codeTree({ [FUNCTIONAL_REQUIREMENTS]: [item] })).split("\n").find((entry) => entry.startsWith("{"));
64
+ if (line !== undefined)
65
+ acceptance.set(line, (item[ACCEPTANCE_TESTS] ?? []).map((test) => test.markdown ?? ""));
66
+ }
67
+ // The record's lines, with what is added in front of each and what becomes of each: the spec's order, the ghosts in theirs.
68
+ const lines = before.split("\n").slice(0, -1);
69
+ const removedBy = new Map();
70
+ const addedAt = new Map();
71
+ for (const change of changes) {
72
+ if (change.removes !== undefined)
73
+ removedBy.set(change.removes, change);
74
+ if (change.kind === "add" && change.inserts !== undefined)
75
+ addedAt.set(change.inserts.at, [...(addedAt.get(change.inserts.at) ?? []), change]);
76
+ }
77
+ const items = [];
78
+ const push = (section, state, line, wasLine) => {
79
+ const text = markdownOf(line ?? wasLine);
80
+ items.push({
81
+ key: "",
82
+ section,
83
+ state,
84
+ text,
85
+ ...(state === "changed" ? { was: markdownOf(wasLine) } : {}),
86
+ acceptance: line !== undefined ? (acceptance.get(line) ?? []) : [],
87
+ ...(line !== undefined ? { line } : {}),
88
+ ...(wasLine !== undefined ? { wasLine } : {}),
89
+ });
90
+ };
91
+ let section = "";
92
+ for (let at = 0; at <= lines.length; at += 1) {
93
+ for (const change of (addedAt.get(at) ?? []).sort((a, b) => a.inserts.order - b.inserts.order))
94
+ push(change.section, "new", change.after, undefined);
95
+ if (at === lines.length)
96
+ break;
97
+ const line = lines[at];
98
+ if (line.startsWith("[")) {
99
+ section = JSON.parse(line)[0];
100
+ continue;
101
+ }
102
+ const change = removedBy.get(at);
103
+ if (change?.kind === "change")
104
+ push(section, "changed", change.after, line);
105
+ else if (change?.kind === "remove")
106
+ push(section, "removed", undefined, line);
107
+ else
108
+ push(section, "built", line, undefined);
109
+ }
110
+ // Test reqs are the conformance tests', never the code's: written for, as the tests record has them, or not yet.
111
+ const testReqs = new Set(itemsOf(input.tested ?? null, TEST_REQUIREMENTS).map((item) => item.markdown.trim()));
112
+ if (input.own) {
113
+ for (const item of itemsOf(input.spec, TEST_REQUIREMENTS)) {
114
+ items.push({ key: "", section: TEST_REQUIREMENTS, state: "test", text: item.markdown, acceptance: [], ...(testReqs.has(item.markdown.trim()) ? { tests: "now" } : {}) });
115
+ }
116
+ }
117
+ // Conformance tests: of the functional specs, those the tests record has as they read now, as they read before, or taken out.
118
+ const tested = new Set(itemsOf(input.tested ?? null, FUNCTIONAL_REQUIREMENTS).map((item) => item.markdown.trim()));
119
+ for (const item of items) {
120
+ if (item.section !== FUNCTIONAL_REQUIREMENTS || tested.size === 0)
121
+ continue;
122
+ if (item.state === "removed") {
123
+ if (tested.has(item.text.trim()))
124
+ item.tests = "removed";
125
+ }
126
+ else if (tested.has(item.text.trim()))
127
+ item.tests = "now";
128
+ else if (item.was !== undefined && tested.has(item.was.trim()))
129
+ item.tests = "older";
130
+ }
131
+ return keyed(items);
132
+ }
133
+ /** Each item's key: its section, whether it is a ghost, its text - and which of the same it is. */
134
+ function keyed(items) {
135
+ const seen = new Map();
136
+ for (const item of items) {
137
+ const base = `${item.section}\n${item.state === "removed" ? "-" : "+"}${item.text.trim()}`;
138
+ const count = seen.get(base) ?? 0;
139
+ seen.set(base, count + 1);
140
+ item.key = count === 0 ? base : `${base}\n${count}`;
141
+ }
142
+ return items;
143
+ }
144
+ /** A spec file as the map names it: a template of turri's standard library by its import path, others from the project. */
145
+ export function importLabel(path, projectDir, stdlibRoot = STDLIB_ROOT) {
146
+ const name = path.startsWith(`${stdlibRoot}/stdlib/`) ? relative(stdlibRoot, path) : relative(projectDir, path) || basename(path);
147
+ return name.replace(/\.plain$/, "");
148
+ }
149
+ /**
150
+ * The items laid out as the files have them: the module's own file first - its sections in its order, its definitions
151
+ * in its order - then each file it imports, the same way. A ghost keeps its place after the item before it.
152
+ */
153
+ export function layOut(items, files) {
154
+ const firstLine = (text) => text.trim().split("\n")[0].trim();
155
+ // Each item's file: the first that has its first line - else the module's own, where a ghost was.
156
+ const fileOf = (item) => {
157
+ const line = firstLine(item.text);
158
+ const at = files.findIndex((file) => file.text.includes(line));
159
+ return at === -1 ? 0 : at;
160
+ };
161
+ const layouts = files.map((file) => layoutOf(file.text));
162
+ const placed = items.map((item, index) => ({ item, index, file: item.state === "removed" ? -1 : fileOf(item) }));
163
+ // A ghost goes in the file of the item before it.
164
+ placed.forEach((entry, at) => {
165
+ if (entry.file === -1)
166
+ entry.file = at > 0 ? placed[at - 1].file : 0;
167
+ });
168
+ // Within a file, sections in its order; definitions by their concept's place, a ghost just after the one before it.
169
+ let lastRank = -1;
170
+ const rank = placed.map((entry) => {
171
+ if (entry.item.section !== DEFINITIONS)
172
+ return 0;
173
+ const concept = /^- :([^:\s]+):/.exec(entry.item.text.trim())?.[1];
174
+ const found = concept !== undefined && entry.item.state !== "removed" ? layouts[entry.file].concepts.get(concept) : undefined;
175
+ lastRank = found ?? lastRank + 0.001;
176
+ return lastRank;
177
+ });
178
+ const sectionPlace = (entry) => {
179
+ const order = layouts[entry.file].sections;
180
+ const at = order.indexOf(entry.item.section);
181
+ return at === -1 ? order.length + [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS].indexOf(entry.item.section) : at;
182
+ };
183
+ return placed
184
+ .sort((a, b) => a.file - b.file || sectionPlace(a) - sectionPlace(b) || rank[a.index] - rank[b.index] || a.index - b.index)
185
+ .map((entry) => (entry.file === 0 ? entry.item : { ...entry.item, file: files[entry.file].label ?? "" }));
186
+ }
187
+ /** Whether a module's codebase has been started: anything in it, or a record. */
188
+ const started = (entry) => {
189
+ try {
190
+ if (readdirSync(entry.buildDir).length > 0)
191
+ return true;
192
+ }
193
+ catch {
194
+ /* no folder */
195
+ }
196
+ return readRecord(entry.buildDir) !== null;
197
+ };
198
+ /** The files a module's page is read from: its spec files, its records, and its history - a commit moves the record read. */
199
+ function depsOf(entry, codebase, testsDir) {
200
+ const history = (dir) => [join(dirname(dir), HISTORY_DIR, "logs", "HEAD"), join(dir, RECORD_FILE)];
201
+ return [entry.plainFile, ...history(codebase.buildDir), ...history(testsDir), dirname(dirname(codebase.buildDir))];
202
+ }
203
+ /** The chain a file's map reads: where its code lives - its furthest-built tip - or its own requires chain. */
204
+ function chainOf(plainFile) {
205
+ return builtTips(plainFile)[0] ?? resolveRequiresChain(plainFile, [], undefined, { allowNoFunctionalities: true });
206
+ }
207
+ /**
208
+ * The pages of the files given, from the files alone: one a module of each file's requires chain, bottom first, each
209
+ * once. A module below the furthest-built one is read from that one's codebase, whose record has every module's items;
210
+ * a module above it from its own, empty until its turn comes.
211
+ */
212
+ export function readSpecMap(files) {
213
+ return readSpecMapWithDeps(files).pages;
214
+ }
215
+ /** The pages, and the files they were read from: while none of them changes, the pages read the same. */
216
+ export function readSpecMapWithDeps(files) {
217
+ const deps = new Set(files);
218
+ const pages = withParseCache(() => {
219
+ const pages = [];
220
+ const seen = new Set();
221
+ for (const file of files) {
222
+ let chain;
223
+ try {
224
+ chain = chainOf(file);
225
+ }
226
+ catch (error) {
227
+ if (!seen.has(file))
228
+ pages.push({ module: basename(file, ".plain"), plainFile: file, buildDir: "", imports: [], items: [], error: message(error) });
229
+ seen.add(file);
230
+ continue;
231
+ }
232
+ const furthest = chain.findLastIndex(started);
233
+ const specs = new Map();
234
+ const specOf = (at) => {
235
+ if (!specs.has(at)) {
236
+ try {
237
+ specs.set(at, chainSpec(chain.slice(0, at + 1)).tree);
238
+ }
239
+ catch (error) {
240
+ specs.set(at, error instanceof Error ? error : new Error(String(error)));
241
+ }
242
+ }
243
+ return specs.get(at);
244
+ };
245
+ chain.forEach((entry, at) => {
246
+ if (seen.has(entry.plainFile))
247
+ return;
248
+ seen.add(entry.plainFile);
249
+ const home = Math.max(at, furthest);
250
+ const codebase = chain[home];
251
+ const page = {
252
+ module: entry.moduleName,
253
+ plainFile: entry.plainFile,
254
+ buildDir: codebase.buildDir,
255
+ imports: [],
256
+ items: [],
257
+ ...(entry.requires !== null ? { requires: entry.requires } : {}),
258
+ conformance: conformanceOn(entry.plainFile),
259
+ };
260
+ const spec = specOf(home);
261
+ if (spec instanceof Error) {
262
+ pages.push({ ...page, error: spec.message });
263
+ return;
264
+ }
265
+ const projectDir = projectDirOf(entry.plainFile);
266
+ const record = home <= furthest ? readRecord(codebase.buildDir) : null;
267
+ const tested = readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDir)));
268
+ const paths = specFilesOf(entry.plainFile);
269
+ for (const path of [...paths, ...depsOf(entry, codebase, conformanceDirFor(entry.moduleName, modulesRoot(projectDir)))])
270
+ deps.add(path);
271
+ const texts = paths.map((path, index) => ({ ...(index > 0 ? { label: importLabel(path, projectDir) } : {}), text: readText(path) }));
272
+ const items = pageItems({ module: entry.moduleName, own: home === at, spec, record, tested });
273
+ pages.push({ ...page, imports: texts.slice(1).map((text) => text.label), items: layOut(items, texts) });
274
+ });
275
+ }
276
+ return pages;
277
+ });
278
+ return { pages, deps: [...deps] };
279
+ }
280
+ /** What the files read last are now: each one's time and size, or that it is not there. */
281
+ function fingerprint(paths) {
282
+ return paths
283
+ .map((path) => {
284
+ try {
285
+ const stat = statSync(path);
286
+ return `${path}:${stat.mtimeMs}:${stat.size}`;
287
+ }
288
+ catch {
289
+ return `${path}:-`;
290
+ }
291
+ })
292
+ .join("\n");
293
+ }
294
+ /** Whether a module's config has its conformance tests on: `conformance-tests:` is all (true) or chosen - off when not set. */
295
+ function conformanceOn(plainFile) {
296
+ try {
297
+ return conformanceSwitchedOn(configForSpec(plainFile).conformanceTests);
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ function readText(path) {
304
+ try {
305
+ return readFileSync(path, "utf8");
306
+ }
307
+ catch {
308
+ return "";
309
+ }
310
+ }
311
+ const message = (error) => (error instanceof Error ? error.message : String(error));
312
+ /** The lines of a serialized tree, as a set: what a pulse's trees are matched by. */
313
+ const linesSeen = new WeakMap();
314
+ function linesOf(pulse) {
315
+ let seen = linesSeen.get(pulse);
316
+ if (seen === undefined) {
317
+ const set = (text) => new Set(text.split("\n").filter((line) => line.startsWith("{")));
318
+ // A conformance pulse's trees are the tests record's: its functional specs' lines - each with its number and acceptance tests.
319
+ const specLines = (text, of = FUNCTIONAL_REQUIREMENTS) => {
320
+ const out = [];
321
+ let section = "";
322
+ for (const line of text.split("\n")) {
323
+ if (line.startsWith("["))
324
+ section = JSON.parse(line)[0];
325
+ else if (line.startsWith("{") && section === of)
326
+ out.push(line);
327
+ }
328
+ return out;
329
+ };
330
+ // The test reqs it writes tests to follow: those new in it.
331
+ const testReqs = () => {
332
+ const before = new Set(specLines(pulse.before, TEST_REQUIREMENTS));
333
+ return new Set(specLines(pulse.after, TEST_REQUIREMENTS).filter((line) => !before.has(line)).map((line) => markdownOf(line).trim()));
334
+ };
335
+ const testing = () => {
336
+ // What it writes tests for: each functional spec whose line is new - its text, its acceptance tests or its number
337
+ // changed - by its text; none, and it is the definitions, reqs or test reqs that changed: it tests every one again.
338
+ const before = new Set(specLines(pulse.before));
339
+ const after = specLines(pulse.after);
340
+ const changed = after.filter((line) => !before.has(line));
341
+ return new Set((changed.length > 0 ? changed : after).map((line) => markdownOf(line).trim()));
342
+ };
343
+ seen = pulse.track === "code" ? { before: set(pulse.before), after: set(pulse.after), testing: new Set(), testReqs: new Set() } : { before: new Set(), after: new Set(), testing: testing(), testReqs: testReqs() };
344
+ linesSeen.set(pulse, seen);
345
+ }
346
+ return seen;
347
+ }
348
+ /** A functional spec's first line, as a conformance pulse scheduled ahead of its code names it. */
349
+ const headline = (text) => (text.trim().split("\n")[0] ?? "").replace(/^-\s*/, "").trim();
350
+ /** Whether a conformance pulse writes tests for a functional spec: by its trees, or - scheduled, with none - by the specs it names. */
351
+ function writesTestsFor(pulse, item) {
352
+ if (pulse.specs !== undefined)
353
+ return pulse.specs.some((text) => headline(text) === headline(item.text));
354
+ return linesOf(pulse).testing.has(item.text.trim());
355
+ }
356
+ /** Whether a pulse regenerates an item: brings its line now, or takes out its line in the record. */
357
+ function regenerates(pulse, item) {
358
+ if (pulse.track !== "code")
359
+ return false;
360
+ const lines = linesOf(pulse);
361
+ if (item.state === "removed")
362
+ return item.wasLine !== undefined && lines.before.has(item.wasLine) && !lines.after.has(item.wasLine);
363
+ return item.line !== undefined && lines.after.has(item.line) && !lines.before.has(item.line);
364
+ }
365
+ /**
366
+ * What a module needs the person for, short, from the loop's reason for a halt: "pulse 3 didn't land - its unit tests
367
+ * never passed". What to do about it is the chat's to work out with them.
368
+ */
369
+ function needsText(stuck) {
370
+ if (stuck.pulse !== undefined) {
371
+ const what = /was stopped/.test(stuck.why) ? "was stopped" : /did not start/.test(stuck.why) ? "didn't start" : /did not pass/.test(stuck.why) ? "didn't pass" : "didn't land";
372
+ return `pulse ${stuck.pulse.number} ${what}${stuck.cause !== undefined ? ` - ${stuck.cause}` : ""}`;
373
+ }
374
+ if (/planner found no way/.test(stuck.why))
375
+ return "the planner couldn't cut it into pulses";
376
+ return stuck.why.split(" - ")[0];
377
+ }
378
+ /**
379
+ * A test req's mark, from the conformance track: followed by the conformance tests as it reads now, being written
380
+ * for in the running conformance pulse, planned in one, or not yet - none, where no conformance test is written at all.
381
+ */
382
+ function testReqMark(item, codebase, page) {
383
+ if (item.tests === "now")
384
+ return { mark: "built" };
385
+ const text = item.text.trim();
386
+ const pulse = codebase?.pulses.find((candidate) => candidate.track === "conformance" && linesOf(candidate).testReqs.has(text));
387
+ if (pulse !== undefined)
388
+ return { mark: pulse.running ? "regenerating" : "planned", pulse };
389
+ const anyTested = page.items.some((other) => other.tests !== undefined);
390
+ return { mark: anyTested ? "waiting" : "none" };
391
+ }
392
+ /** The count a page's top line starts with: how many of its functional specs are built as the spec has them. */
393
+ export function countOf(page) {
394
+ const specs = page.items.filter((item) => item.section === FUNCTIONAL_REQUIREMENTS && item.state !== "removed");
395
+ return { built: specs.filter((item) => item.state === "built").length, total: specs.length };
396
+ }
397
+ /**
398
+ * The plan laid over the pages: each item that differs from the code marked with the pulse regenerating it, the pulse
399
+ * planned to, or - with none - planning, stuck or waiting; and each page's reason on its top line.
400
+ */
401
+ export function markSpecMap(pages, plan) {
402
+ return pages.map((page) => {
403
+ const codebase = plan.codebases.find((entry) => entry.buildDir === page.buildDir);
404
+ if (page.error !== undefined)
405
+ return { ...page, items: [], tracked: codebase !== undefined, ...(codebase !== undefined ? { codebase } : {}), status: `its spec does not parse: ${page.error.split("\n")[0]}`, stuck: true };
406
+ const stuckPulse = codebase?.stuck?.pulse;
407
+ const failed = stuckPulse !== undefined ? { number: stuckPulse.number, title: "", track: "code", running: false, rowId: "", before: stuckPulse.before, after: stuckPulse.after } : undefined;
408
+ const conformance = codebase?.pulses.find((pulse) => pulse.running && pulse.track === "conformance");
409
+ // Test reqs only where conformance tests are on: the loop's word for it - the config, or asked for this session - else the config's.
410
+ const conformanceIsOn = codebase?.conformance ?? page.conformance !== false;
411
+ const items = page.items.filter((item) => item.state !== "test" || conformanceIsOn).map((item) => {
412
+ const testing = conformance !== undefined && item.section === FUNCTIONAL_REQUIREMENTS && item.state !== "removed" && (() => {
413
+ return linesOf(conformance).testing.has(item.text.trim());
414
+ })();
415
+ // Not being tested now: the conformance pulse planned to - its tests written for it as it reads now, or not.
416
+ const testsPulse = testing || item.section !== FUNCTIONAL_REQUIREMENTS || item.state === "removed"
417
+ ? undefined
418
+ : codebase?.pulses.find((pulse) => !pulse.running && pulse.track === "conformance" && writesTestsFor(pulse, item));
419
+ const extra = testing ? { testing: true } : testsPulse !== undefined ? { testsPulse } : {};
420
+ if (item.state === "test")
421
+ return { ...item, ...testReqMark(item, codebase, page), ...extra };
422
+ if (item.state === "built")
423
+ return { ...item, mark: "built", ...extra };
424
+ const pulse = codebase?.pulses.find((candidate) => regenerates(candidate, item));
425
+ if (pulse !== undefined)
426
+ return { ...item, mark: pulse.running ? "regenerating" : "planned", pulse, ...extra };
427
+ if (codebase?.planning === "code")
428
+ return { ...item, mark: "planning", ...extra };
429
+ if (codebase?.stuck !== undefined) {
430
+ // The planner found no plan: every item is stuck. A halt: the items of the pulse that did not land are; the rest wait.
431
+ if (failed === undefined)
432
+ return { ...item, mark: "stuck", ...extra };
433
+ if (regenerates(failed, item))
434
+ return { ...item, mark: "stuck", rolledBack: failed.number, ...extra };
435
+ }
436
+ return { ...item, mark: "waiting", ...extra };
437
+ });
438
+ const differing = items.filter((item) => item.state === "new" || item.state === "changed" || item.state === "removed").length;
439
+ const pulses = codebase?.pulses ?? [];
440
+ const running = pulses.find((pulse) => pulse.running);
441
+ const last = pulses.at(-1)?.number;
442
+ const status = codebase === undefined
443
+ ? differing > 0
444
+ ? "not asked to build - regenerate plans it"
445
+ : "all built"
446
+ : running !== undefined
447
+ ? // The count is of all the module's pulses: what the running one does follows it, not "conformance pulse 5 of 5".
448
+ `pulse ${running.number} of ${last} regenerating${running.track === "conformance" ? " · conformance tests" : ""}`
449
+ : codebase.planning !== undefined
450
+ ? "planning"
451
+ : codebase.waitsFor !== undefined
452
+ ? `waits for ${codebase.waitsFor}`
453
+ : codebase.stuck !== undefined
454
+ ? needsText(codebase.stuck)
455
+ : pulses.length > 0
456
+ ? `pulse ${pulses[0].number} of ${last} next`
457
+ : differing === 0
458
+ ? "all built"
459
+ : (codebase.why ?? "waiting");
460
+ return { ...page, items, tracked: codebase !== undefined, ...(codebase !== undefined ? { codebase } : {}), status, stuck: codebase?.stuck !== undefined && running === undefined };
461
+ });
462
+ }
463
+ /**
464
+ * The .plain files the map shows when nothing has been asked for: the project's module from config.turri.yaml, else the
465
+ * .plain files in the folder that no other one there imports or requires.
466
+ */
467
+ export function defaultMapFiles(cwd) {
468
+ // Asked for on every read of the map, every second while regen shows: worked out again only once the folder's .plain
469
+ // files or its config change - parsing each file's chains takes seconds in a project of many modules.
470
+ const key = `${resolve(cwd)}\n${folderPrint(cwd)}`;
471
+ if (keptDefaultFiles?.key === key)
472
+ return keptDefaultFiles.files;
473
+ const files = withParseCache(() => findDefaultMapFiles(cwd));
474
+ keptDefaultFiles = { key, files };
475
+ return files;
476
+ }
477
+ let keptDefaultFiles;
478
+ /** The folder's .plain files and its config, by name, size and when each changed. */
479
+ function folderPrint(cwd) {
480
+ try {
481
+ const names = readdirSync(cwd).filter((name) => name.endsWith(".plain") || /^config(\..+)?\.yaml$/.test(name)).sort();
482
+ return fingerprint(names.map((name) => join(cwd, name)));
483
+ }
484
+ catch {
485
+ return "";
486
+ }
487
+ }
488
+ function findDefaultMapFiles(cwd) {
489
+ try {
490
+ const module = readProjectConfig(cwd).module;
491
+ if (module !== undefined && existsSync(resolve(cwd, module)))
492
+ return [resolve(cwd, module)];
493
+ }
494
+ catch {
495
+ /* no config: the folder's files */
496
+ }
497
+ let files;
498
+ try {
499
+ files = readdirSync(cwd)
500
+ .filter((name) => name.endsWith(".plain"))
501
+ .sort()
502
+ .map((name) => join(cwd, name));
503
+ }
504
+ catch {
505
+ return [];
506
+ }
507
+ const below = new Set();
508
+ for (const file of files) {
509
+ try {
510
+ for (const path of specFilesOf(file).slice(1))
511
+ below.add(path);
512
+ for (const entry of resolveRequiresChain(file, [], undefined, { allowNoFunctionalities: true }).slice(0, -1))
513
+ below.add(resolve(entry.plainFile));
514
+ }
515
+ catch {
516
+ /* a file that does not parse is still shown */
517
+ }
518
+ }
519
+ return files.filter((file) => !below.has(resolve(file)));
520
+ }
521
+ /**
522
+ * The map as the side panel reads it on every frame: read again from the files and laid over the plan at most every
523
+ * `every` ms - git runs for the records and the diffs - and the same pages in between.
524
+ */
525
+ export function createSpecMapSource(options) {
526
+ const every = options.every ?? 1000;
527
+ const now = options.now ?? Date.now;
528
+ let last;
529
+ // The pages from the files, and what the files were: read again only once one of them changes.
530
+ let read;
531
+ return {
532
+ read() {
533
+ if (last !== undefined && now() - last.at < every)
534
+ return last.pages;
535
+ let pages;
536
+ // Nothing here may throw: the panel draws from it on every frame, and a frame that throws draws nothing again.
537
+ try {
538
+ const plan = options.plan();
539
+ const files = options.files();
540
+ const key = files.join("\n");
541
+ if (read === undefined || read.files !== key || fingerprint(read.deps) !== read.print) {
542
+ const fresh = readSpecMapWithDeps(files);
543
+ read = { files: key, print: fingerprint(fresh.deps), deps: fresh.deps, pages: fresh.pages };
544
+ }
545
+ pages = markSpecMap(read.pages, plan);
546
+ }
547
+ catch (error) {
548
+ pages = last?.pages ?? [];
549
+ if (pages.length === 0)
550
+ pages = [{ module: "spec", plainFile: "", buildDir: "", imports: [], items: [], tracked: false, status: `the map could not be read: ${message(error)}`, stuck: true }];
551
+ }
552
+ last = { at: now(), pages };
553
+ return pages;
554
+ },
555
+ /** Read the files again on the next frame, whatever they look like. */
556
+ refresh() {
557
+ last = undefined;
558
+ read = undefined;
559
+ },
560
+ };
561
+ }