@printpractical/cruze 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/README.md +61 -118
  2. package/dist/adapters/inbound/cli/cli_context.js +0 -27
  3. package/dist/adapters/inbound/cli/commands/code.js +5 -18
  4. package/dist/adapters/inbound/cli/commands/documents.js +11 -37
  5. package/dist/adapters/inbound/cli/commands/init.js +1 -1
  6. package/dist/adapters/inbound/cli/commands/install.js +1 -0
  7. package/dist/adapters/inbound/cli/commands/review.js +2 -9
  8. package/dist/adapters/inbound/cli/commands/status.js +16 -63
  9. package/dist/adapters/inbound/cli/commands.js +10 -37
  10. package/dist/adapters/inbound/cli/parse_command.js +0 -54
  11. package/dist/adapters/outbound/git_repository.js +2 -71
  12. package/dist/app/ports/repository.js +0 -12
  13. package/dist/app/project_context.js +15 -25
  14. package/dist/app/use_cases/approve_document.js +29 -0
  15. package/dist/app/use_cases/check_code.js +21 -56
  16. package/dist/app/use_cases/install_skills.js +7 -1
  17. package/dist/app/use_cases/new_document.js +25 -0
  18. package/dist/app/use_cases/project_status.js +43 -67
  19. package/dist/app/use_cases/run_review.js +11 -30
  20. package/dist/app/use_cases/validate_project.js +8 -5
  21. package/dist/domain/approvals.js +51 -0
  22. package/dist/domain/check/check_source.js +3 -32
  23. package/dist/domain/check/imports.js +3 -3
  24. package/dist/domain/check/rust_imports.js +25 -3
  25. package/dist/domain/config.js +8 -84
  26. package/dist/domain/hashing.js +16 -30
  27. package/dist/domain/ids.js +13 -27
  28. package/dist/domain/next_step.js +41 -0
  29. package/dist/domain/project/features.js +71 -0
  30. package/dist/domain/project/layout.js +11 -27
  31. package/dist/domain/project/project_docs.js +48 -0
  32. package/dist/domain/project/release.js +30 -0
  33. package/dist/domain/scaffold.js +0 -1
  34. package/dist/domain/skill_budget.js +4 -4
  35. package/dist/domain/trace.js +8 -25
  36. package/dist/domain/validation/document_rules.js +65 -0
  37. package/dist/domain/validation/release_rules.js +45 -0
  38. package/dist/domain/validation/review_rules.js +21 -0
  39. package/dist/domain/validation/validate_project.js +20 -27
  40. package/dist/domain/versions/cruze_version.js +3 -9
  41. package/dist/main.js +1 -1
  42. package/package.json +1 -1
  43. package/skills/asking/SKILL.md +51 -0
  44. package/skills/behavioural-testing/SKILL.md +14 -16
  45. package/skills/behavioural-testing/examples.md +1 -2
  46. package/skills/behavioural-testing/fakes.md +2 -2
  47. package/skills/build/SKILL.md +34 -38
  48. package/skills/dependency-approval/SKILL.md +34 -31
  49. package/skills/envision/SKILL.md +23 -18
  50. package/skills/envision/architecture.md +16 -0
  51. package/skills/envision/breakdown.md +14 -0
  52. package/skills/envision/possibilities.md +6 -7
  53. package/skills/fix/SKILL.md +34 -0
  54. package/skills/formats/SKILL.md +39 -87
  55. package/skills/formats/config.md +47 -0
  56. package/skills/formats/templates/architecture.md +18 -74
  57. package/skills/formats/templates/feature.md +18 -34
  58. package/skills/formats/templates/vision.md +15 -27
  59. package/skills/hexagonal-design/SKILL.md +11 -11
  60. package/skills/hexagonal-design/contracts.md +3 -3
  61. package/skills/hexagonal-design/fundamentals.md +1 -1
  62. package/skills/hexagonal-design/languages/cpp.md +1 -1
  63. package/skills/hexagonal-design/languages/rust.md +1 -1
  64. package/skills/hexagonal-design/languages/swift.md +2 -2
  65. package/skills/hexagonal-design/runtime.md +2 -2
  66. package/skills/research/SKILL.md +4 -4
  67. package/skills/research/evaluation.md +1 -1
  68. package/skills/review/SKILL.md +27 -0
  69. package/skills/roles/SKILL.md +8 -59
  70. package/skills/roles/code-reviewer.md +22 -32
  71. package/skills/roles/researcher.md +3 -3
  72. package/skills/shape/SKILL.md +37 -0
  73. package/skills/shape/self-check.md +12 -0
  74. package/src/adapters/inbound/cli/cli_context.ts +0 -27
  75. package/src/adapters/inbound/cli/commands/code.ts +5 -18
  76. package/src/adapters/inbound/cli/commands/documents.ts +11 -37
  77. package/src/adapters/inbound/cli/commands/init.ts +1 -1
  78. package/src/adapters/inbound/cli/commands/install.ts +1 -0
  79. package/src/adapters/inbound/cli/commands/review.ts +2 -9
  80. package/src/adapters/inbound/cli/commands/status.ts +16 -63
  81. package/src/adapters/inbound/cli/commands.ts +10 -37
  82. package/src/adapters/inbound/cli/parse_command.ts +0 -54
  83. package/src/adapters/outbound/git_repository.ts +2 -71
  84. package/src/app/ports/repository.ts +0 -12
  85. package/src/app/project_context.ts +15 -25
  86. package/src/app/use_cases/approve_document.ts +29 -0
  87. package/src/app/use_cases/check_code.ts +21 -56
  88. package/src/app/use_cases/install_skills.ts +7 -1
  89. package/src/app/use_cases/new_document.ts +25 -0
  90. package/src/app/use_cases/project_status.ts +49 -73
  91. package/src/app/use_cases/run_review.ts +11 -30
  92. package/src/app/use_cases/validate_project.ts +8 -5
  93. package/src/domain/approvals.ts +51 -0
  94. package/src/domain/check/check_source.ts +3 -32
  95. package/src/domain/check/imports.ts +3 -3
  96. package/src/domain/check/rust_imports.ts +25 -3
  97. package/src/domain/config.ts +9 -85
  98. package/src/domain/hashing.ts +16 -30
  99. package/src/domain/ids.ts +13 -27
  100. package/src/domain/next_step.ts +41 -0
  101. package/src/domain/project/features.ts +71 -0
  102. package/src/domain/project/layout.ts +11 -27
  103. package/src/domain/project/project_docs.ts +48 -0
  104. package/src/domain/project/release.ts +30 -0
  105. package/src/domain/scaffold.ts +0 -1
  106. package/src/domain/skill_budget.ts +4 -4
  107. package/src/domain/trace.ts +8 -25
  108. package/src/domain/validation/document_rules.ts +65 -0
  109. package/src/domain/validation/release_rules.ts +45 -0
  110. package/src/domain/validation/review_rules.ts +21 -0
  111. package/src/domain/validation/validate_project.ts +21 -28
  112. package/src/domain/versions/cruze_version.ts +3 -9
  113. package/src/main.ts +1 -1
  114. package/templates/init/AGENTS.md +4 -12
  115. package/templates/init/ci.yml +3 -3
  116. package/templates/init/config.yaml +8 -17
  117. package/templates/init/review/claude.yaml +2 -2
  118. package/dist/adapters/inbound/cli/argument_files.js +0 -29
  119. package/dist/adapters/inbound/cli/commands/diff.js +0 -23
  120. package/dist/adapters/inbound/cli/commands/journal.js +0 -32
  121. package/dist/adapters/inbound/cli/commands/progress.js +0 -55
  122. package/dist/adapters/inbound/cli/commands/realign.js +0 -28
  123. package/dist/adapters/inbound/cli/commands/show.js +0 -96
  124. package/dist/app/use_cases/abandon_work.js +0 -42
  125. package/dist/app/use_cases/approve_artifact.js +0 -197
  126. package/dist/app/use_cases/journal_events.js +0 -64
  127. package/dist/app/use_cases/land_change.js +0 -134
  128. package/dist/app/use_cases/new_project_doc.js +0 -48
  129. package/dist/app/use_cases/new_work_item.js +0 -75
  130. package/dist/app/use_cases/realign_done.js +0 -45
  131. package/dist/app/use_cases/realign_status.js +0 -106
  132. package/dist/app/use_cases/record_progress.js +0 -118
  133. package/dist/app/use_cases/show_elements.js +0 -18
  134. package/dist/app/use_cases/show_notes.js +0 -11
  135. package/dist/app/use_cases/show_scope.js +0 -51
  136. package/dist/app/use_cases/upstream_diff.js +0 -55
  137. package/dist/domain/approvals/agent_approval.js +0 -68
  138. package/dist/domain/approvals/evaluate.js +0 -36
  139. package/dist/domain/approvals/fingerprint.js +0 -52
  140. package/dist/domain/approvals/rebase.js +0 -39
  141. package/dist/domain/approvals/records.js +0 -50
  142. package/dist/domain/approvals/upstream_text.js +0 -36
  143. package/dist/domain/architecture_sections.js +0 -23
  144. package/dist/domain/check/decision_comments.js +0 -38
  145. package/dist/domain/decision_citations.js +0 -62
  146. package/dist/domain/edits/agents_file.js +0 -58
  147. package/dist/domain/edits/managed_tables.js +0 -60
  148. package/dist/domain/edits/progress_block.js +0 -33
  149. package/dist/domain/edits/status_lines.js +0 -52
  150. package/dist/domain/elements.js +0 -126
  151. package/dist/domain/journal.js +0 -109
  152. package/dist/domain/land/bookkeeping.js +0 -54
  153. package/dist/domain/land/merge_plan.js +0 -137
  154. package/dist/domain/land/text_ops.js +0 -66
  155. package/dist/domain/line_diff.js +0 -56
  156. package/dist/domain/project/artifact_ref.js +0 -42
  157. package/dist/domain/project/deltas.js +0 -49
  158. package/dist/domain/project/element_lookup.js +0 -124
  159. package/dist/domain/project/living_model.js +0 -55
  160. package/dist/domain/project/new_items.js +0 -48
  161. package/dist/domain/project/notes.js +0 -74
  162. package/dist/domain/project/plan_parts.js +0 -111
  163. package/dist/domain/project/progress.js +0 -43
  164. package/dist/domain/project/project_view.js +0 -72
  165. package/dist/domain/project/roadmap.js +0 -43
  166. package/dist/domain/project/scope_view.js +0 -134
  167. package/dist/domain/project/work_items.js +0 -68
  168. package/dist/domain/status/next_step.js +0 -181
  169. package/dist/domain/status/pending_rethink.js +0 -22
  170. package/dist/domain/status/verification.js +0 -29
  171. package/dist/domain/status/work_status.js +0 -125
  172. package/dist/domain/validation/decision_rules.js +0 -76
  173. package/dist/domain/validation/delta_rules.js +0 -81
  174. package/dist/domain/validation/element_rules.js +0 -104
  175. package/dist/domain/validation/future_rules.js +0 -40
  176. package/dist/domain/validation/living_rules.js +0 -99
  177. package/dist/domain/validation/plan_rules.js +0 -124
  178. package/dist/domain/validation/project_rules.js +0 -115
  179. package/dist/domain/validation/size_rules.js +0 -45
  180. package/dist/domain/validation/vision_rules.js +0 -41
  181. package/dist/domain/validation/work_item_rules.js +0 -175
  182. package/dist/domain/versions/realign_notes.js +0 -72
  183. package/dist/domain/versions/rule_versions.js +0 -29
  184. package/dist/domain/versions/scheduled_exceptions.js +0 -41
  185. package/skills/about/SKILL.md +0 -21
  186. package/skills/architect/SKILL.md +0 -49
  187. package/skills/architect/feature.md +0 -40
  188. package/skills/architect/project.md +0 -71
  189. package/skills/architect/tweak.md +0 -22
  190. package/skills/architect/walkthrough.md +0 -35
  191. package/skills/domain-language/SKILL.md +0 -37
  192. package/skills/envision/feature.md +0 -25
  193. package/skills/envision/intake.md +0 -28
  194. package/skills/envision/project.md +0 -24
  195. package/skills/explore/SKILL.md +0 -39
  196. package/skills/formats/reference/architecture.md +0 -84
  197. package/skills/formats/reference/config.md +0 -80
  198. package/skills/formats/reference/durable-docs.md +0 -74
  199. package/skills/formats/reference/specs.md +0 -65
  200. package/skills/formats/reference/work-items.md +0 -100
  201. package/skills/formats/templates/adr.md +0 -7
  202. package/skills/formats/templates/change.md +0 -29
  203. package/skills/formats/templates/glossary.md +0 -8
  204. package/skills/formats/templates/roadmap.md +0 -32
  205. package/skills/formats/templates/spec.md +0 -11
  206. package/skills/formats/templates/standalone-change.md +0 -46
  207. package/skills/grilling/SKILL.md +0 -90
  208. package/skills/grilling/illustrating.md +0 -38
  209. package/skills/improve/SKILL.md +0 -51
  210. package/skills/land/SKILL.md +0 -48
  211. package/skills/next/SKILL.md +0 -31
  212. package/skills/plan/SKILL.md +0 -62
  213. package/skills/plan/tasks.md +0 -35
  214. package/skills/plan/test-plan.md +0 -14
  215. package/skills/realign/SKILL.md +0 -37
  216. package/skills/realign/notes/0.0.10.md +0 -131
  217. package/skills/realign/notes/0.0.11.md +0 -15
  218. package/skills/realign/notes/0.0.12.md +0 -31
  219. package/skills/realign/notes/0.0.13.md +0 -11
  220. package/skills/realign/notes/0.0.2.md +0 -69
  221. package/skills/realign/notes/0.0.3.md +0 -11
  222. package/skills/realign/notes/0.0.4.md +0 -27
  223. package/skills/realign/notes/0.0.5.md +0 -38
  224. package/skills/realign/notes/0.0.6.md +0 -44
  225. package/skills/realign/notes/0.0.7.md +0 -11
  226. package/skills/realign/notes/0.0.8.md +0 -97
  227. package/skills/realign/notes/0.0.9.md +0 -59
  228. package/skills/realign/notes.md +0 -40
  229. package/skills/rethink/SKILL.md +0 -62
  230. package/skills/rethink/levels.md +0 -54
  231. package/skills/roadmap/SKILL.md +0 -54
  232. package/skills/roles/auditor.md +0 -35
  233. package/skills/roles/design-reviewer.md +0 -43
  234. package/skills/roles/design-rubric.md +0 -41
  235. package/skills/roles/plan-rubric.md +0 -11
  236. package/skills/roles/surveyor.md +0 -41
  237. package/skills/roles/verifier.md +0 -42
  238. package/skills/triage/SKILL.md +0 -37
  239. package/skills/verify/SKILL.md +0 -45
  240. package/skills/verify/manual-test.md +0 -23
  241. package/src/adapters/inbound/cli/argument_files.ts +0 -29
  242. package/src/adapters/inbound/cli/commands/diff.ts +0 -23
  243. package/src/adapters/inbound/cli/commands/journal.ts +0 -32
  244. package/src/adapters/inbound/cli/commands/progress.ts +0 -55
  245. package/src/adapters/inbound/cli/commands/realign.ts +0 -28
  246. package/src/adapters/inbound/cli/commands/show.ts +0 -96
  247. package/src/app/use_cases/abandon_work.ts +0 -42
  248. package/src/app/use_cases/approve_artifact.ts +0 -197
  249. package/src/app/use_cases/journal_events.ts +0 -64
  250. package/src/app/use_cases/land_change.ts +0 -134
  251. package/src/app/use_cases/new_project_doc.ts +0 -48
  252. package/src/app/use_cases/new_work_item.ts +0 -75
  253. package/src/app/use_cases/realign_done.ts +0 -45
  254. package/src/app/use_cases/realign_status.ts +0 -106
  255. package/src/app/use_cases/record_progress.ts +0 -118
  256. package/src/app/use_cases/show_elements.ts +0 -18
  257. package/src/app/use_cases/show_notes.ts +0 -11
  258. package/src/app/use_cases/show_scope.ts +0 -51
  259. package/src/app/use_cases/upstream_diff.ts +0 -55
  260. package/src/domain/approvals/agent_approval.ts +0 -68
  261. package/src/domain/approvals/evaluate.ts +0 -36
  262. package/src/domain/approvals/fingerprint.ts +0 -52
  263. package/src/domain/approvals/rebase.ts +0 -39
  264. package/src/domain/approvals/records.ts +0 -50
  265. package/src/domain/approvals/upstream_text.ts +0 -36
  266. package/src/domain/architecture_sections.ts +0 -23
  267. package/src/domain/check/decision_comments.ts +0 -38
  268. package/src/domain/decision_citations.ts +0 -62
  269. package/src/domain/edits/agents_file.ts +0 -58
  270. package/src/domain/edits/managed_tables.ts +0 -60
  271. package/src/domain/edits/progress_block.ts +0 -33
  272. package/src/domain/edits/status_lines.ts +0 -52
  273. package/src/domain/elements.ts +0 -126
  274. package/src/domain/journal.ts +0 -109
  275. package/src/domain/land/bookkeeping.ts +0 -54
  276. package/src/domain/land/merge_plan.ts +0 -137
  277. package/src/domain/land/text_ops.ts +0 -66
  278. package/src/domain/line_diff.ts +0 -56
  279. package/src/domain/project/artifact_ref.ts +0 -42
  280. package/src/domain/project/deltas.ts +0 -49
  281. package/src/domain/project/element_lookup.ts +0 -124
  282. package/src/domain/project/living_model.ts +0 -55
  283. package/src/domain/project/new_items.ts +0 -48
  284. package/src/domain/project/notes.ts +0 -74
  285. package/src/domain/project/plan_parts.ts +0 -111
  286. package/src/domain/project/progress.ts +0 -43
  287. package/src/domain/project/project_view.ts +0 -72
  288. package/src/domain/project/roadmap.ts +0 -43
  289. package/src/domain/project/scope_view.ts +0 -134
  290. package/src/domain/project/work_items.ts +0 -68
  291. package/src/domain/status/next_step.ts +0 -181
  292. package/src/domain/status/pending_rethink.ts +0 -22
  293. package/src/domain/status/verification.ts +0 -29
  294. package/src/domain/status/work_status.ts +0 -125
  295. package/src/domain/validation/decision_rules.ts +0 -76
  296. package/src/domain/validation/delta_rules.ts +0 -81
  297. package/src/domain/validation/element_rules.ts +0 -104
  298. package/src/domain/validation/future_rules.ts +0 -40
  299. package/src/domain/validation/living_rules.ts +0 -99
  300. package/src/domain/validation/plan_rules.ts +0 -124
  301. package/src/domain/validation/project_rules.ts +0 -115
  302. package/src/domain/validation/size_rules.ts +0 -45
  303. package/src/domain/validation/vision_rules.ts +0 -41
  304. package/src/domain/validation/work_item_rules.ts +0 -175
  305. package/src/domain/versions/realign_notes.ts +0 -72
  306. package/src/domain/versions/rule_versions.ts +0 -29
  307. package/src/domain/versions/scheduled_exceptions.ts +0 -41
  308. package/templates/init/gitattributes +0 -2
@@ -1,124 +0,0 @@
1
- import { insideModuleMap } from "../check/check_source.js";
2
- import { factsOf, isUnbuilt } from "../elements.js";
3
- import { matchesAny } from "../glob.js";
4
- import { findIds, kindOf } from "../ids.js";
5
-
6
-
7
- import { isPlanned, readScope, readTasks, readTestPlan, } from "../project/plan_parts.js";
8
-
9
- import { error, warning, } from "./problem.js";
10
- import { planSizeProblems } from "./size_rules.js";
11
-
12
- const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
13
-
14
- /**
15
- * Rules for one buildable change: its scope names only things that can be built. Once it is
16
- * planned, every scope ID is proved by a task, every delivered scenario has a behaviour test,
17
- * and every port a built adapter implements has a contract test.
18
- */
19
- export function changePlanProblems(
20
- view ,
21
- path ,
22
- doc ,
23
- delta ,
24
- available ,
25
- ) {
26
- const scope = readScope(doc);
27
- if (scope === null) return [error(path, undefined, "missing-section", 'the change needs a "## Scope" section')];
28
- const problems = [...scopeTargetProblems(view, path, scope, delta.delta)];
29
- if (!isPlanned(doc)) return [...problems, warning(path, undefined, "not-planned", "the change has no test plan or tasks yet; plan fills them")];
30
- const { tasks, malformed } = readTasks(doc);
31
- for (const line of malformed) {
32
- problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
33
- }
34
- if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
35
- problems.push(...planSizeProblems(view, path, tasks.length));
36
-
37
- const proved = new Set(tasks.flatMap((task) => task.proves));
38
- const taskPaths = tasks.flatMap((task) => task.paths);
39
- for (const task of tasks) {
40
- for (const id of task.proves.filter((id) => !available.has(id))) {
41
- problems.push(error(path, task.line, "unknown-id", `${task.number} proves ${id}, which is not defined`));
42
- }
43
- }
44
- problems.push(...outsideModuleMap(view, path, tasks, delta));
45
- for (const id of [...scope.delivers, ...scope.builds]) {
46
- if (kindOf(id) === "MOD") {
47
- const modulePaths = modulePathsOf(view, delta.doc, delta.delta, id);
48
- if (!taskPaths.some((p) => modulePaths.some((m) => p.startsWith(m)))) {
49
- problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}; a module that owns other files, such as a build script, lists each under its Path`));
50
- }
51
- } else if (kindOf(id) === "REQ") {
52
- const scenarios = delta.delta.entries.find((e) => e.element.id === id)?.scenarios.map((s) => s.id) ?? [];
53
- if (!proved.has(id) && !scenarios.some((s) => proved.has(s))) problems.push(error(path, scope.line, "unproved-scope", `no task proves ${id} or its scenarios`));
54
- } else if (!proved.has(id)) {
55
- problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task proves it`));
56
- }
57
- }
58
-
59
- const rows = readTestPlan(doc);
60
- for (const row of rows) {
61
- if (!TEST_KINDS.includes(row.kind)) problems.push(error(path, row.line, "test-kind", `test kind must be one of ${TEST_KINDS.join(", ")}`));
62
- const subject = findIds(row.subject)[0];
63
- if (subject === undefined || !available.has(subject)) problems.push(error(path, row.line, "unknown-id", `test subject "${row.subject}" is not a defined ID`));
64
- if (row.file === "") problems.push(error(path, row.line, "test-file", "every test plan row names its test file"));
65
- }
66
- for (const id of scope.delivers.filter((id) => kindOf(id) === "SCN")) {
67
- if (!rows.some((row) => findIds(row.subject)[0] === id && row.kind === "behaviour")) {
68
- problems.push(error(path, scope.line, "missing-behaviour-test", `${id} is delivered but has no behaviour row in the test plan`));
69
- }
70
- }
71
- for (const adapter of scope.builds.filter((id) => kindOf(id) === "ADP")) {
72
- const ports = (factsOfScoped(view, delta.doc, delta.delta, adapter).get("Implements") ?? []).flatMap(findIds).filter((id) => kindOf(id) === "PORT");
73
- for (const port of ports.filter((p) => !rows.some((row) => findIds(row.subject)[0] === p && row.kind === "contract"))) {
74
- problems.push(error(path, scope.line, "missing-contract-test", `${adapter} implements ${port} but the test plan has no contract row for ${port}`));
75
- }
76
- }
77
- return problems;
78
- }
79
-
80
- /** Scope IDs must come from the item's deltas or be planned in the living docs. */
81
- function scopeTargetProblems(view , path , scope , delta ) {
82
- const problems = [];
83
- for (const id of [...scope.delivers, ...scope.builds]) {
84
- if (delta.ids.has(id)) continue;
85
- const living = view.living.elements.get(id);
86
- if (living === undefined) problems.push(error(path, scope.line, "unknown-id", `${id} is in scope but defined nowhere`));
87
- else if (!isUnbuilt(living.doc, living.element)) {
88
- problems.push(error(path, scope.line, "scope-not-planned", `${id} is already built; change it through a MODIFIED operation in the delta`));
89
- }
90
- }
91
- for (const id of scope.removes) {
92
- const removal = delta.entries.find((e) => e.element.id === id && e.element.op === "REMOVED");
93
- if (removal === undefined) problems.push(error(path, scope.line, "scope-removes", `${id} is in Removes but the delta has no REMOVED operation for it`));
94
- }
95
- if (scope.delivers.length + scope.builds.length + scope.removes.length === 0) {
96
- problems.push(warning(path, scope.line, "empty-scope", "the scope delivers and builds nothing"));
97
- }
98
- return problems;
99
- }
100
-
101
- /** An element's facts as this item will build it: from its delta when it has one, else from the living docs. */
102
- function factsOfScoped(view , doc , delta , id ) {
103
- const fromDelta = delta.entries.find((e) => e.element.id === id);
104
- const living = view.living.elements.get(id);
105
- return fromDelta !== undefined ? factsOf(doc, fromDelta.element) : living !== undefined ? factsOf(living.doc, living.element) : new Map ();
106
- }
107
-
108
- function modulePathsOf(view , doc , delta , id ) {
109
- return (factsOfScoped(view, doc, delta, id).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => p !== "");
110
- }
111
-
112
- /** Task paths that `cruze check` would report as outside every module, caught while the plan can still fix the module map. */
113
- function outsideModuleMap(view , path , tasks , delta ) {
114
- const source = view.config?.config?.source ?? [];
115
- const removed = new Set(delta.delta.entries.filter((e) => e.element.op === "REMOVED").map((e) => e.element.id));
116
- const modules = new Set([...view.living.elements.keys(), ...delta.delta.entries.map((e) => e.element.id)].filter((id) => kindOf(id) === "MOD" && !removed.has(id)));
117
- const modulePaths = [...modules].flatMap((id) => modulePathsOf(view, delta.doc, delta.delta, id));
118
- if (modulePaths.length === 0) return [];
119
- return tasks.flatMap((task) =>
120
- task.paths
121
- .filter((file) => matchesAny(file, source) && !insideModuleMap(file, modulePaths))
122
- .map((file) => warning(path, task.line, "outside-module-map", `${task.number} writes ${file}, which no module's Path covers; add it to a module's Path or move the file`)),
123
- );
124
- }
@@ -1,115 +0,0 @@
1
- import { factsOf } from "../elements.js";
2
- import { matchesAny } from "../glob.js";
3
- import { findIds } from "../ids.js";
4
- import { parseMarkdown, } from "../markdown.js";
5
- import { PATHS } from "../project/layout.js";
6
- import { readRoadmapItems } from "../project/roadmap.js";
7
-
8
- import { error, warning, } from "./problem.js";
9
-
10
- /** Rules that span the project: leftover template guides, the roadmap, the config and the README. */
11
-
12
- const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
13
- const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
14
-
15
- /**
16
- * Unfilled `<guides>` and `{{placeholders}}` outside code, in living docs and active work.
17
- * Managed blocks are skipped: the CLI writes them, and they may quote text such as `<COMMAND>`.
18
- */
19
- export function templateLeftovers(view ) {
20
- const paths = [...view.living.docs.keys(), PATHS.roadmap, PATHS.glossary, ...view.items.filter((i) => !i.archived).map((i) => i.path)];
21
- return paths.flatMap((path) => {
22
- const text = view.snapshot.get(path);
23
- if (text === undefined) return [];
24
- const doc = parseMarkdown(text);
25
- return doc.lines.flatMap((line, i) => {
26
- if (doc.inFence[i] || doc.inManaged[i]) return [];
27
- const withoutCode = line.replace(/`[^`]*`/g, "");
28
- if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) {
29
- return [error(path, i, "template-leftover", "replace the template guide or placeholder on this line")];
30
- }
31
- return [];
32
- });
33
- });
34
- }
35
-
36
- export function roadmapProblems(view ) {
37
- const text = view.snapshot.get(PATHS.roadmap);
38
- if (text === undefined) return [];
39
- const doc = parseMarkdown(text);
40
- const problems = [];
41
- const items = new Map ();
42
- for (const { slug: item, kind, blockedBy, line } of readRoadmapItems(text)) {
43
- if (items.has(item)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} appears twice`));
44
- if (!["feature", "change"].includes(kind)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} kind must be feature or change`));
45
- items.set(item, { blockedBy, line });
46
- }
47
- for (const [item, { blockedBy, line }] of items) {
48
- for (const blocker of blockedBy.filter((b) => !items.has(b))) problems.push(error(PATHS.roadmap, line, "roadmap-blocker", `${item} is blocked by unknown item ${blocker}`));
49
- }
50
- const cycle = findCycle(items);
51
- if (cycle !== null) problems.push(error(PATHS.roadmap, undefined, "roadmap-cycle", `blocking cycle: ${cycle.join(" -> ")}`));
52
- const coverage = doc.lines.join("\n").split(/^## Coverage$/m)[1] ?? "";
53
- for (const goal of [...view.living.elements.keys()].filter((id) => id.startsWith("GOAL-"))) {
54
- if (!findIds(coverage).includes(goal)) problems.push(warning(PATHS.roadmap, undefined, "goal-uncovered", `${goal} has no items in the coverage table`));
55
- }
56
- return problems;
57
- }
58
-
59
- function findCycle(items ) {
60
- const state = new Map ();
61
- const visit = (item , path ) => {
62
- if (state.get(item) === "done") return null;
63
- if (state.get(item) === "visiting") return [...path.slice(path.indexOf(item)), item];
64
- state.set(item, "visiting");
65
- for (const next of items.get(item)?.blockedBy ?? []) {
66
- const found = visit(next, [...path, item]);
67
- if (found !== null) return found;
68
- }
69
- state.set(item, "done");
70
- return null;
71
- };
72
- for (const item of items.keys()) {
73
- const found = visit(item, []);
74
- if (found !== null) return found;
75
- }
76
- return null;
77
- }
78
-
79
- export function configProblems(view ) {
80
- if (view.config === null) return [error(PATHS.config, undefined, "config-missing", "the project has no .cruze/config.yaml; run cruze init")];
81
- const problems = view.config.problems.map((message) => error(PATHS.config, undefined, "config", message));
82
- const config = view.config.config;
83
- const architecture = view.living.docs.get(PATHS.architecture);
84
- if (config === null || architecture === undefined || config.layers.length === 0) return problems;
85
- for (const { element } of [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-"))) {
86
- for (const path of modulePaths(architecture, element)) {
87
- const probe = path.endsWith("/") ? `${path}probe.file` : path;
88
- const layers = config.layers.filter((layer) => matchesAny(probe, layer.paths));
89
- if (layers.length !== 1) {
90
- problems.push(warning(PATHS.config, undefined, "module-layer", `${element.id} path ${path} falls in ${layers.length} layers; it should fall in exactly one`));
91
- }
92
- }
93
- }
94
- return problems;
95
- }
96
-
97
- function modulePaths(doc , element ) {
98
- return (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? ""));
99
- }
100
-
101
- /**
102
- * Once the vision is approved, the README says what the project is under its title.
103
- * Images, badges and HTML lines don't count as that overview.
104
- */
105
- export function readmeProblems(view ) {
106
- const text = view.snapshot.get(PATHS.readme);
107
- const visionApproved = [...view.approvals.values()].some((file) => file.approvals.some((record) => record.artifact === PATHS.vision));
108
- if (text === undefined || !visionApproved) return [];
109
- const doc = parseMarkdown(text);
110
- const title = doc.lines.findIndex((line) => /^# /.test(line));
111
- const next = doc.lines.findIndex((line, i) => i > title && /^#{1,6} /.test(line));
112
- const intro = doc.lines.slice(title + 1, next === -1 ? undefined : next);
113
- const overview = intro.some((line, i) => !doc.inFence[title + 1 + i] && line.trim() !== "" && !/^\s*(!\[|\[!\[|<)/.test(line));
114
- return overview ? [] : [warning(PATHS.readme, Math.max(title, 0), "readme-overview", "add a paragraph under the title saying what the project is, who it is for and why, from docs/vision.md")];
115
- }
@@ -1,45 +0,0 @@
1
- import { DEFAULT_CHANGE_LIMITS, } from "../config.js";
2
- import { kindOf } from "../ids.js";
3
-
4
-
5
- import { warning, } from "./problem.js";
6
-
7
- /**
8
- * Every change pays for its own plan review, code review, verifier run and land, so a change
9
- * should carry enough work to be worth them, and no more than one review and one run can hold.
10
- */
11
- export function changeLimits(view ) {
12
- return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
13
- }
14
-
15
- /** The rules a size warning comes from. An item stays outside the band only with the user's recorded acceptance. */
16
- export const SIZE_RULES = ["feature-size", "change-too-small", "change-too-large", "plan-too-large"];
17
-
18
- /** Whether the user accepted this feature or change outside the band, under `changes.exceptions`. */
19
- export function sizeAccepted(view , ref ) {
20
- return changeLimits(view).exceptions.some((exception) => exception.item === ref);
21
- }
22
-
23
- /** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
24
- export function changeRowSizeProblems(view , feature , rows , landed ) {
25
- const limits = changeLimits(view);
26
- const problems = [];
27
- for (const row of rows.filter((r) => !landed.has(r.change) && !sizeAccepted(view, `${feature.ref}/${r.change}`))) {
28
- if (rows.length > 1 && row.builds.length < limits.minBuilds) {
29
- problems.push(warning(feature.path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
30
- }
31
- const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
32
- if (scenarios > limits.maxScenarios) {
33
- problems.push(warning(feature.path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
34
- }
35
- }
36
- return problems;
37
- }
38
-
39
- /** A plan with more tasks than one code review and one verify can hold. */
40
- export function planSizeProblems(view , path , tasks ) {
41
- const { maxTasks } = changeLimits(view);
42
- const ref = view.items.find((item) => item.path === path)?.ref ?? path;
43
- if (tasks <= maxTasks || sizeAccepted(view, ref)) return [];
44
- return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
45
- }
@@ -1,41 +0,0 @@
1
- import { rowElements } from "../elements.js";
2
- import { findSection } from "../markdown.js";
3
- import { splitRow } from "../markdown_tables.js";
4
- import { PATHS } from "../project/layout.js";
5
-
6
- import { error, } from "./problem.js";
7
-
8
- /** The section and the cells after the ID that each long-term row of the vision holds. */
9
- const INTENT_ROWS = {
10
- PRINCIPLE: { section: "Principles", cells: ["Principle", "Why"] },
11
- LATER: { section: "Must stay possible", cells: ["Capability", "Likelihood", "Why", "Leave room for"] },
12
- };
13
-
14
- export const LIKELIHOODS = ["likely", "possible"];
15
-
16
- /**
17
- * The vision's principles and the capabilities that must stay possible are rows, so designs cite
18
- * them and approvals pin them. One project kept its principles in a seed note and its long-term
19
- * plans in the user's head: a feature reversed a principle unnoticed, and a later review proposed
20
- * removing what those plans needed.
21
- */
22
- export function visionProblems(view ) {
23
- const doc = view.living.docs.get(PATHS.vision);
24
- if (doc === undefined) return [];
25
- return rowElements(doc).flatMap((row) => {
26
- const shape = INTENT_ROWS[row.kind];
27
- if (shape === undefined) return [];
28
- const section = findSection(doc, shape.section);
29
- if (section === null || row.start < section.start || row.start >= section.end) {
30
- return [error(PATHS.vision, row.start, "vision-row", `${row.id} belongs in the table under ## ${shape.section}`)];
31
- }
32
- const cells = splitRow(doc.lines[row.start] ?? "").slice(1);
33
- if (cells.length !== shape.cells.length || cells.some((cell) => cell === "")) {
34
- return [error(PATHS.vision, row.start, "vision-row", `${row.id} needs ${shape.cells.join(", ")}, each filled`)];
35
- }
36
- if (row.kind === "LATER" && !LIKELIHOODS.includes(cells[1] ?? "")) {
37
- return [error(PATHS.vision, row.start, "vision-row", `${row.id}'s likelihood must be ${LIKELIHOODS.join(" or ")}`)];
38
- }
39
- return [];
40
- });
41
- }
@@ -1,175 +0,0 @@
1
- import { isUnbuilt } from "../elements.js";
2
- import { findIds, kindOf } from "../ids.js";
3
- import { elementHash } from "../hashing.js";
4
- import { findSection, } from "../markdown.js";
5
- import { readDelta, } from "../project/deltas.js";
6
- import { readChangesTable, readScope } from "../project/plan_parts.js";
7
- import { readProgress } from "../project/progress.js";
8
-
9
- import { changesOf, featureOf, } from "../project/work_items.js";
10
- import { deltaProblems } from "./delta_rules.js";
11
- import { malformedIdHeadings, lineOf } from "./living_rules.js";
12
- import { changePlanProblems } from "./plan_rules.js";
13
- import { error, warning, } from "./problem.js";
14
- import { changeRowSizeProblems, sizeAccepted } from "./size_rules.js";
15
-
16
- const REQUIRED_SECTIONS = {
17
- feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
18
- change: ["Scope", "Test plan", "Tasks"],
19
- standalone: ["Intent", "Spec delta", "Architecture delta", "Scope", "Test plan", "Tasks"],
20
- };
21
-
22
- /** Rules for every active feature and change. Archived work is history and is not checked. */
23
- export function workItemProblems(view ) {
24
- return view.items.filter((item) => !item.archived).flatMap((item) => itemProblems(view, item));
25
- }
26
-
27
- function itemProblems(view , item ) {
28
- const { path, doc } = item;
29
- const problems = [...malformedIdHeadings(path, doc)];
30
- const id = doc.frontmatter?.["id"];
31
- if (id !== item.folderName) problems.push(error(path, 0, "frontmatter", `frontmatter id must be "${item.folderName}", the folder name`));
32
- const title = doc.frontmatter?.["title"];
33
- if (typeof title !== "string" || title.trim() === "") problems.push(error(path, 0, "frontmatter", "frontmatter needs a title"));
34
- for (const section of REQUIRED_SECTIONS[item.kind]) {
35
- if (findSection(doc, section) === null) problems.push(error(path, undefined, "missing-section", `missing "## ${section}"`));
36
- }
37
-
38
- const feature = item.kind === "change" ? featureOf(view.items, item) : undefined;
39
- if (item.kind === "change" && feature === undefined) problems.push(error(path, undefined, "orphan-change", "the change's feature.md is missing"));
40
- const deltaSource = item.kind === "change" ? feature : item;
41
- const delta = deltaSource === undefined ? readDelta(doc) : readDelta(deltaSource.doc);
42
- const available = new Set([...view.living.elements.keys(), ...delta.ids]);
43
-
44
- const text = doc.lines.join("\n");
45
- for (const cited of findIds(text).filter((cited) => !available.has(cited))) {
46
- problems.push(error(path, lineOf(text, cited), "unknown-id", `${cited} is cited but defined neither in the living docs nor in this work's deltas`));
47
- }
48
- const dropped = droppedScenarios(view, delta);
49
- for (const cited of findIds(text).filter((cited) => dropped.has(cited))) {
50
- problems.push(error(path, lineOf(text, cited), "cites-dropped", `${cited} is cited, but ${dropped.get(cited)} in this work's delta leaves it out, so it won't exist once the work lands; describe it in words instead`));
51
- }
52
- if (item.kind !== "change") problems.push(...deltaProblems(view, item));
53
- if (item.kind === "feature") problems.push(...featureScopeProblems(view, item, delta));
54
- // A landed change is history: its scope named what it built, which is now built.
55
- const landed = item.kind !== "feature" && readProgress(doc).landed !== undefined;
56
- if (item.kind !== "feature" && deltaSource !== undefined && !landed) {
57
- problems.push(...changePlanProblems(view, path, doc, { delta, doc: deltaSource.doc }, available));
58
- }
59
- if (item.kind === "standalone") problems.push(...coverageProblems(view, path, delta, doc, [{ name: "this change", ids: scopeIds(item) }]));
60
- return problems;
61
- }
62
-
63
- /** A feature's Changes table covers its deltas, and each change folder agrees with its row. */
64
- /** More changes than this, and one design review can't hold the whole feature: contradictions slip through to plan. */
65
- const MAX_CHANGES = 6;
66
-
67
- function featureScopeProblems(view , feature , delta ) {
68
- const rows = readChangesTable(feature.doc);
69
- if (rows.length === 0) return [warning(feature.path, undefined, "not-designed", "the feature has no changes yet; architect designs and splits it")];
70
- const problems = [];
71
- const landedChanges = new Set(
72
- [...readProgress(feature.doc).changes].filter(([, state]) => state === "landed").map(([change]) => change),
73
- );
74
- const names = rows.map((row) => row.change);
75
- const extra = rows[MAX_CHANGES];
76
- if (extra !== undefined && !sizeAccepted(view, feature.ref)) {
77
- problems.push(warning(feature.path, extra.line, "feature-size", `the feature has ${rows.length} changes, over ${MAX_CHANGES}; split it into features a single design review can hold`));
78
- }
79
- problems.push(...changeRowSizeProblems(view, feature, rows, landedChanges));
80
- for (const row of rows) {
81
- if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
82
- if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
83
- for (const dependency of row.dependsOn) {
84
- const at = names.indexOf(dependency);
85
- if (at === -1 || at >= names.indexOf(row.change)) problems.push(error(feature.path, row.line, "change-order", `${row.change} depends on ${dependency}, which must be an earlier change`));
86
- }
87
- for (const id of landedChanges.has(row.change) ? [] : [...row.delivers, ...row.builds]) {
88
- const living = view.living.elements.get(id);
89
- const planned = living !== undefined && isUnbuilt(living.doc, living.element);
90
- if (!delta.ids.has(id) && !planned) problems.push(error(feature.path, row.line, "scope-not-planned", `${id} must come from this feature's deltas or be planned in the living docs`));
91
- }
92
- }
93
- problems.push(...coverageProblems(view, feature.path, delta, feature.doc, rows.map((row) => ({ name: row.change, ids: [...row.delivers, ...row.builds, ...row.removes], landed: landedChanges.has(row.change) }))));
94
-
95
- for (const change of changesOf(view.items, feature)) {
96
- const row = rows.find((r) => r.change === change.folderName);
97
- if (row === undefined) {
98
- problems.push(error(change.path, undefined, "change-not-listed", `${change.folderName} is not in the feature's Changes table`));
99
- continue;
100
- }
101
- const expected = new Set([...row.delivers, ...row.builds, ...row.removes]);
102
- const actual = new Set(scopeIds(change));
103
- const differ = [...expected].filter((id) => !actual.has(id)).concat([...actual].filter((id) => !expected.has(id)));
104
- if (differ.length > 0) problems.push(error(change.path, readScope(change.doc)?.line, "scope-mismatch", `Scope differs from the feature's Changes row: ${differ.join(", ")}`));
105
- }
106
- return problems;
107
- }
108
-
109
- /**
110
- * Every delta ID lands exactly once. A requirement is covered by its own ID or one of its
111
- * scenarios. A scenario must be covered when it is new or its text changed; an unchanged
112
- * scenario kept by a MODIFIED requirement may be left out.
113
- */
114
- function coverageProblems(
115
- view ,
116
- path ,
117
- delta ,
118
- deltaDoc ,
119
- groups ,
120
- ) {
121
- const coveredBy = new Map ();
122
- const pendingBy = new Map ();
123
- for (const group of groups) {
124
- for (const id of group.ids) {
125
- coveredBy.set(id, [...(coveredBy.get(id) ?? []), group.name]);
126
- if (group.landed !== true) pendingBy.set(id, [...(pendingBy.get(id) ?? []), group.name]);
127
- }
128
- }
129
- const problems = [];
130
- // A change that already landed may be followed by one that rebuilds the same element after a rethink.
131
- const once = (id , line , required ) => {
132
- const pending = pendingBy.get(id) ?? [];
133
- if (required && (coveredBy.get(id) ?? []).length === 0) problems.push(error(path, line, "uncovered", `${id} is not in any change's scope`));
134
- if (pending.length > 1) problems.push(error(path, line, "covered-twice", `${id} is in the scope of ${pending.join(" and ")}; it must land exactly once`));
135
- };
136
- for (const { element, scenarios } of delta.entries) {
137
- if (kindOf(element.id) !== "REQ") {
138
- once(element.id, element.start, true);
139
- continue;
140
- }
141
- once(element.id, element.start, false);
142
- if (![element.id, ...scenarios.map((s) => s.id)].some((id) => coveredBy.has(id))) {
143
- problems.push(error(path, element.start, "uncovered", `${element.id} is not delivered by any change`));
144
- }
145
- for (const scenario of scenarios) {
146
- const living = view.living.elements.get(scenario.id);
147
- const changed = living === undefined || elementHash(living.doc, living.element) !== elementHash(deltaDoc, scenario);
148
- once(scenario.id, scenario.start, element.op !== "REMOVED" && changed);
149
- }
150
- }
151
- return problems;
152
- }
153
-
154
- function scopeIds(item ) {
155
- const scope = readScope(item.doc);
156
- return scope === null ? [] : [...scope.delivers, ...scope.builds, ...scope.removes];
157
- }
158
-
159
- /**
160
- * Living scenarios that a MODIFIED requirement in the delta leaves out, each with that requirement.
161
- * Land drops them, so a citation of one passes validation now and breaks only at land.
162
- */
163
- function droppedScenarios(view , delta ) {
164
- const dropped = new Map ();
165
- for (const entry of delta.entries.filter((e) => e.element.op === "MODIFIED" && kindOf(e.element.id) === "REQ")) {
166
- const living = view.living.elements.get(entry.element.id);
167
- if (living === undefined) continue;
168
- const kept = new Set(entry.scenarios.map((s) => s.id));
169
- for (const scenario of view.living.elements.values()) {
170
- const nested = scenario.doc === living.doc && scenario.element.start > living.element.start && scenario.element.start < living.element.end;
171
- if (nested && kindOf(scenario.id) === "SCN" && !kept.has(scenario.id) && !delta.ids.has(scenario.id)) dropped.set(scenario.id, entry.element.id);
172
- }
173
- }
174
- return dropped;
175
- }
@@ -1,72 +0,0 @@
1
- import { compareVersions } from "./cruze_version.js";
2
-
3
- /**
4
- * A realign note: one change in what Cruze expects of a project, shipped in
5
- * `skills/realign/notes/<version>.md` so `realign` can bring existing code up to it.
6
- */
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
- const APPLIES = { "existing code": "existing", "new work only": "new" };
28
-
29
- /** Parses one version's notes file. Each note is a `### <id>: <title>` heading with facts under it. */
30
- export function parseRealignNotes(version , text ) {
31
- const notes = [];
32
- const problems = [];
33
- const sections = text.split(/^### /m).slice(1);
34
- for (const section of sections) {
35
- const [heading = "", ...body] = section.split("\n");
36
- const match = /^([a-z0-9]+(?:-[a-z0-9]+)*): (.+)$/.exec(heading.trim());
37
- if (match === null) {
38
- problems.push(`${version}: "### ${heading.trim()}" must read "### <id>: <title>"`);
39
- continue;
40
- }
41
- const [, id = "", title = ""] = match;
42
- const fact = (key ) => body.map((line) => new RegExp(`^- ${key}: (.+)$`).exec(line)?.[1]?.trim()).find((v) => v !== undefined);
43
- const applies = APPLIES[fact("Applies to") ?? ""];
44
- const rule = fact("Rule");
45
- const detect = fact("Detect")?.replace(/`/g, "");
46
- const ask = fact("Ask");
47
- const migrate = fact("Migrate");
48
- if (applies === undefined) problems.push(`${version} ${id}: "- Applies to:" must be "existing code" or "new work only"`);
49
- if (rule === undefined) problems.push(`${version} ${id}: needs "- Rule:"`);
50
- if (detect !== undefined && !/^(check|validate):[a-z-]+$/.test(detect)) problems.push(`${version} ${id}: "- Detect:" names a rule as check:<rule> or validate:<rule>`);
51
- if (applies === "existing" && detect === undefined && ask === undefined) problems.push(`${version} ${id}: a note for existing code needs "- Detect:" or "- Ask:"`);
52
- if (applies === "existing" && migrate === undefined) problems.push(`${version} ${id}: a note for existing code needs "- Migrate:"`);
53
- notes.push({
54
- id,
55
- title,
56
- version,
57
- appliesTo: applies ?? "new",
58
- rule: rule ?? "",
59
- ...(detect === undefined ? {} : { detect }),
60
- ...(ask === undefined ? {} : { ask }),
61
- ...(migrate === undefined ? {} : { migrate }),
62
- });
63
- }
64
- const ids = notes.map((n) => n.id);
65
- ids.filter((id, i) => ids.indexOf(id) !== i).forEach((id) => problems.push(`${version}: note ${id} appears twice`));
66
- return { notes, problems };
67
- }
68
-
69
- /** Notes after the version the code meets, up to and including the version installed. */
70
- export function notesBetween(notes , standards , target ) {
71
- return notes.filter((n) => compareVersions(n.version, standards) > 0 && compareVersions(n.version, target) <= 0);
72
- }
@@ -1,29 +0,0 @@
1
- import { compareVersions } from "./cruze_version.js";
2
-
3
- /** The first release of Cruze. A project that records no `standards:` meets its expectations. */
4
- export const FIRST_RELEASE = "0.0.1";
5
-
6
- /**
7
- * The release that introduced each rule reporting on existing code, keyed as `<command>:<rule>`.
8
- * A rule newer than the project's `standards:` still reports, but can't fail CI until the project realigns.
9
- * Every entry needs a realign note in `skills/realign/notes/<version>.md`; the bundle lint checks it.
10
- */
11
- export const RULE_SINCE = {
12
- "check:test-placement": "0.0.2",
13
- "validate:outside-module-map": "0.0.2",
14
- "validate:readme-overview": "0.0.2",
15
- "validate:feature-size": "0.0.2",
16
- "validate:change-too-small": "0.0.5",
17
- "validate:change-too-large": "0.0.5",
18
- "validate:plan-too-large": "0.0.5",
19
- "validate:module-path-glob": "0.0.8",
20
- "validate:decision-citation": "0.0.10",
21
- "check:decision-citation": "0.0.10",
22
- "validate:future-unanchored": "0.0.10",
23
- };
24
-
25
- /** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
26
- export function isPending(command , rule , standards ) {
27
- const since = RULE_SINCE[`${command}:${rule}`];
28
- return since !== undefined && compareVersions(since, standards) > 0;
29
- }
@@ -1,41 +0,0 @@
1
- import { parseDocument, } from "yaml";
2
-
3
- /**
4
- * `check.exceptions` entries that `realign` adds for findings a prefactor change will fix,
5
- * and that landing that change removes. Edited through the YAML document, so comments survive.
6
- */
7
-
8
-
9
-
10
-
11
-
12
-
13
- const scheduledReason = (version , note , ref ) => `realign ${version} (${note}): scheduled in ${ref}`;
14
-
15
- /** The config text with an exception for each finding, naming the change that will fix it. */
16
- export function withScheduledExceptions(configText , version , ref , findings ) {
17
- if (findings.length === 0) return configText;
18
- const doc = parseDocument(configText);
19
- const listed = new Set(((doc.getIn(["check", "exceptions"]) )?.toJSON() ?? []).map((e) => e.path));
20
- for (const finding of findings.filter((f, i) => !listed.has(f.path) && findings.findIndex((o) => o.path === f.path) === i)) {
21
- doc.addIn(["check", "exceptions"], doc.createNode({ path: finding.path, reason: scheduledReason(version, finding.note, ref) }));
22
- }
23
- return render(doc);
24
- }
25
-
26
- /** The config text without the exceptions scheduled in a change, once it lands; unchanged when it had none. */
27
- export function withoutScheduledExceptions(configText , ref ) {
28
- const doc = parseDocument(configText);
29
- const exceptions = doc.getIn(["check", "exceptions"]) ;
30
- if (exceptions === undefined || !Array.isArray(exceptions.items)) return { text: configText, removed: [] };
31
- const suffix = `: scheduled in ${ref}`;
32
- const removed = exceptions.items.map((item) => item.toJSON()).filter((e) => e.reason?.endsWith(suffix)).map((e) => e.path ?? "");
33
- if (removed.length === 0) return { text: configText, removed: [] };
34
- exceptions.items = exceptions.items.filter((item) => !(item.toJSON().reason ?? "").endsWith(suffix));
35
- return { text: render(doc), removed };
36
- }
37
-
38
- /** Renders without wrapping long lines or padding flow lists, so an edit changes only what it touches. */
39
- function render(doc ) {
40
- return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
41
- }