cap-pro 1.0.0

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 (275) hide show
  1. package/.claude-plugin/README.md +26 -0
  2. package/.claude-plugin/marketplace.json +24 -0
  3. package/.claude-plugin/plugin.json +24 -0
  4. package/LICENSE +21 -0
  5. package/README.ja-JP.md +834 -0
  6. package/README.ko-KR.md +823 -0
  7. package/README.md +806 -0
  8. package/README.pt-BR.md +452 -0
  9. package/README.zh-CN.md +800 -0
  10. package/agents/cap-architect.md +269 -0
  11. package/agents/cap-brainstormer.md +207 -0
  12. package/agents/cap-curator.md +276 -0
  13. package/agents/cap-debugger.md +365 -0
  14. package/agents/cap-designer.md +246 -0
  15. package/agents/cap-historian.md +464 -0
  16. package/agents/cap-migrator.md +291 -0
  17. package/agents/cap-prototyper.md +197 -0
  18. package/agents/cap-validator.md +308 -0
  19. package/bin/install.js +5433 -0
  20. package/cap/bin/cap-tools.cjs +853 -0
  21. package/cap/bin/lib/arc-scanner.cjs +344 -0
  22. package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
  23. package/cap/bin/lib/cap-anchor.cjs +228 -0
  24. package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
  25. package/cap/bin/lib/cap-checkpoint.cjs +434 -0
  26. package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
  27. package/cap/bin/lib/cap-cluster-display.cjs +52 -0
  28. package/cap/bin/lib/cap-cluster-format.cjs +245 -0
  29. package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
  30. package/cap/bin/lib/cap-cluster-io.cjs +212 -0
  31. package/cap/bin/lib/cap-completeness.cjs +540 -0
  32. package/cap/bin/lib/cap-deps.cjs +583 -0
  33. package/cap/bin/lib/cap-design-families.cjs +332 -0
  34. package/cap/bin/lib/cap-design.cjs +966 -0
  35. package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
  36. package/cap/bin/lib/cap-doctor.cjs +752 -0
  37. package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
  38. package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
  39. package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
  40. package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
  41. package/cap/bin/lib/cap-feature-map.cjs +1943 -0
  42. package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
  43. package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
  44. package/cap/bin/lib/cap-learn-review.cjs +1072 -0
  45. package/cap/bin/lib/cap-learning-signals.cjs +627 -0
  46. package/cap/bin/lib/cap-loader.cjs +227 -0
  47. package/cap/bin/lib/cap-logger.cjs +57 -0
  48. package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
  49. package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
  50. package/cap/bin/lib/cap-memory-dir.cjs +987 -0
  51. package/cap/bin/lib/cap-memory-engine.cjs +698 -0
  52. package/cap/bin/lib/cap-memory-extends.cjs +398 -0
  53. package/cap/bin/lib/cap-memory-graph.cjs +790 -0
  54. package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
  55. package/cap/bin/lib/cap-memory-pin.cjs +183 -0
  56. package/cap/bin/lib/cap-memory-platform.cjs +490 -0
  57. package/cap/bin/lib/cap-memory-prune.cjs +707 -0
  58. package/cap/bin/lib/cap-memory-schema.cjs +812 -0
  59. package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
  60. package/cap/bin/lib/cap-migrate.cjs +540 -0
  61. package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
  62. package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
  63. package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
  64. package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
  65. package/cap/bin/lib/cap-reconcile.cjs +570 -0
  66. package/cap/bin/lib/cap-research-gate.cjs +218 -0
  67. package/cap/bin/lib/cap-scope-filter.cjs +402 -0
  68. package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
  69. package/cap/bin/lib/cap-session-extract.cjs +987 -0
  70. package/cap/bin/lib/cap-session.cjs +445 -0
  71. package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
  72. package/cap/bin/lib/cap-stack-docs.cjs +646 -0
  73. package/cap/bin/lib/cap-tag-observer.cjs +371 -0
  74. package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
  75. package/cap/bin/lib/cap-telemetry.cjs +466 -0
  76. package/cap/bin/lib/cap-test-audit.cjs +1438 -0
  77. package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
  78. package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
  79. package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
  80. package/cap/bin/lib/cap-trace.cjs +399 -0
  81. package/cap/bin/lib/cap-trust-mode.cjs +336 -0
  82. package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
  83. package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
  84. package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
  85. package/cap/bin/lib/cap-ui.cjs +1245 -0
  86. package/cap/bin/lib/cap-upgrade.cjs +1028 -0
  87. package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
  88. package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
  89. package/cap/bin/lib/cli/init-router.cjs +68 -0
  90. package/cap/bin/lib/cli/phase-router.cjs +102 -0
  91. package/cap/bin/lib/cli/state-router.cjs +61 -0
  92. package/cap/bin/lib/cli/template-router.cjs +37 -0
  93. package/cap/bin/lib/cli/uat-router.cjs +29 -0
  94. package/cap/bin/lib/cli/validation-router.cjs +26 -0
  95. package/cap/bin/lib/cli/verification-router.cjs +31 -0
  96. package/cap/bin/lib/cli/workstream-router.cjs +39 -0
  97. package/cap/bin/lib/commands.cjs +961 -0
  98. package/cap/bin/lib/config.cjs +467 -0
  99. package/cap/bin/lib/convention-reader.cjs +258 -0
  100. package/cap/bin/lib/core.cjs +1241 -0
  101. package/cap/bin/lib/feature-aggregator.cjs +423 -0
  102. package/cap/bin/lib/frontmatter.cjs +337 -0
  103. package/cap/bin/lib/init.cjs +1443 -0
  104. package/cap/bin/lib/manifest-generator.cjs +383 -0
  105. package/cap/bin/lib/milestone.cjs +253 -0
  106. package/cap/bin/lib/model-profiles.cjs +69 -0
  107. package/cap/bin/lib/monorepo-context.cjs +226 -0
  108. package/cap/bin/lib/monorepo-migrator.cjs +509 -0
  109. package/cap/bin/lib/phase.cjs +889 -0
  110. package/cap/bin/lib/profile-output.cjs +989 -0
  111. package/cap/bin/lib/profile-pipeline.cjs +540 -0
  112. package/cap/bin/lib/roadmap.cjs +330 -0
  113. package/cap/bin/lib/security.cjs +394 -0
  114. package/cap/bin/lib/session-manager.cjs +292 -0
  115. package/cap/bin/lib/skeleton-generator.cjs +179 -0
  116. package/cap/bin/lib/state.cjs +1032 -0
  117. package/cap/bin/lib/template.cjs +231 -0
  118. package/cap/bin/lib/test-detector.cjs +62 -0
  119. package/cap/bin/lib/uat.cjs +283 -0
  120. package/cap/bin/lib/verify.cjs +889 -0
  121. package/cap/bin/lib/workspace-detector.cjs +371 -0
  122. package/cap/bin/lib/workstream.cjs +492 -0
  123. package/cap/commands/gsd/workstreams.md +63 -0
  124. package/cap/references/arc-standard.md +315 -0
  125. package/cap/references/cap-agent-architecture.md +101 -0
  126. package/cap/references/cap-gitignore-template +9 -0
  127. package/cap/references/cap-zero-deps.md +158 -0
  128. package/cap/references/checkpoints.md +778 -0
  129. package/cap/references/continuation-format.md +249 -0
  130. package/cap/references/contract-test-templates.md +312 -0
  131. package/cap/references/feature-map-template.md +25 -0
  132. package/cap/references/git-integration.md +295 -0
  133. package/cap/references/git-planning-commit.md +38 -0
  134. package/cap/references/model-profiles.md +174 -0
  135. package/cap/references/phase-numbering.md +126 -0
  136. package/cap/references/planning-config.md +202 -0
  137. package/cap/references/property-test-templates.md +316 -0
  138. package/cap/references/security-test-templates.md +347 -0
  139. package/cap/references/session-template.json +8 -0
  140. package/cap/references/tdd.md +263 -0
  141. package/cap/references/user-profiling.md +681 -0
  142. package/cap/references/verification-patterns.md +612 -0
  143. package/cap/templates/UAT.md +265 -0
  144. package/cap/templates/claude-md.md +175 -0
  145. package/cap/templates/codebase/architecture.md +255 -0
  146. package/cap/templates/codebase/concerns.md +310 -0
  147. package/cap/templates/codebase/conventions.md +307 -0
  148. package/cap/templates/codebase/integrations.md +280 -0
  149. package/cap/templates/codebase/stack.md +186 -0
  150. package/cap/templates/codebase/structure.md +285 -0
  151. package/cap/templates/codebase/testing.md +480 -0
  152. package/cap/templates/config.json +44 -0
  153. package/cap/templates/context.md +352 -0
  154. package/cap/templates/continue-here.md +78 -0
  155. package/cap/templates/copilot-instructions.md +7 -0
  156. package/cap/templates/debug-subagent-prompt.md +91 -0
  157. package/cap/templates/discussion-log.md +63 -0
  158. package/cap/templates/milestone-archive.md +123 -0
  159. package/cap/templates/milestone.md +115 -0
  160. package/cap/templates/phase-prompt.md +610 -0
  161. package/cap/templates/planner-subagent-prompt.md +117 -0
  162. package/cap/templates/project.md +186 -0
  163. package/cap/templates/requirements.md +231 -0
  164. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  165. package/cap/templates/research-project/FEATURES.md +147 -0
  166. package/cap/templates/research-project/PITFALLS.md +200 -0
  167. package/cap/templates/research-project/STACK.md +120 -0
  168. package/cap/templates/research-project/SUMMARY.md +170 -0
  169. package/cap/templates/research.md +552 -0
  170. package/cap/templates/roadmap.md +202 -0
  171. package/cap/templates/state.md +176 -0
  172. package/cap/templates/summary.md +364 -0
  173. package/cap/templates/user-preferences.md +498 -0
  174. package/cap/templates/verification-report.md +322 -0
  175. package/cap/workflows/add-phase.md +112 -0
  176. package/cap/workflows/add-tests.md +351 -0
  177. package/cap/workflows/add-todo.md +158 -0
  178. package/cap/workflows/audit-milestone.md +340 -0
  179. package/cap/workflows/audit-uat.md +109 -0
  180. package/cap/workflows/autonomous.md +891 -0
  181. package/cap/workflows/check-todos.md +177 -0
  182. package/cap/workflows/cleanup.md +152 -0
  183. package/cap/workflows/complete-milestone.md +767 -0
  184. package/cap/workflows/diagnose-issues.md +231 -0
  185. package/cap/workflows/discovery-phase.md +289 -0
  186. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  187. package/cap/workflows/discuss-phase.md +1049 -0
  188. package/cap/workflows/do.md +104 -0
  189. package/cap/workflows/execute-phase.md +846 -0
  190. package/cap/workflows/execute-plan.md +514 -0
  191. package/cap/workflows/fast.md +105 -0
  192. package/cap/workflows/forensics.md +265 -0
  193. package/cap/workflows/health.md +181 -0
  194. package/cap/workflows/help.md +660 -0
  195. package/cap/workflows/insert-phase.md +130 -0
  196. package/cap/workflows/list-phase-assumptions.md +178 -0
  197. package/cap/workflows/list-workspaces.md +56 -0
  198. package/cap/workflows/manager.md +362 -0
  199. package/cap/workflows/map-codebase.md +377 -0
  200. package/cap/workflows/milestone-summary.md +223 -0
  201. package/cap/workflows/new-milestone.md +486 -0
  202. package/cap/workflows/new-project.md +1250 -0
  203. package/cap/workflows/new-workspace.md +237 -0
  204. package/cap/workflows/next.md +97 -0
  205. package/cap/workflows/node-repair.md +92 -0
  206. package/cap/workflows/note.md +156 -0
  207. package/cap/workflows/pause-work.md +176 -0
  208. package/cap/workflows/plan-milestone-gaps.md +273 -0
  209. package/cap/workflows/plan-phase.md +857 -0
  210. package/cap/workflows/plant-seed.md +169 -0
  211. package/cap/workflows/pr-branch.md +129 -0
  212. package/cap/workflows/profile-user.md +449 -0
  213. package/cap/workflows/progress.md +507 -0
  214. package/cap/workflows/quick.md +757 -0
  215. package/cap/workflows/remove-phase.md +155 -0
  216. package/cap/workflows/remove-workspace.md +90 -0
  217. package/cap/workflows/research-phase.md +82 -0
  218. package/cap/workflows/resume-project.md +326 -0
  219. package/cap/workflows/review.md +228 -0
  220. package/cap/workflows/session-report.md +146 -0
  221. package/cap/workflows/settings.md +283 -0
  222. package/cap/workflows/ship.md +228 -0
  223. package/cap/workflows/stats.md +60 -0
  224. package/cap/workflows/transition.md +671 -0
  225. package/cap/workflows/ui-phase.md +298 -0
  226. package/cap/workflows/ui-review.md +161 -0
  227. package/cap/workflows/update.md +323 -0
  228. package/cap/workflows/validate-phase.md +170 -0
  229. package/cap/workflows/verify-phase.md +254 -0
  230. package/cap/workflows/verify-work.md +637 -0
  231. package/commands/cap/annotate.md +165 -0
  232. package/commands/cap/brainstorm.md +393 -0
  233. package/commands/cap/checkpoint.md +106 -0
  234. package/commands/cap/completeness.md +94 -0
  235. package/commands/cap/continue.md +72 -0
  236. package/commands/cap/debug.md +588 -0
  237. package/commands/cap/deps.md +169 -0
  238. package/commands/cap/design.md +479 -0
  239. package/commands/cap/init.md +354 -0
  240. package/commands/cap/iterate.md +249 -0
  241. package/commands/cap/learn.md +459 -0
  242. package/commands/cap/memory.md +275 -0
  243. package/commands/cap/migrate-feature-map.md +91 -0
  244. package/commands/cap/migrate-memory.md +108 -0
  245. package/commands/cap/migrate-tags.md +91 -0
  246. package/commands/cap/migrate.md +131 -0
  247. package/commands/cap/prototype.md +510 -0
  248. package/commands/cap/reconcile.md +121 -0
  249. package/commands/cap/review.md +360 -0
  250. package/commands/cap/save.md +72 -0
  251. package/commands/cap/scan.md +404 -0
  252. package/commands/cap/start.md +356 -0
  253. package/commands/cap/status.md +118 -0
  254. package/commands/cap/test-audit.md +262 -0
  255. package/commands/cap/test.md +394 -0
  256. package/commands/cap/trace.md +133 -0
  257. package/commands/cap/ui.md +167 -0
  258. package/hooks/dist/cap-check-update.js +115 -0
  259. package/hooks/dist/cap-context-monitor.js +185 -0
  260. package/hooks/dist/cap-learn-review-hook.js +114 -0
  261. package/hooks/dist/cap-learning-hook.js +192 -0
  262. package/hooks/dist/cap-memory.js +299 -0
  263. package/hooks/dist/cap-prompt-guard.js +97 -0
  264. package/hooks/dist/cap-statusline.js +157 -0
  265. package/hooks/dist/cap-tag-observer.js +115 -0
  266. package/hooks/dist/cap-version-check.js +112 -0
  267. package/hooks/dist/cap-workflow-guard.js +175 -0
  268. package/hooks/hooks.json +55 -0
  269. package/package.json +58 -0
  270. package/scripts/base64-scan.sh +262 -0
  271. package/scripts/build-hooks.js +93 -0
  272. package/scripts/cap-removal-checklist.md +202 -0
  273. package/scripts/prompt-injection-scan.sh +199 -0
  274. package/scripts/run-tests.cjs +181 -0
  275. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,966 @@
1
+ // @cap-context CAP F-062 DESIGN.md engine -- deterministic aesthetic picker + idempotent DESIGN.md writer.
2
+ // @cap-context CAP F-063 additively attaches stable DT-NNN / DC-NNN IDs to DESIGN.md entries and exposes helpers for feature-map traceability.
3
+ // @cap-context CAP F-064 adds a pure, read-only review engine for DESIGN.md (Anti-Slop-Check).
4
+ // @cap-decision DESIGN.md is a single markdown file at project root next to FEATURE-MAP.md. Zero-deps, diffable, inspectable.
5
+ // @cap-decision(F-063/D1) ID format is an INLINE suffix — bullet form `- key: value (id: DT-NNN)`, component form `### Name (id: DC-NNN)`. Preserves F-062's line-scan merge, stays dense, diff-friendly.
6
+ // @cap-decision(F-063/D2) ID assignment is SEQUENTIAL per type (DT-001, DT-002...; DC-001, DC-002...). Computed from max existing ID + 1 — deterministic, human-readable, no gaps required but tolerated.
7
+ // @cap-decision(F-063/D4) Stable-ID guarantee — once assigned, NEVER renumbered. assignDesignIds only fills entries without ids; existing IDs are preserved byte-identical. This keeps F-068 (visual editor) edits from churning diffs.
8
+ // @cap-decision(F-064/D1) Design rules source-of-truth: DEFAULT_DESIGN_RULES is a frozen superset of ANTI_SLOP_RULES plus structural checks. Users override via `.cap/design-rules.md` (markdown-bullet format, no YAML, matches CAP convention).
9
+ // @cap-decision(F-064/D2) Review report artifact location: `.cap/DESIGN-REVIEW.md`. DESIGN.md itself is never modified by review — AC-3 is a hard read-only constraint enforced at library boundary (reviewDesign takes strings, never a path).
10
+ // @cap-decision(F-064/D3) Violation severity levels: `error` (hard rule violation), `warning` (style suggestion), `info` (informational). Default `warning`. Declared per-rule.
11
+ // @cap-decision(F-064/D4) Idempotence guarantee: violations sorted by (location.id || '__global__', rule-name, location.line || 0). No timestamps in report. Same input → byte-identical output.
12
+ // @cap-decision Idempotence guaranteed by pinning all tokens per aesthetic family in a lookup table and emitting NO timestamps, NO LLM-generated flavor text.
13
+ // @cap-constraint Zero external dependencies -- Node.js built-ins only (fs, path).
14
+
15
+ 'use strict';
16
+
17
+ // @cap-feature(feature:F-062) cap:design Core — DESIGN.md + Aesthetic Picker
18
+ // @cap-feature(feature:F-063) Design-Feature Traceability — DT/DC IDs, tag recognition, and feature-map usesDesign helpers
19
+ // @cap-feature(feature:F-064) cap:design --review — Anti-Slop-Check (pure review engine, read-only)
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ // @cap-decision(F-064) Data extracted to cap-design-families.cjs once this file crossed 40KB.
25
+ // Re-exported below so the public API surface (AESTHETIC_FAMILIES, ANTI_SLOP_RULES, FAMILY_MAP,
26
+ // VALID_*) is unchanged for existing callers (F-062/F-063 tests, commands).
27
+ const {
28
+ ANTI_SLOP_RULES,
29
+ AESTHETIC_FAMILIES,
30
+ FAMILY_MAP,
31
+ VALID_READ_HEAVY,
32
+ VALID_USER_TYPES,
33
+ VALID_COURAGE,
34
+ } = require('./cap-design-families.cjs');
35
+
36
+ const DESIGN_FILE = 'DESIGN.md';
37
+
38
+ /**
39
+ * @typedef {Object} ColorTokens
40
+ * @property {string} primary
41
+ * @property {string} secondary
42
+ * @property {string} background
43
+ * @property {string} surface
44
+ * @property {string} text
45
+ * @property {string} muted
46
+ * @property {string} accent
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} TypographyTokens
51
+ * @property {string} family
52
+ * @property {string} familyMono
53
+ * @property {number[]} scale
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} AestheticFamily
58
+ * @property {string} key
59
+ * @property {string} name
60
+ * @property {string[]} referenceBrands
61
+ * @property {ColorTokens} colors
62
+ * @property {number[]} spacing
63
+ * @property {TypographyTokens} typography
64
+ * @property {Object<string, { variants: string[], states: string[] }>} components
65
+ */
66
+
67
+ // Data moved to cap-design-families.cjs — see require() at top of file.
68
+
69
+
70
+ // @cap-api mapAnswersToFamily(readHeavy, userType, courageFactor) -- Deterministic wizard-answer lookup.
71
+ // @cap-todo(ac:F-062/AC-2) Maps the 3 wizard answers to exactly one of the 9 aesthetic families.
72
+ // @cap-todo(ac:F-062/AC-7) Must return byte-identical result on repeated calls with same input.
73
+ /**
74
+ * @param {string} readHeavy - 'read-heavy' | 'scan-heavy'
75
+ * @param {string} userType - 'consumer' | 'professional' | 'developer'
76
+ * @param {string} courageFactor - 'safe' | 'balanced' | 'bold'
77
+ * @returns {AestheticFamily} The resolved family object (from AESTHETIC_FAMILIES lookup).
78
+ * @throws {Error} If any answer is not in the valid set.
79
+ */
80
+ function mapAnswersToFamily(readHeavy, userType, courageFactor) {
81
+ if (!VALID_READ_HEAVY.includes(readHeavy)) {
82
+ throw new Error(`Invalid readHeavy: ${readHeavy}. Expected one of ${VALID_READ_HEAVY.join(', ')}`);
83
+ }
84
+ if (!VALID_USER_TYPES.includes(userType)) {
85
+ throw new Error(`Invalid userType: ${userType}. Expected one of ${VALID_USER_TYPES.join(', ')}`);
86
+ }
87
+ if (!VALID_COURAGE.includes(courageFactor)) {
88
+ throw new Error(`Invalid courageFactor: ${courageFactor}. Expected one of ${VALID_COURAGE.join(', ')}`);
89
+ }
90
+
91
+ const key = `${readHeavy}|${userType}|${courageFactor}`;
92
+ const familyKey = FAMILY_MAP[key];
93
+ // @cap-risk If FAMILY_MAP becomes incomplete, this throws -- fail loud rather than returning default silently.
94
+ if (!familyKey) {
95
+ throw new Error(`No family mapping for key: ${key}`);
96
+ }
97
+ return AESTHETIC_FAMILIES[familyKey];
98
+ }
99
+
100
+ // @cap-api buildDesignMd({family, extras, withIds}) -- Returns DESIGN.md content string (idempotent).
101
+ // @cap-todo(ac:F-062/AC-3) Output contains: Aesthetic Family, Tokens (colors/spacing/typography), Components (Button + Card), Anti-Patterns.
102
+ // @cap-todo(ac:F-062/AC-6) Anti-Patterns block rendered from ANTI_SLOP_RULES.
103
+ // @cap-todo(ac:F-062/AC-7) No timestamps, no randomness -- same input -> byte-identical output.
104
+ // @cap-todo(ac:F-063/AC-1) When `withIds` option is truthy, tokens and components are born with inline DT-NNN / DC-NNN suffixes. Default-off for backward-compatible F-062 test snapshots.
105
+ // @cap-decision F-063 hook: tokens/components are written in a stable ordered list form so F-063 can append `id: DT-NNN` / `id: DC-NNN` inline without breaking the v1 parser.
106
+ /**
107
+ * @param {{ family: AestheticFamily, extras?: Object, withIds?: boolean }} input
108
+ * @returns {string} Full DESIGN.md content.
109
+ */
110
+ function buildDesignMd(input) {
111
+ if (!input || !input.family) {
112
+ throw new Error('buildDesignMd requires { family } input');
113
+ }
114
+ const fam = input.family;
115
+ const withIds = Boolean(input.withIds);
116
+ const lines = [];
117
+
118
+ lines.push('# DESIGN.md');
119
+ lines.push('');
120
+ lines.push('> Single source of truth for design identity, tokens, components, and anti-patterns.');
121
+ lines.push('> Written by /cap:design. Diff this file in git alongside FEATURE-MAP.md.');
122
+ lines.push('');
123
+
124
+ // Aesthetic Family
125
+ lines.push(`## Aesthetic Family: ${fam.name}`);
126
+ lines.push('');
127
+ lines.push(`Key: \`${fam.key}\``);
128
+ lines.push('');
129
+ lines.push(`Reference brands: ${fam.referenceBrands.join(', ')}`);
130
+ lines.push('');
131
+
132
+ // Tokens
133
+ lines.push('## Tokens');
134
+ lines.push('');
135
+ lines.push('### Colors');
136
+ lines.push('');
137
+ // Stable key order -- sorted alphabetically for determinism.
138
+ const colorKeys = Object.keys(fam.colors).sort();
139
+ // @cap-todo(ac:F-063/AC-1) Deterministic DT-NNN assignment: sequential in sorted key order.
140
+ let dtCounter = 1;
141
+ for (const k of colorKeys) {
142
+ const suffix = withIds ? ` (id: ${formatDesignId('DT', dtCounter++)})` : '';
143
+ lines.push(`- ${k}: ${fam.colors[k]}${suffix}`);
144
+ }
145
+ lines.push('');
146
+
147
+ lines.push('### Spacing');
148
+ lines.push('');
149
+ // @cap-decision Spacing/typography scales are single-entry tokens. F-063 v1 assigns IDs only to colors (per-token) and components; scales remain unIDed until a user feature requires them. Revisit if impact-analysis demand surfaces (AC-6).
150
+ lines.push(`- scale: [${fam.spacing.join(', ')}]`);
151
+ lines.push('');
152
+
153
+ lines.push('### Typography');
154
+ lines.push('');
155
+ lines.push(`- family: "${fam.typography.family}"`);
156
+ lines.push(`- familyMono: "${fam.typography.familyMono}"`);
157
+ lines.push(`- scale: [${fam.typography.scale.join(', ')}]`);
158
+ lines.push('');
159
+
160
+ // Components
161
+ lines.push('## Components');
162
+ lines.push('');
163
+ const compKeys = Object.keys(fam.components).sort();
164
+ // @cap-todo(ac:F-063/AC-1) Deterministic DC-NNN assignment: sequential in sorted component name order.
165
+ let dcCounter = 1;
166
+ for (const compName of compKeys) {
167
+ const comp = fam.components[compName];
168
+ const suffix = withIds ? ` (id: ${formatDesignId('DC', dcCounter++)})` : '';
169
+ lines.push(`### ${compName}${suffix}`);
170
+ lines.push('');
171
+ lines.push(`- variants: [${comp.variants.join(', ')}]`);
172
+ lines.push(`- states: [${comp.states.join(', ')}]`);
173
+ lines.push('');
174
+ }
175
+
176
+ // Anti-Patterns
177
+ lines.push('## Anti-Patterns');
178
+ lines.push('');
179
+ lines.push('Hard constraints enforced by cap-designer. Violating entries will be rejected during /cap:design --extend.');
180
+ lines.push('');
181
+ for (const rule of ANTI_SLOP_RULES) {
182
+ lines.push(`- ${rule}`);
183
+ }
184
+ lines.push('');
185
+
186
+ return lines.join('\n');
187
+ }
188
+
189
+ // @cap-api readDesignMd(projectRoot) -- Read DESIGN.md from project root.
190
+ /**
191
+ * @param {string} projectRoot - Absolute path to project root.
192
+ * @returns {string|null} File contents, or null if the file does not exist.
193
+ */
194
+ function readDesignMd(projectRoot) {
195
+ const filePath = path.join(projectRoot, DESIGN_FILE);
196
+ if (!fs.existsSync(filePath)) return null;
197
+ return fs.readFileSync(filePath, 'utf8');
198
+ }
199
+
200
+ // @cap-api writeDesignMd(projectRoot, content) -- Write DESIGN.md to project root.
201
+ // @cap-todo(ac:F-062/AC-4) DESIGN.md lies at project root next to FEATURE-MAP.md and is versioned via git (no gitignore).
202
+ /**
203
+ * @param {string} projectRoot - Absolute path to project root.
204
+ * @param {string} content - Full DESIGN.md content.
205
+ */
206
+ function writeDesignMd(projectRoot, content) {
207
+ const filePath = path.join(projectRoot, DESIGN_FILE);
208
+ fs.writeFileSync(filePath, content, 'utf8');
209
+ }
210
+
211
+ // @cap-api extendDesignMd(existing, additions, options) -- Append-only merge for /cap:design --extend.
212
+ // @cap-todo(ac:F-062/AC-5) Adds new tokens/components to existing DESIGN.md without overwriting existing entries.
213
+ // @cap-todo(ac:F-063/AC-1) When options.withIds is truthy, newly appended entries receive the next free DT-NNN / DC-NNN ID. Existing entries keep whatever IDs they already have (stable-ID guarantee, D4).
214
+ // @cap-decision Line-scan merge instead of markdown parsing -- keeps zero-deps and preserves author edits in unrelated sections.
215
+ // @cap-risk extendDesignMd multi-line injection is snapshot-locked (F-062 review note). F-063 only appends an optional trailing ` (id: DT-NNN)` suffix to NEW bullet / header lines. Existing injection tests still hold — the lines we splice in retain the same base shape.
216
+ /**
217
+ * @param {string} existing - Current DESIGN.md content.
218
+ * @param {{ colors?: Object<string,string>, components?: Object<string, { variants: string[], states: string[] }> }} additions
219
+ * @param {{ withIds?: boolean }} [options]
220
+ * @returns {string} Updated DESIGN.md content. Existing token/component entries are preserved verbatim.
221
+ */
222
+ function extendDesignMd(existing, additions, options) {
223
+ if (typeof existing !== 'string') {
224
+ throw new Error('extendDesignMd requires existing content string');
225
+ }
226
+ const adds = additions || {};
227
+ const opts = options || {};
228
+ const withIds = Boolean(opts.withIds);
229
+ const lines = existing.split('\n');
230
+
231
+ // Pre-scan for existing IDs so new entries get the NEXT free number.
232
+ // Stable-ID guarantee (D4): pre-existing IDs are never rewritten.
233
+ const existingIds = parseDesignIds(existing);
234
+ let nextDt = nextIdNumber(existingIds.tokens);
235
+ let nextDc = nextIdNumber(existingIds.components);
236
+
237
+ // --- Merge colors (append new keys under ### Colors, skip duplicates) ---
238
+ if (adds.colors && Object.keys(adds.colors).length > 0) {
239
+ const colorsIdx = lines.findIndex(l => l.trim() === '### Colors');
240
+ if (colorsIdx !== -1) {
241
+ // Find end of the Colors block (next blank line after the list)
242
+ let insertAt = colorsIdx + 1;
243
+ const existingColorKeys = new Set();
244
+ // Skip the blank line after the header
245
+ while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;
246
+ // Walk the bullet list
247
+ while (insertAt < lines.length && lines[insertAt].startsWith('- ')) {
248
+ const match = lines[insertAt].match(/^-\s+([^:]+):/);
249
+ if (match) existingColorKeys.add(match[1].trim());
250
+ insertAt++;
251
+ }
252
+ // @cap-todo(ac:F-062/AC-5) Only append keys not already present -- preserves existing entries.
253
+ const newLines = [];
254
+ const newColorKeys = Object.keys(adds.colors).sort();
255
+ for (const k of newColorKeys) {
256
+ if (!existingColorKeys.has(k)) {
257
+ const suffix = withIds ? ` (id: ${formatDesignId('DT', nextDt++)})` : '';
258
+ newLines.push(`- ${k}: ${adds.colors[k]}${suffix}`);
259
+ }
260
+ }
261
+ if (newLines.length > 0) {
262
+ lines.splice(insertAt, 0, ...newLines);
263
+ }
264
+ }
265
+ }
266
+
267
+ // --- Merge components (append new component sections under ## Components, skip duplicates) ---
268
+ if (adds.components && Object.keys(adds.components).length > 0) {
269
+ const compHdrIdx = lines.findIndex(l => l.trim() === '## Components');
270
+ if (compHdrIdx !== -1) {
271
+ // Find end of Components section (next "## " header or EOF)
272
+ let sectionEnd = compHdrIdx + 1;
273
+ while (sectionEnd < lines.length && !lines[sectionEnd].startsWith('## ')) {
274
+ sectionEnd++;
275
+ }
276
+ // Collect existing component names (### Foo or ### Foo (id: DC-001))
277
+ const existingCompNames = new Set();
278
+ for (let i = compHdrIdx + 1; i < sectionEnd; i++) {
279
+ const m = lines[i].match(/^###\s+(\S+)/);
280
+ if (m) existingCompNames.add(m[1]);
281
+ }
282
+ // @cap-todo(ac:F-062/AC-5) Only append components not already present.
283
+ const newCompNames = Object.keys(adds.components).sort();
284
+ const insertion = [];
285
+ for (const name of newCompNames) {
286
+ if (existingCompNames.has(name)) continue;
287
+ const comp = adds.components[name];
288
+ const suffix = withIds ? ` (id: ${formatDesignId('DC', nextDc++)})` : '';
289
+ insertion.push(`### ${name}${suffix}`);
290
+ insertion.push('');
291
+ insertion.push(`- variants: [${(comp.variants || []).join(', ')}]`);
292
+ insertion.push(`- states: [${(comp.states || []).join(', ')}]`);
293
+ insertion.push('');
294
+ }
295
+ if (insertion.length > 0) {
296
+ lines.splice(sectionEnd, 0, ...insertion);
297
+ }
298
+ }
299
+ }
300
+
301
+ return lines.join('\n');
302
+ }
303
+
304
+ // @cap-feature(feature:F-063) Design-ID helpers — parser + assigner + impact-analysis.
305
+
306
+ // @cap-decision(F-063/D1) Inline ID regex:
307
+ // - Token: "- primary: #HEX (id: DT-001)" — captured on bullet lines inside ### Colors
308
+ // - Component: "### Button (id: DC-001)" — captured on component headers inside ## Components
309
+ // @cap-risk The regex is intentionally permissive about surrounding whitespace/punctuation
310
+ // but REQUIRES the `(id: XX-NNN)` parenthesized form. Markdown-linters that collapse
311
+ // trailing whitespace will not break the suffix.
312
+ const DESIGN_TOKEN_ID_RE = /\(id:\s*(DT-\d{3,})\)/;
313
+ const DESIGN_COMPONENT_ID_RE = /\(id:\s*(DC-\d{3,})\)/;
314
+
315
+ // @cap-api formatDesignId(prefix, n) -- zero-padded 3-digit ID formatter.
316
+ /**
317
+ * @param {'DT'|'DC'} prefix
318
+ * @param {number} n - 1-based counter
319
+ * @returns {string}
320
+ */
321
+ function formatDesignId(prefix, n) {
322
+ return `${prefix}-${String(n).padStart(3, '0')}`;
323
+ }
324
+
325
+ // @cap-api nextIdNumber(existingIds) -- Return the next sequential number not already used.
326
+ // Gaps are tolerated — we always return max+1 to satisfy the stable-ID guarantee (D4).
327
+ /**
328
+ * @param {string[]} existingIds - e.g. ['DT-001', 'DT-003']
329
+ * @returns {number} - next number to use (e.g. 4)
330
+ */
331
+ function nextIdNumber(existingIds) {
332
+ let max = 0;
333
+ for (const id of existingIds || []) {
334
+ const m = String(id).match(/-(\d+)$/);
335
+ if (!m) continue;
336
+ const n = parseInt(m[1], 10);
337
+ if (Number.isFinite(n) && n > max) max = n;
338
+ }
339
+ return max + 1;
340
+ }
341
+
342
+ // @cap-api getNextDesignId(type, existing) -- Convenience wrapper that returns a formatted ID.
343
+ // @cap-todo(ac:F-063/AC-1) Sequential DT-NNN / DC-NNN assignment driven by max existing + 1.
344
+ /**
345
+ * @param {'token'|'component'} type
346
+ * @param {string[]} existing - existing IDs of the same type
347
+ * @returns {string} - next formatted ID, e.g. "DT-004"
348
+ */
349
+ function getNextDesignId(type, existing) {
350
+ if (type !== 'token' && type !== 'component') {
351
+ throw new Error(`getNextDesignId: type must be 'token' or 'component', got ${type}`);
352
+ }
353
+ const prefix = type === 'token' ? 'DT' : 'DC';
354
+ return formatDesignId(prefix, nextIdNumber(existing));
355
+ }
356
+
357
+ // @cap-api parseDesignIds(content) -- Extract all DT-NNN / DC-NNN IDs from DESIGN.md content.
358
+ // @cap-todo(ac:F-063/AC-1) Parser recognises inline `(id: DT-NNN)` and `(id: DC-NNN)` suffixes so callers can see which entries are already stable-ID-tagged.
359
+ /**
360
+ * @param {string} content - DESIGN.md content
361
+ * @returns {{
362
+ * tokens: string[], // all DT-NNN IDs in file order
363
+ * components: string[], // all DC-NNN IDs in file order
364
+ * byToken: Object<string,{id:string,key:string,value:string,line:number}>,
365
+ * byComponent: Object<string,{id:string,name:string,line:number}>,
366
+ * }}
367
+ */
368
+ function parseDesignIds(content) {
369
+ const result = {
370
+ tokens: [],
371
+ components: [],
372
+ byToken: {},
373
+ byComponent: {},
374
+ };
375
+ if (typeof content !== 'string' || content.length === 0) return result;
376
+
377
+ const lines = content.split('\n');
378
+ // @cap-decision Section tracking mirrors F-062's extendDesignMd approach — scan by ## / ### boundaries.
379
+ // We do not hard-code a section-name allowlist; any bullet with `(id: DT-NNN)` counts as a token entry,
380
+ // any `### Foo (id: DC-NNN)` header counts as a component. This tolerates user-added sections (e.g. typography
381
+ // tokens in the future) without breaking.
382
+ for (let i = 0; i < lines.length; i++) {
383
+ const line = lines[i];
384
+
385
+ // Component header
386
+ const compHeaderMatch = line.match(/^###\s+(\S[^(]*?)\s*\(id:\s*(DC-\d{3,})\)/);
387
+ if (compHeaderMatch) {
388
+ const name = compHeaderMatch[1].trim();
389
+ const id = compHeaderMatch[2];
390
+ result.components.push(id);
391
+ result.byComponent[id] = { id, name, line: i + 1 };
392
+ continue;
393
+ }
394
+
395
+ // Bullet token
396
+ const bulletMatch = line.match(/^-\s+([^:]+):\s*(.+?)\s*\(id:\s*(DT-\d{3,})\)\s*$/);
397
+ if (bulletMatch) {
398
+ const key = bulletMatch[1].trim();
399
+ const value = bulletMatch[2].trim();
400
+ const id = bulletMatch[3];
401
+ result.tokens.push(id);
402
+ result.byToken[id] = { id, key, value, line: i + 1 };
403
+ }
404
+ }
405
+
406
+ return result;
407
+ }
408
+
409
+ // @cap-api assignDesignIds(content) -- Walk DESIGN.md content and add DT/DC IDs to entries that lack them.
410
+ // @cap-todo(ac:F-063/AC-1) Retrofits IDs onto an F-062-era DESIGN.md. Existing IDs are preserved verbatim (stable-ID guarantee D4).
411
+ // @cap-decision Only color bullets (under `### Colors`) and component headers (under `## Components`) are IDed in v1.
412
+ // Spacing/typography scales stay un-IDed until a user feature explicitly needs to reference them.
413
+ /**
414
+ * @param {string} content - DESIGN.md content
415
+ * @returns {{ content: string, assigned: { tokens: Array<{key:string,id:string}>, components: Array<{name:string,id:string}> } }}
416
+ */
417
+ function assignDesignIds(content) {
418
+ const out = { content, assigned: { tokens: [], components: [] } };
419
+ if (typeof content !== 'string' || content.length === 0) return out;
420
+
421
+ const existing = parseDesignIds(content);
422
+ let nextDt = nextIdNumber(existing.tokens);
423
+ let nextDc = nextIdNumber(existing.components);
424
+
425
+ const lines = content.split('\n');
426
+ let inColors = false;
427
+ let inComponents = false;
428
+
429
+ for (let i = 0; i < lines.length; i++) {
430
+ const line = lines[i];
431
+ const trimmed = line.trim();
432
+
433
+ // Section tracking
434
+ if (trimmed.startsWith('### ')) {
435
+ // entering a subsection — decide context from the header text
436
+ if (trimmed === '### Colors') { inColors = true; continue; }
437
+ if (inComponents) {
438
+ // Component header — tag with DC if it lacks one
439
+ if (!DESIGN_COMPONENT_ID_RE.test(line)) {
440
+ const hdrMatch = line.match(/^###\s+(.+?)\s*$/);
441
+ if (hdrMatch) {
442
+ const name = hdrMatch[1].trim();
443
+ const id = formatDesignId('DC', nextDc++);
444
+ lines[i] = `### ${name} (id: ${id})`;
445
+ out.assigned.components.push({ name, id });
446
+ }
447
+ }
448
+ continue;
449
+ }
450
+ // other ### subsection (Spacing, Typography) — leave alone but close Colors
451
+ inColors = false;
452
+ continue;
453
+ }
454
+ if (trimmed.startsWith('## ')) {
455
+ inColors = false;
456
+ inComponents = (trimmed === '## Components');
457
+ continue;
458
+ }
459
+
460
+ if (inColors && line.startsWith('- ')) {
461
+ if (!DESIGN_TOKEN_ID_RE.test(line)) {
462
+ // Add ID suffix — preserve existing key/value text byte-for-byte up to the trailing whitespace
463
+ const bulletMatch = line.match(/^(-\s+([^:]+):\s*.+?)\s*$/);
464
+ if (bulletMatch) {
465
+ const base = bulletMatch[1];
466
+ const key = bulletMatch[2].trim();
467
+ const id = formatDesignId('DT', nextDt++);
468
+ lines[i] = `${base} (id: ${id})`;
469
+ out.assigned.tokens.push({ key, id });
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ out.content = lines.join('\n');
476
+ return out;
477
+ }
478
+
479
+ // @cap-api findFeaturesUsingDesignId(featureMap, designId) -- Impact-analysis for AC-6.
480
+ // @cap-todo(ac:F-063/AC-6) Given a DT-NNN or DC-NNN, return features whose `usesDesign` includes it.
481
+ /**
482
+ * @param {{features: Array<{id:string,title?:string,usesDesign?:string[]}>}} featureMap
483
+ * @param {string} designId - e.g. "DT-001"
484
+ * @returns {Array<{id:string,title:string|null}>} - features that reference this design ID
485
+ */
486
+ function findFeaturesUsingDesignId(featureMap, designId) {
487
+ const out = [];
488
+ if (!featureMap || !Array.isArray(featureMap.features) || !designId) return out;
489
+ for (const f of featureMap.features) {
490
+ const uses = Array.isArray(f.usesDesign) ? f.usesDesign : [];
491
+ if (uses.includes(designId)) {
492
+ out.push({ id: f.id, title: f.title || null });
493
+ }
494
+ }
495
+ return out;
496
+ }
497
+
498
+ // @cap-feature(feature:F-064) Anti-Slop Review Engine — pure review function over DESIGN.md content.
499
+ // @cap-constraint reviewDesign is synchronous, pure (string-in / array-out). No I/O, no fs access, no mutation of inputs.
500
+
501
+ const DESIGN_REVIEW_FILE = '.cap/DESIGN-REVIEW.md';
502
+
503
+ // @cap-decision(F-064/D3) Severity levels frozen here so callers cannot introduce silent new levels.
504
+ const REVIEW_SEVERITIES = Object.freeze(['error', 'warning', 'info']);
505
+
506
+ // @cap-todo(ac:F-064/AC-2) Structured rule schema: name, severity, kind, check(content, ctx) -> violations[].
507
+ // @cap-decision(F-064/D1) Default rules are pinned here. Built-in checks cover typography, color, layout, structure.
508
+ // Each rule returns an array of { id, kind, rule, location, suggestion, severity }.
509
+ // Rules are PURE functions of (content, parsedContext). No network, no fs, no randomness.
510
+ const DEFAULT_DESIGN_RULES = Object.freeze([
511
+ // Typography
512
+ Object.freeze({
513
+ name: 'typography/no-generic-fonts',
514
+ severity: 'error',
515
+ kind: 'typography',
516
+ description: 'Reject generic fonts (Inter, Roboto, Arial, Helvetica, sans-serif) as primary typefaces.',
517
+ suggestion: 'Use an opinionated typeface aligned with the aesthetic family (see Tokens).',
518
+ }),
519
+ // Color
520
+ Object.freeze({
521
+ name: 'color/no-cliche-gradients',
522
+ severity: 'error',
523
+ kind: 'color',
524
+ description: 'Reject cliche purple-blue SaaS-template gradients (e.g. #667eea -> #764ba2).',
525
+ suggestion: 'Derive gradients from the primary + accent tokens; purple-blue duos are SaaS-template slop.',
526
+ }),
527
+ // Layout
528
+ Object.freeze({
529
+ name: 'layout/no-cookie-cutter',
530
+ severity: 'warning',
531
+ kind: 'layout',
532
+ description: 'Reject "centered hero + 3-column feature cards + CTA" template-grammar mentions.',
533
+ suggestion: 'Break the template grammar — vary hero alignment or section grammar.',
534
+ }),
535
+ // Structure (F-063-aware)
536
+ Object.freeze({
537
+ name: 'structure/inconsistent-token-ids',
538
+ severity: 'warning',
539
+ kind: 'structure',
540
+ description: 'Tokens without DT-NNN IDs when the DESIGN.md has some IDs already (inconsistent coverage).',
541
+ suggestion: 'Run /cap:design --review or re-run /cap:design to retrofit DT-NNN IDs.',
542
+ }),
543
+ Object.freeze({
544
+ name: 'structure/inconsistent-component-ids',
545
+ severity: 'warning',
546
+ kind: 'structure',
547
+ description: 'Components without DC-NNN IDs when the DESIGN.md has some IDs already (inconsistent coverage).',
548
+ suggestion: 'Retrofit DC-NNN IDs via assignDesignIds.',
549
+ }),
550
+ Object.freeze({
551
+ name: 'structure/duplicate-ids',
552
+ severity: 'error',
553
+ kind: 'structure',
554
+ description: 'Duplicate DT-NNN or DC-NNN ID detected — violates stable-ID guarantee (F-063/D4).',
555
+ suggestion: 'Rename the duplicate to the next free ID; investigate which entry is the original.',
556
+ }),
557
+ ]);
558
+
559
+ // @cap-decision(F-064/D1) Generic font list is a frozen set — matched case-insensitively against the typography family value.
560
+ // "SF Pro" is excluded here (allowed for glass-soft-futurism family per F-062) — cf. ANTI_SLOP_RULES wording.
561
+ const GENERIC_FONT_PATTERNS = Object.freeze([
562
+ /(^|[,\s"'])inter([,\s"']|$)/i,
563
+ /(^|[,\s"'])roboto([,\s"']|$)/i,
564
+ /(^|[,\s"'])arial([,\s"']|$)/i,
565
+ /(^|[,\s"'])helvetica([,\s"']|$)/i,
566
+ /(^|[,\s"'])sans-serif([,\s"']|$)/i,
567
+ ]);
568
+
569
+ // @cap-decision(F-064/D1) Purple-blue cliche gradient regex — matches the canonical #667eea/#764ba2 duo AND common near-variants.
570
+ // We do not attempt full color-space analysis in v1 — literal string matching is adequate for template-slop detection.
571
+ const CLICHE_GRADIENT_PATTERNS = Object.freeze([
572
+ /#667eea/i,
573
+ /#764ba2/i,
574
+ /linear-gradient\s*\(\s*[^)]*#66[67][a-f0-9]{3}[^)]*#76[4-5][a-f0-9]{3}/i,
575
+ ]);
576
+
577
+ // @cap-decision(F-064/D1) Cookie-cutter layout markers — substring matches against DESIGN.md content.
578
+ const COOKIE_CUTTER_PHRASES = Object.freeze([
579
+ 'centered hero + 3-column feature cards',
580
+ 'hero + 3-column feature cards + cta',
581
+ '3-column feature cards',
582
+ ]);
583
+
584
+ // @cap-api parseDesignRules(ruleMarkdown) -- Parse optional `.cap/design-rules.md`; return default rules if input is empty/null.
585
+ // @cap-todo(ac:F-064/AC-4) Review-Regelbasis ist konfigurierbar via `.cap/design-rules.md` — markdown bullets under `## Rules`.
586
+ // @cap-decision(F-064/D1) Custom-rule format is MARKDOWN bullets, not YAML, matching CAP's `.md` convention.
587
+ // Each bullet: `- **[kind] rule-name**: description. Suggestion: ...` with optional `[severity: error|warning|info]` prefix.
588
+ // Parser is intentionally forgiving — malformed bullets are SKIPPED (not errored).
589
+ /**
590
+ * @param {string|null|undefined} ruleMarkdown - Content of `.cap/design-rules.md` or null.
591
+ * @returns {Array<{name:string, severity:string, kind:string, description:string, suggestion:string}>}
592
+ * Frozen default ruleset if input is empty; otherwise the parsed ruleset.
593
+ */
594
+ function parseDesignRules(ruleMarkdown) {
595
+ if (typeof ruleMarkdown !== 'string' || ruleMarkdown.trim().length === 0) {
596
+ return DEFAULT_DESIGN_RULES;
597
+ }
598
+
599
+ const lines = ruleMarkdown.split('\n');
600
+ let inRulesSection = false;
601
+ const out = [];
602
+ let currentRule = null;
603
+
604
+ for (let i = 0; i < lines.length; i++) {
605
+ const line = lines[i];
606
+ const trimmed = line.trim();
607
+
608
+ if (/^##\s+Rules\s*$/i.test(trimmed)) {
609
+ inRulesSection = true;
610
+ continue;
611
+ }
612
+ if (inRulesSection && /^##\s+\S/.test(trimmed)) {
613
+ // Entering a different H2 closes the Rules section.
614
+ inRulesSection = false;
615
+ if (currentRule) { out.push(Object.freeze(currentRule)); currentRule = null; }
616
+ continue;
617
+ }
618
+
619
+ if (!inRulesSection) continue;
620
+
621
+ // Bullet start: `- **[kind] rule-name**: description`
622
+ // or `- **[kind][severity:error] rule-name**: description`
623
+ const bulletMatch = line.match(/^\s*-\s+\*\*\[([^\]]+)\](?:\[severity:\s*(error|warning|info)\])?\s+([^*]+?)\*\*\s*:\s*(.*)$/i);
624
+ if (bulletMatch) {
625
+ if (currentRule) out.push(Object.freeze(currentRule));
626
+ const kind = bulletMatch[1].trim();
627
+ const severity = (bulletMatch[2] || 'warning').trim().toLowerCase();
628
+ const name = `${kind}/${bulletMatch[3].trim()}`;
629
+ const description = bulletMatch[4].trim();
630
+ currentRule = { name, severity, kind, description, suggestion: '' };
631
+ continue;
632
+ }
633
+
634
+ // Continuation: " Suggestion: ..."
635
+ const sugMatch = line.match(/^\s+Suggestion:\s*(.+)$/i);
636
+ if (sugMatch && currentRule) {
637
+ currentRule.suggestion = sugMatch[1].trim();
638
+ continue;
639
+ }
640
+ }
641
+
642
+ if (currentRule) out.push(Object.freeze(currentRule));
643
+
644
+ // @cap-decision If user's file has NO valid rules, fall back to defaults (loud-quiet middle ground).
645
+ // A user with malformed rules should still get the baseline Anti-Slop coverage.
646
+ if (out.length === 0) return DEFAULT_DESIGN_RULES;
647
+
648
+ return Object.freeze(out);
649
+ }
650
+
651
+ // @cap-api reviewDesign(designMdContent, rules) -- Pure review function. Returns sorted, deterministic violations array.
652
+ // @cap-todo(ac:F-064/AC-1) Review engine applies rules to DESIGN.md content, returns structured violations.
653
+ // @cap-todo(ac:F-064/AC-2) Violation schema: { id, kind, rule, location, suggestion, severity }.
654
+ // @cap-todo(ac:F-064/AC-3) PURE function — no writes. Takes string input, returns array. Never touches fs.
655
+ // @cap-todo(ac:F-064/AC-5) Deterministic: sort order is (location.id || '__global__', rule-name, location.line || 0).
656
+ /**
657
+ * @param {string} designMdContent - DESIGN.md content to review.
658
+ * @param {Array<object>} [rules] - Ruleset (defaults to DEFAULT_DESIGN_RULES).
659
+ * @returns {Array<{id:string|null, kind:string, rule:string, location:{line:number|null,id:string|null,section:string|null}, suggestion:string, severity:string}>}
660
+ */
661
+ function reviewDesign(designMdContent, rules) {
662
+ if (typeof designMdContent !== 'string') {
663
+ throw new Error('reviewDesign requires a string (DESIGN.md content)');
664
+ }
665
+ const activeRules = Array.isArray(rules) && rules.length > 0 ? rules : DEFAULT_DESIGN_RULES;
666
+ const ruleIndex = new Map();
667
+ for (const r of activeRules) ruleIndex.set(r.name, r);
668
+
669
+ const violations = [];
670
+ const lines = designMdContent.split('\n');
671
+ const ids = parseDesignIds(designMdContent);
672
+
673
+ // --- Rule: typography/no-generic-fonts ---
674
+ // @cap-todo(ac:F-064/AC-1) Check typography.family bullet value against the generic-font denylist.
675
+ const typoRule = ruleIndex.get('typography/no-generic-fonts');
676
+ if (typoRule) {
677
+ for (let i = 0; i < lines.length; i++) {
678
+ const line = lines[i];
679
+ const m = line.match(/^-\s+family(?:Mono)?:\s*"([^"]+)"/);
680
+ if (!m) continue;
681
+ const value = m[1];
682
+ for (const pat of GENERIC_FONT_PATTERNS) {
683
+ if (pat.test(value)) {
684
+ violations.push({
685
+ id: null,
686
+ kind: typoRule.kind,
687
+ rule: typoRule.name,
688
+ location: { line: i + 1, id: null, section: 'Typography' },
689
+ suggestion: typoRule.suggestion,
690
+ severity: typoRule.severity,
691
+ });
692
+ break; // one violation per line is enough
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ // --- Rule: color/no-cliche-gradients ---
699
+ // @cap-todo(ac:F-064/AC-1) Match cliche purple-blue gradients anywhere in the document.
700
+ const gradRule = ruleIndex.get('color/no-cliche-gradients');
701
+ if (gradRule) {
702
+ for (let i = 0; i < lines.length; i++) {
703
+ const line = lines[i];
704
+ for (const pat of CLICHE_GRADIENT_PATTERNS) {
705
+ if (pat.test(line)) {
706
+ // Attempt to attach a DT-NNN id if the bullet has one inline.
707
+ const idMatch = line.match(DESIGN_TOKEN_ID_RE);
708
+ violations.push({
709
+ id: idMatch ? idMatch[1] : null,
710
+ kind: gradRule.kind,
711
+ rule: gradRule.name,
712
+ location: { line: i + 1, id: idMatch ? idMatch[1] : null, section: null },
713
+ suggestion: gradRule.suggestion,
714
+ severity: gradRule.severity,
715
+ });
716
+ break;
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ // --- Rule: layout/no-cookie-cutter ---
723
+ // @cap-todo(ac:F-064/AC-1) Match cookie-cutter layout phrases (case-insensitive substring).
724
+ const layoutRule = ruleIndex.get('layout/no-cookie-cutter');
725
+ if (layoutRule) {
726
+ const lcContent = designMdContent.toLowerCase();
727
+ for (const phrase of COOKIE_CUTTER_PHRASES) {
728
+ const idx = lcContent.indexOf(phrase);
729
+ if (idx === -1) continue;
730
+ // Find the line number for the first hit only — deterministic, one violation per phrase.
731
+ let cum = 0;
732
+ let lineNum = 1;
733
+ for (let i = 0; i < lines.length; i++) {
734
+ if (cum + lines[i].length >= idx) { lineNum = i + 1; break; }
735
+ cum += lines[i].length + 1; // +1 for the \n
736
+ }
737
+ violations.push({
738
+ id: null,
739
+ kind: layoutRule.kind,
740
+ rule: layoutRule.name,
741
+ location: { line: lineNum, id: null, section: null },
742
+ suggestion: layoutRule.suggestion,
743
+ severity: layoutRule.severity,
744
+ });
745
+ }
746
+ }
747
+
748
+ // --- Rule: structure/inconsistent-token-ids ---
749
+ // @cap-todo(ac:F-064/AC-1) Flag token bullets without DT-NNN when any DT-NNN exists in the file.
750
+ const tokenStructRule = ruleIndex.get('structure/inconsistent-token-ids');
751
+ if (tokenStructRule && ids.tokens.length > 0) {
752
+ let inColors = false;
753
+ for (let i = 0; i < lines.length; i++) {
754
+ const trimmed = lines[i].trim();
755
+ if (trimmed === '### Colors') { inColors = true; continue; }
756
+ if (trimmed.startsWith('## ') || (trimmed.startsWith('### ') && trimmed !== '### Colors')) {
757
+ inColors = false;
758
+ continue;
759
+ }
760
+ if (inColors && lines[i].startsWith('- ') && !DESIGN_TOKEN_ID_RE.test(lines[i])) {
761
+ const keyMatch = lines[i].match(/^-\s+([^:]+):/);
762
+ const key = keyMatch ? keyMatch[1].trim() : null;
763
+ violations.push({
764
+ id: null,
765
+ kind: tokenStructRule.kind,
766
+ rule: tokenStructRule.name,
767
+ location: { line: i + 1, id: null, section: 'Colors' },
768
+ suggestion: tokenStructRule.suggestion + (key ? ` (missing on: ${key})` : ''),
769
+ severity: tokenStructRule.severity,
770
+ });
771
+ }
772
+ }
773
+ }
774
+
775
+ // --- Rule: structure/inconsistent-component-ids ---
776
+ // @cap-todo(ac:F-064/AC-1) Flag component headers without DC-NNN when any DC-NNN exists.
777
+ const compStructRule = ruleIndex.get('structure/inconsistent-component-ids');
778
+ if (compStructRule && ids.components.length > 0) {
779
+ let inComponents = false;
780
+ for (let i = 0; i < lines.length; i++) {
781
+ const trimmed = lines[i].trim();
782
+ if (trimmed === '## Components') { inComponents = true; continue; }
783
+ if (trimmed.startsWith('## ') && trimmed !== '## Components') { inComponents = false; continue; }
784
+ if (inComponents && trimmed.startsWith('### ') && !DESIGN_COMPONENT_ID_RE.test(lines[i])) {
785
+ const nameMatch = lines[i].match(/^###\s+(.+?)\s*$/);
786
+ const name = nameMatch ? nameMatch[1].trim() : null;
787
+ violations.push({
788
+ id: null,
789
+ kind: compStructRule.kind,
790
+ rule: compStructRule.name,
791
+ location: { line: i + 1, id: null, section: 'Components' },
792
+ suggestion: compStructRule.suggestion + (name ? ` (missing on: ${name})` : ''),
793
+ severity: compStructRule.severity,
794
+ });
795
+ }
796
+ }
797
+ }
798
+
799
+ // --- Rule: structure/duplicate-ids ---
800
+ // @cap-todo(ac:F-064/AC-1) Defensive check — duplicate IDs should never exist but we scan anyway.
801
+ const dupRule = ruleIndex.get('structure/duplicate-ids');
802
+ if (dupRule) {
803
+ const seenDt = new Map();
804
+ const seenDc = new Map();
805
+ for (const id of ids.tokens) seenDt.set(id, (seenDt.get(id) || 0) + 1);
806
+ for (const id of ids.components) seenDc.set(id, (seenDc.get(id) || 0) + 1);
807
+ for (const [id, count] of seenDt.entries()) {
808
+ if (count > 1) {
809
+ violations.push({
810
+ id,
811
+ kind: dupRule.kind,
812
+ rule: dupRule.name,
813
+ location: { line: ids.byToken[id] ? ids.byToken[id].line : null, id, section: 'Colors' },
814
+ suggestion: dupRule.suggestion,
815
+ severity: dupRule.severity,
816
+ });
817
+ }
818
+ }
819
+ for (const [id, count] of seenDc.entries()) {
820
+ if (count > 1) {
821
+ violations.push({
822
+ id,
823
+ kind: dupRule.kind,
824
+ rule: dupRule.name,
825
+ location: { line: ids.byComponent[id] ? ids.byComponent[id].line : null, id, section: 'Components' },
826
+ suggestion: dupRule.suggestion,
827
+ severity: dupRule.severity,
828
+ });
829
+ }
830
+ }
831
+ }
832
+
833
+ // @cap-todo(ac:F-064/AC-5) Deterministic sort: (location.id || '__global__', rule-name, location.line || 0).
834
+ // @cap-decision(F-064/D4) '__global__' sentinel sorts after real IDs alphabetically because 'DT-' < 'DC-' < '__global__' in ASCII.
835
+ // That is acceptable — the important invariant is CONSISTENT ordering, not a specific bucket order.
836
+ violations.sort((a, b) => {
837
+ const aId = a.location && a.location.id ? a.location.id : '__global__';
838
+ const bId = b.location && b.location.id ? b.location.id : '__global__';
839
+ if (aId !== bId) return aId < bId ? -1 : 1;
840
+ if (a.rule !== b.rule) return a.rule < b.rule ? -1 : 1;
841
+ const aLine = (a.location && typeof a.location.line === 'number') ? a.location.line : 0;
842
+ const bLine = (b.location && typeof b.location.line === 'number') ? b.location.line : 0;
843
+ return aLine - bLine;
844
+ });
845
+
846
+ return violations;
847
+ }
848
+
849
+ // @cap-api formatReviewReport(violations) -- Render a deterministic markdown report.
850
+ // @cap-todo(ac:F-064/AC-2) Renders violations as structured markdown (IDs, rules, locations, suggestions).
851
+ // @cap-todo(ac:F-064/AC-5) No timestamps. Byte-identical output for byte-identical input.
852
+ /**
853
+ * @param {Array<object>} violations - Output of reviewDesign.
854
+ * @returns {string} Markdown report body.
855
+ */
856
+ function formatReviewReport(violations) {
857
+ const lines = [];
858
+ lines.push('# DESIGN.md Review');
859
+ lines.push('');
860
+ lines.push('> Anti-Slop-Check report. Read-only artifact produced by /cap:design --review.');
861
+ lines.push('> Violations listed below. DESIGN.md itself is NEVER modified by review.');
862
+ lines.push('');
863
+
864
+ if (!Array.isArray(violations) || violations.length === 0) {
865
+ lines.push('## Summary');
866
+ lines.push('');
867
+ lines.push('No violations found. DESIGN.md passes all configured rules.');
868
+ lines.push('');
869
+ return lines.join('\n');
870
+ }
871
+
872
+ // --- Summary counts by severity ---
873
+ const counts = { error: 0, warning: 0, info: 0 };
874
+ for (const v of violations) {
875
+ const sev = v.severity && counts[v.severity] !== undefined ? v.severity : 'warning';
876
+ counts[sev]++;
877
+ }
878
+
879
+ lines.push('## Summary');
880
+ lines.push('');
881
+ lines.push(`- Total violations: ${violations.length}`);
882
+ lines.push(`- Errors: ${counts.error}`);
883
+ lines.push(`- Warnings: ${counts.warning}`);
884
+ lines.push(`- Info: ${counts.info}`);
885
+ lines.push('');
886
+
887
+ lines.push('## Violations');
888
+ lines.push('');
889
+
890
+ for (const v of violations) {
891
+ const id = v.id || (v.location && v.location.id) || '(global)';
892
+ const rule = v.rule || '(unnamed)';
893
+ const kind = v.kind || 'unknown';
894
+ const severity = v.severity || 'warning';
895
+ const line = v.location && typeof v.location.line === 'number' ? `line ${v.location.line}` : 'n/a';
896
+ const section = v.location && v.location.section ? v.location.section : null;
897
+
898
+ lines.push(`### ${id} — ${rule} [${severity}]`);
899
+ lines.push('');
900
+ lines.push(`- kind: ${kind}`);
901
+ lines.push(`- location: ${line}${section ? ` (${section})` : ''}`);
902
+ if (v.suggestion) lines.push(`- suggestion: ${v.suggestion}`);
903
+ lines.push('');
904
+ }
905
+
906
+ return lines.join('\n');
907
+ }
908
+
909
+ // @cap-api readDesignRules(projectRoot) -- Read `.cap/design-rules.md` if present.
910
+ // @cap-todo(ac:F-064/AC-4) Returns null when no file (parseDesignRules treats null as default-rules trigger).
911
+ /**
912
+ * @param {string} projectRoot - Absolute path to project root.
913
+ * @returns {string|null}
914
+ */
915
+ function readDesignRules(projectRoot) {
916
+ const filePath = path.join(projectRoot, '.cap', 'design-rules.md');
917
+ if (!fs.existsSync(filePath)) return null;
918
+ return fs.readFileSync(filePath, 'utf8');
919
+ }
920
+
921
+ // @cap-api writeDesignReview(projectRoot, content) -- Write review report to `.cap/DESIGN-REVIEW.md`.
922
+ // @cap-decision(F-064/D2) Report artifact lives under `.cap/` runtime dir, matching `.cap/REVIEW.md` pattern.
923
+ // DESIGN.md is NEVER touched by review — AC-3 hard read-only constraint.
924
+ /**
925
+ * @param {string} projectRoot - Absolute path to project root.
926
+ * @param {string} content - Report content.
927
+ */
928
+ function writeDesignReview(projectRoot, content) {
929
+ const capDir = path.join(projectRoot, '.cap');
930
+ if (!fs.existsSync(capDir)) fs.mkdirSync(capDir, { recursive: true });
931
+ const filePath = path.join(projectRoot, DESIGN_REVIEW_FILE);
932
+ fs.writeFileSync(filePath, content, 'utf8');
933
+ }
934
+
935
+ module.exports = {
936
+ DESIGN_FILE,
937
+ AESTHETIC_FAMILIES,
938
+ ANTI_SLOP_RULES,
939
+ FAMILY_MAP,
940
+ VALID_READ_HEAVY,
941
+ VALID_USER_TYPES,
942
+ VALID_COURAGE,
943
+ mapAnswersToFamily,
944
+ buildDesignMd,
945
+ readDesignMd,
946
+ writeDesignMd,
947
+ extendDesignMd,
948
+ // F-063
949
+ DESIGN_TOKEN_ID_RE,
950
+ DESIGN_COMPONENT_ID_RE,
951
+ formatDesignId,
952
+ nextIdNumber,
953
+ getNextDesignId,
954
+ parseDesignIds,
955
+ assignDesignIds,
956
+ findFeaturesUsingDesignId,
957
+ // F-064
958
+ DESIGN_REVIEW_FILE,
959
+ REVIEW_SEVERITIES,
960
+ DEFAULT_DESIGN_RULES,
961
+ parseDesignRules,
962
+ reviewDesign,
963
+ formatReviewReport,
964
+ readDesignRules,
965
+ writeDesignReview,
966
+ };