@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,81 @@
1
+ import { posix } from "node:path";
2
+ import { rustImportCandidates } from "./rust_imports.js";
3
+
4
+ /** One import in a source file, with the project files it could refer to, most specific first. */
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+ const JS = /\.(?:[cm]?[jt]sx?)$/;
13
+ const JS_EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", "/index.ts", "/index.js"];
14
+
15
+ /** Imports that point inside the project. Imports of libraries and the standard library are left out. */
16
+ export function importsOf(path , text ) {
17
+ if (JS.test(path)) return jsImports(path, text);
18
+ if (path.endsWith(".py")) return pythonImports(path, text);
19
+ if (path.endsWith(".rs")) return rustImportCandidates(path, text).map((i) => ({ ...i, bySuffix: false }));
20
+ if (/\.(?:c|h|cc|cpp|cxx|hh|hpp|hxx)$/.test(path)) return matches(text, /^\s*#\s*include\s+"([^"]+)"/gm, (target) => [target], true);
21
+ if (path.endsWith(".go")) return goImports(text);
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);
23
+ return [];
24
+ }
25
+
26
+ function jsImports(path , text ) {
27
+ const pattern = /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["']([^"']+)["']/g;
28
+ return matches(text, pattern, (target) => {
29
+ if (!target.startsWith(".")) return [];
30
+ const base = posix.normalize(posix.join(posix.dirname(path), target));
31
+ const stripped = base.replace(/\.[cm]?jsx?$/, "");
32
+ return [...new Set([...JS_EXTENSIONS.map((ext) => base + ext), ...JS_EXTENSIONS.map((ext) => stripped + ext)])];
33
+ }, false);
34
+ }
35
+
36
+ function pythonImports(path , text ) {
37
+ const results = [];
38
+ for (const match of text.matchAll(/^\s*(?:from\s+(\.*)([\w.]*)\s+import\s+([\w, ]+)|import\s+([\w.]+))/gm)) {
39
+ const line = text.slice(0, match.index).split("\n").length - 1;
40
+ const dots = match[1] ?? "";
41
+ const module = (match[2] ?? match[4] ?? "").split(".").filter((s) => s !== "");
42
+ const names = (match[3] ?? "").split(",").map((s) => s.trim().split(" ")[0] ?? "").filter((s) => s !== "");
43
+ const bases = dots === "" ? [module] : [[...posix.dirname(path).split("/").slice(0, dots.length === 1 ? undefined : -(dots.length - 1)), ...module]];
44
+ const candidates = bases.flatMap((segments) => [
45
+ ...names.map((name) => `${[...segments, name].join("/")}.py`),
46
+ `${segments.join("/")}.py`,
47
+ `${segments.join("/")}/__init__.py`,
48
+ ]);
49
+ results.push({ line, candidates, bySuffix: dots === "" });
50
+ }
51
+ return results;
52
+ }
53
+
54
+ function goImports(text ) {
55
+ const results = [];
56
+ for (const match of text.matchAll(/^\s*(?:import\s+)?(?:\w+\s+)?"([\w./-]+)"\s*$/gm)) {
57
+ const line = text.slice(0, match.index).split("\n").length - 1;
58
+ results.push({ line, candidates: [`${match[1] ?? ""}/`], bySuffix: true });
59
+ }
60
+ return results;
61
+ }
62
+
63
+ function matches(text , pattern , candidates , bySuffix ) {
64
+ return [...text.matchAll(pattern)]
65
+ .map((match) => ({ line: text.slice(0, match.index).split("\n").length - 1, candidates: candidates(match[1] ?? ""), bySuffix }))
66
+ .filter((found) => found.candidates.length > 0);
67
+ }
68
+
69
+ /** The project file an import refers to, or undefined when it points outside the project. */
70
+ export function resolveImport(found , files ) {
71
+ for (const candidate of found.candidates) {
72
+ if (candidate.endsWith("/")) {
73
+ const inDir = files.find((f) => (found.bySuffix ? `/${f}`.includes(`/${candidate}`) : f.startsWith(candidate)));
74
+ if (inDir !== undefined) return inDir;
75
+ continue;
76
+ }
77
+ const hit = found.bySuffix ? files.find((f) => f === candidate || f.endsWith(`/${candidate}`)) : files.find((f) => f === candidate);
78
+ if (hit !== undefined) return hit;
79
+ }
80
+ return undefined;
81
+ }
@@ -0,0 +1,84 @@
1
+ /** Resolves Rust `use` paths inside a crate to candidate source files. */
2
+
3
+ /** Candidate files for each `use crate::…`, `use super::…` and `use self::…` path in a Rust file. */
4
+ export function rustImportCandidates(path , text ) {
5
+ const root = crateRoot(path);
6
+ if (root === null) return [];
7
+ const own = moduleSegments(path, root);
8
+ const results = [];
9
+ const pattern = /^\s*(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/gms;
10
+ for (const match of text.matchAll(pattern)) {
11
+ const line = text.slice(0, match.index).split("\n").length - 1;
12
+ for (const segments of expandUse((match[1] ?? "").replace(/\s+/g, ""))) {
13
+ const absolute = absoluteSegments(segments, own);
14
+ if (absolute !== null) results.push({ line, candidates: candidatesFor(root, absolute) });
15
+ }
16
+ }
17
+ return results;
18
+ }
19
+
20
+ /** The directory holding the crate root: everything up to and including `src/`. */
21
+ function crateRoot(path ) {
22
+ const index = path.lastIndexOf("src/");
23
+ return index === -1 ? null : path.slice(0, index + "src/".length);
24
+ }
25
+
26
+ /** The module path of a file: `src/a/b.rs` and `src/a/b/mod.rs` are both `a::b`. */
27
+ function moduleSegments(path , root ) {
28
+ const rest = path.slice(root.length).replace(/\.rs$/, "");
29
+ const segments = rest.split("/");
30
+ const last = segments[segments.length - 1];
31
+ if (last === "mod" || (segments.length === 1 && (last === "lib" || last === "main"))) segments.pop();
32
+ return segments;
33
+ }
34
+
35
+ /** `a::{b, c::d}` becomes `[a, b]` and `[a, c, d]`. */
36
+ function expandUse(tree ) {
37
+ const brace = tree.indexOf("{");
38
+ if (brace === -1) return [tree.split("::").filter((s) => s !== "" && s !== "*").map((s) => s.split(" as ")[0] ?? s)];
39
+ const prefix = tree.slice(0, brace).split("::").filter((s) => s !== "");
40
+ const inner = tree.slice(brace + 1, tree.lastIndexOf("}"));
41
+ return splitTopLevel(inner).flatMap((part) => (part === "self" ? [prefix] : expandUse(part).map((rest) => [...prefix, ...rest])));
42
+ }
43
+
44
+ function splitTopLevel(inner ) {
45
+ const parts = [];
46
+ let depth = 0;
47
+ let current = "";
48
+ for (const char of inner) {
49
+ if (char === "{") depth++;
50
+ if (char === "}") depth--;
51
+ if (char === "," && depth === 0) {
52
+ parts.push(current);
53
+ current = "";
54
+ } else current += char;
55
+ }
56
+ if (current !== "") parts.push(current);
57
+ return parts.filter((part) => part !== "");
58
+ }
59
+
60
+ function absoluteSegments(segments , own ) {
61
+ const [head, ...rest] = segments;
62
+ if (head === "crate") return rest;
63
+ if (head === "self") return [...own, ...rest];
64
+ if (head === "super") {
65
+ let base = own.slice(0, -1);
66
+ let tail = rest;
67
+ while (tail[0] === "super") {
68
+ base = base.slice(0, -1);
69
+ tail = tail.slice(1);
70
+ }
71
+ return [...base, ...tail];
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /** The longest module path first: an imported item may be a type inside a module file. */
77
+ function candidatesFor(root , segments ) {
78
+ const candidates = [];
79
+ for (let n = segments.length; n > 0; n--) {
80
+ const module = segments.slice(0, n).join("/");
81
+ candidates.push(`${root}${module}.rs`, `${root}${module}/mod.rs`);
82
+ }
83
+ return candidates;
84
+ }
@@ -0,0 +1,17 @@
1
+ /** Counts top-level type declarations, by the file's language. */
2
+
3
+ const PATTERNS = [
4
+ { extensions: /\.(?:[cm]?[jt]sx?)$/, declaration: /^(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:class|interface|enum|type)\s+[A-Za-z_]/ },
5
+ { extensions: /\.py$/, declaration: /^class\s+[A-Za-z_]/ },
6
+ { extensions: /\.rs$/, declaration: /^(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait|union|type)\s+[A-Za-z_]/ },
7
+ { extensions: /\.go$/, declaration: /^type\s+[A-Za-z_]/ },
8
+ { extensions: /\.(?:c|h|cc|cpp|cxx|hh|hpp|hxx)$/, declaration: /^(?:typedef\s+)?(?:struct|class|enum|union)\s+[A-Za-z_]\w*(?:\s*[:{].*)?$/ },
9
+ { extensions: /\.(?:java|kt|swift|scala)$/, declaration: /^(?:(?:public|private|internal|open|final|abstract|data|sealed|static)\s+)*(?:class|interface|enum|object|struct|protocol|record)\s+[A-Za-z_]/ },
10
+ ];
11
+
12
+ /** Top-level type declarations in the file, or null when its language isn't recognised. */
13
+ export function countTypes(path , text ) {
14
+ const pattern = PATTERNS.find((p) => p.extensions.test(path));
15
+ if (pattern === undefined) return null;
16
+ return text.split("\n").filter((line) => pattern.declaration.test(line)).length;
17
+ }
@@ -0,0 +1,128 @@
1
+ import { parse as parseYaml } from "yaml";
2
+
3
+ /** `.cruze/config.yaml`, as described in the cruze-formats skill. */
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ /** Claude Code in print mode, allowed to read the project, its history and run `cruze`, but not to edit. */
23
+ export const DEFAULT_REVIEW_COMMAND = ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"];
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ /** Parses the config, collecting every problem; returns a config only when it is usable. */
34
+ export function parseConfig(text ) {
35
+ let raw ;
36
+ try {
37
+ raw = parseYaml(text);
38
+ } catch (error) {
39
+ return { config: null, problems: [`not valid YAML: ${error instanceof Error ? error.message : String(error)}`] };
40
+ }
41
+ const problems = [];
42
+ const root = asRecord(raw);
43
+ if (root === null) return { config: null, problems: ["the file must be a YAML mapping"] };
44
+
45
+ if (root["version"] !== 1) problems.push("version must be 1");
46
+ if (typeof root["project"] !== "string" || root["project"] === "") problems.push("project must be a non-empty string");
47
+ if (root["tracker"] !== "markdown") problems.push("tracker must be markdown");
48
+ const source = stringList(root["source"], "source", problems);
49
+ const tests = stringList(root["tests"], "tests", problems);
50
+
51
+ const check = asRecord(root["check"]) ?? {};
52
+ const maxLines = positiveInt(check["max_lines"], "check.max_lines", problems);
53
+ const maxTypes = positiveInt(check["max_types"], "check.max_types", problems);
54
+ const exceptions = (Array.isArray(check["exceptions"]) ? check["exceptions"] : []).flatMap((entry , i ) => {
55
+ const record = asRecord(entry);
56
+ if (typeof record?.["path"] !== "string" || typeof record["reason"] !== "string" || record["reason"].trim() === "") {
57
+ problems.push(`check.exceptions[${i}] needs a path and a non-empty reason`);
58
+ return [];
59
+ }
60
+ return [{ path: record["path"], reason: record["reason"] }];
61
+ });
62
+
63
+ const layers = (Array.isArray(root["layers"]) ? root["layers"] : []).flatMap((entry , i ) => {
64
+ const record = asRecord(entry);
65
+ if (typeof record?.["name"] !== "string") {
66
+ problems.push(`layers[${i}] needs a name`);
67
+ return [];
68
+ }
69
+ return [{ name: record["name"], paths: stringList(record["paths"], `layers[${i}].paths`, problems), mayImport: stringList(record["may_import"], `layers[${i}].may_import`, problems) }];
70
+ });
71
+ const names = layers.map((layer) => layer.name);
72
+ names.filter((name, i) => names.indexOf(name) !== i).forEach((name) => problems.push(`layer ${name} is defined twice`));
73
+ for (const layer of layers) {
74
+ for (const target of layer.mayImport.filter((t) => !names.includes(t))) {
75
+ problems.push(`layer ${layer.name} may_import names unknown layer ${target}`);
76
+ }
77
+ }
78
+
79
+ const commands = {};
80
+ const rawCommands = root["commands"];
81
+ if (rawCommands !== undefined && rawCommands !== null) {
82
+ const record = asRecord(rawCommands);
83
+ if (record === null) problems.push("commands must be a mapping of names to commands");
84
+ for (const [name, command] of Object.entries(record ?? {})) {
85
+ if (typeof command === "string" && command.trim() !== "") commands[name] = command;
86
+ else problems.push(`commands.${name} must be a command string`);
87
+ }
88
+ }
89
+ const review = asRecord(root["review"]) ?? {};
90
+ const reviewCommand = review["command"] === undefined ? DEFAULT_REVIEW_COMMAND : stringList(review["command"], "review.command", problems);
91
+ if (reviewCommand.length === 0) problems.push("review.command needs at least the program to run");
92
+
93
+ const config = {
94
+ version: 1,
95
+ project: typeof root["project"] === "string" ? root["project"] : "",
96
+ tracker: "markdown",
97
+ source,
98
+ tests,
99
+ check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
100
+ layers,
101
+ commands,
102
+ review: { command: reviewCommand.length === 0 ? DEFAULT_REVIEW_COMMAND : reviewCommand },
103
+ };
104
+ return { config, problems };
105
+ }
106
+
107
+ /** The first layer whose paths match the file, as the formats define. */
108
+ export function layerOf(config , path , matches ) {
109
+ return config.layers.find((layer) => matches(path, layer.paths));
110
+ }
111
+
112
+ function asRecord(value ) {
113
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? (value ) : null;
114
+ }
115
+
116
+ function stringList(value , key , problems ) {
117
+ if (value === undefined || value === null) return [];
118
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return value ;
119
+ problems.push(`${key} must be a list of strings`);
120
+ return [];
121
+ }
122
+
123
+ function positiveInt(value , key , problems ) {
124
+ if (value === undefined) return undefined;
125
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
126
+ problems.push(`${key} must be a positive whole number`);
127
+ return undefined;
128
+ }
@@ -0,0 +1,10 @@
1
+ /** An expected failure the CLI reports to the user, as opposed to a bug. */
2
+ export class CruzeError extends Error {
3
+ code ;
4
+
5
+ constructor(code , message ) {
6
+ super(message);
7
+ this.name = "CruzeError";
8
+ this.code = code;
9
+ }
10
+ }
@@ -0,0 +1,44 @@
1
+ import { factsOf, statusOf } from "../elements.js";
2
+
3
+
4
+ /**
5
+ * The managed sections of a project's `AGENTS.md`: its commands, from `.cruze/config.yaml`,
6
+ * and its layout, from the built modules in `docs/architecture.md`.
7
+ */
8
+
9
+ const OPEN = "<!-- cruze:managed -->";
10
+ const CLOSE = "<!-- /cruze:managed -->";
11
+
12
+ /** Rewrites the managed blocks under `## Commands` and `## Layout`; other text is left alone. */
13
+ export function renderAgentsFile(text , view ) {
14
+ let next = text;
15
+ const commands = Object.entries(view.config?.config?.commands ?? {});
16
+ if (commands.length > 0) next = replaceBlock(next, "Commands", commands.map(([name, command]) => `- ${name}: \`${command}\``));
17
+ const modules = builtModules(view);
18
+ if (modules.length > 0) next = replaceBlock(next, "Layout", ["Modules, from `docs/architecture.md`:", "", ...modules]);
19
+ return next;
20
+ }
21
+
22
+ function builtModules(view ) {
23
+ return [...view.living.elements.values()]
24
+ .filter(({ element, doc }) => element.kind === "MOD" && statusOf(doc, element) === "built")
25
+ .map(({ element, doc }) => {
26
+ const facts = factsOf(doc, element);
27
+ const paths = (facts.get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => `\`${m[1] ?? ""}\``));
28
+ return `- ${paths.join(", ")} (${facts.get("Layer")?.[0] ?? "unknown"}): ${element.title}`;
29
+ })
30
+ .sort();
31
+ }
32
+
33
+ /** Replaces the body of the first managed block after `## <heading>`, before the next `## ` heading. */
34
+ function replaceBlock(text , heading , body ) {
35
+ const lines = text.split("\n");
36
+ const at = lines.findIndex((line) => line === `## ${heading}`);
37
+ if (at === -1) return text;
38
+ const sectionEnd = lines.findIndex((line, i) => i > at && line.startsWith("## "));
39
+ const end = sectionEnd === -1 ? lines.length : sectionEnd;
40
+ const open = lines.findIndex((line, i) => i > at && i < end && line.trim() === OPEN);
41
+ const close = lines.findIndex((line, i) => i > open && i < end && line.trim() === CLOSE);
42
+ if (open === -1 || close === -1) return text;
43
+ return [...lines.slice(0, open + 1), ...body, ...lines.slice(close)].join("\n");
44
+ }
@@ -0,0 +1,60 @@
1
+ import { parseMarkdown } from "../markdown.js";
2
+ import { splitRow, tablesIn } from "../markdown_tables.js";
3
+
4
+ /**
5
+ * Edits to the managed tables: the feature map in vision.md and the status table
6
+ * in roadmap.md. Rows are keyed by their first cell.
7
+ */
8
+
9
+ /** Replaces or appends the row keyed by `cells[0]` in the table under a heading. */
10
+ export function upsertRow(text , heading , cells ) {
11
+ return editTable(text, heading, (rows) => {
12
+ const index = rows.findIndex((row) => row[0] === cells[0]);
13
+ if (index === -1) rows.push(cells);
14
+ else rows[index] = cells;
15
+ });
16
+ }
17
+
18
+ export function removeRow(text , heading , key ) {
19
+ let removed = null;
20
+ const next = editTable(text, heading, (rows) => {
21
+ const index = rows.findIndex((row) => row[0] === key);
22
+ if (index !== -1) removed = rows.splice(index, 1)[0] ?? null;
23
+ });
24
+ return { text: next, removed };
25
+ }
26
+
27
+ export function readRows(text , heading ) {
28
+ const doc = parseMarkdown(text);
29
+ const at = doc.headings.find((h) => h.text === heading);
30
+ if (at === undefined) return [];
31
+ const end = doc.headings.find((h) => h.line > at.line && h.level <= at.level)?.line ?? doc.lines.length;
32
+ return tablesIn(doc.lines, doc.inFence, at.line + 1, end)[0]?.rows.map((row) => row.cells) ?? [];
33
+ }
34
+
35
+ function editTable(text , heading , edit ) {
36
+ const doc = parseMarkdown(text);
37
+ const at = doc.headings.find((h) => h.text === heading);
38
+ if (at === undefined) throw new Error(`No "${heading}" heading to edit`);
39
+ const end = doc.headings.find((h) => h.line > at.line && h.level <= at.level)?.line ?? doc.lines.length;
40
+ let first = -1;
41
+ let last = -1;
42
+ for (let i = at.line + 1; i < end; i++) {
43
+ if (/^\s*\|.*\|\s*$/.test(doc.lines[i] ?? "")) {
44
+ if (first === -1) first = i;
45
+ last = i;
46
+ } else if (first !== -1) break;
47
+ }
48
+ if (first === -1) throw new Error(`No table under "${heading}"`);
49
+ const header = splitRow(doc.lines[first] ?? "");
50
+ const rows = doc.lines.slice(first + 2, last + 1).map(splitRow);
51
+ edit(rows);
52
+ const rendered = [renderRow(header), renderRow(header.map(() => "---")), ...rows.map(renderRow)];
53
+ const lines = [...doc.lines];
54
+ lines.splice(first, last - first + 1, ...rendered);
55
+ return lines.join("\n");
56
+ }
57
+
58
+ function renderRow(cells ) {
59
+ return `| ${cells.join(" | ")} |`;
60
+ }
@@ -0,0 +1,33 @@
1
+ import { MANAGED_CLOSE, MANAGED_OPEN, parseMarkdown } from "../markdown.js";
2
+ import { readTasks } from "../project/plan_parts.js";
3
+ import { progressRange, readProgress, } from "../project/progress.js";
4
+
5
+ /** Rewrites the managed `## Progress` block of a feature or change from a changed Progress. */
6
+ export function updateProgress(text , change ) {
7
+ const doc = parseMarkdown(text);
8
+ const progress = readProgress(doc);
9
+ for (const task of readTasks(doc).tasks) if (!progress.tasks.has(task.number)) progress.tasks.set(task.number, null);
10
+ change(progress);
11
+ const block = [MANAGED_OPEN, "## Progress", ...renderProgress(progress), MANAGED_CLOSE];
12
+ const range = progressRange(doc);
13
+ const lines = [...doc.lines];
14
+ if (range === null) {
15
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
16
+ return [...lines, "", ...block, ""].join("\n");
17
+ }
18
+ lines.splice(range.open, range.end - range.open + 1, ...block);
19
+ return lines.join("\n");
20
+ }
21
+
22
+ function renderProgress(progress ) {
23
+ const lines = [];
24
+ if (progress.branch !== undefined) lines.push(`- Branch: ${progress.branch}`);
25
+ for (const [change, state] of progress.changes) lines.push(`- ${change}: ${state}`);
26
+ const ordered = [...progress.tasks].sort(([a], [b]) => Number(a.slice(1)) - Number(b.slice(1)));
27
+ for (const [task, commit] of ordered) {
28
+ lines.push(commit === null ? `- [ ] ${task}` : commit === "" ? `- [x] ${task}` : `- [x] ${task} (${commit})`);
29
+ }
30
+ for (const deviation of progress.deviations) lines.push(`- Deviation ${deviation}`);
31
+ if (progress.landed !== undefined) lines.push(`- Landed: ${progress.landed}`);
32
+ return lines;
33
+ }
@@ -0,0 +1,52 @@
1
+ import { headingElements, isStatusLine, statusOf, } from "../elements.js";
2
+ import { STATUS_KINDS } from "../ids.js";
3
+ import { parseMarkdown, } from "../markdown.js";
4
+
5
+ /** Managed `- Status:` lines: only the CLI adds or changes them. */
6
+
7
+ /** Sets one element's status, adding the line after its facts when it has none. Returns the new text. */
8
+ export function setStatus(text , id , status ) {
9
+ const doc = parseMarkdown(text);
10
+ const element = headingElements(doc).find((e) => e.id === id);
11
+ if (element === undefined) throw new Error(`${id} is not in this document`);
12
+ const lines = [...doc.lines];
13
+ const at = statusLineIndex(doc, element.start, element.end);
14
+ if (at !== null) lines[at] = `- Status: ${status}`;
15
+ else lines.splice(insertionPoint(doc, element.start, element.end), 0, `- Status: ${status}`);
16
+ return lines.join("\n");
17
+ }
18
+
19
+ /** Adds `- Status: planned` to every status-carrying element that has none. */
20
+ export function stampMissingStatuses(text ) {
21
+ let current = text;
22
+ const stamped = [];
23
+ for (const element of headingElements(parseMarkdown(text))) {
24
+ if (!STATUS_KINDS.includes(element.kind)) continue;
25
+ const doc = parseMarkdown(current);
26
+ const fresh = headingElements(doc).find((e) => e.id === element.id);
27
+ if (fresh === undefined || statusOf(doc, fresh) !== undefined) continue;
28
+ current = setStatus(current, element.id, "planned");
29
+ stamped.push(element.id);
30
+ }
31
+ return { text: current, stamped };
32
+ }
33
+
34
+ function statusLineIndex(doc , start , end ) {
35
+ for (let i = start + 1; i < end; i++) {
36
+ if (!doc.inFence[i] && /^#{1,6} /.test(doc.lines[i] ?? "")) return null;
37
+ if (!doc.inFence[i] && isStatusLine(doc.lines[i] ?? "")) return i;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /** After the element's last top-level fact or step, before any diagram or nested heading. */
43
+ function insertionPoint(doc , start , end ) {
44
+ let point = start + 1;
45
+ for (let i = start + 1; i < end; i++) {
46
+ const line = doc.lines[i] ?? "";
47
+ if (doc.inFence[i] || /^#{1,6} /.test(line)) break;
48
+ if (/^(- |\s{2,}- )/.test(line)) point = i + 1;
49
+ else if (line.trim() !== "" && point > start + 1) break;
50
+ }
51
+ return point;
52
+ }
@@ -0,0 +1,126 @@
1
+ import { ID_PATTERN, STATUS_KINDS, kindOf, } from "./ids.js";
2
+ import { sectionEnd, } from "./markdown.js";
3
+
4
+
5
+
6
+
7
+ /** One element: a heading `<ID>: <Title>` and its body, or a table row whose first cell is an ID. */
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+ /** A delta heading that creates a spec file: `### ADDED CAPABILITY <name>: <Title>`. */
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+ const HEADING = new RegExp(`^(?:(ADDED|MODIFIED|REMOVED) )?(${ID_PATTERN}): (.+)$`);
30
+ const CAPABILITY = /^ADDED CAPABILITY ([a-z0-9]+(?:-[a-z0-9]+)*): (.+)$/;
31
+ const ROW = new RegExp(`^\\|\\s*(${ID_PATTERN})\\s*\\|(.*)\\|\\s*$`);
32
+ const STATUS_LINE = /^- Status: (planned|built)\s*$/;
33
+
34
+ /** Heading elements whose heading lies in [from, to). */
35
+ export function headingElements(doc , from = 0, to = doc.lines.length) {
36
+ const elements = [];
37
+ for (const heading of doc.headings) {
38
+ if (heading.line < from || heading.line >= to) continue;
39
+ const match = HEADING.exec(heading.text);
40
+ if (match?.[2] === undefined || match[3] === undefined) continue;
41
+ const element = {
42
+ id: match[2],
43
+ kind: kindOf(match[2]),
44
+ title: match[3],
45
+ form: "heading",
46
+ level: heading.level,
47
+ start: heading.line,
48
+ end: Math.min(sectionEnd(doc, heading), to),
49
+ };
50
+ if (match[1] !== undefined) element.op = match[1] ;
51
+ elements.push(element);
52
+ }
53
+ return elements;
54
+ }
55
+
56
+ export function capabilityOps(doc , from = 0, to = doc.lines.length) {
57
+ return doc.headings
58
+ .filter((h) => h.line >= from && h.line < to)
59
+ .flatMap((heading) => {
60
+ const match = CAPABILITY.exec(heading.text);
61
+ if (match?.[1] === undefined || match[2] === undefined) return [];
62
+ return [{ capability: match[1], title: match[2], start: heading.line, end: Math.min(sectionEnd(doc, heading), to) }];
63
+ });
64
+ }
65
+
66
+ /** Row elements: table rows, outside code fences, whose first cell is an ID. */
67
+ export function rowElements(doc ) {
68
+ return doc.lines.flatMap((line, index) => {
69
+ if (doc.inFence[index]) return [];
70
+ const match = ROW.exec(line);
71
+ if (match?.[1] === undefined) return [];
72
+ const title = match[2]?.split("|")[0]?.trim() ?? "";
73
+ return [{ id: match[1], kind: kindOf(match[1]), title, form: "row" , level: 0, start: index, end: index + 1 }];
74
+ });
75
+ }
76
+
77
+ export function elementLines(doc , element ) {
78
+ return doc.lines.slice(element.start, element.end);
79
+ }
80
+
81
+ export function isStatusLine(line ) {
82
+ return STATUS_LINE.test(line);
83
+ }
84
+
85
+ /** The element's own managed status: the first status line before any nested heading. */
86
+ /**
87
+ * Whether an element is still to be built: marked `planned`, or a kind that carries a status
88
+ * but not stamped yet, because the document holding it has never been approved.
89
+ */
90
+ export function isUnbuilt(doc , element ) {
91
+ const status = statusOf(doc, element);
92
+ return status === "planned" || (status === undefined && STATUS_KINDS.includes(element.kind));
93
+ }
94
+
95
+ export function statusOf(doc , element ) {
96
+ for (let i = element.start + 1; i < element.end; i++) {
97
+ if (/^#{1,6} /.test(doc.lines[i] ?? "") && !doc.inFence[i]) return undefined;
98
+ const match = STATUS_LINE.exec(doc.lines[i] ?? "");
99
+ if (match?.[1] !== undefined) return match[1] ;
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /**
105
+ * The element's top-level facts: `- Key: value` items and their indented sub-items.
106
+ * Nested elements' facts are excluded.
107
+ */
108
+ export function factsOf(doc , element ) {
109
+ const facts = new Map ();
110
+ let current = null;
111
+ for (let i = element.start + 1; i < element.end; i++) {
112
+ const line = doc.lines[i] ?? "";
113
+ if (doc.inFence[i]) continue;
114
+ if (/^#{1,6} /.test(line)) break;
115
+ const fact = /^- ([A-Z][A-Za-z ]*): ?(.*)$/.exec(line);
116
+ if (fact?.[1] !== undefined) {
117
+ current = fact[2] === undefined || fact[2] === "" ? [] : [fact[2]];
118
+ facts.set(fact[1], current);
119
+ continue;
120
+ }
121
+ const sub = /^\s{2,}- (.+)$/.exec(line);
122
+ if (sub?.[1] !== undefined && current !== null) current.push(sub[1]);
123
+ else if (line.trim() !== "") current = null;
124
+ }
125
+ return facts;
126
+ }