@turri-dev/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +319 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +75 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +118 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
@@ -0,0 +1,76 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, extname, join, resolve } from "node:path";
3
+ import { plainFileParser, PLAIN_SOURCE_FILE_EXTENSION } from "./parser.js";
4
+ import { getFrids } from "./spec.js";
5
+ import { resolveTemplateDirs } from "../regenerate/paths.js";
6
+ export function checkPlainFile(file, templateDirs = []) {
7
+ const path = resolve(file);
8
+ if (extname(path) !== PLAIN_SOURCE_FILE_EXTENSION) {
9
+ return {
10
+ file,
11
+ ok: false,
12
+ error: `Not a ${PLAIN_SOURCE_FILE_EXTENSION} file.`,
13
+ errorType: "UsageError",
14
+ };
15
+ }
16
+ if (!existsSync(path)) {
17
+ return { file, ok: false, error: "File does not exist.", errorType: "UsageError" };
18
+ }
19
+ try {
20
+ const dirs = resolveTemplateDirs(path, templateDirs);
21
+ // A module with no functional specs yet is valid - it is how every module starts, and how
22
+ // an import module always is. It is reported, not failed.
23
+ const [moduleName, tree, requiredModules] = plainFileParser(basename(path), dirs, { allowNoFunctionalities: true });
24
+ return {
25
+ file,
26
+ ok: true,
27
+ moduleName,
28
+ frids: [...getFrids(tree)],
29
+ requiredModules,
30
+ };
31
+ }
32
+ catch (error) {
33
+ return {
34
+ file,
35
+ ok: false,
36
+ error: error instanceof Error ? error.message : String(error),
37
+ errorType: error instanceof Error ? error.name : "Error",
38
+ };
39
+ }
40
+ }
41
+ /** Every `.plain` file directly in a directory, or the single file given. */
42
+ export function resolvePlainTargets(target) {
43
+ const path = resolve(target);
44
+ if (existsSync(path) && statSync(path).isDirectory()) {
45
+ return readdirSync(path)
46
+ .filter((entry) => entry.endsWith(PLAIN_SOURCE_FILE_EXTENSION))
47
+ .sort()
48
+ .map((entry) => join(path, entry));
49
+ }
50
+ return [path];
51
+ }
52
+ export function checkPlainTargets(target, templateDirs = []) {
53
+ return resolvePlainTargets(target).map((file) => checkPlainFile(file, templateDirs));
54
+ }
55
+ /** One line per file, plus the parser's message under any that failed. */
56
+ export function formatCheckResults(results) {
57
+ const lines = [];
58
+ for (const result of results) {
59
+ const name = basename(result.file);
60
+ if (result.ok) {
61
+ const frids = result.frids ?? [];
62
+ const requires = result.requiredModules && result.requiredModules.length > 0
63
+ ? `, requires ${result.requiredModules.join(", ")}`
64
+ : "";
65
+ lines.push(frids.length === 0
66
+ ? `PASS ${name} no functional specs yet${requires}`
67
+ : `PASS ${name} ${frids.length} functionalit${frids.length === 1 ? "y" : "ies"} (${frids.join(", ")})${requires}`);
68
+ }
69
+ else {
70
+ lines.push(`FAIL ${name} ${result.errorType ?? "Error"}`);
71
+ for (const line of (result.error ?? "").split("\n"))
72
+ lines.push(` ${line}`);
73
+ }
74
+ }
75
+ return lines.join("\n");
76
+ }
@@ -0,0 +1,213 @@
1
+ import { PlainSyntaxError } from "./errors.js";
2
+ import { renderToMarkdown } from "./markdown.js";
3
+ import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, getSpecificationItems, } from "./spec.js";
4
+ /** Port of concept_utils.py. */
5
+ /** Reserved concept names that must not be redefined in the definitions section. */
6
+ export const DEFAULT_CONCEPTS = new Set([
7
+ ":ConformanceTests:",
8
+ ":UnitTests:",
9
+ ":AcceptanceTest:",
10
+ ":AcceptanceTests:",
11
+ ":Implementation:",
12
+ ":plainDefinitions:",
13
+ ":plainImplementationReqs:",
14
+ ":plainFunctionality:",
15
+ ":plainTestReqs:",
16
+ ":plainImplementationCode:",
17
+ ]);
18
+ const DEFINITION_PATTERN = /-\s(:[^:]+:)(?:,\s*:[^:]+:)*/;
19
+ const CONCEPT_CANDIDATE_PATTERN = /:[^:]+:/g;
20
+ const VALID_CONCEPT_PATTERN = /^:[+\-.0-9A-Z_a-z]+:/;
21
+ const SPEC_CONCEPT_PATTERN = /:[+\-.0-9A-Z_a-z]+:/g;
22
+ export function extractConceptsFromDefinition(text) {
23
+ const match = DEFINITION_PATTERN.exec(text);
24
+ if (!match) {
25
+ return [
26
+ [],
27
+ [
28
+ `Syntax error: Invalid definition specification text: ${text}. Should start with \`- :<concept>:\` ` +
29
+ "(where <concept> is any string that does not contain `:`).",
30
+ ],
31
+ ];
32
+ }
33
+ const candidates = match[0].match(CONCEPT_CANDIDATE_PATTERN) ?? [];
34
+ const valid = [];
35
+ const errors = [];
36
+ for (const candidate of candidates) {
37
+ if (VALID_CONCEPT_PATTERN.test(candidate)) {
38
+ valid.push(candidate);
39
+ }
40
+ else {
41
+ errors.push(`Syntax error: Invalid concept: ${candidate}. Should contain only letters, numbers, hyphens and dots.`);
42
+ }
43
+ }
44
+ return [valid, errors];
45
+ }
46
+ export function extractConceptsFromSpecText(text) {
47
+ return text.match(SPEC_CONCEPT_PATTERN) ?? [];
48
+ }
49
+ export function collectConcepts(plainSource) {
50
+ const definitions = getSpecificationItems(plainSource, DEFINITIONS);
51
+ if (definitions === undefined)
52
+ return [[], []];
53
+ const concepts = [];
54
+ const errors = [];
55
+ for (const definition of definitions) {
56
+ const [newConcepts, newErrors] = extractConceptsFromDefinition(definition.markdown);
57
+ errors.push(...newErrors);
58
+ concepts.push(...newConcepts);
59
+ }
60
+ return [concepts, errors];
61
+ }
62
+ function validateConceptsInSpec(spec, concepts, specGroup) {
63
+ const errors = [];
64
+ for (const used of extractConceptsFromSpecText(spec.markdown)) {
65
+ if (!concepts.has(used) && !DEFAULT_CONCEPTS.has(used)) {
66
+ errors.push(`Syntax error: Concept ${used} is not defined in the definitions. ${specGroup}: ${spec.markdown}`);
67
+ }
68
+ }
69
+ return errors;
70
+ }
71
+ function validateConceptsInSpecGroup(specGroup, plainSource, concepts) {
72
+ const specs = getSpecificationItems(plainSource, specGroup);
73
+ if (specs === undefined)
74
+ return [];
75
+ return specs.flatMap((spec) => validateConceptsInSpec(spec, concepts, specGroup));
76
+ }
77
+ export function validateConcepts(marshalled) {
78
+ const errors = [];
79
+ const concepts = new Set(DEFAULT_CONCEPTS);
80
+ const [newConcepts, newErrors] = collectConcepts(marshalled);
81
+ errors.push(...newErrors);
82
+ const uniqueNewConcepts = new Set(newConcepts);
83
+ const intersection = [...uniqueNewConcepts].filter((c) => concepts.has(c));
84
+ if (uniqueNewConcepts.size < newConcepts.length || intersection.length > 0) {
85
+ const misused = new Set(newConcepts.filter((c) => newConcepts.filter((o) => o === c).length > 1));
86
+ for (const c of intersection)
87
+ misused.add(c);
88
+ errors.push(`Syntax error: Concepts were defined multiple times: ${[...misused].join(", ")} .`);
89
+ }
90
+ const treeBasedConcepts = new Set([...DEFAULT_CONCEPTS, ...uniqueNewConcepts]);
91
+ for (const specGroup of [
92
+ NON_FUNCTIONAL_REQUIREMENTS,
93
+ TEST_REQUIREMENTS,
94
+ FUNCTIONAL_REQUIREMENTS,
95
+ DEFINITIONS,
96
+ ]) {
97
+ errors.push(...validateConceptsInSpecGroup(specGroup, marshalled, treeBasedConcepts));
98
+ }
99
+ for (const funcSpec of getSpecificationItems(marshalled, FUNCTIONAL_REQUIREMENTS) ?? []) {
100
+ for (const test of funcSpec[ACCEPTANCE_TESTS] ?? []) {
101
+ errors.push(...validateConceptsInSpec(test, treeBasedConcepts, ACCEPTANCE_TESTS));
102
+ }
103
+ }
104
+ return errors;
105
+ }
106
+ export function findConceptDefinitionsInPlainSource(concept, definitions) {
107
+ const found = [];
108
+ if (definitions === null)
109
+ return found;
110
+ for (const definition of definitions.children) {
111
+ const rendered = renderToMarkdown(definition);
112
+ const [definedConcepts] = extractConceptsFromDefinition(rendered);
113
+ if (!definedConcepts.includes(concept))
114
+ continue;
115
+ found.push(definition);
116
+ const usedConcepts = extractConceptsFromSpecText(rendered).filter((used) => used !== concept && !DEFAULT_CONCEPTS.has(used));
117
+ for (const used of usedConcepts) {
118
+ found.push(...findConceptDefinitionsInPlainSource(used, definitions));
119
+ }
120
+ }
121
+ return found;
122
+ }
123
+ function buildAdjacencyList(definitions) {
124
+ const adjacency = new Map();
125
+ const conceptDefinitions = new Map();
126
+ for (const definition of definitions) {
127
+ const text = definition.markdown;
128
+ const [newConcepts] = extractConceptsFromDefinition(text);
129
+ const usedConcepts = new Set(extractConceptsFromSpecText(text).filter((used) => !newConcepts.includes(used) && !DEFAULT_CONCEPTS.has(used)));
130
+ for (const newConcept of newConcepts) {
131
+ if (!adjacency.has(newConcept))
132
+ adjacency.set(newConcept, []);
133
+ }
134
+ for (const used of usedConcepts) {
135
+ if (!adjacency.has(used))
136
+ adjacency.set(used, []);
137
+ adjacency.get(used).push(...newConcepts);
138
+ }
139
+ for (const newConcept of newConcepts) {
140
+ conceptDefinitions.set(newConcept, definition);
141
+ }
142
+ }
143
+ return [adjacency, conceptDefinitions];
144
+ }
145
+ /**
146
+ * networkx's topological_sort, reproduced.
147
+ *
148
+ * It flattens `topological_generations`, which is Kahn's algorithm processed one
149
+ * generation at a time with nodes visited in graph insertion order. The ordering of
150
+ * independent concepts therefore depends on insertion order, and the Python tests
151
+ * assert that exact order - a generic topological sort would pass the dependency
152
+ * constraints but produce a different sequence.
153
+ */
154
+ function topologicalSort(successors) {
155
+ const inDegree = new Map();
156
+ for (const node of successors.keys())
157
+ inDegree.set(node, 0);
158
+ for (const [, targets] of successors) {
159
+ for (const target of targets)
160
+ inDegree.set(target, (inDegree.get(target) ?? 0) + 1);
161
+ }
162
+ let generation = [...successors.keys()].filter((node) => (inDegree.get(node) ?? 0) === 0);
163
+ const order = [];
164
+ const remaining = new Map(inDegree);
165
+ for (const node of generation)
166
+ remaining.delete(node);
167
+ while (generation.length > 0) {
168
+ const next = [];
169
+ for (const node of generation) {
170
+ order.push(node);
171
+ for (const child of successors.get(node) ?? []) {
172
+ const degree = (remaining.get(child) ?? 0) - 1;
173
+ remaining.set(child, degree);
174
+ if (degree === 0) {
175
+ next.push(child);
176
+ remaining.delete(child);
177
+ }
178
+ }
179
+ }
180
+ generation = next;
181
+ }
182
+ if (remaining.size > 0)
183
+ return { order, cycle: [...remaining.keys()] };
184
+ return { order, cycle: null };
185
+ }
186
+ export function sortDefinitions(definitions) {
187
+ if (definitions.length <= 1)
188
+ return;
189
+ const [adjacency, conceptDefinitions] = buildAdjacencyList(definitions);
190
+ const { order, cycle } = topologicalSort(adjacency);
191
+ if (cycle !== null) {
192
+ let msg = "Found cycles in the concept graph. Cycles are not allowed.";
193
+ const cyclicDefinitions = cycle
194
+ .slice(0, 2)
195
+ .map((concept) => conceptDefinitions.get(concept)?.markdown)
196
+ .filter((markdown) => markdown !== undefined);
197
+ msg += "Cyclic definitons:\n";
198
+ msg += cyclicDefinitions.join("\n");
199
+ msg += "\n";
200
+ throw new PlainSyntaxError(`Plain syntax error: ${msg}`);
201
+ }
202
+ if (order.length === 0)
203
+ return;
204
+ const definitionOrder = new Map();
205
+ order.forEach((concept, index) => {
206
+ // Python keys this map by definition and lets later concepts overwrite, so a
207
+ // definition that introduces several concepts takes the index of its last one.
208
+ const definition = conceptDefinitions.get(concept);
209
+ if (definition !== undefined)
210
+ definitionOrder.set(definition, index);
211
+ });
212
+ definitions.sort((a, b) => (definitionOrder.get(a) ?? 0) - (definitionOrder.get(b) ?? 0));
213
+ }
@@ -0,0 +1,43 @@
1
+ /** Ports of the exception classes in plain2code_exceptions.py that this parser raises. */
2
+ export class PlainSyntaxError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "PlainSyntaxError";
6
+ }
7
+ }
8
+ export class UnsupportedBase64Content extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "UnsupportedBase64Content";
12
+ }
13
+ }
14
+ export class ModuleDoesNotExistError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "ModuleDoesNotExistError";
18
+ }
19
+ }
20
+ export class MissingFunctionalitiesError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "MissingFunctionalitiesError";
24
+ }
25
+ }
26
+ export class ImportedModuleWithFunctionalitiesError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "ImportedModuleWithFunctionalitiesError";
30
+ }
31
+ }
32
+ export class InvalidFridArgument extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "InvalidFridArgument";
36
+ }
37
+ }
38
+ export class InvalidLiquidVariableName extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "InvalidLiquidVariableName";
42
+ }
43
+ }
@@ -0,0 +1,57 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createEnvironment } from "./liquid.js";
4
+ /** Ports of the file_utils.py and plain2code_utils.py helpers this parser needs. */
5
+ export function openFrom(dirs, fileName) {
6
+ for (const dir of dirs) {
7
+ const fullPath = join(dir, fileName);
8
+ try {
9
+ if (!statSync(fullPath).isFile())
10
+ continue;
11
+ }
12
+ catch {
13
+ continue;
14
+ }
15
+ return readFileSync(fullPath, "utf8");
16
+ }
17
+ return null;
18
+ }
19
+ const MAX_BASE64_BLOB_LENGTH = 8192;
20
+ const BASE64_BLOB_PATTERN = new RegExp(`(?:data:[\\w.+-]+/[\\w.+-]+;base64,)?[A-Za-z0-9+/_-]{${MAX_BASE64_BLOB_LENGTH},}={0,2}`);
21
+ /** First contiguous base64 blob at or above the threshold, or null. */
22
+ export function findLargeBase64Blob(text) {
23
+ const match = BASE64_BLOB_PATTERN.exec(text);
24
+ return match ? match[0] : null;
25
+ }
26
+ /**
27
+ * Render once against the filesystem to discover which templates the source pulls in.
28
+ *
29
+ * The rendered text is discarded by the caller; what matters is the map of template
30
+ * name to raw source, which is what the second (real) render is given so that it does
31
+ * not touch the filesystem again.
32
+ */
33
+ export function getLoadedTemplates(templateDirs, plainSource) {
34
+ const loadedTemplates = {};
35
+ const engine = createEnvironment({
36
+ strictVariables: true,
37
+ loadTemplateSource: (name) => {
38
+ const source = openFrom(templateDirs, name);
39
+ if (source === null)
40
+ return null;
41
+ loadedTemplates[name] = source;
42
+ return source;
43
+ },
44
+ });
45
+ let rendered;
46
+ try {
47
+ rendered = engine.parseAndRenderSync(plainSource, {});
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ if (/undefined variable/i.test(message)) {
52
+ throw new Error(`Undefined liquid variable: ${message}`);
53
+ }
54
+ throw error;
55
+ }
56
+ return [rendered, loadedTemplates];
57
+ }
@@ -0,0 +1,195 @@
1
+ import { readFileSync } from "node:fs";
2
+ const compile = (source) => new RegExp(source, "dg");
3
+ const namesOf = (captures) => captures === undefined ? undefined : Object.fromEntries(Object.entries(captures).map(([group, capture]) => [group, capture.name]));
4
+ function readGrammar(raw) {
5
+ const repository = new Map();
6
+ const root = [];
7
+ const read = (rules, into, seen) => {
8
+ for (const rule of rules) {
9
+ if (rule.include !== undefined) {
10
+ // Only the repository's: plyn's grammar includes nothing else, and a rule that includes itself is followed once.
11
+ if (!rule.include.startsWith("#"))
12
+ continue;
13
+ const key = rule.include.slice(1);
14
+ if (seen.has(key))
15
+ continue;
16
+ into.push(...repositoryRules(key, seen));
17
+ continue;
18
+ }
19
+ if (rule.match !== undefined) {
20
+ into.push({ kind: "match", ...(rule.name !== undefined ? { name: rule.name } : {}), re: compile(rule.match), ...(rule.captures !== undefined ? { captures: namesOf(rule.captures) } : {}) });
21
+ }
22
+ else if (rule.begin !== undefined && rule.end !== undefined) {
23
+ const inner = [];
24
+ read(rule.patterns ?? [], inner, seen);
25
+ into.push({
26
+ kind: "block",
27
+ ...(rule.name !== undefined ? { name: rule.name } : {}),
28
+ begin: compile(rule.begin),
29
+ end: compile(rule.end),
30
+ patterns: inner,
31
+ ...(rule.beginCaptures ?? rule.captures ? { beginCaptures: namesOf(rule.beginCaptures ?? rule.captures) } : {}),
32
+ ...(rule.endCaptures ?? rule.captures ? { endCaptures: namesOf(rule.endCaptures ?? rule.captures) } : {}),
33
+ });
34
+ }
35
+ else if (rule.patterns !== undefined) {
36
+ read(rule.patterns, into, seen);
37
+ }
38
+ }
39
+ };
40
+ const repositoryRules = (key, seen) => {
41
+ const known = repository.get(key);
42
+ if (known !== undefined)
43
+ return known;
44
+ const entry = raw.repository?.[key];
45
+ if (entry === undefined)
46
+ return [];
47
+ const rules = [];
48
+ repository.set(key, rules);
49
+ read([entry], rules, new Set([...seen, key]));
50
+ return rules;
51
+ };
52
+ read(raw.patterns, root, new Set());
53
+ return root;
54
+ }
55
+ let grammar;
56
+ /** plyn's grammar, read once - or null when it cannot be. */
57
+ function plainGrammar() {
58
+ if (grammar !== undefined)
59
+ return grammar;
60
+ try {
61
+ grammar = readGrammar(JSON.parse(readFileSync(new URL("./plain.tmLanguage.json", import.meta.url), "utf8")));
62
+ }
63
+ catch {
64
+ grammar = null;
65
+ }
66
+ return grammar;
67
+ }
68
+ /** The pieces of a match: its capture groups under their scopes, the rest under the rule's. */
69
+ function piecesOf(match, scopes, name, captures) {
70
+ const own = name !== undefined ? [...scopes, name] : scopes;
71
+ const [start, end] = match.indices[0];
72
+ const groups = Object.entries(captures ?? {})
73
+ .map(([group, scope]) => ({ scope, at: match.indices[Number(group)] }))
74
+ .filter((group) => group.at !== undefined && group.at[1] > group.at[0])
75
+ .sort((a, b) => a.at[0] - b.at[0]);
76
+ const whole = match.input;
77
+ const tokens = [];
78
+ let at = start;
79
+ for (const group of groups) {
80
+ if (group.at[0] < at)
81
+ continue;
82
+ if (group.at[0] > at)
83
+ tokens.push({ text: whole.slice(at, group.at[0]), scopes: own });
84
+ tokens.push({ text: whole.slice(group.at[0], group.at[1]), scopes: group.scope !== undefined ? [...own, group.scope] : own });
85
+ at = group.at[1];
86
+ }
87
+ if (end > at)
88
+ tokens.push({ text: whole.slice(at, end), scopes: own });
89
+ return tokens;
90
+ }
91
+ /**
92
+ * One line of ***plain, highlighted: its tokens, and the state the next line starts from - the blocks
93
+ * still open at its end.
94
+ */
95
+ export function highlightLine(line, state = []) {
96
+ const rules = plainGrammar();
97
+ if (rules === null)
98
+ return { tokens: line === "" ? [] : [{ text: line, scopes: [] }], state };
99
+ const stack = [...state];
100
+ const tokens = [];
101
+ const scopesNow = () => stack.map((block) => block.name).filter((name) => name !== undefined);
102
+ const push = (text, scopes) => {
103
+ if (text === "")
104
+ return;
105
+ const last = tokens.at(-1);
106
+ if (last !== undefined && last.scopes.join(" ") === scopes.join(" "))
107
+ last.text += text;
108
+ else
109
+ tokens.push({ text, scopes });
110
+ };
111
+ let at = 0;
112
+ // A rule that matches nothing at a place moves the place on by one: every line ends.
113
+ while (at <= line.length) {
114
+ const top = stack.at(-1);
115
+ const candidates = top?.patterns ?? rules;
116
+ let best;
117
+ const tryRe = (re, rule) => {
118
+ re.lastIndex = at;
119
+ const match = re.exec(line);
120
+ if (match === null)
121
+ return;
122
+ // The first to match earliest wins; the end of a block before the rules inside it.
123
+ if (best === undefined || match.index < best.match.index)
124
+ best = { match, rule };
125
+ };
126
+ if (top !== undefined)
127
+ tryRe(top.end, "end");
128
+ for (const rule of candidates)
129
+ tryRe(rule.kind === "match" ? rule.re : rule.begin, rule);
130
+ if (best === undefined) {
131
+ push(line.slice(at), scopesNow());
132
+ break;
133
+ }
134
+ const { match, rule } = best;
135
+ push(line.slice(at, match.index), scopesNow());
136
+ const scopes = scopesNow();
137
+ if (rule === "end") {
138
+ const block = stack.pop();
139
+ for (const token of piecesOf(match, scopes.slice(0, block.name !== undefined ? -1 : undefined), block.name, block.endCaptures))
140
+ push(token.text, token.scopes);
141
+ }
142
+ else if (rule.kind === "match") {
143
+ for (const token of piecesOf(match, scopes, rule.name, rule.captures))
144
+ push(token.text, token.scopes);
145
+ }
146
+ else {
147
+ for (const token of piecesOf(match, scopes, rule.name, rule.beginCaptures))
148
+ push(token.text, token.scopes);
149
+ stack.push(rule);
150
+ }
151
+ const next = match.index + match[0].length;
152
+ if (next === at && match[0].length === 0) {
153
+ // Nothing matched here: take one character as it is, and go on.
154
+ push(line.slice(at, at + 1), scopesNow());
155
+ at += 1;
156
+ }
157
+ else
158
+ at = next;
159
+ if (at >= line.length)
160
+ break;
161
+ }
162
+ return { tokens, state: stack };
163
+ }
164
+ /** Lines of ***plain, highlighted one after another - a block opened on one carried to the next. */
165
+ export function highlightLines(lines) {
166
+ let state = [];
167
+ return lines.map((line) => {
168
+ const result = highlightLine(line, state);
169
+ state = result.state;
170
+ return result.tokens;
171
+ });
172
+ }
173
+ /** A scope's role, by what it is - the most specific first. None: the text around it. */
174
+ const ROLES = [
175
+ ["comment", "comment"],
176
+ ["keyword", "keyword"],
177
+ ["entity.name.function", "section"],
178
+ ["entity.name.type", "module"],
179
+ ["entity.name.module", "module"],
180
+ ["entity.punctuation", "bullet"],
181
+ ["punctuation", "bullet"],
182
+ ["variable", "concept"],
183
+ // A link is plyn's `string.quoted.other.plain`; its quotes are `string.quoted.double`, `...other.backtick` and the like.
184
+ ["string.quoted.other.plain", "link"],
185
+ ["string", "literal"],
186
+ ];
187
+ /** A token's role: its innermost scope that has one. */
188
+ export function roleOf(scopes) {
189
+ for (const scope of [...scopes].reverse()) {
190
+ const found = ROLES.find(([prefix]) => scope === prefix || scope.startsWith(`${prefix}.`));
191
+ if (found !== undefined)
192
+ return found[1];
193
+ }
194
+ return undefined;
195
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The ***plain*** parser: a TypeScript port of the reference implementation.
3
+ *
4
+ * Mirrors plain_file.py, plain_spec.py, concept_utils.py and the file_utils.py
5
+ * helpers they depend on. The entry points are `plainFileParser` (a .plain file to a
6
+ * marshalled tree) and `iterateFunctionalSpecs` (the tree to one rendering context per
7
+ * functionality).
8
+ */
9
+ export * from "./errors.js";
10
+ export * from "./markdown.js";
11
+ export * from "./concepts.js";
12
+ export * from "./files.js";
13
+ export * from "./liquid.js";
14
+ export * from "./parser.js";
15
+ export * from "./spec.js";