@printpractical/cruze 0.0.13 → 0.0.14

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 (298) hide show
  1. package/README.md +61 -118
  2. package/dist/adapters/inbound/cli/cli_context.js +0 -27
  3. package/dist/adapters/inbound/cli/commands/code.js +5 -18
  4. package/dist/adapters/inbound/cli/commands/documents.js +11 -37
  5. package/dist/adapters/inbound/cli/commands/init.js +1 -1
  6. package/dist/adapters/inbound/cli/commands/review.js +2 -9
  7. package/dist/adapters/inbound/cli/commands/status.js +16 -63
  8. package/dist/adapters/inbound/cli/commands.js +10 -37
  9. package/dist/adapters/inbound/cli/parse_command.js +0 -54
  10. package/dist/adapters/outbound/git_repository.js +2 -71
  11. package/dist/app/ports/repository.js +0 -12
  12. package/dist/app/project_context.js +15 -25
  13. package/dist/app/use_cases/approve_document.js +29 -0
  14. package/dist/app/use_cases/check_code.js +21 -56
  15. package/dist/app/use_cases/new_document.js +25 -0
  16. package/dist/app/use_cases/project_status.js +43 -67
  17. package/dist/app/use_cases/run_review.js +11 -30
  18. package/dist/app/use_cases/validate_project.js +4 -4
  19. package/dist/domain/approvals.js +51 -0
  20. package/dist/domain/check/check_source.js +2 -31
  21. package/dist/domain/config.js +8 -84
  22. package/dist/domain/hashing.js +16 -30
  23. package/dist/domain/ids.js +13 -27
  24. package/dist/domain/next_step.js +41 -0
  25. package/dist/domain/project/features.js +71 -0
  26. package/dist/domain/project/layout.js +11 -27
  27. package/dist/domain/project/project_docs.js +48 -0
  28. package/dist/domain/project/release.js +30 -0
  29. package/dist/domain/scaffold.js +0 -1
  30. package/dist/domain/skill_budget.js +4 -4
  31. package/dist/domain/trace.js +8 -25
  32. package/dist/domain/validation/document_rules.js +70 -0
  33. package/dist/domain/validation/release_rules.js +45 -0
  34. package/dist/domain/validation/validate_project.js +20 -27
  35. package/dist/domain/versions/cruze_version.js +3 -9
  36. package/dist/main.js +1 -1
  37. package/package.json +1 -1
  38. package/skills/asking/SKILL.md +51 -0
  39. package/skills/behavioural-testing/SKILL.md +14 -16
  40. package/skills/behavioural-testing/examples.md +1 -2
  41. package/skills/behavioural-testing/fakes.md +2 -2
  42. package/skills/build/SKILL.md +34 -38
  43. package/skills/dependency-approval/SKILL.md +34 -31
  44. package/skills/envision/SKILL.md +23 -18
  45. package/skills/envision/architecture.md +16 -0
  46. package/skills/envision/breakdown.md +14 -0
  47. package/skills/envision/possibilities.md +6 -7
  48. package/skills/fix/SKILL.md +34 -0
  49. package/skills/formats/SKILL.md +39 -87
  50. package/skills/formats/config.md +47 -0
  51. package/skills/formats/templates/architecture.md +18 -74
  52. package/skills/formats/templates/feature.md +18 -34
  53. package/skills/formats/templates/vision.md +15 -27
  54. package/skills/hexagonal-design/SKILL.md +11 -11
  55. package/skills/hexagonal-design/contracts.md +3 -3
  56. package/skills/hexagonal-design/fundamentals.md +1 -1
  57. package/skills/hexagonal-design/languages/cpp.md +1 -1
  58. package/skills/hexagonal-design/languages/rust.md +1 -1
  59. package/skills/hexagonal-design/languages/swift.md +2 -2
  60. package/skills/hexagonal-design/runtime.md +2 -2
  61. package/skills/research/SKILL.md +4 -4
  62. package/skills/research/evaluation.md +1 -1
  63. package/skills/review/SKILL.md +27 -0
  64. package/skills/roles/SKILL.md +8 -59
  65. package/skills/roles/code-reviewer.md +21 -32
  66. package/skills/roles/researcher.md +3 -3
  67. package/skills/shape/SKILL.md +37 -0
  68. package/skills/shape/self-check.md +12 -0
  69. package/src/adapters/inbound/cli/cli_context.ts +0 -27
  70. package/src/adapters/inbound/cli/commands/code.ts +5 -18
  71. package/src/adapters/inbound/cli/commands/documents.ts +11 -37
  72. package/src/adapters/inbound/cli/commands/init.ts +1 -1
  73. package/src/adapters/inbound/cli/commands/review.ts +2 -9
  74. package/src/adapters/inbound/cli/commands/status.ts +16 -63
  75. package/src/adapters/inbound/cli/commands.ts +10 -37
  76. package/src/adapters/inbound/cli/parse_command.ts +0 -54
  77. package/src/adapters/outbound/git_repository.ts +2 -71
  78. package/src/app/ports/repository.ts +0 -12
  79. package/src/app/project_context.ts +15 -25
  80. package/src/app/use_cases/approve_document.ts +29 -0
  81. package/src/app/use_cases/check_code.ts +21 -56
  82. package/src/app/use_cases/new_document.ts +25 -0
  83. package/src/app/use_cases/project_status.ts +49 -73
  84. package/src/app/use_cases/run_review.ts +11 -30
  85. package/src/app/use_cases/validate_project.ts +4 -4
  86. package/src/domain/approvals.ts +51 -0
  87. package/src/domain/check/check_source.ts +2 -31
  88. package/src/domain/config.ts +9 -85
  89. package/src/domain/hashing.ts +16 -30
  90. package/src/domain/ids.ts +13 -27
  91. package/src/domain/next_step.ts +41 -0
  92. package/src/domain/project/features.ts +71 -0
  93. package/src/domain/project/layout.ts +11 -27
  94. package/src/domain/project/project_docs.ts +48 -0
  95. package/src/domain/project/release.ts +30 -0
  96. package/src/domain/scaffold.ts +0 -1
  97. package/src/domain/skill_budget.ts +4 -4
  98. package/src/domain/trace.ts +8 -25
  99. package/src/domain/validation/document_rules.ts +70 -0
  100. package/src/domain/validation/release_rules.ts +45 -0
  101. package/src/domain/validation/validate_project.ts +21 -28
  102. package/src/domain/versions/cruze_version.ts +3 -9
  103. package/src/main.ts +1 -1
  104. package/templates/init/AGENTS.md +4 -12
  105. package/templates/init/ci.yml +3 -3
  106. package/templates/init/config.yaml +8 -17
  107. package/templates/init/review/claude.yaml +2 -2
  108. package/dist/adapters/inbound/cli/argument_files.js +0 -29
  109. package/dist/adapters/inbound/cli/commands/diff.js +0 -23
  110. package/dist/adapters/inbound/cli/commands/journal.js +0 -32
  111. package/dist/adapters/inbound/cli/commands/progress.js +0 -55
  112. package/dist/adapters/inbound/cli/commands/realign.js +0 -28
  113. package/dist/adapters/inbound/cli/commands/show.js +0 -96
  114. package/dist/app/use_cases/abandon_work.js +0 -42
  115. package/dist/app/use_cases/approve_artifact.js +0 -197
  116. package/dist/app/use_cases/journal_events.js +0 -64
  117. package/dist/app/use_cases/land_change.js +0 -134
  118. package/dist/app/use_cases/new_project_doc.js +0 -48
  119. package/dist/app/use_cases/new_work_item.js +0 -75
  120. package/dist/app/use_cases/realign_done.js +0 -45
  121. package/dist/app/use_cases/realign_status.js +0 -106
  122. package/dist/app/use_cases/record_progress.js +0 -118
  123. package/dist/app/use_cases/show_elements.js +0 -18
  124. package/dist/app/use_cases/show_notes.js +0 -11
  125. package/dist/app/use_cases/show_scope.js +0 -51
  126. package/dist/app/use_cases/upstream_diff.js +0 -55
  127. package/dist/domain/approvals/agent_approval.js +0 -68
  128. package/dist/domain/approvals/evaluate.js +0 -36
  129. package/dist/domain/approvals/fingerprint.js +0 -52
  130. package/dist/domain/approvals/rebase.js +0 -39
  131. package/dist/domain/approvals/records.js +0 -50
  132. package/dist/domain/approvals/upstream_text.js +0 -36
  133. package/dist/domain/architecture_sections.js +0 -23
  134. package/dist/domain/check/decision_comments.js +0 -38
  135. package/dist/domain/decision_citations.js +0 -62
  136. package/dist/domain/edits/agents_file.js +0 -58
  137. package/dist/domain/edits/managed_tables.js +0 -60
  138. package/dist/domain/edits/progress_block.js +0 -33
  139. package/dist/domain/edits/status_lines.js +0 -52
  140. package/dist/domain/elements.js +0 -126
  141. package/dist/domain/journal.js +0 -109
  142. package/dist/domain/land/bookkeeping.js +0 -54
  143. package/dist/domain/land/merge_plan.js +0 -137
  144. package/dist/domain/land/text_ops.js +0 -66
  145. package/dist/domain/line_diff.js +0 -56
  146. package/dist/domain/project/artifact_ref.js +0 -42
  147. package/dist/domain/project/deltas.js +0 -49
  148. package/dist/domain/project/element_lookup.js +0 -124
  149. package/dist/domain/project/living_model.js +0 -55
  150. package/dist/domain/project/new_items.js +0 -48
  151. package/dist/domain/project/notes.js +0 -74
  152. package/dist/domain/project/plan_parts.js +0 -111
  153. package/dist/domain/project/progress.js +0 -43
  154. package/dist/domain/project/project_view.js +0 -72
  155. package/dist/domain/project/roadmap.js +0 -43
  156. package/dist/domain/project/scope_view.js +0 -134
  157. package/dist/domain/project/work_items.js +0 -68
  158. package/dist/domain/status/next_step.js +0 -181
  159. package/dist/domain/status/pending_rethink.js +0 -22
  160. package/dist/domain/status/verification.js +0 -29
  161. package/dist/domain/status/work_status.js +0 -125
  162. package/dist/domain/validation/decision_rules.js +0 -76
  163. package/dist/domain/validation/delta_rules.js +0 -81
  164. package/dist/domain/validation/element_rules.js +0 -104
  165. package/dist/domain/validation/future_rules.js +0 -40
  166. package/dist/domain/validation/living_rules.js +0 -99
  167. package/dist/domain/validation/plan_rules.js +0 -124
  168. package/dist/domain/validation/project_rules.js +0 -115
  169. package/dist/domain/validation/size_rules.js +0 -45
  170. package/dist/domain/validation/vision_rules.js +0 -41
  171. package/dist/domain/validation/work_item_rules.js +0 -175
  172. package/dist/domain/versions/realign_notes.js +0 -72
  173. package/dist/domain/versions/rule_versions.js +0 -29
  174. package/dist/domain/versions/scheduled_exceptions.js +0 -41
  175. package/skills/about/SKILL.md +0 -21
  176. package/skills/architect/SKILL.md +0 -49
  177. package/skills/architect/feature.md +0 -40
  178. package/skills/architect/project.md +0 -71
  179. package/skills/architect/tweak.md +0 -22
  180. package/skills/architect/walkthrough.md +0 -35
  181. package/skills/domain-language/SKILL.md +0 -37
  182. package/skills/envision/feature.md +0 -25
  183. package/skills/envision/intake.md +0 -28
  184. package/skills/envision/project.md +0 -24
  185. package/skills/explore/SKILL.md +0 -39
  186. package/skills/formats/reference/architecture.md +0 -84
  187. package/skills/formats/reference/config.md +0 -80
  188. package/skills/formats/reference/durable-docs.md +0 -74
  189. package/skills/formats/reference/specs.md +0 -65
  190. package/skills/formats/reference/work-items.md +0 -100
  191. package/skills/formats/templates/adr.md +0 -7
  192. package/skills/formats/templates/change.md +0 -29
  193. package/skills/formats/templates/glossary.md +0 -8
  194. package/skills/formats/templates/roadmap.md +0 -32
  195. package/skills/formats/templates/spec.md +0 -11
  196. package/skills/formats/templates/standalone-change.md +0 -46
  197. package/skills/grilling/SKILL.md +0 -90
  198. package/skills/grilling/illustrating.md +0 -38
  199. package/skills/improve/SKILL.md +0 -51
  200. package/skills/land/SKILL.md +0 -48
  201. package/skills/next/SKILL.md +0 -31
  202. package/skills/plan/SKILL.md +0 -62
  203. package/skills/plan/tasks.md +0 -35
  204. package/skills/plan/test-plan.md +0 -14
  205. package/skills/realign/SKILL.md +0 -37
  206. package/skills/realign/notes/0.0.10.md +0 -131
  207. package/skills/realign/notes/0.0.11.md +0 -15
  208. package/skills/realign/notes/0.0.12.md +0 -31
  209. package/skills/realign/notes/0.0.13.md +0 -11
  210. package/skills/realign/notes/0.0.2.md +0 -69
  211. package/skills/realign/notes/0.0.3.md +0 -11
  212. package/skills/realign/notes/0.0.4.md +0 -27
  213. package/skills/realign/notes/0.0.5.md +0 -38
  214. package/skills/realign/notes/0.0.6.md +0 -44
  215. package/skills/realign/notes/0.0.7.md +0 -11
  216. package/skills/realign/notes/0.0.8.md +0 -97
  217. package/skills/realign/notes/0.0.9.md +0 -59
  218. package/skills/realign/notes.md +0 -40
  219. package/skills/rethink/SKILL.md +0 -62
  220. package/skills/rethink/levels.md +0 -54
  221. package/skills/roadmap/SKILL.md +0 -54
  222. package/skills/roles/auditor.md +0 -35
  223. package/skills/roles/design-reviewer.md +0 -43
  224. package/skills/roles/design-rubric.md +0 -41
  225. package/skills/roles/plan-rubric.md +0 -11
  226. package/skills/roles/surveyor.md +0 -41
  227. package/skills/roles/verifier.md +0 -42
  228. package/skills/triage/SKILL.md +0 -37
  229. package/skills/verify/SKILL.md +0 -45
  230. package/skills/verify/manual-test.md +0 -23
  231. package/src/adapters/inbound/cli/argument_files.ts +0 -29
  232. package/src/adapters/inbound/cli/commands/diff.ts +0 -23
  233. package/src/adapters/inbound/cli/commands/journal.ts +0 -32
  234. package/src/adapters/inbound/cli/commands/progress.ts +0 -55
  235. package/src/adapters/inbound/cli/commands/realign.ts +0 -28
  236. package/src/adapters/inbound/cli/commands/show.ts +0 -96
  237. package/src/app/use_cases/abandon_work.ts +0 -42
  238. package/src/app/use_cases/approve_artifact.ts +0 -197
  239. package/src/app/use_cases/journal_events.ts +0 -64
  240. package/src/app/use_cases/land_change.ts +0 -134
  241. package/src/app/use_cases/new_project_doc.ts +0 -48
  242. package/src/app/use_cases/new_work_item.ts +0 -75
  243. package/src/app/use_cases/realign_done.ts +0 -45
  244. package/src/app/use_cases/realign_status.ts +0 -106
  245. package/src/app/use_cases/record_progress.ts +0 -118
  246. package/src/app/use_cases/show_elements.ts +0 -18
  247. package/src/app/use_cases/show_notes.ts +0 -11
  248. package/src/app/use_cases/show_scope.ts +0 -51
  249. package/src/app/use_cases/upstream_diff.ts +0 -55
  250. package/src/domain/approvals/agent_approval.ts +0 -68
  251. package/src/domain/approvals/evaluate.ts +0 -36
  252. package/src/domain/approvals/fingerprint.ts +0 -52
  253. package/src/domain/approvals/rebase.ts +0 -39
  254. package/src/domain/approvals/records.ts +0 -50
  255. package/src/domain/approvals/upstream_text.ts +0 -36
  256. package/src/domain/architecture_sections.ts +0 -23
  257. package/src/domain/check/decision_comments.ts +0 -38
  258. package/src/domain/decision_citations.ts +0 -62
  259. package/src/domain/edits/agents_file.ts +0 -58
  260. package/src/domain/edits/managed_tables.ts +0 -60
  261. package/src/domain/edits/progress_block.ts +0 -33
  262. package/src/domain/edits/status_lines.ts +0 -52
  263. package/src/domain/elements.ts +0 -126
  264. package/src/domain/journal.ts +0 -109
  265. package/src/domain/land/bookkeeping.ts +0 -54
  266. package/src/domain/land/merge_plan.ts +0 -137
  267. package/src/domain/land/text_ops.ts +0 -66
  268. package/src/domain/line_diff.ts +0 -56
  269. package/src/domain/project/artifact_ref.ts +0 -42
  270. package/src/domain/project/deltas.ts +0 -49
  271. package/src/domain/project/element_lookup.ts +0 -124
  272. package/src/domain/project/living_model.ts +0 -55
  273. package/src/domain/project/new_items.ts +0 -48
  274. package/src/domain/project/notes.ts +0 -74
  275. package/src/domain/project/plan_parts.ts +0 -111
  276. package/src/domain/project/progress.ts +0 -43
  277. package/src/domain/project/project_view.ts +0 -72
  278. package/src/domain/project/roadmap.ts +0 -43
  279. package/src/domain/project/scope_view.ts +0 -134
  280. package/src/domain/project/work_items.ts +0 -68
  281. package/src/domain/status/next_step.ts +0 -181
  282. package/src/domain/status/pending_rethink.ts +0 -22
  283. package/src/domain/status/verification.ts +0 -29
  284. package/src/domain/status/work_status.ts +0 -125
  285. package/src/domain/validation/decision_rules.ts +0 -76
  286. package/src/domain/validation/delta_rules.ts +0 -81
  287. package/src/domain/validation/element_rules.ts +0 -104
  288. package/src/domain/validation/future_rules.ts +0 -40
  289. package/src/domain/validation/living_rules.ts +0 -99
  290. package/src/domain/validation/plan_rules.ts +0 -124
  291. package/src/domain/validation/project_rules.ts +0 -115
  292. package/src/domain/validation/size_rules.ts +0 -45
  293. package/src/domain/validation/vision_rules.ts +0 -41
  294. package/src/domain/validation/work_item_rules.ts +0 -175
  295. package/src/domain/versions/realign_notes.ts +0 -72
  296. package/src/domain/versions/rule_versions.ts +0 -29
  297. package/src/domain/versions/scheduled_exceptions.ts +0 -41
  298. package/templates/init/gitattributes +0 -2
@@ -1,20 +1,14 @@
1
1
  import { CruzeError } from "../../domain/cruze_error.js";
2
- import { resolveArtifact } from "../../domain/project/artifact_ref.js";
3
- import { isPlanned } from "../../domain/project/plan_parts.js";
4
- import { featureOf, } from "../../domain/project/work_items.js";
5
2
 
6
- import { loadView, } from "../project_context.js";
3
+ import { loadDocs, } from "../project_context.js";
7
4
 
8
- export const ROLES = ["design-reviewer", "code-reviewer", "verifier", "researcher", "auditor", "surveyor"] ;
5
+ export const ROLES = ["code-reviewer", "researcher"] ;
9
6
 
10
7
 
11
8
 
12
9
 
13
-
14
-
15
-
16
10
 
17
-
11
+
18
12
 
19
13
 
20
14
 
@@ -30,37 +24,31 @@ export const ROLES = ["design-reviewer", "code-reviewer", "verifier", "researche
30
24
 
31
25
  /**
32
26
  * Runs a Cruze role in a fresh agent context: the role's prompt, what it reviews, and nothing
33
- * from the conversation that produced the work. Returns the role's report as written, or the
34
- * prompt to run in a helper when the configured agent isn't installed.
27
+ * from the conversation that produced the work.
35
28
  */
36
29
  export async function runReview(deps , request ) {
37
30
  if (!(ROLES ).includes(request.role)) throw new CruzeError("unknown-role", `the roles are ${ROLES.join(", ")}`);
38
31
  const role = request.role ;
39
- const view = await loadView(deps.files);
32
+ const docs = await loadDocs(deps.files);
40
33
  const roleText = (await deps.files.readText(`.agents/skills/cruze-roles/${role}.md`)) ?? (await bundledRole(deps, role));
41
34
 
42
- const context = [`Round: ${request.round ?? 1}`];
35
+ const context = [];
43
36
  if (request.item !== undefined) {
44
- const { path, item } = resolveArtifact(view, request.item);
45
- context.push(`Under review: ${item?.ref ?? path} (\`${path}\`)`);
46
- const feature = item === undefined ? undefined : featureOf(view.items, item);
47
- if (feature !== undefined) context.push(`Its feature: \`${feature.path}\``);
48
- if (role === "design-reviewer" && item !== undefined) context.push(rubricLine(item));
49
- } else {
50
- context.push("Under review: the project documents the role lists for project scope");
37
+ const feature = docs.features.find((f) => f.slug === request.item);
38
+ if (feature === undefined) throw new CruzeError("not-found", `no feature ${request.item} in docs/features/`);
39
+ context.push(`Under review: the feature in \`${feature.path}\``);
51
40
  }
52
41
  if (request.base !== undefined) context.push(`The changes under review: run \`git diff ${request.base}\`.`);
53
- if (request.blockers !== undefined) context.push(`Round-1 blockers to check:\n\n${request.blockers}`);
54
42
 
55
43
  const prompt = [
56
44
  roleText.trim(),
57
45
  "## This run",
58
46
  "You are running the role above in a fresh context, in the project's repository. Read every input it lists from the files, and run the commands it names. Edit no files. End with the report the role describes.",
59
- context.map((line) => `- ${line}`).join("\n"),
47
+ ...(context.length === 0 ? [] : [context.map((line) => `- ${line}`).join("\n")]),
60
48
  ].join("\n\n");
61
49
 
62
50
  const subject = { role, ...(request.item === undefined ? {} : { item: request.item }) };
63
- const command = view.config?.config?.review.command;
51
+ const command = docs.config?.config?.review.command;
64
52
  if (command === undefined) return { ...subject, status: "run-in-helper", instruction: helperInstruction(undefined), prompt };
65
53
  const run = await deps.runner.run(command, prompt);
66
54
  if (run.kind === "missing") return { ...subject, command, status: "run-in-helper", instruction: helperInstruction(command[0]), prompt };
@@ -84,10 +72,3 @@ async function bundledRole(deps , role ) {
84
72
  if (file === undefined) throw new CruzeError("role-missing", `the ${role} role is not installed; run cruze install`);
85
73
  return file.text;
86
74
  }
87
-
88
- /** A design reviewer reads one rubric: the plan rubric for a planned change, the design rubric otherwise. */
89
- function rubricLine(item ) {
90
- return item.kind !== "feature" && isPlanned(item.doc)
91
- ? "This is a change's plan: review it against `.agents/skills/cruze-roles/plan-rubric.md`."
92
- : "This is a design: review it against `.agents/skills/cruze-roles/design-rubric.md`.";
93
- }
@@ -3,8 +3,8 @@ import { warning, } from "../../domain/validation/problem.js";
3
3
  import { PATHS } from "../../domain/project/layout.js";
4
4
  import { versionSkew } from "../../domain/versions/cruze_version.js";
5
5
 
6
- import { loadView } from "../project_context.js";
7
6
 
7
+ import { loadDocs } from "../project_context.js";
8
8
 
9
9
 
10
10
 
@@ -15,10 +15,10 @@ import { loadView } from "../project_context.js";
15
15
 
16
16
  /** Checks every document in the project against the cruze-formats rules, and that its skills match this CLI. */
17
17
  export async function validate(deps ) {
18
- const view = await loadView(deps.files);
19
- const config = view.config?.config;
18
+ const docs = await loadDocs(deps.files);
19
+ const config = docs.config?.config;
20
20
  const skew = config === null || config === undefined ? undefined : versionSkew(deps.bundle.version, config.cruze);
21
- const problems = [...validateProject(view), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)])];
21
+ const problems = [...validateProject(docs), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)])];
22
22
  return {
23
23
  valid: !hasErrors(problems),
24
24
  errors: problems.filter((p) => p.severity === "error").length,
@@ -0,0 +1,51 @@
1
+ import { designHash } from "./hashing.js";
2
+
3
+ import { slugOfFeaturePath } from "./project/layout.js";
4
+
5
+ /** One approval of one document: the hash of its design content when the user approved it. */
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+ /** Reads `.cruze/approvals.json`; anything unreadable counts as no approvals, and validate reports it. */
16
+ export function parseApprovals(text ) {
17
+ if (text === undefined) return [];
18
+ try {
19
+ const parsed = JSON.parse(text);
20
+ const list = (parsed ).approvals;
21
+ if (!Array.isArray(list)) return [];
22
+ return list.filter((r) => typeof r?.path === "string" && typeof r?.hash === "string" && typeof r?.approvedAt === "string" && typeof r?.by === "string");
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ export function serializeApprovals(records ) {
29
+ const sorted = [...records].sort((a, b) => a.path.localeCompare(b.path));
30
+ return `${JSON.stringify({ version: 2, approvals: sorted }, null, 2)}\n`;
31
+ }
32
+
33
+ /** The records with this one replacing any earlier approval of the same document. */
34
+ export function withApproval(records , record ) {
35
+ return [...records.filter((r) => r.path !== record.path), record];
36
+ }
37
+
38
+ /**
39
+ * The hash an approval records. A feature's `## Decisions` is the agent's record of what it chose,
40
+ * which build and review add to, so it stays out: the user's approval covers the rest.
41
+ */
42
+ export function approvalHash(path , doc ) {
43
+ return designHash(doc, slugOfFeaturePath(path) === undefined ? [] : ["Decisions"]);
44
+ }
45
+
46
+ /** Whether the document still reads as it did when it was approved. */
47
+ export function approvalState(records , path , doc ) {
48
+ const record = records.find((r) => r.path === path);
49
+ if (record === undefined) return "unapproved";
50
+ return record.hash === approvalHash(path, doc) ? "approved" : "edited";
51
+ }
@@ -1,36 +1,29 @@
1
1
 
2
2
  import { matchesAny } from "../glob.js";
3
3
  import { budgetedText } from "./budget_text.js";
4
- import { decisionCitationsInComments } from "./decision_comments.js";
5
4
  import { importsOf, resolveImport } from "./imports.js";
6
5
  import { countTypes } from "./type_count.js";
7
6
 
8
7
 
9
8
 
10
9
 
11
-
10
+
12
11
 
13
12
 
14
13
 
15
-
16
-
17
14
 
18
15
 
19
16
 
20
17
 
21
18
 
22
19
 
23
-
24
-
25
-
26
-
27
20
 
28
21
 
29
22
 
30
23
 
31
24
  /** Enforces the architecture's layer rules and the file budgets on source code. */
32
25
  export function checkSource(input ) {
33
- const { config, sources, modulePaths } = input;
26
+ const { config, sources } = input;
34
27
  const all = [...sources.keys()];
35
28
  const targets = input.only ?? all;
36
29
  const layerOf = (path ) => config.layers.find((layer) => matchesAny(path, layer.paths));
@@ -67,32 +60,10 @@ export function checkSource(input ) {
67
60
  if (!excepted && isRustTestFileInSource(path)) {
68
61
  findings.push({ path, rule: "test-placement", severity: "warning", message: "Rust tests that leave their inline module go in the crate's tests/ directory, not under src/" });
69
62
  }
70
- if (modulePaths.length > 0 && !insideModuleMap(path, modulePaths)) {
71
- findings.push({ path, rule: "outside-module-map", severity: "warning", message: "the file is outside every module in the architecture's module map" });
72
- }
73
- findings.push(...decisionFindings(path, text));
74
- }
75
- for (const [path, text] of input.tests ?? []) {
76
- if (!sources.has(path) && (input.only === undefined || input.only.includes(path))) findings.push(...decisionFindings(path, text));
77
63
  }
78
64
  return findings;
79
65
  }
80
66
 
81
- function decisionFindings(path , text ) {
82
- return decisionCitationsInComments(path, text).map((citation) => ({
83
- path,
84
- line: citation.line + 1,
85
- rule: "decision-citation",
86
- severity: "warning",
87
- message: `the comment cites ${citation.text}, a decision number a reader of the code can't follow; say what was decided, or cite the element or ADR that holds it`,
88
- }));
89
- }
90
-
91
- /** Whether a module's Path covers the file: a directory path ending in `/` covers everything under it, any other names one file. */
92
- export function insideModuleMap(path , modulePaths ) {
93
- return modulePaths.some((module) => (module.endsWith("/") ? path.startsWith(module) : path === module));
94
- }
95
-
96
67
  /** A Rust file of tests split out of a module: `tests.rs`, `*_tests.rs`, or anything under a `tests/` folder inside `src/`. */
97
68
  function isRustTestFileInSource(path ) {
98
69
  if (!path.endsWith(".rs")) return false;
@@ -5,10 +5,7 @@ import { parse as parseYaml } from "yaml";
5
5
 
6
6
 
7
7
 
8
-
9
-
10
8
 
11
-
12
9
 
13
10
 
14
11
 
@@ -17,34 +14,12 @@ import { parse as parseYaml } from "yaml";
17
14
 
18
15
 
19
16
 
20
-
21
-
22
17
 
23
18
 
24
19
 
25
20
 
26
-
27
-
28
-
29
-
30
-
21
+
31
22
 
32
-
33
- /** Which review findings the user decides: only the exceptions, or every one. */
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
- export const DEFAULT_CHANGE_LIMITS = { minBuilds: 3, maxScenarios: 30, maxTasks: 25, exceptions: [] };
48
23
 
49
24
 
50
25
 
@@ -62,17 +37,14 @@ export function parseConfig(text ) {
62
37
  } catch (error) {
63
38
  return { config: null, problems: [`not valid YAML: ${error instanceof Error ? error.message : String(error)}`] };
64
39
  }
65
- const problems = [];
66
40
  const root = asRecord(raw);
67
41
  if (root === null) return { config: null, problems: ["the file must be a YAML mapping"] };
68
-
69
- if (root["version"] !== 1) problems.push("version must be 1");
42
+ if (root["version"] === 1) return { config: null, problems: ["this config was created by an older Cruze, whose documents this release doesn't read; start the project's Cruze documents again with cruze init in a fresh checkout"] };
43
+ const problems = [];
44
+ if (root["version"] !== 2) problems.push("version must be 2");
70
45
  const cruze = root["cruze"];
71
- if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.2\"");
72
- const standards = root["standards"];
73
- if (standards !== undefined && (typeof standards !== "string" || standards.trim() === "")) problems.push("standards must be a version string, such as \"0.0.2\"");
46
+ if (cruze !== undefined && (typeof cruze !== "string" || cruze.trim() === "")) problems.push("cruze must be a version string, such as \"0.0.14\"");
74
47
  if (typeof root["project"] !== "string" || root["project"] === "") problems.push("project must be a non-empty string");
75
- if (root["tracker"] !== "markdown") problems.push("tracker must be markdown");
76
48
  const source = stringList(root["source"], "source", problems);
77
49
  const tests = stringList(root["tests"], "tests", problems);
78
50
 
@@ -104,71 +76,23 @@ export function parseConfig(text ) {
104
76
  }
105
77
  }
106
78
 
107
- const commands = {};
108
- const rawCommands = root["commands"];
109
- if (rawCommands !== undefined && rawCommands !== null) {
110
- const record = asRecord(rawCommands);
111
- if (record === null) problems.push("commands must be a mapping of names to commands");
112
- for (const [name, command] of Object.entries(record ?? {})) {
113
- if (typeof command === "string" && command.trim() !== "") commands[name] = command;
114
- else problems.push(`commands.${name} must be a command string`);
115
- }
116
- }
117
79
  const review = asRecord(root["review"]) ?? {};
118
80
  const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
119
81
  if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
120
- const decide = review["decide"] ?? "exceptions";
121
- if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
122
- const limits = asRecord(root["changes"]) ?? {};
123
- const changes = {
124
- minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
125
- maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
126
- maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
127
- exceptions: (Array.isArray(limits["exceptions"]) ? limits["exceptions"] : []).flatMap((entry , i ) => {
128
- const record = asRecord(entry);
129
- if (typeof record?.["item"] !== "string" || typeof record["reason"] !== "string" || record["reason"].trim() === "") {
130
- problems.push(`changes.exceptions[${i}] needs an item and a non-empty reason`);
131
- return [];
132
- }
133
- return [{ item: record["item"], reason: record["reason"] }];
134
- }),
135
- };
136
-
137
- const references = (Array.isArray(root["references"]) ? root["references"] : root["references"] === undefined ? [] : [null]).flatMap((entry , i ) => {
138
- const record = asRecord(entry);
139
- if (typeof record?.["path"] !== "string" || record["path"].trim() === "" || typeof record["use"] !== "string" || record["use"].trim() === "") {
140
- problems.push(`references[${i}] needs a path and a non-empty use`);
141
- return [];
142
- }
143
- return [{ path: record["path"], use: record["use"] }];
144
- });
145
82
 
146
83
  const config = {
147
- version: 1,
84
+ version: 2,
148
85
  ...(typeof cruze === "string" && cruze.trim() !== "" ? { cruze } : {}),
149
- ...(typeof standards === "string" && standards.trim() !== "" ? { standards } : {}),
150
86
  project: typeof root["project"] === "string" ? root["project"] : "",
151
- tracker: "markdown",
152
87
  source,
153
88
  tests,
154
- check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
89
+ check: { maxLines: maxLines ?? 600, maxTypes: maxTypes ?? 12, exceptions },
155
90
  layers,
156
- commands,
157
- review: {
158
- ...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
159
- decide: decide === "all" ? "all" : "exceptions",
160
- },
161
- changes,
162
- references,
91
+ review: reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand },
163
92
  };
164
93
  return { config, problems };
165
94
  }
166
95
 
167
- /** The first layer whose paths match the file, as the formats define. */
168
- export function layerOf(config , path , matches ) {
169
- return config.layers.find((layer) => matches(path, layer.paths));
170
- }
171
-
172
96
  function asRecord(value ) {
173
97
  return value !== null && typeof value === "object" && !Array.isArray(value) ? (value ) : null;
174
98
  }
@@ -1,43 +1,29 @@
1
1
  import { createHash } from "node:crypto";
2
- import { elementLines, isStatusLine, } from "./elements.js";
3
-
2
+ import { findSection, } from "./markdown.js";
4
3
 
5
4
  /**
6
- * Hashes cover design content only. Managed blocks and status lines are
7
- * excluded, so bookkeeping by the CLI never makes a design look edited.
5
+ * Hashes cover design content only. A feature's `- Status:` line is bookkeeping, so marking a
6
+ * feature built never makes its approved design look edited.
8
7
  */
9
8
 
10
- export function sha256(text ) {
11
- return `sha256:${createHash("sha256").update(text).digest("hex")}`;
12
- }
9
+ const STATUS_LINE = /^- Status: \S+\s*$/;
13
10
 
14
- /** Hash of a whole document's design content. */
15
- export function designHash(doc ) {
16
- const kept = doc.lines.filter((line, i) => !doc.inManaged[i] && (doc.inFence[i] || !isStatusLine(line)));
17
- return sha256(normalize(kept));
11
+ export function isStatusLine(line ) {
12
+ return STATUS_LINE.test(line);
18
13
  }
19
14
 
20
- /**
21
- * Hash of one element, independent of where it sits: heading levels are made
22
- * relative to the element and a delta operation prefix is dropped, so an element
23
- * hashes the same in a delta and after it is merged into a living document.
24
- */
25
- export function elementHash(doc , element ) {
26
- if (element.form === "row") return sha256(normalize(elementLines(doc, element)));
27
- const kept = [];
28
- for (let i = element.start; i < element.end; i++) {
29
- const line = doc.lines[i] ?? "";
30
- if (doc.inManaged[i] || (!doc.inFence[i] && isStatusLine(line))) continue;
31
- kept.push(doc.inFence[i] ? line : relativeHeading(line, element.level, i === element.start));
32
- }
33
- return sha256(normalize(kept));
15
+ export function sha256(text ) {
16
+ return `sha256:${createHash("sha256").update(text).digest("hex")}`;
34
17
  }
35
18
 
36
- function relativeHeading(line , baseLevel , isOwnHeading ) {
37
- const match = /^(#{1,6}) (.*)$/.exec(line);
38
- if (match?.[1] === undefined || match[2] === undefined) return line;
39
- const text = isOwnHeading ? match[2].replace(/^(ADDED|MODIFIED|REMOVED) /, "") : match[2];
40
- return `${"#".repeat(match[1].length - baseLevel + 1)} ${text}`;
19
+ /** Hash of a document's design content, leaving out the `##` sections named. */
20
+ export function designHash(doc , leftOut = []) {
21
+ const skipped = leftOut.flatMap((title) => {
22
+ const section = findSection(doc, title);
23
+ return section === null ? [] : [[section.heading.line, section.end] ];
24
+ });
25
+ const kept = doc.lines.filter((line, i) => !skipped.some(([start, end]) => i >= start && i < end) && (doc.inFence[i] || !isStatusLine(line)));
26
+ return sha256(normalize(kept));
41
27
  }
42
28
 
43
29
  function normalize(lines ) {
@@ -1,39 +1,25 @@
1
- /** Element IDs: `<KIND>-<name>` or `<KIND>-<scope>.<name>`, named and never reused. */
2
-
3
- export const KINDS = ["GOAL", "PRINCIPLE", "LATER", "CTX", "ENT", "UC", "PORT", "ADP", "FLOW", "MOD", "RULE", "VIEW", "XC", "REQ", "SCN", "ADR"] ;
4
-
5
-
6
- /** Kinds of the vision's row elements: goals, the principles every design follows, and what must stay possible later. */
7
- export const VISION_KINDS = ["GOAL", "PRINCIPLE", "LATER"];
8
- /** Kinds that describe code, and so carry a managed planned/built status. */
9
- export const CODE_KINDS = ["ENT", "UC", "PORT", "ADP", "FLOW", "MOD"];
10
- /** Kinds whose elements carry a managed status: code kinds and scenarios. */
11
- export const STATUS_KINDS = [...CODE_KINDS, "SCN"];
12
- /** Kinds that must name their context or capability as a scope. */
13
- export const SCOPED_KINDS = ["ENT", "UC", "PORT", "ADP", "FLOW", "REQ", "SCN"];
1
+ /** Scenario IDs: `SCN-<feature>.<name>`, lowercase words joined by single hyphens, named and never reused. */
14
2
 
15
3
  const WORD = "[a-z0-9]+(?:-[a-z0-9]+)*";
16
- export const ID_PATTERN = `(?:${KINDS.join("|")})-${WORD}(?:\\.${WORD})?`;
17
-
18
- const EXACT = new RegExp(`^${ID_PATTERN}$`);
19
- const ANYWHERE = new RegExp(`(?<![A-Za-z0-9_-])${ID_PATTERN}(?![A-Za-z0-9_-])`, "g");
4
+ export const SLUG = new RegExp(`^${WORD}$`);
5
+ const SCENARIO = `SCN-${WORD}\\.${WORD}`;
6
+ const EXACT = new RegExp(`^${SCENARIO}$`);
7
+ const ANYWHERE = new RegExp(`(?<![A-Za-z0-9_-])${SCENARIO}(?![A-Za-z0-9_.-])`, "g");
20
8
 
21
- export function isId(text ) {
9
+ export function isScenarioId(text ) {
22
10
  return EXACT.test(text);
23
11
  }
24
12
 
25
- export function kindOf(id ) {
26
- return id.slice(0, id.indexOf("-")) ;
13
+ export function isSlug(text ) {
14
+ return SLUG.test(text);
27
15
  }
28
16
 
29
- /** The context or capability an ID belongs to, if it has one. */
30
- export function scopeOf(id ) {
31
- const rest = id.slice(id.indexOf("-") + 1);
32
- const dot = rest.indexOf(".");
33
- return dot === -1 ? undefined : rest.slice(0, dot);
17
+ /** The feature a scenario belongs to. */
18
+ export function featureOfScenario(id ) {
19
+ return id.slice(4, id.indexOf("."));
34
20
  }
35
21
 
36
- /** Every ID mentioned in the text, once each, in order of first mention. */
37
- export function findIds(text ) {
22
+ /** Every scenario ID mentioned in the text, once each, in order of first mention. */
23
+ export function findScenarioIds(text ) {
38
24
  return [...new Set(text.match(ANYWHERE) ?? [])];
39
25
  }
@@ -0,0 +1,41 @@
1
+ import { approvalState } from "./approvals.js";
2
+ import { PATHS } from "./project/layout.js";
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+ /**
15
+ * The step to run next, from the documents alone. Work in progress comes first, in release order,
16
+ * then the first release feature not yet shaped whose needs are all built.
17
+ */
18
+ export function nextStep(docs ) {
19
+ if (docs.config === undefined) return { step: "init", reason: `${PATHS.config} is missing` };
20
+ if (docs.vision === undefined) return { step: "envision", reason: "the project has no vision yet" };
21
+ if (approvalState(docs.approvals, PATHS.vision, docs.vision) !== "approved") return { step: "envision", reason: "the vision isn't approved as it stands" };
22
+ if (docs.architecture === undefined) return { step: "envision", reason: "the project has no architecture map yet" };
23
+ if (approvalState(docs.approvals, PATHS.architecture, docs.architecture) !== "approved") return { step: "envision", reason: "the architecture map isn't approved as it stands" };
24
+
25
+ const order = [...docs.release.map((r) => r.slug), ...docs.features.map((f) => f.slug).filter((slug) => !docs.release.some((r) => r.slug === slug))];
26
+ for (const slug of order) {
27
+ const feature = docs.features.find((f) => f.slug === slug);
28
+ if (feature === undefined || feature.status === "built") continue;
29
+ const state = approvalState(docs.approvals, feature.path, feature.doc);
30
+ if (state === "unapproved") return { step: "shape", target: slug, reason: `${slug} is shaped but not approved` };
31
+ if (state === "edited") return { step: "shape", target: slug, reason: `${slug} changed since its approval; show the user what changed and approve it again` };
32
+ return { step: "build", target: slug, reason: `${slug} is approved and not built` };
33
+ }
34
+
35
+ const built = new Set(docs.features.filter((f) => f.status === "built").map((f) => f.slug));
36
+ const pending = docs.release.filter((r) => !built.has(r.slug));
37
+ const ready = pending.find((r) => !docs.features.some((f) => f.slug === r.slug) && r.needs.every((need) => built.has(need)));
38
+ if (ready !== undefined) return { step: "shape", target: ready.slug, reason: ready.needs.length === 0 ? `${ready.slug} is next in the release` : `${ready.slug} is next in the release, and everything it needs is built` };
39
+ if (pending.length > 0) return { step: "envision", reason: `no remaining release feature has all its needs built (${pending.map((r) => r.slug).join(", ")}); fix the Needs column` };
40
+ return { step: "envision", reason: docs.release.length === 0 ? "the vision's Release table lists no features" : "every release feature is built; plan the next release" };
41
+ }
@@ -0,0 +1,71 @@
1
+ import { isStatusLine } from "../hashing.js";
2
+ import { findScenarioIds, isScenarioId } from "../ids.js";
3
+ import { findSection, parseMarkdown, } from "../markdown.js";
4
+ import { featurePath } from "./layout.js";
5
+
6
+ /** A feature document, docs/features/<slug>.md: one slice of behaviour, its public API and its stack sketch. */
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+ export const FEATURE_STATUSES = ["shaped", "built"];
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+ export const FEATURE_SECTIONS = ["Intent", "Behaviour", "Public API", "Stack sketch", "Decisions"] ;
29
+
30
+ export function readFeature(slug , text ) {
31
+ const doc = parseMarkdown(text);
32
+ return { slug, path: featurePath(slug), text, doc, status: statusOf(doc), scenarios: scenariosOf(doc) };
33
+ }
34
+
35
+ /** The feature's `- Status:` line, the first one outside code. */
36
+ export function statusLine(doc ) {
37
+ const index = doc.lines.findIndex((line, i) => !doc.inFence[i] && isStatusLine(line));
38
+ if (index === -1) return undefined;
39
+ return { line: index, value: (doc.lines[index] ?? "").replace("- Status:", "").trim() };
40
+ }
41
+
42
+ function statusOf(doc ) {
43
+ const value = statusLine(doc)?.value;
44
+ return (FEATURE_STATUSES ).includes(value ?? "") ? (value ) : undefined;
45
+ }
46
+
47
+ /** Scenario headings under `## Behaviour`: any heading whose text starts with a scenario ID and a colon. */
48
+ function scenariosOf(doc ) {
49
+ const section = findSection(doc, "Behaviour");
50
+ if (section === null) return [];
51
+ return doc.headings
52
+ .filter((h) => h.line >= section.start && h.line < section.end)
53
+ .flatMap((h) => {
54
+ const match = /^(\S+): (.+)$/.exec(h.text);
55
+ if (match?.[1] === undefined || match[2] === undefined || !isScenarioId(match[1])) return [];
56
+ return [{ id: match[1], title: match[2], line: h.line }];
57
+ });
58
+ }
59
+
60
+ /** Scenario-like IDs under `## Behaviour` that aren't a proper scenario heading, for validate to report. */
61
+ export function strayScenarioIds(feature ) {
62
+ const section = findSection(feature.doc, "Behaviour");
63
+ if (section === null) return [];
64
+ const headed = new Set(feature.scenarios.map((s) => s.line));
65
+ const stray = [];
66
+ for (let i = section.start; i < section.end; i++) {
67
+ if (headed.has(i) || feature.doc.inFence[i] || !/^#{1,6} /.test(feature.doc.lines[i] ?? "")) continue;
68
+ for (const id of findScenarioIds(feature.doc.lines[i] ?? "")) stray.push({ id, line: i });
69
+ }
70
+ return stray;
71
+ }