@supportpages.io/wtfm 0.0.0-stage → 0.3.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/LICENSE +201 -0
  2. package/NOTICE +23 -0
  3. package/README.md +392 -2
  4. package/dist/actions.d.ts +98 -0
  5. package/dist/actions.js +86 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-settings.d.ts +22 -0
  8. package/dist/agent-settings.js +35 -0
  9. package/dist/agent-settings.js.map +1 -0
  10. package/dist/api.d.ts +14 -0
  11. package/dist/api.js +159 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/article-link.d.ts +16 -0
  14. package/dist/article-link.js +25 -0
  15. package/dist/article-link.js.map +1 -0
  16. package/dist/artifacts.d.ts +45 -0
  17. package/dist/artifacts.js +144 -0
  18. package/dist/artifacts.js.map +1 -0
  19. package/dist/brand.d.ts +4 -0
  20. package/dist/brand.js +9 -0
  21. package/dist/brand.js.map +1 -0
  22. package/dist/bridge.d.ts +2964 -0
  23. package/dist/bridge.js +1036 -0
  24. package/dist/bridge.js.map +1 -0
  25. package/dist/capacity.d.ts +5 -0
  26. package/dist/capacity.js +11 -0
  27. package/dist/capacity.js.map +1 -0
  28. package/dist/credentials.d.ts +13 -0
  29. package/dist/credentials.js +88 -0
  30. package/dist/credentials.js.map +1 -0
  31. package/dist/development-tls.d.ts +8 -0
  32. package/dist/development-tls.js +38 -0
  33. package/dist/development-tls.js.map +1 -0
  34. package/dist/errors.d.ts +15 -0
  35. package/dist/errors.js +18 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/export.d.ts +11 -0
  38. package/dist/export.js +91 -0
  39. package/dist/export.js.map +1 -0
  40. package/dist/hosted-operations.d.ts +108 -0
  41. package/dist/hosted-operations.js +120 -0
  42. package/dist/hosted-operations.js.map +1 -0
  43. package/dist/hosting-benefits.d.ts +66 -0
  44. package/dist/hosting-benefits.js +67 -0
  45. package/dist/hosting-benefits.js.map +1 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +49 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/local-inventory.d.ts +22 -0
  50. package/dist/local-inventory.js +59 -0
  51. package/dist/local-inventory.js.map +1 -0
  52. package/dist/local-setup.d.ts +208 -0
  53. package/dist/local-setup.js +140 -0
  54. package/dist/local-setup.js.map +1 -0
  55. package/dist/pairing.d.ts +71 -0
  56. package/dist/pairing.js +235 -0
  57. package/dist/pairing.js.map +1 -0
  58. package/dist/preferences.d.ts +18 -0
  59. package/dist/preferences.js +44 -0
  60. package/dist/preferences.js.map +1 -0
  61. package/dist/progress.d.ts +205 -0
  62. package/dist/progress.js +224 -0
  63. package/dist/progress.js.map +1 -0
  64. package/dist/reminders.d.ts +31 -0
  65. package/dist/reminders.js +73 -0
  66. package/dist/reminders.js.map +1 -0
  67. package/dist/replace-connection.d.ts +6 -0
  68. package/dist/replace-connection.js +77 -0
  69. package/dist/replace-connection.js.map +1 -0
  70. package/dist/repository-actions.d.ts +18 -0
  71. package/dist/repository-actions.js +9 -0
  72. package/dist/repository-actions.js.map +1 -0
  73. package/dist/repository-benefits.d.ts +96 -0
  74. package/dist/repository-benefits.js +64 -0
  75. package/dist/repository-benefits.js.map +1 -0
  76. package/dist/run-update.d.ts +17 -0
  77. package/dist/run-update.js +27 -0
  78. package/dist/run-update.js.map +1 -0
  79. package/dist/runs.d.ts +541 -0
  80. package/dist/runs.js +146 -0
  81. package/dist/runs.js.map +1 -0
  82. package/dist/runtime.d.ts +12 -0
  83. package/dist/runtime.js +58 -0
  84. package/dist/runtime.js.map +1 -0
  85. package/dist/schema.d.ts +277 -0
  86. package/dist/schema.js +66 -0
  87. package/dist/schema.js.map +1 -0
  88. package/dist/server.d.ts +4 -0
  89. package/dist/server.js +298 -0
  90. package/dist/server.js.map +1 -0
  91. package/dist/session.d.ts +1714 -0
  92. package/dist/session.js +619 -0
  93. package/dist/session.js.map +1 -0
  94. package/dist/settings.d.ts +9 -0
  95. package/dist/settings.js +21 -0
  96. package/dist/settings.js.map +1 -0
  97. package/dist/sync.d.ts +495 -0
  98. package/dist/sync.js +191 -0
  99. package/dist/sync.js.map +1 -0
  100. package/dist/telemetry-scrub.d.ts +13 -0
  101. package/dist/telemetry-scrub.js +51 -0
  102. package/dist/telemetry-scrub.js.map +1 -0
  103. package/dist/telemetry.d.ts +73 -0
  104. package/dist/telemetry.js +173 -0
  105. package/dist/telemetry.js.map +1 -0
  106. package/dist/walkthroughs.d.ts +224 -0
  107. package/dist/walkthroughs.js +109 -0
  108. package/dist/walkthroughs.js.map +1 -0
  109. package/dist/workspace.d.ts +14 -0
  110. package/dist/workspace.js +127 -0
  111. package/dist/workspace.js.map +1 -0
  112. package/dist/writer-agent.d.ts +21 -0
  113. package/dist/writer-agent.js +27 -0
  114. package/dist/writer-agent.js.map +1 -0
  115. package/dist/writer-entry.d.ts +171 -0
  116. package/dist/writer-entry.js +233 -0
  117. package/dist/writer-entry.js.map +1 -0
  118. package/dist/writing-style.d.ts +7 -0
  119. package/dist/writing-style.js +52 -0
  120. package/dist/writing-style.js.map +1 -0
  121. package/engine/SYNC.json +4 -0
  122. package/engine/VERSION +1 -0
  123. package/engine/detect-project/README.md +141 -0
  124. package/engine/detect-project/SKILL.md +1421 -0
  125. package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
  126. package/engine/detect-project/assets/game/game-frame.css +132 -0
  127. package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
  128. package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
  129. package/engine/detect-project/assets/macosui/fonts.css +15 -0
  130. package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
  131. package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
  132. package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
  133. package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
  134. package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
  135. package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
  136. package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
  137. package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
  138. package/engine/detect-project/assets/mobileui/fonts.css +16 -0
  139. package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
  140. package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
  141. package/engine/detect-project/assets/mobileui/icons.css +31 -0
  142. package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
  143. package/engine/detect-project/assets/mobileui/platforms.json +46 -0
  144. package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
  145. package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
  146. package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
  147. package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
  148. package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
  149. package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
  150. package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
  151. package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
  152. package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
  153. package/engine/detect-project/assets/webtui/themes.json +37 -0
  154. package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
  155. package/engine/detect-project/assets/win32ui/7css.css +2 -0
  156. package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
  157. package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
  158. package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
  159. package/engine/detect-project/package-lock.json +12 -0
  160. package/engine/detect-project/package.json +10 -0
  161. package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
  162. package/engine/detect-project/scripts/check_css_health.js +412 -0
  163. package/engine/detect-project/scripts/check_project_map.js +150 -0
  164. package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
  165. package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
  166. package/engine/detect-project/scripts/classify_app_type.sh +246 -0
  167. package/engine/detect-project/scripts/classify_surface.sh +95 -0
  168. package/engine/detect-project/scripts/classify_workspace.js +39 -0
  169. package/engine/detect-project/scripts/compile_css.sh +447 -0
  170. package/engine/detect-project/scripts/detect_static.js +645 -0
  171. package/engine/detect-project/scripts/detect_structure.js +187 -0
  172. package/engine/detect-project/scripts/include_census.js +451 -0
  173. package/engine/detect-project/scripts/json_get.js +142 -0
  174. package/engine/detect-project/scripts/merge_json.js +52 -0
  175. package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
  176. package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
  177. package/engine/detect-project/scripts/run_css_build.sh +40 -0
  178. package/engine/detect-project/scripts/sanitize_css.js +83 -0
  179. package/engine/detect-project/scripts/test_classify_surface.js +108 -0
  180. package/engine/detect-project/scripts/test_node_helpers.js +142 -0
  181. package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
  182. package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
  183. package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
  184. package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
  185. package/engine/detect-project/scripts/theme_overrides.js +169 -0
  186. package/engine/detect-project/scripts/write_branding.js +129 -0
  187. package/engine/generate-illustrated-article/SKILL.md +461 -0
  188. package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
  189. package/engine/generate-illustrated-article/contracts/game.md +38 -0
  190. package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
  191. package/engine/generate-illustrated-article/contracts/macos.md +79 -0
  192. package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
  193. package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
  194. package/engine/generate-illustrated-article/contracts/win32.md +43 -0
  195. package/engine/generate-illustrated-article/package-lock.json +366 -0
  196. package/engine/generate-illustrated-article/package.json +15 -0
  197. package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
  198. package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
  199. package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
  200. package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
  201. package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
  202. package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
  203. package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
  204. package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
  205. package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
  206. package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
  207. package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
  208. package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
  209. package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
  210. package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
  211. package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
  212. package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
  213. package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
  214. package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
  215. package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
  216. package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
  217. package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
  218. package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
  219. package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
  220. package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
  221. package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
  222. package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
  223. package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
  224. package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
  225. package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
  226. package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
  227. package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
  228. package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
  229. package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
  230. package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
  231. package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
  232. package/install.sh +14 -0
  233. package/package.json +57 -4
  234. package/scripts/auth.mjs +33 -0
  235. package/scripts/auto-update.mjs +7 -0
  236. package/scripts/build-plugin.mjs +58 -0
  237. package/scripts/build-release.mjs +94 -0
  238. package/scripts/check-release.mjs +40 -0
  239. package/scripts/check-runtime.mjs +10 -0
  240. package/scripts/cli.mjs +8 -0
  241. package/scripts/install.mjs +64 -0
  242. package/scripts/lib/agent-runner.mjs +181 -0
  243. package/scripts/lib/agent-settings.mjs +215 -0
  244. package/scripts/lib/article-skills.mjs +82 -0
  245. package/scripts/lib/auto-update.mjs +50 -0
  246. package/scripts/lib/brand.mjs +9 -0
  247. package/scripts/lib/browser.mjs +12 -0
  248. package/scripts/lib/claude-connection.mjs +33 -0
  249. package/scripts/lib/claude-permissions.mjs +47 -0
  250. package/scripts/lib/claude-plugin.mjs +15 -0
  251. package/scripts/lib/claude-writer.mjs +10 -0
  252. package/scripts/lib/cli-main.mjs +85 -0
  253. package/scripts/lib/cli.mjs +888 -0
  254. package/scripts/lib/codex-config.mjs +51 -0
  255. package/scripts/lib/codex-integration.mjs +61 -0
  256. package/scripts/lib/codex-skill.mjs +34 -0
  257. package/scripts/lib/harness-models.mjs +109 -0
  258. package/scripts/lib/install.mjs +269 -0
  259. package/scripts/lib/managed-writer.mjs +38 -0
  260. package/scripts/lib/planning.mjs +263 -0
  261. package/scripts/lib/prepare-update.mjs +85 -0
  262. package/scripts/lib/refresh-writers.mjs +12 -0
  263. package/scripts/lib/remove.mjs +167 -0
  264. package/scripts/lib/renderer.mjs +34 -0
  265. package/scripts/lib/terminal.mjs +252 -0
  266. package/scripts/lib/uninit.mjs +72 -0
  267. package/scripts/lib/update.mjs +55 -0
  268. package/scripts/lib/writer-recovery.mjs +35 -0
  269. package/scripts/lib/yolo.mjs +362 -0
  270. package/scripts/plugin-session.mjs +65 -0
  271. package/scripts/prepare-update.mjs +12 -0
  272. package/scripts/publish-release.mjs +92 -0
  273. package/scripts/skills/supportpages/SKILL.md +120 -0
  274. package/scripts/smoke-release.mjs +101 -0
  275. package/server.json +27 -0
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check_css_health.js — deterministic gate that a freshly-assembled branding.css
4
+ * actually STYLES the project, not merely that it is large and syntactically valid.
5
+ *
6
+ * Motivation (a WordPress admin article): a hand-assembled bundle mirrored
7
+ * wp-admin.css's @import list and missed the separately-enqueued login.css and
8
+ * buttons.css, and its compiled colour-scheme section consumed
9
+ * var(--wp-admin-theme-color) 131x with zero definitions. The result passed every
10
+ * existing gate (valid CSS, 467KB, real selectors) and rendered the login mockup
11
+ * as bare HTML with invisible white-on-white primary buttons. Neither failure is
12
+ * WordPress-specific: any project that loads stylesheets per page/surface, or
13
+ * whose theme variables are emitted by a build step, can lose them the same way.
14
+ *
15
+ * Three checks, all framework-agnostic:
16
+ *
17
+ * 1. Undefined custom properties — every `var(--x)` consumed WITHOUT a fallback
18
+ * must be defined somewhere in the CSS. An undefined var invalidates its whole
19
+ * declaration at computed-value time (background: var(--theme) paints nothing
20
+ * while a sibling color: #fff still applies — the invisible-button failure).
21
+ * Usages WITH a fallback are fine (the fallback paints).
22
+ *
23
+ * 2. View selector coverage — class/id tokens sampled from the project's real
24
+ * view/template files (route_index primary views, layouts, app_shell from
25
+ * project_map.json, or explicit file args) must mostly resolve to selectors
26
+ * in the CSS. A documented surface whose tokens are largely unmatched means
27
+ * the stylesheet that styles it was never bundled.
28
+ *
29
+ * 3. Root offset classes — a rule like `html.wp-toolbar { padding-top: … }` is
30
+ * the compiled-CSS fingerprint of a fixed top toolbar whose offset hangs on a
31
+ * root state class. When the map records a persistent app_shell but
32
+ * app_shell.root_classes omits every such class, every downstream mockup
33
+ * inherits the defect mechanically (the authoring contract copies
34
+ * root_classes verbatim and the fidelity lint can only enforce recorded
35
+ * tokens) — the fixed bar paints OVER the sidebar/content. Observed: a
36
+ * detect run recorded root_classes.html: [] on WordPress while its own
37
+ * bundle carried html.wp-toolbar { padding-top: var(…) }; the generated
38
+ * admin screenshots rendered the admin bar overlapping the menu. The CSS is
39
+ * ground truth the detecting model cannot skip. Trivial offsets (0–2px
40
+ * hacks like body.iframe { padding-top: 1px }) don't qualify. ERROR when
41
+ * the corresponding root_classes list is empty/absent; WARN when it is
42
+ * non-empty but missing a qualifying class (a deliberate judgment call —
43
+ * verify the class doesn't apply to the default authenticated page).
44
+ *
45
+ * Usage:
46
+ * node check_css_health.js <branding.css> [view files…] \
47
+ * [--project-map <project_map.json>] [--project-dir <dir>] \
48
+ * [--soft] [--json <report.json>]
49
+ *
50
+ * Exit codes: 0 healthy (or --soft), 1 gate failed, 2 usage error.
51
+ * Node builtins only — no dependencies (detect-project has no package.json deps).
52
+ */
53
+
54
+ const fs = require('fs');
55
+ const path = require('path');
56
+
57
+ // ─── Coverage thresholds ─────────────────────────────────────────────────────
58
+ // A view participates in the gate only when it declares enough distinct tokens
59
+ // to be meaningful. CSS-in-JS / hashed-class projects extract few or no literal
60
+ // tokens and are skipped rather than false-failed.
61
+ const MIN_TOKENS_FOR_GATE = 8;
62
+ const FAIL_RATIO = 0.5;
63
+ const WARN_RATIO = 0.75;
64
+ const MAX_VIEWS = 12;
65
+ const MAX_VIEW_BYTES = 512 * 1024;
66
+ // Undefined vars: 1–2 stray no-fallback usages may be an intentional optional
67
+ // hook; 3+ means a consumed theme/scheme file is missing its definitions.
68
+ const VAR_ERROR_MIN_USES = 3;
69
+
70
+ const MARKUP_EXTENSIONS = new Set([
71
+ '.html', '.htm', '.xhtml', '.erb', '.rhtml', '.haml', '.slim',
72
+ '.php', '.phtml', '.twig', '.blade.php',
73
+ '.heex', '.leex', '.eex', '.ex',
74
+ '.tsx', '.jsx', '.js', '.mjs', '.vue', '.svelte', '.astro',
75
+ '.ejs', '.hbs', '.handlebars', '.mustache', '.njk', '.liquid',
76
+ '.cshtml', '.razor', '.pug', '.jade',
77
+ ]);
78
+
79
+ function stripCssNoise(css) {
80
+ return css
81
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
82
+ .replace(/url\(\s*(?:'[^']*'|"[^"]*"|[^)]*)\)/gi, 'url()');
83
+ }
84
+
85
+ // ─── Check 3: root offset classes ────────────────────────────────────────────
86
+ // A padding-top value counts as a real chrome offset unless it is a reset/hack:
87
+ // 0, 1–2px, or a non-length keyword. var()/rem/em/≥3px all qualify.
88
+ function isTrivialOffset(value) {
89
+ const v = value.replace(/!important/gi, '').trim().toLowerCase();
90
+ if (['initial', 'unset', 'inherit', 'revert', 'auto', ''].includes(v)) return true;
91
+ return /^0(\.0+)?(px|rem|em|%|vh|vw)?$/.test(v) || /^[12](\.\d+)?px$/.test(v);
92
+ }
93
+
94
+ // Scan innermost CSS rules (the regex naturally skips at-rule wrappers) for
95
+ // html.<class> / body.<class> selectors whose declarations set a non-trivial
96
+ // padding-top. Returns { html: Set<class>, body: Set<class> }.
97
+ function findRootOffsetClasses(css) {
98
+ const found = { html: new Set(), body: new Set() };
99
+ for (const m of css.matchAll(/([^{};]+)\{([^{}]*)\}/g)) {
100
+ const [, selector, decls] = m;
101
+ const pt = decls.match(/(?:^|;)\s*padding-top\s*:\s*([^;]+)/i);
102
+ if (!pt || isTrivialOffset(pt[1])) continue;
103
+ for (const sm of selector.matchAll(/(?:^|[,\s])(html|body)\.([A-Za-z0-9_-]+)/g)) {
104
+ found[sm[1]].add(sm[2]);
105
+ }
106
+ }
107
+ return found;
108
+ }
109
+
110
+ function analyzeRootOffsetClasses(css, projectMap) {
111
+ const out = { errors: [], warnings: [], candidates: { html: [], body: [] } };
112
+ const shell = projectMap && projectMap.app_shell;
113
+ if (!shell || typeof shell !== 'object') return out; // no persistent shell — nothing to offset
114
+ const offsets = findRootOffsetClasses(css);
115
+ const rc = shell.root_classes || {};
116
+ for (const root of ['html', 'body']) {
117
+ const candidates = [...offsets[root]].sort();
118
+ out.candidates[root] = candidates;
119
+ if (!candidates.length) continue;
120
+ const recorded = new Set(Array.isArray(rc[root]) ? rc[root] : []);
121
+ const missing = candidates.filter(c => !recorded.has(c));
122
+ if (!missing.length) continue;
123
+ const msg = `branding.css scopes a fixed-chrome offset to ${root}.` +
124
+ `${missing.join(` / ${root}.`)} (padding-top rule) but app_shell.root_classes.${root} ` +
125
+ `${recorded.size ? `records [${[...recorded].join(', ')}] without it` : 'is empty'} — ` +
126
+ `if the default authenticated page carries the class, mockups without it render the ` +
127
+ `fixed bar OVER the content`;
128
+ if (recorded.size) out.warnings.push(msg);
129
+ else out.errors.push(msg);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ // ─── Check 1: custom properties ──────────────────────────────────────────────
135
+ function analyzeCustomProperties(css) {
136
+ const defined = new Set();
137
+ for (const m of css.matchAll(/(?:^|[{;\s])(--[A-Za-z0-9_-]+)\s*:/g)) defined.add(m[1]);
138
+ for (const m of css.matchAll(/@property\s+(--[A-Za-z0-9_-]+)/g)) defined.add(m[1]);
139
+
140
+ // Count var(--x) only inside CONCRETE declarations (property not itself a
141
+ // custom prop). A var() inside another custom property's value resolves
142
+ // lazily and is often intentionally undefined — Tailwind v3's
143
+ // `--tw-shadow-colored: … var(--tw-shadow-color)` idiom would otherwise
144
+ // false-fail every v3 project. var(--x, …) with a fallback is always fine.
145
+ const noFallback = new Map();
146
+ const withFallback = new Map();
147
+ for (const d of css.matchAll(/(?:^|[{;])\s*([A-Za-z-][A-Za-z0-9_-]*)\s*:\s*([^;{}]*)/g)) {
148
+ if (d[1].startsWith('--')) continue;
149
+ for (const m of d[2].matchAll(/var\(\s*(--[A-Za-z0-9_-]+)\s*([,)])/g)) {
150
+ const bucket = m[2] === ')' ? noFallback : withFallback;
151
+ bucket.set(m[1], (bucket.get(m[1]) || 0) + 1);
152
+ }
153
+ }
154
+
155
+ const errors = [];
156
+ const warnings = [];
157
+ for (const [name, uses] of [...noFallback.entries()].sort((a, b) => b[1] - a[1])) {
158
+ if (defined.has(name)) continue;
159
+ const msg = `var(${name}) used ${uses}x without a fallback but never defined — ` +
160
+ `every declaration consuming it is invalid at computed-value time`;
161
+ if (uses >= VAR_ERROR_MIN_USES) errors.push(msg);
162
+ else warnings.push(msg);
163
+ }
164
+ return { defined, errors, warnings };
165
+ }
166
+
167
+ // ─── Check 2: view selector coverage ─────────────────────────────────────────
168
+ // One scan of the CSS builds the sets of class/id names that appear in selector
169
+ // position (Tailwind's escaped selectors like `.lg\:grid-cols-3` are unescaped).
170
+ // Number/hex noise from declarations ("1.5s" → "5s", "#fff") is harmless: no
171
+ // real template token looks like it.
172
+ function buildSelectorSets(css) {
173
+ const classes = new Set();
174
+ const ids = new Set();
175
+ for (const m of css.matchAll(/\.((?:[A-Za-z0-9_-]|\\[^\s])+)/g)) {
176
+ classes.add(m[1].replace(/\\(.)/g, '$1'));
177
+ }
178
+ for (const m of css.matchAll(/#((?:[A-Za-z0-9_-]|\\[^\s])+)/g)) {
179
+ ids.add(m[1].replace(/\\(.)/g, '$1'));
180
+ }
181
+ return { classes, ids };
182
+ }
183
+
184
+ function stripTemplateInterpolation(value) {
185
+ return value
186
+ .replace(/<%[\s\S]*?%>/g, ' ') // ERB
187
+ .replace(/<\?(?:php)?[\s\S]*?(?:\?>|$)/g, ' ') // PHP
188
+ .replace(/\{\{[\s\S]*?\}\}/g, ' ') // mustache/vue/liquid
189
+ .replace(/\{%[\s\S]*?%\}/g, ' ') // twig/liquid tags
190
+ .replace(/\$\{[\s\S]*?\}/g, ' ') // JS template literal
191
+ .replace(/#\{[\s\S]*?\}/g, ' '); // ruby/haml
192
+ }
193
+
194
+ function collectTokens(value, into) {
195
+ for (const raw of stripTemplateInterpolation(value).split(/\s+/)) {
196
+ const t = raw.trim();
197
+ if (!t || t.length > 64) continue;
198
+ if (/[<>="'`;{}]/.test(t)) continue; // interpolation shrapnel
199
+ if (/[-:(]$/.test(t)) continue; // truncated by stripped interp
200
+ if (!/^-?[A-Za-z_]/.test(t)) continue; // classes start with a letter/_/-
201
+ into.add(t);
202
+ }
203
+ }
204
+
205
+ function extractViewTokens(text) {
206
+ const classes = new Set();
207
+ const ids = new Set();
208
+ for (const m of text.matchAll(/\bclass(?:Name)?\s*=\s*(["'])([\s\S]*?)\1/g)) {
209
+ collectTokens(m[2], classes);
210
+ }
211
+ // className={clsx('a', cond && 'b')} — string literals inside the braces
212
+ for (const m of text.matchAll(/\bclassName\s*=\s*\{([^}]*)\}/g)) {
213
+ for (const s of m[1].matchAll(/(["'`])([^"'`]*)\1/g)) collectTokens(s[2], classes);
214
+ }
215
+ for (const m of text.matchAll(/\bid\s*=\s*(["'])([^"']*)\1/g)) {
216
+ const t = stripTemplateInterpolation(m[2]).trim();
217
+ if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(t)) ids.add(t);
218
+ }
219
+ return { classes, ids };
220
+ }
221
+
222
+ function coverageForView(viewPath, text, selectorSets) {
223
+ const { classes, ids } = extractViewTokens(text);
224
+ const unmatched = [];
225
+ let matched = 0;
226
+ for (const c of classes) {
227
+ if (selectorSets.classes.has(c)) matched++;
228
+ else unmatched.push('.' + c);
229
+ }
230
+ for (const i of ids) {
231
+ if (selectorSets.ids.has(i)) matched++;
232
+ else unmatched.push('#' + i);
233
+ }
234
+ const total = classes.size + ids.size;
235
+ return { view: viewPath, total, matched, unmatched };
236
+ }
237
+
238
+ // ─── View sampling from project_map.json ─────────────────────────────────────
239
+ function looksLikeMarkupFile(p) {
240
+ if (typeof p !== 'string' || !p || p.startsWith('http')) return false;
241
+ const lower = p.toLowerCase();
242
+ if (lower.endsWith('.blade.php')) return true;
243
+ return MARKUP_EXTENSIONS.has(path.extname(lower));
244
+ }
245
+
246
+ function collectViewsFromMap(pm, projectDir) {
247
+ const candidates = [];
248
+ const push = (p) => { if (looksLikeMarkupFile(p)) candidates.push(p); };
249
+
250
+ const routeIndex = pm.route_index || {};
251
+ for (const entry of Object.values(routeIndex)) {
252
+ if (!entry || typeof entry !== 'object') continue;
253
+ push(entry.primary_view);
254
+ push(entry.view);
255
+ push(entry.definition_file);
256
+ push(entry.entry);
257
+ }
258
+ for (const layout of pm.layouts || []) {
259
+ push(typeof layout === 'string' ? layout : layout && layout.path);
260
+ }
261
+ for (const chrome of pm.global_chrome || []) {
262
+ push(typeof chrome === 'string' ? chrome : chrome && chrome.path);
263
+ }
264
+ if (pm.app_shell && typeof pm.app_shell === 'object') {
265
+ push(pm.app_shell.definition_file);
266
+ }
267
+
268
+ const seen = new Set();
269
+ const out = [];
270
+ for (const rel of candidates) {
271
+ if (seen.has(rel)) continue;
272
+ seen.add(rel);
273
+ const abs = path.isAbsolute(rel) ? rel : path.join(projectDir, rel);
274
+ try {
275
+ const st = fs.statSync(abs);
276
+ if (st.isFile() && st.size <= MAX_VIEW_BYTES) out.push({ rel, abs });
277
+ } catch { /* listed but missing — skip */ }
278
+ if (out.length >= MAX_VIEWS) break;
279
+ }
280
+ return out;
281
+ }
282
+
283
+ // ─── Main ────────────────────────────────────────────────────────────────────
284
+ function main() {
285
+ const args = process.argv.slice(2);
286
+ let cssPath = null;
287
+ let projectMapPath = null;
288
+ let projectDir = process.cwd();
289
+ let soft = false;
290
+ let jsonOut = null;
291
+ const explicitViews = [];
292
+
293
+ for (let i = 0; i < args.length; i++) {
294
+ const a = args[i];
295
+ if (a === '--project-map') projectMapPath = args[++i];
296
+ else if (a === '--project-dir') projectDir = args[++i];
297
+ else if (a === '--soft') soft = true;
298
+ else if (a === '--json') jsonOut = args[++i];
299
+ else if (!cssPath) cssPath = a;
300
+ else explicitViews.push(a);
301
+ }
302
+
303
+ if (!cssPath || !fs.existsSync(cssPath)) {
304
+ console.error('Usage: check_css_health.js <branding.css> [view files…] ' +
305
+ '[--project-map <project_map.json>] [--project-dir <dir>] [--soft] [--json <out>]');
306
+ process.exit(2);
307
+ }
308
+
309
+ const css = stripCssNoise(fs.readFileSync(cssPath, 'utf8'));
310
+
311
+ // Check 1 — custom properties
312
+ const vars = analyzeCustomProperties(css);
313
+ console.log('== CSS health: custom properties ==');
314
+ if (!vars.errors.length && !vars.warnings.length) {
315
+ console.log('[ OK ] every var(--x) consumed without a fallback is defined');
316
+ }
317
+ for (const e of vars.errors) console.log(`[FAIL] ERROR: ${e}`);
318
+ for (const w of vars.warnings) console.log(`[WARN] WARN: ${w}`);
319
+
320
+ // Check 2 — view selector coverage
321
+ let pm = null;
322
+ let views = explicitViews
323
+ .map(rel => ({ rel, abs: path.isAbsolute(rel) ? rel : path.join(projectDir, rel) }))
324
+ .filter(v => fs.existsSync(v.abs));
325
+ if (projectMapPath && fs.existsSync(projectMapPath)) {
326
+ try {
327
+ pm = JSON.parse(fs.readFileSync(projectMapPath, 'utf8'));
328
+ const sampled = collectViewsFromMap(pm, projectDir);
329
+ const have = new Set(views.map(v => v.rel));
330
+ for (const v of sampled) if (!have.has(v.rel)) views.push(v);
331
+ } catch (e) {
332
+ console.log(`[WARN] WARN: could not parse project map ${projectMapPath}: ${e.message}`);
333
+ }
334
+ }
335
+ views = views.slice(0, MAX_VIEWS);
336
+
337
+ console.log('== CSS health: view selector coverage ==');
338
+ const results = [];
339
+ if (!views.length) {
340
+ console.log('[SKIP] no view/template files to probe (empty route_index/layouts and no view args) — coverage not verified');
341
+ }
342
+ const selectorSets = buildSelectorSets(css);
343
+ let coverageErrors = 0;
344
+ for (const v of views) {
345
+ const text = fs.readFileSync(v.abs, 'utf8');
346
+ const r = coverageForView(v.rel, text, selectorSets);
347
+ results.push(r);
348
+ const pct = r.total ? Math.round((r.matched / r.total) * 100) : 100;
349
+ const sample = r.unmatched.slice(0, 8).join(', ') + (r.unmatched.length > 8 ? ', …' : '');
350
+ if (r.total < MIN_TOKENS_FOR_GATE) {
351
+ console.log(`[SKIP] ${r.view}: only ${r.total} class/id token(s) extracted — too few to judge`);
352
+ } else if (r.matched / r.total < FAIL_RATIO) {
353
+ coverageErrors++;
354
+ console.log(`[FAIL] ${r.view}: ${r.matched}/${r.total} tokens styled (${pct}%) — unmatched: ${sample}`);
355
+ } else if (r.matched / r.total < WARN_RATIO) {
356
+ console.log(`[WARN] ${r.view}: ${r.matched}/${r.total} tokens styled (${pct}%) — unmatched: ${sample}`);
357
+ } else {
358
+ console.log(`[ OK ] ${r.view}: ${r.matched}/${r.total} tokens styled (${pct}%)`);
359
+ }
360
+ }
361
+
362
+ // Check 3 — root offset classes (needs the map's app_shell; skips without it)
363
+ console.log('== CSS health: root offset classes ==');
364
+ const rootOffsets = analyzeRootOffsetClasses(css, pm);
365
+ if (!pm || !pm.app_shell) {
366
+ console.log('[SKIP] no project map / app_shell — root offset classes not verified');
367
+ } else if (!rootOffsets.errors.length && !rootOffsets.warnings.length) {
368
+ const n = rootOffsets.candidates.html.length + rootOffsets.candidates.body.length;
369
+ console.log(n
370
+ ? `[ OK ] every root-scoped offset class is recorded in app_shell.root_classes (${n} candidate(s))`
371
+ : '[ OK ] no root-scoped fixed-chrome offset rules in branding.css');
372
+ }
373
+ for (const e of rootOffsets.errors) console.log(`[FAIL] ERROR: ${e}`);
374
+ for (const w of rootOffsets.warnings) console.log(`[WARN] WARN: ${w}`);
375
+
376
+ const failed = vars.errors.length > 0 || coverageErrors > 0 || rootOffsets.errors.length > 0;
377
+ if (jsonOut) {
378
+ fs.writeFileSync(jsonOut, JSON.stringify({
379
+ css: cssPath,
380
+ undefined_vars: { errors: vars.errors, warnings: vars.warnings },
381
+ views: results,
382
+ root_offset_classes: rootOffsets,
383
+ failed,
384
+ soft,
385
+ }, null, 2));
386
+ }
387
+
388
+ if (failed) {
389
+ console.log('');
390
+ console.log('STOP: CSS HEALTH GATE FAILED — branding.css does not fully style this project.');
391
+ console.log('Repair the bundle, then re-run this check until it passes:');
392
+ console.log('- [FAIL] coverage view(s): the project loads stylesheets per page/surface. Find the');
393
+ console.log(' stylesheet(s) THOSE views actually load — their <link>/enqueue/import wiring, not');
394
+ console.log(' the main bundle\'s import list — and append them to branding.css.');
395
+ console.log('- undefined vars: append the theme/scheme/tokens stylesheet that defines them (often');
396
+ console.log(' a build-emitted file), or define them in a :root block with the real detected values.');
397
+ console.log('- root offset classes: the CSS proves a fixed-chrome offset hangs on a root state class');
398
+ console.log(' the map does not record. Chase the root-class emission (the layout, or the helper it');
399
+ console.log(' calls) and record the resolved DEFAULT-user tokens in app_shell.root_classes, then');
400
+ console.log(' re-run this check. This is a project_map.json repair, not a CSS repair.');
401
+ console.log('- re-run sanitize_css.js after appending CSS, then re-run this check.');
402
+ if (soft) {
403
+ console.log('(soft mode: fallback_synthesis is the known-degraded floor — reporting only, not failing)');
404
+ process.exit(0);
405
+ }
406
+ process.exit(1);
407
+ }
408
+ console.log('\nCSS health gate passed.');
409
+ process.exit(0);
410
+ }
411
+
412
+ main();
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * check_project_map.js — validate project_map.json for an app type before the
6
+ * summary step. Replaces the jq gates, and names every failing field instead
7
+ * of printing a bare "false".
8
+ *
9
+ * Usage:
10
+ * node check_project_map.js <web|terminal|mobile|desktop|win32|macos|game> <project_map.json> [codebase_dir]
11
+ *
12
+ * For web, a codebase_dir containing config/routes.rb also requires a
13
+ * non-empty route_index. Exits 0 when valid, 1 with one problem per line.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const PLACEMENTS = new Set(['top', 'left', 'right', 'bottom', 'canvas', 'overlay-left', 'overlay-right', 'overlay-center', 'overlay-bottom']);
20
+ const STATE_KINDS = new Set(['overview', 'action', 'confirm']);
21
+ const PRIORITIES = new Set(['orientation', 'primary-action', 'secondary-action', 'terminal-action']);
22
+
23
+ const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ const isString = value => typeof value === 'string';
25
+ const nonEmptyString = value => isString(value) && value.length > 0;
26
+ const nonEmptyArray = value => Array.isArray(value) && value.length > 0;
27
+ const nonEmptyObject = value => isObject(value) && Object.keys(value).length > 0;
28
+
29
+ function checkRuntimeChrome(chrome, where, problems) {
30
+ const fail = message => problems.push(`${where}: ${message}`);
31
+ if (chrome.source_kind !== 'repo' && chrome.source_kind !== 'knowledge') fail('source_kind must be "repo" or "knowledge"');
32
+ if (!Array.isArray(chrome.source_files)) fail('source_files must be an array');
33
+ if (!nonEmptyArray(chrome.regions)) fail('regions must be a non-empty array');
34
+ if (!nonEmptyArray(chrome.states)) fail('states must be a non-empty array');
35
+
36
+ (Array.isArray(chrome.regions) ? chrome.regions : []).forEach((region, i) => {
37
+ const at = `regions[${i}]`;
38
+ if (!isObject(region)) return fail(`${at} must be an object`);
39
+ if (!nonEmptyString(region.id)) fail(`${at}.id must be a non-empty string`);
40
+ if (!PLACEMENTS.has(region.placement)) fail(`${at}.placement must be one of ${[...PLACEMENTS].join(', ')}`);
41
+ if (typeof region.required !== 'boolean') fail(`${at}.required must be a boolean`);
42
+ if (!nonEmptyString(region.appearance)) fail(`${at}.appearance must be a non-empty string`);
43
+ if (!nonEmptyArray(region.items)) fail(`${at}.items must be a non-empty array`);
44
+ (Array.isArray(region.items) ? region.items : []).forEach((item, j) => {
45
+ const it = `${at}.items[${j}]`;
46
+ if (!isObject(item)) return fail(`${it} must be an object`);
47
+ for (const key of ['id', 'kind', 'description']) if (!nonEmptyString(item[key])) fail(`${it}.${key} must be a non-empty string`);
48
+ if (!Array.isArray(item.required_strings)) fail(`${it}.required_strings must be an array`);
49
+ });
50
+ if (!Array.isArray(region.required_strings)) fail(`${at}.required_strings must be an array`);
51
+ if (!Array.isArray(region.source_files)) fail(`${at}.source_files must be an array`);
52
+ });
53
+
54
+ const states = Array.isArray(chrome.states) ? chrome.states : [];
55
+ states.forEach((state, i) => {
56
+ const at = `states[${i}]`;
57
+ if (!isObject(state)) return fail(`${at} must be an object`);
58
+ if (!nonEmptyString(state.id)) fail(`${at}.id must be a non-empty string`);
59
+ if (!STATE_KINDS.has(state.kind)) fail(`${at}.kind must be one of ${[...STATE_KINDS].join(', ')}`);
60
+ if (!nonEmptyArray(state.topic_tags)) fail(`${at}.topic_tags must be a non-empty array`);
61
+ if (!PRIORITIES.has(state.instructional_priority)) fail(`${at}.instructional_priority must be one of ${[...PRIORITIES].join(', ')}`);
62
+ if (!nonEmptyString(state.canvas_presentation)) fail(`${at}.canvas_presentation must be a non-empty string`);
63
+ if ((state.kind === 'action' || state.kind === 'confirm') && !nonEmptyString(state.context_label)) {
64
+ fail(`${at}.context_label must be a non-empty string for ${state.kind} states`);
65
+ }
66
+ if (typeof state.screenshot_required !== 'boolean') fail(`${at}.screenshot_required must be a boolean`);
67
+ if (!nonEmptyArray(state.visible_regions)) fail(`${at}.visible_regions must be a non-empty array`);
68
+ for (const key of ['required_regions', 'required_strings', 'source_files']) {
69
+ if (!Array.isArray(state[key])) fail(`${at}.${key} must be an array`);
70
+ }
71
+ });
72
+ const screenshots = states.filter(state => isObject(state) && state.screenshot_required).length;
73
+ if (screenshots > 3) fail(`at most 3 states may set screenshot_required (found ${screenshots})`);
74
+ }
75
+
76
+ function checkIndexedApp(map, appType, indexKey, metadataKey, problems) {
77
+ if (!nonEmptyObject(map[indexKey])) problems.push(`${indexKey} must be a non-empty object`);
78
+ if (!isObject(map[metadataKey])) problems.push(`${metadataKey} must be an object`);
79
+ if (!Object.hasOwn(map, 'app_shell')) problems.push('app_shell is missing');
80
+ }
81
+
82
+ const CHECKS = {
83
+ web(map, problems, codebase) {
84
+ if (map.schema_version !== 5) problems.push('schema_version must be 5');
85
+ if (!isObject(map.dir_map)) problems.push('dir_map must be an object');
86
+ if (!nonEmptyArray(map.layouts)) problems.push('layouts must be a non-empty array');
87
+ (Array.isArray(map.layouts) ? map.layouts : []).forEach((layout, i) => {
88
+ if (isObject(layout) && layout.runtime_chrome != null) {
89
+ if (!isObject(layout.runtime_chrome)) problems.push(`layouts[${i}].runtime_chrome must be an object`);
90
+ else checkRuntimeChrome(layout.runtime_chrome, `layouts[${i}].runtime_chrome`, problems);
91
+ }
92
+ });
93
+ if (codebase && fs.existsSync(path.join(codebase, 'config', 'routes.rb')) && !nonEmptyObject(map.route_index)) {
94
+ problems.push('route_index must be a non-empty object (config/routes.rb exists)');
95
+ }
96
+ },
97
+ terminal(map, problems) {
98
+ if (!nonEmptyObject(map.command_index)) problems.push('command_index must be a non-empty object');
99
+ if (!isObject(map.cli_metadata)) problems.push('cli_metadata must be an object');
100
+ },
101
+ mobile: (map, problems) => checkIndexedApp(map, 'mobile', 'screen_index', 'mobile_metadata', problems),
102
+ desktop: (map, problems) => checkIndexedApp(map, 'desktop', 'route_index', 'desktop_metadata', problems),
103
+ win32: (map, problems) => checkIndexedApp(map, 'win32', 'dialog_index', 'win32_metadata', problems),
104
+ macos: (map, problems) => checkIndexedApp(map, 'macos', 'view_index', 'macos_metadata', problems),
105
+ game(map, problems) {
106
+ if (!isObject(map.game_metadata)) problems.push('game_metadata must be an object');
107
+ if (!nonEmptyObject(map.route_index)) problems.push('route_index must be a non-empty object');
108
+ const canvas = Object.entries(isObject(map.route_index) ? map.route_index : {})
109
+ .filter(([, entry]) => isObject(entry) && entry.method === 'CANVAS');
110
+ if (!canvas.length) problems.push('route_index must contain at least one CANVAS state');
111
+ for (const [key, entry] of canvas) {
112
+ if (!nonEmptyArray(entry.scene_anatomy?.layers)) problems.push(`route_index.${key}.scene_anatomy.layers must be a non-empty array`);
113
+ }
114
+ if (!Object.hasOwn(map, 'app_shell')) problems.push('app_shell is missing');
115
+ },
116
+ };
117
+
118
+ function check(mode, map, codebase) {
119
+ const problems = [];
120
+ if (!isObject(map)) return ['project_map must be a JSON object'];
121
+ if (map.app_type !== mode) problems.push(`app_type must be "${mode}" (found ${JSON.stringify(map.app_type)})`);
122
+ if (!isString(map.app_type_source)) problems.push('app_type_source must be a string');
123
+ CHECKS[mode](map, problems, codebase);
124
+ return problems;
125
+ }
126
+
127
+ function main(argv) {
128
+ const [mode, file, codebase] = argv;
129
+ if (!CHECKS[mode] || !file) {
130
+ process.stderr.write(`Usage: check_project_map.js <${Object.keys(CHECKS).join('|')}> <project_map.json> [codebase_dir]\n`);
131
+ return 2;
132
+ }
133
+ let map;
134
+ try { map = JSON.parse(fs.readFileSync(file, 'utf8')); }
135
+ catch (error) {
136
+ process.stderr.write(`${file}: ${error.message}\n`);
137
+ return 1;
138
+ }
139
+ const problems = check(mode, map, codebase);
140
+ if (problems.length) {
141
+ process.stderr.write(`project_map.json is not valid for ${mode}:\n${problems.map(p => ` - ${p}`).join('\n')}\n`);
142
+ return 1;
143
+ }
144
+ process.stdout.write(`project_map.json OK (${mode})\n`);
145
+ return 0;
146
+ }
147
+
148
+ if (require.main === module) process.exitCode = main(process.argv.slice(2));
149
+
150
+ module.exports = { check };