baldart 3.6.2

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 (230) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/README.md +566 -0
  3. package/VERSION +1 -0
  4. package/bin/baldart.js +143 -0
  5. package/framework/.claude/agents/REGISTRY.md +169 -0
  6. package/framework/.claude/agents/api-perf-cost-auditor.md +291 -0
  7. package/framework/.claude/agents/code-reviewer.md +350 -0
  8. package/framework/.claude/agents/codebase-architect.md +391 -0
  9. package/framework/.claude/agents/coder.md +291 -0
  10. package/framework/.claude/agents/deep-human-insight.md +198 -0
  11. package/framework/.claude/agents/doc-reviewer.md +440 -0
  12. package/framework/.claude/agents/email-deliverability-architect.md +193 -0
  13. package/framework/.claude/agents/hybrid-ml-architect.md +285 -0
  14. package/framework/.claude/agents/hyper-gamification-designer.md +149 -0
  15. package/framework/.claude/agents/legal-counsel-gdpr.md +179 -0
  16. package/framework/.claude/agents/marketing-conversion-strategist.md +162 -0
  17. package/framework/.claude/agents/motion-expert.md +108 -0
  18. package/framework/.claude/agents/onboarding-architect-lead.md +230 -0
  19. package/framework/.claude/agents/plan-auditor.md +546 -0
  20. package/framework/.claude/agents/prd-card-writer.md +372 -0
  21. package/framework/.claude/agents/prd.md +744 -0
  22. package/framework/.claude/agents/qa-sentinel.md +305 -0
  23. package/framework/.claude/agents/remotion-animator-orchestrator.md +218 -0
  24. package/framework/.claude/agents/security-reviewer.md +276 -0
  25. package/framework/.claude/agents/senior-researcher.md +175 -0
  26. package/framework/.claude/agents/seo-analytics-strategist.md +156 -0
  27. package/framework/.claude/agents/skill-improver.md +61 -0
  28. package/framework/.claude/agents/ui-expert.md +191 -0
  29. package/framework/.claude/agents/visual-designer.md +190 -0
  30. package/framework/.claude/agents/website-orchestrator.md +118 -0
  31. package/framework/.claude/agents/wiki-curator.md +145 -0
  32. package/framework/.claude/commands/baldart-push.md +15 -0
  33. package/framework/.claude/commands/check.md +237 -0
  34. package/framework/.claude/commands/codexreview.md +203 -0
  35. package/framework/.claude/commands/design-review.md +11 -0
  36. package/framework/.claude/commands/issue-review.md +34 -0
  37. package/framework/.claude/commands/new.md +331 -0
  38. package/framework/.claude/commands/qa.md +257 -0
  39. package/framework/.claude/hooks/framework-edit-gate.js +208 -0
  40. package/framework/.claude/hooks/lint-before-commit.sh.template +66 -0
  41. package/framework/.claude/settings.local.json.example +32 -0
  42. package/framework/.claude/skills/api-design-principles/SKILL.md +567 -0
  43. package/framework/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
  44. package/framework/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
  45. package/framework/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
  46. package/framework/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
  47. package/framework/.claude/skills/baldart-push/SKILL.md +222 -0
  48. package/framework/.claude/skills/bug/SKILL.md +200 -0
  49. package/framework/.claude/skills/bug/references/logging-patterns.md +174 -0
  50. package/framework/.claude/skills/capture/SKILL.md +125 -0
  51. package/framework/.claude/skills/capture/references/synthesis-template.md +42 -0
  52. package/framework/.claude/skills/context-primer/SKILL.md +189 -0
  53. package/framework/.claude/skills/copywriting/SKILL.md +273 -0
  54. package/framework/.claude/skills/copywriting/references/copy-frameworks.md +338 -0
  55. package/framework/.claude/skills/copywriting/references/natural-transitions.md +252 -0
  56. package/framework/.claude/skills/doc-writing-for-rag/SKILL.md +119 -0
  57. package/framework/.claude/skills/doc-writing-for-rag/references/before-after-examples.md +291 -0
  58. package/framework/.claude/skills/doc-writing-for-rag/references/compact-templates.md +183 -0
  59. package/framework/.claude/skills/doc-writing-for-rag/references/frontmatter-minimal.md +112 -0
  60. package/framework/.claude/skills/doc-writing-for-rag/references/line-count-targets.md +110 -0
  61. package/framework/.claude/skills/doc-writing-for-rag/references/schemas-and-errors.md +129 -0
  62. package/framework/.claude/skills/find-skills/SKILL.md +133 -0
  63. package/framework/.claude/skills/frontend-design/LICENSE.txt +177 -0
  64. package/framework/.claude/skills/frontend-design/SKILL.md +84 -0
  65. package/framework/.claude/skills/gamification-design/SKILL.md +130 -0
  66. package/framework/.claude/skills/issue-review/SKILL.md +45 -0
  67. package/framework/.claude/skills/kie-ai/SKILL.md +262 -0
  68. package/framework/.claude/skills/kie-ai/references/models-catalog.md +272 -0
  69. package/framework/.claude/skills/kie-ai/scripts/kie_api.sh +209 -0
  70. package/framework/.claude/skills/kie-ai/scripts/remove_greenscreen.py +69 -0
  71. package/framework/.claude/skills/kie-ai/scripts/setup_api_key.sh +77 -0
  72. package/framework/.claude/skills/motion-design/LICENSE +21 -0
  73. package/framework/.claude/skills/motion-design/README.md +82 -0
  74. package/framework/.claude/skills/motion-design/SKILL.md +336 -0
  75. package/framework/.claude/skills/motion-design/director/choreography.md +93 -0
  76. package/framework/.claude/skills/motion-design/director/context-adaptation.md +83 -0
  77. package/framework/.claude/skills/motion-design/director/core-philosophy.md +53 -0
  78. package/framework/.claude/skills/motion-design/director/decision-framework.md +91 -0
  79. package/framework/.claude/skills/motion-design/director/disney-principles.md +102 -0
  80. package/framework/.claude/skills/motion-design/director/emotion-mapping.md +71 -0
  81. package/framework/.claude/skills/motion-design/director/motion-personality.md +89 -0
  82. package/framework/.claude/skills/motion-design/director/narrative-structure.md +62 -0
  83. package/framework/.claude/skills/motion-design/patterns/ambient-continuous.md +81 -0
  84. package/framework/.claude/skills/motion-design/patterns/entrance-exit.md +82 -0
  85. package/framework/.claude/skills/motion-design/patterns/multi-element.md +69 -0
  86. package/framework/.claude/skills/motion-design/patterns/state-feedback.md +96 -0
  87. package/framework/.claude/skills/motion-design/reference/property-selection.md +95 -0
  88. package/framework/.claude/skills/motion-design/reference/quality-checklist.md +67 -0
  89. package/framework/.claude/skills/motion-design/reference/timing-easing-tables.md +106 -0
  90. package/framework/.claude/skills/motion-design/reference/troubleshooting.md +73 -0
  91. package/framework/.claude/skills/new/SKILL.md +1687 -0
  92. package/framework/.claude/skills/playwright-skill/API_REFERENCE.md +652 -0
  93. package/framework/.claude/skills/playwright-skill/SKILL.md +157 -0
  94. package/framework/.claude/skills/playwright-skill/package.json +26 -0
  95. package/framework/.claude/skills/prd/SKILL.md +228 -0
  96. package/framework/.claude/skills/prd/assets/card-template.yml +232 -0
  97. package/framework/.claude/skills/prd/assets/epic-template.yml +190 -0
  98. package/framework/.claude/skills/prd/assets/prd-template.md +230 -0
  99. package/framework/.claude/skills/prd/assets/state-template.md +78 -0
  100. package/framework/.claude/skills/prd/references/api-perf-gate.md +152 -0
  101. package/framework/.claude/skills/prd/references/audit-phase.md +478 -0
  102. package/framework/.claude/skills/prd/references/backlog-phase.md +145 -0
  103. package/framework/.claude/skills/prd/references/discovery-phase.md +359 -0
  104. package/framework/.claude/skills/prd/references/impact-analysis.md +233 -0
  105. package/framework/.claude/skills/prd/references/prd-add-phase.md +214 -0
  106. package/framework/.claude/skills/prd/references/prd-writing-phase.md +145 -0
  107. package/framework/.claude/skills/prd/references/research-phase.md +216 -0
  108. package/framework/.claude/skills/prd/references/ui-design-phase.md +61 -0
  109. package/framework/.claude/skills/prd/references/validation-phase.md +72 -0
  110. package/framework/.claude/skills/prd-add/SKILL.md +222 -0
  111. package/framework/.claude/skills/prd-add/references/impact-analysis.md +233 -0
  112. package/framework/.claude/skills/remotion-best-practices/SKILL.md +48 -0
  113. package/framework/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
  114. package/framework/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
  115. package/framework/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  116. package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  117. package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  118. package/framework/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
  119. package/framework/.claude/skills/remotion-best-practices/rules/audio.md +169 -0
  120. package/framework/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  121. package/framework/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
  122. package/framework/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
  123. package/framework/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
  124. package/framework/.claude/skills/remotion-best-practices/rules/display-captions.md +184 -0
  125. package/framework/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  126. package/framework/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
  127. package/framework/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  128. package/framework/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  129. package/framework/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  130. package/framework/.claude/skills/remotion-best-practices/rules/gifs.md +141 -0
  131. package/framework/.claude/skills/remotion-best-practices/rules/images.md +130 -0
  132. package/framework/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  133. package/framework/.claude/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  134. package/framework/.claude/skills/remotion-best-practices/rules/lottie.md +67 -0
  135. package/framework/.claude/skills/remotion-best-practices/rules/maps.md +401 -0
  136. package/framework/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  137. package/framework/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  138. package/framework/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
  139. package/framework/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
  140. package/framework/.claude/skills/remotion-best-practices/rules/subtitles.md +36 -0
  141. package/framework/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
  142. package/framework/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
  143. package/framework/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
  144. package/framework/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  145. package/framework/.claude/skills/remotion-best-practices/rules/transitions.md +197 -0
  146. package/framework/.claude/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  147. package/framework/.claude/skills/remotion-best-practices/rules/trimming.md +52 -0
  148. package/framework/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
  149. package/framework/.claude/skills/seo-audit/SKILL.md +394 -0
  150. package/framework/.claude/skills/seo-audit/references/aeo-geo-patterns.md +279 -0
  151. package/framework/.claude/skills/seo-audit/references/ai-writing-detection.md +190 -0
  152. package/framework/.claude/skills/simplify/SKILL.md +137 -0
  153. package/framework/.claude/skills/skill-creator/LICENSE.txt +202 -0
  154. package/framework/.claude/skills/skill-creator/SKILL.md +356 -0
  155. package/framework/.claude/skills/skill-creator/references/output-patterns.md +82 -0
  156. package/framework/.claude/skills/skill-creator/references/workflows.md +28 -0
  157. package/framework/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  158. package/framework/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  159. package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
  160. package/framework/.claude/skills/ui-design/SKILL.md +199 -0
  161. package/framework/.claude/skills/ui-design/references/component-discovery.md +54 -0
  162. package/framework/.claude/skills/ui-design/references/evaluation.md +171 -0
  163. package/framework/.claude/skills/ui-design/references/generation.md +109 -0
  164. package/framework/.claude/skills/ui-design/references/inventory.md +59 -0
  165. package/framework/.claude/skills/webapp-testing/LICENSE.txt +202 -0
  166. package/framework/.claude/skills/webapp-testing/SKILL.md +123 -0
  167. package/framework/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
  168. package/framework/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
  169. package/framework/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
  170. package/framework/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
  171. package/framework/.claude/skills/worktree-manager/SKILL.md +680 -0
  172. package/framework/AGENTS.md +240 -0
  173. package/framework/agents/api-contracts.md +137 -0
  174. package/framework/agents/architecture.md +145 -0
  175. package/framework/agents/coding-standards.md +148 -0
  176. package/framework/agents/data-model.md +110 -0
  177. package/framework/agents/deployment-protocol.md +232 -0
  178. package/framework/agents/design-review.md +172 -0
  179. package/framework/agents/env-reference.md +171 -0
  180. package/framework/agents/github-issue-subagent.md +252 -0
  181. package/framework/agents/index.md +261 -0
  182. package/framework/agents/llm-wiki-methodology.md +216 -0
  183. package/framework/agents/maintenance-protocol.md +305 -0
  184. package/framework/agents/observability.md +162 -0
  185. package/framework/agents/performance.md +155 -0
  186. package/framework/agents/project-context.md +145 -0
  187. package/framework/agents/runbook.md +208 -0
  188. package/framework/agents/security.md +168 -0
  189. package/framework/agents/skills-mapping.md +286 -0
  190. package/framework/agents/testing.md +111 -0
  191. package/framework/agents/workflows.md +215 -0
  192. package/framework/docs/PROJECT-CONFIGURATION.md +336 -0
  193. package/framework/docs/references/brand-guidelines.md +170 -0
  194. package/framework/docs/references/ui-guidelines.template.md +182 -0
  195. package/framework/routines/code-review.routine.yml +46 -0
  196. package/framework/routines/doc-review.routine.yml +45 -0
  197. package/framework/routines/ds-drift.routine.yml +52 -0
  198. package/framework/routines/full-sweep.routine.yml +51 -0
  199. package/framework/routines/index.yml +70 -0
  200. package/framework/routines/skill-improve.routine.yml +50 -0
  201. package/framework/routines/wiki-review.routine.yml +45 -0
  202. package/framework/templates/baldart.config.template.yml +113 -0
  203. package/framework/templates/breaking-change-checklist.md +484 -0
  204. package/framework/templates/feature-card.template.yml +125 -0
  205. package/framework/templates/overlays/README.md +44 -0
  206. package/framework/templates/overlays/copywriting.fidelity-example.md +62 -0
  207. package/framework/templates/overlays/ui-design.fidelity-example.md +75 -0
  208. package/framework/templates/skill-project-context.snippet.md +19 -0
  209. package/framework/templates/spec.template.md +208 -0
  210. package/package.json +51 -0
  211. package/src/commands/add.js +229 -0
  212. package/src/commands/configure.js +385 -0
  213. package/src/commands/doctor.js +486 -0
  214. package/src/commands/migrate.js +185 -0
  215. package/src/commands/push.js +0 -0
  216. package/src/commands/routines.js +269 -0
  217. package/src/commands/status.js +130 -0
  218. package/src/commands/update.js +419 -0
  219. package/src/commands/version.js +88 -0
  220. package/src/utils/contamination.js +400 -0
  221. package/src/utils/git.js +181 -0
  222. package/src/utils/hooks.js +152 -0
  223. package/src/utils/routine-adapters/claude-code-cloud.js +78 -0
  224. package/src/utils/routine-adapters/cron.js +138 -0
  225. package/src/utils/routine-adapters/github-actions.js +141 -0
  226. package/src/utils/routine-adapters/index.js +21 -0
  227. package/src/utils/routines.js +166 -0
  228. package/src/utils/state.js +143 -0
  229. package/src/utils/symlinks.js +425 -0
  230. package/src/utils/ui.js +133 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Contamination scanner & autofix.
3
+ *
4
+ * Detects project-specific tokens (hardcoded paths, brand identity, opinionated
5
+ * stack choices) that should NOT enter the upstream BALDART framework when a
6
+ * consumer pushes improvements. Patterns are the same ones used to clean the
7
+ * v3.0.0 refactor.
8
+ *
9
+ * Three severities:
10
+ *
11
+ * - 'autofixable' — safe textual substitution (hardcoded paths).
12
+ * autofix() applies these; UI confirms once.
13
+ * - 'requires-decision' — semantic tokens (brand identity, stack opinions).
14
+ * UI lists each line, user decides whether to (a) move
15
+ * to overlay, (b) keep as-is and force push, or (c) abort.
16
+ * - 'block' — secrets / credentials patterns; never auto-cleared.
17
+ *
18
+ * Public API:
19
+ *
20
+ * scan(content, opts?) → { findings: [{rule, line, lineNum, match, severity, suggestion?}] }
21
+ * autofix(content) → { content: <new>, applied: [{rule, count}] }
22
+ * explain(finding) → string (one-line human readable)
23
+ * isClean(findings) → boolean
24
+ * summarise(findings) → { autofixable: N, requiresDecision: N, blocking: N }
25
+ */
26
+
27
+ // -----------------------------------------------------------------------
28
+ // Rules
29
+ // -----------------------------------------------------------------------
30
+
31
+ /**
32
+ * Each rule:
33
+ * id — short slug
34
+ * severity — 'autofixable' | 'requires-decision' | 'block'
35
+ * pattern — RegExp (must be /g for autofix to work)
36
+ * replace — function(match) → string (only meaningful for autofixable)
37
+ * label — short human label
38
+ * advice — one-line guidance for the UI
39
+ */
40
+ const RULES = [
41
+ // ---- Autofixable: hardcoded canonical paths ----
42
+ {
43
+ id: 'path-design-system',
44
+ severity: 'autofixable',
45
+ pattern: /\bdocs\/design-system\//g,
46
+ replace: () => '${paths.design_system}/',
47
+ label: 'docs/design-system/',
48
+ advice: 'replace with `${paths.design_system}/`',
49
+ },
50
+ {
51
+ id: 'path-references-api',
52
+ severity: 'autofixable',
53
+ pattern: /\bdocs\/references\/api\//g,
54
+ replace: () => '${paths.references_dir}/api/',
55
+ label: 'docs/references/api/',
56
+ advice: 'replace with `${paths.references_dir}/api/`',
57
+ },
58
+ {
59
+ id: 'path-references-ui',
60
+ severity: 'autofixable',
61
+ pattern: /\bdocs\/references\/ui\//g,
62
+ replace: () => '${paths.references_dir}/ui/',
63
+ label: 'docs/references/ui/',
64
+ advice: 'replace with `${paths.references_dir}/ui/`',
65
+ },
66
+ {
67
+ id: 'path-references-other',
68
+ severity: 'autofixable',
69
+ // Catches docs/references/<anything> but NOT the api/, ui/ branches
70
+ // already covered, and NOT the bare `docs/references` directory token.
71
+ pattern: /\bdocs\/references\/(?!api\/|ui\/)([a-zA-Z0-9_\-\/.]+)/g,
72
+ replace: (_m, rest) => `\${paths.references_dir}/${rest}`,
73
+ label: 'docs/references/<x>',
74
+ advice: 'replace with `${paths.references_dir}/<x>`',
75
+ },
76
+ {
77
+ id: 'path-prd',
78
+ severity: 'autofixable',
79
+ pattern: /\bdocs\/prd\//g,
80
+ replace: () => '${paths.prd_dir}/',
81
+ label: 'docs/prd/',
82
+ advice: 'replace with `${paths.prd_dir}/`',
83
+ },
84
+ {
85
+ id: 'path-decisions',
86
+ severity: 'autofixable',
87
+ pattern: /\bdocs\/decisions\//g,
88
+ replace: () => '${paths.adrs_dir}/',
89
+ label: 'docs/decisions/',
90
+ advice: 'replace with `${paths.adrs_dir}/`',
91
+ },
92
+ {
93
+ id: 'path-wiki',
94
+ severity: 'autofixable',
95
+ pattern: /\bdocs\/wiki\//g,
96
+ replace: () => '${paths.wiki_dir}/',
97
+ label: 'docs/wiki/',
98
+ advice: 'replace with `${paths.wiki_dir}/`',
99
+ },
100
+ {
101
+ id: 'path-backlog',
102
+ severity: 'autofixable',
103
+ // Match `backlog/` or `/backlog/` when used as a path token. Don't match
104
+ // the word `backlog` inside prose. We require either: it's preceded by a
105
+ // path-like separator (slash, space, quote, paren, ``), AND followed by
106
+ // something path-like (slash, *, alphanumeric, dot).
107
+ pattern: /(^|[\s"'`(\[])\/?backlog\/(?=[a-zA-Z*.\/]|$|\s|["'`)\]])/g,
108
+ replace: (_m, lead) => `${lead}\${paths.backlog_dir}/`,
109
+ label: 'backlog/',
110
+ advice: 'replace with `${paths.backlog_dir}/`',
111
+ },
112
+ {
113
+ id: 'path-components-primitives',
114
+ severity: 'autofixable',
115
+ pattern: /\bsrc\/components\/ui\//g,
116
+ replace: () => '${paths.components_primitives}/',
117
+ label: 'src/components/ui/',
118
+ advice: 'replace with `${paths.components_primitives}/`',
119
+ },
120
+ {
121
+ id: 'path-components-root',
122
+ severity: 'autofixable',
123
+ pattern: /\bsrc\/components\/(?!ui\/)/g,
124
+ replace: () => '${paths.components_root}/',
125
+ label: 'src/components/',
126
+ advice: 'replace with `${paths.components_root}/`',
127
+ },
128
+ {
129
+ id: 'path-global-styles',
130
+ severity: 'autofixable',
131
+ pattern: /\bsrc\/app\/globals\.css\b/g,
132
+ replace: () => '${paths.global_styles}',
133
+ label: 'src/app/globals.css',
134
+ advice: 'replace with `${paths.global_styles}`',
135
+ },
136
+
137
+ // ---- Requires decision: identity / brand tokens ----
138
+ {
139
+ id: 'identity-neo-brutalism',
140
+ severity: 'requires-decision',
141
+ pattern: /\bNeo-?Brutalism\b/g,
142
+ label: 'Neo-Brutalism',
143
+ advice: 'move to .baldart/overlays/<skill>.md or reference identity.design_philosophy',
144
+ },
145
+ {
146
+ id: 'identity-italian-first',
147
+ severity: 'requires-decision',
148
+ pattern: /\bItalian-first\b/gi,
149
+ label: 'Italian-first',
150
+ advice: 'replace with reference to identity.language',
151
+ },
152
+ {
153
+ id: 'identity-merchant',
154
+ severity: 'requires-decision',
155
+ pattern: /\b(?:merchant|merchants)\b/gi,
156
+ label: 'merchant',
157
+ advice: 'audience segment — move to overlay or reference identity.audience_segments',
158
+ },
159
+ {
160
+ id: 'identity-customer',
161
+ severity: 'requires-decision',
162
+ pattern: /\b(?:customer|customers)\b/gi,
163
+ label: 'customer',
164
+ advice: 'audience segment — move to overlay or reference identity.audience_segments',
165
+ },
166
+ {
167
+ id: 'identity-fidelity',
168
+ severity: 'requires-decision',
169
+ pattern: /\bfidelity\b/gi,
170
+ label: 'fidelity',
171
+ advice: 'project name leak — generalise or move to overlay',
172
+ },
173
+ {
174
+ id: 'identity-brutal-wrapper',
175
+ severity: 'requires-decision',
176
+ pattern: /\bBrutal(?:Line|Bar|Donut|Heatmap|Area|Pie|Scatter)Chart\b/g,
177
+ label: 'Brutal*Chart',
178
+ advice: 'project chart wrapper — move to overlay or reference stack.charting.wrappers_root',
179
+ },
180
+
181
+ // ---- Requires decision: stack opinions ----
182
+ {
183
+ id: 'stack-recharts',
184
+ severity: 'requires-decision',
185
+ pattern: /\brecharts\b/gi,
186
+ label: 'recharts',
187
+ advice: 'opinionated charting choice — should live in stack.charting.canonical or overlay',
188
+ },
189
+ {
190
+ id: 'stack-nivo',
191
+ severity: 'requires-decision',
192
+ pattern: /@nivo\/[a-z-]+/g,
193
+ label: '@nivo/*',
194
+ advice: 'opinionated charting choice — should live in stack.charting.canonical or overlay',
195
+ },
196
+ {
197
+ id: 'stack-forbidden-chart-libs',
198
+ severity: 'requires-decision',
199
+ pattern: /\b(?:chart\.js|echarts|victory|visx|observable-plot|tremor)\b/g,
200
+ label: '<chart-lib>',
201
+ advice: 'forbidden-list entry — should live in stack.charting.forbidden or overlay',
202
+ },
203
+ {
204
+ id: 'stack-framer-motion',
205
+ severity: 'requires-decision',
206
+ pattern: /\bframer-motion\b/g,
207
+ label: 'framer-motion',
208
+ advice: 'opinionated animation choice — should live in stack.animation.canonical',
209
+ },
210
+
211
+ // ---- Block: secrets / credentials ----
212
+ // Conservative: we flag clearly-secret-like tokens. False positives are
213
+ // acceptable; false negatives are not.
214
+ {
215
+ id: 'secret-api-key',
216
+ severity: 'block',
217
+ // Quoted or unquoted — covers both `apiKey: "abc..."` (JSON/JS) and
218
+ // `API_KEY=abc...` (.env / shell). Conservative on length to avoid
219
+ // false positives on short config flags.
220
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi,
221
+ label: 'API key literal',
222
+ advice: 'looks like a hardcoded API key — remove before pushing',
223
+ },
224
+ {
225
+ id: 'secret-github-pat',
226
+ severity: 'block',
227
+ pattern: /\bgh[posu]_[A-Za-z0-9]{30,}\b/g,
228
+ label: 'GitHub PAT',
229
+ advice: 'GitHub personal access token pattern — remove before pushing',
230
+ },
231
+ {
232
+ id: 'secret-slack-token',
233
+ severity: 'block',
234
+ pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g,
235
+ label: 'Slack token',
236
+ advice: 'Slack token pattern — remove before pushing',
237
+ },
238
+ {
239
+ id: 'secret-jwt',
240
+ severity: 'block',
241
+ pattern: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g,
242
+ label: 'JWT',
243
+ advice: 'JSON Web Token pattern — remove before pushing',
244
+ },
245
+ {
246
+ id: 'secret-bearer',
247
+ severity: 'block',
248
+ pattern: /bearer\s+[A-Za-z0-9._\-]{20,}/gi,
249
+ label: 'Bearer token',
250
+ advice: 'looks like a hardcoded bearer token — remove before pushing',
251
+ },
252
+ {
253
+ id: 'secret-aws',
254
+ severity: 'block',
255
+ pattern: /AKIA[0-9A-Z]{16}/g,
256
+ label: 'AWS access key id',
257
+ advice: 'AWS access key pattern — remove before pushing',
258
+ },
259
+ {
260
+ id: 'secret-private-key',
261
+ severity: 'block',
262
+ pattern: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g,
263
+ label: 'PEM private key header',
264
+ advice: 'private key block — remove before pushing',
265
+ },
266
+ ];
267
+
268
+ // -----------------------------------------------------------------------
269
+ // Opt-out marker
270
+ // -----------------------------------------------------------------------
271
+
272
+ /**
273
+ * Some files legitimately quote project-default paths or identity tokens as
274
+ * *examples* (e.g. autodetection probe descriptions, migration guides,
275
+ * starter overlays). Autofixing them would destroy their pedagogical value.
276
+ *
277
+ * Such files declare opt-out with either:
278
+ * - an HTML comment `<!-- contamination-scan: skip -->` anywhere in the
279
+ * first 20 lines, OR
280
+ * - a YAML frontmatter key `contamination_scan: skip`.
281
+ *
282
+ * The scan / autofix entry points consult this first and short-circuit.
283
+ */
284
+ function isOptedOut(content) {
285
+ if (typeof content !== 'string') return false;
286
+ const head = content.split('\n', 20).join('\n');
287
+ if (/<!--\s*contamination-scan:\s*skip[\s\S]*?-->/i.test(head)) return true;
288
+ if (/^contamination_scan:\s*skip\b/im.test(head)) return true;
289
+ return false;
290
+ }
291
+
292
+ // -----------------------------------------------------------------------
293
+ // Public API
294
+ // -----------------------------------------------------------------------
295
+
296
+ /**
297
+ * Scan a single file's text content. Returns findings grouped by rule.
298
+ * `opts.skipRules` is an optional array of rule ids to ignore (used when
299
+ * the user has already confirmed those tokens should pass).
300
+ *
301
+ * If the file declares the opt-out marker (see isOptedOut), returns an
302
+ * empty findings list — the file is explicitly exempt.
303
+ */
304
+ function scan(content, opts = {}) {
305
+ if (typeof content === 'string' && isOptedOut(content)) {
306
+ return { findings: [], optedOut: true };
307
+ }
308
+ if (typeof content !== 'string') {
309
+ throw new TypeError('scan(content): expected string');
310
+ }
311
+ const skip = new Set(opts.skipRules || []);
312
+ const lines = content.split('\n');
313
+ const findings = [];
314
+
315
+ for (const rule of RULES) {
316
+ if (skip.has(rule.id)) continue;
317
+ // Reset lastIndex defensively — RULES patterns are /g.
318
+ rule.pattern.lastIndex = 0;
319
+ for (let i = 0; i < lines.length; i++) {
320
+ const line = lines[i];
321
+ // Use String.prototype.matchAll to get all hits per line.
322
+ const local = new RegExp(rule.pattern.source, rule.pattern.flags);
323
+ const hits = line.matchAll(local);
324
+ for (const m of hits) {
325
+ findings.push({
326
+ rule: rule.id,
327
+ label: rule.label,
328
+ severity: rule.severity,
329
+ line: line,
330
+ lineNum: i + 1,
331
+ match: m[0],
332
+ advice: rule.advice,
333
+ });
334
+ }
335
+ }
336
+ }
337
+ return { findings };
338
+ }
339
+
340
+ /**
341
+ * Apply autofix substitutions across content. Returns the new content plus a
342
+ * tally of which rules fired and how many times.
343
+ *
344
+ * Only rules with severity === 'autofixable' AND a `replace` function run.
345
+ */
346
+ function autofix(content) {
347
+ if (typeof content !== 'string') {
348
+ throw new TypeError('autofix(content): expected string');
349
+ }
350
+ if (isOptedOut(content)) return { content, applied: [], optedOut: true };
351
+ let out = content;
352
+ const applied = [];
353
+ for (const rule of RULES) {
354
+ if (rule.severity !== 'autofixable' || typeof rule.replace !== 'function') continue;
355
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
356
+ let count = 0;
357
+ out = out.replace(re, (...args) => {
358
+ count++;
359
+ return rule.replace(...args);
360
+ });
361
+ if (count > 0) applied.push({ rule: rule.id, label: rule.label, count });
362
+ }
363
+ return { content: out, applied };
364
+ }
365
+
366
+ function explain(finding) {
367
+ const sev = finding.severity === 'autofixable'
368
+ ? 'AUTOFIX'
369
+ : finding.severity === 'requires-decision'
370
+ ? 'REVIEW '
371
+ : 'BLOCK ';
372
+ return `${sev} L${finding.lineNum} ${finding.label} — ${finding.advice}`;
373
+ }
374
+
375
+ function isClean(findings) {
376
+ return !findings || findings.length === 0;
377
+ }
378
+
379
+ function summarise(findings) {
380
+ const out = { autofixable: 0, requiresDecision: 0, blocking: 0, total: 0 };
381
+ if (!findings) return out;
382
+ for (const f of findings) {
383
+ out.total++;
384
+ if (f.severity === 'autofixable') out.autofixable++;
385
+ else if (f.severity === 'requires-decision') out.requiresDecision++;
386
+ else if (f.severity === 'block') out.blocking++;
387
+ }
388
+ return out;
389
+ }
390
+
391
+ module.exports = {
392
+ scan,
393
+ autofix,
394
+ explain,
395
+ isClean,
396
+ isOptedOut,
397
+ summarise,
398
+ // Exposed for tests / introspection
399
+ _rules: RULES,
400
+ };
@@ -0,0 +1,181 @@
1
+ const simpleGit = require('simple-git');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const FRAMEWORK_DIR = '.framework';
6
+
7
+ class GitUtils {
8
+ constructor(cwd = process.cwd()) {
9
+ this.cwd = cwd;
10
+ this.git = simpleGit(cwd);
11
+ }
12
+
13
+ async isGitRepo() {
14
+ try {
15
+ await this.git.status();
16
+ return true;
17
+ } catch (error) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async hasCleanWorkingTree() {
23
+ const status = await this.git.status();
24
+ return status.isClean();
25
+ }
26
+
27
+ async frameworkExists() {
28
+ const frameworkPath = path.join(this.cwd, FRAMEWORK_DIR);
29
+ return fs.existsSync(frameworkPath);
30
+ }
31
+
32
+ async addSubtree(repo, branch = 'main') {
33
+ const repoUrl = this.normalizeRepoUrl(repo);
34
+
35
+ await this.git.raw([
36
+ 'subtree',
37
+ 'add',
38
+ '--prefix',
39
+ FRAMEWORK_DIR,
40
+ repoUrl,
41
+ branch,
42
+ '--squash'
43
+ ]);
44
+ }
45
+
46
+ async updateSubtree(repo, branch = 'main') {
47
+ const repoUrl = this.normalizeRepoUrl(repo);
48
+
49
+ await this.git.raw([
50
+ 'subtree',
51
+ 'pull',
52
+ '--prefix',
53
+ FRAMEWORK_DIR,
54
+ repoUrl,
55
+ branch,
56
+ '--squash'
57
+ ]);
58
+ }
59
+
60
+ async pushSubtree(repo, branch = 'main') {
61
+ const repoUrl = this.normalizeRepoUrl(repo);
62
+
63
+ await this.git.raw([
64
+ 'subtree',
65
+ 'push',
66
+ '--prefix',
67
+ FRAMEWORK_DIR,
68
+ repoUrl,
69
+ branch
70
+ ]);
71
+ }
72
+
73
+ async getFrameworkVersion() {
74
+ const versionFile = path.join(this.cwd, FRAMEWORK_DIR, 'VERSION');
75
+ if (fs.existsSync(versionFile)) {
76
+ return fs.readFileSync(versionFile, 'utf8').trim();
77
+ }
78
+ return 'unknown';
79
+ }
80
+
81
+ async createBackupTag() {
82
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
83
+ const tagName = `backup/${timestamp}`;
84
+ await this.git.addTag(tagName);
85
+ return tagName;
86
+ }
87
+
88
+ async getRemoteVersion(repo, branch = 'main') {
89
+ const repoUrl = this.normalizeRepoUrl(repo);
90
+
91
+ try {
92
+ const versionContent = await this.git.raw([
93
+ 'show',
94
+ `${repoUrl}/${branch}:VERSION`
95
+ ]);
96
+ return versionContent.trim();
97
+ } catch (error) {
98
+ return 'unknown';
99
+ }
100
+ }
101
+
102
+ async hasChangesToPush() {
103
+ try {
104
+ const log = await this.git.raw([
105
+ 'log',
106
+ 'origin/main..HEAD',
107
+ '--oneline',
108
+ '--',
109
+ FRAMEWORK_DIR
110
+ ]);
111
+ return log.trim().length > 0;
112
+ } catch (error) {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ async getChangesSummary() {
118
+ try {
119
+ const log = await this.git.raw([
120
+ 'log',
121
+ 'origin/main..HEAD',
122
+ '--oneline',
123
+ '--',
124
+ FRAMEWORK_DIR
125
+ ]);
126
+
127
+ const stat = await this.git.raw([
128
+ 'diff',
129
+ 'origin/main..HEAD',
130
+ '--stat',
131
+ '--',
132
+ FRAMEWORK_DIR
133
+ ]);
134
+
135
+ return { log: log.trim(), stat: stat.trim() };
136
+ } catch (error) {
137
+ return { log: '', stat: '' };
138
+ }
139
+ }
140
+
141
+ normalizeRepoUrl(repo) {
142
+ // Handle different formats:
143
+ // - "owner/repo" -> "https://github.com/owner/repo.git"
144
+ // - "https://github.com/owner/repo" -> "https://github.com/owner/repo.git"
145
+ // - "https://github.com/owner/repo.git" -> unchanged
146
+
147
+ if (repo.startsWith('http://') || repo.startsWith('https://')) {
148
+ return repo.endsWith('.git') ? repo : `${repo}.git`;
149
+ }
150
+
151
+ // Assume GitHub shorthand
152
+ return `https://github.com/${repo}.git`;
153
+ }
154
+
155
+ async commitChanges(message) {
156
+ await this.git.add(FRAMEWORK_DIR);
157
+ await this.git.commit(message);
158
+ }
159
+
160
+ async fetch(repo, branch = 'main') {
161
+ const repoUrl = this.normalizeRepoUrl(repo);
162
+ await this.git.fetch(repoUrl, branch);
163
+ }
164
+
165
+ async diffWithRemote() {
166
+ try {
167
+ const diff = await this.git.raw([
168
+ 'diff',
169
+ 'HEAD',
170
+ 'FETCH_HEAD',
171
+ '--',
172
+ FRAMEWORK_DIR
173
+ ]);
174
+ return diff;
175
+ } catch (error) {
176
+ return '';
177
+ }
178
+ }
179
+ }
180
+
181
+ module.exports = GitUtils;