@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.
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +392 -2
- package/dist/actions.d.ts +98 -0
- package/dist/actions.js +86 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-settings.d.ts +22 -0
- package/dist/agent-settings.js +35 -0
- package/dist/agent-settings.js.map +1 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +159 -0
- package/dist/api.js.map +1 -0
- package/dist/article-link.d.ts +16 -0
- package/dist/article-link.js +25 -0
- package/dist/article-link.js.map +1 -0
- package/dist/artifacts.d.ts +45 -0
- package/dist/artifacts.js +144 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +9 -0
- package/dist/brand.js.map +1 -0
- package/dist/bridge.d.ts +2964 -0
- package/dist/bridge.js +1036 -0
- package/dist/bridge.js.map +1 -0
- package/dist/capacity.d.ts +5 -0
- package/dist/capacity.js +11 -0
- package/dist/capacity.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +88 -0
- package/dist/credentials.js.map +1 -0
- package/dist/development-tls.d.ts +8 -0
- package/dist/development-tls.js +38 -0
- package/dist/development-tls.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +11 -0
- package/dist/export.js +91 -0
- package/dist/export.js.map +1 -0
- package/dist/hosted-operations.d.ts +108 -0
- package/dist/hosted-operations.js +120 -0
- package/dist/hosted-operations.js.map +1 -0
- package/dist/hosting-benefits.d.ts +66 -0
- package/dist/hosting-benefits.js +67 -0
- package/dist/hosting-benefits.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/local-inventory.d.ts +22 -0
- package/dist/local-inventory.js +59 -0
- package/dist/local-inventory.js.map +1 -0
- package/dist/local-setup.d.ts +208 -0
- package/dist/local-setup.js +140 -0
- package/dist/local-setup.js.map +1 -0
- package/dist/pairing.d.ts +71 -0
- package/dist/pairing.js +235 -0
- package/dist/pairing.js.map +1 -0
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +44 -0
- package/dist/preferences.js.map +1 -0
- package/dist/progress.d.ts +205 -0
- package/dist/progress.js +224 -0
- package/dist/progress.js.map +1 -0
- package/dist/reminders.d.ts +31 -0
- package/dist/reminders.js +73 -0
- package/dist/reminders.js.map +1 -0
- package/dist/replace-connection.d.ts +6 -0
- package/dist/replace-connection.js +77 -0
- package/dist/replace-connection.js.map +1 -0
- package/dist/repository-actions.d.ts +18 -0
- package/dist/repository-actions.js +9 -0
- package/dist/repository-actions.js.map +1 -0
- package/dist/repository-benefits.d.ts +96 -0
- package/dist/repository-benefits.js +64 -0
- package/dist/repository-benefits.js.map +1 -0
- package/dist/run-update.d.ts +17 -0
- package/dist/run-update.js +27 -0
- package/dist/run-update.js.map +1 -0
- package/dist/runs.d.ts +541 -0
- package/dist/runs.js +146 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +277 -0
- package/dist/schema.js +66 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +1714 -0
- package/dist/session.js +619 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.js +21 -0
- package/dist/settings.js.map +1 -0
- package/dist/sync.d.ts +495 -0
- package/dist/sync.js +191 -0
- package/dist/sync.js.map +1 -0
- package/dist/telemetry-scrub.d.ts +13 -0
- package/dist/telemetry-scrub.js +51 -0
- package/dist/telemetry-scrub.js.map +1 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +173 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/walkthroughs.d.ts +224 -0
- package/dist/walkthroughs.js +109 -0
- package/dist/walkthroughs.js.map +1 -0
- package/dist/workspace.d.ts +14 -0
- package/dist/workspace.js +127 -0
- package/dist/workspace.js.map +1 -0
- package/dist/writer-agent.d.ts +21 -0
- package/dist/writer-agent.js +27 -0
- package/dist/writer-agent.js.map +1 -0
- package/dist/writer-entry.d.ts +171 -0
- package/dist/writer-entry.js +233 -0
- package/dist/writer-entry.js.map +1 -0
- package/dist/writing-style.d.ts +7 -0
- package/dist/writing-style.js +52 -0
- package/dist/writing-style.js.map +1 -0
- package/engine/SYNC.json +4 -0
- package/engine/VERSION +1 -0
- package/engine/detect-project/README.md +141 -0
- package/engine/detect-project/SKILL.md +1421 -0
- package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
- package/engine/detect-project/assets/game/game-frame.css +132 -0
- package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
- package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/macosui/fonts.css +15 -0
- package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
- package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
- package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
- package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
- package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
- package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
- package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
- package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
- package/engine/detect-project/assets/mobileui/fonts.css +16 -0
- package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
- package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
- package/engine/detect-project/assets/mobileui/icons.css +31 -0
- package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
- package/engine/detect-project/assets/mobileui/platforms.json +46 -0
- package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
- package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
- package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
- package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
- package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
- package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
- package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
- package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
- package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
- package/engine/detect-project/assets/webtui/themes.json +37 -0
- package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
- package/engine/detect-project/assets/win32ui/7css.css +2 -0
- package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
- package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
- package/engine/detect-project/package-lock.json +12 -0
- package/engine/detect-project/package.json +10 -0
- package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
- package/engine/detect-project/scripts/check_css_health.js +412 -0
- package/engine/detect-project/scripts/check_project_map.js +150 -0
- package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
- package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
- package/engine/detect-project/scripts/classify_app_type.sh +246 -0
- package/engine/detect-project/scripts/classify_surface.sh +95 -0
- package/engine/detect-project/scripts/classify_workspace.js +39 -0
- package/engine/detect-project/scripts/compile_css.sh +447 -0
- package/engine/detect-project/scripts/detect_static.js +645 -0
- package/engine/detect-project/scripts/detect_structure.js +187 -0
- package/engine/detect-project/scripts/include_census.js +451 -0
- package/engine/detect-project/scripts/json_get.js +142 -0
- package/engine/detect-project/scripts/merge_json.js +52 -0
- package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
- package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
- package/engine/detect-project/scripts/run_css_build.sh +40 -0
- package/engine/detect-project/scripts/sanitize_css.js +83 -0
- package/engine/detect-project/scripts/test_classify_surface.js +108 -0
- package/engine/detect-project/scripts/test_node_helpers.js +142 -0
- package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
- package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
- package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
- package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
- package/engine/detect-project/scripts/theme_overrides.js +169 -0
- package/engine/detect-project/scripts/write_branding.js +129 -0
- package/engine/generate-illustrated-article/SKILL.md +461 -0
- package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
- package/engine/generate-illustrated-article/contracts/game.md +38 -0
- package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
- package/engine/generate-illustrated-article/contracts/macos.md +79 -0
- package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
- package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
- package/engine/generate-illustrated-article/contracts/win32.md +43 -0
- package/engine/generate-illustrated-article/package-lock.json +366 -0
- package/engine/generate-illustrated-article/package.json +15 -0
- package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
- package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
- package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
- package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
- package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
- package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
- package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
- package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
- package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
- package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
- package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
- package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
- package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
- package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
- package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
- package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
- package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
- package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
- package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
- package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
- package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
- package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
- package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
- package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
- package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
- package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
- package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
- package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
- package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
- package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
- package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
- package/install.sh +14 -0
- package/package.json +57 -4
- package/scripts/auth.mjs +33 -0
- package/scripts/auto-update.mjs +7 -0
- package/scripts/build-plugin.mjs +58 -0
- package/scripts/build-release.mjs +94 -0
- package/scripts/check-release.mjs +40 -0
- package/scripts/check-runtime.mjs +10 -0
- package/scripts/cli.mjs +8 -0
- package/scripts/install.mjs +64 -0
- package/scripts/lib/agent-runner.mjs +181 -0
- package/scripts/lib/agent-settings.mjs +215 -0
- package/scripts/lib/article-skills.mjs +82 -0
- package/scripts/lib/auto-update.mjs +50 -0
- package/scripts/lib/brand.mjs +9 -0
- package/scripts/lib/browser.mjs +12 -0
- package/scripts/lib/claude-connection.mjs +33 -0
- package/scripts/lib/claude-permissions.mjs +47 -0
- package/scripts/lib/claude-plugin.mjs +15 -0
- package/scripts/lib/claude-writer.mjs +10 -0
- package/scripts/lib/cli-main.mjs +85 -0
- package/scripts/lib/cli.mjs +888 -0
- package/scripts/lib/codex-config.mjs +51 -0
- package/scripts/lib/codex-integration.mjs +61 -0
- package/scripts/lib/codex-skill.mjs +34 -0
- package/scripts/lib/harness-models.mjs +109 -0
- package/scripts/lib/install.mjs +269 -0
- package/scripts/lib/managed-writer.mjs +38 -0
- package/scripts/lib/planning.mjs +263 -0
- package/scripts/lib/prepare-update.mjs +85 -0
- package/scripts/lib/refresh-writers.mjs +12 -0
- package/scripts/lib/remove.mjs +167 -0
- package/scripts/lib/renderer.mjs +34 -0
- package/scripts/lib/terminal.mjs +252 -0
- package/scripts/lib/uninit.mjs +72 -0
- package/scripts/lib/update.mjs +55 -0
- package/scripts/lib/writer-recovery.mjs +35 -0
- package/scripts/lib/yolo.mjs +362 -0
- package/scripts/plugin-session.mjs +65 -0
- package/scripts/prepare-update.mjs +12 -0
- package/scripts/publish-release.mjs +92 -0
- package/scripts/skills/supportpages/SKILL.md +120 -0
- package/scripts/smoke-release.mjs +101 -0
- 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 };
|