@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
@@ -0,0 +1,42 @@
1
+ import { type MarshalledPlainSource } from "../plain/spec.ts";
2
+ import { type LedgerPlace } from "./ledger.ts";
3
+ export type PureRename = {
4
+ kind: "func-id";
5
+ module: string;
6
+ from: string;
7
+ to: string;
8
+ } | {
9
+ kind: "scenario-id";
10
+ module: string;
11
+ funcId: string;
12
+ scenario: "conformance" | "acceptance";
13
+ from: string;
14
+ to: string;
15
+ };
16
+ /** The renames found, and the records as they are once made - in memory: nothing is written here. */
17
+ export interface Renamed {
18
+ renames: PureRename[];
19
+ code: MarshalledPlainSource | null;
20
+ tests: MarshalledPlainSource | null;
21
+ }
22
+ /**
23
+ * The pure renames from what the code and its tests are built for to the spec now, in the codebase of `module`. `code`
24
+ * and `tests` are its records, `spec` its chain's spec with the scenarios on each functional spec.
25
+ */
26
+ export declare function pureRenames(code: MarshalledPlainSource | null, tests: MarshalledPlainSource | null, spec: MarshalledPlainSource, module: string): Renamed;
27
+ /** A rename as a person reads it. */
28
+ export declare function renameText(rename: PureRename): string;
29
+ /** A test file's text with the labels of the renames rewritten to the new names - matched on the whole key. */
30
+ export declare function relabelled(text: string, renames: PureRename[]): string;
31
+ /**
32
+ * Make the renames in a codebase, as one unit of work: the code record and the tests record rewritten to the new names,
33
+ * the tests relabelled, committed together - or, failing anywhere, rolled back whole. No agent runs, and the code is not
34
+ * touched. Returns whether it landed.
35
+ */
36
+ export declare function makeRenames(options: {
37
+ buildDir: string;
38
+ testsDir: string;
39
+ moduleName: string;
40
+ renamed: Renamed;
41
+ place?: LedgerPlace;
42
+ }): boolean;
@@ -0,0 +1,185 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS } from "../plain/spec.js";
4
+ import { reportActivity } from "./activity.js";
5
+ import { record } from "./ledger.js";
6
+ import { abandonWork, landWork, moduleDirOf, startWork } from "./lineage.js";
7
+ import { hasScenarios, marksRegenerateAgain, writeCodeRecord, writeTestsRecord } from "./records.js";
8
+ import { listFiles } from "./testing.js";
9
+ const SECTIONS = [REQUIRED_FUNCTIONAL_SPECS, FUNCTIONAL_REQUIREMENTS];
10
+ const KINDS = [
11
+ ["conformance", CONFORMANCE_TESTS],
12
+ ["acceptance", ACCEPTANCE_TESTS],
13
+ ];
14
+ /** A value as text with its keys sorted: two items alike whatever order their keys came in. */
15
+ function stable(value) {
16
+ const sorted = (entry) => {
17
+ if (Array.isArray(entry))
18
+ return entry.map(sorted);
19
+ if (entry !== null && typeof entry === "object")
20
+ return Object.fromEntries(Object.keys(entry).sort().map((key) => [key, sorted(entry[key])]));
21
+ return entry;
22
+ };
23
+ return JSON.stringify(sorted(value));
24
+ }
25
+ /** What of a functional spec the code is built from, but its func-id: its text, its links. */
26
+ function codeOf(item) {
27
+ const { id: _id, [CONFORMANCE_TESTS]: _conformance, [ACCEPTANCE_TESTS]: _acceptance, regenerate_again: _again, ...rest } = item;
28
+ return stable({ ...rest, markdown: String(rest.markdown ?? "").trim() });
29
+ }
30
+ const scenariosOf = (item, key) => item?.[key] ?? [];
31
+ /** A scenario as it reads, but its scenario-id: its text and its links. */
32
+ const readsAs = (scenario) => stable({ text: scenario.text.trim(), linked_resources: scenario.linked_resources ?? [] });
33
+ /** Every scenario of a functional spec, by kind and scenario-id, as it reads. */
34
+ function scenarioKeys(item) {
35
+ return stable(KINDS.map(([kind, key]) => scenariosOf(item, key).map((scenario) => `${kind} ${scenario.id} ${readsAs(scenario)}`).sort()));
36
+ }
37
+ const sectionOf = (tree, heading) => (tree?.[heading] ?? []).slice();
38
+ /**
39
+ * The pure renames from what the code and its tests are built for to the spec now, in the codebase of `module`. `code`
40
+ * and `tests` are its records, `spec` its chain's spec with the scenarios on each functional spec.
41
+ */
42
+ export function pureRenames(code, tests, spec, module) {
43
+ const renames = [];
44
+ let codeNow = code;
45
+ let testsNow = tests;
46
+ for (const heading of SECTIONS) {
47
+ const own = heading === FUNCTIONAL_REQUIREMENTS;
48
+ const moduleOf = (item) => (own ? module : (item.module ?? ""));
49
+ const key = (item) => `${moduleOf(item)}:${item.id ?? ""}`;
50
+ const built = sectionOf(codeNow, heading);
51
+ const now = sectionOf(spec, heading);
52
+ const tested = sectionOf(testsNow, heading);
53
+ const inSpec = new Set(now.map(key));
54
+ const inCode = new Set(built.map(key));
55
+ const fresh = now.filter((item) => !inCode.has(key(item)));
56
+ const taken = new Set();
57
+ const renamed = new Map();
58
+ for (const gone of built.filter((item) => !inSpec.has(key(item)) && !marksRegenerateAgain(item))) {
59
+ const testedItem = tested.find((item) => key(item) === key(gone));
60
+ // Its tests, when it has them, written for it as it is built: the same text, not marked to be written again.
61
+ if (testedItem !== undefined && (marksRegenerateAgain(testedItem) || codeOf(testedItem) !== codeOf(gone)))
62
+ continue;
63
+ const match = fresh.find((item) => !taken.has(item) &&
64
+ moduleOf(item) === moduleOf(gone) &&
65
+ codeOf(item) === codeOf(gone) &&
66
+ (testedItem !== undefined ? scenarioKeys(testedItem) === scenarioKeys(item) : !hasScenarios(item)));
67
+ if (match === undefined)
68
+ continue;
69
+ taken.add(match);
70
+ renamed.set(key(gone), match.id ?? "");
71
+ renames.push({ kind: "func-id", module: moduleOf(gone), from: gone.id ?? "", to: match.id ?? "" });
72
+ }
73
+ if (renamed.size > 0) {
74
+ const rename = (item) => (renamed.has(key(item)) ? { ...item, id: renamed.get(key(item)) } : item);
75
+ codeNow = { ...codeNow, [heading]: built.map(rename) };
76
+ if (testsNow !== null)
77
+ testsNow = { ...testsNow, [heading]: tested.map(rename) };
78
+ }
79
+ // Scenario-ids renamed, under a functional spec whose code and tests are caught up and nothing else of it changed.
80
+ const builtNow = new Map(sectionOf(codeNow, heading).map((item) => [key(item), item]));
81
+ const nowByKey = new Map(now.map((item) => [key(item), item]));
82
+ let changed = false;
83
+ const testedNow = sectionOf(testsNow, heading).map((item) => {
84
+ const current = nowByKey.get(key(item));
85
+ const have = builtNow.get(key(item));
86
+ if (current === undefined || have === undefined || marksRegenerateAgain(item) || marksRegenerateAgain(have))
87
+ return item;
88
+ if (codeOf(item) !== codeOf(current) || codeOf(have) !== codeOf(current))
89
+ return item;
90
+ const pairs = [];
91
+ for (const [kind, scenarioKey] of KINDS) {
92
+ const was = scenariosOf(item, scenarioKey);
93
+ const is = scenariosOf(current, scenarioKey);
94
+ const wasIds = new Set(was.map((scenario) => scenario.id));
95
+ const isIds = new Set(is.map((scenario) => scenario.id));
96
+ // A scenario kept under its id must read as it did: one changed is no rename.
97
+ for (const scenario of was) {
98
+ const kept = is.find((other) => other.id === scenario.id);
99
+ if (kept !== undefined && readsAs(kept) !== readsAs(scenario))
100
+ return item;
101
+ }
102
+ const removed = was.filter((scenario) => !isIds.has(scenario.id));
103
+ const added = is.filter((scenario) => !wasIds.has(scenario.id));
104
+ if (removed.length !== added.length)
105
+ return item;
106
+ const used = new Set();
107
+ for (const scenario of removed) {
108
+ const match = added.find((other) => !used.has(other) && readsAs(other) === readsAs(scenario));
109
+ if (match === undefined)
110
+ return item;
111
+ used.add(match);
112
+ pairs.push({ kind: "scenario-id", module: moduleOf(item), funcId: item.id ?? "", scenario: kind, from: scenario.id, to: match.id });
113
+ }
114
+ }
115
+ if (pairs.length === 0)
116
+ return item;
117
+ renames.push(...pairs);
118
+ changed = true;
119
+ return { ...item, [CONFORMANCE_TESTS]: current[CONFORMANCE_TESTS], [ACCEPTANCE_TESTS]: current[ACCEPTANCE_TESTS] };
120
+ });
121
+ if (changed && testsNow !== null)
122
+ testsNow = { ...testsNow, [heading]: testedNow };
123
+ }
124
+ return { renames, code: codeNow, tests: testsNow };
125
+ }
126
+ /** A rename as a person reads it. */
127
+ export function renameText(rename) {
128
+ return rename.kind === "func-id"
129
+ ? `${rename.module}:${rename.from} to ${rename.module}:${rename.to}`
130
+ : `${rename.scenario} scenario ${rename.from} of ${rename.module}:${rename.funcId} to ${rename.to}`;
131
+ }
132
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ /** Where a label's key ends: never inside a longer func-id or scenario-id. */
134
+ const END = "(?![a-z0-9_]|-[a-z0-9])";
135
+ /** A test file's text with the labels of the renames rewritten to the new names - matched on the whole key. */
136
+ export function relabelled(text, renames) {
137
+ let out = text;
138
+ for (const rename of renames) {
139
+ if (rename.kind === "func-id") {
140
+ const id = "[a-z0-9]+(?:-[a-z0-9]+)*";
141
+ out = out.replace(new RegExp(`\\b(conformance tests of|(?:conformance|acceptance) scenario ${id} of) ${escaped(rename.module)}:${escaped(rename.from)}${END}`, "g"), `$1 ${rename.module}:${rename.to}`);
142
+ }
143
+ else {
144
+ out = out.replace(new RegExp(`\\b${rename.scenario} scenario ${escaped(rename.from)} of ${escaped(rename.module)}:${escaped(rename.funcId)}${END}`, "g"), `${rename.scenario} scenario ${rename.to} of ${rename.module}:${rename.funcId}`);
145
+ }
146
+ }
147
+ return out;
148
+ }
149
+ /**
150
+ * Make the renames in a codebase, as one unit of work: the code record and the tests record rewritten to the new names,
151
+ * the tests relabelled, committed together - or, failing anywhere, rolled back whole. No agent runs, and the code is not
152
+ * touched. Returns whether it landed.
153
+ */
154
+ export function makeRenames(options) {
155
+ const { buildDir, testsDir, moduleName, renamed } = options;
156
+ if (renamed.renames.length === 0)
157
+ return true;
158
+ const said = renamed.renames.map(renameText).join(", ");
159
+ const unit = startWork([buildDir, testsDir], moduleName);
160
+ try {
161
+ if (renamed.code !== null)
162
+ writeCodeRecord(buildDir, renamed.code);
163
+ if (renamed.tests !== null)
164
+ writeTestsRecord(testsDir, renamed.tests);
165
+ for (const file of listFiles(testsDir)) {
166
+ const path = join(testsDir, file);
167
+ const text = readFileSync(path, "utf8");
168
+ const now = relabelled(text, renamed.renames);
169
+ if (now !== text)
170
+ writeFileSync(path, now);
171
+ }
172
+ const subject = `renamed ${said}`;
173
+ landWork(buildDir, subject);
174
+ if (moduleDirOf(buildDir) !== moduleDirOf(testsDir))
175
+ landWork(testsDir, subject);
176
+ }
177
+ catch (error) {
178
+ abandonWork(unit, moduleName, `renaming ${said} failed: ${error instanceof Error ? error.message : String(error)}`);
179
+ return false;
180
+ }
181
+ for (const rename of renamed.renames)
182
+ record({ kind: "loop.renamed", what: rename.kind, from: rename.from, to: rename.to, spec: rename.kind === "func-id" ? `${rename.module}:${rename.to}` : `${rename.module}:${rename.funcId}` }, options.place);
183
+ reportActivity(` ${moduleName}: renamed ${said} - the records and the tests' labels, nothing regenerated`);
184
+ return true;
185
+ }
@@ -16,7 +16,7 @@ export interface ChainEntry {
16
16
  buildDir: string;
17
17
  /** The module whose code is copied in before this one is built. */
18
18
  requires: string | null;
19
- frids: string[];
19
+ funcIds: string[];
20
20
  }
21
21
  /** A required module's file: the first template folder that has it. */
22
22
  export declare function locateModule(moduleName: string, templateDirs: string[]): string;
@@ -5,9 +5,10 @@ import { getFrids, getInheritedFunctionalSpecs } from "../plain/spec.js";
5
5
  import { buildDirUnder, modulesRoot, projectDirOf } from "./paths.js";
6
6
  import { notesPath } from "./notes.js";
7
7
  import { resolveTemplateDirs } from "./paths.js";
8
- import { inheritedRecord, readRecord, RECORD_FILE, writeCodeRecord } from "./records.js";
8
+ import { inheritedRecord, inheritedTestsRecord, readRecord, RECORD_FILE, writeCodeRecord, writeTestsRecord } from "./records.js";
9
9
  import { head, historyLog, recordHistory } from "./lineage.js";
10
10
  import { listFiles } from "./testing.js";
11
+ import { carryBaseRecord } from "./base-folder.js";
11
12
  /** A required module's file: the first template folder that has it. */
12
13
  export function locateModule(moduleName, templateDirs) {
13
14
  for (const dir of templateDirs) {
@@ -48,7 +49,7 @@ options = {}) {
48
49
  plainFile: resolve(file),
49
50
  buildDir: buildDirUnder(root, moduleName),
50
51
  requires: previous,
51
- frids: [...getFrids(tree)],
52
+ funcIds: [...getFrids(tree)],
52
53
  });
53
54
  return moduleName;
54
55
  };
@@ -221,9 +222,10 @@ fromModule) {
221
222
  copyFileSync(join(from, file), target);
222
223
  copied.push(file);
223
224
  }
224
- // The working notes describe this code, so they come up with it.
225
+ // The working notes describe this code, so they come up with it - and so does what it started from, a base folder's files.
225
226
  if (existsSync(notesPath(from)) && !existsSync(notesPath(to)))
226
227
  copyFileSync(notesPath(from), notesPath(to));
228
+ carryBaseRecord(from, to);
227
229
  // What the code is built from comes with it, as what the new module inherits - not as its own work.
228
230
  const built = readRecord(from);
229
231
  writeCodeRecord(to, built !== null ? inheritedRecord(built, fromModule) : {});
@@ -245,6 +247,11 @@ fromModule) {
245
247
  // And their notes, which listFiles leaves out as it does the code's.
246
248
  if (existsSync(notesPath(testsFrom)) && !existsSync(notesPath(testsTo)))
247
249
  copyFileSync(notesPath(testsFrom), notesPath(testsTo));
250
+ // What they are written for comes with them, as what the new module inherits: its conformance suite is the chain's,
251
+ // kept up to date here from now on.
252
+ const tested = readRecord(testsFrom);
253
+ if (tested !== null)
254
+ writeTestsRecord(testsTo, inheritedTestsRecord(tested, fromModule));
248
255
  }
249
256
  recordStart();
250
257
  return copied;
@@ -9,6 +9,8 @@ export interface LoadedResource {
9
9
  image?: ResourceImage;
10
10
  /** Where the file is: copy_resource copies it whole from here. */
11
11
  source?: string;
12
+ /** It is in the codebase already, at this path - a file the base folder held: used from there, never copied. */
13
+ inCodebase?: string;
12
14
  /** Its text was cut for the prompt: how long it is whole. */
13
15
  cutFrom?: number;
14
16
  }
@@ -26,12 +28,12 @@ export declare class LinkedImageError extends Error {
26
28
  /**
27
29
  * Load the resources linked from the specifications in scope for one functionality.
28
30
  *
29
- * `collectLinkedResources` stops at the requested FRID, so a resource introduced by a
30
- * later functionality is not pulled into this prompt.
31
+ * `collectLinkedResources` stops at the requested functional spec, so a resource introduced by a
32
+ * later one is not pulled into this prompt.
31
33
  */
32
34
  export declare function loadLinkedResources(tree: MarshalledPlainSource,
33
35
  /** Up to and including this functionality. Null: the whole module. */
34
- frid: string | null, templateDirs: string[]): LoadedResource[];
36
+ funcId: string | null, templateDirs: string[]): LoadedResource[];
35
37
  /** The file a linked resource is read from: the first template folder that has it, else the current folder - or null. */
36
38
  export declare function resourceFile(target: string, templateDirs: string[]): string | null;
37
39
  /**
@@ -19,14 +19,14 @@ export class LinkedImageError extends Error {
19
19
  /**
20
20
  * Load the resources linked from the specifications in scope for one functionality.
21
21
  *
22
- * `collectLinkedResources` stops at the requested FRID, so a resource introduced by a
23
- * later functionality is not pulled into this prompt.
22
+ * `collectLinkedResources` stops at the requested functional spec, so a resource introduced by a
23
+ * later one is not pulled into this prompt.
24
24
  */
25
25
  export function loadLinkedResources(tree,
26
26
  /** Up to and including this functionality. Null: the whole module. */
27
- frid, templateDirs) {
27
+ funcId, templateDirs) {
28
28
  const collected = [];
29
- collectLinkedResources(tree, collected, null, true, frid);
29
+ collectLinkedResources(tree, collected, null, true, funcId);
30
30
  const loaded = [];
31
31
  for (const resource of collected) {
32
32
  // A testing script reaches an agent as one - whole, and only when it is run on its work -
@@ -0,0 +1,19 @@
1
+ import type { ScriptKind } from "./test-scripts.ts";
2
+ /**
3
+ * The testing scripts being written in the background - by a skill run cultivate started (docs/skills-and-gates.md): while
4
+ * one is, the work loop holds the modules that need it, rather than fail their pulses for the script they lack, and it
5
+ * looks at them again once the run ends. On `globalThis`, as the chat's scope is: pi's own loader may load this file twice.
6
+ */
7
+ export interface ScriptBeingWritten {
8
+ /** The project's folder. */
9
+ project: string;
10
+ kind: ScriptKind;
11
+ /** The stack it is written for, when the run was for one. */
12
+ stack?: string;
13
+ }
14
+ /** A script being written from now: what it returns says it is no longer, written or not. */
15
+ export declare function writingScript(run: ScriptBeingWritten): () => void;
16
+ /** Whether a script of this kind is being written for a module of this project on this stack - one written for no stack is anyone's. */
17
+ export declare function scriptBeingWritten(project: string, kind: ScriptKind, stack?: string): boolean;
18
+ /** Told each time a run writing a script ends. Returns what stops it. */
19
+ export declare function onScriptRunEnded(listener: () => void): () => void;
@@ -0,0 +1,30 @@
1
+ import { resolve } from "node:path";
2
+ const KEY = Symbol.for("turri.scriptsBeingWritten");
3
+ const writing = () => (globalThis[KEY] ??= { runs: new Set(), ended: new Set() });
4
+ /** A script being written from now: what it returns says it is no longer, written or not. */
5
+ export function writingScript(run) {
6
+ const entry = { ...run, project: resolve(run.project) };
7
+ writing().runs.add(entry);
8
+ return () => {
9
+ if (!writing().runs.delete(entry))
10
+ return;
11
+ for (const listener of [...writing().ended]) {
12
+ try {
13
+ listener();
14
+ }
15
+ catch {
16
+ /* what listens never stops the run's end */
17
+ }
18
+ }
19
+ };
20
+ }
21
+ /** Whether a script of this kind is being written for a module of this project on this stack - one written for no stack is anyone's. */
22
+ export function scriptBeingWritten(project, kind, stack) {
23
+ const folder = resolve(project);
24
+ return [...writing().runs].some((run) => run.project === folder && run.kind === kind && (run.stack === undefined || stack === undefined || run.stack === stack));
25
+ }
26
+ /** Told each time a run writing a script ends. Returns what stops it. */
27
+ export function onScriptRunEnded(listener) {
28
+ writing().ended.add(listener);
29
+ return () => writing().ended.delete(listener);
30
+ }
@@ -6,7 +6,7 @@ import type { TranscriptStyle } from "./transcript-view.ts";
6
6
  *
7
7
  * Each item's first line, with its one mark in the gutter - its rung on the ladder; a changed item's changed
8
8
  * sub-bullets under it, word by word; an item taken out as a struck-through ghost. Enter opens an item: all of it, its
9
- * acceptance tests, the pulse that brings it, its conformance tests, and - a definition - the built specs that use it.
9
+ * scenarios, the pulse that brings it, its conformance tests, and - a definition - the built specs that use it.
10
10
  * One module is shown at a time, a tab a module of the chain - ←→ go from one to the next: the module regenerating now, until
11
11
  * another tab is chosen, and again once another module starts regenerating. Under its own text, the files it imports, a
12
12
  * folded block each. A chain of many modules nested one in another was too deep to read, and too much to draw.
@@ -99,7 +99,7 @@ export declare function columnHeadings(page: MarkedPage, width: number, style: T
99
99
  export declare function legendLines(page: MarkedPage, style: TranscriptStyle): string[];
100
100
  /** What the person is needed for, and how: the line under a page's first `!`, and its top line's end. */
101
101
  export declare const DISCUSS_KEY = "d";
102
- /** A line of ***plain highlighted as plyn highlights it: the whole line read, so a span over several words keeps its colour. */
102
+ /** A line of ***plain highlighted as turri's grammar highlights it: the whole line read, so a span over several words keeps its colour. */
103
103
  export declare function highlighted(line: string, style: TranscriptStyle): string;
104
104
  /** One line become another, word by word: what is gone struck through in red, what is new in green, the rest as it is. */
105
105
  export declare function wordDiff(was: string, now: string, style: TranscriptStyle): string;
@@ -1,7 +1,7 @@
1
1
  import { basename } from "node:path";
2
2
  import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
3
  import { highlightLine, roleOf } from "../plain/highlight.js";
4
- import { ACCEPTANCE_TEST_HEADING, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS } from "../plain/spec.js";
4
+ import { ACCEPTANCE_TEST_HEADING, CONFORMANCE_TEST_HEADING, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS } from "../plain/spec.js";
5
5
  import { spinner } from "./job-panel.js";
6
6
  import { countOf } from "./spec-map.js";
7
7
  export function createSpecMapState() {
@@ -75,6 +75,8 @@ const needs = (style) => style.warning ?? style.error;
75
75
  const strike = (text) => `\x1b[9m${text}\x1b[29m`;
76
76
  /** How far a group, and what is in it, is indented under its module. */
77
77
  const INDENT = " ";
78
+ /** What the first test req of a module none of whose functional specs has scenarios says. */
79
+ const NOT_USED = " not used: no scenarios";
78
80
  /**
79
81
  * Where an item stands, as one mark on a ladder every item climbs: planned, working, regenerated, validating, conform - and needs
80
82
  * you, off it - named in the legend in the panel's corner. The same mark means the same thing whatever the config says
@@ -160,7 +162,7 @@ export function legendLines(page, style) {
160
162
  /** What the person is needed for, and how: the line under a page's first `!`, and its top line's end. */
161
163
  export const DISCUSS_KEY = "d";
162
164
  const needsLine = (page) => `needs you - ${page.status} · ${DISCUSS_KEY} to talk it through in the chat`;
163
- /** Each character's role in a line of ***plain, as plyn highlights it - kept, the map redraws every half second. */
165
+ /** Each character's role in a line of ***plain, as turri's grammar highlights it - kept, the map redraws every half second. */
164
166
  const rolesSeen = new Map();
165
167
  function rolesOf(line) {
166
168
  const seen = rolesSeen.get(line);
@@ -193,7 +195,7 @@ function paintRoles(text, from, roles, paint) {
193
195
  }
194
196
  return out + (run === "" ? "" : role === undefined ? run : paint.syntax(role, run));
195
197
  }
196
- /** A line of ***plain highlighted as plyn highlights it: the whole line read, so a span over several words keeps its colour. */
198
+ /** A line of ***plain highlighted as turri's grammar highlights it: the whole line read, so a span over several words keeps its colour. */
197
199
  export function highlighted(line, style) {
198
200
  return style.diff === undefined ? line : paintRoles(line, 0, rolesOf(line), style.diff);
199
201
  }
@@ -325,7 +327,11 @@ function wrapped(text, width, colour = (line) => line) {
325
327
  .flatMap((line) => (line === "" ? [""] : wrapTextWithAnsi(line, Math.max(10, width - CONTINUED.length))))
326
328
  .map((line) => `${CONTINUED}${colour(line)}`);
327
329
  }
328
- /** All of an item, opened: the rest of its text against the code's, and its acceptance tests. Where it stands is beside its mark. */
330
+ /**
331
+ * All of an item, opened: the rest of its text against the code's, and a functional spec's scenarios - under
332
+ * `***conformance tests***` and `***acceptance tests***`, each scenario its `# <scenario-id>` and its text, as its test
333
+ * plain file has them. Where it stands is beside its mark.
334
+ */
329
335
  function detailLines(item, width, style) {
330
336
  const lines = [];
331
337
  // The rest of its text - its first line is the row - against the record's, for a change.
@@ -334,15 +340,21 @@ function detailLines(item, width, style) {
334
340
  const drawn = item.state === "removed" ? style.removed(strike(line.now ?? "")) : item.state === "changed" ? diffLine(line, style) : highlighted(line.now ?? "", style);
335
341
  lines.push(...wrapped(drawn, width));
336
342
  }
337
- if (item.acceptance.length > 0) {
338
- // As the spec has them: nested under the functional spec as deep as its sub-bullets, under a heading of their own
339
- // drawn as a section's is.
340
- const nested = /^( +)\S/m.exec(item.text.split("\n").slice(1).join("\n"))?.[1] ?? INDENT;
341
- const heading = `***${ACCEPTANCE_TEST_HEADING}***`;
342
- lines.push(CONTINUED.trimEnd());
343
- lines.push(`${CONTINUED}${nested}${style.diff !== undefined ? style.diff.syntax("section", heading) : style.muted(heading)}`);
344
- for (const test of item.acceptance)
345
- lines.push(...wrapped(test.split("\n").map((line) => `${nested}${highlighted(line, style)}`).join("\n"), width));
343
+ // Each kind under its section heading, as the test plain file has it - the heading and its scenarios indented under the
344
+ // functional spec, a blank line after each scenario, so one reads apart from the next.
345
+ const blank = CONTINUED.trimEnd();
346
+ for (const [heading, scenarios] of [[CONFORMANCE_TEST_HEADING, item.scenarios.conformance], [ACCEPTANCE_TEST_HEADING, item.scenarios.acceptance]]) {
347
+ if (scenarios.length === 0)
348
+ continue;
349
+ const title = `***${heading}***`;
350
+ if (lines.at(-1) !== blank)
351
+ lines.push(blank);
352
+ lines.push(`${CONTINUED}${INDENT}${style.diff !== undefined ? style.diff.syntax("section", title) : style.muted(title)}`);
353
+ for (const scenario of scenarios) {
354
+ const text = [`# ${scenario.id}`, ...scenario.text.split("\n")].map((line) => highlighted(line, style)).join("\n");
355
+ lines.push(...wrapped(text, width - INDENT.length).map((line) => (line === CONTINUED ? blank : `${CONTINUED}${INDENT}${line.slice(CONTINUED.length)}`)));
356
+ lines.push(blank);
357
+ }
346
358
  }
347
359
  return lines;
348
360
  }
@@ -431,6 +443,8 @@ export function specMapLines(pages, state, width, style, _jobs = [], now = Date.
431
443
  let underImport = false;
432
444
  // Whether what the module needs the person for is said yet, under its first `!`.
433
445
  let toldWhy = false;
446
+ // The first test req nothing uses: it says so for them all.
447
+ const firstUnused = shown?.items.find((item) => item.unused === true);
434
448
  rows.forEach((row, index) => {
435
449
  const here = index === state.cursor;
436
450
  const pointer = here ? style.accent("›") : " ";
@@ -467,6 +481,11 @@ export function specMapLines(pages, state, width, style, _jobs = [], now = Date.
467
481
  add(`${CONTINUED}${style.diff !== undefined ? style.diff.syntax("section", `***${item.section}***`) : style.muted(`***${item.section}***`)}`);
468
482
  }
469
483
  underImport = false;
484
+ // Functional specs are written apart, as their file writes them: a line of space between one and the next - one an
485
+ // opened spec's scenarios end on already does.
486
+ const above = rows[index - 1];
487
+ if (item.funcId !== undefined && above?.kind === "item" && above.item.funcId !== undefined && above.item.section === item.section && lines.at(-1) !== CONTINUED.trimEnd())
488
+ add(CONTINUED.trimEnd());
470
489
  rowStarts.push(lines.length);
471
490
  const mark = item.state === "removed" ? "removed" : item.mark;
472
491
  const gutter = `${pointer}${markText(item, style, now)} ${BAR}`;
@@ -474,17 +493,29 @@ export function specMapLines(pages, state, width, style, _jobs = [], now = Date.
474
493
  const room = Math.max(10, width - GUTTER - BAR.length);
475
494
  const first = firstLine(item.text);
476
495
  const wasFirst = firstLine(item.was ?? "");
477
- const drawn = item.state === "removed"
496
+ const body = item.state === "removed"
478
497
  ? style.removed(strike(first))
479
498
  : item.state === "changed" && wasFirst !== first
480
499
  ? wordDiff(wasFirst, first, style)
481
500
  : highlighted(first, style);
501
+ // A functional spec's row is as its file writes it: its `# <func-id>` heading, its body's first line under it.
502
+ const unused = item.unused === true && item === firstUnused ? NOT_USED : "";
503
+ const said = unused;
504
+ // A func-id renamed, nothing else: the one it is built under beside it - renamed, not regenerated.
505
+ const renamed = item.renamedFrom !== undefined ? ` (was ${item.renamedFrom})` : "";
506
+ const heading = item.funcId !== undefined ? `# ${item.funcId}` : undefined;
507
+ const headingDrawn = heading === undefined ? "" : item.state === "removed" ? style.removed(strike(heading)) : highlighted(heading, style);
508
+ const plain = heading !== undefined ? `${heading}${renamed}${said}` : `${first}${said}`;
509
+ const drawn = `${heading !== undefined ? `${headingDrawn}${renamed !== "" ? style.muted(renamed) : ""}` : body}${said !== "" ? style.muted(said) : ""}`;
482
510
  // Wrapped, never cut short: what follows a `…` is what a person came to read. The lines after the first hang
483
511
  // under the item's text, past its bullet.
484
- const [head = "", ...rest] = hanging(drawn, first, room);
512
+ const [head = "", ...rest] = hanging(drawn, plain, room);
485
513
  add(`${gutter}${head}`, mark);
486
514
  for (const line of rest)
487
515
  add(`${CONTINUED}${line}`, mark);
516
+ if (heading !== undefined)
517
+ for (const line of hanging(body, first, room))
518
+ add(`${CONTINUED}${line}`, mark);
488
519
  // The first item that needs the person says what for, and how to go about it - opened or closed.
489
520
  if (page.stuck && !toldWhy && rungOf(item) === "needsYou") {
490
521
  toldWhy = true;
@@ -9,8 +9,9 @@ import { type MapCodebase, type MapPlan, type MapPulse } from "./loop.ts";
9
9
  * loop's plan is laid over it (`markSpecMap`): the pulse regenerating each item, the pulse planned to, or why there is
10
10
  * none.
11
11
  *
12
- * An item is what turri plans and diffs by: a definition, a req, a functional spec with its sub-bullets and acceptance
13
- * tests. Items are matched to pulses by their lines in the trees each pulse goes between, as the loop has them.
12
+ * An item is what turri plans and diffs by: a definition, a req, a functional spec with its body and its scenarios. Items
13
+ * are matched to pulses by their lines in the trees each pulse goes between, as the loop has them - a functional spec,
14
+ * on the conformance track, by its func-id.
14
15
  */
15
16
  /** Where an item stands against the code record. A test req is the conformance track's: it is never built. */
16
17
  export type ItemState = "built" | "new" | "changed" | "removed" | "test";
@@ -25,14 +26,31 @@ export interface MapItem {
25
26
  text: string;
26
27
  /** The code record's text, for a changed item. */
27
28
  was?: string;
28
- /** Its acceptance tests, as the spec has them now. */
29
- acceptance: string[];
29
+ /** A functional spec's func-id: what it is matched and kept by. */
30
+ funcId?: string;
31
+ /** A functional spec's scenarios, as its test plain file has them now: none, with no file - it is not tested. */
32
+ scenarios: MapScenarios;
30
33
  /** Its line in the code's tree now, and in the code record's: what a pulse's trees are matched by. */
31
34
  line?: string;
32
35
  wasLine?: string;
33
36
  /** A functional spec's conformance tests: written for it as it reads now, for an earlier text, or for one taken out. */
34
37
  tests?: "now" | "older" | "removed";
38
+ /** A functional spec whose func-id was renamed, and nothing else: the func-id it is built under - turri renames it, regenerating nothing. */
39
+ renamedFrom?: string;
35
40
  }
41
+ /** A functional spec's scenarios, each its scenario-id and its text. */
42
+ export interface MapScenarios {
43
+ conformance: Array<{
44
+ id: string;
45
+ text: string;
46
+ }>;
47
+ acceptance: Array<{
48
+ id: string;
49
+ text: string;
50
+ }>;
51
+ }
52
+ /** Whether a functional spec has scenarios - and so conformance tests to write. */
53
+ export declare function hasScenarioViews(scenarios: MapScenarios): boolean;
36
54
  export interface MapPage {
37
55
  module: string;
38
56
  plainFile: string;
@@ -42,8 +60,10 @@ export interface MapPage {
42
60
  imports: string[];
43
61
  /** The module it requires - whose code it is built on - when it requires one. */
44
62
  requires?: string;
45
- /** Its conformance tests are on, as its config has it: `conformance-tests:` is not false. */
63
+ /** Whether any of its functional specs has scenarios, and so conformance tests to write: its test reqs are used. */
46
64
  conformance?: boolean;
65
+ /** Its functional specs have scenarios, and a step of adding tests is skipped - the test reqs, or the conformance testing script: what is missing, in a few words. */
66
+ testingMissing?: string;
47
67
  items: MapItem[];
48
68
  /** Its spec does not parse, or could not be read. */
49
69
  error?: string;
@@ -60,6 +80,8 @@ export interface MarkedItem extends MapItem {
60
80
  testing?: boolean;
61
81
  /** The conformance pulse planned to write its tests, not running yet: what its provisional number is read from. */
62
82
  testsPulse?: MapPulse;
83
+ /** A test req of a module none of whose functional specs has scenarios: nothing uses it. */
84
+ unused?: boolean;
63
85
  }
64
86
  export interface MarkedPage extends MapPage {
65
87
  items: MarkedItem[];