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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +6 -6
  2. package/dist/advisor-chat.d.ts +63 -0
  3. package/dist/advisor-chat.js +290 -0
  4. package/dist/background-skills.d.ts +53 -0
  5. package/dist/background-skills.js +121 -0
  6. package/dist/bedrock-region.d.ts +66 -0
  7. package/dist/bedrock-region.js +144 -0
  8. package/dist/btw.d.ts +1 -1
  9. package/dist/btw.js +1 -1
  10. package/dist/change-plain-cli.js +20 -13
  11. package/dist/chat-idle.d.ts +7 -0
  12. package/dist/chat-idle.js +20 -0
  13. package/dist/chats.d.ts +10 -0
  14. package/dist/chats.js +23 -0
  15. package/dist/cli-options.d.ts +1 -2
  16. package/dist/cli-options.js +2 -7
  17. package/dist/cli.js +11 -3
  18. package/dist/config-command.d.ts +14 -0
  19. package/dist/config-command.js +35 -15
  20. package/dist/config-edit.d.ts +3 -3
  21. package/dist/config-edit.js +16 -12
  22. package/dist/config.d.ts +54 -34
  23. package/dist/config.js +40 -44
  24. package/dist/conform-cli.js +12 -10
  25. package/dist/conformance-switch.d.ts +34 -0
  26. package/dist/conformance-switch.js +108 -0
  27. package/dist/cultivate-cli.js +15 -5
  28. package/dist/drift-cli.js +2 -0
  29. package/dist/extension.d.ts +15 -0
  30. package/dist/extension.js +260 -83
  31. package/dist/host-session.d.ts +26 -0
  32. package/dist/host-session.js +30 -2
  33. package/dist/host.d.ts +5 -2
  34. package/dist/host.js +7 -1
  35. package/dist/init-config.d.ts +1 -1
  36. package/dist/init-config.js +20 -11
  37. package/dist/loop-cli.d.ts +3 -3
  38. package/dist/loop-cli.js +13 -16
  39. package/dist/loop-tools.js +18 -10
  40. package/dist/model-check.d.ts +8 -3
  41. package/dist/model-check.js +13 -8
  42. package/dist/new-project.js +12 -8
  43. package/dist/{validate-cli.js → parse-cli.js} +11 -5
  44. package/dist/parse-tool.d.ts +9 -0
  45. package/dist/parse-tool.js +33 -0
  46. package/dist/pi-launch.js +1 -1
  47. package/dist/plain/check.d.ts +53 -6
  48. package/dist/plain/check.js +217 -24
  49. package/dist/plain/concepts.d.ts +8 -0
  50. package/dist/plain/concepts.js +66 -23
  51. package/dist/plain/cycles.d.ts +21 -0
  52. package/dist/plain/cycles.js +190 -0
  53. package/dist/plain/errors.d.ts +4 -0
  54. package/dist/plain/errors.js +7 -0
  55. package/dist/plain/files.d.ts +18 -1
  56. package/dist/plain/files.js +39 -2
  57. package/dist/plain/graph.d.ts +33 -4
  58. package/dist/plain/graph.js +60 -12
  59. package/dist/plain/highlight.d.ts +3 -2
  60. package/dist/plain/highlight.js +4 -3
  61. package/dist/plain/index.d.ts +8 -5
  62. package/dist/plain/index.js +8 -5
  63. package/dist/plain/liquid.js +1 -6
  64. package/dist/plain/markdown.d.ts +32 -3
  65. package/dist/plain/markdown.js +146 -24
  66. package/dist/plain/module-concepts.js +2 -2
  67. package/dist/plain/parser.d.ts +83 -28
  68. package/dist/plain/parser.js +381 -260
  69. package/dist/plain/plain.tmLanguage.json +29 -3
  70. package/dist/plain/project.d.ts +48 -0
  71. package/dist/plain/project.js +155 -0
  72. package/dist/plain/spec.d.ts +90 -28
  73. package/dist/plain/spec.js +107 -133
  74. package/dist/plain/test-plain-file.d.ts +59 -0
  75. package/dist/plain/test-plain-file.js +232 -0
  76. package/dist/regenerate/activity.d.ts +20 -9
  77. package/dist/regenerate/activity.js +11 -4
  78. package/dist/regenerate/advisor-records.d.ts +93 -0
  79. package/dist/regenerate/advisor-records.js +110 -0
  80. package/dist/regenerate/advisor-runs.d.ts +87 -0
  81. package/dist/regenerate/advisor-runs.js +327 -0
  82. package/dist/regenerate/base-folder.d.ts +68 -0
  83. package/dist/regenerate/base-folder.js +168 -0
  84. package/dist/regenerate/cases.d.ts +9 -6
  85. package/dist/regenerate/cases.js +71 -15
  86. package/dist/regenerate/change-view.d.ts +8 -3
  87. package/dist/regenerate/change-view.js +71 -22
  88. package/dist/regenerate/chunk-plan.d.ts +36 -15
  89. package/dist/regenerate/chunk-plan.js +165 -59
  90. package/dist/regenerate/conform.d.ts +3 -0
  91. package/dist/regenerate/conform.js +6 -5
  92. package/dist/regenerate/conformance-agents.d.ts +18 -39
  93. package/dist/regenerate/conformance-agents.js +60 -88
  94. package/dist/regenerate/conformance.d.ts +23 -20
  95. package/dist/regenerate/conformance.js +118 -121
  96. package/dist/regenerate/diff-view.d.ts +1 -1
  97. package/dist/regenerate/diff-view.js +20 -4
  98. package/dist/regenerate/drift.d.ts +47 -15
  99. package/dist/regenerate/drift.js +101 -30
  100. package/dist/regenerate/jobs.d.ts +2 -2
  101. package/dist/regenerate/jobs.js +1 -1
  102. package/dist/regenerate/ledger.d.ts +19 -0
  103. package/dist/regenerate/loop.d.ts +61 -49
  104. package/dist/regenerate/loop.js +473 -340
  105. package/dist/regenerate/paths.d.ts +4 -0
  106. package/dist/regenerate/paths.js +7 -1
  107. package/dist/regenerate/plain-palette.js +2 -0
  108. package/dist/regenerate/prompt.d.ts +57 -19
  109. package/dist/regenerate/prompt.js +157 -122
  110. package/dist/regenerate/records.d.ts +43 -12
  111. package/dist/regenerate/records.js +102 -88
  112. package/dist/regenerate/regenerate.js +29 -11
  113. package/dist/regenerate/regression.d.ts +4 -4
  114. package/dist/regenerate/regression.js +14 -10
  115. package/dist/regenerate/renames.d.ts +42 -0
  116. package/dist/regenerate/renames.js +185 -0
  117. package/dist/regenerate/requires.d.ts +1 -1
  118. package/dist/regenerate/requires.js +10 -3
  119. package/dist/regenerate/resources.d.ts +5 -3
  120. package/dist/regenerate/resources.js +4 -4
  121. package/dist/regenerate/scripts-being-written.d.ts +19 -0
  122. package/dist/regenerate/scripts-being-written.js +30 -0
  123. package/dist/regenerate/spec-map-view.d.ts +2 -2
  124. package/dist/regenerate/spec-map-view.js +46 -15
  125. package/dist/regenerate/spec-map.d.ts +27 -5
  126. package/dist/regenerate/spec-map.js +92 -60
  127. package/dist/regenerate/spec-steps.d.ts +48 -5
  128. package/dist/regenerate/spec-steps.js +213 -28
  129. package/dist/regenerate/state.d.ts +4 -3
  130. package/dist/regenerate/stop-report.d.ts +1 -1
  131. package/dist/regenerate/stop-report.js +10 -10
  132. package/dist/regenerate/test-advisor.d.ts +101 -0
  133. package/dist/regenerate/test-advisor.js +379 -0
  134. package/dist/regenerate/test-kinds.d.ts +69 -31
  135. package/dist/regenerate/test-kinds.js +109 -80
  136. package/dist/regenerate/test-scripts.d.ts +2 -0
  137. package/dist/regenerate/test-scripts.js +4 -0
  138. package/dist/regenerate/testing-steps.d.ts +41 -0
  139. package/dist/regenerate/testing-steps.js +110 -0
  140. package/dist/regenerate/testing.d.ts +1 -1
  141. package/dist/regenerate/testing.js +5 -4
  142. package/dist/regenerate/unit-review.d.ts +1 -1
  143. package/dist/regenerate/unit-review.js +3 -3
  144. package/dist/regenerate-cli.js +11 -12
  145. package/dist/release-agent.js +4 -0
  146. package/dist/release-cli.js +2 -0
  147. package/dist/release-tools.js +2 -0
  148. package/dist/run-report.d.ts +2 -2
  149. package/dist/run-report.js +4 -3
  150. package/dist/self-invocation.d.ts +1 -1
  151. package/dist/self-invocation.js +1 -1
  152. package/dist/sentinel.js +1 -1
  153. package/dist/setup.d.ts +10 -0
  154. package/dist/setup.js +52 -4
  155. package/dist/skill.d.ts +10 -0
  156. package/dist/skill.js +6 -0
  157. package/dist/skills.js +3 -1
  158. package/dist/spec-tools.js +21 -31
  159. package/dist/test-plain-files.d.ts +41 -0
  160. package/dist/test-plain-files.js +177 -0
  161. package/docs/README.md +3 -1
  162. package/docs/authoring-plain.md +46 -13
  163. package/docs/chats.md +3 -1
  164. package/docs/config.md +20 -15
  165. package/docs/deploy.md +1 -1
  166. package/docs/development.md +2 -1
  167. package/docs/headless.md +15 -18
  168. package/docs/hosting.md +28 -12
  169. package/docs/installation.md +1 -0
  170. package/docs/parser.md +88 -27
  171. package/docs/regenerating.md +89 -54
  172. package/docs/sentinel.md +1 -1
  173. package/docs/skills-and-gates.md +3 -1
  174. package/docs/test-advisor.md +110 -0
  175. package/docs/theming.md +1 -1
  176. package/docs/using.md +17 -15
  177. package/genome/rules/bullet-continuation.md +8 -8
  178. package/genome/rules/concise-specs-examples.md +111 -80
  179. package/genome/rules/concise-specs-integration-examples.md +56 -41
  180. package/genome/rules/concise-specs.md +17 -16
  181. package/genome/rules/definitions.md +5 -5
  182. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
  183. package/genome/rules/func-specs.md +69 -74
  184. package/genome/rules/impl-reqs.md +4 -4
  185. package/genome/rules/integration-embedded-testing.md +20 -20
  186. package/genome/rules/integration-embedded.md +31 -31
  187. package/genome/rules/integration-standalone.md +9 -9
  188. package/genome/rules/integrations.md +18 -18
  189. package/genome/rules/linked-resources.md +19 -16
  190. package/genome/rules/module-structure.md +21 -14
  191. package/genome/rules/project-model.md +18 -17
  192. package/genome/rules/required-concepts.md +1 -1
  193. package/genome/rules/requires-modules.md +6 -5
  194. package/genome/rules/semantic-ids.md +49 -0
  195. package/genome/rules/stdlib.md +4 -5
  196. package/genome/rules/test-plain-files.md +130 -0
  197. package/genome/rules/test-reqs.md +8 -8
  198. package/genome/rules/turri-regeneration.md +24 -8
  199. package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
  200. package/genome/skills/analyze-func-specs/SKILL.md +46 -32
  201. package/genome/skills/break-down-func-spec/SKILL.md +91 -67
  202. package/genome/skills/consolidate-concepts/SKILL.md +10 -10
  203. package/genome/skills/convert-old-syntax/SKILL.md +68 -0
  204. package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
  205. package/genome/skills/debug-specs/SKILL.md +62 -56
  206. package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
  207. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
  208. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
  209. package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
  210. package/genome/skills/refactor-module/SKILL.md +35 -35
  211. package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
  212. package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
  213. package/package.json +7 -6
  214. package/dist/regenerate/choose-conformance.d.ts +0 -32
  215. package/dist/regenerate/choose-conformance.js +0 -117
  216. /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
@@ -1,44 +1,55 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { InvalidFridArgument } from "./errors.js";
3
- /** Port of plain_spec.py. */
3
+ /**
4
+ * Port of plain_spec.py. The reference keeps its names for a functional spec's id - `frid`, `get_frids` - and so do
5
+ * the ports here: a frid is the functional spec's func-id, the `# <func-id>` heading it is written under.
6
+ */
4
7
  export const DEFINITIONS = "definitions";
5
8
  export const NON_FUNCTIONAL_REQUIREMENTS = "implementation reqs";
6
9
  export const TEST_REQUIREMENTS = "test reqs";
7
10
  export const FUNCTIONAL_REQUIREMENTS = "functional specs";
11
+ export const CONFORMANCE_TESTS = "conformance_tests";
8
12
  export const ACCEPTANCE_TESTS = "acceptance_tests";
13
+ export const CONFORMANCE_TEST_HEADING = "conformance tests";
9
14
  export const ACCEPTANCE_TEST_HEADING = "acceptance tests";
10
15
  /**
11
16
  * Where a module's tree carries the functional specs it inherits through `requires`: every
12
- * functional spec of every module it requires, transitively, bottom of the chain first.
13
- * Kept apart from its own, which are numbered 1, 2, 3 on their own.
17
+ * functional spec of every module it requires, transitively, bottom of the chain first, each
18
+ * with its module. turri's, not the reference parser's.
14
19
  */
15
20
  export const REQUIRED_FUNCTIONAL_SPECS = "required functional specs";
16
- export const ALLOWED_SPECIFICATION_HEADINGS = [
17
- DEFINITIONS,
18
- NON_FUNCTIONAL_REQUIREMENTS,
19
- TEST_REQUIREMENTS,
20
- FUNCTIONAL_REQUIREMENTS,
21
- ACCEPTANCE_TEST_HEADING,
22
- ];
23
- export const ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [
24
- DEFINITIONS,
25
- NON_FUNCTIONAL_REQUIREMENTS,
26
- TEST_REQUIREMENTS,
27
- ];
21
+ /**
22
+ * Where a module's tree carries the test plain files bound to its chain, as the reference's parse output has them
23
+ * (`test_plain_files`): each file's path, from the module's folder, to the functional spec it binds to. turri's.
24
+ */
25
+ export const TEST_PLAIN_FILES = "test plain files";
26
+ /** The sections whose content is a list of `- ` items. */
27
+ export const LIST_SECTION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS];
28
+ export const ALLOWED_SPECIFICATION_HEADINGS = [...LIST_SECTION_HEADINGS, FUNCTIONAL_REQUIREMENTS];
29
+ export const ALLOWED_IMPORT_SPECIFICATION_HEADINGS = LIST_SECTION_HEADINGS;
30
+ /**
31
+ * A func-id or scenario-id: segments of lowercase letters and digits, with one hyphen between segments. So a hyphen
32
+ * never starts or ends an id, two hyphens never touch, and an id is never only hyphens.
33
+ */
34
+ export const SEMANTIC_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
+ export function isSemanticId(text) {
36
+ return SEMANTIC_ID_PATTERN.test(text);
37
+ }
28
38
  /**
29
39
  * The functional specs a module inherits through `requires`, rendered: each with its
30
- * module, its id there, and its acceptance tests.
40
+ * module, its func-id, and its scenarios.
31
41
  */
32
42
  export function getInheritedFunctionalSpecs(tree, replaceCodeVariables = true) {
33
43
  const items = tree[REQUIRED_FUNCTIONAL_SPECS] ?? [];
34
44
  return items.map((item) => {
35
45
  const codeVariables = {};
36
- const tests = item[ACCEPTANCE_TESTS];
46
+ const scenarios = (list) => (list ?? []).map((scenario) => ({ id: scenario.id, text: getSpecificationItemMarkdown(scenario, codeVariables, replaceCodeVariables, "text") }));
37
47
  return {
38
48
  module: item.module,
39
- frid: item.frid,
49
+ funcId: item.id,
40
50
  spec: getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables),
41
- acceptanceTests: tests?.map((test) => getSpecificationItemMarkdown(test, codeVariables, replaceCodeVariables)),
51
+ conformanceTests: scenarios(item[CONFORMANCE_TESTS]),
52
+ acceptanceTests: scenarios(item[ACCEPTANCE_TESTS]),
42
53
  };
43
54
  });
44
55
  }
@@ -46,13 +57,14 @@ export function getSpecificationItems(tree, heading) {
46
57
  const value = tree[heading];
47
58
  return Array.isArray(value) ? value : undefined;
48
59
  }
60
+ /** The functional specs of a tree, in file order. */
61
+ export function getFunctionalSpecs(tree) {
62
+ const value = tree[FUNCTIONAL_REQUIREMENTS];
63
+ return Array.isArray(value) ? value : [];
64
+ }
65
+ /** True if any functional spec of the tree has acceptance scenarios. */
49
66
  export function hasAcceptanceTests(tree) {
50
- for (const requirement of getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS) ?? []) {
51
- const tests = requirement[ACCEPTANCE_TESTS];
52
- if (tests !== undefined && tests.length > 0)
53
- return true;
54
- }
55
- return false;
67
+ return getFunctionalSpecs(tree).some((item) => (item[ACCEPTANCE_TESTS] ?? []).length > 0);
56
68
  }
57
69
  export function collectSpecificationLinkedResources(specification, specificationHeading, linkedResourcesList) {
58
70
  for (const resource of specification.linked_resources ?? []) {
@@ -86,59 +98,49 @@ export function collectSpecificationLinkedResources(specification, specification
86
98
  }
87
99
  }
88
100
  }
89
- export function collectLinkedResourcesInSection(section, linkedResourcesList, specificationsList, includeAcceptanceTests, frid) {
90
- let toCollect = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS];
91
- if (specificationsList && specificationsList.length > 0) {
92
- toCollect = toCollect.filter((heading) => specificationsList.includes(heading));
101
+ /**
102
+ * Add the linked resources of the tree to `linkedResourcesList` and sort the list by link text.
103
+ *
104
+ * `specificationsList` limits the sections read; null or empty reads every section. The functional specs are read up
105
+ * to and including `frid`, or all of them when it is null. With `includeTestScenarios` the links of the conformance
106
+ * and acceptance scenarios are added too, for `frid` alone when one is given. Returns true when `frid` was reached.
107
+ */
108
+ export function collectLinkedResources(tree, linkedResourcesList, specificationsList, includeTestScenarios, frid = null) {
109
+ if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
110
+ throw new TypeError("plain_source_tree must be a dictionary.");
93
111
  }
94
- for (const heading of toCollect) {
95
- for (const requirement of getSpecificationItems(section, heading) ?? []) {
96
- collectSpecificationLinkedResources(requirement, heading, linkedResourcesList);
97
- }
112
+ if (frid !== null && ![...getFrids(tree)].includes(frid)) {
113
+ throw new Error(`frid ${frid} does not exist.`);
98
114
  }
99
- const functionalRequirements = getSpecificationItems(section, FUNCTIONAL_REQUIREMENTS);
100
- if (functionalRequirements !== undefined &&
101
- (!specificationsList ||
102
- specificationsList.length === 0 ||
103
- specificationsList.includes(FUNCTIONAL_REQUIREMENTS))) {
104
- let count = 0;
105
- for (const requirement of functionalRequirements) {
106
- collectSpecificationLinkedResources(requirement, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
107
- count += 1;
108
- const currentFrid = String(count);
109
- const tests = requirement[ACCEPTANCE_TESTS];
110
- if (tests !== undefined && includeAcceptanceTests && (frid === null || frid === currentFrid)) {
111
- for (const test of tests) {
112
- collectSpecificationLinkedResources(test, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
115
+ const sectionsToCollect = ALLOWED_SPECIFICATION_HEADINGS.filter((heading) => heading in tree && (!specificationsList || specificationsList.length === 0 || specificationsList.includes(heading)));
116
+ let fridReached = false;
117
+ for (const heading of sectionsToCollect) {
118
+ for (const specification of tree[heading] ?? []) {
119
+ collectSpecificationLinkedResources(specification, heading, linkedResourcesList);
120
+ if (heading !== FUNCTIONAL_REQUIREMENTS)
121
+ continue;
122
+ if (includeTestScenarios && (frid === null || frid === specification.id)) {
123
+ for (const scenario of [...(specification[CONFORMANCE_TESTS] ?? []), ...(specification[ACCEPTANCE_TESTS] ?? [])]) {
124
+ collectSpecificationLinkedResources(scenario, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
113
125
  }
114
126
  }
115
- // FRIDs increment, so once the requested one is reached nothing after it counts.
116
- if (frid !== null && currentFrid === frid)
117
- return true;
127
+ if (specification.id === frid) {
128
+ fridReached = true;
129
+ break;
130
+ }
118
131
  }
119
132
  }
120
- return false;
121
- }
122
- export function collectLinkedResources(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid = null) {
123
- if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
124
- throw new TypeError("[plain_source_tree must be a dictionary.");
125
- }
126
- if (frid !== null && ![...getFrids(tree)].includes(frid)) {
127
- throw new Error(`frid ${frid} does not exist.`);
128
- }
129
- const result = collectLinkedResourcesInSection(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid);
130
133
  linkedResourcesList.sort((a, b) => (a.text < b.text ? -1 : a.text > b.text ? 1 : 0));
131
- return result;
134
+ return fridReached;
132
135
  }
133
136
  /**
134
- * The FRID iterator: one functionality ID per functional spec, 1, 2, 3 in order.
135
- * Everything that walks the spec one functionality at a time - rendering, ranges, context
137
+ * The FRID iterator: the func-ids of the tree's functional specs, in file order.
138
+ * Everything that walks the spec one functional spec at a time - rendering, ranges, context
136
139
  * building - is driven by this.
137
140
  */
138
141
  export function* getFrids(tree) {
139
- const functionalRequirements = getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS) ?? [];
140
- for (let id = 1; id <= functionalRequirements.length; id += 1)
141
- yield String(id);
142
+ for (const functionality of getFunctionalSpecs(tree))
143
+ yield functionality.id;
142
144
  }
143
145
  export function getFirstFrid(tree) {
144
146
  const first = getFrids(tree).next();
@@ -163,6 +165,7 @@ export function getPreviousFrid(tree, frid) {
163
165
  }
164
166
  throw new Error(`Functionality ${frid} does not exist.`);
165
167
  }
168
+ /** The func-ids that come before `targetFrid` in the specification, in order. */
166
169
  export function getFridsBefore(tree, targetFrid) {
167
170
  const before = [];
168
171
  for (const frid of getFrids(tree)) {
@@ -172,8 +175,12 @@ export function getFridsBefore(tree, targetFrid) {
172
175
  }
173
176
  return before;
174
177
  }
175
- export function getSpecificationItemMarkdown(specificationItem, codeVariables, replaceCodeVariables) {
176
- let markdown = specificationItem.markdown;
178
+ /**
179
+ * The text under `key` of the item, with its code variables recorded in `codeVariables` and, when
180
+ * `replaceCodeVariables` is set, their values put in place of the `{{ name }}` placeholders.
181
+ */
182
+ export function getSpecificationItemMarkdown(specificationItem, codeVariables, replaceCodeVariables, key = "markdown") {
183
+ let markdown = String(specificationItem[key]);
177
184
  for (const codeVariable of specificationItem.code_variables ?? []) {
178
185
  if (codeVariable.name in codeVariables) {
179
186
  if (codeVariables[codeVariable.name] !== codeVariable.value) {
@@ -189,67 +196,41 @@ export function getSpecificationItemMarkdown(specificationItem, codeVariables, r
189
196
  }
190
197
  return markdown;
191
198
  }
192
- function getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables) {
193
- let returnFrid = null;
194
- const requirements = getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS);
195
- if (requirements !== undefined && requirements.length > 0) {
196
- let count = 0;
197
- for (const requirement of requirements) {
198
- count += 1;
199
- const currentFrid = String(count);
200
- functionalRequirements.push(getSpecificationItemMarkdown(requirement, codeVariables, replaceCodeVariables));
201
- if (currentFrid === frid) {
202
- for (const test of requirement[ACCEPTANCE_TESTS] ?? []) {
203
- acceptanceTests.push(getSpecificationItemMarkdown(test, codeVariables, replaceCodeVariables));
204
- }
205
- returnFrid = currentFrid;
206
- break;
207
- }
208
- }
209
- }
210
- if (returnFrid !== null) {
211
- const prepend = (target, heading) => {
212
- const source = getSpecificationItems(tree, heading);
213
- if (source === undefined)
214
- return;
215
- target.unshift(...source.map((item) => getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables)));
216
- };
217
- prepend(definitions, DEFINITIONS);
218
- prepend(nonFunctionalRequirements, NON_FUNCTIONAL_REQUIREMENTS);
219
- prepend(testRequirements, TEST_REQUIREMENTS);
220
- }
221
- return returnFrid;
222
- }
223
199
  /**
224
- * Build the rendering context for one functionality: every definition and
225
- * implementation/test req in scope, the functionalities up to and including this one,
226
- * and this one's acceptance tests.
200
+ * The specifications that build `frid`: every list section, the functional specs up to and including `frid`, and
201
+ * the conformance and acceptance scenarios of `frid` as `{id, text}` entries.
202
+ *
203
+ * Returns the specifications and the code variables they use (null when there are none). Throws when `frid` is not
204
+ * in the tree.
227
205
  */
228
206
  export function getSpecificationsForFrid(tree, frid, replaceCodeVariables = true) {
229
- const definitions = [];
230
- const nonFunctionalRequirements = [];
231
- const testRequirements = [];
232
- const functionalRequirements = [];
233
- const acceptanceTests = [];
234
- const codeVariables = {};
235
- const result = getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables);
236
- if (result === null)
207
+ const functionalities = getFunctionalSpecs(tree);
208
+ const position = functionalities.findIndex((item) => item.id === frid);
209
+ if (position === -1)
237
210
  throw new Error(`Functionality ${frid} does not exist.`);
211
+ const codeVariables = {};
212
+ const texts = (items, key = "markdown") => items.map((item) => getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables, key));
213
+ const scenarios = (items) => {
214
+ const scenarioTexts = texts(items, "text");
215
+ return items.map((item, index) => ({ id: item.id, text: scenarioTexts[index] }));
216
+ };
217
+ const listOf = (heading) => getSpecificationItems(tree, heading) ?? [];
218
+ const functionality = functionalities[position];
238
219
  const specifications = {
239
- [DEFINITIONS]: definitions,
240
- [NON_FUNCTIONAL_REQUIREMENTS]: nonFunctionalRequirements,
241
- [TEST_REQUIREMENTS]: testRequirements,
242
- [FUNCTIONAL_REQUIREMENTS]: functionalRequirements,
220
+ [DEFINITIONS]: texts(listOf(DEFINITIONS)),
221
+ [NON_FUNCTIONAL_REQUIREMENTS]: texts(listOf(NON_FUNCTIONAL_REQUIREMENTS)),
222
+ [TEST_REQUIREMENTS]: texts(listOf(TEST_REQUIREMENTS)),
223
+ [FUNCTIONAL_REQUIREMENTS]: texts(functionalities.slice(0, position + 1)),
224
+ [CONFORMANCE_TESTS]: scenarios(functionality[CONFORMANCE_TESTS] ?? []),
225
+ [ACCEPTANCE_TESTS]: scenarios(functionality[ACCEPTANCE_TESTS] ?? []),
243
226
  };
244
- if (acceptanceTests.length > 0)
245
- specifications[ACCEPTANCE_TESTS] = acceptanceTests;
246
227
  return [specifications, Object.keys(codeVariables).length > 0 ? codeVariables : null];
247
228
  }
248
- /** Iterate every functionality with the context that should be rendered for it. */
229
+ /** Iterate every functional spec with the context that should be rendered for it. turri's, for a host. */
249
230
  export function* iterateFunctionalSpecs(tree, replaceCodeVariables = true) {
250
- for (const frid of getFrids(tree)) {
251
- const [specifications, codeVariables] = getSpecificationsForFrid(tree, frid, replaceCodeVariables);
252
- yield { frid, specifications, codeVariables };
231
+ for (const funcId of getFrids(tree)) {
232
+ const [specifications, codeVariables] = getSpecificationsForFrid(tree, funcId, replaceCodeVariables);
233
+ yield { funcId, specifications, codeVariables };
253
234
  }
254
235
  }
255
236
  export function hashText(text) {
@@ -258,17 +239,18 @@ export function hashText(text) {
258
239
  export function getHashValue(specifications) {
259
240
  return hashText(JSON.stringify(specifications, null, 4));
260
241
  }
261
- function getFridsRange(tree, start, end) {
242
+ /** The func-ids from `start` through `end` inclusive, or through the last one when `end` is not given. */
243
+ export function getFridsRange(tree, start, end) {
262
244
  const frids = [...getFrids(tree)];
263
- start = String(start);
245
+ // Python prints the list with repr.
246
+ const valid = `[${frids.map((frid) => `'${frid}'`).join(", ")}]`;
264
247
  if (!frids.includes(start)) {
265
- throw new InvalidFridArgument(`Invalid start functionality ID: ${start}. Valid IDs are: ${JSON.stringify(frids)}.`);
248
+ throw new InvalidFridArgument(`Invalid start functionality ID: ${start}. Valid IDs are: ${valid}.`);
266
249
  }
267
250
  let endIdx;
268
251
  if (end !== undefined) {
269
- end = String(end);
270
252
  if (!frids.includes(end)) {
271
- throw new InvalidFridArgument(`Invalid end functionality ID: ${end}. Valid IDs are: ${JSON.stringify(frids)}.`);
253
+ throw new InvalidFridArgument(`Invalid end functionality ID: ${end}. Valid IDs are: ${valid}.`);
272
254
  }
273
255
  endIdx = frids.indexOf(end) + 1;
274
256
  }
@@ -281,11 +263,3 @@ function getFridsRange(tree, start, end) {
281
263
  }
282
264
  return frids.slice(startIdx, endIdx);
283
265
  }
284
- export function getRenderRange(renderRange, tree) {
285
- const parts = renderRange.split(",");
286
- const rangeEnd = parts.length === 2 ? parts[1] : parts[0];
287
- return getFridsRange(tree, parts[0], rangeEnd);
288
- }
289
- export function getRenderRangeFrom(start, tree) {
290
- return getFridsRange(tree, start);
291
- }
@@ -0,0 +1,59 @@
1
+ import { type LinkedResource } from "./markdown.ts";
2
+ import { type CodeVariable, type ScenarioItem } from "./spec.ts";
3
+ /**
4
+ * Port of test_plain_file.py: test plain files, the conformance and acceptance scenarios of one functionality.
5
+ *
6
+ * A test plain file binds to a functionality with the frontmatter keys `module` and `func_id`. It holds a
7
+ * `***conformance tests***` section, an `***acceptance tests***` section or both, in that order, each with at least one
8
+ * scenario: a section with none is left out, and a file with none is not written - a functional spec with no scenarios has
9
+ * no test plain file. Each scenario is a `# <scenario-id>` heading followed by free text, which is kept verbatim.
10
+ */
11
+ export declare const MODULE_KEY = "module";
12
+ export declare const FUNC_ID_KEY = "func_id";
13
+ export declare const FRONTMATTER_KEYS: string[];
14
+ export declare const SECTION_HEADINGS: readonly ["conformance tests", "acceptance tests"];
15
+ export interface Scenario {
16
+ id: string;
17
+ lineNumber: number;
18
+ /** The body as written, without `>` comment lines and without leading and trailing blank lines. */
19
+ text: string;
20
+ linkedResources: LinkedResource[];
21
+ codeVariables: CodeVariable[];
22
+ }
23
+ /** A scenario as the parse gives it: `{id, text, linked_resources}`, and `code_variables` when it uses any. */
24
+ export declare function scenarioItem(scenario: Scenario): ScenarioItem;
25
+ export interface TestPlainFile {
26
+ /** The path of the file as shown in messages. */
27
+ name: string;
28
+ module: string;
29
+ funcId: string;
30
+ conformanceTests: Scenario[];
31
+ acceptanceTests: Scenario[];
32
+ }
33
+ export declare function scenariosOf(file: TestPlainFile): Scenario[];
34
+ /** Whether a frontmatter binds its file to a functionality, which makes the file a test plain file. */
35
+ export declare function isTestPlainFile(metadata: Record<string, unknown>): boolean;
36
+ /**
37
+ * The frontmatter and content of a ***plain file, as python-frontmatter reads them: a block between two lines of
38
+ * three or more dashes, loaded as YAML - a block that is not a mapping is no metadata - and the content after it,
39
+ * stripped. Raises the YAML error as it is.
40
+ */
41
+ export declare function loadFrontmatter(text: string): {
42
+ metadata: Record<string, unknown>;
43
+ content: string;
44
+ };
45
+ /** The frontmatter and content of a test plain file; raise unless the keys are exactly module and func_id. */
46
+ export declare function readMetadata(text: string, name: string): {
47
+ metadata: Record<string, unknown>;
48
+ content: string;
49
+ };
50
+ /** The scenarios of the rendered content, keyed by section heading - a section left out, none; raise on the first structural error. */
51
+ export declare function readScenarios(renderedText: string, name: string): Record<string, Scenario[]>;
52
+ export declare function findScenarioLinkedResources(scenario: Scenario, templateDirs: string[], name: string): LinkedResource[];
53
+ /**
54
+ * Parse the text of a test plain file. Liquid runs as on a spec plain file, with the same template folders; every
55
+ * link in a scenario is resolved against them. Concepts are not checked here, see `validateScenarioConcepts`.
56
+ */
57
+ export declare function parseTestPlainFileSource(plainSourceText: string, name: string, templateDirs: string[], codeVariables: Record<string, Record<string, string>>): TestPlainFile;
58
+ /** One message per concept used in a scenario that is neither in `concepts` nor predefined. */
59
+ export declare function validateScenarioConcepts(file: TestPlainFile, concepts: Set<string>): string[];
@@ -0,0 +1,232 @@
1
+ import { load as loadYaml } from "js-yaml";
2
+ import { extractConceptsFromSpecText, DEFAULT_CONCEPTS } from "./concepts.js";
3
+ import { PlainSyntaxError } from "./errors.js";
4
+ import { getLoadedTemplates } from "./files.js";
5
+ import { renderPlainSource } from "./liquid.js";
6
+ import { parseMarkdown } from "./markdown.js";
7
+ import { findLinkedResources, normalizeLineEndings, processSectionCodeVariables, restoreStrippedLines } from "./parser.js";
8
+ import { ACCEPTANCE_TEST_HEADING, CONFORMANCE_TEST_HEADING, isSemanticId } from "./spec.js";
9
+ /**
10
+ * Port of test_plain_file.py: test plain files, the conformance and acceptance scenarios of one functionality.
11
+ *
12
+ * A test plain file binds to a functionality with the frontmatter keys `module` and `func_id`. It holds a
13
+ * `***conformance tests***` section, an `***acceptance tests***` section or both, in that order, each with at least one
14
+ * scenario: a section with none is left out, and a file with none is not written - a functional spec with no scenarios has
15
+ * no test plain file. Each scenario is a `# <scenario-id>` heading followed by free text, which is kept verbatim.
16
+ */
17
+ export const MODULE_KEY = "module";
18
+ export const FUNC_ID_KEY = "func_id";
19
+ export const FRONTMATTER_KEYS = [MODULE_KEY, FUNC_ID_KEY];
20
+ export const SECTION_HEADINGS = [CONFORMANCE_TEST_HEADING, ACCEPTANCE_TEST_HEADING];
21
+ /** A scenario as the parse gives it: `{id, text, linked_resources}`, and `code_variables` when it uses any. */
22
+ export function scenarioItem(scenario) {
23
+ const entry = { id: scenario.id, text: scenario.text, linked_resources: scenario.linkedResources };
24
+ if (scenario.codeVariables.length > 0)
25
+ entry.code_variables = scenario.codeVariables;
26
+ return entry;
27
+ }
28
+ export function scenariosOf(file) {
29
+ return [...file.conformanceTests, ...file.acceptanceTests];
30
+ }
31
+ /** Whether a frontmatter binds its file to a functionality, which makes the file a test plain file. */
32
+ export function isTestPlainFile(metadata) {
33
+ return FRONTMATTER_KEYS.every((key) => key in metadata);
34
+ }
35
+ const error = (message) => new PlainSyntaxError(`Plain syntax error: ${message}`);
36
+ const at = (lineNumber, name) => `Syntax error at line ${lineNumber} in test plain file '${name}'`;
37
+ /**
38
+ * The frontmatter and content of a ***plain file, as python-frontmatter reads them: a block between two lines of
39
+ * three or more dashes, loaded as YAML - a block that is not a mapping is no metadata - and the content after it,
40
+ * stripped. Raises the YAML error as it is.
41
+ */
42
+ export function loadFrontmatter(text) {
43
+ const match = /^-{3,}[ \t]*\n(?:([\s\S]*?)\n)?-{3,}[ \t]*(?:\n|$)/.exec(text);
44
+ if (match === null)
45
+ return { metadata: {}, content: text.trim() };
46
+ const block = match[1] ?? "";
47
+ const loaded = block.trim() === "" ? {} : loadYaml(block);
48
+ const metadata = loaded !== null && typeof loaded === "object" && !Array.isArray(loaded) ? loaded : {};
49
+ return { metadata, content: text.slice(match[0].length).trim() };
50
+ }
51
+ /** The frontmatter and content of a test plain file; raise unless the keys are exactly module and func_id. */
52
+ export function readMetadata(text, name) {
53
+ let post;
54
+ try {
55
+ post = loadFrontmatter(text);
56
+ }
57
+ catch (e) {
58
+ throw error(`Test plain file '${name}' has invalid frontmatter: ${e instanceof Error ? e.message : String(e)}`);
59
+ }
60
+ for (const key of FRONTMATTER_KEYS) {
61
+ if (!(key in post.metadata))
62
+ throw error(`Test plain file '${name}' has no \`${key}:\` key in its frontmatter.`);
63
+ const value = post.metadata[key];
64
+ if (typeof value !== "string" || value.trim() === "") {
65
+ throw error(`Test plain file '${name}': the frontmatter key \`${key}:\` must be a non-empty string.`);
66
+ }
67
+ }
68
+ for (const key of Object.keys(post.metadata)) {
69
+ if (!FRONTMATTER_KEYS.includes(key)) {
70
+ throw error(`Test plain file '${name}' has the frontmatter key \`${key}:\`; only \`module:\` and \`func_id:\` are allowed.`);
71
+ }
72
+ }
73
+ return post;
74
+ }
75
+ function trimBlankLines(lines) {
76
+ let start = 0;
77
+ let end = lines.length;
78
+ while (start < end && lines[start].trim() === "")
79
+ start += 1;
80
+ while (end > start && lines[end - 1].trim() === "")
81
+ end -= 1;
82
+ return lines.slice(start, end);
83
+ }
84
+ /** The scenarios of the rendered content, keyed by section heading - a section left out, none; raise on the first structural error. */
85
+ export function readScenarios(renderedText, name) {
86
+ const sections = {};
87
+ // Each section's heading line, for one left with no scenario.
88
+ const sectionLines = new Map();
89
+ let currentSection = null;
90
+ let current = null;
91
+ let body = [];
92
+ // The marker of the open fenced code block (``` or ~~~), null outside one.
93
+ let fence = null;
94
+ // Scenario-id to the line of its heading.
95
+ const headingLines = new Map();
96
+ const closeScenario = () => {
97
+ if (current === null)
98
+ return;
99
+ if (fence !== null)
100
+ throw error(`${at(current.lineNumber, name)}: Scenario \`${current.id}\` has a code fence that is not closed.`);
101
+ const lines = trimBlankLines(body);
102
+ if (lines.length === 0)
103
+ throw error(`${at(current.lineNumber, name)}: Scenario \`${current.id}\` has no text.`);
104
+ current.text = lines.join("\n");
105
+ };
106
+ renderedText.split("\n").forEach((line, index) => {
107
+ const lineNumber = index + 1;
108
+ const where = at(lineNumber, name);
109
+ // Inside a fenced code block every line is body text: a `# ` line is not a heading there, a `***` line is not a
110
+ // section marker, and a `>` line is not a comment.
111
+ const stripped = line.trimStart();
112
+ if (stripped.startsWith("```") || stripped.startsWith("~~~")) {
113
+ const marker = stripped.slice(0, 3);
114
+ if (fence === null)
115
+ fence = marker;
116
+ else if (fence === marker)
117
+ fence = null;
118
+ if (current !== null) {
119
+ body.push(line);
120
+ return;
121
+ }
122
+ }
123
+ else if (fence !== null && current !== null) {
124
+ body.push(line);
125
+ return;
126
+ }
127
+ if (line.startsWith("***")) {
128
+ const trimmed = line.trim();
129
+ const heading = trimmed.endsWith("***") && trimmed.length > 6 ? trimmed.slice(3, -3) : null;
130
+ if (heading === null || !SECTION_HEADINGS.includes(heading)) {
131
+ throw error(`${where}: Invalid section (\`${trimmed}\`). A test plain file has the sections ` +
132
+ `\`***${SECTION_HEADINGS[0]}***\` and \`***${SECTION_HEADINGS[1]}***\`, in this order.`);
133
+ }
134
+ if (heading in sections)
135
+ throw error(`${where}: Duplicate section \`***${heading}***\`.`);
136
+ if (heading === SECTION_HEADINGS[0] && SECTION_HEADINGS[1] in sections) {
137
+ throw error(`${where}: The \`***${heading}***\` section must come before \`***${SECTION_HEADINGS[1]}***\`.`);
138
+ }
139
+ closeScenario();
140
+ current = null;
141
+ currentSection = heading;
142
+ sections[heading] = [];
143
+ sectionLines.set(heading, lineNumber);
144
+ }
145
+ else if (line.startsWith("# ") || line === "#") {
146
+ const scenarioId = line.slice(1).trim();
147
+ if (currentSection === null) {
148
+ throw error(`${where}: Scenario \`${scenarioId}\` comes before the first section; a test plain file starts ` +
149
+ `with \`***${SECTION_HEADINGS[0]}***\`.`);
150
+ }
151
+ if (!isSemanticId(scenarioId)) {
152
+ throw error(`${where}: Invalid scenario-id (\`${scenarioId}\`). A scenario-id is lowercase letters and digits, ` +
153
+ "with single hyphens between them.");
154
+ }
155
+ if (headingLines.has(scenarioId)) {
156
+ throw error(`${where}: Duplicate scenario-id \`${scenarioId}\`; it is first used at line ${headingLines.get(scenarioId)}.`);
157
+ }
158
+ closeScenario();
159
+ current = { id: scenarioId, lineNumber, text: "", linkedResources: [], codeVariables: [] };
160
+ body = [];
161
+ headingLines.set(scenarioId, lineNumber);
162
+ sections[currentSection].push(current);
163
+ }
164
+ else if (line.trimStart().startsWith(">")) {
165
+ return;
166
+ }
167
+ else if (current !== null) {
168
+ body.push(line);
169
+ }
170
+ else if (line.trim() !== "") {
171
+ if (currentSection === null) {
172
+ throw error(`${where}: Content before the first section (\`${line.trim()}\`); a test plain file starts with ` +
173
+ `\`***${SECTION_HEADINGS[0]}***\`.`);
174
+ }
175
+ throw error(`${where}: Content before the first \`# <scenario-id>\` heading in the \`${currentSection}\` section (\`${line.trim()}\`).`);
176
+ }
177
+ });
178
+ closeScenario();
179
+ for (const heading of SECTION_HEADINGS) {
180
+ if (sections[heading]?.length === 0) {
181
+ throw error(`${at(sectionLines.get(heading), name)}: The \`***${heading}***\` section has no scenarios. Leave out a section with none.`);
182
+ }
183
+ }
184
+ if (Object.keys(sections).length === 0) {
185
+ throw error(`Test plain file '${name}' has no scenarios. A functional spec with none has no test plain file: delete it.`);
186
+ }
187
+ for (const heading of SECTION_HEADINGS)
188
+ sections[heading] ??= [];
189
+ return sections;
190
+ }
191
+ export function findScenarioLinkedResources(scenario, templateDirs, name) {
192
+ const document = parseMarkdown(scenario.text);
193
+ return findLinkedResources(document.children ?? [], templateDirs, ` in test plain file '${name}', scenario \`${scenario.id}\``);
194
+ }
195
+ /**
196
+ * Parse the text of a test plain file. Liquid runs as on a spec plain file, with the same template folders; every
197
+ * link in a scenario is resolved against them. Concepts are not checked here, see `validateScenarioConcepts`.
198
+ */
199
+ export function parseTestPlainFileSource(plainSourceText, name, templateDirs, codeVariables) {
200
+ plainSourceText = normalizeLineEndings(plainSourceText);
201
+ const post = readMetadata(plainSourceText, name);
202
+ const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText, name);
203
+ const content = restoreStrippedLines(plainSourceText, post.content);
204
+ const renderedText = renderPlainSource(content, loadedTemplates, codeVariables);
205
+ const sections = readScenarios(renderedText, name);
206
+ for (const scenario of [...sections[SECTION_HEADINGS[0]], ...sections[SECTION_HEADINGS[1]]]) {
207
+ scenario.linkedResources = findScenarioLinkedResources(scenario, templateDirs, name);
208
+ const entry = { text: scenario.text };
209
+ processSectionCodeVariables(entry, codeVariables, "text");
210
+ scenario.text = entry.text;
211
+ scenario.codeVariables = entry.code_variables ?? [];
212
+ }
213
+ return {
214
+ name,
215
+ module: String(post.metadata[MODULE_KEY]),
216
+ funcId: String(post.metadata[FUNC_ID_KEY]),
217
+ conformanceTests: sections[SECTION_HEADINGS[0]],
218
+ acceptanceTests: sections[SECTION_HEADINGS[1]],
219
+ };
220
+ }
221
+ /** One message per concept used in a scenario that is neither in `concepts` nor predefined. */
222
+ export function validateScenarioConcepts(file, concepts) {
223
+ const errors = [];
224
+ for (const scenario of scenariosOf(file)) {
225
+ for (const concept of new Set(extractConceptsFromSpecText(scenario.text))) {
226
+ if (!concepts.has(concept) && !DEFAULT_CONCEPTS.has(concept)) {
227
+ errors.push(`${at(scenario.lineNumber, file.name)}: Concept ${concept} used in scenario \`${scenario.id}\` is not defined in module '${file.module}'.`);
228
+ }
229
+ }
230
+ }
231
+ return errors;
232
+ }