@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
@@ -6,11 +6,14 @@
6
6
  * us the first. The second is `renderToMarkdown` below - a serializer for the
7
7
  * subset of markdown the plain language uses, matching mistletoe's output
8
8
  * (source indentation preserved, links rewritten as `[resource]` spans).
9
+ *
10
+ * The reference parses inside the renderer's context, so blank lines are tokens of
11
+ * their own (BlankLine), one per blank line, and survive in what is rendered; they
12
+ * are recovered here from the source.
9
13
  */
10
14
  export declare const RESOURCE_MARKER = "[resource]";
11
15
  /** Extra fields the parser hangs off AST nodes, as the Python code hangs them off tokens. */
12
16
  export interface PlainAnnotations {
13
- acceptance_tests?: AnyNode[];
14
17
  linked_resources?: LinkedResource[];
15
18
  /** Set when `value` holds the original source slice rather than mdast's decoded text. */
16
19
  raw?: boolean;
@@ -20,6 +23,19 @@ export interface PlainAnnotations {
20
23
  leader?: string;
21
24
  indentation?: number;
22
25
  prepend?: number;
26
+ /**
27
+ * The blank lines in the source before a block, after the block before it: one run, and one more for each comment
28
+ * dropped in between - mistletoe's BlankLine tokens, one per blank line.
29
+ */
30
+ blankRuns?: number[];
31
+ /** A thematic break's line, or a setext heading's underline, as its container holds it. */
32
+ sourceLine?: string;
33
+ /** A fenced code block's opening: its indentation, its delimiter and its info string, as written. */
34
+ fence?: {
35
+ indentation: number;
36
+ delimiter: string;
37
+ infoString: string;
38
+ };
23
39
  }
24
40
  export interface LinkedResource {
25
41
  text: string;
@@ -60,15 +76,28 @@ export interface Section {
60
76
  * loses the fact that it was not `*`. mistletoe's MarkdownRenderer round-trips the
61
77
  * source instead, so re-serialising a decoded tree drifts from the Python output.
62
78
  * Recording the raw slice for leaf nodes and the real delimiters for emphasis and
63
- * strong keeps the two renderers in agreement.
79
+ * strong keeps the two renderers in agreement. Tables are GFM's, as mistletoe parses them.
64
80
  */
65
81
  export declare function parseMarkdown(text: string): Root;
66
82
  export declare function lineNumber(node: AnyNode): number;
67
- /** Port of plain_file.remove_quotes: block quotes are comments and are dropped. */
83
+ /**
84
+ * Port of plain_file.remove_quotes: block quotes are comments and are dropped. The blank
85
+ * lines around a dropped one stay, as mistletoe's BlankLine tokens do.
86
+ */
68
87
  export declare function removeQuotes(node: AnyNode): void;
69
88
  /** Port of plain_file.get_raw_text. */
70
89
  export declare function getRawText(node: AnyNode): string;
71
90
  /** Port of mistletoe.utils.traverse restricted to a node type. */
72
91
  export declare function traverse(node: AnyNode, type: string): AnyNode[];
92
+ /**
93
+ * Port of MarkdownRenderer.blocks_to_lines. Blocks are concatenated with no separator;
94
+ * blank lines are their own tokens in mistletoe, and are recovered here from the source.
95
+ */
96
+ export declare function blocksToLines(nodes: AnyNode[]): string[];
73
97
  /** Port of PlainRenderer.render. */
74
98
  export declare function renderToMarkdown(node: AnyNode): string;
99
+ /**
100
+ * Port of plain_file.render_tokens: blocks rendered as one markdown text, without leading
101
+ * and trailing blank lines.
102
+ */
103
+ export declare function renderTokens(nodes: AnyNode[]): string;
@@ -1,4 +1,6 @@
1
1
  import { fromMarkdown } from "mdast-util-from-markdown";
2
+ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
3
+ import { gfmTable } from "micromark-extension-gfm-table";
2
4
  /**
3
5
  * The markdown layer.
4
6
  *
@@ -7,6 +9,10 @@ import { fromMarkdown } from "mdast-util-from-markdown";
7
9
  * us the first. The second is `renderToMarkdown` below - a serializer for the
8
10
  * subset of markdown the plain language uses, matching mistletoe's output
9
11
  * (source indentation preserved, links rewritten as `[resource]` spans).
12
+ *
13
+ * The reference parses inside the renderer's context, so blank lines are tokens of
14
+ * their own (BlankLine), one per blank line, and survive in what is rendered; they
15
+ * are recovered here from the source.
10
16
  */
11
17
  export const RESOURCE_MARKER = "[resource]";
12
18
  /**
@@ -16,17 +22,35 @@ export const RESOURCE_MARKER = "[resource]";
16
22
  * loses the fact that it was not `*`. mistletoe's MarkdownRenderer round-trips the
17
23
  * source instead, so re-serialising a decoded tree drifts from the Python output.
18
24
  * Recording the raw slice for leaf nodes and the real delimiters for emphasis and
19
- * strong keeps the two renderers in agreement.
25
+ * strong keeps the two renderers in agreement. Tables are GFM's, as mistletoe parses them.
20
26
  */
21
27
  export function parseMarkdown(text) {
22
- const root = fromMarkdown(text);
23
- pinToSource(root, text);
28
+ const root = fromMarkdown(text, { extensions: [gfmTable()], mdastExtensions: [gfmTableFromMarkdown()] });
29
+ pinToSource(root, text, 1, text.split("\n"));
24
30
  return root;
25
31
  }
26
32
  const RAW_LEAF_TYPES = new Set(["text", "inlineCode", "html"]);
27
- function pinToSource(node, source, origin = 1) {
33
+ /** The containers whose children are blocks, with blank lines between them as mistletoe keeps them. */
34
+ const BLOCK_CONTAINERS = new Set(["root", "list", "listItem", "blockquote"]);
35
+ /** The whitespace-only lines strictly between two source lines. */
36
+ function blankLinesWithin(lines, afterLine, beforeLine) {
37
+ let count = 0;
38
+ for (let line = afterLine + 1; line < beforeLine; line += 1)
39
+ if ((lines[line - 1] ?? "").trim() === "")
40
+ count += 1;
41
+ return count;
42
+ }
43
+ function pinToSource(node, source, origin, lines) {
28
44
  const start = node.position?.start.offset;
29
45
  const end = node.position?.end.offset;
46
+ if (BLOCK_CONTAINERS.has(node.type)) {
47
+ const children = childrenOf(node);
48
+ children.forEach((child, index) => {
49
+ const previousEnd = children[index - 1]?.position?.end.line;
50
+ const childStart = child.position?.start.line;
51
+ child.blankRuns = [previousEnd === undefined || childStart === undefined ? 0 : blankLinesWithin(lines, previousEnd, childStart)];
52
+ });
53
+ }
30
54
  if (start !== undefined && end !== undefined) {
31
55
  if (RAW_LEAF_TYPES.has(node.type)) {
32
56
  const raw = source.slice(start, end);
@@ -41,6 +65,23 @@ function pinToSource(node, source, origin = 1) {
41
65
  node.openDelimiter = source.slice(start, start + width);
42
66
  node.closeDelimiter = source.slice(end - width, end);
43
67
  }
68
+ else if (node.type === "thematicBreak") {
69
+ // mistletoe keeps the line as written, with its indentation in the container.
70
+ node.sourceLine = (lines[node.position.start.line - 1] ?? "").slice(origin - 1);
71
+ }
72
+ else if (node.type === "heading" && source[start] !== "#") {
73
+ // A setext heading: mistletoe keeps its underline as written.
74
+ node.sourceLine = (lines[node.position.end.line - 1] ?? "").slice(origin - 1).trimEnd();
75
+ }
76
+ else if (node.type === "code") {
77
+ const opening = /^(`{3,}|~{3,})([^\n]*)/.exec(source.slice(start, end));
78
+ if (opening !== null)
79
+ node.fence = { indentation: column(node) - origin, delimiter: opening[1], infoString: opening[2] };
80
+ }
81
+ else if (node.type === "definition") {
82
+ node.value = source.slice(start, end);
83
+ node.raw = true;
84
+ }
44
85
  }
45
86
  let childOrigin = origin;
46
87
  if (node.type === "listItem") {
@@ -61,7 +102,7 @@ function pinToSource(node, source, origin = 1) {
61
102
  childOrigin = contentColumn;
62
103
  }
63
104
  for (const child of childrenOf(node))
64
- pinToSource(child, source, childOrigin);
105
+ pinToSource(child, source, childOrigin, lines);
65
106
  }
66
107
  export function lineNumber(node) {
67
108
  return node.position?.start.line ?? 0;
@@ -75,14 +116,24 @@ function isParent(node) {
75
116
  function childrenOf(node) {
76
117
  return node.children ?? [];
77
118
  }
78
- /** Port of plain_file.remove_quotes: block quotes are comments and are dropped. */
119
+ /**
120
+ * Port of plain_file.remove_quotes: block quotes are comments and are dropped. The blank
121
+ * lines around a dropped one stay, as mistletoe's BlankLine tokens do.
122
+ */
79
123
  export function removeQuotes(node) {
80
124
  if (!isParent(node))
81
125
  return;
82
126
  const kept = [];
127
+ let dropped = [];
83
128
  for (const child of node.children) {
84
- if (child.type === "blockquote")
129
+ if (child.type === "blockquote") {
130
+ dropped = [...dropped, ...(child.blankRuns ?? [0])];
85
131
  continue;
132
+ }
133
+ if (dropped.length > 0) {
134
+ child.blankRuns = [...dropped, ...(child.blankRuns ?? [0])];
135
+ dropped = [];
136
+ }
86
137
  removeQuotes(child);
87
138
  kept.push(child);
88
139
  }
@@ -98,6 +149,8 @@ export function getRawText(node) {
98
149
  const value = node.value;
99
150
  if (value !== undefined)
100
151
  return value;
152
+ if (node.type === "thematicBreak")
153
+ return "";
101
154
  throw new Error(`Unknown token type: ${node.type}`);
102
155
  }
103
156
  /** Port of mistletoe.utils.traverse restricted to a node type. */
@@ -140,13 +193,24 @@ function renderInline(node) {
140
193
  function renderInlineChildren(node) {
141
194
  return childrenOf(node).map(renderInline).join("");
142
195
  }
143
- /** Blank lines between two sibling blocks, as they appeared in the source. */
144
- function blankLinesBetween(previous, next) {
145
- const previousEnd = previous.position?.end.line;
146
- const nextStart = next.position?.start.line;
147
- if (previousEnd === undefined || nextStart === undefined)
148
- return 0;
149
- return Math.max(0, nextStart - previousEnd - 1);
196
+ /** Whether a block ends with a list: a list, or a list item whose last block does. */
197
+ function endsWithList(node) {
198
+ if (node.type === "list")
199
+ return true;
200
+ const last = childrenOf(node).at(-1);
201
+ return node.type === "listItem" && last !== undefined && endsWithList(last);
202
+ }
203
+ /**
204
+ * The blank lines mistletoe keeps between two sibling blocks: one per blank line in the
205
+ * source, comment lines dropped in between counted out. A list item that ends without a
206
+ * next item gives back only the last of the blank lines after it (`ListItem.read` steps
207
+ * back one line), so the run right after a list counts once.
208
+ */
209
+ function blankLinesBefore(previous, node) {
210
+ const runs = [...(node.blankRuns ?? [0])];
211
+ if (endsWithList(previous))
212
+ runs[0] = Math.min(runs[0], 1);
213
+ return runs.reduce((sum, run) => sum + run, 0);
150
214
  }
151
215
  /** Port of MarkdownRenderer.prefix_lines. */
152
216
  function prefixLines(lines, firstPrefix, followingPrefix) {
@@ -157,14 +221,14 @@ function prefixLines(lines, firstPrefix, followingPrefix) {
157
221
  }
158
222
  /**
159
223
  * Port of MarkdownRenderer.blocks_to_lines. Blocks are concatenated with no separator;
160
- * blank lines are their own tokens in mistletoe, and are recovered here from the gaps
161
- * between sibling positions.
224
+ * blank lines are their own tokens in mistletoe, and are recovered here from the source.
162
225
  */
163
- function blocksToLines(nodes) {
226
+ export function blocksToLines(nodes) {
164
227
  const lines = [];
165
228
  nodes.forEach((node, index) => {
166
- if (index > 0 && blankLinesBetween(nodes[index - 1], node) > 0)
167
- lines.push("");
229
+ if (index > 0)
230
+ for (let blank = blankLinesBefore(nodes[index - 1], node); blank > 0; blank -= 1)
231
+ lines.push("");
168
232
  lines.push(...renderBlockLines(node));
169
233
  });
170
234
  return lines;
@@ -180,16 +244,67 @@ function renderListItemLines(item) {
180
244
  " ".repeat(Math.max(0, prepend - leader.length - indentation));
181
245
  return prefixLines(lines.length > 0 ? lines : [""], firstPrefix, " ".repeat(Math.max(0, prepend)));
182
246
  }
247
+ /** mistletoe's alignment of a table column: null for left or none, 0 for centre, 1 for right. */
248
+ function alignOf(align) {
249
+ return align === "center" ? 0 : align === "right" ? 1 : null;
250
+ }
251
+ /**
252
+ * Port of MarkdownRenderer.render_table: each cell re-rendered with its pipes escaped,
253
+ * every column as wide as its widest cell and at least three, padded by its alignment,
254
+ * and the separator row rebuilt from the alignments.
255
+ */
256
+ function renderTableLines(node) {
257
+ const aligns = (node.align ?? []).map(alignOf);
258
+ const rows = childrenOf(node).map((row) => childrenOf(row).map((cell) => (renderInlineChildren(cell).split("\n")[0] ?? "").replace(/\\\|/g, "|").replace(/\|/g, "\\|")));
259
+ const [header = [], ...body] = rows;
260
+ const columns = Math.max(aligns.length, ...rows.map((row) => row.length));
261
+ const widths = Array.from({ length: columns }, (_, index) => Math.max(3, ...rows.map((row) => (row[index] ?? "").length)));
262
+ const lineOf = (cells) => "| " +
263
+ widths
264
+ .map((width, index) => {
265
+ const text = cells[index] ?? "";
266
+ const align = aligns[index] ?? null;
267
+ if (align === null)
268
+ return text.padEnd(width);
269
+ if (align === 1)
270
+ return text.padStart(width);
271
+ const left = Math.floor((width - text.length) / 2);
272
+ return " ".repeat(Math.max(0, left)) + text + " ".repeat(Math.max(0, width - text.length - left));
273
+ })
274
+ .join(" | ") +
275
+ " |";
276
+ const separator = widths.map((width, index) => {
277
+ const align = aligns[index] ?? null;
278
+ return (align === 0 ? ":" : "-") + "-".repeat(width - 2) + (align === 0 || align === 1 ? ":" : "-");
279
+ });
280
+ return [lineOf(header), `| ${separator.join(" | ")} |`, ...body.map(lineOf)];
281
+ }
183
282
  function renderBlockLines(node) {
184
283
  switch (node.type) {
185
284
  case "paragraph":
186
285
  return renderInlineChildren(node).split("\n");
187
- case "heading":
188
- return [`${"#".repeat(node.depth)} ${renderInlineChildren(node)}`];
189
- case "code":
190
- return [`\`\`\`${node.lang ?? ""}`, ...(node.value ?? "").split("\n"), "\`\`\`"];
286
+ case "heading": {
287
+ // A setext heading keeps its underline; an ATX one is its hashes and its text.
288
+ if (node.sourceLine !== undefined)
289
+ return [...renderInlineChildren(node).split("\n"), node.sourceLine];
290
+ const text = renderInlineChildren(node);
291
+ return [`${"#".repeat(node.depth)}${text ? ` ${text}` : ""}`];
292
+ }
293
+ case "code": {
294
+ const content = (node.value ?? "").split("\n");
295
+ if (node.fence === undefined)
296
+ return prefixLines(content, " ", " ");
297
+ const indentation = " ".repeat(Math.max(0, node.fence.indentation));
298
+ return [
299
+ indentation + node.fence.delimiter + node.fence.infoString,
300
+ ...prefixLines(content, indentation, indentation),
301
+ indentation + node.fence.delimiter,
302
+ ];
303
+ }
191
304
  case "thematicBreak":
192
- return ["---"];
305
+ return [node.sourceLine ?? "---"];
306
+ case "table":
307
+ return renderTableLines(node);
193
308
  case "blockquote":
194
309
  return blocksToLines(childrenOf(node)).map((line) => (line ? `> ${line}` : ">"));
195
310
  case "list":
@@ -206,3 +321,10 @@ function renderBlockLines(node) {
206
321
  export function renderToMarkdown(node) {
207
322
  return renderBlockLines(node).join("\n");
208
323
  }
324
+ /**
325
+ * Port of plain_file.render_tokens: blocks rendered as one markdown text, without leading
326
+ * and trailing blank lines.
327
+ */
328
+ export function renderTokens(nodes) {
329
+ return blocksToLines(nodes).join("\n").replace(/^\n+|\n+$/g, "");
330
+ }
@@ -3,7 +3,7 @@ import { basename } from "node:path";
3
3
  import { resolveTemplateDirs } from "../regenerate/paths.js";
4
4
  import { DEFAULT_CONCEPT_DEFINITIONS, definitionText, extractConceptsFromDefinition } from "./concepts.js";
5
5
  import { renderToMarkdown } from "./markdown.js";
6
- import { exportedDefinitionsOf, IMPORT_DIRECTIVE, parsePlainFile, parsePlainSource, PLAIN_SOURCE_FILE_EXTENSION, readPlainSourceMetadata, REQUIRES_DIRECTIVE, restoreStrippedLines, } from "./parser.js";
6
+ import { exportedDefinitionsOf, IMPORT_DIRECTIVE, parsePlainFile, parsePlainSource, PLAIN_SOURCE_FILE_EXTENSION, readPlainSourceMetadata, REQUIRES_DIRECTIVE, restoreStrippedLines, sectionOf, } from "./parser.js";
7
7
  import { DEFINITIONS } from "./spec.js";
8
8
  /**
9
9
  * A module read for what it can name, as the parser builds its definitions: its own, its imports' (`import`), and its
@@ -17,7 +17,7 @@ export function readModule(moduleName, plainFile, templateDirs) {
17
17
  for (const node of nodes)
18
18
  definitions.push({ node, module: result.importedFrom.get(node) ?? module });
19
19
  };
20
- const definitionsOf = (result) => result.plainSource[DEFINITIONS]?.children ?? [];
20
+ const definitionsOf = (result) => sectionOf(result.plainSource, DEFINITIONS)?.children ?? [];
21
21
  let requires;
22
22
  let parsed = parse(moduleName);
23
23
  if (parsed !== undefined) {
@@ -1,12 +1,28 @@
1
- import { type AnyNode, type Section } from "./markdown.ts";
2
- import { type MarshalledPlainSource, type SpecificationItem } from "./spec.ts";
1
+ import { type AnyNode, type LinkedResource, type Section } from "./markdown.ts";
2
+ import { type CodeVariable, type FunctionalSpecItem, type MarshalledPlainSource } from "./spec.ts";
3
3
  export declare const IMPORT_DIRECTIVE = "import";
4
4
  export declare const REQUIRES_DIRECTIVE = "requires";
5
+ export declare const PREVIOUS_SYNTAX_HINT = "A functionality is a `# <func-id>` heading followed by its body; the previous syntax is not supported.";
5
6
  export declare const REQUIRED_CONCEPTS_DIRECTIVE = "required_concepts";
6
7
  export declare const EXPORTED_CONCEPTS_DIRECTIVE = "exported_concepts";
7
8
  export declare const PLAIN_SOURCE_FILE_EXTENSION = ".plain";
8
- /** definitions / implementation reqs / test reqs always exist as keys; functional specs does not. */
9
- export type ParsedPlainSource = Record<string, Section | null | undefined>;
9
+ /** A functional spec as it is read: its func-id and the blocks of its body. */
10
+ export interface Functionality {
11
+ id: string;
12
+ lineNumber: number;
13
+ /** Every block after its heading, up to the next heading or section marker, in source order. */
14
+ body: AnyNode[];
15
+ linkedResources: LinkedResource[];
16
+ }
17
+ /**
18
+ * definitions / implementation reqs / test reqs always exist as keys, each a section of list items; functional specs
19
+ * does not, and is a list of functional specs when it does.
20
+ */
21
+ export type ParsedPlainSource = Record<string, Section | Functionality[] | null | undefined>;
22
+ /** A list section of a parsed module. */
23
+ export declare function sectionOf(plainSource: ParsedPlainSource, heading: string): Section | null | undefined;
24
+ /** The functional specs of a parsed module, none when it has no section for them. */
25
+ export declare function functionalitiesOf(plainSource: ParsedPlainSource): Functionality[];
10
26
  export interface FrontmatterPost {
11
27
  metadata: Record<string, unknown>;
12
28
  content: string;
@@ -22,41 +38,37 @@ export interface PlainFileParseResult {
22
38
  export declare function getFilenameFromModuleName(moduleName: string): string;
23
39
  export declare function getModuleNameFromFilename(filename: string): string;
24
40
  export declare function hasScheme(target: string): boolean;
41
+ /**
42
+ * Port of loaders.resolve_linked_resource: a link's file, in the first of the folders resources are loaded from - the
43
+ * spec's own folder first, then the other template folders - where it exists, or null. Existence, not being a file,
44
+ * decides the match, as in `open_from`'s search. Links are relative to the spec, so a spec in a subfolder is checked
45
+ * where it is, wherever turri was started.
46
+ */
47
+ export declare function resolveLinkedResource(dirs: string[], target: string): string | null;
25
48
  /** The links written in an item - what each says and where it points - as the parser reads them, checked or not. */
26
49
  export declare function linksIn(section: AnyNode): Array<{
27
50
  text: string;
28
51
  target: string;
29
52
  node: AnyNode;
30
53
  }>;
54
+ /**
55
+ * The links in the blocks, each checked to be a relative path to an existing file that is not a .plain file.
56
+ * `where` names the place of the links in error messages, e.g. " in test plain file 'tests/m/f.plain', scenario `s`".
57
+ */
58
+ export declare function findLinkedResources(nodes: AnyNode[], dirs?: string[], where?: string): LinkedResource[];
31
59
  export declare function checkSectionForLinkedResources(section: AnyNode, dirs?: string[]): void;
32
60
  export declare function checkForLinkedResources(plainSource: ParsedPlainSource, dirs?: string[]): void;
33
- export declare function processSectionCodeVariables(section: SpecificationItem, codeVariables: Record<string, Record<string, string>>): void;
61
+ /** Put `{{ name }}` placeholders back into the section text and record the code variables it uses. */
62
+ export declare function processSectionCodeVariables(section: {
63
+ code_variables?: CodeVariable[];
64
+ } & Record<string, unknown>, codeVariables: Record<string, Record<string, string>>, textKey?: string): void;
34
65
  export declare function processCodeVariables(marshalled: MarshalledPlainSource, codeVariables: Record<string, Record<string, string>>): void;
35
66
  /** Whether the module declares a ***functional specs*** section (even if it is empty). */
36
67
  export declare function hasFunctionalSpecsSection(plainSource: ParsedPlainSource): boolean;
37
68
  /** Number of functionalities under ***functional specs*** (0 if absent or empty). */
38
69
  export declare function countFunctionalities(plainSource: ParsedPlainSource): number;
39
70
  export declare function validateFunctionalitiesHaveImplementationReqs(plainSource: ParsedPlainSource, moduleName: string, hasRequires?: boolean): void;
40
- /**
41
- * Is this token an `acceptance tests` heading?
42
- *
43
- * True only for a paragraph holding exactly one emphasis, holding exactly one strong,
44
- * holding exactly one text node whose content is the heading. Anything that looks like
45
- * a heading but reads differently comes back as a problem string rather than false, so
46
- * the caller can reject a near miss instead of silently keeping it.
47
- */
48
- export declare function isAcceptanceTestHeading(token: AnyNode): [boolean, string | null];
49
- /** Find an acceptance tests heading anywhere in a subtree. */
50
- export declare function findAcceptanceTestHeading(token: AnyNode): AnyNode | null;
51
- /**
52
- * Pull the acceptance tests out of one functionality.
53
- *
54
- * A functionality with acceptance tests has three children: its own text, the
55
- * `***acceptance tests***` heading, and the list of tests. The heading and list are
56
- * lifted onto `acceptance_tests` and removed from the children.
57
- */
58
- export declare function processSingleAcceptanceTestRequirement(functionalRequirement: AnyNode): void;
59
- export declare function processAcceptanceTests(plainSource: ParsedPlainSource): void;
71
+ export declare function marshallFunctionality(functionality: Functionality): FunctionalSpecItem;
60
72
  export declare function marshallPlainSource(input: ParsedPlainSource): MarshalledPlainSource;
61
73
  export declare function normalizeLineEndings(plainSourceText: string): string;
62
74
  /**
@@ -65,7 +77,22 @@ export declare function normalizeLineEndings(plainSourceText: string): string;
65
77
  */
66
78
  export declare function restoreStrippedLines(plainSourceText: string, content: string): string;
67
79
  export declare function readPlainSourceMetadata(plainSourceText: string): FrontmatterPost;
68
- export declare function parsePlainSource(plainSourceText: string, codeVariables: Record<string, Record<string, string>>, templateDirs: string[], importedModules: string[], modulesTrace: string[]): PlainFileParseResult;
80
+ /** `Syntax error at line N in module 'm'`, without the parts that are not known. */
81
+ export declare function syntaxErrorAt(line: number | null | undefined, moduleName: string | null | undefined): string;
82
+ export declare const TEST_PLAIN_FILE_SECTION_HEADINGS: string[];
83
+ /** Throw when a `***conformance tests***` or `***acceptance tests***` marker is anywhere in a spec plain file. */
84
+ export declare function checkNoTestSections(plainSourceFullText: string, moduleName?: string | null): void;
85
+ /** The name in a `***name***` paragraph, or null when the paragraph is not a section marker. */
86
+ export declare function sectionMarker(token: AnyNode): string | null;
87
+ /**
88
+ * Add the sections of the rendered spec plain file to `plainSource`.
89
+ *
90
+ * A list section holds a section whose children are its list items; `functional specs` holds a list of
91
+ * functionalities, one per `# <func-id>` heading, each with every block up to the next heading or section marker.
92
+ * Throw on the first structural error.
93
+ */
94
+ export declare function readSections(plainSource: ParsedPlainSource, document: AnyNode, moduleName?: string | null): void;
95
+ export declare function parsePlainSource(plainSourceText: string, codeVariables: Record<string, Record<string, string>>, templateDirs: string[], importedModules: string[], modulesTrace: string[], moduleName?: string): PlainFileParseResult;
69
96
  /**
70
97
  * The file a module name is read from, as `readModulePlainSource` reads it - a standard library module's in turri's
71
98
  * `stdlib/`, any other's in the first template folder that has it - or null when there is none.
@@ -73,8 +100,17 @@ export declare function parsePlainSource(plainSourceText: string, codeVariables:
73
100
  export declare function modulePlainFile(moduleName: string, templateDirs: string[]): string | null;
74
101
  export declare function readModulePlainSource(moduleName: string, templateDirs: string[]): string;
75
102
  export declare function parsePlainFile(moduleName: string, codeVariables: Record<string, Record<string, string>>, templateDirs: string[], importedModules: string[], modulesTrace: string[]): PlainFileParseResult;
76
- export declare function processRequiredModules(requiredModules: string[], codeVariables: Record<string, Record<string, string>>, templateDirs: string[], allRequiredModules: string[], modulesTrace: string[]): AnyNode[];
77
- /** The definitions a required module exports: those of its `exported_concepts`, with the definitions they use. */
103
+ /**
104
+ * Parse every required module, deepest first, without validating it - each with code variables of its own - and
105
+ * return the definitions the directly required modules export. A module listed again right after it was walked is
106
+ * not parsed again, and what it exports is still visible to this module. `exports` records what each module walked
107
+ * exports, so a repeat can find it.
108
+ */
109
+ export declare function processRequiredModules(requiredModules: string[], templateDirs: string[], allRequiredModules: string[], modulesTrace: string[], exports?: Map<string, AnyNode[]>): AnyNode[];
110
+ /**
111
+ * The definitions a required module exports: those of its `exported_concepts`, with the definitions they use. What
112
+ * it may export is checked as it is parsed (`parsePlainSource`).
113
+ */
78
114
  export declare function exportedDefinitionsOf(result: PlainFileParseResult): AnyNode[];
79
115
  export declare function processExportedDefinitions(plainSource: ParsedPlainSource, exportedDefinitions: AnyNode[]): void;
80
116
  /**
@@ -89,8 +125,27 @@ export interface ParseOptions {
89
125
  * product interview has so far given it definitions and reqs.
90
126
  */
91
127
  allowNoFunctionalities?: boolean;
128
+ /**
129
+ * The project's test folder: the module's chain is parsed with its test plain files, `<testDir>/<module>` for each
130
+ * module of the chain - turri hands the reference one test folder per module - and their scenarios are bound to
131
+ * the functional specs they name. turri's: the reference takes the test folders themselves.
132
+ */
133
+ testDir?: string;
134
+ /**
135
+ * With `testDir`: every functional spec of the chain must have a test plain file, which may hold no scenario
136
+ * (`MissingTestPlainFileError` for the first one that has none) - as the reference asks when the project names a
137
+ * conformance testing script. turri never asks: a functional spec with no test plain file is not tested, as one
138
+ * whose file is empty, and no file is the way to say so.
139
+ */
140
+ requireTestPlainFiles?: boolean;
92
141
  }
93
142
  export declare function plainFileParser(plainSourceFileName: string, templateDirs: string[], options?: ParseOptions): [string, MarshalledPlainSource, string[]];
143
+ /**
144
+ * Port of plain_file.parse_module_chain: a module and every module in its `requires` chain, each as its name and its
145
+ * tree - every required module first, deepest ancestors first, then the module itself. A module reached through more
146
+ * than one `requires` path appears once, at its first (deepest) position.
147
+ */
148
+ export declare function parseModuleChain(plainSourceFileName: string, templateDirs: string[], options?: ParseOptions): Array<[string, MarshalledPlainSource]>;
94
149
  /**
95
150
  * One parse per file for the length of `run`, however many times it is asked for.
96
151
  *