@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.
- package/README.md +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +71 -15
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -20
- package/dist/regenerate/conformance.js +118 -121
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +61 -49
- package/dist/regenerate/loop.js +473 -340
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +69 -31
- package/dist/regenerate/test-kinds.js +109 -80
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +89 -54
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +24 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
package/dist/plain/parser.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { extname, isAbsolute, basename, resolve } from "node:path";
|
|
3
|
+
import { extname, isAbsolute, basename, join, resolve } from "node:path";
|
|
4
4
|
import { load as loadYaml } from "js-yaml";
|
|
5
5
|
import { extractConceptsFromDefinition, findConceptDefinitionsInPlainSource, sortDefinitions, validateConcepts, DEFAULT_CONCEPTS, } from "./concepts.js";
|
|
6
6
|
import { ImportedModuleWithFunctionalitiesError, MissingFunctionalitiesError, ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content, } from "./errors.js";
|
|
7
7
|
import { findIn, findLargeBase64Blob, getLoadedTemplates, openFrom } from "./files.js";
|
|
8
8
|
import { renderPlainSource } from "./liquid.js";
|
|
9
9
|
import { isStdlibModule, readStdlibModule, stdlibModuleFile } 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";
|
|
10
|
+
import { getRawText, lineNumber, parseMarkdown, removeQuotes, renderToMarkdown, renderTokens, traverse, } from "./markdown.js";
|
|
11
|
+
import { ACCEPTANCE_TESTS, ACCEPTANCE_TEST_HEADING, ALLOWED_IMPORT_SPECIFICATION_HEADINGS, ALLOWED_SPECIFICATION_HEADINGS, CONFORMANCE_TESTS, CONFORMANCE_TEST_HEADING, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, LIST_SECTION_HEADINGS, NON_FUNCTIONAL_REQUIREMENTS, TEST_PLAIN_FILES, TEST_REQUIREMENTS, getFunctionalSpecs, getSpecificationItems, isSemanticId, REQUIRED_FUNCTIONAL_SPECS, } from "./spec.js";
|
|
12
|
+
import { bindTestPlainFiles, checkTestPlainFiles } from "./project.js";
|
|
12
13
|
export const IMPORT_DIRECTIVE = "import";
|
|
13
14
|
export const REQUIRES_DIRECTIVE = "requires";
|
|
15
|
+
export const PREVIOUS_SYNTAX_HINT = "A functionality is a `# <func-id>` heading followed by its body; the previous syntax is not supported.";
|
|
14
16
|
export const REQUIRED_CONCEPTS_DIRECTIVE = "required_concepts";
|
|
15
17
|
export const EXPORTED_CONCEPTS_DIRECTIVE = "exported_concepts";
|
|
16
18
|
export const PLAIN_SOURCE_FILE_EXTENSION = ".plain";
|
|
19
|
+
/** A list section of a parsed module. */
|
|
20
|
+
export function sectionOf(plainSource, heading) {
|
|
21
|
+
return plainSource[heading];
|
|
22
|
+
}
|
|
23
|
+
/** The functional specs of a parsed module, none when it has no section for them. */
|
|
24
|
+
export function functionalitiesOf(plainSource) {
|
|
25
|
+
return plainSource[FUNCTIONAL_REQUIREMENTS] ?? [];
|
|
26
|
+
}
|
|
17
27
|
function newPlainSource() {
|
|
18
28
|
return {
|
|
19
29
|
[DEFINITIONS]: null,
|
|
@@ -31,20 +41,19 @@ export function hasScheme(target) {
|
|
|
31
41
|
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(target);
|
|
32
42
|
}
|
|
33
43
|
/**
|
|
34
|
-
*
|
|
35
|
-
* other template folders -
|
|
36
|
-
* Links are relative to the spec, so a spec in a subfolder is checked
|
|
37
|
-
* turri was started.
|
|
44
|
+
* Port of loaders.resolve_linked_resource: a link's file, in the first of the folders resources are loaded from - the
|
|
45
|
+
* spec's own folder first, then the other template folders - where it exists, or null. Existence, not being a file,
|
|
46
|
+
* decides the match, as in `open_from`'s search. Links are relative to the spec, so a spec in a subfolder is checked
|
|
47
|
+
* where it is, wherever turri was started.
|
|
38
48
|
*/
|
|
39
|
-
function
|
|
49
|
+
export function resolveLinkedResource(dirs, target) {
|
|
40
50
|
for (const dir of dirs) {
|
|
41
|
-
const candidate =
|
|
42
|
-
consulted?.add(candidate);
|
|
51
|
+
const candidate = join(dir, target);
|
|
52
|
+
consulted?.add(resolve(candidate));
|
|
43
53
|
if (existsSync(candidate))
|
|
44
54
|
return candidate;
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
return target;
|
|
56
|
+
return null;
|
|
48
57
|
}
|
|
49
58
|
/**
|
|
50
59
|
* The linked files a parse looked for, found or not, while one runs: a kept parse holds while each is as it was - a
|
|
@@ -92,55 +101,63 @@ function stillHolds(kept, fingerprint) {
|
|
|
92
101
|
export function linksIn(section) {
|
|
93
102
|
return [...traverse(section, "link")].map((link) => ({ text: String(link.children?.[0]?.value ?? ""), target: String(link.url ?? ""), node: link }));
|
|
94
103
|
}
|
|
95
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The links in the blocks, each checked to be a relative path to an existing file that is not a .plain file.
|
|
106
|
+
* `where` names the place of the links in error messages, e.g. " in test plain file 'tests/m/f.plain', scenario `s`".
|
|
107
|
+
*/
|
|
108
|
+
export function findLinkedResources(nodes, dirs = [], where = "") {
|
|
96
109
|
const linkedResources = [];
|
|
97
|
-
for (const { text, target, node: link } of linksIn(
|
|
110
|
+
for (const { text, target, node: link } of nodes.flatMap((node) => linksIn(node))) {
|
|
98
111
|
const children = link.children ?? [];
|
|
99
112
|
if (hasScheme(target) || isAbsolute(target)) {
|
|
100
|
-
throw new PlainSyntaxError(`Plain syntax error: Only relative links are allowed (text: ${text}, target: ${target}).`);
|
|
113
|
+
throw new PlainSyntaxError(`Plain syntax error: Only relative links are allowed${where} (text: ${text}, target: ${target}).`);
|
|
101
114
|
}
|
|
102
|
-
const file =
|
|
103
|
-
if (
|
|
104
|
-
|
|
115
|
+
const file = resolveLinkedResource(dirs, target);
|
|
116
|
+
if (file === null) {
|
|
117
|
+
const searched = dirs.map((dir, index) => ` ${index + 1}. ${dir}`).join("\n");
|
|
118
|
+
throw new PlainSyntaxError(`Plain syntax error: Link ${target}${where} does not exist. ` +
|
|
119
|
+
"Linked resources are looked up relative to the following directories " +
|
|
120
|
+
`(highest to lowest precedence):\n${searched}`);
|
|
105
121
|
}
|
|
106
122
|
if (!statSync(file).isFile()) {
|
|
107
|
-
throw new PlainSyntaxError(`Plain syntax error: Link ${target} must be a file.`);
|
|
123
|
+
throw new PlainSyntaxError(`Plain syntax error: Link ${target}${where} must be a file (resolved to ${file}).`);
|
|
108
124
|
}
|
|
109
125
|
if (children.length !== 1) {
|
|
110
|
-
throw new PlainSyntaxError(`Plain syntax error: Link must have text specified (link: ${target}).`);
|
|
126
|
+
throw new PlainSyntaxError(`Plain syntax error: Link${where} must have text specified (link: ${target}).`);
|
|
111
127
|
}
|
|
112
128
|
if (extname(basename(target)) === ".plain") {
|
|
113
|
-
throw new PlainSyntaxError(`Referenced resource '${target}' is a .plain file. ` +
|
|
129
|
+
throw new PlainSyntaxError(`Referenced resource '${target}'${where} is a .plain file. ` +
|
|
114
130
|
"Referencing .plain files through Linked Resources is not supported. " +
|
|
115
131
|
"Please use the import section to include definitions or implementation/test requirements from another module.");
|
|
116
132
|
}
|
|
117
133
|
linkedResources.push({ text, target });
|
|
118
134
|
}
|
|
135
|
+
return linkedResources;
|
|
136
|
+
}
|
|
137
|
+
export function checkSectionForLinkedResources(section, dirs = []) {
|
|
138
|
+
const linkedResources = findLinkedResources([section], dirs);
|
|
119
139
|
if (linkedResources.length > 0)
|
|
120
140
|
section.linked_resources = linkedResources;
|
|
121
141
|
}
|
|
122
142
|
export function checkForLinkedResources(plainSource, dirs = []) {
|
|
123
|
-
for (const heading of
|
|
124
|
-
const
|
|
125
|
-
if (!section)
|
|
126
|
-
continue;
|
|
127
|
-
for (const requirement of section.children) {
|
|
143
|
+
for (const heading of LIST_SECTION_HEADINGS) {
|
|
144
|
+
for (const requirement of sectionOf(plainSource, heading)?.children ?? [])
|
|
128
145
|
checkSectionForLinkedResources(requirement, dirs);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
146
|
+
}
|
|
147
|
+
for (const functionality of functionalitiesOf(plainSource)) {
|
|
148
|
+
functionality.linkedResources = findLinkedResources(functionality.body, dirs);
|
|
133
149
|
}
|
|
134
150
|
}
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
/** Put `{{ name }}` placeholders back into the section text and record the code variables it uses. */
|
|
152
|
+
export function processSectionCodeVariables(section, codeVariables, textKey = "markdown") {
|
|
153
|
+
if (!(textKey in section))
|
|
137
154
|
return;
|
|
138
155
|
const codeVariablesArray = [];
|
|
139
156
|
for (const [key, value] of Object.entries(codeVariables)) {
|
|
140
|
-
if (!section.
|
|
157
|
+
if (!String(section[textKey]).includes(key))
|
|
141
158
|
continue;
|
|
142
159
|
const variableName = Object.keys(value)[0];
|
|
143
|
-
section
|
|
160
|
+
section[textKey] = String(section[textKey]).split(key).join(`{{ ${variableName} }}`);
|
|
144
161
|
codeVariablesArray.push({ name: variableName, value: value[variableName] });
|
|
145
162
|
}
|
|
146
163
|
if (codeVariablesArray.length > 0)
|
|
@@ -159,11 +176,10 @@ export function hasFunctionalSpecsSection(plainSource) {
|
|
|
159
176
|
}
|
|
160
177
|
/** Number of functionalities under ***functional specs*** (0 if absent or empty). */
|
|
161
178
|
export function countFunctionalities(plainSource) {
|
|
162
|
-
|
|
163
|
-
return section ? section.children.length : 0;
|
|
179
|
+
return functionalitiesOf(plainSource).length;
|
|
164
180
|
}
|
|
165
181
|
export function validateFunctionalitiesHaveImplementationReqs(plainSource, moduleName, hasRequires = false) {
|
|
166
|
-
const implementationReqs = plainSource
|
|
182
|
+
const implementationReqs = sectionOf(plainSource, NON_FUNCTIONAL_REQUIREMENTS);
|
|
167
183
|
const hasImplementationReqs = Boolean(implementationReqs && implementationReqs.children.length > 0);
|
|
168
184
|
if (hasImplementationReqs)
|
|
169
185
|
return;
|
|
@@ -176,113 +192,19 @@ export function validateFunctionalitiesHaveImplementationReqs(plainSource, modul
|
|
|
176
192
|
}
|
|
177
193
|
throw new PlainSyntaxError(message);
|
|
178
194
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
export function isAcceptanceTestHeading(token) {
|
|
188
|
-
if (token.type !== "paragraph")
|
|
189
|
-
return [false, null];
|
|
190
|
-
const children = token.children ?? [];
|
|
191
|
-
if (children.length !== 1 || children[0].type !== "emphasis")
|
|
192
|
-
return [false, null];
|
|
193
|
-
const emphasisChildren = children[0].children ?? [];
|
|
194
|
-
if (emphasisChildren.length !== 1 || emphasisChildren[0].type !== "strong")
|
|
195
|
-
return [false, null];
|
|
196
|
-
const strongChildren = emphasisChildren[0].children ?? [];
|
|
197
|
-
if (strongChildren.length !== 1 || strongChildren[0].type !== "text")
|
|
198
|
-
return [false, null];
|
|
199
|
-
const content = String(strongChildren[0].value ?? "").trim();
|
|
200
|
-
if (content === ACCEPTANCE_TEST_HEADING)
|
|
201
|
-
return [true, null];
|
|
202
|
-
return [
|
|
203
|
-
false,
|
|
204
|
-
`Syntax error at line ${lineNumber(token)}: Invalid acceptance test heading (\`${content}\`). ` +
|
|
205
|
-
`Expected: \`${ACCEPTANCE_TEST_HEADING}.\``,
|
|
206
|
-
];
|
|
207
|
-
}
|
|
208
|
-
/** Find an acceptance tests heading anywhere in a subtree. */
|
|
209
|
-
export function findAcceptanceTestHeading(token) {
|
|
210
|
-
const [isHeading] = isAcceptanceTestHeading(token);
|
|
211
|
-
if (isHeading)
|
|
212
|
-
return token;
|
|
213
|
-
for (const child of token.children ?? []) {
|
|
214
|
-
const found = findAcceptanceTestHeading(child);
|
|
215
|
-
if (found !== null)
|
|
216
|
-
return found;
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Pull the acceptance tests out of one functionality.
|
|
222
|
-
*
|
|
223
|
-
* A functionality with acceptance tests has three children: its own text, the
|
|
224
|
-
* `***acceptance tests***` heading, and the list of tests. The heading and list are
|
|
225
|
-
* lifted onto `acceptance_tests` and removed from the children.
|
|
226
|
-
*/
|
|
227
|
-
export function processSingleAcceptanceTestRequirement(functionalRequirement) {
|
|
228
|
-
const newChildren = [];
|
|
229
|
-
const children = functionalRequirement.children ?? [];
|
|
230
|
-
let acceptanceTestsFoundAlready = false;
|
|
231
|
-
for (let index = 0; index < children.length; index += 1) {
|
|
232
|
-
const child = children[index];
|
|
233
|
-
const [isHeading, problem] = isAcceptanceTestHeading(child);
|
|
234
|
-
if (problem) {
|
|
235
|
-
// A near-miss heading: `acceptance test` instead of `acceptance tests`, or some
|
|
236
|
-
// other block written where the tests were meant to go.
|
|
237
|
-
throw new PlainSyntaxError(`Plain syntax error: ${problem}`);
|
|
238
|
-
}
|
|
239
|
-
if (!isHeading) {
|
|
240
|
-
newChildren.push(child);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
if (acceptanceTestsFoundAlready) {
|
|
244
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(child)}: Duplicate 'acceptance tests' ` +
|
|
245
|
-
"heading found within the same functionality. Only one block of acceptance tests is allowed per functionality.");
|
|
246
|
-
}
|
|
247
|
-
const nextToken = children[index + 1];
|
|
248
|
-
if (nextToken === undefined) {
|
|
249
|
-
newChildren.push(child);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
index += 1;
|
|
253
|
-
if (nextToken.type === "list") {
|
|
254
|
-
if (functionalRequirement[ACCEPTANCE_TESTS] === undefined) {
|
|
255
|
-
functionalRequirement[ACCEPTANCE_TESTS] = [];
|
|
256
|
-
}
|
|
257
|
-
for (const listItem of nextToken.children ?? []) {
|
|
258
|
-
functionalRequirement[ACCEPTANCE_TESTS].push(listItem);
|
|
259
|
-
}
|
|
260
|
-
acceptanceTestsFoundAlready = true;
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
// Not followed by a list: keep both tokens.
|
|
264
|
-
newChildren.push(child);
|
|
265
|
-
newChildren.push(nextToken);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
functionalRequirement.children = newChildren;
|
|
269
|
-
}
|
|
270
|
-
export function processAcceptanceTests(plainSource) {
|
|
271
|
-
if (!(FUNCTIONAL_REQUIREMENTS in plainSource))
|
|
272
|
-
return;
|
|
273
|
-
const frs = plainSource[FUNCTIONAL_REQUIREMENTS];
|
|
274
|
-
if (!frs)
|
|
275
|
-
return;
|
|
276
|
-
for (const functionalRequirement of frs.children) {
|
|
277
|
-
if (functionalRequirement.children === undefined)
|
|
278
|
-
continue;
|
|
279
|
-
processSingleAcceptanceTestRequirement(functionalRequirement);
|
|
280
|
-
}
|
|
195
|
+
export function marshallFunctionality(functionality) {
|
|
196
|
+
return {
|
|
197
|
+
id: functionality.id,
|
|
198
|
+
markdown: renderTokens(functionality.body),
|
|
199
|
+
linked_resources: functionality.linkedResources,
|
|
200
|
+
[CONFORMANCE_TESTS]: [],
|
|
201
|
+
[ACCEPTANCE_TESTS]: [],
|
|
202
|
+
};
|
|
281
203
|
}
|
|
282
204
|
export function marshallPlainSource(input) {
|
|
283
205
|
const plainSource = {};
|
|
284
|
-
for (const heading of
|
|
285
|
-
const section = input
|
|
206
|
+
for (const heading of LIST_SECTION_HEADINGS) {
|
|
207
|
+
const section = sectionOf(input, heading);
|
|
286
208
|
if (!section)
|
|
287
209
|
continue;
|
|
288
210
|
const listOfRequirements = [];
|
|
@@ -293,19 +215,13 @@ export function marshallPlainSource(input) {
|
|
|
293
215
|
if (requirement.linked_resources) {
|
|
294
216
|
requirementSection.linked_resources = requirement.linked_resources;
|
|
295
217
|
}
|
|
296
|
-
const acceptanceTests = requirement[ACCEPTANCE_TESTS];
|
|
297
|
-
if (acceptanceTests !== undefined) {
|
|
298
|
-
requirementSection[ACCEPTANCE_TESTS] = acceptanceTests.map((acceptanceTest) => {
|
|
299
|
-
const section = { markdown: renderToMarkdown(acceptanceTest).trim() };
|
|
300
|
-
if (acceptanceTest.linked_resources)
|
|
301
|
-
section.linked_resources = acceptanceTest.linked_resources;
|
|
302
|
-
return section;
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
218
|
listOfRequirements.push(requirementSection);
|
|
306
219
|
}
|
|
307
220
|
plainSource[heading] = listOfRequirements;
|
|
308
221
|
}
|
|
222
|
+
if (FUNCTIONAL_REQUIREMENTS in input) {
|
|
223
|
+
plainSource[FUNCTIONAL_REQUIREMENTS] = functionalitiesOf(input).map(marshallFunctionality);
|
|
224
|
+
}
|
|
309
225
|
return plainSource;
|
|
310
226
|
}
|
|
311
227
|
export function normalizeLineEndings(plainSourceText) {
|
|
@@ -326,12 +242,14 @@ export function restoreStrippedLines(plainSourceText, content) {
|
|
|
326
242
|
export function readPlainSourceMetadata(plainSourceText) {
|
|
327
243
|
let metadata = {};
|
|
328
244
|
let content = plainSourceText;
|
|
329
|
-
|
|
245
|
+
// python-frontmatter's boundary: a line of three or more dashes, the block between them possibly empty.
|
|
246
|
+
const match = /^-{3,}[ \t]*\n(?:([\s\S]*?)\n)?-{3,}[ \t]*(?:\n|$)/.exec(plainSourceText);
|
|
330
247
|
if (match) {
|
|
331
248
|
try {
|
|
332
249
|
// python-frontmatter yields {} when the block does not parse to a mapping,
|
|
333
250
|
// so a malformed directive block is ignored rather than fatal.
|
|
334
|
-
const
|
|
251
|
+
const block = match[1] ?? "";
|
|
252
|
+
const loaded = block.trim() === "" ? {} : loadYaml(block);
|
|
335
253
|
metadata =
|
|
336
254
|
loaded !== null && typeof loaded === "object" && !Array.isArray(loaded)
|
|
337
255
|
? loaded
|
|
@@ -368,6 +286,141 @@ export function readPlainSourceMetadata(plainSourceText) {
|
|
|
368
286
|
}
|
|
369
287
|
return { metadata, content };
|
|
370
288
|
}
|
|
289
|
+
/** `Syntax error at line N in module 'm'`, without the parts that are not known. */
|
|
290
|
+
export function syntaxErrorAt(line, moduleName) {
|
|
291
|
+
let where = "Syntax error";
|
|
292
|
+
if (line !== null && line !== undefined)
|
|
293
|
+
where += ` at line ${line}`;
|
|
294
|
+
if (moduleName)
|
|
295
|
+
where += ` in module '${moduleName}'`;
|
|
296
|
+
return where;
|
|
297
|
+
}
|
|
298
|
+
export const TEST_PLAIN_FILE_SECTION_HEADINGS = [CONFORMANCE_TEST_HEADING, ACCEPTANCE_TEST_HEADING];
|
|
299
|
+
/** Throw when a `***conformance tests***` or `***acceptance tests***` marker is anywhere in a spec plain file. */
|
|
300
|
+
export function checkNoTestSections(plainSourceFullText, moduleName) {
|
|
301
|
+
plainSourceFullText.split("\n").forEach((line, index) => {
|
|
302
|
+
for (const heading of TEST_PLAIN_FILE_SECTION_HEADINGS) {
|
|
303
|
+
if (line.trim() === `***${heading}***`) {
|
|
304
|
+
throw new PlainSyntaxError(`Plain syntax error: ${syntaxErrorAt(index + 1, moduleName)}: The \`${heading}\` section is not ` +
|
|
305
|
+
"allowed in a spec plain file; test scenarios live in test plain files.");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/** The name in a `***name***` paragraph, or null when the paragraph is not a section marker. */
|
|
311
|
+
export function sectionMarker(token) {
|
|
312
|
+
const paragraphChildren = token.children ?? [];
|
|
313
|
+
if (paragraphChildren.length !== 1 || paragraphChildren[0].type !== "emphasis")
|
|
314
|
+
return null;
|
|
315
|
+
const emphasisChildren = paragraphChildren[0].children ?? [];
|
|
316
|
+
if (emphasisChildren.length !== 1 || emphasisChildren[0].type !== "strong")
|
|
317
|
+
return null;
|
|
318
|
+
return String((emphasisChildren[0].children ?? [])[0]?.value ?? "");
|
|
319
|
+
}
|
|
320
|
+
/** Whether a block is an ATX heading - `# text` - as mistletoe's `Heading`; a setext one is not. */
|
|
321
|
+
function isAtxHeading(token) {
|
|
322
|
+
return token.type === "heading" && token.sourceLine === undefined;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Add the sections of the rendered spec plain file to `plainSource`.
|
|
326
|
+
*
|
|
327
|
+
* A list section holds a section whose children are its list items; `functional specs` holds a list of
|
|
328
|
+
* functionalities, one per `# <func-id>` heading, each with every block up to the next heading or section marker.
|
|
329
|
+
* Throw on the first structural error.
|
|
330
|
+
*/
|
|
331
|
+
export function readSections(plainSource, document, moduleName) {
|
|
332
|
+
let currentSpecificationHeading = null;
|
|
333
|
+
let currentFunctionality = null;
|
|
334
|
+
const processedSpecificationHeadings = new Set();
|
|
335
|
+
// Func-id to the line of its heading.
|
|
336
|
+
const functionalityLines = new Map();
|
|
337
|
+
const closeFunctionality = () => {
|
|
338
|
+
if (currentFunctionality !== null && currentFunctionality.body.length === 0) {
|
|
339
|
+
throw new PlainSyntaxError(`Plain syntax error: ${syntaxErrorAt(currentFunctionality.lineNumber, moduleName)}: ` +
|
|
340
|
+
`Functionality \`${currentFunctionality.id}\` has no body.`);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
for (const token of document.children ?? []) {
|
|
344
|
+
const tokenText = getRawText(token).trim();
|
|
345
|
+
const where = syntaxErrorAt(lineNumber(token), moduleName);
|
|
346
|
+
const inFunctionalSpecs = currentSpecificationHeading === FUNCTIONAL_REQUIREMENTS;
|
|
347
|
+
if (isAtxHeading(token)) {
|
|
348
|
+
if (!inFunctionalSpecs) {
|
|
349
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: A \`# <func-id>\` heading is allowed only in the ` +
|
|
350
|
+
`${FUNCTIONAL_REQUIREMENTS} section (\`${tokenText}\`).`);
|
|
351
|
+
}
|
|
352
|
+
if (token.depth !== 1) {
|
|
353
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Invalid heading level ${token.depth} (\`${tokenText}\`). ` +
|
|
354
|
+
"A functionality heading is `# <func-id>`.");
|
|
355
|
+
}
|
|
356
|
+
if (!isSemanticId(tokenText)) {
|
|
357
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Invalid func-id (\`${tokenText}\`). A func-id is lowercase letters ` +
|
|
358
|
+
"and digits, with single hyphens between them.");
|
|
359
|
+
}
|
|
360
|
+
if (functionalityLines.has(tokenText)) {
|
|
361
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Duplicate func-id \`${tokenText}\`; it is first used at line ` +
|
|
362
|
+
`${functionalityLines.get(tokenText)}.`);
|
|
363
|
+
}
|
|
364
|
+
closeFunctionality();
|
|
365
|
+
functionalityLines.set(tokenText, lineNumber(token));
|
|
366
|
+
currentFunctionality = { id: tokenText, lineNumber: lineNumber(token), body: [], linkedResources: [] };
|
|
367
|
+
functionalitiesOf(plainSource).push(currentFunctionality);
|
|
368
|
+
}
|
|
369
|
+
else if (token.type === "paragraph") {
|
|
370
|
+
const specificationHeading = sectionMarker(token);
|
|
371
|
+
if (specificationHeading === null) {
|
|
372
|
+
if (!inFunctionalSpecs) {
|
|
373
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Invalid specification (\`${tokenText}\`)`);
|
|
374
|
+
}
|
|
375
|
+
if (currentFunctionality === null) {
|
|
376
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Content before the first \`# <func-id>\` heading in the ` +
|
|
377
|
+
`${FUNCTIONAL_REQUIREMENTS} section (\`${tokenText}\`). ${PREVIOUS_SYNTAX_HINT}`);
|
|
378
|
+
}
|
|
379
|
+
currentFunctionality.body.push(token);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!ALLOWED_SPECIFICATION_HEADINGS.includes(specificationHeading)) {
|
|
383
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Invalid specification heading (\`${specificationHeading}\`). ` +
|
|
384
|
+
`Allowed headings: ${ALLOWED_SPECIFICATION_HEADINGS.join(", ")}`);
|
|
385
|
+
}
|
|
386
|
+
if (processedSpecificationHeadings.has(specificationHeading)) {
|
|
387
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Duplicate specification heading (\`${specificationHeading}\`)`);
|
|
388
|
+
}
|
|
389
|
+
if (specificationHeading === DEFINITIONS && currentSpecificationHeading !== null) {
|
|
390
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Definitions specification must be the first specification in ` +
|
|
391
|
+
`the section (\`${tokenText}\`)`);
|
|
392
|
+
}
|
|
393
|
+
closeFunctionality();
|
|
394
|
+
currentFunctionality = null;
|
|
395
|
+
currentSpecificationHeading = specificationHeading;
|
|
396
|
+
if (specificationHeading === FUNCTIONAL_REQUIREMENTS) {
|
|
397
|
+
plainSource[specificationHeading] = [];
|
|
398
|
+
}
|
|
399
|
+
else if (plainSource[specificationHeading] == null) {
|
|
400
|
+
plainSource[specificationHeading] = { children: [] };
|
|
401
|
+
}
|
|
402
|
+
processedSpecificationHeadings.add(specificationHeading);
|
|
403
|
+
}
|
|
404
|
+
else if (token.type === "list" && !inFunctionalSpecs) {
|
|
405
|
+
if (currentSpecificationHeading === null) {
|
|
406
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Missing specification heading (\`${tokenText}\`)`);
|
|
407
|
+
}
|
|
408
|
+
sectionOf(plainSource, currentSpecificationHeading).children.push(...(token.children ?? []));
|
|
409
|
+
}
|
|
410
|
+
else if (inFunctionalSpecs) {
|
|
411
|
+
// The body of a functionality is free text: a list at any indentation, a code block, or any other block.
|
|
412
|
+
if (currentFunctionality === null) {
|
|
413
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Content before the first \`# <func-id>\` heading in the ` +
|
|
414
|
+
`${FUNCTIONAL_REQUIREMENTS} section (\`${tokenText}\`). ${PREVIOUS_SYNTAX_HINT}`);
|
|
415
|
+
}
|
|
416
|
+
currentFunctionality.body.push(token);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
throw new PlainSyntaxError(`Plain syntax error: ${where}: Invalid source structure (\`${tokenText}\`)`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
closeFunctionality();
|
|
423
|
+
}
|
|
371
424
|
function processImports(plainSource, imports, codeVariables, templateDirs, importedModules, modulesTrace, importedFrom) {
|
|
372
425
|
const requiredConcepts = [];
|
|
373
426
|
for (const moduleName of imports) {
|
|
@@ -382,8 +435,8 @@ function processImports(plainSource, imports, codeVariables, templateDirs, impor
|
|
|
382
435
|
"Imported modules may only provide definitions, implementation reqs, and test reqs — " +
|
|
383
436
|
"use 'requires' instead of 'import' if this module's functionalities should be built on.");
|
|
384
437
|
}
|
|
385
|
-
for (const
|
|
386
|
-
for (const item of
|
|
438
|
+
for (const heading of LIST_SECTION_HEADINGS) {
|
|
439
|
+
for (const item of sectionOf(result.plainSource, heading)?.children ?? [])
|
|
387
440
|
importedFrom.set(item, result.importedFrom.get(item) ?? moduleName);
|
|
388
441
|
}
|
|
389
442
|
for (const heading of Object.keys(result.plainSource)) {
|
|
@@ -395,7 +448,7 @@ function processImports(plainSource, imports, codeVariables, templateDirs, impor
|
|
|
395
448
|
plainSource[heading] = result.plainSource[heading] ?? null;
|
|
396
449
|
}
|
|
397
450
|
else if (result.plainSource[heading] != null) {
|
|
398
|
-
plainSource
|
|
451
|
+
sectionOf(plainSource, heading).children.push(...sectionOf(result.plainSource, heading).children);
|
|
399
452
|
}
|
|
400
453
|
}
|
|
401
454
|
if (REQUIRED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata) {
|
|
@@ -412,7 +465,7 @@ function processImports(plainSource, imports, codeVariables, templateDirs, impor
|
|
|
412
465
|
}
|
|
413
466
|
return requiredConcepts;
|
|
414
467
|
}
|
|
415
|
-
export function parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, modulesTrace) {
|
|
468
|
+
export function parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, modulesTrace, moduleName) {
|
|
416
469
|
plainSourceText = normalizeLineEndings(plainSourceText);
|
|
417
470
|
const plainSourceObj = readPlainSourceMetadata(plainSourceText);
|
|
418
471
|
const plainSource = newPlainSource();
|
|
@@ -421,81 +474,53 @@ export function parsePlainSource(plainSourceText, codeVariables, templateDirs, i
|
|
|
421
474
|
if (IMPORT_DIRECTIVE in plainSourceObj.metadata) {
|
|
422
475
|
requiredConcepts = processImports(plainSource, plainSourceObj.metadata[IMPORT_DIRECTIVE], codeVariables, templateDirs, importedModules, modulesTrace, importedFrom);
|
|
423
476
|
}
|
|
424
|
-
const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText);
|
|
477
|
+
const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText, moduleName);
|
|
425
478
|
const plainSourceContent = restoreStrippedLines(plainSourceText, plainSourceObj.content);
|
|
426
479
|
const plainSourceFullText = renderPlainSource(plainSourceContent, loadedTemplates, codeVariables);
|
|
480
|
+
checkNoTestSections(plainSourceFullText, moduleName);
|
|
481
|
+
// Blank lines are kept as the reference keeps them, parsed inside its renderer: one BlankLine token per blank line.
|
|
427
482
|
const plainFile = parseMarkdown(plainSourceFullText);
|
|
428
483
|
removeQuotes(plainFile);
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
const tokenText = getRawText(token).trim();
|
|
433
|
-
if (token.type === "paragraph") {
|
|
434
|
-
const paragraphChildren = token.children ?? [];
|
|
435
|
-
const emphasisChildren = paragraphChildren.length > 0 && paragraphChildren[0].type === "emphasis"
|
|
436
|
-
? (paragraphChildren[0].children ?? [])
|
|
437
|
-
: [];
|
|
438
|
-
if (!(paragraphChildren.length === 1 &&
|
|
439
|
-
paragraphChildren[0].type === "emphasis" &&
|
|
440
|
-
emphasisChildren.length === 1 &&
|
|
441
|
-
emphasisChildren[0].type === "strong")) {
|
|
442
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid specification (\`${tokenText}\`)`);
|
|
443
|
-
}
|
|
444
|
-
const specificationHeading = String((emphasisChildren[0].children ?? [])[0]?.value ?? "");
|
|
445
|
-
if (specificationHeading === ACCEPTANCE_TEST_HEADING) {
|
|
446
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: ${ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec.`);
|
|
447
|
-
}
|
|
448
|
-
if (!ALLOWED_SPECIFICATION_HEADINGS.includes(specificationHeading)) {
|
|
449
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid specification heading (\`${specificationHeading}\`). Allowed headings: ${ALLOWED_SPECIFICATION_HEADINGS.join(", ")}`);
|
|
450
|
-
}
|
|
451
|
-
if (specificationHeading === FUNCTIONAL_REQUIREMENTS && !(specificationHeading in plainSource)) {
|
|
452
|
-
plainSource[specificationHeading] = null;
|
|
453
|
-
}
|
|
454
|
-
if (processedSpecificationHeadings.has(specificationHeading)) {
|
|
455
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Duplicate specification heading (\`${specificationHeading}\`)`);
|
|
456
|
-
}
|
|
457
|
-
if (specificationHeading === DEFINITIONS && currentSpecificationHeading !== null) {
|
|
458
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Definitions specification must be the first specification in the section (\`${tokenText}\`)`);
|
|
459
|
-
}
|
|
460
|
-
currentSpecificationHeading = specificationHeading;
|
|
461
|
-
if (plainSource[currentSpecificationHeading] == null) {
|
|
462
|
-
plainSource[currentSpecificationHeading] = { children: [] };
|
|
463
|
-
}
|
|
464
|
-
processedSpecificationHeadings.add(currentSpecificationHeading);
|
|
465
|
-
}
|
|
466
|
-
else if (token.type === "list") {
|
|
467
|
-
if (currentSpecificationHeading === null) {
|
|
468
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Missing specification heading (\`${tokenText}\`)`);
|
|
469
|
-
}
|
|
470
|
-
plainSource[currentSpecificationHeading].children.push(...(token.children ?? []));
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(token)}: Invalid source structure (\`${tokenText}\`)`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
// Acceptance tests belong under functional specs only.
|
|
477
|
-
for (const nonFunctionalHeading of [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]) {
|
|
478
|
-
const section = plainSource[nonFunctionalHeading];
|
|
479
|
-
if (!section)
|
|
480
|
-
continue;
|
|
481
|
-
for (const child of section.children) {
|
|
482
|
-
const nested = findAcceptanceTestHeading(child);
|
|
483
|
-
if (nested !== null) {
|
|
484
|
-
throw new PlainSyntaxError(`Plain syntax error: Syntax error at line ${lineNumber(nested)}: ${ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec.`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
const definitionsSection = plainSource[DEFINITIONS];
|
|
484
|
+
readSections(plainSource, plainFile, moduleName);
|
|
485
|
+
const definedConcepts = new Set();
|
|
486
|
+
const definitionsSection = sectionOf(plainSource, DEFINITIONS);
|
|
489
487
|
if (definitionsSection) {
|
|
490
488
|
for (const token of definitionsSection.children) {
|
|
491
489
|
const [newConcepts] = extractConceptsFromDefinition(renderToMarkdown(token));
|
|
492
490
|
for (const concept of newConcepts) {
|
|
491
|
+
definedConcepts.add(concept);
|
|
493
492
|
const index = requiredConcepts.indexOf(concept);
|
|
494
493
|
if (index !== -1)
|
|
495
494
|
requiredConcepts.splice(index, 1);
|
|
496
495
|
}
|
|
497
496
|
}
|
|
498
497
|
}
|
|
498
|
+
if (EXPORTED_CONCEPTS_DIRECTIVE in plainSourceObj.metadata) {
|
|
499
|
+
const tracedModule = modulesTrace[modulesTrace.length - 1];
|
|
500
|
+
const subject = tracedModule ? `Module '${tracedModule}'` : "Module";
|
|
501
|
+
for (const entry of plainSourceObj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]) {
|
|
502
|
+
const [exportedConcepts, errors] = extractConceptsFromDefinition(entry);
|
|
503
|
+
if (errors.length > 0) {
|
|
504
|
+
throw new PlainSyntaxError(`Plain syntax error: ${subject} exports an invalid concept (\`${entry}\`). ` +
|
|
505
|
+
"Expected a concept token, e.g. :ConceptName:.");
|
|
506
|
+
}
|
|
507
|
+
for (const exportedConcept of exportedConcepts) {
|
|
508
|
+
if (DEFAULT_CONCEPTS.has(exportedConcept)) {
|
|
509
|
+
throw new PlainSyntaxError(`Plain syntax error: ${subject} cannot export default concept ${exportedConcept}. ` +
|
|
510
|
+
"Only user-defined concepts can be exported.");
|
|
511
|
+
}
|
|
512
|
+
if (!definedConcepts.has(exportedConcept)) {
|
|
513
|
+
let message = `Plain syntax error: ${subject} exports undefined concept ${exportedConcept}.`;
|
|
514
|
+
if (REQUIRES_DIRECTIVE in plainSourceObj.metadata) {
|
|
515
|
+
message +=
|
|
516
|
+
" An exported concept must be defined in this module's definitions or in an " +
|
|
517
|
+
"imported module; a concept inherited through requires cannot be re-exported.";
|
|
518
|
+
}
|
|
519
|
+
throw new PlainSyntaxError(message);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
499
524
|
const requiredModules = plainSourceObj.metadata[REQUIRES_DIRECTIVE] ?? [];
|
|
500
525
|
return { plainSource, plainSourceObj, requiredModules, requiredConcepts, importedFrom };
|
|
501
526
|
}
|
|
@@ -515,29 +540,34 @@ export function readModulePlainSource(moduleName, templateDirs) {
|
|
|
515
540
|
const blob = findLargeBase64Blob(plainSourceText);
|
|
516
541
|
if (blob !== null) {
|
|
517
542
|
throw new UnsupportedBase64Content(`Module '${moduleName}' contains a base64-encoded blob (${blob.length} characters) ` +
|
|
518
|
-
"inlined in the specification. This is not supported." +
|
|
519
|
-
"Remove the base64 data from the .plain file or if necessary," +
|
|
543
|
+
"inlined in the specification. This is not supported. " +
|
|
544
|
+
"Remove the base64 data from the .plain file or if necessary, " +
|
|
520
545
|
"include the binary file path in the specification.");
|
|
521
546
|
}
|
|
522
547
|
return plainSourceText;
|
|
523
548
|
}
|
|
524
549
|
export function parsePlainFile(moduleName, codeVariables, templateDirs, importedModules, modulesTrace) {
|
|
525
550
|
const plainSourceText = readModulePlainSource(moduleName, templateDirs);
|
|
526
|
-
return parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, [
|
|
527
|
-
...modulesTrace,
|
|
528
|
-
moduleName,
|
|
529
|
-
]);
|
|
551
|
+
return parsePlainSource(plainSourceText, codeVariables, templateDirs, importedModules, [...modulesTrace, moduleName], moduleName);
|
|
530
552
|
}
|
|
531
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Parse every required module, deepest first, without validating it - each with code variables of its own - and
|
|
555
|
+
* return the definitions the directly required modules export. A module listed again right after it was walked is
|
|
556
|
+
* not parsed again, and what it exports is still visible to this module. `exports` records what each module walked
|
|
557
|
+
* exports, so a repeat can find it.
|
|
558
|
+
*/
|
|
559
|
+
export function processRequiredModules(requiredModules, templateDirs, allRequiredModules, modulesTrace, exports = new Map()) {
|
|
532
560
|
const exportedDefinitions = [];
|
|
533
561
|
for (const moduleName of requiredModules) {
|
|
534
562
|
if (modulesTrace.includes(moduleName)) {
|
|
535
563
|
throw new PlainSyntaxError(`Plain syntax error: Circular required module detected: ${moduleName}.`);
|
|
536
564
|
}
|
|
537
565
|
if (allRequiredModules.length > 0 && moduleName === allRequiredModules[allRequiredModules.length - 1]) {
|
|
566
|
+
// Already walked as an earlier entry; its exports are still visible to this module.
|
|
567
|
+
exportedDefinitions.push(...(exports.get(moduleName) ?? []));
|
|
538
568
|
continue;
|
|
539
569
|
}
|
|
540
|
-
const result = parsePlainFile(moduleName,
|
|
570
|
+
const result = parsePlainFile(moduleName, {}, templateDirs, [], []);
|
|
541
571
|
if (result.requiredModules.length === 0) {
|
|
542
572
|
if (allRequiredModules.length > 0) {
|
|
543
573
|
// Required modules must form one chain; independent modules cannot yet be merged.
|
|
@@ -545,40 +575,31 @@ export function processRequiredModules(requiredModules, codeVariables, templateD
|
|
|
545
575
|
}
|
|
546
576
|
}
|
|
547
577
|
else {
|
|
548
|
-
processRequiredModules(result.requiredModules,
|
|
549
|
-
...modulesTrace,
|
|
550
|
-
moduleName,
|
|
551
|
-
]);
|
|
578
|
+
processRequiredModules(result.requiredModules, templateDirs, allRequiredModules, [...modulesTrace, moduleName], exports);
|
|
552
579
|
}
|
|
553
|
-
|
|
580
|
+
const own = exportedDefinitionsOf(result);
|
|
581
|
+
exports.set(moduleName, own);
|
|
582
|
+
exportedDefinitions.push(...own);
|
|
554
583
|
allRequiredModules.push(moduleName);
|
|
555
584
|
}
|
|
556
585
|
return exportedDefinitions;
|
|
557
586
|
}
|
|
558
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* The definitions a required module exports: those of its `exported_concepts`, with the definitions they use. What
|
|
589
|
+
* it may export is checked as it is parsed (`parsePlainSource`).
|
|
590
|
+
*/
|
|
559
591
|
export function exportedDefinitionsOf(result) {
|
|
560
592
|
if (!(EXPORTED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata))
|
|
561
593
|
return [];
|
|
562
|
-
const exportedConcepts = [];
|
|
563
|
-
|
|
564
|
-
if (typeof concept === "string" && DEFAULT_CONCEPTS.has(concept)) {
|
|
565
|
-
throw new PlainSyntaxError(`Plain syntax error: Default concept cannot be exported: ${concept}. Only user-defined concepts can be exported.`);
|
|
566
|
-
}
|
|
567
|
-
if (typeof concept === "string") {
|
|
568
|
-
exportedConcepts.push(...extractConceptsFromDefinition(concept)[0]);
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
throw new PlainSyntaxError(`Plain syntax error: Invalid exported concept: ${concept}.`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return exportedConcepts.flatMap((exportedConcept) => findConceptDefinitionsInPlainSource(exportedConcept, result.plainSource[DEFINITIONS] ?? null));
|
|
594
|
+
const exportedConcepts = result.plainSourceObj.metadata[EXPORTED_CONCEPTS_DIRECTIVE].flatMap((concept) => extractConceptsFromDefinition(concept)[0]);
|
|
595
|
+
return exportedConcepts.flatMap((exportedConcept) => findConceptDefinitionsInPlainSource(exportedConcept, sectionOf(result.plainSource, DEFINITIONS) ?? null));
|
|
575
596
|
}
|
|
576
597
|
export function processExportedDefinitions(plainSource, exportedDefinitions) {
|
|
577
598
|
if (exportedDefinitions.length === 0)
|
|
578
599
|
return;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
600
|
+
if (!plainSource[DEFINITIONS])
|
|
601
|
+
plainSource[DEFINITIONS] = { children: [] };
|
|
602
|
+
const definitions = sectionOf(plainSource, DEFINITIONS);
|
|
582
603
|
for (const exportedDefinition of exportedDefinitions) {
|
|
583
604
|
const exportedRendered = renderToMarkdown(exportedDefinition).trim();
|
|
584
605
|
const alreadyPresent = definitions.children.some((definition) => renderToMarkdown(definition).trim() === exportedRendered);
|
|
@@ -587,17 +608,18 @@ export function processExportedDefinitions(plainSource, exportedDefinitions) {
|
|
|
587
608
|
}
|
|
588
609
|
}
|
|
589
610
|
export function plainFileParser(plainSourceFileName, templateDirs, options = {}) {
|
|
590
|
-
const
|
|
611
|
+
const tests = options.testDir === undefined ? "" : `${resolve(options.testDir)}${options.requireTestPlainFiles === true ? "!" : ""}`;
|
|
612
|
+
const key = `${options.allowNoFunctionalities ? "any" : "renderable"}\0${process.cwd()}\0${templateDirs.join("\0")}\0${plainSourceFileName}\0${tests}`;
|
|
591
613
|
if (activeCache === undefined) {
|
|
592
614
|
// Between calls: the parse from before, while nothing it could have read has changed since.
|
|
593
|
-
const fingerprint = filesFingerprint(templateDirs);
|
|
615
|
+
const fingerprint = filesFingerprint(templateDirs, options.testDir);
|
|
594
616
|
const kept = lastParses.get(key);
|
|
595
617
|
if (stillHolds(kept, fingerprint)) {
|
|
596
618
|
if (kept.error !== undefined)
|
|
597
619
|
throw kept.error;
|
|
598
620
|
return structuredClone(kept.parsed);
|
|
599
621
|
}
|
|
600
|
-
const result = parseNoting(() =>
|
|
622
|
+
const result = parseNoting(() => parseWithTestPlainFiles(plainSourceFileName, templateDirs, new Map(), options));
|
|
601
623
|
// 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.
|
|
602
624
|
if (fingerprint !== undefined)
|
|
603
625
|
remember(key, { fingerprint, linked: result.linked, linkedStamp: linkedStamp(result.linked), ...(result.error !== undefined ? { error: result.error } : { parsed: structuredClone(result.parsed) }) });
|
|
@@ -608,10 +630,10 @@ export function plainFileParser(plainSourceFileName, templateDirs, options = {})
|
|
|
608
630
|
let parsed = activeCache.get(key);
|
|
609
631
|
if (parsed === undefined) {
|
|
610
632
|
// Nothing changes while `run` runs: each folder set's fingerprint is taken once.
|
|
611
|
-
const dirsKey = templateDirs.join("\0")
|
|
633
|
+
const dirsKey = `${templateDirs.join("\0")}\0${options.testDir ?? ""}`;
|
|
612
634
|
let fingerprint = activeCache.fingerprints.get(dirsKey);
|
|
613
635
|
if (!activeCache.fingerprints.has(dirsKey)) {
|
|
614
|
-
fingerprint = filesFingerprint(templateDirs);
|
|
636
|
+
fingerprint = filesFingerprint(templateDirs, options.testDir);
|
|
615
637
|
activeCache.fingerprints.set(dirsKey, fingerprint);
|
|
616
638
|
}
|
|
617
639
|
const kept = lastParses.get(key);
|
|
@@ -622,7 +644,7 @@ export function plainFileParser(plainSourceFileName, templateDirs, options = {})
|
|
|
622
644
|
}
|
|
623
645
|
else {
|
|
624
646
|
const modules = activeCache.modules;
|
|
625
|
-
const result = parseNoting(() =>
|
|
647
|
+
const result = parseNoting(() => parseWithTestPlainFiles(plainSourceFileName, templateDirs, modules, options));
|
|
626
648
|
if (fingerprint !== undefined)
|
|
627
649
|
remember(key, { fingerprint, linked: result.linked, linkedStamp: linkedStamp(result.linked), ...(result.error !== undefined ? { error: result.error } : { parsed: structuredClone(result.parsed) }) });
|
|
628
650
|
if (result.error !== undefined)
|
|
@@ -633,6 +655,33 @@ export function plainFileParser(plainSourceFileName, templateDirs, options = {})
|
|
|
633
655
|
}
|
|
634
656
|
return parsed;
|
|
635
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Port of plain_file.parse_module_chain: a module and every module in its `requires` chain, each as its name and its
|
|
660
|
+
* tree - every required module first, deepest ancestors first, then the module itself. A module reached through more
|
|
661
|
+
* than one `requires` path appears once, at its first (deepest) position.
|
|
662
|
+
*/
|
|
663
|
+
export function parseModuleChain(plainSourceFileName, templateDirs, options = {}) {
|
|
664
|
+
return withParseCache(() => {
|
|
665
|
+
const [moduleName, tree, requiredModules] = plainFileParser(plainSourceFileName, templateDirs, options);
|
|
666
|
+
const chain = [];
|
|
667
|
+
const walked = new Set();
|
|
668
|
+
const walk = (names) => {
|
|
669
|
+
for (const name of names) {
|
|
670
|
+
if (walked.has(name))
|
|
671
|
+
continue;
|
|
672
|
+
const [requiredName, requiredTree, itsRequires] = plainFileParser(`${name}${PLAIN_SOURCE_FILE_EXTENSION}`, templateDirs);
|
|
673
|
+
walk(itsRequires);
|
|
674
|
+
if (walked.has(requiredName))
|
|
675
|
+
continue;
|
|
676
|
+
walked.add(requiredName);
|
|
677
|
+
chain.push([requiredName, requiredTree]);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
walk(requiredModules);
|
|
681
|
+
chain.push([moduleName, tree]);
|
|
682
|
+
return chain;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
636
685
|
/**
|
|
637
686
|
* One parse per file for the length of `run`, however many times it is asked for.
|
|
638
687
|
*
|
|
@@ -662,9 +711,13 @@ function remember(key, entry) {
|
|
|
662
711
|
while (lastParses.size > KEPT_PARSES)
|
|
663
712
|
lastParses.delete(lastParses.keys().next().value);
|
|
664
713
|
}
|
|
665
|
-
function filesFingerprint(templateDirs) {
|
|
714
|
+
function filesFingerprint(templateDirs, testDir) {
|
|
666
715
|
try {
|
|
667
716
|
const parts = [];
|
|
717
|
+
// Every .plain file under the test folder, by name and text: a scenario written, changed or removed is seen.
|
|
718
|
+
if (testDir !== undefined)
|
|
719
|
+
for (const path of plainFilesUnder(resolve(testDir)))
|
|
720
|
+
parts.push(`${path}:${readFileSync(path, "utf8")}`);
|
|
668
721
|
for (const dir of [process.cwd(), ...templateDirs]) {
|
|
669
722
|
let entries;
|
|
670
723
|
try {
|
|
@@ -688,6 +741,74 @@ function filesFingerprint(templateDirs) {
|
|
|
688
741
|
return undefined;
|
|
689
742
|
}
|
|
690
743
|
}
|
|
744
|
+
/** Every .plain file under a folder, at any depth, sorted; none when it is not there. */
|
|
745
|
+
function plainFilesUnder(dir) {
|
|
746
|
+
let entries;
|
|
747
|
+
try {
|
|
748
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
const found = [];
|
|
754
|
+
for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
755
|
+
const path = join(dir, entry.name);
|
|
756
|
+
if (entry.isDirectory())
|
|
757
|
+
found.push(...plainFilesUnder(path));
|
|
758
|
+
else if (entry.name.endsWith(PLAIN_SOURCE_FILE_EXTENSION))
|
|
759
|
+
found.push(path);
|
|
760
|
+
}
|
|
761
|
+
return found;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* A module parsed, and - with `options.testDir` - its chain's test plain files bound to it: the scenarios on its
|
|
765
|
+
* functional specs and on the ones it inherits, `TEST_PLAIN_FILES` naming each file and what it binds to.
|
|
766
|
+
*/
|
|
767
|
+
function parseWithTestPlainFiles(plainSourceFileName, templateDirs, cache, options) {
|
|
768
|
+
const [moduleName, tree, requiredModules] = parseModule(plainSourceFileName, templateDirs, cache, options);
|
|
769
|
+
if (options.testDir === undefined)
|
|
770
|
+
return [moduleName, tree, requiredModules];
|
|
771
|
+
// The chain, deepest first, each tree a copy the scenarios are bound into.
|
|
772
|
+
const chain = [];
|
|
773
|
+
const walked = new Set();
|
|
774
|
+
const walk = (names) => {
|
|
775
|
+
for (const name of names) {
|
|
776
|
+
if (walked.has(name))
|
|
777
|
+
continue;
|
|
778
|
+
let parsed = cache.get(name);
|
|
779
|
+
if (parsed === undefined) {
|
|
780
|
+
parsed = parseModule(`${name}${PLAIN_SOURCE_FILE_EXTENSION}`, templateDirs, cache);
|
|
781
|
+
cache.set(name, parsed);
|
|
782
|
+
}
|
|
783
|
+
walk(parsed[2]);
|
|
784
|
+
if (walked.has(parsed[0]))
|
|
785
|
+
continue;
|
|
786
|
+
walked.add(parsed[0]);
|
|
787
|
+
chain.push([parsed[0], structuredClone(parsed[1])]);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
walk(requiredModules);
|
|
791
|
+
const own = structuredClone(tree);
|
|
792
|
+
chain.push([moduleName, own]);
|
|
793
|
+
const testDirs = chain.map(([name]) => join(options.testDir, name));
|
|
794
|
+
const bound = bindTestPlainFiles(chain, testDirs, templateDirs, templateDirs[0] ?? ".");
|
|
795
|
+
if (options.requireTestPlainFiles === true)
|
|
796
|
+
checkTestPlainFiles(chain, bound);
|
|
797
|
+
// What the module inherits carries the scenarios bound in the module it is written in.
|
|
798
|
+
const boundItems = new Map();
|
|
799
|
+
for (const [name, chainTree] of chain)
|
|
800
|
+
for (const item of getFunctionalSpecs(chainTree))
|
|
801
|
+
boundItems.set(`${name}\0${item.id}`, item);
|
|
802
|
+
const inherited = own[REQUIRED_FUNCTIONAL_SPECS];
|
|
803
|
+
if (inherited !== undefined) {
|
|
804
|
+
own[REQUIRED_FUNCTIONAL_SPECS] = inherited.map((item) => {
|
|
805
|
+
const boundItem = boundItems.get(`${item.module}\0${item.id}`);
|
|
806
|
+
return boundItem === undefined ? item : { ...item, [CONFORMANCE_TESTS]: boundItem[CONFORMANCE_TESTS], [ACCEPTANCE_TESTS]: boundItem[ACCEPTANCE_TESTS] };
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
own[TEST_PLAIN_FILES] = Object.fromEntries(bound.map((file) => [file.name, { module: file.parsed.module, func_id: file.parsed.funcId }]));
|
|
810
|
+
return [moduleName, own, requiredModules];
|
|
811
|
+
}
|
|
691
812
|
/**
|
|
692
813
|
* The functional specs a module inherits through `requires`, bottom of the chain first.
|
|
693
814
|
*
|
|
@@ -701,7 +822,7 @@ function inheritFunctionalSpecs(requires, templateDirs, cache) {
|
|
|
701
822
|
const inherited = [];
|
|
702
823
|
const seen = new Set();
|
|
703
824
|
const add = (item) => {
|
|
704
|
-
const key = `${item.module}/${item.
|
|
825
|
+
const key = `${item.module}/${item.id}`;
|
|
705
826
|
if (seen.has(key))
|
|
706
827
|
return;
|
|
707
828
|
seen.add(key);
|
|
@@ -716,7 +837,8 @@ function inheritFunctionalSpecs(requires, templateDirs, cache) {
|
|
|
716
837
|
const [moduleName, tree] = parsed;
|
|
717
838
|
for (const item of tree[REQUIRED_FUNCTIONAL_SPECS] ?? [])
|
|
718
839
|
add(item);
|
|
719
|
-
|
|
840
|
+
for (const item of getFunctionalSpecs(tree))
|
|
841
|
+
add({ ...item, module: moduleName });
|
|
720
842
|
}
|
|
721
843
|
return inherited;
|
|
722
844
|
}
|
|
@@ -741,13 +863,12 @@ function parseModule(plainSourceFileName, templateDirs, cache, options = {}) {
|
|
|
741
863
|
"At least one functionality is required for rendering.");
|
|
742
864
|
}
|
|
743
865
|
if (!options.allowNoFunctionalities && countFunctionalities(result.plainSource) === 0) {
|
|
744
|
-
throw new PlainSyntaxError(`Plain syntax error: Module '${moduleName}' has
|
|
745
|
-
|
|
866
|
+
throw new PlainSyntaxError(`Plain syntax error: Module '${moduleName}' has a '${FUNCTIONAL_REQUIREMENTS}' section ` +
|
|
867
|
+
`without a \`# <func-id>\` heading. ${PREVIOUS_SYNTAX_HINT}`);
|
|
746
868
|
}
|
|
747
869
|
validateFunctionalitiesHaveImplementationReqs(result.plainSource, moduleName, result.requiredModules.length > 0);
|
|
748
|
-
const exportedDefinitions = processRequiredModules(result.requiredModules,
|
|
870
|
+
const exportedDefinitions = processRequiredModules(result.requiredModules, templateDirs, [], []);
|
|
749
871
|
processExportedDefinitions(result.plainSource, exportedDefinitions);
|
|
750
|
-
processAcceptanceTests(result.plainSource);
|
|
751
872
|
checkForLinkedResources(result.plainSource, templateDirs);
|
|
752
873
|
const marshalled = marshallPlainSource(result.plainSource);
|
|
753
874
|
processCodeVariables(marshalled, codeVariables);
|