@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,752 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { extname, isAbsolute, basename, resolve } from "node:path";
4
+ import { load as loadYaml } from "js-yaml";
5
+ import { extractConceptsFromDefinition, findConceptDefinitionsInPlainSource, sortDefinitions, validateConcepts, DEFAULT_CONCEPTS, } from "./concepts.js";
6
+ import { ImportedModuleWithFunctionalitiesError, MissingFunctionalitiesError, ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content, } from "./errors.js";
7
+ import { findLargeBase64Blob, getLoadedTemplates, openFrom } from "./files.js";
8
+ import { renderPlainSource } from "./liquid.js";
9
+ import { isStdlibModule, readStdlibModule } from "./stdlib.js";
10
+ import { getRawText, lineNumber, parseMarkdown, removeQuotes, renderToMarkdown, traverse, } from "./markdown.js";
11
+ import { ACCEPTANCE_TESTS, ACCEPTANCE_TEST_HEADING, ALLOWED_IMPORT_SPECIFICATION_HEADINGS, ALLOWED_SPECIFICATION_HEADINGS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, getSpecificationItems, REQUIRED_FUNCTIONAL_SPECS, } from "./spec.js";
12
+ export const IMPORT_DIRECTIVE = "import";
13
+ export const REQUIRES_DIRECTIVE = "requires";
14
+ export const REQUIRED_CONCEPTS_DIRECTIVE = "required_concepts";
15
+ export const EXPORTED_CONCEPTS_DIRECTIVE = "exported_concepts";
16
+ export const PLAIN_SOURCE_FILE_EXTENSION = ".plain";
17
+ function newPlainSource() {
18
+ return {
19
+ [DEFINITIONS]: null,
20
+ [NON_FUNCTIONAL_REQUIREMENTS]: null,
21
+ [TEST_REQUIREMENTS]: null,
22
+ };
23
+ }
24
+ export function getFilenameFromModuleName(moduleName) {
25
+ return `${moduleName}${PLAIN_SOURCE_FILE_EXTENSION}`;
26
+ }
27
+ export function getModuleNameFromFilename(filename) {
28
+ return filename.split(PLAIN_SOURCE_FILE_EXTENSION).join("");
29
+ }
30
+ function hasScheme(target) {
31
+ return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(target);
32
+ }
33
+ /**
34
+ * A link's file: found where resources are loaded from - the spec's own folder first, then the
35
+ * other template folders - or else from the current directory, as the reference parser has it.
36
+ * Links are relative to the spec, so a spec in a subfolder is checked where it is, wherever
37
+ * turri was started.
38
+ */
39
+ function linkedFile(target, dirs) {
40
+ for (const dir of dirs) {
41
+ const candidate = resolve(dir, target);
42
+ consulted?.add(candidate);
43
+ if (existsSync(candidate))
44
+ return candidate;
45
+ }
46
+ consulted?.add(resolve(target));
47
+ return target;
48
+ }
49
+ /**
50
+ * The linked files a parse looked for, found or not, while one runs: a kept parse holds while each is as it was - a
51
+ * resource written after a parse that did not find it is seen, though it is in a folder the fingerprint does not walk.
52
+ */
53
+ let consulted;
54
+ /** What the linked files a parse looked for are now: there or not, their size and time. */
55
+ function linkedStamp(paths) {
56
+ return paths
57
+ .map((path) => {
58
+ try {
59
+ const stat = statSync(path);
60
+ return `${path}:${stat.size}:${stat.mtimeMs}`;
61
+ }
62
+ catch {
63
+ return `${path}:-`;
64
+ }
65
+ })
66
+ .join("\n");
67
+ }
68
+ /** A parse, with every linked file it looked for gathered as it ran. */
69
+ function parseNoting(run) {
70
+ const outer = consulted;
71
+ const mine = new Set();
72
+ consulted = mine;
73
+ try {
74
+ return { parsed: run(), linked: [...mine] };
75
+ }
76
+ catch (error) {
77
+ return { error, linked: [...mine] };
78
+ }
79
+ finally {
80
+ // A parse inside another's - an import, a required module - is part of what the outer one looked for too.
81
+ if (outer !== undefined)
82
+ for (const path of mine)
83
+ outer.add(path);
84
+ consulted = outer;
85
+ }
86
+ }
87
+ /** A kept parse that still holds: nothing it could read has changed, the linked files it looked for included. */
88
+ function stillHolds(kept, fingerprint) {
89
+ return kept !== undefined && fingerprint !== undefined && kept.fingerprint === fingerprint && linkedStamp(kept.linked) === kept.linkedStamp;
90
+ }
91
+ export function checkSectionForLinkedResources(section, dirs = []) {
92
+ const linkedResources = [];
93
+ for (const link of traverse(section, "link")) {
94
+ const target = String(link.url ?? "");
95
+ const children = link.children ?? [];
96
+ if (hasScheme(target) || isAbsolute(target)) {
97
+ const text = children[0]?.value ?? "";
98
+ throw new PlainSyntaxError(`Plain syntax error: Only relative links are allowed (text: ${text}, target: ${target}).`);
99
+ }
100
+ const file = linkedFile(target, dirs);
101
+ if (!existsSync(file)) {
102
+ throw new PlainSyntaxError(`Plain syntax error: Link ${target} does not exist.`);
103
+ }
104
+ if (!statSync(file).isFile()) {
105
+ throw new PlainSyntaxError(`Plain syntax error: Link ${target} must be a file.`);
106
+ }
107
+ if (children.length !== 1) {
108
+ throw new PlainSyntaxError(`Plain syntax error: Link must have text specified (link: ${target}).`);
109
+ }
110
+ if (extname(basename(target)) === ".plain") {
111
+ throw new PlainSyntaxError(`Referenced resource '${target}' is a .plain file. ` +
112
+ "Referencing .plain files through Linked Resources is not supported. " +
113
+ "Please use the import section to include definitions or implementation/test requirements from another module.");
114
+ }
115
+ linkedResources.push({ text: String(children[0].value ?? ""), target });
116
+ }
117
+ if (linkedResources.length > 0)
118
+ section.linked_resources = linkedResources;
119
+ }
120
+ export function checkForLinkedResources(plainSource, dirs = []) {
121
+ for (const heading of ALLOWED_SPECIFICATION_HEADINGS) {
122
+ const section = plainSource[heading];
123
+ if (!section)
124
+ continue;
125
+ for (const requirement of section.children) {
126
+ checkSectionForLinkedResources(requirement, dirs);
127
+ for (const acceptanceTest of requirement[ACCEPTANCE_TESTS] ?? []) {
128
+ checkSectionForLinkedResources(acceptanceTest, dirs);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ export function processSectionCodeVariables(section, codeVariables) {
134
+ if (!("markdown" in section))
135
+ return;
136
+ const codeVariablesArray = [];
137
+ for (const [key, value] of Object.entries(codeVariables)) {
138
+ if (!section.markdown.includes(key))
139
+ continue;
140
+ const variableName = Object.keys(value)[0];
141
+ section.markdown = section.markdown.split(key).join(`{{ ${variableName} }}`);
142
+ codeVariablesArray.push({ name: variableName, value: value[variableName] });
143
+ }
144
+ if (codeVariablesArray.length > 0)
145
+ section.code_variables = codeVariablesArray;
146
+ }
147
+ export function processCodeVariables(marshalled, codeVariables) {
148
+ for (const heading of ALLOWED_SPECIFICATION_HEADINGS) {
149
+ for (const requirement of getSpecificationItems(marshalled, heading) ?? []) {
150
+ processSectionCodeVariables(requirement, codeVariables);
151
+ }
152
+ }
153
+ }
154
+ /** Whether the module declares a ***functional specs*** section (even if it is empty). */
155
+ export function hasFunctionalSpecsSection(plainSource) {
156
+ return FUNCTIONAL_REQUIREMENTS in plainSource;
157
+ }
158
+ /** Number of functionalities under ***functional specs*** (0 if absent or empty). */
159
+ export function countFunctionalities(plainSource) {
160
+ const section = plainSource[FUNCTIONAL_REQUIREMENTS];
161
+ return section ? section.children.length : 0;
162
+ }
163
+ export function validateFunctionalitiesHaveImplementationReqs(plainSource, moduleName, hasRequires = false) {
164
+ const implementationReqs = plainSource[NON_FUNCTIONAL_REQUIREMENTS];
165
+ const hasImplementationReqs = Boolean(implementationReqs && implementationReqs.children.length > 0);
166
+ if (hasImplementationReqs)
167
+ return;
168
+ let message = `Plain syntax error: Module '${moduleName}' defines functionalities but specifies no ` +
169
+ `${NON_FUNCTIONAL_REQUIREMENTS}. At least one implementation req is required.`;
170
+ if (hasRequires) {
171
+ message +=
172
+ " Implementation reqs are not inherited through 'requires' — " +
173
+ "add them to this module, or 'import' a module that provides them.";
174
+ }
175
+ throw new PlainSyntaxError(message);
176
+ }
177
+ /**
178
+ * Is this token an `acceptance tests` heading?
179
+ *
180
+ * True only for a paragraph holding exactly one emphasis, holding exactly one strong,
181
+ * holding exactly one text node whose content is the heading. Anything that looks like
182
+ * a heading but reads differently comes back as a problem string rather than false, so
183
+ * the caller can reject a near miss instead of silently keeping it.
184
+ */
185
+ export function isAcceptanceTestHeading(token) {
186
+ if (token.type !== "paragraph")
187
+ return [false, null];
188
+ const children = token.children ?? [];
189
+ if (children.length !== 1 || children[0].type !== "emphasis")
190
+ return [false, null];
191
+ const emphasisChildren = children[0].children ?? [];
192
+ if (emphasisChildren.length !== 1 || emphasisChildren[0].type !== "strong")
193
+ return [false, null];
194
+ const strongChildren = emphasisChildren[0].children ?? [];
195
+ if (strongChildren.length !== 1 || strongChildren[0].type !== "text")
196
+ return [false, null];
197
+ const content = String(strongChildren[0].value ?? "").trim();
198
+ if (content === ACCEPTANCE_TEST_HEADING)
199
+ return [true, null];
200
+ return [
201
+ false,
202
+ `Syntax error at line ${lineNumber(token)}: Invalid acceptance test heading (\`${content}\`). ` +
203
+ `Expected: \`${ACCEPTANCE_TEST_HEADING}.\``,
204
+ ];
205
+ }
206
+ /** Find an acceptance tests heading anywhere in a subtree. */
207
+ export function findAcceptanceTestHeading(token) {
208
+ const [isHeading] = isAcceptanceTestHeading(token);
209
+ if (isHeading)
210
+ return token;
211
+ for (const child of token.children ?? []) {
212
+ const found = findAcceptanceTestHeading(child);
213
+ if (found !== null)
214
+ return found;
215
+ }
216
+ return null;
217
+ }
218
+ /**
219
+ * Pull the acceptance tests out of one functionality.
220
+ *
221
+ * A functionality with acceptance tests has three children: its own text, the
222
+ * `***acceptance tests***` heading, and the list of tests. The heading and list are
223
+ * lifted onto `acceptance_tests` and removed from the children.
224
+ */
225
+ export function processSingleAcceptanceTestRequirement(functionalRequirement) {
226
+ const newChildren = [];
227
+ const children = functionalRequirement.children ?? [];
228
+ let acceptanceTestsFoundAlready = false;
229
+ for (let index = 0; index < children.length; index += 1) {
230
+ const child = children[index];
231
+ const [isHeading, problem] = isAcceptanceTestHeading(child);
232
+ if (problem) {
233
+ // A near-miss heading: `acceptance test` instead of `acceptance tests`, or some
234
+ // other block written where the tests were meant to go.
235
+ throw new PlainSyntaxError(`Plain syntax error: ${problem}`);
236
+ }
237
+ if (!isHeading) {
238
+ newChildren.push(child);
239
+ continue;
240
+ }
241
+ if (acceptanceTestsFoundAlready) {
242
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(child)}: Duplicate 'acceptance tests' ` +
243
+ "heading found within the same functionality. Only one block of acceptance tests is allowed per functionality.");
244
+ }
245
+ const nextToken = children[index + 1];
246
+ if (nextToken === undefined) {
247
+ newChildren.push(child);
248
+ continue;
249
+ }
250
+ index += 1;
251
+ if (nextToken.type === "list") {
252
+ if (functionalRequirement[ACCEPTANCE_TESTS] === undefined) {
253
+ functionalRequirement[ACCEPTANCE_TESTS] = [];
254
+ }
255
+ for (const listItem of nextToken.children ?? []) {
256
+ functionalRequirement[ACCEPTANCE_TESTS].push(listItem);
257
+ }
258
+ acceptanceTestsFoundAlready = true;
259
+ }
260
+ else {
261
+ // Not followed by a list: keep both tokens.
262
+ newChildren.push(child);
263
+ newChildren.push(nextToken);
264
+ }
265
+ }
266
+ functionalRequirement.children = newChildren;
267
+ }
268
+ export function processAcceptanceTests(plainSource) {
269
+ if (!(FUNCTIONAL_REQUIREMENTS in plainSource))
270
+ return;
271
+ const frs = plainSource[FUNCTIONAL_REQUIREMENTS];
272
+ if (!frs)
273
+ return;
274
+ for (const functionalRequirement of frs.children) {
275
+ if (functionalRequirement.children === undefined)
276
+ continue;
277
+ processSingleAcceptanceTestRequirement(functionalRequirement);
278
+ }
279
+ }
280
+ export function marshallPlainSource(input) {
281
+ const plainSource = {};
282
+ for (const heading of ALLOWED_SPECIFICATION_HEADINGS) {
283
+ const section = input[heading];
284
+ if (!section)
285
+ continue;
286
+ const listOfRequirements = [];
287
+ for (const requirement of section.children) {
288
+ const requirementSection = {
289
+ markdown: renderToMarkdown(requirement).trim(),
290
+ };
291
+ if (requirement.linked_resources) {
292
+ requirementSection.linked_resources = requirement.linked_resources;
293
+ }
294
+ const acceptanceTests = requirement[ACCEPTANCE_TESTS];
295
+ if (acceptanceTests !== undefined) {
296
+ requirementSection[ACCEPTANCE_TESTS] = acceptanceTests.map((acceptanceTest) => {
297
+ const section = { markdown: renderToMarkdown(acceptanceTest).trim() };
298
+ if (acceptanceTest.linked_resources)
299
+ section.linked_resources = acceptanceTest.linked_resources;
300
+ return section;
301
+ });
302
+ }
303
+ listOfRequirements.push(requirementSection);
304
+ }
305
+ plainSource[heading] = listOfRequirements;
306
+ }
307
+ return plainSource;
308
+ }
309
+ export function normalizeLineEndings(plainSourceText) {
310
+ return plainSourceText.split("\r\n").join("\n").split("\r").join("\n");
311
+ }
312
+ /**
313
+ * Frontmatter parsing strips leading blank lines, which would shift every reported
314
+ * line number. Put them back so syntax errors point at the real line.
315
+ */
316
+ export function restoreStrippedLines(plainSourceText, content) {
317
+ const strippedSource = plainSourceText.replace(/\s+$/, "");
318
+ if (!content || !strippedSource.endsWith(content))
319
+ return content;
320
+ const prefix = strippedSource.slice(0, strippedSource.length - content.length);
321
+ const strippedLineCount = (prefix.match(/\n/g) ?? []).length;
322
+ return "\n".repeat(strippedLineCount) + content;
323
+ }
324
+ export function readPlainSourceMetadata(plainSourceText) {
325
+ let metadata = {};
326
+ let content = plainSourceText;
327
+ const match = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/.exec(plainSourceText);
328
+ if (match) {
329
+ try {
330
+ // python-frontmatter yields {} when the block does not parse to a mapping,
331
+ // so a malformed directive block is ignored rather than fatal.
332
+ const loaded = loadYaml(match[1]);
333
+ metadata =
334
+ loaded !== null && typeof loaded === "object" && !Array.isArray(loaded)
335
+ ? loaded
336
+ : {};
337
+ }
338
+ catch (error) {
339
+ throw new PlainSyntaxError(`Plain syntax error: Invalid frontmatter: ${error instanceof Error ? error.message : String(error)}`);
340
+ }
341
+ content = plainSourceText.slice(match[0].length);
342
+ }
343
+ content = content.trim();
344
+ for (const directive of [EXPORTED_CONCEPTS_DIRECTIVE, REQUIRED_CONCEPTS_DIRECTIVE]) {
345
+ if (!(directive in metadata))
346
+ continue;
347
+ const value = metadata[directive];
348
+ if (!Array.isArray(value)) {
349
+ throw new Error(`Syntax error: Invalid ${directive} metadata. Expected a list.`);
350
+ }
351
+ const prepared = [];
352
+ for (const item of value) {
353
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
354
+ for (const [k, v] of Object.entries(item)) {
355
+ prepared.push(`- ${k}: ${v}`);
356
+ }
357
+ }
358
+ else if (typeof item === "string") {
359
+ prepared.push(`- ${item}`);
360
+ }
361
+ else {
362
+ throw new PlainSyntaxError(`Plain syntax error: Invalid ${directive} metadata. Expected a dictionary or a string.`);
363
+ }
364
+ }
365
+ metadata[directive] = prepared;
366
+ }
367
+ return { metadata, content };
368
+ }
369
+ function processImports(plainSource, imports, codeVariables, templateDirs, importedModules, modulesTrace) {
370
+ const requiredConcepts = [];
371
+ for (const moduleName of imports) {
372
+ if (modulesTrace.includes(moduleName)) {
373
+ throw new PlainSyntaxError(`Plain syntax error: Circular import detected: ${moduleName}.`);
374
+ }
375
+ if (importedModules.includes(moduleName))
376
+ continue;
377
+ const result = parsePlainFile(moduleName, codeVariables, templateDirs, importedModules, modulesTrace);
378
+ if (hasFunctionalSpecsSection(result.plainSource)) {
379
+ throw new ImportedModuleWithFunctionalitiesError(`Module '${moduleName}' is imported but contains functional specs. ` +
380
+ "Imported modules may only provide definitions, implementation reqs, and test reqs — " +
381
+ "use 'requires' instead of 'import' if this module's functionalities should be built on.");
382
+ }
383
+ for (const heading of Object.keys(result.plainSource)) {
384
+ if (!ALLOWED_IMPORT_SPECIFICATION_HEADINGS.includes(heading)) {
385
+ throw new PlainSyntaxError(`Plain syntax error: Invalid specification heading (\`${heading}\`). ` +
386
+ `Allowed headings: ${ALLOWED_IMPORT_SPECIFICATION_HEADINGS.join(", ")}`);
387
+ }
388
+ if (plainSource[heading] == null) {
389
+ plainSource[heading] = result.plainSource[heading] ?? null;
390
+ }
391
+ else if (result.plainSource[heading] != null) {
392
+ plainSource[heading].children.push(...result.plainSource[heading].children);
393
+ }
394
+ }
395
+ if (REQUIRED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata) {
396
+ for (const item of result.plainSourceObj.metadata[REQUIRED_CONCEPTS_DIRECTIVE]) {
397
+ if (typeof item !== "string") {
398
+ throw new Error(`Syntax error: Invalid ${REQUIRED_CONCEPTS_DIRECTIVE} metadata. Expected a string.`);
399
+ }
400
+ const [newConcepts] = extractConceptsFromDefinition(item);
401
+ requiredConcepts.push(...newConcepts);
402
+ }
403
+ requiredConcepts.push(...result.requiredConcepts);
404
+ }
405
+ importedModules.push(moduleName);
406
+ }
407
+ return requiredConcepts;
408
+ }
409
+ export function parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, modulesTrace) {
410
+ plainSourceText = normalizeLineEndings(plainSourceText);
411
+ const plainSourceObj = readPlainSourceMetadata(plainSourceText);
412
+ const plainSource = newPlainSource();
413
+ let requiredConcepts = [];
414
+ if (IMPORT_DIRECTIVE in plainSourceObj.metadata) {
415
+ requiredConcepts = processImports(plainSource, plainSourceObj.metadata[IMPORT_DIRECTIVE], codeVariables, templateDirs, importedModules, modulesTrace);
416
+ }
417
+ const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText);
418
+ const plainSourceContent = restoreStrippedLines(plainSourceText, plainSourceObj.content);
419
+ const plainSourceFullText = renderPlainSource(plainSourceContent, loadedTemplates, codeVariables);
420
+ const plainFile = parseMarkdown(plainSourceFullText);
421
+ removeQuotes(plainFile);
422
+ let currentSpecificationHeading = null;
423
+ const processedSpecificationHeadings = new Set();
424
+ for (const token of plainFile.children ?? []) {
425
+ const tokenText = getRawText(token).trim();
426
+ if (token.type === "paragraph") {
427
+ const paragraphChildren = token.children ?? [];
428
+ const emphasisChildren = paragraphChildren.length > 0 && paragraphChildren[0].type === "emphasis"
429
+ ? (paragraphChildren[0].children ?? [])
430
+ : [];
431
+ if (!(paragraphChildren.length === 1 &&
432
+ paragraphChildren[0].type === "emphasis" &&
433
+ emphasisChildren.length === 1 &&
434
+ emphasisChildren[0].type === "strong")) {
435
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid specification (\`${tokenText}\`)`);
436
+ }
437
+ const specificationHeading = String((emphasisChildren[0].children ?? [])[0]?.value ?? "");
438
+ if (specificationHeading === ACCEPTANCE_TEST_HEADING) {
439
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: ${ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec.`);
440
+ }
441
+ if (!ALLOWED_SPECIFICATION_HEADINGS.includes(specificationHeading)) {
442
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid specification heading (\`${specificationHeading}\`). Allowed headings: ${ALLOWED_SPECIFICATION_HEADINGS.join(", ")}`);
443
+ }
444
+ if (specificationHeading === FUNCTIONAL_REQUIREMENTS && !(specificationHeading in plainSource)) {
445
+ plainSource[specificationHeading] = null;
446
+ }
447
+ if (processedSpecificationHeadings.has(specificationHeading)) {
448
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Duplicate specification heading (\`${specificationHeading}\`)`);
449
+ }
450
+ if (specificationHeading === DEFINITIONS && currentSpecificationHeading !== null) {
451
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Definitions specification must be the first specification in the section (\`${tokenText}\`)`);
452
+ }
453
+ currentSpecificationHeading = specificationHeading;
454
+ if (plainSource[currentSpecificationHeading] == null) {
455
+ plainSource[currentSpecificationHeading] = { children: [] };
456
+ }
457
+ processedSpecificationHeadings.add(currentSpecificationHeading);
458
+ }
459
+ else if (token.type === "list") {
460
+ if (currentSpecificationHeading === null) {
461
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Missing specification heading (\`${tokenText}\`)`);
462
+ }
463
+ plainSource[currentSpecificationHeading].children.push(...(token.children ?? []));
464
+ }
465
+ else {
466
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid source structure (\`${tokenText}\`)`);
467
+ }
468
+ }
469
+ // Acceptance tests belong under functional specs only.
470
+ for (const nonFunctionalHeading of [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]) {
471
+ const section = plainSource[nonFunctionalHeading];
472
+ if (!section)
473
+ continue;
474
+ for (const child of section.children) {
475
+ const nested = findAcceptanceTestHeading(child);
476
+ if (nested !== null) {
477
+ throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(nested)}: ${ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec.`);
478
+ }
479
+ }
480
+ }
481
+ const definitionsSection = plainSource[DEFINITIONS];
482
+ if (definitionsSection) {
483
+ for (const token of definitionsSection.children) {
484
+ const [newConcepts] = extractConceptsFromDefinition(renderToMarkdown(token));
485
+ for (const concept of newConcepts) {
486
+ const index = requiredConcepts.indexOf(concept);
487
+ if (index !== -1)
488
+ requiredConcepts.splice(index, 1);
489
+ }
490
+ }
491
+ }
492
+ const requiredModules = plainSourceObj.metadata[REQUIRES_DIRECTIVE] ?? [];
493
+ return { plainSource, plainSourceObj, requiredModules, requiredConcepts };
494
+ }
495
+ export function readModulePlainSource(moduleName, templateDirs) {
496
+ // `stdlib/...` is turri's standard library, and only that - see stdlib.ts.
497
+ const plainSourceText = isStdlibModule(moduleName) ? readStdlibModule(moduleName, templateDirs) : openFrom(templateDirs, moduleName + PLAIN_SOURCE_FILE_EXTENSION);
498
+ if (plainSourceText === null) {
499
+ throw new ModuleDoesNotExistError(`Module does not exist (${moduleName}).`);
500
+ }
501
+ const blob = findLargeBase64Blob(plainSourceText);
502
+ if (blob !== null) {
503
+ throw new UnsupportedBase64Content(`Module '${moduleName}' contains a base64-encoded blob (${blob.length} characters) ` +
504
+ "inlined in the specification. This is not supported." +
505
+ "Remove the base64 data from the .plain file or if necessary," +
506
+ "include the binary file path in the specification.");
507
+ }
508
+ return plainSourceText;
509
+ }
510
+ export function parsePlainFile(moduleName, codeVariables, templateDirs, importedModules, modulesTrace) {
511
+ const plainSourceText = readModulePlainSource(moduleName, templateDirs);
512
+ return parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, [
513
+ ...modulesTrace,
514
+ moduleName,
515
+ ]);
516
+ }
517
+ export function processRequiredModules(requiredModules, codeVariables, templateDirs, allRequiredModules, modulesTrace) {
518
+ const exportedDefinitions = [];
519
+ for (const moduleName of requiredModules) {
520
+ if (modulesTrace.includes(moduleName)) {
521
+ throw new PlainSyntaxError(`Plain syntax error: Circular required module detected: ${moduleName}.`);
522
+ }
523
+ if (allRequiredModules.length > 0 && moduleName === allRequiredModules[allRequiredModules.length - 1]) {
524
+ continue;
525
+ }
526
+ const result = parsePlainFile(moduleName, codeVariables, templateDirs, [], []);
527
+ if (result.requiredModules.length === 0) {
528
+ if (allRequiredModules.length > 0) {
529
+ // Required modules must form one chain; independent modules cannot yet be merged.
530
+ throw new PlainSyntaxError(`Plain syntax error: There must be a fixed order how required modules are dependent (${moduleName}).`);
531
+ }
532
+ }
533
+ else {
534
+ processRequiredModules(result.requiredModules, codeVariables, templateDirs, allRequiredModules, [
535
+ ...modulesTrace,
536
+ moduleName,
537
+ ]);
538
+ }
539
+ if (EXPORTED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata) {
540
+ const exportedConcepts = [];
541
+ for (const concept of result.plainSourceObj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]) {
542
+ if (typeof concept === "string" && DEFAULT_CONCEPTS.has(concept)) {
543
+ throw new PlainSyntaxError(`Plain syntax error: Default concept cannot be exported: ${concept}. Only user-defined concepts can be exported.`);
544
+ }
545
+ if (typeof concept === "string") {
546
+ exportedConcepts.push(...extractConceptsFromDefinition(concept)[0]);
547
+ }
548
+ else {
549
+ throw new PlainSyntaxError(`Plain syntax error: Invalid exported concept: ${concept}.`);
550
+ }
551
+ }
552
+ for (const exportedConcept of exportedConcepts) {
553
+ exportedDefinitions.push(...findConceptDefinitionsInPlainSource(exportedConcept, result.plainSource[DEFINITIONS] ?? null));
554
+ }
555
+ }
556
+ allRequiredModules.push(moduleName);
557
+ }
558
+ return exportedDefinitions;
559
+ }
560
+ export function processExportedDefinitions(plainSource, exportedDefinitions) {
561
+ if (exportedDefinitions.length === 0)
562
+ return;
563
+ const definitions = plainSource[DEFINITIONS];
564
+ if (!definitions)
565
+ return;
566
+ for (const exportedDefinition of exportedDefinitions) {
567
+ const exportedRendered = renderToMarkdown(exportedDefinition).trim();
568
+ const alreadyPresent = definitions.children.some((definition) => renderToMarkdown(definition).trim() === exportedRendered);
569
+ if (!alreadyPresent)
570
+ definitions.children.push(exportedDefinition);
571
+ }
572
+ }
573
+ export function plainFileParser(plainSourceFileName, templateDirs, options = {}) {
574
+ const key = `${options.allowNoFunctionalities ? "any" : "renderable"}\0${process.cwd()}\0${templateDirs.join("\0")}\0${plainSourceFileName}`;
575
+ if (activeCache === undefined) {
576
+ // Between calls: the parse from before, while nothing it could have read has changed since.
577
+ const fingerprint = filesFingerprint(templateDirs);
578
+ const kept = lastParses.get(key);
579
+ if (stillHolds(kept, fingerprint)) {
580
+ if (kept.error !== undefined)
581
+ throw kept.error;
582
+ return structuredClone(kept.parsed);
583
+ }
584
+ const result = parseNoting(() => parseModule(plainSourceFileName, templateDirs, new Map(), options));
585
+ // A spec that does not parse does not parse again until it, or a file it links, changes: it is not parsed again to say so.
586
+ if (fingerprint !== undefined)
587
+ remember(key, { fingerprint, linked: result.linked, linkedStamp: linkedStamp(result.linked), ...(result.error !== undefined ? { error: result.error } : { parsed: structuredClone(result.parsed) }) });
588
+ if (result.error !== undefined)
589
+ throw result.error;
590
+ return result.parsed;
591
+ }
592
+ let parsed = activeCache.get(key);
593
+ if (parsed === undefined) {
594
+ // Nothing changes while `run` runs: each folder set's fingerprint is taken once.
595
+ const dirsKey = templateDirs.join("\0");
596
+ let fingerprint = activeCache.fingerprints.get(dirsKey);
597
+ if (!activeCache.fingerprints.has(dirsKey)) {
598
+ fingerprint = filesFingerprint(templateDirs);
599
+ activeCache.fingerprints.set(dirsKey, fingerprint);
600
+ }
601
+ const kept = lastParses.get(key);
602
+ if (stillHolds(kept, fingerprint)) {
603
+ if (kept.error !== undefined)
604
+ throw kept.error;
605
+ parsed = structuredClone(kept.parsed);
606
+ }
607
+ else {
608
+ const modules = activeCache.modules;
609
+ const result = parseNoting(() => parseModule(plainSourceFileName, templateDirs, modules, options));
610
+ if (fingerprint !== undefined)
611
+ remember(key, { fingerprint, linked: result.linked, linkedStamp: linkedStamp(result.linked), ...(result.error !== undefined ? { error: result.error } : { parsed: structuredClone(result.parsed) }) });
612
+ if (result.error !== undefined)
613
+ throw result.error;
614
+ parsed = result.parsed;
615
+ }
616
+ activeCache.set(key, parsed);
617
+ }
618
+ return parsed;
619
+ }
620
+ /**
621
+ * One parse per file for the length of `run`, however many times it is asked for.
622
+ *
623
+ * Walking a requires chain parses each module in it, and each parse gathers what it inherits
624
+ * by parsing the modules below - so looking across a project of long chains asks for the
625
+ * same files thousands of times. Inside `run` they are parsed once. Outside it nothing is
626
+ * kept, so an edit made between two calls is always seen: the cache is for one question
627
+ * about the specs, not for the life of the process.
628
+ */
629
+ export function withParseCache(run) {
630
+ if (activeCache !== undefined)
631
+ return run();
632
+ activeCache = Object.assign(new Map(), { modules: new Map(), fingerprints: new Map() });
633
+ try {
634
+ return run();
635
+ }
636
+ finally {
637
+ activeCache = undefined;
638
+ }
639
+ }
640
+ let activeCache;
641
+ const lastParses = new Map();
642
+ const KEPT_PARSES = 200;
643
+ function remember(key, entry) {
644
+ lastParses.delete(key);
645
+ lastParses.set(key, entry);
646
+ while (lastParses.size > KEPT_PARSES)
647
+ lastParses.delete(lastParses.keys().next().value);
648
+ }
649
+ function filesFingerprint(templateDirs) {
650
+ try {
651
+ const parts = [];
652
+ for (const dir of [process.cwd(), ...templateDirs]) {
653
+ let entries;
654
+ try {
655
+ entries = readdirSync(dir, { withFileTypes: true });
656
+ }
657
+ catch {
658
+ parts.push(`${dir}:-`);
659
+ continue;
660
+ }
661
+ for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
662
+ if (!entry.isFile())
663
+ continue;
664
+ const path = resolve(dir, entry.name);
665
+ const stat = statSync(path);
666
+ parts.push(`${path}:${stat.size}:${stat.mtimeMs}${entry.name.endsWith(".plain") ? `:${readFileSync(path, "utf8")}` : ""}`);
667
+ }
668
+ }
669
+ return createHash("sha1").update(parts.join("\n")).digest("hex");
670
+ }
671
+ catch {
672
+ return undefined;
673
+ }
674
+ }
675
+ /**
676
+ * The functional specs a module inherits through `requires`, bottom of the chain first.
677
+ *
678
+ * This is what `requires` means: the required module's code is this module's starting
679
+ * point, so its functional specs - and, transitively, its ancestors' - are previous
680
+ * functional specs here. Each required module's own tree already carries what it inherits,
681
+ * so taking that and then its own specs walks the whole chain; a module reached twice, as
682
+ * when `requires` lists ancestors for their exported concepts, is counted once.
683
+ */
684
+ function inheritFunctionalSpecs(requires, templateDirs, cache) {
685
+ const inherited = [];
686
+ const seen = new Set();
687
+ const add = (item) => {
688
+ const key = `${item.module}/${item.frid}`;
689
+ if (seen.has(key))
690
+ return;
691
+ seen.add(key);
692
+ inherited.push(item);
693
+ };
694
+ for (const name of requires) {
695
+ let parsed = cache.get(name);
696
+ if (parsed === undefined) {
697
+ parsed = parseModule(`${name}${PLAIN_SOURCE_FILE_EXTENSION}`, templateDirs, cache);
698
+ cache.set(name, parsed);
699
+ }
700
+ const [moduleName, tree] = parsed;
701
+ for (const item of tree[REQUIRED_FUNCTIONAL_SPECS] ?? [])
702
+ add(item);
703
+ (getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS) ?? []).forEach((item, index) => add({ ...item, module: moduleName, frid: String(index + 1) }));
704
+ }
705
+ return inherited;
706
+ }
707
+ function parseModule(plainSourceFileName, templateDirs, cache, options = {}) {
708
+ const suffix = extname(plainSourceFileName);
709
+ if (suffix !== PLAIN_SOURCE_FILE_EXTENSION) {
710
+ throw new PlainSyntaxError(`Plain syntax error: Invalid plain file extension: ${suffix}. Expected: ${PLAIN_SOURCE_FILE_EXTENSION}.`);
711
+ }
712
+ const moduleName = isAbsolute(plainSourceFileName)
713
+ ? basename(plainSourceFileName, PLAIN_SOURCE_FILE_EXTENSION)
714
+ : plainSourceFileName.slice(0, -PLAIN_SOURCE_FILE_EXTENSION.length).split("\\").join("/");
715
+ // Populated while Liquid renders; read back after the tree is marshalled.
716
+ const codeVariables = {};
717
+ const result = parsePlainFile(moduleName, codeVariables, templateDirs, [], []);
718
+ if (result.requiredConcepts.length > 0) {
719
+ throw new PlainSyntaxError("Plain syntax error: Not all required concepts were defined. Missing required concepts: " +
720
+ `${result.requiredConcepts.join(", ")}.`);
721
+ }
722
+ if (!options.allowNoFunctionalities && !hasFunctionalSpecsSection(result.plainSource)) {
723
+ // Valid as an import, but not renderable.
724
+ throw new MissingFunctionalitiesError(`Module '${moduleName}' does not have any functionality specified. ` +
725
+ "At least one functionality is required for rendering.");
726
+ }
727
+ if (!options.allowNoFunctionalities && countFunctionalities(result.plainSource) === 0) {
728
+ throw new PlainSyntaxError(`Plain syntax error: Module '${moduleName}' has an empty '${FUNCTIONAL_REQUIREMENTS}' section. ` +
729
+ "At least one functionality must be specified.");
730
+ }
731
+ validateFunctionalitiesHaveImplementationReqs(result.plainSource, moduleName, result.requiredModules.length > 0);
732
+ const exportedDefinitions = processRequiredModules(result.requiredModules, {}, templateDirs, [], []);
733
+ processExportedDefinitions(result.plainSource, exportedDefinitions);
734
+ processAcceptanceTests(result.plainSource);
735
+ checkForLinkedResources(result.plainSource, templateDirs);
736
+ const marshalled = marshallPlainSource(result.plainSource);
737
+ processCodeVariables(marshalled, codeVariables);
738
+ const validationErrors = validateConcepts(marshalled);
739
+ if (validationErrors.length > 0) {
740
+ const msg = `Found ${validationErrors.length} errors in the plain file:\n${validationErrors.join("\n")}`;
741
+ throw new PlainSyntaxError(`Plain syntax error: ${msg}`);
742
+ }
743
+ const definitions = getSpecificationItems(marshalled, DEFINITIONS);
744
+ if (definitions !== undefined)
745
+ sortDefinitions(definitions);
746
+ // What `import` brings - definitions, implementation reqs, test reqs - is already merged
747
+ // into the module's own sections. What `requires` brings is the functional specs.
748
+ const inherited = inheritFunctionalSpecs(result.requiredModules, templateDirs, cache);
749
+ if (inherited.length > 0)
750
+ marshalled[REQUIRED_FUNCTIONAL_SPECS] = inherited;
751
+ return [moduleName, marshalled, result.requiredModules];
752
+ }