@printpractical/cruze 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/README.md +61 -118
  2. package/dist/adapters/inbound/cli/cli_context.js +0 -27
  3. package/dist/adapters/inbound/cli/commands/code.js +5 -18
  4. package/dist/adapters/inbound/cli/commands/documents.js +11 -37
  5. package/dist/adapters/inbound/cli/commands/init.js +1 -1
  6. package/dist/adapters/inbound/cli/commands/review.js +2 -9
  7. package/dist/adapters/inbound/cli/commands/status.js +16 -63
  8. package/dist/adapters/inbound/cli/commands.js +10 -37
  9. package/dist/adapters/inbound/cli/parse_command.js +0 -54
  10. package/dist/adapters/outbound/git_repository.js +2 -71
  11. package/dist/app/ports/repository.js +0 -12
  12. package/dist/app/project_context.js +15 -25
  13. package/dist/app/use_cases/approve_document.js +29 -0
  14. package/dist/app/use_cases/check_code.js +21 -56
  15. package/dist/app/use_cases/new_document.js +25 -0
  16. package/dist/app/use_cases/project_status.js +43 -67
  17. package/dist/app/use_cases/run_review.js +11 -30
  18. package/dist/app/use_cases/validate_project.js +4 -4
  19. package/dist/domain/approvals.js +51 -0
  20. package/dist/domain/check/check_source.js +2 -31
  21. package/dist/domain/config.js +8 -84
  22. package/dist/domain/hashing.js +16 -30
  23. package/dist/domain/ids.js +13 -27
  24. package/dist/domain/next_step.js +41 -0
  25. package/dist/domain/project/features.js +71 -0
  26. package/dist/domain/project/layout.js +11 -27
  27. package/dist/domain/project/project_docs.js +48 -0
  28. package/dist/domain/project/release.js +30 -0
  29. package/dist/domain/scaffold.js +0 -1
  30. package/dist/domain/skill_budget.js +4 -4
  31. package/dist/domain/trace.js +8 -25
  32. package/dist/domain/validation/document_rules.js +70 -0
  33. package/dist/domain/validation/release_rules.js +45 -0
  34. package/dist/domain/validation/validate_project.js +20 -27
  35. package/dist/domain/versions/cruze_version.js +3 -9
  36. package/dist/main.js +1 -1
  37. package/package.json +1 -1
  38. package/skills/asking/SKILL.md +51 -0
  39. package/skills/behavioural-testing/SKILL.md +14 -16
  40. package/skills/behavioural-testing/examples.md +1 -2
  41. package/skills/behavioural-testing/fakes.md +2 -2
  42. package/skills/build/SKILL.md +34 -38
  43. package/skills/dependency-approval/SKILL.md +34 -31
  44. package/skills/envision/SKILL.md +23 -18
  45. package/skills/envision/architecture.md +16 -0
  46. package/skills/envision/breakdown.md +14 -0
  47. package/skills/envision/possibilities.md +6 -7
  48. package/skills/fix/SKILL.md +34 -0
  49. package/skills/formats/SKILL.md +39 -87
  50. package/skills/formats/config.md +47 -0
  51. package/skills/formats/templates/architecture.md +18 -74
  52. package/skills/formats/templates/feature.md +18 -34
  53. package/skills/formats/templates/vision.md +15 -27
  54. package/skills/hexagonal-design/SKILL.md +11 -11
  55. package/skills/hexagonal-design/contracts.md +3 -3
  56. package/skills/hexagonal-design/fundamentals.md +1 -1
  57. package/skills/hexagonal-design/languages/cpp.md +1 -1
  58. package/skills/hexagonal-design/languages/rust.md +1 -1
  59. package/skills/hexagonal-design/languages/swift.md +2 -2
  60. package/skills/hexagonal-design/runtime.md +2 -2
  61. package/skills/research/SKILL.md +4 -4
  62. package/skills/research/evaluation.md +1 -1
  63. package/skills/review/SKILL.md +27 -0
  64. package/skills/roles/SKILL.md +8 -59
  65. package/skills/roles/code-reviewer.md +21 -32
  66. package/skills/roles/researcher.md +3 -3
  67. package/skills/shape/SKILL.md +37 -0
  68. package/skills/shape/self-check.md +12 -0
  69. package/src/adapters/inbound/cli/cli_context.ts +0 -27
  70. package/src/adapters/inbound/cli/commands/code.ts +5 -18
  71. package/src/adapters/inbound/cli/commands/documents.ts +11 -37
  72. package/src/adapters/inbound/cli/commands/init.ts +1 -1
  73. package/src/adapters/inbound/cli/commands/review.ts +2 -9
  74. package/src/adapters/inbound/cli/commands/status.ts +16 -63
  75. package/src/adapters/inbound/cli/commands.ts +10 -37
  76. package/src/adapters/inbound/cli/parse_command.ts +0 -54
  77. package/src/adapters/outbound/git_repository.ts +2 -71
  78. package/src/app/ports/repository.ts +0 -12
  79. package/src/app/project_context.ts +15 -25
  80. package/src/app/use_cases/approve_document.ts +29 -0
  81. package/src/app/use_cases/check_code.ts +21 -56
  82. package/src/app/use_cases/new_document.ts +25 -0
  83. package/src/app/use_cases/project_status.ts +49 -73
  84. package/src/app/use_cases/run_review.ts +11 -30
  85. package/src/app/use_cases/validate_project.ts +4 -4
  86. package/src/domain/approvals.ts +51 -0
  87. package/src/domain/check/check_source.ts +2 -31
  88. package/src/domain/config.ts +9 -85
  89. package/src/domain/hashing.ts +16 -30
  90. package/src/domain/ids.ts +13 -27
  91. package/src/domain/next_step.ts +41 -0
  92. package/src/domain/project/features.ts +71 -0
  93. package/src/domain/project/layout.ts +11 -27
  94. package/src/domain/project/project_docs.ts +48 -0
  95. package/src/domain/project/release.ts +30 -0
  96. package/src/domain/scaffold.ts +0 -1
  97. package/src/domain/skill_budget.ts +4 -4
  98. package/src/domain/trace.ts +8 -25
  99. package/src/domain/validation/document_rules.ts +70 -0
  100. package/src/domain/validation/release_rules.ts +45 -0
  101. package/src/domain/validation/validate_project.ts +21 -28
  102. package/src/domain/versions/cruze_version.ts +3 -9
  103. package/src/main.ts +1 -1
  104. package/templates/init/AGENTS.md +4 -12
  105. package/templates/init/ci.yml +3 -3
  106. package/templates/init/config.yaml +8 -17
  107. package/templates/init/review/claude.yaml +2 -2
  108. package/dist/adapters/inbound/cli/argument_files.js +0 -29
  109. package/dist/adapters/inbound/cli/commands/diff.js +0 -23
  110. package/dist/adapters/inbound/cli/commands/journal.js +0 -32
  111. package/dist/adapters/inbound/cli/commands/progress.js +0 -55
  112. package/dist/adapters/inbound/cli/commands/realign.js +0 -28
  113. package/dist/adapters/inbound/cli/commands/show.js +0 -96
  114. package/dist/app/use_cases/abandon_work.js +0 -42
  115. package/dist/app/use_cases/approve_artifact.js +0 -197
  116. package/dist/app/use_cases/journal_events.js +0 -64
  117. package/dist/app/use_cases/land_change.js +0 -134
  118. package/dist/app/use_cases/new_project_doc.js +0 -48
  119. package/dist/app/use_cases/new_work_item.js +0 -75
  120. package/dist/app/use_cases/realign_done.js +0 -45
  121. package/dist/app/use_cases/realign_status.js +0 -106
  122. package/dist/app/use_cases/record_progress.js +0 -118
  123. package/dist/app/use_cases/show_elements.js +0 -18
  124. package/dist/app/use_cases/show_notes.js +0 -11
  125. package/dist/app/use_cases/show_scope.js +0 -51
  126. package/dist/app/use_cases/upstream_diff.js +0 -55
  127. package/dist/domain/approvals/agent_approval.js +0 -68
  128. package/dist/domain/approvals/evaluate.js +0 -36
  129. package/dist/domain/approvals/fingerprint.js +0 -52
  130. package/dist/domain/approvals/rebase.js +0 -39
  131. package/dist/domain/approvals/records.js +0 -50
  132. package/dist/domain/approvals/upstream_text.js +0 -36
  133. package/dist/domain/architecture_sections.js +0 -23
  134. package/dist/domain/check/decision_comments.js +0 -38
  135. package/dist/domain/decision_citations.js +0 -62
  136. package/dist/domain/edits/agents_file.js +0 -58
  137. package/dist/domain/edits/managed_tables.js +0 -60
  138. package/dist/domain/edits/progress_block.js +0 -33
  139. package/dist/domain/edits/status_lines.js +0 -52
  140. package/dist/domain/elements.js +0 -126
  141. package/dist/domain/journal.js +0 -109
  142. package/dist/domain/land/bookkeeping.js +0 -54
  143. package/dist/domain/land/merge_plan.js +0 -137
  144. package/dist/domain/land/text_ops.js +0 -66
  145. package/dist/domain/line_diff.js +0 -56
  146. package/dist/domain/project/artifact_ref.js +0 -42
  147. package/dist/domain/project/deltas.js +0 -49
  148. package/dist/domain/project/element_lookup.js +0 -124
  149. package/dist/domain/project/living_model.js +0 -55
  150. package/dist/domain/project/new_items.js +0 -48
  151. package/dist/domain/project/notes.js +0 -74
  152. package/dist/domain/project/plan_parts.js +0 -111
  153. package/dist/domain/project/progress.js +0 -43
  154. package/dist/domain/project/project_view.js +0 -72
  155. package/dist/domain/project/roadmap.js +0 -43
  156. package/dist/domain/project/scope_view.js +0 -134
  157. package/dist/domain/project/work_items.js +0 -68
  158. package/dist/domain/status/next_step.js +0 -181
  159. package/dist/domain/status/pending_rethink.js +0 -22
  160. package/dist/domain/status/verification.js +0 -29
  161. package/dist/domain/status/work_status.js +0 -125
  162. package/dist/domain/validation/decision_rules.js +0 -76
  163. package/dist/domain/validation/delta_rules.js +0 -81
  164. package/dist/domain/validation/element_rules.js +0 -104
  165. package/dist/domain/validation/future_rules.js +0 -40
  166. package/dist/domain/validation/living_rules.js +0 -99
  167. package/dist/domain/validation/plan_rules.js +0 -124
  168. package/dist/domain/validation/project_rules.js +0 -115
  169. package/dist/domain/validation/size_rules.js +0 -45
  170. package/dist/domain/validation/vision_rules.js +0 -41
  171. package/dist/domain/validation/work_item_rules.js +0 -175
  172. package/dist/domain/versions/realign_notes.js +0 -72
  173. package/dist/domain/versions/rule_versions.js +0 -29
  174. package/dist/domain/versions/scheduled_exceptions.js +0 -41
  175. package/skills/about/SKILL.md +0 -21
  176. package/skills/architect/SKILL.md +0 -49
  177. package/skills/architect/feature.md +0 -40
  178. package/skills/architect/project.md +0 -71
  179. package/skills/architect/tweak.md +0 -22
  180. package/skills/architect/walkthrough.md +0 -35
  181. package/skills/domain-language/SKILL.md +0 -37
  182. package/skills/envision/feature.md +0 -25
  183. package/skills/envision/intake.md +0 -28
  184. package/skills/envision/project.md +0 -24
  185. package/skills/explore/SKILL.md +0 -39
  186. package/skills/formats/reference/architecture.md +0 -84
  187. package/skills/formats/reference/config.md +0 -80
  188. package/skills/formats/reference/durable-docs.md +0 -74
  189. package/skills/formats/reference/specs.md +0 -65
  190. package/skills/formats/reference/work-items.md +0 -100
  191. package/skills/formats/templates/adr.md +0 -7
  192. package/skills/formats/templates/change.md +0 -29
  193. package/skills/formats/templates/glossary.md +0 -8
  194. package/skills/formats/templates/roadmap.md +0 -32
  195. package/skills/formats/templates/spec.md +0 -11
  196. package/skills/formats/templates/standalone-change.md +0 -46
  197. package/skills/grilling/SKILL.md +0 -90
  198. package/skills/grilling/illustrating.md +0 -38
  199. package/skills/improve/SKILL.md +0 -51
  200. package/skills/land/SKILL.md +0 -48
  201. package/skills/next/SKILL.md +0 -31
  202. package/skills/plan/SKILL.md +0 -62
  203. package/skills/plan/tasks.md +0 -35
  204. package/skills/plan/test-plan.md +0 -14
  205. package/skills/realign/SKILL.md +0 -37
  206. package/skills/realign/notes/0.0.10.md +0 -131
  207. package/skills/realign/notes/0.0.11.md +0 -15
  208. package/skills/realign/notes/0.0.12.md +0 -31
  209. package/skills/realign/notes/0.0.13.md +0 -11
  210. package/skills/realign/notes/0.0.2.md +0 -69
  211. package/skills/realign/notes/0.0.3.md +0 -11
  212. package/skills/realign/notes/0.0.4.md +0 -27
  213. package/skills/realign/notes/0.0.5.md +0 -38
  214. package/skills/realign/notes/0.0.6.md +0 -44
  215. package/skills/realign/notes/0.0.7.md +0 -11
  216. package/skills/realign/notes/0.0.8.md +0 -97
  217. package/skills/realign/notes/0.0.9.md +0 -59
  218. package/skills/realign/notes.md +0 -40
  219. package/skills/rethink/SKILL.md +0 -62
  220. package/skills/rethink/levels.md +0 -54
  221. package/skills/roadmap/SKILL.md +0 -54
  222. package/skills/roles/auditor.md +0 -35
  223. package/skills/roles/design-reviewer.md +0 -43
  224. package/skills/roles/design-rubric.md +0 -41
  225. package/skills/roles/plan-rubric.md +0 -11
  226. package/skills/roles/surveyor.md +0 -41
  227. package/skills/roles/verifier.md +0 -42
  228. package/skills/triage/SKILL.md +0 -37
  229. package/skills/verify/SKILL.md +0 -45
  230. package/skills/verify/manual-test.md +0 -23
  231. package/src/adapters/inbound/cli/argument_files.ts +0 -29
  232. package/src/adapters/inbound/cli/commands/diff.ts +0 -23
  233. package/src/adapters/inbound/cli/commands/journal.ts +0 -32
  234. package/src/adapters/inbound/cli/commands/progress.ts +0 -55
  235. package/src/adapters/inbound/cli/commands/realign.ts +0 -28
  236. package/src/adapters/inbound/cli/commands/show.ts +0 -96
  237. package/src/app/use_cases/abandon_work.ts +0 -42
  238. package/src/app/use_cases/approve_artifact.ts +0 -197
  239. package/src/app/use_cases/journal_events.ts +0 -64
  240. package/src/app/use_cases/land_change.ts +0 -134
  241. package/src/app/use_cases/new_project_doc.ts +0 -48
  242. package/src/app/use_cases/new_work_item.ts +0 -75
  243. package/src/app/use_cases/realign_done.ts +0 -45
  244. package/src/app/use_cases/realign_status.ts +0 -106
  245. package/src/app/use_cases/record_progress.ts +0 -118
  246. package/src/app/use_cases/show_elements.ts +0 -18
  247. package/src/app/use_cases/show_notes.ts +0 -11
  248. package/src/app/use_cases/show_scope.ts +0 -51
  249. package/src/app/use_cases/upstream_diff.ts +0 -55
  250. package/src/domain/approvals/agent_approval.ts +0 -68
  251. package/src/domain/approvals/evaluate.ts +0 -36
  252. package/src/domain/approvals/fingerprint.ts +0 -52
  253. package/src/domain/approvals/rebase.ts +0 -39
  254. package/src/domain/approvals/records.ts +0 -50
  255. package/src/domain/approvals/upstream_text.ts +0 -36
  256. package/src/domain/architecture_sections.ts +0 -23
  257. package/src/domain/check/decision_comments.ts +0 -38
  258. package/src/domain/decision_citations.ts +0 -62
  259. package/src/domain/edits/agents_file.ts +0 -58
  260. package/src/domain/edits/managed_tables.ts +0 -60
  261. package/src/domain/edits/progress_block.ts +0 -33
  262. package/src/domain/edits/status_lines.ts +0 -52
  263. package/src/domain/elements.ts +0 -126
  264. package/src/domain/journal.ts +0 -109
  265. package/src/domain/land/bookkeeping.ts +0 -54
  266. package/src/domain/land/merge_plan.ts +0 -137
  267. package/src/domain/land/text_ops.ts +0 -66
  268. package/src/domain/line_diff.ts +0 -56
  269. package/src/domain/project/artifact_ref.ts +0 -42
  270. package/src/domain/project/deltas.ts +0 -49
  271. package/src/domain/project/element_lookup.ts +0 -124
  272. package/src/domain/project/living_model.ts +0 -55
  273. package/src/domain/project/new_items.ts +0 -48
  274. package/src/domain/project/notes.ts +0 -74
  275. package/src/domain/project/plan_parts.ts +0 -111
  276. package/src/domain/project/progress.ts +0 -43
  277. package/src/domain/project/project_view.ts +0 -72
  278. package/src/domain/project/roadmap.ts +0 -43
  279. package/src/domain/project/scope_view.ts +0 -134
  280. package/src/domain/project/work_items.ts +0 -68
  281. package/src/domain/status/next_step.ts +0 -181
  282. package/src/domain/status/pending_rethink.ts +0 -22
  283. package/src/domain/status/verification.ts +0 -29
  284. package/src/domain/status/work_status.ts +0 -125
  285. package/src/domain/validation/decision_rules.ts +0 -76
  286. package/src/domain/validation/delta_rules.ts +0 -81
  287. package/src/domain/validation/element_rules.ts +0 -104
  288. package/src/domain/validation/future_rules.ts +0 -40
  289. package/src/domain/validation/living_rules.ts +0 -99
  290. package/src/domain/validation/plan_rules.ts +0 -124
  291. package/src/domain/validation/project_rules.ts +0 -115
  292. package/src/domain/validation/size_rules.ts +0 -45
  293. package/src/domain/validation/vision_rules.ts +0 -41
  294. package/src/domain/validation/work_item_rules.ts +0 -175
  295. package/src/domain/versions/realign_notes.ts +0 -72
  296. package/src/domain/versions/rule_versions.ts +0 -29
  297. package/src/domain/versions/scheduled_exceptions.ts +0 -41
  298. package/templates/init/gitattributes +0 -2
@@ -1,64 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { knownPitfalls } from "../../domain/edits/agents_file.ts";
3
- import { journalPath, parseJournal, recordedEventProblems, recordedFields, type JournalEntry } from "../../domain/journal.ts";
4
- import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
5
- import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
6
-
7
- /** Records an event a skill observed: a rethink, a review round, a disposition, an override or a bug. */
8
- export async function recordEvent(deps: ProjectDeps, event: string, fields: Record<string, string>, item?: string): Promise<JournalEntry> {
9
- const problems = recordedEventProblems(event, fields);
10
- if (problems.length > 0) throw new CruzeError("invalid-event", problems.join("; "));
11
- let folder = ".cruze";
12
- let recorded = recordedFields(fields);
13
- if (item !== undefined) {
14
- const resolved = resolveArtifact(await loadView(deps.files), item).item;
15
- if (resolved === undefined) throw new CruzeError("not-found", `"${item}" is not a feature or change`);
16
- folder = resolved.folder;
17
- recorded = { ...recorded, item: resolved.ref };
18
- }
19
- return appendJournal(deps, folder, event, { ...recorded, cruze: deps.bundle.version });
20
- }
21
-
22
- /**
23
- * The journal of the work in flight and the project, or of one feature or change (a feature's changes' too).
24
- * Landed work's entries are history, listed only with `all`, since every entry ever recorded is too much to read.
25
- */
26
- export async function listEvents(deps: ProjectDeps, event?: string, item?: string, all = false): Promise<JournalEntry[]> {
27
- const view = await loadView(deps.files);
28
- let folders = [".cruze", ...view.items.filter((i) => all || !i.archived).map((i) => i.folder)];
29
- if (item !== undefined) {
30
- const resolved = resolveArtifact(view, item).item;
31
- if (resolved === undefined) throw new CruzeError("not-found", `"${item}" is not a feature or change`);
32
- folders = [resolved, ...view.items.filter((i) => resolved.kind === "feature" && i.featureRef === resolved.ref)].map((part) => part.folder);
33
- }
34
- const entries = folders.flatMap((folder) => parseJournal(view.snapshot.get(journalPath(folder))).entries).sort((a, b) => a.at.localeCompare(b.at));
35
- return event === undefined ? entries : entries.filter((entry) => entry.event === event);
36
- }
37
-
38
- export interface FeedbackExport {
39
- format: "cruze-feedback/1";
40
- cruze: string;
41
- exported_at: string;
42
- redacted: boolean;
43
- entries: JournalEntry[];
44
- /** The lines under `## Known pitfalls` in `AGENTS.md`: lessons that may belong in Cruze rather than the project. */
45
- pitfalls: string[];
46
- }
47
-
48
- /**
49
- * Bundles the journal, and the project's known pitfalls, for the framework feedback loop. By default the project name and
50
- * the approver's identity are redacted, since the bundle may leave the project.
51
- */
52
- export async function exportFeedback(deps: ProjectDeps, redact: boolean): Promise<FeedbackExport> {
53
- const view = await loadView(deps.files);
54
- const project = view.config?.config?.project ?? "";
55
- const scrub = (value: unknown): unknown => {
56
- if (typeof value === "string") return project === "" ? value : value.split(project).join("<project>");
57
- if (Array.isArray(value)) return value.map(scrub);
58
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrub(v)]));
59
- return value;
60
- };
61
- const entries = redact ? view.journal.map((entry) => ({ ...(scrub(entry) as JournalEntry), by: "<redacted>" })) : view.journal;
62
- const pitfalls = knownPitfalls((await deps.files.readText("AGENTS.md")) ?? "").map((line) => (redact ? (scrub(line) as string) : line));
63
- return { format: "cruze-feedback/1", cruze: deps.bundle.version, exported_at: deps.clock.now().toISOString(), redacted: redact, entries, pitfalls };
64
- }
@@ -1,134 +0,0 @@
1
- import { evaluateApproval } from "../../domain/approvals/evaluate.ts";
2
- import { fingerprint } from "../../domain/approvals/fingerprint.ts";
3
- import { serializeApprovals, withApproval, type ApprovalsFile } from "../../domain/approvals/records.ts";
4
- import { CruzeError } from "../../domain/cruze_error.ts";
5
- import { renderAgentsFile } from "../../domain/edits/agents_file.ts";
6
- import { applyBookkeeping } from "../../domain/land/bookkeeping.ts";
7
- import { planMerge } from "../../domain/land/merge_plan.ts";
8
- import { parseMarkdown } from "../../domain/markdown.ts";
9
- import { resolveChange } from "../../domain/project/artifact_ref.ts";
10
- import { PATHS } from "../../domain/project/layout.ts";
11
- import { readTasks } from "../../domain/project/plan_parts.ts";
12
- import { readProgress } from "../../domain/project/progress.ts";
13
- import { approvalFolder, approvalsPath, type ProjectView } from "../../domain/project/project_view.ts";
14
- import { changesOf, featureOf, type WorkItem } from "../../domain/project/work_items.ts";
15
- import { isVerified } from "../../domain/status/verification.ts";
16
- import { activeChange } from "../../domain/status/work_status.ts";
17
- import { validateProject } from "../../domain/validation/validate_project.ts";
18
- import { withoutScheduledExceptions } from "../../domain/versions/scheduled_exceptions.ts";
19
- import { appendJournal, loadView, withFiles, type ProjectDeps } from "../project_context.ts";
20
-
21
- const AGENTS_FILE = "AGENTS.md";
22
-
23
- export interface LandReport {
24
- change: string;
25
- merged: string[];
26
- built: string[];
27
- removed: string[];
28
- restamped: string[];
29
- finished: boolean;
30
- /** Files whose `check.exceptions` entry `realign` scheduled in this change, removed now that it lands. */
31
- unexcepted: string[];
32
- archivedTo?: string;
33
- }
34
-
35
- /**
36
- * Lands a finished change: merges its work into the living docs, re-stamps the approvals the
37
- * merge would otherwise make stale, records the land, and archives the work once it is finished.
38
- * Nothing is written unless the merged docs still validate.
39
- */
40
- export async function landChange(deps: ProjectDeps, ref?: string, options: { override?: string } = {}): Promise<LandReport> {
41
- const view = await loadView(deps.files);
42
- const change = ref !== undefined ? resolveChange(view, ref) : activeChange(view, await deps.repository.currentBranch());
43
- if (change === undefined) throw new CruzeError("no-active-change", "no change is bound to this branch; name one to land");
44
- const source = change.kind === "change" ? featureOf(view.items, change) : change;
45
- if (source === undefined) throw new CruzeError("orphan-change", `${change.ref} has no feature.md`);
46
- requireReady(view, change, source);
47
- const verified = isVerified(view.journal, change);
48
- if (!verified && (options.override ?? "").trim() === "") {
49
- throw new CruzeError("not-verified", `${change.ref} has no accepted verification; run verify, or land with --override <reason>`);
50
- }
51
-
52
- const restamp = [PATHS.architecture, source.path, ...(source === change ? [] : changesOf(view.items, source).map((c) => c.path))]
53
- .filter((path, i, all) => all.indexOf(path) === i && view.snapshot.has(path) && evaluateApproval(view, path).state === "approved");
54
-
55
- const plan = planMerge(view, change, source);
56
- const commit = await deps.repository.headCommit();
57
- const landed = `${deps.clock.today()}${commit === null ? "" : ` (${commit})`}`;
58
- const books = applyBookkeeping(view, change, source, landed, plan.files);
59
- const merged = withFiles(view, plan.files);
60
- const recorded = { ...(merged.approvals.get(source.folder) ?? { version: 1, approvals: [], merged: {} }), merged: plan.merged };
61
- const checked = withFiles(merged, new Map([[approvalsPath(source.folder), serializeApprovals(recorded)]]));
62
- requireStillValid(view, checked, [source.path, ...changesOf(view.items, source).map((c) => c.path)]);
63
-
64
- const approvals = new Map<string, ApprovalsFile>();
65
- const by = (await deps.repository.userName()) ?? "unknown";
66
- for (const path of restamp) {
67
- const folder = approvalFolder(merged, path);
68
- const { hash, upstream } = fingerprint(merged, path, parseMarkdown(merged.snapshot.get(path) ?? ""));
69
- const file = approvals.get(folder) ?? merged.approvals.get(folder) ?? { version: 1, approvals: [], merged: {} };
70
- approvals.set(folder, withApproval(file, { artifact: path, hash, upstream, approvedAt: deps.clock.now().toISOString(), by, basis: `land ${change.ref}` }));
71
- }
72
- const sourceApprovals = approvals.get(source.folder) ?? merged.approvals.get(source.folder) ?? { version: 1, approvals: [], merged: {} };
73
- approvals.set(source.folder, { ...sourceApprovals, merged: plan.merged });
74
-
75
- for (const [path, text] of plan.files) await deps.files.writeText(path, text);
76
- for (const [folder, file] of approvals) await deps.files.writeText(approvalsPath(folder), serializeApprovals(file));
77
- for (const path of restamp) {
78
- const record = approvals.get(approvalFolder(merged, path))?.approvals.find((r) => r.artifact === path);
79
- await appendJournal(deps, approvalFolder(merged, path), "approve", { artifact: path, hash: record?.hash, basis: `land ${change.ref}` });
80
- }
81
- if (!verified) await appendJournal(deps, source.folder, "override", { gate: "land", reason: options.override, item: change.ref });
82
- await appendJournal(deps, source.folder, "land", { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, release: books.release, commit });
83
- if (books.archive !== undefined) await deps.files.move(books.archive.from, books.archive.to);
84
- const agents = await deps.files.readText(AGENTS_FILE);
85
- if (agents !== undefined) {
86
- const rendered = renderAgentsFile(agents, merged);
87
- if (rendered !== agents) await deps.files.writeText(AGENTS_FILE, rendered);
88
- }
89
- const unexcepted = await clearScheduledExceptions(deps, [change.ref, source.ref]);
90
-
91
- const report: LandReport = { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, restamped: restamp, finished: books.finished, unexcepted };
92
- if (books.archive !== undefined) report.archivedTo = books.archive.to;
93
- return report;
94
- }
95
-
96
- /** The change, its feature and the architecture are approved and current, valid, and every task is done. */
97
- function requireReady(view: ProjectView, change: WorkItem, source: WorkItem): void {
98
- const reasons: string[] = [];
99
- for (const path of [change.path, source.path, PATHS.architecture].filter((p, i, all) => all.indexOf(p) === i && view.snapshot.has(p))) {
100
- const status = evaluateApproval(view, path);
101
- if (status.state !== "approved") reasons.push(`${path} is ${status.state}${status.changed.length > 0 ? ` (${status.changed.join(", ")})` : ""}`);
102
- }
103
- const errors = validateProject(view).filter((p) => p.severity === "error" && (p.path === change.path || p.path === source.path));
104
- reasons.push(...errors.map((p) => `${p.path}:${p.line ?? ""} ${p.message}`));
105
- const progress = readProgress(change.doc);
106
- const open = readTasks(change.doc).tasks.filter((task) => (progress.tasks.get(task.number) ?? null) === null).map((task) => task.number);
107
- if (open.length > 0) reasons.push(`tasks not done: ${open.join(", ")}`);
108
- if (reasons.length > 0) throw new CruzeError("not-ready", `${change.ref} cannot land yet:\n ${reasons.join("\n ")}`);
109
- }
110
-
111
- /** The merge may not introduce errors into the living docs, or into the work it lands, such as a citation of an ID it removes. */
112
- function requireStillValid(before: ProjectView, after: ProjectView, work: string[]): void {
113
- const key = (p: { path: string; message: string }): string => `${p.path}|${p.message}`;
114
- const existing = new Set(validateProject(before).map(key));
115
- const watched = (path: string): boolean => path.startsWith("docs/") || work.includes(path);
116
- const introduced = validateProject(after).filter((p) => p.severity === "error" && watched(p.path) && !existing.has(key(p)));
117
- if (introduced.length > 0) {
118
- throw new CruzeError("merge-invalid", `landing would leave errors behind; nothing was written:\n ${introduced.map((p) => `${p.path}:${p.line ?? ""} ${p.message}`).join("\n ")}`);
119
- }
120
- }
121
-
122
- /** Removes the exceptions `realign` scheduled in the landed work, so CI checks those files again. */
123
- async function clearScheduledExceptions(deps: ProjectDeps, refs: string[]): Promise<string[]> {
124
- let text = await deps.files.readText(PATHS.config);
125
- if (text === undefined) return [];
126
- const removed: string[] = [];
127
- for (const ref of [...new Set(refs)]) {
128
- const result = withoutScheduledExceptions(text, ref);
129
- text = result.text;
130
- removed.push(...result.removed);
131
- }
132
- if (removed.length > 0) await deps.files.writeText(PATHS.config, text);
133
- return removed;
134
- }
@@ -1,48 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { PATHS } from "../../domain/project/layout.ts";
3
- import { NOTE_KINDS } from "../../domain/project/notes.ts";
4
- import { requireSlug } from "../../domain/project/new_items.ts";
5
- import { renderTemplate } from "../../domain/scaffold.ts";
6
- import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
7
-
8
- export type ProjectDocKind = "vision" | "glossary" | "architecture" | "roadmap";
9
-
10
- export const PROJECT_DOC_KINDS: readonly ProjectDocKind[] = ["vision", "glossary", "architecture", "roadmap"];
11
-
12
- export interface NewDocReport {
13
- kind: ProjectDocKind | "note";
14
- path: string;
15
- }
16
-
17
- /** Creates one of the project's living documents from its template, named for the project. */
18
- export async function createProjectDoc(deps: ProjectDeps, kind: ProjectDocKind): Promise<NewDocReport> {
19
- const view = await loadView(deps.files);
20
- const path = PATHS[kind];
21
- if (view.snapshot.has(path)) throw new CruzeError("exists", `${path} already exists`);
22
- const project = view.config?.config?.project ?? "";
23
- if (project === "") throw new CruzeError("no-config", `${PATHS.config} is missing or has no project name; run cruze init first`);
24
- const values = { project_name: project, id: "", title: "", date: deps.clock.today() };
25
- await deps.files.writeText(path, renderTemplate(await deps.bundle.formatTemplate(`${kind}.md`), values));
26
- await appendJournal(deps, ".cruze", "new", { kind, ref: path });
27
- return { kind, path };
28
- }
29
-
30
- /** Creates a dated note in `.cruze/notes/`, such as a captured explore session or research evidence. */
31
- /**
32
- * A dated note, saying what kind it is and what it is about, so `cruze show` finds it for the work it
33
- * informs: `project`, feature slugs, or context and capability names.
34
- */
35
- export async function createNote(deps: ProjectDeps, slug: string, title: string, kind: string, about: string): Promise<NewDocReport> {
36
- requireSlug(slug);
37
- if (title.trim() === "") throw new CruzeError("missing-title", "a title is required");
38
- if (!(NOTE_KINDS as readonly string[]).includes(kind)) throw new CruzeError("note-kind", `a note's kind is one of ${NOTE_KINDS.join(", ")}`);
39
- const subjects = about.split(",").map((a) => a.trim()).filter((a) => a !== "");
40
- if (subjects.length === 0) throw new CruzeError("note-about", "name what the note is about: project, feature slugs, or contexts, comma-separated");
41
- const view = await loadView(deps.files);
42
- const date = deps.clock.today();
43
- const base = `${PATHS.notesDir}/${date}-${slug}`;
44
- let path = `${base}.md`;
45
- for (let n = 2; view.snapshot.has(path); n++) path = `${base}-${n}.md`;
46
- await deps.files.writeText(path, `# ${title}\n\n- Date: ${date}\n- Kind: ${kind}\n- About: ${subjects.join(", ")}\n\n`);
47
- return { kind: "note", path };
48
- }
@@ -1,75 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { upsertRow } from "../../domain/edits/managed_tables.ts";
3
- import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
4
- import { PATHS } from "../../domain/project/layout.ts";
5
- import { allocateId, changeRow, fillTemplate, requireSlug } from "../../domain/project/new_items.ts";
6
- import { renderTemplate } from "../../domain/scaffold.ts";
7
- import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
8
-
9
- export type NewKind = "feature" | "change" | "adr";
10
-
11
- export interface NewRequest {
12
- kind: NewKind;
13
- slug: string;
14
- title: string;
15
- /** For a change: the feature it belongs to. Without one, the change is standalone. */
16
- feature?: string;
17
- /** The roadmap item this feature or standalone change delivers; defaults to the slug. */
18
- roadmap?: string;
19
- }
20
-
21
- export interface NewReport {
22
- kind: "feature" | "change" | "standalone" | "adr";
23
- ref: string;
24
- path: string;
25
- }
26
-
27
- /** Creates a feature, change or ADR from its template, with a fresh ID. */
28
- export async function createWorkItem(deps: ProjectDeps, request: NewRequest): Promise<NewReport> {
29
- const view = await loadView(deps.files);
30
- const date = deps.clock.today();
31
- const values = { id: "", title: request.title, title_quoted: JSON.stringify(request.title), date, project_name: "" };
32
- if (request.title.trim() === "") throw new CruzeError("missing-title", "a title is required");
33
-
34
- if (request.kind === "adr") {
35
- requireSlug(request.slug);
36
- const path = `${PATHS.adrDir}/${date}-${request.slug}.md`;
37
- if (view.snapshot.has(path)) throw new CruzeError("exists", `${path} already exists`);
38
- const text = renderTemplate(await deps.bundle.formatTemplate("adr.md"), values).replace(/^# .*$/m, `# ${request.title}`);
39
- await deps.files.writeText(path, text);
40
- await appendJournal(deps, ".cruze", "new", { kind: "adr", ref: path });
41
- return { kind: "adr", ref: `ADR-${date}-${request.slug}`, path };
42
- }
43
-
44
- if (request.kind === "change" && request.feature !== undefined) {
45
- const { item: feature } = resolveArtifact(view, request.feature);
46
- if (feature === undefined || feature.kind !== "feature" || feature.archived) throw new CruzeError("not-a-feature", `"${request.feature}" is not an active feature`);
47
- const row = changeRow(view, feature, request.slug);
48
- const folder = `${feature.folder}/changes/${row.change}`;
49
- const template = renderTemplate(await deps.bundle.formatTemplate("change.md"), { ...values, id: row.change });
50
- await deps.files.writeText(`${folder}/change.md`, fillTemplate(template, { delivers: row.delivers, builds: [...row.builds, ...row.removes] }));
51
- await appendJournal(deps, feature.folder, "new", { kind: "change", ref: `${feature.ref}/${row.change}` });
52
- return { kind: "change", ref: `${feature.ref}/${row.change}`, path: `${folder}/change.md` };
53
- }
54
-
55
- const kind = request.kind === "feature" ? "feature" : "standalone";
56
- const id = allocateId(view, date, request.slug);
57
- const folder = `${kind === "feature" ? PATHS.featuresDir : PATHS.changesDir}/${id}`;
58
- const roadmap = request.roadmap ?? (roadmapHasItem(view.snapshot.get(PATHS.roadmap), request.slug) ? request.slug : undefined);
59
- const template = renderTemplate(await deps.bundle.formatTemplate(kind === "feature" ? "feature.md" : "standalone-change.md"), { ...values, id });
60
- const path = `${folder}/${kind === "feature" ? "feature.md" : "change.md"}`;
61
- await deps.files.writeText(path, fillTemplate(template, roadmap === undefined ? {} : { roadmap }));
62
- const roadmapText = view.snapshot.get(PATHS.roadmap);
63
- if (roadmap !== undefined && roadmapText !== undefined) {
64
- await deps.files.writeText(PATHS.roadmap, upsertRow(roadmapText, "Status", [roadmap, id, "designing"]));
65
- }
66
- await appendJournal(deps, folder, "new", { kind, ref: id });
67
- return { kind, ref: id, path };
68
- }
69
-
70
- /** Whether a roadmap phase lists this slug as an item. */
71
- function roadmapHasItem(roadmap: string | undefined, slug: string): boolean {
72
- if (roadmap === undefined) return false;
73
- const phases = roadmap.split(/^## /m).find((section) => section.startsWith("Phases")) ?? "";
74
- return phases.split("\n").some((line) => line.startsWith(`| ${slug} |`));
75
- }
@@ -1,45 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { resolveArtifact } from "../../domain/project/artifact_ref.ts";
3
- import { PATHS } from "../../domain/project/layout.ts";
4
- import { withVersionLine } from "../../domain/versions/cruze_version.ts";
5
- import { withScheduledExceptions } from "../../domain/versions/scheduled_exceptions.ts";
6
- import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
7
- import { realignStatus } from "./realign_status.ts";
8
-
9
- export interface RealignDone {
10
- from: string;
11
- to: string;
12
- /** The change that fixes what is still open, when anything is. */
13
- scheduled?: string;
14
- /** Files listed under `check.exceptions` until that change lands. */
15
- excepted: string[];
16
- }
17
-
18
- /**
19
- * Moves `standards:` up to the installed Cruze once every mechanical finding is fixed, waived,
20
- * or scheduled in a change. A scheduled `check` finding becomes an exception naming that change,
21
- * so CI stays green until it lands, and landing it removes the exception.
22
- */
23
- export async function realignDone(deps: ProjectDeps, options: { change?: string }): Promise<RealignDone> {
24
- const status = await realignStatus(deps, { full: false });
25
- if (status.standards === status.target) return { from: status.standards, to: status.target, excepted: [] };
26
- const open = status.notes.flatMap((note) => note.findings.filter((f) => !f.waived).map((f) => ({ note, finding: f })));
27
- let scheduled: string | undefined;
28
- if (open.length > 0) {
29
- if (options.change === undefined) {
30
- const list = open.map(({ note, finding }) => `${note.id}: ${finding.path}${finding.line === undefined ? "" : `:${finding.line}`}`);
31
- throw new CruzeError("realign-open", `fix, waive or schedule these first, then pass --change <ref> for the change that fixes the rest:\n ${list.join("\n ")}`);
32
- }
33
- const item = resolveArtifact(await loadView(deps.files), options.change).item;
34
- if (item === undefined) throw new CruzeError("not-found", `"${options.change}" is not a feature or change; create the prefactor change first`);
35
- scheduled = item.ref;
36
- }
37
-
38
- const excepted = open.filter(({ note }) => note.detect?.startsWith("check:") === true).map(({ note, finding }) => ({ path: finding.path, note: note.id }));
39
- const config = (await deps.files.readText(PATHS.config)) ?? "";
40
- const withExceptions = scheduled === undefined ? config : withScheduledExceptions(config, status.target, scheduled, excepted);
41
- await deps.files.writeText(PATHS.config, withVersionLine(withExceptions, "standards", status.target));
42
- const paths = [...new Set(excepted.map((e) => e.path))];
43
- await appendJournal(deps, ".cruze", "realign", { from: status.standards, to: status.target, ...(scheduled === undefined ? {} : { scheduled }), excepted: paths });
44
- return { from: status.standards, to: status.target, ...(scheduled === undefined ? {} : { scheduled }), excepted: paths };
45
- }
@@ -1,106 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import type { CruzeConfig } from "../../domain/config.ts";
3
- import { PATHS } from "../../domain/project/layout.ts";
4
- import type { ProjectView } from "../../domain/project/project_view.ts";
5
- import { validateProject } from "../../domain/validation/validate_project.ts";
6
- import { notesBetween, parseRealignNotes, type RealignNote } from "../../domain/versions/realign_notes.ts";
7
- import { FIRST_RELEASE } from "../../domain/versions/rule_versions.ts";
8
- import type { Bundle } from "../ports/bundle.ts";
9
- import { loadView, type ProjectDeps } from "../project_context.ts";
10
- import { checkCode } from "./check_code.ts";
11
-
12
- export const REALIGN_SKILL = "realign";
13
-
14
- export interface RealignFinding {
15
- path: string;
16
- line?: number;
17
- message: string;
18
- /** Waived by an earlier realign, so never raised again. */
19
- waived: boolean;
20
- }
21
-
22
- export interface RealignNoteReport extends RealignNote {
23
- /** What the note's CLI rule reports now, for a note with `Detect`. */
24
- findings: RealignFinding[];
25
- }
26
-
27
- export interface RealignStatus {
28
- /** The version the code meets now, and the one the installed skills expect. */
29
- standards: string;
30
- target: string;
31
- full: boolean;
32
- /** Notes for existing code: those with findings to fix, waive or schedule, and those an auditor answers. */
33
- notes: RealignNoteReport[];
34
- /** Notes that change only new work, so nothing existing has to move. */
35
- newWorkOnly: string[];
36
- /** Mechanical findings not yet waived. */
37
- open: number;
38
- }
39
-
40
- /**
41
- * What realigning the project to its installed Cruze involves: the notes of every release
42
- * since `standards:`, or of all releases with `full`, and what their CLI rules find now.
43
- */
44
- export async function realignStatus(deps: ProjectDeps, options: { full: boolean }): Promise<RealignStatus> {
45
- const view = await loadView(deps.files);
46
- const config = requireInstalled(view, deps.bundle.version);
47
- const standards = config.standards ?? FIRST_RELEASE;
48
- const target = deps.bundle.version;
49
- const notes = notesBetween(await bundledNotes(deps.bundle), options.full ? "0.0.0" : standards, target);
50
- const waived = waivedFindings(view);
51
- const check = (await checkCode(deps, { ci: false })).findings;
52
- const problems = validateProject(view);
53
- const report = notes.filter((n) => n.appliesTo === "existing").map((note): RealignNoteReport => {
54
- const [command, rule] = (note.detect ?? ":").split(":");
55
- const found = command === "check" ? check.filter((f) => f.rule === rule) : command === "validate" ? problems.filter((p) => p.rule === rule) : [];
56
- const findings = found.map((f) => ({
57
- path: f.path,
58
- ...(f.line === undefined ? {} : { line: f.line }),
59
- message: f.message,
60
- // A check finding is waived by listing the file under check.exceptions, which also keeps CI green.
61
- waived: command === "validate" && waived.has(findingKey(note.id, f.path)),
62
- }));
63
- return { ...note, findings };
64
- });
65
- return {
66
- standards,
67
- target,
68
- full: options.full,
69
- notes: report,
70
- newWorkOnly: notes.filter((n) => n.appliesTo === "new").map((n) => n.id),
71
- open: report.flatMap((n) => n.findings).filter((f) => !f.waived).length,
72
- };
73
- }
74
-
75
- /** The key a realign disposition records for a mechanical finding, so a waiver can be matched later. */
76
- export function findingKey(note: string, path: string): string {
77
- return `${note}: ${path}`;
78
- }
79
-
80
- /** The project's config, once its skills come from this CLI; realign works from the installed notes. */
81
- export function requireInstalled(view: ProjectView, version: string): CruzeConfig {
82
- const config = view.config?.config;
83
- if (config === null || config === undefined) throw new CruzeError("config", `fix ${PATHS.config} first; run cruze validate for details`);
84
- if (config.cruze !== version) {
85
- throw new CruzeError("install-first", `the project's skills are from Cruze ${config.cruze ?? "an unrecorded version"}, and this CLI is ${version}; run cruze install first`);
86
- }
87
- return config;
88
- }
89
-
90
- async function bundledNotes(bundle: Bundle): Promise<RealignNote[]> {
91
- const skill = (await bundle.skills()).find((s) => s.folder === REALIGN_SKILL);
92
- return (skill?.files ?? [])
93
- .flatMap((file) => {
94
- const version = /^notes\/(\d+\.\d+\.\d+)\.md$/.exec(file.path)?.[1];
95
- return version === undefined ? [] : parseRealignNotes(version, file.text).notes;
96
- });
97
- }
98
-
99
- /** Findings earlier realigns waived, by key. */
100
- function waivedFindings(view: ProjectView): Set<string> {
101
- return new Set(
102
- view.journal
103
- .filter((e) => e.event === "disposition" && e["review"] === "realign" && e["disposition"] === "waived")
104
- .map((e) => String(e["finding"])),
105
- );
106
- }
@@ -1,118 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { readRows, removeRow, upsertRow } from "../../domain/edits/managed_tables.ts";
3
- import { updateProgress } from "../../domain/edits/progress_block.ts";
4
- import { resolveChange } from "../../domain/project/artifact_ref.ts";
5
- import { PATHS } from "../../domain/project/layout.ts";
6
- import { readTasks } from "../../domain/project/plan_parts.ts";
7
- import { requireSlug } from "../../domain/project/new_items.ts";
8
- import { activeChange } from "../../domain/status/work_status.ts";
9
- import type { WorkItem } from "../../domain/project/work_items.ts";
10
- import { appendJournal, loadView, type ProjectDeps } from "../project_context.ts";
11
-
12
- /** Commands that write the CLI-owned progress records: task ticks, deviations and the feature map. */
13
-
14
- export interface TaskReport {
15
- change: string;
16
- task: string;
17
- done: number;
18
- total: number;
19
- }
20
-
21
- /** Marks a task done, recording the commit that completed it (HEAD by default). */
22
- export async function completeTask(deps: ProjectDeps, task: string, options: { change?: string; commit?: string }): Promise<TaskReport> {
23
- const change = await changeFor(deps, options.change);
24
- requireTask(change, task);
25
- const commit = options.commit ?? (await deps.repository.headCommit()) ?? "";
26
- const text = updateProgress(change.doc.lines.join("\n"), (progress) => progress.tasks.set(task, commit));
27
- await deps.files.writeText(change.path, text);
28
- const ticks = [...text.matchAll(/^- \[([ x])\] T\d+/gm)];
29
- return { change: change.ref, task, done: ticks.filter((m) => m[1] === "x").length, total: ticks.length };
30
- }
31
-
32
- /** Reopens a done task whose design a rethink changed, so build does it again. */
33
- export async function reopenTask(deps: ProjectDeps, task: string, reason: string, options: { change?: string }): Promise<{ change: string; task: string }> {
34
- const change = await changeFor(deps, options.change);
35
- requireTask(change, task);
36
- if (reason.trim() === "") throw new CruzeError("missing-reason", "say why the task reopens");
37
- await deps.files.writeText(change.path, updateProgress(change.doc.lines.join("\n"), (progress) => progress.tasks.set(task, null)));
38
- await appendJournal(deps, change.folder, "reopen", { change: change.ref, task, reason: reason.trim() });
39
- return { change: change.ref, task };
40
- }
41
-
42
- /** Records a task-level choice the agent made on its own. Anything larger is a rethink. */
43
- export async function recordDeviation(deps: ProjectDeps, task: string, text: string, options: { change?: string }): Promise<{ change: string; task: string }> {
44
- const change = await changeFor(deps, options.change);
45
- requireTask(change, task);
46
- if (text.trim() === "") throw new CruzeError("missing-text", "describe the deviation");
47
- await deps.files.writeText(change.path, updateProgress(change.doc.lines.join("\n"), (progress) => progress.deviations.push(`${task}: ${text.trim()}`)));
48
- await appendJournal(deps, change.folder, "deviation", { change: change.ref, task, text: text.trim() });
49
- return { change: change.ref, task };
50
- }
51
-
52
- /** Adds a feature to the future list of the feature map, or replaces its entry. */
53
- export async function addFutureFeature(deps: ProjectDeps, slug: string, summary: string, goals: string[]): Promise<{ feature: string }> {
54
- requireSlug(slug);
55
- const vision = await requireVision(deps);
56
- await deps.files.writeText(PATHS.vision, upsertRow(vision, "Future", [slug, summary, goals.join(", ")]));
57
- await appendJournal(deps, ".cruze", "feature-map", { action: "add", feature: slug, summary, goals });
58
- return { feature: slug };
59
- }
60
-
61
- /**
62
- * Replaces a feature's summary on the feature map, in the future list or the implemented list,
63
- * when a decision changed what the feature delivers. Its goals or release stay as they are.
64
- */
65
- export async function updateFeatureSummary(deps: ProjectDeps, slug: string, summary: string): Promise<{ feature: string; list: "Future" | "Implemented" }> {
66
- if (summary.trim() === "") throw new CruzeError("missing-summary", "a feature needs a summary");
67
- const vision = await requireVision(deps);
68
- const future = readRows(vision, "Future").find((row) => row[0] === slug);
69
- const implemented = readRows(vision, "Implemented").find((row) => row[0] === slug);
70
- if (future === undefined && implemented === undefined) throw new CruzeError("not-found", `${slug} is not on the feature map`);
71
- const list = future !== undefined ? "Future" : "Implemented";
72
- const cells = future !== undefined ? [slug, summary.trim(), future[2] ?? ""] : [slug, implemented?.[1] ?? "", summary.trim()];
73
- await deps.files.writeText(PATHS.vision, upsertRow(vision, list, cells));
74
- await appendJournal(deps, ".cruze", "feature-map", { action: "update", feature: slug, summary: summary.trim() });
75
- return { feature: slug, list };
76
- }
77
-
78
- export async function dropFutureFeature(deps: ProjectDeps, slug: string, reason: string): Promise<{ feature: string }> {
79
- const vision = await requireVision(deps);
80
- const { text, removed } = removeRow(vision, "Future", slug);
81
- if (removed === null) throw new CruzeError("not-found", `${slug} is not on the future list`);
82
- if (reason.trim() === "") throw new CruzeError("missing-reason", "dropping a feature needs a reason");
83
- await deps.files.writeText(PATHS.vision, text);
84
- await appendJournal(deps, ".cruze", "feature-map", { action: "drop", feature: slug, reason });
85
- return { feature: slug };
86
- }
87
-
88
- /** Clears the landed rows from the roadmap's Status table, when a release closes. */
89
- export async function pruneRoadmap(deps: ProjectDeps): Promise<{ removed: string[] }> {
90
- const roadmap = await deps.files.readText(PATHS.roadmap);
91
- if (roadmap === undefined) throw new CruzeError("not-found", `${PATHS.roadmap} does not exist yet`);
92
- const landed = readRows(roadmap, "Status").filter((row) => row[2] === "landed").map((row) => row[0] ?? "");
93
- let text = roadmap;
94
- for (const item of landed) text = removeRow(text, "Status", item).text;
95
- if (landed.length > 0) {
96
- await deps.files.writeText(PATHS.roadmap, text);
97
- await appendJournal(deps, ".cruze", "roadmap-prune", { items: landed });
98
- }
99
- return { removed: landed };
100
- }
101
-
102
- async function changeFor(deps: ProjectDeps, ref: string | undefined): Promise<WorkItem> {
103
- const view = await loadView(deps.files);
104
- if (ref !== undefined) return resolveChange(view, ref);
105
- const change = activeChange(view, await deps.repository.currentBranch());
106
- if (change === undefined) throw new CruzeError("no-active-change", "no change is bound to this branch; name one with --change");
107
- return change;
108
- }
109
-
110
- function requireTask(change: WorkItem, task: string): void {
111
- if (!readTasks(change.doc).tasks.some((t) => t.number === task)) throw new CruzeError("unknown-task", `${change.ref} has no task ${task}`);
112
- }
113
-
114
- async function requireVision(deps: ProjectDeps): Promise<string> {
115
- const vision = await deps.files.readText(PATHS.vision);
116
- if (vision === undefined) throw new CruzeError("not-found", `${PATHS.vision} does not exist yet`);
117
- return vision;
118
- }
@@ -1,18 +0,0 @@
1
- import { CruzeError } from "../../domain/cruze_error.ts";
2
- import { elementIndex, lookupElements, type ElementIndex } from "../../domain/project/element_lookup.ts";
3
- import type { ShownElement } from "../../domain/project/scope_view.ts";
4
- import { loadView, type ProjectDeps } from "../project_context.ts";
5
-
6
- /** The bodies of the named elements, living and in each in-flight delta that changes them. */
7
- export async function showElements(deps: ProjectDeps, ids: string[]): Promise<{ elements: ShownElement[] }> {
8
- const { elements, missing } = lookupElements(await loadView(deps.files), ids);
9
- if (missing.length > 0) throw new CruzeError("unknown-id", `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} defined nowhere in the living docs or the work in flight; cruze show --index lists every element`);
10
- return { elements };
11
- }
12
-
13
- /** One line per element of the living architecture and specs, with what work in flight adds or changes. */
14
- export async function showIndex(deps: ProjectDeps, scope?: string): Promise<ElementIndex & { scope?: string }> {
15
- const index = elementIndex(await loadView(deps.files), scope);
16
- if (scope !== undefined && index.entries.length === 0) throw new CruzeError("unknown-scope", `no element belongs to "${scope}"; name a context or capability, such as the part of an ID before its dot`);
17
- return scope === undefined ? index : { ...index, scope };
18
- }
@@ -1,11 +0,0 @@
1
- import { notesFor, readNotes, type Note, type NotesFor } from "../../domain/project/notes.ts";
2
- import { loadView, type ProjectDeps } from "../project_context.ts";
3
-
4
- /**
5
- * Every note with what it is about and the hand-off it holds, or, given a target such as
6
- * `project`, `architect` or a feature slug, the notes and hand-off rows for it.
7
- */
8
- export async function showNotes(deps: ProjectDeps, target?: string): Promise<{ notes: Note[] } | (NotesFor & { target: string })> {
9
- const notes = readNotes((await loadView(deps.files)).snapshot);
10
- return target === undefined ? { notes } : { target, ...notesFor(notes, [target]) };
11
- }