@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
package/docs/hosting.md CHANGED
@@ -12,17 +12,19 @@
12
12
  | `MAX_CHATS`, `ChatInfo`, `ChatStatus` | the most chats a session has open - nine - and what the host is told of each. |
13
13
  | `pi` | [pi](https://pi.dev) itself, as turri is built against it. Make the host's own extensions and UI with this one, not another copy. |
14
14
  | `readProjectConfig`, `projectModels`, `modelSpec`, `configForSpec`, `hasProjectConfig`, `stacksOf` | the project's settings, as [config.turri.yaml](config.md) has them. |
15
- | `configSettings`, `currentValue`, `readConfigText`, `overriddenBy`, `EFFECT_TEXT` | every setting of config.turri.yaml as [/turri-config](config.md#changing-it-from-the-chat) knows it - its key, what it does, its kind (a yes or no, one of a few words, a number, a model, a list, a path, a budget), its words or the values offered first, what it is unset and when a change takes effect - and what the file has now (`currentValue(readConfigText(dir), key)`), and what wins over it. Change one as the person would, by sending `/turri-config <key> <value>` to the chat: turri checks it and writes it with the file's comments kept. |
15
+ | `configSettings`, `currentValue`, `readConfigText`, `overriddenBy`, `EFFECT_TEXT` | every setting of config.turri.yaml as [/turri-config](config.md#changing-it-from-the-chat) knows it - its key, what it does, its kind (a yes or no, one of a few words, a number, a model, a list, a path, a budget), its words or the values offered first, what it is unset and when a change takes effect - and what the file has now (`currentValue(readConfigText(dir), key)`), and what wins over it. Change one as the person would, by sending `/turri-config <key> <value>` to the chat: turri checks it and writes it with the file's comments kept. A setting's values for a list are the session's `settingChoices(key, typed?)`: what /turri-config's completions offer after `typed`, in their order, but whole - uncapped, where the terminal shows forty - and without unset, which the host draws itself; the values alone. A model setting's are every model of every provider signed in to, from the model registry the chats use, whether or not one has run /turri-config - and after a colon, that model's thinking levels. An unknown key gives `[]`. |
16
16
  | `debugMode` | whether turri keeps, in a project, what it writes only for debugging it - `debug:`, or `TURRI_DEBUG` over it (see [debug mode](config.md#debug-mode)). |
17
17
  | `initConfig(dir, { vendor, bedrock, models, force, catalogue })`, `VENDORS`, `BEDROCK_REGIONS`, `BEDROCK_DEFAULT` | a project's first config.turri.yaml, as `turri init-config` writes it - async, it may read [pi](https://pi.dev)'s catalogue - see [a project's first config](#a-projects-first-config). |
18
18
  | `piModelCatalogue`, `bedrockMakers`, `bedrockModelsIn`, `bedrockDefaultModels`, `bedrockModels`, `bedrockPlaceOf`, `makerLabel`, `BEDROCK_GEOGRAPHIES`, `BEDROCK_IN_REGION` | [pi](https://pi.dev)'s model catalogue, read without a request, and Bedrock in it: whose models it serves, in which geographies, which two turri offers for a maker and geography. |
19
- | `bedrockAwsRegion`, `bedrockNeedsAwsRegion` | the AWS region to set `AWS_REGION` to for a Bedrock geography, and whether it has to be set. |
20
- | `checkPlainFile`, `plainFileParser`, `resolveTemplateDirs`, `iterateFunctionalSpecs`, `FUNCTIONAL_REQUIREMENTS` | \*\*\*plain checked and parsed, its functional specs one by one - as `turri validate` reads it. |
21
- | `highlightLines`, `roleOf` | \*\*\*plain's lines coloured by turri's own [\*\*\*plain](https://plainlang.org/docs/) grammar. |
19
+ | `bedrockAwsRegion`, `bedrockNeedsAwsRegion` | the AWS region turri calls a Bedrock geography's models in, and whether that geography needs a region of its own - every one but `global`, `us` and `in-region`, which [pi](https://pi.dev)'s default `us-east-1` serves. |
20
+ | `bedrockRegionFor(projectDir)`, `BedrockRegion`, `BedrockRegionSource` | the AWS region the project's Bedrock models are called in, read without setting anything: `{ region?, source, models, needs?, conflict? }` - `source` `set` (the person's `AWS_REGION` or `AWS_DEFAULT_REGION`, never touched), `turri` (turri sets `AWS_REGION` as the session starts, the models needing it and none set), `profile` (the `AWS_PROFILE`'s region, in the models' geography) or `default` (nothing names one: `us-east-1`); `models` the Bedrock models the config names, each stack's too; `needs` the region and geography they need, when one; `conflict`, the models needing two regions, each with its models - then no region is set, one `AWS_REGION` serving every model. `region` is undefined with no Bedrock model, or with a conflict and nothing set. Undefined on an older turri. |
21
+ | `checkPlainFile`, `plainFileParser`, `resolveTemplateDirs`, `iterateFunctionalSpecs`, `FUNCTIONAL_REQUIREMENTS` | \*\*\*plain checked and parsed, with its test plain files, its functional specs one by one - `{ funcId, specifications }`, the specifications with the functional spec's conformance and acceptance scenarios, each `{ id, text }` - as `turri parse` reads it. |
22
+ | `highlightLines`, `roleOf` | \*\*\*plain's lines coloured by turri's own [\*\*\*plain](https://plainlang.org/docs/) grammar, each piece by its `PlainRole`: `section`, `concept`, `link`, `literal`, `keyword`, `module`, `comment`, `bullet` and `id` - the `# <id>` heading of a functional spec or a scenario, and a test plain file's `func_id:` value, the functional spec it binds to. A test plain file's `module:` value is a `module`, whole - `my-app`, `billing.v2` - as a module name listed under `import:` or `requires:` is. |
22
23
  | `plainGraph(projectDir)` | a project's \*\*\*plain as a graph to move around in: its modules with what each imports and requires and what imports and requires it, each item of their sections with its line, and the concepts - an item linked to those it names and to where each is defined - see [\*\*\*plain for a host](#plain-for-a-host). |
24
+ | `testDirOf(plainFile)`, `DEFAULT_TEST_DIR` | the absolute folder a module's test plain files are in - `test-dir`, its stack's over the project's, else `tests` (`DEFAULT_TEST_DIR`), from the project's folder - for a host that lists a project's modules by walking its .plain files and leaves the test plain files out. It is a module's, not a project's, because a stack's `config.turri.<stack>.yaml` may name a folder of its own. |
23
25
  | `conceptsOf(plainFile)`, `findConcepts(text)`, `conceptBeingTyped(text)` | the concepts a module can name, with what each is and the module that defines it; the concepts written in a piece of text; the one being typed at its end. |
24
26
  | `stdlibTemplates()`, `isStack`, `STDLIB_KINDS` | turri's standard library: every template with its kind, file, text, imports and its conformance template or the stack it tests, and the kinds in order with their titles. |
25
- | `functionalityCompletion`, `createWorkLoop`, `createJobRegistry`, `promptOf`, `describeModules` | how far each functional spec is built, and - on a loop of the host's own with `dryRun` - the work a \*\*\*plain change would plan, as `turri glimpse` does. |
27
+ | `functionalityCompletion`, `createWorkLoop`, `createJobRegistry`, `promptOf`, `describeModules` | how far each functional spec is built - each module's `specified`, `withScenarios` (those tested, which tested-of-specified counts over), `completed`, `conformanceTested`, `tested` and `incomplete` - and - on a loop of the host's own with `dryRun` - the work a \*\*\*plain change would plan, as `turri glimpse` does. |
26
28
  | `onModelEvent`, `describeTurnIncident` | a provider failing and waited out, a turn turri ended itself. |
27
29
  | `onLedgerEvent`, `readLedger`, `ledgerDir`, `runViews`, `pulseView`, `describePulseView`, `describeRuns`, `ledgerProjects`, `earlierPulses`, `asJob` | what turri did, as facts - the [ledger](using.md#the-ledger): each event heard as it is recorded, and every run and pulse read back, earlier sessions' too - see [the ledger in a host](#the-ledger-in-a-host). |
28
30
  | `doctor({ dir, models, env })`, `Check`, `Level` | whether the machine - and, with `dir`, that project - is ready for turri, as `turri doctor` checks it: a list of checks to show in the host's own way - see [doctor in a host](#doctor-in-a-host). |
@@ -49,7 +51,7 @@ const { written, message } = await initConfig(project, { models: { harness, codi
49
51
  - `models: { harness, coding, strong? }` writes those two instead - any pair [pi](https://pi.dev)'s catalogue has as chat models, full model ids without a thinking level (`amazon-bedrock/global.xai.grok-4.7`). They go wherever the vendor's own would: the two models with the thinking turri writes, the commented-out `judge-model` and `models:` examples, `strong` - the judging examples' model - being `harness` unless given. The vendor, the Bedrock maker and the geography are read from them, so neither `vendor` nor `bedrock` is needed. A model the catalogue does not have, or that is not a chat model, writes nothing: `written` is false and `message` says which, with the closest the catalogue has.
50
52
  - `catalogue`, the one the host already read, saves reading it again.
51
53
 
52
- The message says whose models were written - "with xAI's models on Amazon Bedrock (global)" - and how to sign in, with the `AWS_REGION` for the geography: `bedrockAwsRegion(geography)` is that region, and `bedrockNeedsAwsRegion(geography)` whether it has to be set - every geography but `global`, `us` and `in-region`, [pi](https://pi.dev) calling Bedrock in `us-east-1` unless `AWS_REGION` or the profile names another.
54
+ The message says whose models were written - "with xAI's models on Amazon Bedrock (global)" - and how to sign in: an `AWS_PROFILE` or a Bedrock API key, never a region. For a geography that needs one, it says which turri calls it in - `turri calls eu's models in eu-central-1. AWS_REGION, set, overrides it - and so does your AWS profile's region, when it is in eu.` - `bedrockAwsRegion(geography)` being that region, and `bedrockNeedsAwsRegion(geography)` whether the geography needs one. turri sets it itself: `createTurri` - and `turri` in a terminal, and every command that calls a model - sets `AWS_REGION` for the project's Bedrock models before any is called, when they need a region and neither `AWS_REGION` nor `AWS_DEFAULT_REGION` is set, and works it out again when the config's models change through `/turri-config` and as each pulse takes its model. A host that shows the person where their models run reads `bedrockRegionFor(projectDir)` - "turri calls these models in eu-central-1" - rather than asking them to set anything.
53
55
 
54
56
  ## doctor in a host
55
57
 
@@ -101,6 +103,9 @@ await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, e
101
103
  await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
102
104
  turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown, role? }]
103
105
  turri.notices(); // the notices open now
106
+ turri.settingChoices("harness-model"); // every value /turri-config offers for a setting, whole: every model signed in to
107
+ turri.settingChoices("harness-model", "openai/gpt-6-astra:"); // after a colon, that model's thinking levels
108
+ turri.turnOnConformanceTests("app.plain"); // conformance tests on: the file written - config.turri.yaml, or the stack's
104
109
  await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }, { ignore: true }: "done", "dismissed", "ignored", "refused", "over", "missing"
105
110
  turri.onChats((chats) => {}); // a chat opened, closed, renamed, working or done
106
111
  await turri.closeChat(chat.id); // its turn stopped, its conversation deleted - its pulses run on
@@ -113,12 +118,20 @@ await turri.dispose(); // every chat let go, what activate start
113
118
  - **Interrupting.** `interrupt(id)` is the terminal's ctrl+c for a host's key or button: it stops what that chat is doing - its turn, a skill or a tool in it, a `!` command, a compaction - and nothing in the other chats (`stopped`). A skill stopped keeps its run, to be continued. A question of the chat's open in the host's dialogs when its turn is stopped is over - declined - and its dialog's `signal` aborted, for the host to close it. A chat doing nothing ends the session, every chat with it, as `dispose` does (`ended`): turri never exits the host's process, so the host closes what showed it. An interrupt straight after one that stopped something is `ignored`, so pressing twice to be sure ends nothing. `session.abort()` is the chat's turn alone, and never ends the session.
114
119
  - **Declining a question.** `decline(id)` is ctrl+c at a question in turri's terminal: the question the chat asks now - its ask_user, a skill's interview, a review gate's escalation, whatever goes through the host's dialogs - declined. The asker is told "The user declined to answer the question." and the turn goes on; a review gate declined stands. It declines each question it is called at, however many in a row, and never stops the turn - that is `interrupt`. `none` when the chat asks nothing now, `missing` for a chat not open. A dialog that resolves with `undefined` is something else - dismissed, and in a question of several answers "done" with the boxes ticked so far.
115
120
  - **The record.** The chats are recorded in `.turri/chats.json` as the terminal records them, and `restore: true` reopens the last ended session's - without it, a host starts with none and deletes nothing. A chat closed is deleted; a session's chats are kept until five newer sessions have recorded theirs.
116
- - **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'. A job's \*\*\*plain diff is `job.diff`, and `job.diffWhere` places its lines in the `.plain` files: by a line's place in the diff, its `file`, its `line` there, the `section` it is in and `written` - the line as the file has it. The diff is \*\*\*plain as read, a link `[text](path)` in it as `` `[resource]text` ``, so a host that finds the lines again in a file's text - after an edit since the pulse started - matches them by `written`, never by the diff's text. A `job.diff` in a ledger written before `written` was added has none. `job.cases` is the functional specs the pulse covers, as planned, by what it does to each - `{ add, patch, again, delete }`, each a list of `{ module, frid }`, `module` the pulse's own or one below it: new, changed, regenerated again with its text unchanged, taken out. A host lights those specs from it, and marks a line of the diff as regenerated again from `again` - never from the diff, where such a line is unchanged. A pulse of none is mechanical - reqs, or definitions none uses; a job that is not a pulse has no `cases`. The ledger's `job.queued` carries them too.
121
+ - **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'. A job's \*\*\*plain diff is `job.diff`, and `job.diffWhere` places its lines in the `.plain` files: by a line's place in the diff, its `file`, its `line` there, the `section` it is in and `written` - the line as the file has it. The diff is \*\*\*plain as read, a link `[text](path)` in it as `` `[resource]text` ``, so a host that finds the lines again in a file's text - after an edit since the pulse started - matches them by `written`, never by the diff's text. A `job.diff` in a ledger written before `written` was added has none. `job.cases` is the functional specs the pulse covers, as planned, by what it does to each - `{ add, patch, again, delete }`, each a list of `{ module, funcId }`, `module` the pulse's own or one below it: new, changed, regenerated again with its text unchanged, taken out. A host lights those specs from it, and marks a line of the diff as regenerated again from `again` - never from the diff, where such a line is unchanged. A pulse of none is mechanical - reqs, or definitions none uses; a job that is not a pulse has no `cases`. The ledger's `job.queued` carries them too. `job.building` is the one functional spec a job is on now, when it is on one: `{ spec: { module, funcId }, label, text, since }` - `spec` the functional spec as data, `label` and `text` what the terminal shows (`core:add-task, functional spec of 3`, its text with its scenarios). Only a conformance run of one named functional spec sets it; a pulse the work loop plans covers its functional specs together and leaves it unset - light those from `cases`.
117
122
  - **The questions.** `dialogs(chat)` gives a chat's two dialogs: `select(title, options, context, at, { signal })` and `input(title, context, { signal })`, each resolving with the answer, or undefined when the person dismissed it. `signal` is aborted when the question is over without the dialog's answer - declined by `decline`, or its turn stopped (`interrupt`, `session.abort()`): the host closes the dialog it opened for it, whatever it resolves with then being ignored. A host may leave it unread. The chat's questions, a skill's interview and a review gate's escalation are put in them; without them, in the `uiContext`'s own dialogs.
118
123
  - **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: with `sentinel: false`, after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released. With the sentinel on, the sentinel tests the release, and the conversation only says it landed.
119
124
  - **The system prompt.** `systemPrompt(prompt)` gets the whole system prompt for the turn - [pi](https://pi.dev)'s, turri's identity and blocks, the rules - after turri's own `before_agent_start` work, and returns what is sent. It is for what `surface` does not cover; to describe the UI, use `surface`, rather than words appended after turri's that contradict them.
120
125
  - **The mode.** Each chat is bound in [pi](https://pi.dev)'s `tui` mode: the work runs in the background and keeps the chat free, as the terminal does. Its other UI calls - its notices, its working line, a confirm - come to `uiContext(chat)`, as [pi](https://pi.dev)'s [SDK](https://github.com/earendil-works/pi) describes it.
121
126
 
127
+ ### The test advisor
128
+
129
+ Conformance tests, and the test advisor with them, are on only with `conformance-tests: true` ([config](config.md#conformance-tests)) - off when unset: then no conformance pulse runs, `evaluateTests` throws saying how they are turned on, and the chats have `turn_on_conformance_tests` in place of `conform` and `evaluate_tests`; `conformance-tests` is one of `configSettings()`, and `settingChoices("conformance-tests")` gives `true` and `false`. To turn them on from the host's own button, the session's `turnOnConformanceTests(plainFile?)` does what the chat's `turn_on_conformance_tests` does, with no model call: it writes `conformance-tests: true` where the module's value comes from - its stack's `config.turri.<stack>.yaml` when that sets the key, which `/turri-config` alone would not change, else `config.turri.yaml` - switches every open chat's tools (`conform` and `evaluate_tests` in, `turn_on_conformance_tests` out), the test advisor starting at its next look, and returns the file it wrote, from the project. `plainFile`, absolute or from the project, picks the module; omitted, the project's. It starts no conformance pulse: the host offers that itself, with `conform`. A host's session has the [test advisor](test-advisor.md), as the terminal's chat does: new and changed functional specs are checked in the background for whether they need testing scenarios. Its verdict goes to the chat the person looks at, once that chat is idle - its agent done: a yes as a hidden message to its agent, `customType: "turri_advisor_verdict"`, that starts a turn in which the chat tells the person and writes the scenarios; a no as an entry of its session, `turri_advisor` (`data.text`, the line), that starts none. A host's chats have the `evaluate_tests` tool too, whatever `test-advisor` says, and a host has its own way in: `turri.evaluateTests(funcIds, { plainFile?, reason? })` - a button on a functional spec, say - asks the same, `module:func-id`s or func-ids of `plainFile`'s module, and returns at once with `{ started, already, scenarios, missing }`, each verdict coming to the chat the person looks at as any does. What it made of each functional spec is `readAdvisor(projectDir, module)`: `{ module, plainFile, entries }`, an entry by func-id - `state` `pending` (being evaluated), `yes`, `no` or `failed`, its `report` (the questions that were a yes, with their evidence, and the inputs to cover), `undelivered` for a yes no chat has had yet - and current while its `hash` is `textHash` of the functional spec's text - `plainGraph`'s `item.text` for it: its body without the `# <func-id>` heading, its links as the parser reads them, exactly the text the advisor hashes; a functional spec with no entry, or a stale one, has not been looked at as it reads. Its facts are ledger events - `advisor.classified`, `advisor.dropped`, `advisor.failed` - heard through `onLedgerEvent`. `test-advisor` is one of `configSettings()`.
130
+
131
+ A host's chats run the [background skills](skills-and-gates.md) in the background, as the terminal's do - the two that write the testing scripts: `cultivate` returns at once, and nothing of the run shows while it runs. Its result's `details` say what it started: `{ background: true, kind?, plainFile? }` - `kind` the testing script it writes, `unit` or `conformance`, and `plainFile` the module it is for, relative to the project, each left out when there is none; `started: false` too when the skill was running already and nothing new was started - enough to show "writing the unit testing script for hello.plain, in the background" with no table of which skill writes which. Its end goes to the chat that started it, once that chat is idle: a run that did its work as an entry of its session, `turri_skill_run` (`data.text`, the line - `Unit testing script written: test_scripts/run_unittests_python.sh.`), that starts no turn; one that did not as a hidden message to its agent, `customType: "turri_skill_run"` (`details.skills`), that starts a turn in which the chat tells the person what went wrong, its `details` `{ skills, runs }` - `runs` one `{ skill, kind?, plainFile? }` for each run in it, for a turn marked "unit testing script not written · hello.plain". Meanwhile a module that lacks the script waits for it, its `why` in `mapPlan()` and `status()` `waiting for its unit testing script, which is being written` - or its conformance testing script - and starts by itself once it is written. `onPlanChange` fires as it starts waiting and as it stops - the script written, or the run ended without it - as it does whenever a module's `why` changes.
132
+
133
+ A codebase that started from a [base folder](regenerating.md#the-base-folder) says so in its module's status: `loop.status()` gives `base: { folder, files }` - the folder as `base-folder:` named it, and how many of the files that came from it the codebase has still - for "started from `brand` · 12 files" in a module's head. A module built on such a codebase has the same, carried up with the code; one that started from none, or has no code yet, has no `base`. It is read again only when a pulse lands, so a host may read it on every `onPlanChange`. The record behind it, `code/.turri-base.json`, is turri's own: a host reads `base`, never the file.
134
+
122
135
  ### The sentinel
123
136
 
124
137
  The [sentinel](sentinel.md) is one of the host's chats: turri opens it with the first pulse that finishes, through the host's own `create` like any chat - `onChats` hears of it, `role: "sentinel"` - and never moves `focus` to it. It closes like any chat, and the next pulse that finishes opens it again; with it on, `openChat` opens up to eight.
@@ -151,21 +164,24 @@ the chat, and the toolbar's buttons send them too.
151
164
  ```js
152
165
  import { conceptBeingTyped, conceptsOf, findConcepts, plainGraph, stdlibTemplates, STDLIB_KINDS } from "@turri-dev/cli/host";
153
166
 
154
- const { modules, items, concepts } = plainGraph(projectDir);
167
+ const { modules, items, concepts, testPlainFiles } = plainGraph(projectDir);
155
168
  const app = modules.find((module) => module.module === "app");
156
169
  app.requires; // [{ name: "base", file: "/…/base.plain" }] - and imports, importedBy, requiredBy, the same way
157
170
  app.items; // ["/…/app.plain:11", "/…/app.plain:15"] - its own items, in order
158
171
  items.find((item) => item.id === app.items[1]);
159
- // { id, file, module: "app", section: "functional specs", line: 15, frid: "1", text: "- :User: can fill :Cart:.",
172
+ // { id, file, module: "app", section: "functional specs", line: 15, funcId: "fill-cart", text: ":User: can fill :Cart:.",
160
173
  // defines: [], uses: [{ concept: ":User:", definition: "/…/base.plain:8" }, { concept: ":Cart:", definition: "/…/app.plain:11" }],
161
- // resources: [{ text: "the schema", target: "schema.json", file: "/…/schema.json", line: 15 }] }
174
+ // resources: [{ text: "the schema", target: "schema.json", file: "/…/schema.json", line: 15 }],
175
+ // testPlainFile: "/…/tests/app/fill-cart.plain", scenarios: { conformance: [{ id: "adds-an-item", text: "…", line: 8 }], acceptance: [] } }
176
+ testPlainFiles[0];
177
+ // { file: "/…/tests/app/fill-cart.plain", module: "app", funcId: "fill-cart", item: "/…/app.plain:15", conformance: [...], acceptance: [...] }
162
178
  concepts.find((concept) => concept.definition === "/…/base.plain:8");
163
179
  // { concept: ":User:", definition: "/…/base.plain:8", module: "base", text: "is a person who signs up.", usedBy: [...] }
164
180
  ```
165
181
 
166
- - **The graph.** `plainGraph` reads every .plain file in the project's folder and its `template/` folder, and every module they import or require, wherever the parser finds it - the standard library's templates too. A **module** is a file: its imports, resolved as the parser reads an import (a `stdlib/` module from turri's own folder), its required modules, resolved as the requires chain finds them - `file: null` when one is not found - and the reverse of both, `importedBy` and `requiredBy`, so a host goes up and down the requires chain without inverting anything. An **item** is one item of a section - a definition, an implementation req, a test req, a functional spec with its acceptance tests - with its file, the line it starts on, its text, a functional spec's `frid`, the concepts a definition defines, and each concept it names with the item that defines it: its own module's definitions, its imports', and those its required modules export, as the parser sees them from that module. `definition` is null for \*\*\*plain's own concepts, and for one defined nowhere the module can see. Its `resources` are the linked resources it names - `[text](target)`, a functional spec's acceptance tests' too, as the parser reads them - each with `file`, the file regenerating reads it from (the template folders first, then the current folder), null when it is not there, and `line`, the line the link is on. `resources` is on every item, `[]` when it names none: a turri that gives no resources has no field. A **concept** is one definition of it - or \*\*\*plain's own, `definition: null` - with every item that names it there. An item's id is `<file>:<line>`: the file to open and the line to go to.
182
+ - **The graph.** `plainGraph` reads every .plain file in the project's folder and its `template/` folder, and every module they import or require, wherever the parser finds it - the standard library's templates too. A **module** is a file: its imports, resolved as the parser reads an import (a `stdlib/` module from turri's own folder), its required modules, resolved as the requires chain finds them - `file: null` when one is not found - and the reverse of both, `importedBy` and `requiredBy`, so a host goes up and down the requires chain without inverting anything. An **item** is one item of a section - a definition, an implementation req, a test req, a functional spec - with its file, the line it starts on (a functional spec's, its `# <func-id>` heading's), its text (a functional spec's, its body), a functional spec's `funcId`, its `testPlainFile` - null when it has none - and its `scenarios`, `{ conformance, acceptance }`, each `{ id, text, line }`, the concepts a definition defines, and each concept it names with the item that defines it: its own module's definitions, its imports', and those its required modules export, as the parser sees them from that module. `definition` is null for \*\*\*plain's own concepts, and for one defined nowhere the module can see. Its `resources` are the linked resources it names - `[text](target)`, as the parser reads them - each with `file`, the file regenerating reads it from (the template folders first, then the current folder), null when it is not there, and `line`, the line the link is on. `resources` is on every item, `[]` when it names none: a turri that gives no resources has no field. A **concept** is one definition of it - or \*\*\*plain's own, `definition: null` - with every item that names it there. An item's id is `<file>:<line>`: the file to open and the line to go to. A **test plain file** - each `.plain` file under the project's test folder (`test-dir`, `tests` by default) - is a node of its own: its `module` and `funcId` as its front matter binds it, `item`, the id of the functional spec it binds to, and its `conformance` and `acceptance` scenarios, each with the line its `# <scenario-id>` heading is on. One that does not parse, is outside its module's folder, or binds to no functional spec has `error`, and `item` null.
167
183
  - **What does not parse.** Nothing throws. A file that does not parse has `error`, the parser's message; its links are what its front matter says, and its items what of its own text reads without its imports. A file whose items Liquid moves keeps the lines of its own text.
168
- - **Concepts.** `conceptsOf(plainFile)` is what one module can name - `{ concept, definition, module }`, `definition` the text after its concepts, `module` null for \*\*\*plain's own - for suggestions as the person types. `findConcepts(text)` is every concept written in a piece of text, `{ concept, start, end }`, and `conceptBeingTyped(text)` the one at its end still open - `{ typed: ":Us", start }`, or undefined - both with the parser's own pattern.
184
+ - **Concepts.** `conceptsOf(plainFile)` is what one module can name - `{ concept, definition, module }`, `definition` the text after its concepts, `module` null for \*\*\*plain's own - for suggestions as the person types. Inside a test plain file, call it with the module's file the test plain file binds to: its scenarios name that module's concepts. `findConcepts(text)` is every concept written in a piece of text, `{ concept, start, end }`, and `conceptBeingTyped(text)` the one at its end still open - `{ typed: ":Us", start }`, or undefined - both with the parser's own pattern.
169
185
  - **The standard library.** `stdlibTemplates()` is every template in turri's `stdlib/`, sorted: `name` - what a module imports it as - `file`, `kind` (`backend`, `frontend`, `cli`, `library` or `lang`), `description`, `text`, `imports` as the parser reads its front matter, a stack's `conformance` template when it has one, and a conformance template's stack as `tests`. `isStack(template)` says whether it is a stack to import; `STDLIB_KINDS` is the kinds in order, each with its title.
170
186
 
171
187
  ## The ledger in a host
@@ -56,6 +56,7 @@ In a project - a folder with `config.turri.yaml` (or the old `config.yaml`) or `
56
56
  | --- | --- |
57
57
  | each module's toolchains | what its implementation reqs name - Python, Node, Java, Go, Rust, C/C++, Zig, ... - against this machine's PATH - and Java against a JDK, for macOS keeps a `/usr/bin/java` that only asks to install one: the agents use this machine's toolchains and install none, so a missing one is yours to install |
58
58
  | the testing scripts | what in them this machine does not have: a program named by its whole path that is not here (a macOS-only `/usr/libexec/java_home` on Linux), a toolchain they run that is not on the PATH |
59
+ | the AWS region | for a project whose models are on Amazon Bedrock: the region they are called in, and where it comes from - turri's, set for the geography they need; the person's `AWS_REGION`; the profile's - and wrong when the person's is outside that geography, or the models need two regions, which one `AWS_REGION` cannot serve |
59
60
  | the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through [pi](https://pi.dev), with the credentials turri's agents will use, for any provider [pi](https://pi.dev) supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials [pi](https://pi.dev) reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids [pi](https://pi.dev) knows for one it does not. `--harness-model`, `--coding-model` (`--model`) and `--judge-model` name models as the config's keys, as `turri regenerate` reads them - checked in a project or not. `--no-models` skips it |
60
61
 
61
62
  Removing that file is the one thing `doctor` changes, and it asks first; `turri doctor --yes` says yes. Run without a terminal - piped, or in CI - it asks nothing and changes nothing. Run it again whenever something breaks; `turri doctor --no-models` leaves out the models' requests. In the chat, ask for a self check: the `doctor` tool runs the same checks, the models only when you ask. A host - a desktop app - runs the same checks through `doctor` and shows them itself (see [doctor in a host](hosting.md#doctor-in-a-host)).
package/docs/parser.md CHANGED
@@ -1,37 +1,94 @@
1
1
  # The \*\*\*plain parser
2
2
 
3
- `src/plain/` is a TypeScript port of the reference [\*\*\*plain](https://plainlang.org/docs/) parser - `plain_file.py`, `plain_spec.py`, `concept_utils.py`, and the `file_utils.py` helpers they depend on.
3
+ `src/plain/` is a TypeScript port of the reference [\*\*\*plain](https://plainlang.org/docs/) parser, [plain-parser](https://github.com/plainlang/plain-parser) - `plain_file.py`, `plain_spec.py`, `concept_utils.py`, `graph.py`, `test_plain_file.py`, `project.py`, and the `loaders.py` helpers they depend on.
4
+
5
+ ## Which commit
6
+
7
+ The port follows plain-parser [#15](https://github.com/plainlang/plain-parser/pull/15) ("explicit test scenarios") at **`e0c5f31`** (2026-10-27), its head when the port was last checked; #15 is not merged yet. #15 branches from `063c8e0` (2026-10-13), and the port was brought there first, from `bdfd1af` (2026-09-20), as a step of its own:
8
+
9
+ - **`bdfd1af`..`063c8e0`:** lookup errors that say only what was not found (`692eaac`), exported concepts checked as each module is parsed (`0abc70d`), the concept graph sorted the way graphlib sorts it and a cycle report capped at `MAX_REPORTED_CYCLES` (`8741e03`, `6934220`), links resolved against the plain file's own folder (`0a1f044`), corrected error spacing and wording (`a275916`), Liquid syntax errors as \*\*\*plain syntax errors (`5d91970`), and the `requires` chain's exports and order (`269be5a`, `f59f51a`, `6917023`, `2cf5298`). The `check` and `parse` command line of `5b3b20c`..`063c8e0` has no counterpart: turri's is `turri parse`.
10
+ - **#15, `37e0b35`..`f1ac046`:** a functional spec is a `# <func-id>` heading with a free-text body; func-ids and scenario-ids are semantic ids (`SEMANTIC_ID_PATTERN`); `- ` bullets and `***acceptance tests***` in a module are the previous syntax, and are errors; conformance and acceptance scenarios live in test plain files, bound to a functional spec by their `module:` and `func_id:`; and with `require_test_plain_files` every functional spec of the chain needs a test plain file, which may hold no scenario (`f1ac046`).
11
+ - **#15, `e0c5f31`:** the parser has no default test directory - test plain files are read only from the directories it is given. Its command line's side of it, a usage error for a `conformance-tests-script` with no `test-dir`, has no counterpart in turri, whose `test-dir` is `tests` unless the config says otherwise (see [the config](config.md)); the parser here, like #15's, reads test plain files only when `ParseOptions.testDir` is given. Its fixture `cli/project/config.yaml` gains `test-dir: tests`.
12
+
13
+ ### Comparison with the Python parser
14
+
15
+ <!-- COMPARISON RESULTS: to be filled in by a run of both parsers outside this repo, at e0c5f31 (and at 063c8e0 for the catch-up), over tests/data - #15's fixtures with testplainfile/ and cli/project/, and partial_rendering/ - the six body cases in tests/plain/renderer.test.ts, and the stdlib templates. -->
16
+
17
+ _Not yet run at these commits._ The comparison is run outside this repository - both parsers over the same corpus, their module name, marshalled tree (format version 2, with `test_plain_files`), required modules, func-ids and each functional spec's \*\*\*plain compared - and its results are recorded here. Its corpus is `tests/data` (#15's fixtures, with `testplainfile/` and `cli/project/`, and turri's own `partial_rendering/`), the six body cases of `tests/plain/renderer.test.ts` - whose expected values were read from mistletoe's renderer, not taken from a run - and the `stdlib/` templates.
4
18
 
5
19
  | module | ports |
6
20
  |---|---|
7
- | `src/plain/parser.ts` | `plain_file.py` - sections, acceptance tests, imports/requires, marshalling |
8
- | `src/plain/spec.ts` | `plain_spec.py` - functionality ids, the per-functionality context, linked resources, ranges |
9
- | `src/plain/concepts.ts` | `concept_utils.py` - concept extraction, validation, topological ordering |
21
+ | `src/plain/parser.ts` | `plain_file.py` - sections and functional specs (`readSections`), imports/requires, the `requires` chain (`parseModuleChain`), marshalling |
22
+ | `src/plain/spec.ts` | `plain_spec.py` - func-ids, the per-functional-spec context with its scenarios, linked resources, ranges |
23
+ | `src/plain/test-plain-file.ts` | `test_plain_file.py` - a test plain file: its binding, its two sections and their scenarios |
24
+ | `src/plain/project.ts` | `project.py` - finding test plain files, binding them to the chain, and the completeness check |
25
+ | `src/plain/concepts.ts` | `concept_utils.py` - concept extraction, validation, topological ordering, the cycle report |
26
+ | `src/plain/cycles.ts` | `graph.py` - the cycle search (Johnson's algorithm over Tarjan's components) the cycle report is made from |
10
27
  | `src/plain/markdown.ts` | mistletoe's AST + `PlainRenderer` (mdast + a matching serializer) |
11
28
  | `src/plain/liquid.ts` | the reference implementation's Liquid nodes + the two filters in `plain_spec.py` (liquidjs) |
12
- | `src/plain/files.ts` | `file_utils.open_from` / `get_loaded_templates`, base64 blob detection |
29
+ | `src/plain/files.ts` | `loaders.open_from` / `get_loaded_templates` / `liquid_error_message` / `relative_resource_path`, base64 blob detection |
13
30
  | `src/plain/module-concepts.ts`, `src/plain/graph.ts` | nothing - turri's own, for a host: the concepts a module can name and where each is defined, and a project's \*\*\*plain as a graph of modules, items and concepts (see [hosting](hosting.md#plain-for-a-host)). The parser keeps, beside the reference's result, which module each item an import brought is written in (`importedFrom`) |
14
31
  | `src/plain/stdlib.ts` | nothing - turri's own: `stdlib/` is a reserved import prefix, read from turri's standard library only, and a project module at such a path is an error (see [the standard library](authoring-plain.md#the-standard-library)) |
15
32
 
33
+ The ported functions keep the reference's names where it keeps them: `getFrids`, `getSpecificationsForFrid`, `getFridsRange` - a frid there is a functional spec's func-id.
34
+
35
+ ## Test plain files
36
+
37
+ A functional spec's conformance and acceptance scenarios are in its **test plain file**, `tests/<module>/<func-id>.plain` by convention (the folder is turri's rule, the file's name is free):
38
+
39
+ ```
40
+ ---
41
+ module: todo
42
+ func_id: add-task
43
+ ---
44
+
45
+ ***conformance tests***
46
+
47
+ # add-first-task
48
+ Running `todo add Buy milk` on an empty list prints `Added task 1: Buy milk`.
49
+
50
+ ***acceptance tests***
51
+
52
+ # add-then-list
53
+ Running `todo add Buy milk` and then `todo list` prints `1 Buy milk`.
54
+ ```
55
+
56
+ Either section may be left out, but one that is there has at least one scenario: a section with none is a syntax error - `The ***acceptance tests*** section has no scenarios. Leave out a section with none.`, at its heading - and so is a file with no section at all, for a functional spec with no scenarios has no test plain file. Conformance comes before acceptance when both are there. This is turri's own rule: the reference parses test plain files with both sections, either empty. turri hands the reference one test folder per module of the chain - `<test-dir>/<module>` for each, `test-dir` from [the config](config.md) - so a test plain file may bind to any module of the chain (`plainFileParser`'s `testDir`). Its scenarios ride on the functional spec it binds to, in the item's `conformance_tests` and `acceptance_tests` - `{id, text, linked_resources}` each - and the tree's `test plain files` names each file, relative to the module's folder, with the functional spec it binds to.
57
+
58
+ **Completeness.** turri never asks the reference's completeness check (`require_test_plain_files`), which wants a test plain file for every functional spec of the chain whenever the project names a conformance testing script. In turri a functional spec with no test plain file is not tested - and no file is the only way to say so, an empty one being a syntax error. `parseProject` keeps the reference's switch. A module parsed without its test folder - the parser's own tests, a fixture - binds nothing and checks nothing.
59
+
16
60
  ## Checking \*\*\*plain
17
61
 
18
62
  ```bash
19
- turri validate <file.plain> # one spec
20
- turri validate . # every .plain file in a directory
21
- turri validate <file.plain> --json
63
+ turri parse <file.plain> # one module
64
+ turri parse . # every .plain module in a directory
65
+ turri parse <file.plain> --json
22
66
  ```
23
67
 
24
- It uses this repo's own parser, so it needs no external tool, no API key and no credits: it parses the file, resolves its imports and required modules, and reports every syntax, concept and structure error with the line it is on.
68
+ It uses this repo's own parser, so it needs no external tool, no API key and no credits: it parses each module with its test plain files, resolves its imports and required modules, and reports every syntax, concept and structure error with the line it is on. A module that passes lists each functional spec with its test plain file and its scenarios - and, where they have scenarios but the module has no test reqs, or no conformance testing script for its stack, a `WARN` line naming the next step ([the steps of adding tests](regenerating.md#the-steps-of-adding-tests)): a warning, never a failure:
25
69
 
26
70
  ```
27
- PASS d365_export.plain 6 functionalities (1, 2, 3, 4, 5, 6), requires d365_restore_api
71
+ PASS todo.plain 3 functional specs, requires base
72
+ add-task tests/todo/add-task.plain 2 conformance, 1 acceptance
73
+ list-tasks tests/todo/list-tasks.plain 1 conformance
74
+ delete-task tests/todo/delete-task.plain no scenarios - not tested
28
75
 
29
76
  FAIL broken.plain PlainSyntaxError
30
- Plain syntax error: Syntax error at line 3: Invalid specification heading
31
- (`Nope:`). Allowed headings: definitions, implementation reqs, test reqs, ...
77
+ Plain syntax error: Syntax error at line 3 in module 'broken': Invalid specification heading
78
+ (`Nope:`). Allowed headings: definitions, implementation reqs, test reqs, functional specs
32
79
  ```
33
80
 
34
- The exit code is 0 only when every file passed. In a conversation the same check is `validate`.
81
+ The parser's errors come one at a time, the first it meets, word for word as the reference gives them; for one the previous syntax makes - bullets as functional specs, acceptance tests in a module - turri adds its next step under it: ask it to convert the module, which it does one module at a time, asking before it writes anything. turri's own checks list everything they find at once. As failures: every test plain file that is not where it belongs - every run, of a file or a folder, checks its project's test folder - saying where it does, and a test plain file whose module or functional spec there is none of. As a `WARN`: a step of adding tests that was skipped - scenarios with no \*\*\*test reqs\*\*\* or no conformance testing script for the module that tests them. A functional spec with no scenarios is no problem: the listing shows it as `no scenarios - not tested`, with or without an empty test plain file, and none is preferred. The strays:
82
+
83
+ ```
84
+ FAIL tests/ 2 test plain files outside a module's folder
85
+ tests/add-task.plain - move it to tests/todo/ (its module: todo)
86
+ tests/todos/x.plain - no module todos; module: todo is in tests/todo/
87
+ ```
88
+
89
+ A test plain file, or a test folder, is never taken for a module: `parse` - and `regenerate`, `conform`, `glimpse`, `drift`, `release`, `/regenerate` and the commands - refuse one, naming its module (`\`tests/todo/add-task.plain\` is a test plain file of \`todo\`; name \`todo.plain\`, or \`todo:add-task\` for that one functional spec`).
90
+
91
+ The exit code is 0 only when every file passed. In a conversation the same check is `parse`; `change_plain` and `turri change-plain` run it on what a change touched - each module, an import module as the modules that import it, a test plain file as its module - and a change that does not pass is rolled back, a test plain file written outside its module's folder with it.
35
92
 
36
93
  ## Two rules of inheritance
37
94
 
@@ -40,42 +97,46 @@ The exit code is 0 only when every file passed. In a conversation the same check
40
97
 
41
98
  ## The functional-spec iterator
42
99
 
43
- `getFrids` walks functionalities depth-first through nested sections; `getSpecificationsForFrid` builds what a single functionality is regenerated with. `iterateFunctionalSpecs` pairs them:
100
+ `getFrids` walks the functional specs in file order, by func-id; `getSpecificationsForFrid` builds what a single functional spec is regenerated with. `iterateFunctionalSpecs` pairs them:
44
101
 
45
102
  ```ts
46
103
  import { plainFileParser, iterateFunctionalSpecs } from "./src/plain/index.ts";
47
104
 
48
- const [moduleName, tree] = plainFileParser("my_app.plain", ["."]);
105
+ const [moduleName, tree] = plainFileParser("my_app.plain", ["."], { testDir: "tests" });
49
106
 
50
- for (const { frid, specifications, codeVariables } of iterateFunctionalSpecs(tree)) {
51
- // definitions and reqs in scope from the outside in, every functionality up to and
52
- // including this one, and only this one's acceptance tests
53
- console.log(frid, specifications["functional specs"].length);
107
+ for (const { funcId, specifications, codeVariables } of iterateFunctionalSpecs(tree)) {
108
+ // definitions and reqs in scope, every functional spec up to and including this one,
109
+ // and only this one's conformance and acceptance scenarios
110
+ console.log(funcId, specifications["functional specs"].length, specifications.conformance_tests.length);
54
111
  }
55
112
  ```
56
113
 
57
114
  ## Parsing once
58
115
 
59
- A module's parse reads its imports and the whole of its `requires` chain, so a long chain asks for the same files over and over: bringing an 18-module chain up to its \*\*\*plain parsed for minutes. Two caches keep it to once. Inside `withParseCache`, one question about \*\*\*plain, each file is parsed once. Between calls, a parse is kept with a fingerprint of every file directly in its template folders and the current one - name, size, modification time, and a `.plain` file's text - and handed out again, as a copy, while the fingerprint is the same; \*\*\*plain that does not parse is kept as that too. What a parse returns is unchanged: the cache only decides whether to parse again.
116
+ A module's parse reads its imports and the whole of its `requires` chain, so a long chain asks for the same files over and over: bringing an 18-module chain up to its \*\*\*plain parsed for minutes. Two caches keep it to once. Inside `withParseCache`, one question about \*\*\*plain, each file is parsed once. Between calls, a parse is kept with a fingerprint of every file directly in its template folders and the current one - name, size, modification time, and a `.plain` file's text - and of every `.plain` file under the test folder, by name and text, and handed out again, as a copy, while the fingerprint is the same; \*\*\*plain that does not parse is kept as that too. A scenario written, changed or removed is seen at once. What a parse returns is unchanged: the cache only decides whether to parse again.
60
117
 
61
118
  ## Fidelity
62
119
 
63
- The port is checked against the Python implementation directly, not only against the ported assertions. Both parsers are run over a corpus and their module name, marshalled tree, required modules, functionality ids and each functionality's \*\*\*plain compared:
120
+ The port is checked against the Python implementation directly, not only against the ported assertions. Both parsers are run over a corpus and their module name, marshalled tree, required modules, func-ids and each functional spec's \*\*\*plain compared. The last comparison recorded, at `bdfd1af` (see [Which commit](#which-commit) for the ones since):
64
121
 
65
122
  | corpus | files | identical | differences |
66
123
  |---|---|---|---|
67
124
  | `tests/data/` fixtures | 84 | 80 | 4 error strings (below) |
68
125
  | `hycu-dynamics-365-modules` | 47 | 47 | none |
69
126
 
70
- The 4 differences are all the same thing: template partials (`implement.plain`, `implement_2.plain`, `header.plain`, `pr_implement.plain`) are meant to be included with arguments, not parsed alone. Both implementations reject them for the same reason - an undefined Liquid variable - but the message text comes from the template engine, and liquidjs phrases it differently from liquid2.
127
+ The 4 differences are all the same thing: template partials (`implement.plain`, `implement_2.plain`, `header.plain`, `pr_implement.plain`) are meant to be included with arguments, not parsed alone. Both implementations reject them for the same reason - an undefined Liquid variable - but the message text comes from the template engine, and liquidjs phrases it differently from liquid2. `hycu-dynamics-365-modules` is in the previous syntax and is no longer a corpus; the `stdlib/` templates are its real-world replacement.
71
128
 
72
- Real \*\*\*plain exposed three places where mdast and mistletoe disagree, each covered by `tests/plain/renderer.test.ts`:
129
+ Real \*\*\*plain exposed places where mdast and mistletoe disagree, each covered by `tests/plain/renderer.test.ts`:
73
130
 
74
131
  - **Inline decoding.** mdast resolves `\"` to `"` and forgets whether emphasis was written with `_` or `*`; mistletoe round-trips the source. Inline leaf nodes are pinned back to their original source slice at parse time.
75
- - **List geometry.** mistletoe indents an item's continuation lines by its `prepend`, measured in coordinates with the enclosing item's indentation stripped - not by the column gap between parent and child. An acceptance test rendered detached from its functionality keeps 6 and 10 spaces where the naive reading gives 4 and 8.
132
+ - **List geometry.** mistletoe indents an item's continuation lines by its `prepend`, measured in coordinates with the enclosing item's indentation stripped - not by the column gap between parent and child. A nested item keeps the indentation of the coordinates it was parsed in.
76
133
  - **Lazy continuation lines.** A paragraph's continuation lines carry leading whitespace that is not part of the text; keeping it in the raw slice indents the line twice.
134
+ - **Blank lines.** The reference parses inside its renderer's context, so every blank line is a `BlankLine` token and is rendered back. mdast has none: each block keeps the blank lines before it from the source, those around a dropped comment included. A list item that ends without a next item gives back only the last of the blank lines after it (`ListItem.read` steps back one line), so the run right after a list counts once.
135
+ - **Free-text bodies.** A fence keeps its delimiter, info string and indentation; an indented code block its four spaces; a thematic break its line as written; a setext heading its underline. A table is parsed as GFM's (`micromark-extension-gfm-table`) and re-rendered as mistletoe renders one: every column as wide as its widest cell and at least three, the separator row rebuilt from the alignments.
77
136
 
78
- Two deliberate differences from the Python source:
137
+ Deliberate differences from the Python source:
79
138
 
80
- - The `test_plainfile.py` tests drive the acceptance-test helpers with `MagicMock` tokens and patch the inner helper to count calls. ESM has no equivalent, so those tests build real ASTs from markdown and assert the same observable outcome.
81
- - `networkx`'s `topological_sort` is reimplemented rather than replaced with a generic one: it flattens `topological_generations`, so the ordering of independent concepts follows graph insertion order, and the Python tests assert that exact sequence.
139
+ - graphlib's `TopologicalSorter.static_order` is reimplemented rather than replaced with a generic sort: it hands out each generation of ready nodes in insertion order, so the ordering of independent concepts follows graph insertion order, and the Python tests assert that exact sequence.
140
+ - A Liquid syntax error is reported as `Invalid Liquid at <module>:<line>:<column>: <message>`, as the Python parser reports it, but the message is liquidjs's and so is the place: liquidjs points at the token it stopped on, liquid2 sometimes at the next one (`unterminated_output.plain` is `4:19` here, `5:0` there).
141
+ - The `requires` walk is turri's: each required module is validated by parsing it in turn, through the parse cache, rather than by `validate_required_modules`. It reports the same first error in the same order - the top module's shape, then its required modules' parse errors, its own concept errors, then its required modules top down - and `parseModuleChain` gives the chain in the same order. `test_each_required_module_is_validated_once_top_down`, which counts calls to the Python function, has no counterpart.
142
+ - mistletoe's table splits a row on every unescaped `|` and drops empty cells; GFM keeps an empty cell, and lets a table go on over a row with no `|`. Tables that differ there are not in the corpus.