@printpractical/cruze 0.0.1

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 (248) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +167 -0
  3. package/bin/cruze.mjs +17 -0
  4. package/dist/adapters/inbound/cli/cli_context.js +75 -0
  5. package/dist/adapters/inbound/cli/commands/code.js +30 -0
  6. package/dist/adapters/inbound/cli/commands/documents.js +52 -0
  7. package/dist/adapters/inbound/cli/commands/init.js +23 -0
  8. package/dist/adapters/inbound/cli/commands/install.js +15 -0
  9. package/dist/adapters/inbound/cli/commands/journal.js +31 -0
  10. package/dist/adapters/inbound/cli/commands/progress.js +51 -0
  11. package/dist/adapters/inbound/cli/commands/review.js +18 -0
  12. package/dist/adapters/inbound/cli/commands/status.js +45 -0
  13. package/dist/adapters/inbound/cli/commands.js +36 -0
  14. package/dist/adapters/inbound/cli/parse_command.js +90 -0
  15. package/dist/adapters/inbound/cli/run_cli.js +50 -0
  16. package/dist/adapters/outbound/git_repository.js +57 -0
  17. package/dist/adapters/outbound/node_project_files.js +101 -0
  18. package/dist/adapters/outbound/package_bundle.js +56 -0
  19. package/dist/adapters/outbound/process_agent_runner.js +26 -0
  20. package/dist/adapters/outbound/system_clock.js +14 -0
  21. package/dist/adapters/outbound/terminal_prompter.js +22 -0
  22. package/dist/app/ports/agent_runner.js +11 -0
  23. package/dist/app/ports/bundle.js +22 -0
  24. package/dist/app/ports/clock.js +6 -0
  25. package/dist/app/ports/project_files.js +20 -0
  26. package/dist/app/ports/prompter.js +5 -0
  27. package/dist/app/ports/repository.js +13 -0
  28. package/dist/app/project_context.js +45 -0
  29. package/dist/app/use_cases/abandon_work.js +42 -0
  30. package/dist/app/use_cases/approve_artifact.js +146 -0
  31. package/dist/app/use_cases/check_code.js +90 -0
  32. package/dist/app/use_cases/init_project.js +59 -0
  33. package/dist/app/use_cases/install_skills.js +93 -0
  34. package/dist/app/use_cases/journal_events.js +48 -0
  35. package/dist/app/use_cases/land_change.js +113 -0
  36. package/dist/app/use_cases/new_project_doc.js +40 -0
  37. package/dist/app/use_cases/new_work_item.js +75 -0
  38. package/dist/app/use_cases/project_status.js +73 -0
  39. package/dist/app/use_cases/record_progress.js +101 -0
  40. package/dist/app/use_cases/run_review.js +71 -0
  41. package/dist/app/use_cases/validate_project.js +22 -0
  42. package/dist/domain/agent_skill_homes.js +25 -0
  43. package/dist/domain/approvals/evaluate.js +36 -0
  44. package/dist/domain/approvals/fingerprint.js +52 -0
  45. package/dist/domain/approvals/records.js +50 -0
  46. package/dist/domain/architecture_sections.js +23 -0
  47. package/dist/domain/check/check_source.js +64 -0
  48. package/dist/domain/check/imports.js +81 -0
  49. package/dist/domain/check/rust_imports.js +84 -0
  50. package/dist/domain/check/type_count.js +17 -0
  51. package/dist/domain/config.js +128 -0
  52. package/dist/domain/cruze_error.js +10 -0
  53. package/dist/domain/edits/agents_file.js +44 -0
  54. package/dist/domain/edits/managed_tables.js +60 -0
  55. package/dist/domain/edits/progress_block.js +33 -0
  56. package/dist/domain/edits/status_lines.js +52 -0
  57. package/dist/domain/elements.js +126 -0
  58. package/dist/domain/glob.js +46 -0
  59. package/dist/domain/hashing.js +52 -0
  60. package/dist/domain/ids.js +37 -0
  61. package/dist/domain/journal.js +62 -0
  62. package/dist/domain/land/bookkeeping.js +54 -0
  63. package/dist/domain/land/merge_plan.js +132 -0
  64. package/dist/domain/land/text_ops.js +66 -0
  65. package/dist/domain/markdown.js +79 -0
  66. package/dist/domain/markdown_tables.js +34 -0
  67. package/dist/domain/project/artifact_ref.js +42 -0
  68. package/dist/domain/project/deltas.js +49 -0
  69. package/dist/domain/project/layout.js +40 -0
  70. package/dist/domain/project/living_model.js +54 -0
  71. package/dist/domain/project/new_items.js +48 -0
  72. package/dist/domain/project/plan_parts.js +111 -0
  73. package/dist/domain/project/progress.js +43 -0
  74. package/dist/domain/project/project_view.js +72 -0
  75. package/dist/domain/project/roadmap.js +43 -0
  76. package/dist/domain/project/work_items.js +68 -0
  77. package/dist/domain/project_name.js +20 -0
  78. package/dist/domain/scaffold.js +29 -0
  79. package/dist/domain/skill.js +106 -0
  80. package/dist/domain/skill_links.js +103 -0
  81. package/dist/domain/status/next_step.js +119 -0
  82. package/dist/domain/status/verification.js +16 -0
  83. package/dist/domain/status/work_status.js +101 -0
  84. package/dist/domain/trace.js +44 -0
  85. package/dist/domain/validation/delta_rules.js +79 -0
  86. package/dist/domain/validation/element_rules.js +99 -0
  87. package/dist/domain/validation/living_rules.js +97 -0
  88. package/dist/domain/validation/plan_rules.js +105 -0
  89. package/dist/domain/validation/problem.js +21 -0
  90. package/dist/domain/validation/project_rules.js +99 -0
  91. package/dist/domain/validation/validate_project.js +31 -0
  92. package/dist/domain/validation/work_item_rules.js +144 -0
  93. package/dist/main.js +32 -0
  94. package/package.json +40 -0
  95. package/scripts/build.mjs +37 -0
  96. package/skills/about/SKILL.md +21 -0
  97. package/skills/architect/SKILL.md +43 -0
  98. package/skills/architect/feature.md +29 -0
  99. package/skills/architect/project.md +58 -0
  100. package/skills/architect/tweak.md +22 -0
  101. package/skills/architect/walkthrough.md +29 -0
  102. package/skills/behavioural-testing/SKILL.md +63 -0
  103. package/skills/behavioural-testing/examples.md +102 -0
  104. package/skills/behavioural-testing/fakes.md +30 -0
  105. package/skills/build/SKILL.md +62 -0
  106. package/skills/dependency-approval/SKILL.md +48 -0
  107. package/skills/domain-language/SKILL.md +37 -0
  108. package/skills/envision/SKILL.md +29 -0
  109. package/skills/envision/feature.md +22 -0
  110. package/skills/envision/project.md +20 -0
  111. package/skills/explore/SKILL.md +39 -0
  112. package/skills/formats/SKILL.md +84 -0
  113. package/skills/formats/reference/architecture.md +80 -0
  114. package/skills/formats/reference/config.md +57 -0
  115. package/skills/formats/reference/durable-docs.md +71 -0
  116. package/skills/formats/reference/specs.md +65 -0
  117. package/skills/formats/reference/work-items.md +100 -0
  118. package/skills/formats/templates/adr.md +7 -0
  119. package/skills/formats/templates/architecture.md +100 -0
  120. package/skills/formats/templates/change.md +29 -0
  121. package/skills/formats/templates/feature.md +49 -0
  122. package/skills/formats/templates/glossary.md +8 -0
  123. package/skills/formats/templates/roadmap.md +32 -0
  124. package/skills/formats/templates/spec.md +11 -0
  125. package/skills/formats/templates/standalone-change.md +46 -0
  126. package/skills/formats/templates/vision.md +37 -0
  127. package/skills/grilling/SKILL.md +66 -0
  128. package/skills/hexagonal-design/SKILL.md +119 -0
  129. package/skills/hexagonal-design/contracts.md +68 -0
  130. package/skills/hexagonal-design/fundamentals.md +50 -0
  131. package/skills/hexagonal-design/languages/cpp.md +37 -0
  132. package/skills/hexagonal-design/languages/rust.md +40 -0
  133. package/skills/hexagonal-design/runtime.md +48 -0
  134. package/skills/land/SKILL.md +43 -0
  135. package/skills/next/SKILL.md +30 -0
  136. package/skills/plan/SKILL.md +52 -0
  137. package/skills/plan/tasks.md +26 -0
  138. package/skills/plan/test-plan.md +14 -0
  139. package/skills/research/SKILL.md +41 -0
  140. package/skills/research/evaluation.md +27 -0
  141. package/skills/rethink/SKILL.md +53 -0
  142. package/skills/rethink/levels.md +45 -0
  143. package/skills/roadmap/SKILL.md +51 -0
  144. package/skills/roles/SKILL.md +57 -0
  145. package/skills/roles/code-reviewer.md +55 -0
  146. package/skills/roles/design-reviewer.md +63 -0
  147. package/skills/roles/researcher.md +33 -0
  148. package/skills/roles/verifier.md +38 -0
  149. package/skills/triage/SKILL.md +35 -0
  150. package/skills/verify/SKILL.md +39 -0
  151. package/skills/verify/manual-test.md +23 -0
  152. package/src/adapters/inbound/cli/cli_context.ts +75 -0
  153. package/src/adapters/inbound/cli/commands/code.ts +30 -0
  154. package/src/adapters/inbound/cli/commands/documents.ts +52 -0
  155. package/src/adapters/inbound/cli/commands/init.ts +23 -0
  156. package/src/adapters/inbound/cli/commands/install.ts +15 -0
  157. package/src/adapters/inbound/cli/commands/journal.ts +31 -0
  158. package/src/adapters/inbound/cli/commands/progress.ts +51 -0
  159. package/src/adapters/inbound/cli/commands/review.ts +18 -0
  160. package/src/adapters/inbound/cli/commands/status.ts +45 -0
  161. package/src/adapters/inbound/cli/commands.ts +36 -0
  162. package/src/adapters/inbound/cli/parse_command.ts +90 -0
  163. package/src/adapters/inbound/cli/run_cli.ts +50 -0
  164. package/src/adapters/outbound/git_repository.ts +57 -0
  165. package/src/adapters/outbound/node_project_files.ts +101 -0
  166. package/src/adapters/outbound/package_bundle.ts +56 -0
  167. package/src/adapters/outbound/process_agent_runner.ts +26 -0
  168. package/src/adapters/outbound/system_clock.ts +14 -0
  169. package/src/adapters/outbound/terminal_prompter.ts +22 -0
  170. package/src/app/ports/agent_runner.ts +11 -0
  171. package/src/app/ports/bundle.ts +22 -0
  172. package/src/app/ports/clock.ts +6 -0
  173. package/src/app/ports/project_files.ts +20 -0
  174. package/src/app/ports/prompter.ts +5 -0
  175. package/src/app/ports/repository.ts +13 -0
  176. package/src/app/project_context.ts +45 -0
  177. package/src/app/use_cases/abandon_work.ts +42 -0
  178. package/src/app/use_cases/approve_artifact.ts +146 -0
  179. package/src/app/use_cases/check_code.ts +90 -0
  180. package/src/app/use_cases/init_project.ts +59 -0
  181. package/src/app/use_cases/install_skills.ts +93 -0
  182. package/src/app/use_cases/journal_events.ts +48 -0
  183. package/src/app/use_cases/land_change.ts +113 -0
  184. package/src/app/use_cases/new_project_doc.ts +40 -0
  185. package/src/app/use_cases/new_work_item.ts +75 -0
  186. package/src/app/use_cases/project_status.ts +73 -0
  187. package/src/app/use_cases/record_progress.ts +101 -0
  188. package/src/app/use_cases/run_review.ts +71 -0
  189. package/src/app/use_cases/validate_project.ts +22 -0
  190. package/src/domain/agent_skill_homes.ts +25 -0
  191. package/src/domain/approvals/evaluate.ts +36 -0
  192. package/src/domain/approvals/fingerprint.ts +52 -0
  193. package/src/domain/approvals/records.ts +50 -0
  194. package/src/domain/architecture_sections.ts +23 -0
  195. package/src/domain/check/check_source.ts +64 -0
  196. package/src/domain/check/imports.ts +81 -0
  197. package/src/domain/check/rust_imports.ts +84 -0
  198. package/src/domain/check/type_count.ts +17 -0
  199. package/src/domain/config.ts +128 -0
  200. package/src/domain/cruze_error.ts +10 -0
  201. package/src/domain/edits/agents_file.ts +44 -0
  202. package/src/domain/edits/managed_tables.ts +60 -0
  203. package/src/domain/edits/progress_block.ts +33 -0
  204. package/src/domain/edits/status_lines.ts +52 -0
  205. package/src/domain/elements.ts +126 -0
  206. package/src/domain/glob.ts +46 -0
  207. package/src/domain/hashing.ts +52 -0
  208. package/src/domain/ids.ts +37 -0
  209. package/src/domain/journal.ts +62 -0
  210. package/src/domain/land/bookkeeping.ts +54 -0
  211. package/src/domain/land/merge_plan.ts +132 -0
  212. package/src/domain/land/text_ops.ts +66 -0
  213. package/src/domain/markdown.ts +79 -0
  214. package/src/domain/markdown_tables.ts +34 -0
  215. package/src/domain/project/artifact_ref.ts +42 -0
  216. package/src/domain/project/deltas.ts +49 -0
  217. package/src/domain/project/layout.ts +40 -0
  218. package/src/domain/project/living_model.ts +54 -0
  219. package/src/domain/project/new_items.ts +48 -0
  220. package/src/domain/project/plan_parts.ts +111 -0
  221. package/src/domain/project/progress.ts +43 -0
  222. package/src/domain/project/project_view.ts +72 -0
  223. package/src/domain/project/roadmap.ts +43 -0
  224. package/src/domain/project/work_items.ts +68 -0
  225. package/src/domain/project_name.ts +20 -0
  226. package/src/domain/scaffold.ts +29 -0
  227. package/src/domain/skill.ts +106 -0
  228. package/src/domain/skill_links.ts +103 -0
  229. package/src/domain/status/next_step.ts +119 -0
  230. package/src/domain/status/verification.ts +16 -0
  231. package/src/domain/status/work_status.ts +101 -0
  232. package/src/domain/trace.ts +44 -0
  233. package/src/domain/validation/delta_rules.ts +79 -0
  234. package/src/domain/validation/element_rules.ts +99 -0
  235. package/src/domain/validation/living_rules.ts +97 -0
  236. package/src/domain/validation/plan_rules.ts +105 -0
  237. package/src/domain/validation/problem.ts +21 -0
  238. package/src/domain/validation/project_rules.ts +99 -0
  239. package/src/domain/validation/validate_project.ts +31 -0
  240. package/src/domain/validation/work_item_rules.ts +144 -0
  241. package/src/main.ts +32 -0
  242. package/templates/init/AGENTS.md +28 -0
  243. package/templates/init/CHANGELOG.md +8 -0
  244. package/templates/init/CLAUDE.md +1 -0
  245. package/templates/init/README.md +1 -0
  246. package/templates/init/ci.yml +23 -0
  247. package/templates/init/config.yaml +33 -0
  248. package/templates/init/gitattributes +2 -0
@@ -0,0 +1,105 @@
1
+ import { factsOf, isUnbuilt } from "../elements.ts";
2
+ import { findIds, kindOf } from "../ids.ts";
3
+ import type { MarkdownDoc } from "../markdown.ts";
4
+ import type { Delta } from "../project/deltas.ts";
5
+ import { isPlanned, readScope, readTasks, readTestPlan, type Scope } from "../project/plan_parts.ts";
6
+ import type { ProjectView } from "../project/project_view.ts";
7
+ import { error, warning, type Problem } from "./problem.ts";
8
+
9
+ const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
10
+
11
+ /**
12
+ * Rules for one buildable change: its scope names only things that can be built. Once it is
13
+ * planned, every scope ID is proved by a task, every delivered scenario has a behaviour test,
14
+ * and every port a built adapter implements has a contract test.
15
+ */
16
+ export function changePlanProblems(
17
+ view: ProjectView,
18
+ path: string,
19
+ doc: MarkdownDoc,
20
+ delta: { delta: Delta; doc: MarkdownDoc },
21
+ available: Set<string>,
22
+ ): Problem[] {
23
+ const scope = readScope(doc);
24
+ if (scope === null) return [error(path, undefined, "missing-section", 'the change needs a "## Scope" section')];
25
+ const problems: Problem[] = [...scopeTargetProblems(view, path, scope, delta.delta)];
26
+ if (!isPlanned(doc)) return [...problems, warning(path, undefined, "not-planned", "the change has no test plan or tasks yet; plan fills them")];
27
+ const { tasks, malformed } = readTasks(doc);
28
+ for (const line of malformed) {
29
+ problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
30
+ }
31
+ if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
32
+
33
+ const proved = new Set(tasks.flatMap((task) => task.proves));
34
+ const taskPaths = tasks.flatMap((task) => task.paths);
35
+ for (const task of tasks) {
36
+ for (const id of task.proves.filter((id) => !available.has(id))) {
37
+ problems.push(error(path, task.line, "unknown-id", `${task.number} proves ${id}, which is not defined`));
38
+ }
39
+ }
40
+ for (const id of [...scope.delivers, ...scope.builds]) {
41
+ if (kindOf(id) === "MOD") {
42
+ const modulePaths = modulePathsOf(view, delta.doc, delta.delta, id);
43
+ if (!taskPaths.some((p) => modulePaths.some((m) => p.startsWith(m)))) {
44
+ problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}`));
45
+ }
46
+ } else if (kindOf(id) === "REQ") {
47
+ const scenarios = delta.delta.entries.find((e) => e.element.id === id)?.scenarios.map((s) => s.id) ?? [];
48
+ if (!proved.has(id) && !scenarios.some((s) => proved.has(s))) problems.push(error(path, scope.line, "unproved-scope", `no task proves ${id} or its scenarios`));
49
+ } else if (!proved.has(id)) {
50
+ problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task proves it`));
51
+ }
52
+ }
53
+
54
+ const rows = readTestPlan(doc);
55
+ for (const row of rows) {
56
+ if (!TEST_KINDS.includes(row.kind)) problems.push(error(path, row.line, "test-kind", `test kind must be one of ${TEST_KINDS.join(", ")}`));
57
+ const subject = findIds(row.subject)[0];
58
+ if (subject === undefined || !available.has(subject)) problems.push(error(path, row.line, "unknown-id", `test subject "${row.subject}" is not a defined ID`));
59
+ if (row.file === "") problems.push(error(path, row.line, "test-file", "every test plan row names its test file"));
60
+ }
61
+ for (const id of scope.delivers.filter((id) => kindOf(id) === "SCN")) {
62
+ if (!rows.some((row) => findIds(row.subject)[0] === id && row.kind === "behaviour")) {
63
+ problems.push(error(path, scope.line, "missing-behaviour-test", `${id} is delivered but has no behaviour row in the test plan`));
64
+ }
65
+ }
66
+ for (const adapter of scope.builds.filter((id) => kindOf(id) === "ADP")) {
67
+ const ports = (factsOfScoped(view, delta.doc, delta.delta, adapter).get("Implements") ?? []).flatMap(findIds).filter((id) => kindOf(id) === "PORT");
68
+ for (const port of ports.filter((p) => !rows.some((row) => findIds(row.subject)[0] === p && row.kind === "contract"))) {
69
+ problems.push(error(path, scope.line, "missing-contract-test", `${adapter} implements ${port} but the test plan has no contract row for ${port}`));
70
+ }
71
+ }
72
+ return problems;
73
+ }
74
+
75
+ /** Scope IDs must come from the item's deltas or be planned in the living docs. */
76
+ function scopeTargetProblems(view: ProjectView, path: string, scope: Scope, delta: Delta): Problem[] {
77
+ const problems: Problem[] = [];
78
+ for (const id of [...scope.delivers, ...scope.builds]) {
79
+ if (delta.ids.has(id)) continue;
80
+ const living = view.living.elements.get(id);
81
+ if (living === undefined) problems.push(error(path, scope.line, "unknown-id", `${id} is in scope but defined nowhere`));
82
+ else if (!isUnbuilt(living.doc, living.element)) {
83
+ problems.push(error(path, scope.line, "scope-not-planned", `${id} is already built; change it through a MODIFIED operation in the delta`));
84
+ }
85
+ }
86
+ for (const id of scope.removes) {
87
+ const removal = delta.entries.find((e) => e.element.id === id && e.element.op === "REMOVED");
88
+ if (removal === undefined) problems.push(error(path, scope.line, "scope-removes", `${id} is in Removes but the delta has no REMOVED operation for it`));
89
+ }
90
+ if (scope.delivers.length + scope.builds.length + scope.removes.length === 0) {
91
+ problems.push(warning(path, scope.line, "empty-scope", "the scope delivers and builds nothing"));
92
+ }
93
+ return problems;
94
+ }
95
+
96
+ /** An element's facts as this item will build it: from its delta when it has one, else from the living docs. */
97
+ function factsOfScoped(view: ProjectView, doc: MarkdownDoc, delta: Delta, id: string): Map<string, string[]> {
98
+ const fromDelta = delta.entries.find((e) => e.element.id === id);
99
+ const living = view.living.elements.get(id);
100
+ return fromDelta !== undefined ? factsOf(doc, fromDelta.element) : living !== undefined ? factsOf(living.doc, living.element) : new Map<string, string[]>();
101
+ }
102
+
103
+ function modulePathsOf(view: ProjectView, doc: MarkdownDoc, delta: Delta, id: string): string[] {
104
+ return (factsOfScoped(view, doc, delta, id).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => p !== "");
105
+ }
@@ -0,0 +1,21 @@
1
+ /** One way a document breaks the cruze-formats rules. */
2
+ export interface Problem {
3
+ path: string;
4
+ /** 1-based line number, when the problem has a location. */
5
+ line?: number;
6
+ rule: string;
7
+ message: string;
8
+ severity: "error" | "warning";
9
+ }
10
+
11
+ export function error(path: string, line: number | undefined, rule: string, message: string): Problem {
12
+ return line === undefined ? { path, rule, message, severity: "error" } : { path, line: line + 1, rule, message, severity: "error" };
13
+ }
14
+
15
+ export function warning(path: string, line: number | undefined, rule: string, message: string): Problem {
16
+ return { ...error(path, line, rule, message), severity: "warning" };
17
+ }
18
+
19
+ export function sortProblems(problems: Problem[]): Problem[] {
20
+ return [...problems].sort((a, b) => a.path.localeCompare(b.path) || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule));
21
+ }
@@ -0,0 +1,99 @@
1
+ import { factsOf } from "../elements.ts";
2
+ import { matchesAny } from "../glob.ts";
3
+ import { findIds } from "../ids.ts";
4
+ import { parseMarkdown, type MarkdownDoc } from "../markdown.ts";
5
+ import { PATHS } from "../project/layout.ts";
6
+ import { readRoadmapItems } from "../project/roadmap.ts";
7
+ import type { ProjectView } from "../project/project_view.ts";
8
+ import { error, warning, type Problem } from "./problem.ts";
9
+
10
+ /** Rules that span the project: leftover template guides, the roadmap and the config. */
11
+
12
+ const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
13
+ const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
14
+
15
+ /**
16
+ * Unfilled `<guides>` and `{{placeholders}}` outside code, in living docs and active work.
17
+ * Managed blocks are skipped: the CLI writes them, and they may quote text such as `<COMMAND>`.
18
+ */
19
+ export function templateLeftovers(view: ProjectView): Problem[] {
20
+ const paths = [...view.living.docs.keys(), PATHS.roadmap, PATHS.glossary, ...view.items.filter((i) => !i.archived).map((i) => i.path)];
21
+ return paths.flatMap((path) => {
22
+ const text = view.snapshot.get(path);
23
+ if (text === undefined) return [];
24
+ const doc = parseMarkdown(text);
25
+ return doc.lines.flatMap((line, i) => {
26
+ if (doc.inFence[i] || doc.inManaged[i]) return [];
27
+ const withoutCode = line.replace(/`[^`]*`/g, "");
28
+ if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) {
29
+ return [error(path, i, "template-leftover", "replace the template guide or placeholder on this line")];
30
+ }
31
+ return [];
32
+ });
33
+ });
34
+ }
35
+
36
+ export function roadmapProblems(view: ProjectView): Problem[] {
37
+ const text = view.snapshot.get(PATHS.roadmap);
38
+ if (text === undefined) return [];
39
+ const doc = parseMarkdown(text);
40
+ const problems: Problem[] = [];
41
+ const items = new Map<string, { blockedBy: string[]; line: number }>();
42
+ for (const { slug: item, kind, blockedBy, line } of readRoadmapItems(text)) {
43
+ if (items.has(item)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} appears twice`));
44
+ if (!["feature", "change"].includes(kind)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} kind must be feature or change`));
45
+ items.set(item, { blockedBy, line });
46
+ }
47
+ for (const [item, { blockedBy, line }] of items) {
48
+ for (const blocker of blockedBy.filter((b) => !items.has(b))) problems.push(error(PATHS.roadmap, line, "roadmap-blocker", `${item} is blocked by unknown item ${blocker}`));
49
+ }
50
+ const cycle = findCycle(items);
51
+ if (cycle !== null) problems.push(error(PATHS.roadmap, undefined, "roadmap-cycle", `blocking cycle: ${cycle.join(" -> ")}`));
52
+ const coverage = doc.lines.join("\n").split(/^## Coverage$/m)[1] ?? "";
53
+ for (const goal of [...view.living.elements.keys()].filter((id) => id.startsWith("GOAL-"))) {
54
+ if (!findIds(coverage).includes(goal)) problems.push(warning(PATHS.roadmap, undefined, "goal-uncovered", `${goal} has no items in the coverage table`));
55
+ }
56
+ return problems;
57
+ }
58
+
59
+ function findCycle(items: Map<string, { blockedBy: string[] }>): string[] | null {
60
+ const state = new Map<string, "visiting" | "done">();
61
+ const visit = (item: string, path: string[]): string[] | null => {
62
+ if (state.get(item) === "done") return null;
63
+ if (state.get(item) === "visiting") return [...path.slice(path.indexOf(item)), item];
64
+ state.set(item, "visiting");
65
+ for (const next of items.get(item)?.blockedBy ?? []) {
66
+ const found = visit(next, [...path, item]);
67
+ if (found !== null) return found;
68
+ }
69
+ state.set(item, "done");
70
+ return null;
71
+ };
72
+ for (const item of items.keys()) {
73
+ const found = visit(item, []);
74
+ if (found !== null) return found;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export function configProblems(view: ProjectView): Problem[] {
80
+ if (view.config === null) return [error(PATHS.config, undefined, "config-missing", "the project has no .cruze/config.yaml; run cruze init")];
81
+ const problems = view.config.problems.map((message) => error(PATHS.config, undefined, "config", message));
82
+ const config = view.config.config;
83
+ const architecture = view.living.docs.get(PATHS.architecture);
84
+ if (config === null || architecture === undefined || config.layers.length === 0) return problems;
85
+ for (const { element } of [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-"))) {
86
+ for (const path of modulePaths(architecture, element)) {
87
+ const probe = path.endsWith("/") ? `${path}probe.file` : path;
88
+ const layers = config.layers.filter((layer) => matchesAny(probe, layer.paths));
89
+ if (layers.length !== 1) {
90
+ problems.push(warning(PATHS.config, undefined, "module-layer", `${element.id} path ${path} falls in ${layers.length} layers; it should fall in exactly one`));
91
+ }
92
+ }
93
+ }
94
+ return problems;
95
+ }
96
+
97
+ function modulePaths(doc: MarkdownDoc, element: Parameters<typeof factsOf>[1]): string[] {
98
+ return (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? ""));
99
+ }
@@ -0,0 +1,31 @@
1
+ import type { ProjectView } from "../project/project_view.ts";
2
+ import { livingProblems } from "./living_rules.ts";
3
+ import { error, sortProblems, type Problem } from "./problem.ts";
4
+ import { configProblems, roadmapProblems, templateLeftovers } from "./project_rules.ts";
5
+ import { workItemProblems } from "./work_item_rules.ts";
6
+
7
+ /** Every rule in the cruze-formats skill, applied to the whole project. */
8
+ export function validateProject(view: ProjectView): Problem[] {
9
+ const approvalProblems = [...view.approvalProblems].map(([path, message]) => error(path, undefined, "approvals-file", message));
10
+ const journalProblems = [...view.journalProblems].flatMap(([path, lines]) =>
11
+ lines.map((line) => error(path, line - 1, "journal-line", "this journal line is not a valid entry")),
12
+ );
13
+ return sortProblems([
14
+ ...configProblems(view),
15
+ ...livingProblems(view),
16
+ ...roadmapProblems(view),
17
+ ...workItemProblems(view),
18
+ ...templateLeftovers(view),
19
+ ...approvalProblems,
20
+ ...journalProblems,
21
+ ]);
22
+ }
23
+
24
+ /** Problems that concern one document. */
25
+ export function problemsFor(problems: Problem[], path: string): Problem[] {
26
+ return problems.filter((problem) => problem.path === path);
27
+ }
28
+
29
+ export function hasErrors(problems: Problem[]): boolean {
30
+ return problems.some((problem) => problem.severity === "error");
31
+ }
@@ -0,0 +1,144 @@
1
+ import { isUnbuilt } from "../elements.ts";
2
+ import { findIds, kindOf } from "../ids.ts";
3
+ import { elementHash } from "../hashing.ts";
4
+ import { findSection, type MarkdownDoc } from "../markdown.ts";
5
+ import { readDelta, type Delta } from "../project/deltas.ts";
6
+ import { readChangesTable, readScope } from "../project/plan_parts.ts";
7
+ import { readProgress } from "../project/progress.ts";
8
+ import type { ProjectView } from "../project/project_view.ts";
9
+ import { changesOf, featureOf, type WorkItem } from "../project/work_items.ts";
10
+ import { deltaProblems } from "./delta_rules.ts";
11
+ import { malformedIdHeadings, lineOf } from "./living_rules.ts";
12
+ import { changePlanProblems } from "./plan_rules.ts";
13
+ import { error, warning, type Problem } from "./problem.ts";
14
+
15
+ const REQUIRED_SECTIONS: Record<WorkItem["kind"], string[]> = {
16
+ feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
17
+ change: ["Scope", "Test plan", "Tasks"],
18
+ standalone: ["Intent", "Spec delta", "Architecture delta", "Scope", "Test plan", "Tasks"],
19
+ };
20
+
21
+ /** Rules for every active feature and change. Archived work is history and is not checked. */
22
+ export function workItemProblems(view: ProjectView): Problem[] {
23
+ return view.items.filter((item) => !item.archived).flatMap((item) => itemProblems(view, item));
24
+ }
25
+
26
+ function itemProblems(view: ProjectView, item: WorkItem): Problem[] {
27
+ const { path, doc } = item;
28
+ const problems: Problem[] = [...malformedIdHeadings(path, doc)];
29
+ const id = doc.frontmatter?.["id"];
30
+ if (id !== item.folderName) problems.push(error(path, 0, "frontmatter", `frontmatter id must be "${item.folderName}", the folder name`));
31
+ const title = doc.frontmatter?.["title"];
32
+ if (typeof title !== "string" || title.trim() === "") problems.push(error(path, 0, "frontmatter", "frontmatter needs a title"));
33
+ for (const section of REQUIRED_SECTIONS[item.kind]) {
34
+ if (findSection(doc, section) === null) problems.push(error(path, undefined, "missing-section", `missing "## ${section}"`));
35
+ }
36
+
37
+ const feature = item.kind === "change" ? featureOf(view.items, item) : undefined;
38
+ if (item.kind === "change" && feature === undefined) problems.push(error(path, undefined, "orphan-change", "the change's feature.md is missing"));
39
+ const deltaSource = item.kind === "change" ? feature : item;
40
+ const delta = deltaSource === undefined ? readDelta(doc) : readDelta(deltaSource.doc);
41
+ const available = new Set([...view.living.elements.keys(), ...delta.ids]);
42
+
43
+ const text = doc.lines.join("\n");
44
+ for (const cited of findIds(text).filter((cited) => !available.has(cited))) {
45
+ problems.push(error(path, lineOf(text, cited), "unknown-id", `${cited} is cited but defined neither in the living docs nor in this work's deltas`));
46
+ }
47
+ if (item.kind !== "change") problems.push(...deltaProblems(view, item));
48
+ if (item.kind === "feature") problems.push(...featureScopeProblems(view, item, delta));
49
+ // A landed change is history: its scope named what it built, which is now built.
50
+ const landed = item.kind !== "feature" && readProgress(doc).landed !== undefined;
51
+ if (item.kind !== "feature" && deltaSource !== undefined && !landed) {
52
+ problems.push(...changePlanProblems(view, path, doc, { delta, doc: deltaSource.doc }, available));
53
+ }
54
+ if (item.kind === "standalone") problems.push(...coverageProblems(view, path, delta, doc, [{ name: "this change", ids: scopeIds(item) }]));
55
+ return problems;
56
+ }
57
+
58
+ /** A feature's Changes table covers its deltas, and each change folder agrees with its row. */
59
+ function featureScopeProblems(view: ProjectView, feature: WorkItem, delta: Delta): Problem[] {
60
+ const rows = readChangesTable(feature.doc);
61
+ if (rows.length === 0) return [warning(feature.path, undefined, "not-designed", "the feature has no changes yet; architect designs and splits it")];
62
+ const problems: Problem[] = [];
63
+ const landedChanges = new Set(
64
+ [...readProgress(feature.doc).changes].filter(([, state]) => state === "landed").map(([change]) => change),
65
+ );
66
+ const names = rows.map((row) => row.change);
67
+ for (const row of rows) {
68
+ if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
69
+ if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
70
+ for (const dependency of row.dependsOn) {
71
+ const at = names.indexOf(dependency);
72
+ if (at === -1 || at >= names.indexOf(row.change)) problems.push(error(feature.path, row.line, "change-order", `${row.change} depends on ${dependency}, which must be an earlier change`));
73
+ }
74
+ for (const id of landedChanges.has(row.change) ? [] : [...row.delivers, ...row.builds]) {
75
+ const living = view.living.elements.get(id);
76
+ const planned = living !== undefined && isUnbuilt(living.doc, living.element);
77
+ if (!delta.ids.has(id) && !planned) problems.push(error(feature.path, row.line, "scope-not-planned", `${id} must come from this feature's deltas or be planned in the living docs`));
78
+ }
79
+ }
80
+ problems.push(...coverageProblems(view, feature.path, delta, feature.doc, rows.map((row) => ({ name: row.change, ids: [...row.delivers, ...row.builds, ...row.removes], landed: landedChanges.has(row.change) }))));
81
+
82
+ for (const change of changesOf(view.items, feature)) {
83
+ const row = rows.find((r) => r.change === change.folderName);
84
+ if (row === undefined) {
85
+ problems.push(error(change.path, undefined, "change-not-listed", `${change.folderName} is not in the feature's Changes table`));
86
+ continue;
87
+ }
88
+ const expected = new Set([...row.delivers, ...row.builds, ...row.removes]);
89
+ const actual = new Set(scopeIds(change));
90
+ const differ = [...expected].filter((id) => !actual.has(id)).concat([...actual].filter((id) => !expected.has(id)));
91
+ if (differ.length > 0) problems.push(error(change.path, readScope(change.doc)?.line, "scope-mismatch", `Scope differs from the feature's Changes row: ${differ.join(", ")}`));
92
+ }
93
+ return problems;
94
+ }
95
+
96
+ /**
97
+ * Every delta ID lands exactly once. A requirement is covered by its own ID or one of its
98
+ * scenarios. A scenario must be covered when it is new or its text changed; an unchanged
99
+ * scenario kept by a MODIFIED requirement may be left out.
100
+ */
101
+ function coverageProblems(
102
+ view: ProjectView,
103
+ path: string,
104
+ delta: Delta,
105
+ deltaDoc: MarkdownDoc,
106
+ groups: Array<{ name: string; ids: string[]; landed?: boolean }>,
107
+ ): Problem[] {
108
+ const coveredBy = new Map<string, string[]>();
109
+ const pendingBy = new Map<string, string[]>();
110
+ for (const group of groups) {
111
+ for (const id of group.ids) {
112
+ coveredBy.set(id, [...(coveredBy.get(id) ?? []), group.name]);
113
+ if (group.landed !== true) pendingBy.set(id, [...(pendingBy.get(id) ?? []), group.name]);
114
+ }
115
+ }
116
+ const problems: Problem[] = [];
117
+ // A change that already landed may be followed by one that rebuilds the same element after a rethink.
118
+ const once = (id: string, line: number, required: boolean): void => {
119
+ const pending = pendingBy.get(id) ?? [];
120
+ if (required && (coveredBy.get(id) ?? []).length === 0) problems.push(error(path, line, "uncovered", `${id} is not in any change's scope`));
121
+ if (pending.length > 1) problems.push(error(path, line, "covered-twice", `${id} is in the scope of ${pending.join(" and ")}; it must land exactly once`));
122
+ };
123
+ for (const { element, scenarios } of delta.entries) {
124
+ if (kindOf(element.id) !== "REQ") {
125
+ once(element.id, element.start, true);
126
+ continue;
127
+ }
128
+ once(element.id, element.start, false);
129
+ if (![element.id, ...scenarios.map((s) => s.id)].some((id) => coveredBy.has(id))) {
130
+ problems.push(error(path, element.start, "uncovered", `${element.id} is not delivered by any change`));
131
+ }
132
+ for (const scenario of scenarios) {
133
+ const living = view.living.elements.get(scenario.id);
134
+ const changed = living === undefined || elementHash(living.doc, living.element) !== elementHash(deltaDoc, scenario);
135
+ once(scenario.id, scenario.start, element.op !== "REMOVED" && changed);
136
+ }
137
+ }
138
+ return problems;
139
+ }
140
+
141
+ function scopeIds(item: WorkItem): string[] {
142
+ const scope = readScope(item.doc);
143
+ return scope === null ? [] : [...scope.delivers, ...scope.builds, ...scope.removes];
144
+ }
package/src/main.ts ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Composition root: the only place that constructs adapters.
3
+ import { basename } from "node:path";
4
+ import { runCli } from "./adapters/inbound/cli/run_cli.ts";
5
+ import { GitRepository } from "./adapters/outbound/git_repository.ts";
6
+ import { NodeProjectFiles } from "./adapters/outbound/node_project_files.ts";
7
+ import { PackageBundle } from "./adapters/outbound/package_bundle.ts";
8
+ import { ProcessAgentRunner } from "./adapters/outbound/process_agent_runner.ts";
9
+ import { SystemClock } from "./adapters/outbound/system_clock.ts";
10
+ import { DefaultAnswerPrompter, TerminalPrompter } from "./adapters/outbound/terminal_prompter.ts";
11
+
12
+ const cwd = process.cwd();
13
+ const exitCode = await runCli(
14
+ process.argv.slice(2),
15
+ {
16
+ files: new NodeProjectFiles(cwd),
17
+ bundle: await PackageBundle.locate(),
18
+ clock: new SystemClock(),
19
+ repository: new GitRepository(cwd),
20
+ interactivePrompter: new TerminalPrompter(),
21
+ unattendedPrompter: new DefaultAnswerPrompter(),
22
+ runner: new ProcessAgentRunner(cwd),
23
+ interactive: process.stdin.isTTY === true,
24
+ directoryName: basename(cwd),
25
+ },
26
+ {
27
+ stdout: (text) => process.stdout.write(`${text}\n`),
28
+ stderr: (text) => process.stderr.write(`${text}\n`),
29
+ stdoutIsTerminal: process.stdout.isTTY === true,
30
+ },
31
+ );
32
+ process.exitCode = exitCode;
@@ -0,0 +1,28 @@
1
+ # {{project_name}}
2
+
3
+ Instructions for coding agents working in this repository. Cruze keeps the managed sections current as changes land; edit everything else freely.
4
+
5
+ ## Workflow
6
+
7
+ This project uses [Cruze](https://github.com/PrintPractical/cruze) for spec-driven development. Durable documents live in `docs/`: vision, glossary, architecture, ADRs, specs and roadmap. Work in progress lives in `.cruze/`. Read `docs/architecture.md` before changing code, once it exists. The Cruze skills run the `cruze` command; when it is missing, install it as https://github.com/PrintPractical/cruze#quick-start describes.
8
+
9
+ ## Commands
10
+
11
+ <!-- cruze:managed -->
12
+ No build or test commands yet. `cruze land` fills this in when the walking-skeleton change lands.
13
+ <!-- /cruze:managed -->
14
+
15
+ ## Layout
16
+
17
+ <!-- cruze:managed -->
18
+ Not defined yet. `cruze land` fills this in from the module map in `docs/architecture.md`.
19
+ <!-- /cruze:managed -->
20
+
21
+ ## Conventions
22
+
23
+ - Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
24
+ - Record notable changes under `[Unreleased]` in `CHANGELOG.md`.
25
+
26
+ ## Known pitfalls
27
+
28
+ None recorded yet. `land` adds a line here when the same review finding or defect happens twice.
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1 @@
1
+ # {{project_name}}
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: actions/setup-node@v5
14
+ with:
15
+ node-version: 22
16
+ # The walking-skeleton change adds the language's format, lint, build and
17
+ # test steps here, from `commands:` in .cruze/config.yaml.
18
+ - name: Documents match the Cruze formats
19
+ run: npx --yes --package={{cruze_package}} cruze validate
20
+ - name: Layer rules and file budgets
21
+ run: npx --yes --package={{cruze_package}} cruze check --ci
22
+ - name: Every built scenario has a test
23
+ run: npx --yes --package={{cruze_package}} cruze trace --all
@@ -0,0 +1,33 @@
1
+ # Cruze project configuration, created by cruze {{cruze_version}}.
2
+ # The format is described in .agents/skills/cruze-formats/reference/config.md.
3
+ version: 1
4
+ project: {{project_name_quoted}}
5
+
6
+ # Where work items are tracked. V1 supports markdown in the repository only.
7
+ tracker: markdown
8
+
9
+ # Source files `cruze check` inspects, and files `cruze trace` searches for
10
+ # scenario IDs. Project-scope architecture design sets these.
11
+ source: []
12
+ tests: []
13
+
14
+ # Budgets enforced by `cruze check`. List a file under exceptions, with a
15
+ # reason, to allow it past a budget.
16
+ check:
17
+ max_lines: 250
18
+ max_types: 5
19
+ exceptions: []
20
+
21
+ # Layer rules, written by project-scope architecture design from the module map.
22
+ layers: []
23
+
24
+ # The project's commands, by name, such as `test: cargo test`. The
25
+ # walking-skeleton change fills them in; `cruze land` copies them into AGENTS.md.
26
+ commands: {}
27
+
28
+ # How `cruze review` starts an agent in a fresh context to run a role, with the
29
+ # prompt on standard input. The default lets Claude Code read the project and run
30
+ # git and cruze. The verifier runs the system, so add the project's build and run
31
+ # commands, such as "Bash(cargo:*)". For Codex, use ["codex", "exec", "-"].
32
+ review:
33
+ command: ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"]
@@ -0,0 +1,2 @@
1
+ # Cruze journals are append-only, so parallel branches merge them by keeping every line.
2
+ **/journal.jsonl merge=union