@printpractical/cruze 0.0.13 → 0.0.15

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 (308) 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/install.js +1 -0
  7. package/dist/adapters/inbound/cli/commands/review.js +2 -9
  8. package/dist/adapters/inbound/cli/commands/status.js +16 -63
  9. package/dist/adapters/inbound/cli/commands.js +10 -37
  10. package/dist/adapters/inbound/cli/parse_command.js +0 -54
  11. package/dist/adapters/outbound/git_repository.js +2 -71
  12. package/dist/app/ports/repository.js +0 -12
  13. package/dist/app/project_context.js +15 -25
  14. package/dist/app/use_cases/approve_document.js +29 -0
  15. package/dist/app/use_cases/check_code.js +21 -56
  16. package/dist/app/use_cases/install_skills.js +7 -1
  17. package/dist/app/use_cases/new_document.js +25 -0
  18. package/dist/app/use_cases/project_status.js +43 -67
  19. package/dist/app/use_cases/run_review.js +11 -30
  20. package/dist/app/use_cases/validate_project.js +8 -5
  21. package/dist/domain/approvals.js +51 -0
  22. package/dist/domain/check/check_source.js +3 -32
  23. package/dist/domain/check/imports.js +3 -3
  24. package/dist/domain/check/rust_imports.js +25 -3
  25. package/dist/domain/config.js +8 -84
  26. package/dist/domain/hashing.js +16 -30
  27. package/dist/domain/ids.js +13 -27
  28. package/dist/domain/next_step.js +41 -0
  29. package/dist/domain/project/features.js +71 -0
  30. package/dist/domain/project/layout.js +11 -27
  31. package/dist/domain/project/project_docs.js +48 -0
  32. package/dist/domain/project/release.js +30 -0
  33. package/dist/domain/scaffold.js +0 -1
  34. package/dist/domain/skill_budget.js +4 -4
  35. package/dist/domain/trace.js +8 -25
  36. package/dist/domain/validation/document_rules.js +65 -0
  37. package/dist/domain/validation/release_rules.js +45 -0
  38. package/dist/domain/validation/review_rules.js +21 -0
  39. package/dist/domain/validation/validate_project.js +20 -27
  40. package/dist/domain/versions/cruze_version.js +3 -9
  41. package/dist/main.js +1 -1
  42. package/package.json +1 -1
  43. package/skills/asking/SKILL.md +51 -0
  44. package/skills/behavioural-testing/SKILL.md +14 -16
  45. package/skills/behavioural-testing/examples.md +1 -2
  46. package/skills/behavioural-testing/fakes.md +2 -2
  47. package/skills/build/SKILL.md +34 -38
  48. package/skills/dependency-approval/SKILL.md +34 -31
  49. package/skills/envision/SKILL.md +23 -18
  50. package/skills/envision/architecture.md +16 -0
  51. package/skills/envision/breakdown.md +14 -0
  52. package/skills/envision/possibilities.md +6 -7
  53. package/skills/fix/SKILL.md +34 -0
  54. package/skills/formats/SKILL.md +39 -87
  55. package/skills/formats/config.md +47 -0
  56. package/skills/formats/templates/architecture.md +18 -74
  57. package/skills/formats/templates/feature.md +18 -34
  58. package/skills/formats/templates/vision.md +15 -27
  59. package/skills/hexagonal-design/SKILL.md +11 -11
  60. package/skills/hexagonal-design/contracts.md +3 -3
  61. package/skills/hexagonal-design/fundamentals.md +1 -1
  62. package/skills/hexagonal-design/languages/cpp.md +1 -1
  63. package/skills/hexagonal-design/languages/rust.md +1 -1
  64. package/skills/hexagonal-design/languages/swift.md +2 -2
  65. package/skills/hexagonal-design/runtime.md +2 -2
  66. package/skills/research/SKILL.md +4 -4
  67. package/skills/research/evaluation.md +1 -1
  68. package/skills/review/SKILL.md +27 -0
  69. package/skills/roles/SKILL.md +8 -59
  70. package/skills/roles/code-reviewer.md +22 -32
  71. package/skills/roles/researcher.md +3 -3
  72. package/skills/shape/SKILL.md +37 -0
  73. package/skills/shape/self-check.md +12 -0
  74. package/src/adapters/inbound/cli/cli_context.ts +0 -27
  75. package/src/adapters/inbound/cli/commands/code.ts +5 -18
  76. package/src/adapters/inbound/cli/commands/documents.ts +11 -37
  77. package/src/adapters/inbound/cli/commands/init.ts +1 -1
  78. package/src/adapters/inbound/cli/commands/install.ts +1 -0
  79. package/src/adapters/inbound/cli/commands/review.ts +2 -9
  80. package/src/adapters/inbound/cli/commands/status.ts +16 -63
  81. package/src/adapters/inbound/cli/commands.ts +10 -37
  82. package/src/adapters/inbound/cli/parse_command.ts +0 -54
  83. package/src/adapters/outbound/git_repository.ts +2 -71
  84. package/src/app/ports/repository.ts +0 -12
  85. package/src/app/project_context.ts +15 -25
  86. package/src/app/use_cases/approve_document.ts +29 -0
  87. package/src/app/use_cases/check_code.ts +21 -56
  88. package/src/app/use_cases/install_skills.ts +7 -1
  89. package/src/app/use_cases/new_document.ts +25 -0
  90. package/src/app/use_cases/project_status.ts +49 -73
  91. package/src/app/use_cases/run_review.ts +11 -30
  92. package/src/app/use_cases/validate_project.ts +8 -5
  93. package/src/domain/approvals.ts +51 -0
  94. package/src/domain/check/check_source.ts +3 -32
  95. package/src/domain/check/imports.ts +3 -3
  96. package/src/domain/check/rust_imports.ts +25 -3
  97. package/src/domain/config.ts +9 -85
  98. package/src/domain/hashing.ts +16 -30
  99. package/src/domain/ids.ts +13 -27
  100. package/src/domain/next_step.ts +41 -0
  101. package/src/domain/project/features.ts +71 -0
  102. package/src/domain/project/layout.ts +11 -27
  103. package/src/domain/project/project_docs.ts +48 -0
  104. package/src/domain/project/release.ts +30 -0
  105. package/src/domain/scaffold.ts +0 -1
  106. package/src/domain/skill_budget.ts +4 -4
  107. package/src/domain/trace.ts +8 -25
  108. package/src/domain/validation/document_rules.ts +65 -0
  109. package/src/domain/validation/release_rules.ts +45 -0
  110. package/src/domain/validation/review_rules.ts +21 -0
  111. package/src/domain/validation/validate_project.ts +21 -28
  112. package/src/domain/versions/cruze_version.ts +3 -9
  113. package/src/main.ts +1 -1
  114. package/templates/init/AGENTS.md +4 -12
  115. package/templates/init/ci.yml +3 -3
  116. package/templates/init/config.yaml +8 -17
  117. package/templates/init/review/claude.yaml +2 -2
  118. package/dist/adapters/inbound/cli/argument_files.js +0 -29
  119. package/dist/adapters/inbound/cli/commands/diff.js +0 -23
  120. package/dist/adapters/inbound/cli/commands/journal.js +0 -32
  121. package/dist/adapters/inbound/cli/commands/progress.js +0 -55
  122. package/dist/adapters/inbound/cli/commands/realign.js +0 -28
  123. package/dist/adapters/inbound/cli/commands/show.js +0 -96
  124. package/dist/app/use_cases/abandon_work.js +0 -42
  125. package/dist/app/use_cases/approve_artifact.js +0 -197
  126. package/dist/app/use_cases/journal_events.js +0 -64
  127. package/dist/app/use_cases/land_change.js +0 -134
  128. package/dist/app/use_cases/new_project_doc.js +0 -48
  129. package/dist/app/use_cases/new_work_item.js +0 -75
  130. package/dist/app/use_cases/realign_done.js +0 -45
  131. package/dist/app/use_cases/realign_status.js +0 -106
  132. package/dist/app/use_cases/record_progress.js +0 -118
  133. package/dist/app/use_cases/show_elements.js +0 -18
  134. package/dist/app/use_cases/show_notes.js +0 -11
  135. package/dist/app/use_cases/show_scope.js +0 -51
  136. package/dist/app/use_cases/upstream_diff.js +0 -55
  137. package/dist/domain/approvals/agent_approval.js +0 -68
  138. package/dist/domain/approvals/evaluate.js +0 -36
  139. package/dist/domain/approvals/fingerprint.js +0 -52
  140. package/dist/domain/approvals/rebase.js +0 -39
  141. package/dist/domain/approvals/records.js +0 -50
  142. package/dist/domain/approvals/upstream_text.js +0 -36
  143. package/dist/domain/architecture_sections.js +0 -23
  144. package/dist/domain/check/decision_comments.js +0 -38
  145. package/dist/domain/decision_citations.js +0 -62
  146. package/dist/domain/edits/agents_file.js +0 -58
  147. package/dist/domain/edits/managed_tables.js +0 -60
  148. package/dist/domain/edits/progress_block.js +0 -33
  149. package/dist/domain/edits/status_lines.js +0 -52
  150. package/dist/domain/elements.js +0 -126
  151. package/dist/domain/journal.js +0 -109
  152. package/dist/domain/land/bookkeeping.js +0 -54
  153. package/dist/domain/land/merge_plan.js +0 -137
  154. package/dist/domain/land/text_ops.js +0 -66
  155. package/dist/domain/line_diff.js +0 -56
  156. package/dist/domain/project/artifact_ref.js +0 -42
  157. package/dist/domain/project/deltas.js +0 -49
  158. package/dist/domain/project/element_lookup.js +0 -124
  159. package/dist/domain/project/living_model.js +0 -55
  160. package/dist/domain/project/new_items.js +0 -48
  161. package/dist/domain/project/notes.js +0 -74
  162. package/dist/domain/project/plan_parts.js +0 -111
  163. package/dist/domain/project/progress.js +0 -43
  164. package/dist/domain/project/project_view.js +0 -72
  165. package/dist/domain/project/roadmap.js +0 -43
  166. package/dist/domain/project/scope_view.js +0 -134
  167. package/dist/domain/project/work_items.js +0 -68
  168. package/dist/domain/status/next_step.js +0 -181
  169. package/dist/domain/status/pending_rethink.js +0 -22
  170. package/dist/domain/status/verification.js +0 -29
  171. package/dist/domain/status/work_status.js +0 -125
  172. package/dist/domain/validation/decision_rules.js +0 -76
  173. package/dist/domain/validation/delta_rules.js +0 -81
  174. package/dist/domain/validation/element_rules.js +0 -104
  175. package/dist/domain/validation/future_rules.js +0 -40
  176. package/dist/domain/validation/living_rules.js +0 -99
  177. package/dist/domain/validation/plan_rules.js +0 -124
  178. package/dist/domain/validation/project_rules.js +0 -115
  179. package/dist/domain/validation/size_rules.js +0 -45
  180. package/dist/domain/validation/vision_rules.js +0 -41
  181. package/dist/domain/validation/work_item_rules.js +0 -175
  182. package/dist/domain/versions/realign_notes.js +0 -72
  183. package/dist/domain/versions/rule_versions.js +0 -29
  184. package/dist/domain/versions/scheduled_exceptions.js +0 -41
  185. package/skills/about/SKILL.md +0 -21
  186. package/skills/architect/SKILL.md +0 -49
  187. package/skills/architect/feature.md +0 -40
  188. package/skills/architect/project.md +0 -71
  189. package/skills/architect/tweak.md +0 -22
  190. package/skills/architect/walkthrough.md +0 -35
  191. package/skills/domain-language/SKILL.md +0 -37
  192. package/skills/envision/feature.md +0 -25
  193. package/skills/envision/intake.md +0 -28
  194. package/skills/envision/project.md +0 -24
  195. package/skills/explore/SKILL.md +0 -39
  196. package/skills/formats/reference/architecture.md +0 -84
  197. package/skills/formats/reference/config.md +0 -80
  198. package/skills/formats/reference/durable-docs.md +0 -74
  199. package/skills/formats/reference/specs.md +0 -65
  200. package/skills/formats/reference/work-items.md +0 -100
  201. package/skills/formats/templates/adr.md +0 -7
  202. package/skills/formats/templates/change.md +0 -29
  203. package/skills/formats/templates/glossary.md +0 -8
  204. package/skills/formats/templates/roadmap.md +0 -32
  205. package/skills/formats/templates/spec.md +0 -11
  206. package/skills/formats/templates/standalone-change.md +0 -46
  207. package/skills/grilling/SKILL.md +0 -90
  208. package/skills/grilling/illustrating.md +0 -38
  209. package/skills/improve/SKILL.md +0 -51
  210. package/skills/land/SKILL.md +0 -48
  211. package/skills/next/SKILL.md +0 -31
  212. package/skills/plan/SKILL.md +0 -62
  213. package/skills/plan/tasks.md +0 -35
  214. package/skills/plan/test-plan.md +0 -14
  215. package/skills/realign/SKILL.md +0 -37
  216. package/skills/realign/notes/0.0.10.md +0 -131
  217. package/skills/realign/notes/0.0.11.md +0 -15
  218. package/skills/realign/notes/0.0.12.md +0 -31
  219. package/skills/realign/notes/0.0.13.md +0 -11
  220. package/skills/realign/notes/0.0.2.md +0 -69
  221. package/skills/realign/notes/0.0.3.md +0 -11
  222. package/skills/realign/notes/0.0.4.md +0 -27
  223. package/skills/realign/notes/0.0.5.md +0 -38
  224. package/skills/realign/notes/0.0.6.md +0 -44
  225. package/skills/realign/notes/0.0.7.md +0 -11
  226. package/skills/realign/notes/0.0.8.md +0 -97
  227. package/skills/realign/notes/0.0.9.md +0 -59
  228. package/skills/realign/notes.md +0 -40
  229. package/skills/rethink/SKILL.md +0 -62
  230. package/skills/rethink/levels.md +0 -54
  231. package/skills/roadmap/SKILL.md +0 -54
  232. package/skills/roles/auditor.md +0 -35
  233. package/skills/roles/design-reviewer.md +0 -43
  234. package/skills/roles/design-rubric.md +0 -41
  235. package/skills/roles/plan-rubric.md +0 -11
  236. package/skills/roles/surveyor.md +0 -41
  237. package/skills/roles/verifier.md +0 -42
  238. package/skills/triage/SKILL.md +0 -37
  239. package/skills/verify/SKILL.md +0 -45
  240. package/skills/verify/manual-test.md +0 -23
  241. package/src/adapters/inbound/cli/argument_files.ts +0 -29
  242. package/src/adapters/inbound/cli/commands/diff.ts +0 -23
  243. package/src/adapters/inbound/cli/commands/journal.ts +0 -32
  244. package/src/adapters/inbound/cli/commands/progress.ts +0 -55
  245. package/src/adapters/inbound/cli/commands/realign.ts +0 -28
  246. package/src/adapters/inbound/cli/commands/show.ts +0 -96
  247. package/src/app/use_cases/abandon_work.ts +0 -42
  248. package/src/app/use_cases/approve_artifact.ts +0 -197
  249. package/src/app/use_cases/journal_events.ts +0 -64
  250. package/src/app/use_cases/land_change.ts +0 -134
  251. package/src/app/use_cases/new_project_doc.ts +0 -48
  252. package/src/app/use_cases/new_work_item.ts +0 -75
  253. package/src/app/use_cases/realign_done.ts +0 -45
  254. package/src/app/use_cases/realign_status.ts +0 -106
  255. package/src/app/use_cases/record_progress.ts +0 -118
  256. package/src/app/use_cases/show_elements.ts +0 -18
  257. package/src/app/use_cases/show_notes.ts +0 -11
  258. package/src/app/use_cases/show_scope.ts +0 -51
  259. package/src/app/use_cases/upstream_diff.ts +0 -55
  260. package/src/domain/approvals/agent_approval.ts +0 -68
  261. package/src/domain/approvals/evaluate.ts +0 -36
  262. package/src/domain/approvals/fingerprint.ts +0 -52
  263. package/src/domain/approvals/rebase.ts +0 -39
  264. package/src/domain/approvals/records.ts +0 -50
  265. package/src/domain/approvals/upstream_text.ts +0 -36
  266. package/src/domain/architecture_sections.ts +0 -23
  267. package/src/domain/check/decision_comments.ts +0 -38
  268. package/src/domain/decision_citations.ts +0 -62
  269. package/src/domain/edits/agents_file.ts +0 -58
  270. package/src/domain/edits/managed_tables.ts +0 -60
  271. package/src/domain/edits/progress_block.ts +0 -33
  272. package/src/domain/edits/status_lines.ts +0 -52
  273. package/src/domain/elements.ts +0 -126
  274. package/src/domain/journal.ts +0 -109
  275. package/src/domain/land/bookkeeping.ts +0 -54
  276. package/src/domain/land/merge_plan.ts +0 -137
  277. package/src/domain/land/text_ops.ts +0 -66
  278. package/src/domain/line_diff.ts +0 -56
  279. package/src/domain/project/artifact_ref.ts +0 -42
  280. package/src/domain/project/deltas.ts +0 -49
  281. package/src/domain/project/element_lookup.ts +0 -124
  282. package/src/domain/project/living_model.ts +0 -55
  283. package/src/domain/project/new_items.ts +0 -48
  284. package/src/domain/project/notes.ts +0 -74
  285. package/src/domain/project/plan_parts.ts +0 -111
  286. package/src/domain/project/progress.ts +0 -43
  287. package/src/domain/project/project_view.ts +0 -72
  288. package/src/domain/project/roadmap.ts +0 -43
  289. package/src/domain/project/scope_view.ts +0 -134
  290. package/src/domain/project/work_items.ts +0 -68
  291. package/src/domain/status/next_step.ts +0 -181
  292. package/src/domain/status/pending_rethink.ts +0 -22
  293. package/src/domain/status/verification.ts +0 -29
  294. package/src/domain/status/work_status.ts +0 -125
  295. package/src/domain/validation/decision_rules.ts +0 -76
  296. package/src/domain/validation/delta_rules.ts +0 -81
  297. package/src/domain/validation/element_rules.ts +0 -104
  298. package/src/domain/validation/future_rules.ts +0 -40
  299. package/src/domain/validation/living_rules.ts +0 -99
  300. package/src/domain/validation/plan_rules.ts +0 -124
  301. package/src/domain/validation/project_rules.ts +0 -115
  302. package/src/domain/validation/size_rules.ts +0 -45
  303. package/src/domain/validation/vision_rules.ts +0 -41
  304. package/src/domain/validation/work_item_rules.ts +0 -175
  305. package/src/domain/versions/realign_notes.ts +0 -72
  306. package/src/domain/versions/rule_versions.ts +0 -29
  307. package/src/domain/versions/scheduled_exceptions.ts +0 -41
  308. package/templates/init/gitattributes +0 -2
@@ -1,82 +1,58 @@
1
- import { CruzeError } from "../../domain/cruze_error.js";
1
+ import { approvalState, } from "../../domain/approvals.js";
2
2
  import { PATHS } from "../../domain/project/layout.js";
3
- import { readProgress } from "../../domain/project/progress.js";
4
- import { buildProjectView } from "../../domain/project/project_view.js";
5
- import { findWorkItem, } from "../../domain/project/work_items.js";
6
- import { nextSteps, } from "../../domain/status/next_step.js";
7
- import { activeChange, buildGateReasons, projectStatus, touchedIds, } from "../../domain/status/work_status.js";
8
- import { appendJournal, loadView, } from "../project_context.js";
3
+
4
+ import { nextStep, } from "../../domain/next_step.js";
5
+
6
+ import { loadDocs, } from "../project_context.js";
9
7
 
10
-
11
-
8
+
9
+
10
+
12
11
 
13
12
 
14
- export async function showStatus(deps ) {
15
- const branch = await deps.repository.currentBranch();
16
- return { branch, ...projectStatus(await loadView(deps.files), branch) };
17
- }
18
-
19
- /** The step to run next on this branch, and other work that could proceed. */
20
- export async function suggestNext(deps ) {
21
- const branch = await deps.repository.currentBranch();
22
- return { branch, ...nextSteps(await loadView(deps.files), branch) };
23
- }
24
-
25
-
13
+
14
+
26
15
 
16
+
17
+
18
+
19
+
27
20
 
28
21
 
29
-
30
22
 
31
23
 
32
- /**
33
- * The build gate: the change bound to this branch, its feature and the architecture are
34
- * all approved and current. An override passes the gate and is journaled with its reason.
35
- */
36
- export async function checkBuildGate(deps , override ) {
37
- const view = await loadView(deps.files);
38
- const branch = await deps.repository.currentBranch();
39
- const reasons = buildGateReasons(view, branch);
40
- if (reasons.length === 0) return { gate: "build", passed: true, reasons, overridden: false };
41
- if (override === undefined) return { gate: "build", passed: false, reasons, overridden: false };
42
- if (override.trim() === "") throw new CruzeError("override-reason", "an override needs a reason");
43
- const folder = activeChange(view, branch)?.folder ?? ".cruze";
44
- await appendJournal(deps, folder, "override", { gate: "build", reason: override, blocked_by: reasons });
45
- return { gate: "build", passed: true, reasons, overridden: true };
46
- }
47
-
48
-
24
+
25
+
26
+
49
27
 
50
-
51
-
52
28
 
53
29
 
54
- /** In-flight work on other branches that touches the same elements as this branch's change. */
55
- export async function findOverlaps(deps , ref ) {
56
- const view = await loadView(deps.files);
57
- const branch = await deps.repository.currentBranch();
58
- const change = ref === undefined ? activeChange(view, branch) : view.items.find((item) => item.ref === ref);
59
- if (change === undefined) throw new CruzeError("no-active-change", "no change is bound to this branch; name one with --change");
60
- const mine = touchedIds(view, change);
61
- const overlaps = [];
62
- for (const other of await deps.repository.otherBranches()) {
63
- // Landed work is archived, so a branch's work in flight is under features/ and changes/ only.
64
- const inFlight = [...(await deps.repository.filesOnBranch(other, PATHS.featuresDir)), ...(await deps.repository.filesOnBranch(other, PATHS.changesDir))];
65
- const theirs = buildProjectView(new Map([...view.snapshot].filter(([p]) => p.startsWith("docs/")).concat(inFlight)));
66
- // Work in flight on a branch is the change bound to it that hasn't landed, there or here.
67
- const boundThere = theirs.items.filter((i) => {
68
- const bound = readProgress(i.doc).branch;
69
- const here = findWorkItem(view.items, i.ref);
70
- return !landed(i) && (here === undefined || !landed(here)) && i.ref !== change.ref && bound !== undefined && (other === bound || other.endsWith(`/${bound}`));
71
- });
72
- for (const item of boundThere) {
73
- const shared = [...touchedIds(theirs, item)].filter((id) => mine.has(id));
74
- if (shared.length > 0) overlaps.push({ branch: other, item: item.ref, shared: shared.sort() });
75
- }
76
- }
77
- return { change: change.ref, overlaps };
30
+ /** Each document's approval, and each feature's progress, in release order. */
31
+ export async function showStatus(deps ) {
32
+ const docs = await loadDocs(deps.files);
33
+ const state = (path , doc ) => (doc === undefined ? "missing" : approvalState(docs.approvals, path, doc));
34
+ const documents = [
35
+ { path: PATHS.vision, state: state(PATHS.vision, docs.vision) },
36
+ { path: PATHS.architecture, state: state(PATHS.architecture, docs.architecture) },
37
+ ];
38
+ const slugs = [...docs.release.map((r) => r.slug), ...docs.features.map((f) => f.slug).filter((s) => !docs.release.some((r) => r.slug === s))];
39
+ const features = slugs.map((slug) => {
40
+ const feature = docs.features.find((f) => f.slug === slug);
41
+ const row = docs.release.find((r) => r.slug === slug);
42
+ return {
43
+ slug,
44
+ ...(feature === undefined ? {} : { path: feature.path }),
45
+ status: feature === undefined ? "not-shaped" : (feature.status ?? "unknown"),
46
+ approval: feature === undefined ? "missing" : approvalState(docs.approvals, feature.path, feature.doc),
47
+ inRelease: row !== undefined,
48
+ needs: row?.needs ?? [],
49
+ scenarios: feature?.scenarios.length ?? 0,
50
+ };
51
+ });
52
+ return { documents, features, next: nextStep(docs) };
78
53
  }
79
54
 
80
- function landed(item ) {
81
- return item.archived || readProgress(item.doc).landed !== undefined;
55
+ /** The step to run next, and why. */
56
+ export async function suggestNext(deps ) {
57
+ return nextStep(await loadDocs(deps.files));
82
58
  }
@@ -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
- }
@@ -1,10 +1,12 @@
1
1
  import { hasErrors, validateProject } from "../../domain/validation/validate_project.js";
2
2
  import { warning, } from "../../domain/validation/problem.js";
3
3
  import { PATHS } from "../../domain/project/layout.js";
4
+ import { reviewSettingTemplate } from "../../domain/scaffold.js";
5
+ import { reviewCommandProblems } from "../../domain/validation/review_rules.js";
4
6
  import { versionSkew } from "../../domain/versions/cruze_version.js";
5
7
 
6
- import { loadView } from "../project_context.js";
7
8
 
9
+ import { loadDocs } from "../project_context.js";
8
10
 
9
11
 
10
12
 
@@ -13,12 +15,13 @@ import { loadView } from "../project_context.js";
13
15
 
14
16
 
15
17
 
16
- /** Checks every document in the project against the cruze-formats rules, and that its skills match this CLI. */
18
+ /** Checks every document in the project against the cruze-formats rules, that its skills match this CLI, and that its reviewer can run the tests. */
17
19
  export async function validate(deps ) {
18
- const view = await loadView(deps.files);
19
- const config = view.config?.config;
20
+ const docs = await loadDocs(deps.files);
21
+ const config = docs.config?.config;
20
22
  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)])];
23
+ const review = reviewCommandProblems(docs, await deps.bundle.template(reviewSettingTemplate("claude")));
24
+ const problems = [...validateProject(docs), ...(skew === undefined ? [] : [warning(PATHS.config, undefined, "cruze-version", skew.message)]), ...review];
22
25
  return {
23
26
  valid: !hasErrors(problems),
24
27
  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));
@@ -44,7 +37,7 @@ export function checkSource(input ) {
44
37
  findings.push({ path, rule: "no-layer", severity: "warning", message: "the file is in no layer, so its imports are unchecked" });
45
38
  }
46
39
  if (layer !== undefined) {
47
- for (const found of importsOf(path, text)) {
40
+ for (const found of importsOf(path, text, all)) {
48
41
  const target = resolveImport(found, all);
49
42
  const targetLayer = target === undefined ? undefined : layerOf(target);
50
43
  if (target === undefined || targetLayer === undefined || targetLayer.name === layer.name) continue;
@@ -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;
@@ -12,11 +12,11 @@ import { rustImportCandidates } from "./rust_imports.js";
12
12
  const JS = /\.(?:[cm]?[jt]sx?)$/;
13
13
  const JS_EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", "/index.ts", "/index.js"];
14
14
 
15
- /** Imports that point inside the project. Imports of libraries and the standard library are left out. */
16
- export function importsOf(path , text ) {
15
+ /** Imports that point inside the project, among `files`. Imports of libraries and the standard library are left out. */
16
+ export function importsOf(path , text , files ) {
17
17
  if (JS.test(path)) return jsImports(path, text);
18
18
  if (path.endsWith(".py")) return pythonImports(path, text);
19
- if (path.endsWith(".rs")) return rustImportCandidates(path, text).map((i) => ({ ...i, bySuffix: false }));
19
+ if (path.endsWith(".rs")) return rustImportCandidates(path, text, files).map((i) => ({ ...i, bySuffix: false }));
20
20
  if (/\.(?:c|h|cc|cpp|cxx|hh|hpp|hxx)$/.test(path)) return matches(text, /^\s*#\s*include\s+"([^"]+)"/gm, (target) => [target], true);
21
21
  if (path.endsWith(".go")) return goImports(text);
22
22
  if (/\.(?:java|kt)$/.test(path)) return matches(text, /^import\s+(?:static\s+)?([\w.]+)(?:\.\*)?;?\s*$/gm, (t) => [`${t.replace(/\./g, "/")}.java`, `${t.replace(/\./g, "/")}.kt`], true);
@@ -1,10 +1,14 @@
1
- /** Resolves Rust `use` paths inside a crate to candidate source files. */
1
+ /** Resolves Rust `use` paths, inside a crate and between a workspace's crates, to candidate source files. */
2
2
 
3
- /** Candidate files for each `use crate::…`, `use super::…` and `use self::…` path in a Rust file. */
4
- export function rustImportCandidates(path , text ) {
3
+ /**
4
+ * Candidate files for each `use crate::…`, `use super::…` and `use self::…` path in a Rust file,
5
+ * and for each `use other_crate::…` that names a library crate among `files`.
6
+ */
7
+ export function rustImportCandidates(path , text , files ) {
5
8
  const root = crateRoot(path);
6
9
  if (root === null) return [];
7
10
  const own = moduleSegments(path, root);
11
+ const crates = libraryCrates(files);
8
12
  const results = [];
9
13
  const pattern = /^\s*(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/gms;
10
14
  for (const match of text.matchAll(pattern)) {
@@ -12,11 +16,29 @@ export function rustImportCandidates(path , text )
12
16
  for (const segments of expandUse((match[1] ?? "").replace(/\s+/g, ""))) {
13
17
  const absolute = absoluteSegments(segments, own);
14
18
  if (absolute !== null) results.push({ line, candidates: candidatesFor(root, absolute) });
19
+ else {
20
+ const [head, ...rest] = segments;
21
+ const other = head === undefined ? undefined : crates.get(head);
22
+ if (other !== undefined && other !== root) results.push({ line, candidates: candidatesFor(other, rest) });
23
+ }
15
24
  }
16
25
  }
17
26
  return results;
18
27
  }
19
28
 
29
+ /**
30
+ * Each library crate in the project by the name code imports it as: the directory
31
+ * holding `src/lib.rs`, with hyphens as underscores, as Cargo names it by default.
32
+ */
33
+ function libraryCrates(files ) {
34
+ const crates = new Map ();
35
+ for (const file of files) {
36
+ const match = /^(?:.*\/)?([^/]+)\/src\/lib\.rs$/.exec(file);
37
+ if (match !== null) crates.set((match[1] ?? "").replace(/-/g, "_"), file.slice(0, -"lib.rs".length));
38
+ }
39
+ return crates;
40
+ }
41
+
20
42
  /** The directory holding the crate root: everything up to and including `src/`. */
21
43
  function crateRoot(path ) {
22
44
  const index = path.lastIndexOf("src/");
@@ -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
  }