@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,311 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Detect high-confidence client-rendered application mounts in files referenced
5
+ // by project_map routes/layouts. Every distinct mount must own a runtime_chrome
6
+ // recipe or carry an explicit reasoned ignore entry.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ function die(message) {
12
+ console.error(`ERROR: ${message}`);
13
+ process.exit(2);
14
+ }
15
+
16
+ const mapPath = process.argv[2];
17
+ const projectDir = process.argv[3];
18
+ const jsonIndex = process.argv.indexOf('--json');
19
+ const reportPath = jsonIndex >= 0 ? process.argv[jsonIndex + 1] : null;
20
+ if (!mapPath || !projectDir) die('usage: check_runtime_coverage.js <project_map.json> <project_dir> [--json report.json]');
21
+
22
+ let projectMap;
23
+ try {
24
+ projectMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
25
+ } catch (err) {
26
+ die(`cannot read project map: ${err.message}`);
27
+ }
28
+
29
+ const mountSignals = [
30
+ ['initialize-app', /(?:\.|\b)initialize(?:Editor|App|Application)\s*\(/],
31
+ ['react-root', /\b(?:createRoot|hydrateRoot)\s*\(|ReactDOM\s*\.\s*(?:render|hydrate)\s*\(/],
32
+ ['vue-app', /\bcreateApp\s*\([^)]*\)\s*\.\s*mount\s*\(|\bnew\s+Vue\s*\(/],
33
+ ['angular-bootstrap', /\bangular\s*\.\s*bootstrap\s*\(/],
34
+ ['explicit-dom-mount', /\b(?:mount|hydrate)\s*\(\s*(?:document\.(?:getElementById|querySelector)|['"`]#[^'"`]+['"`])/],
35
+ ];
36
+
37
+ function norm(value) {
38
+ if (typeof value !== 'string' || !value.trim()) return null;
39
+ const clean = value.split(/[?#]/, 1)[0].replace(/^\.\//, '');
40
+ const abs = path.resolve(projectDir, clean);
41
+ const root = path.resolve(projectDir) + path.sep;
42
+ if (!abs.startsWith(root) || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
43
+ return path.relative(projectDir, abs).split(path.sep).join('/');
44
+ }
45
+
46
+ const sources = new Map();
47
+ function addSource(value, consumer) {
48
+ const rel = norm(value);
49
+ if (!rel) return;
50
+ if (!sources.has(rel)) sources.set(rel, new Set());
51
+ sources.get(rel).add(consumer);
52
+ }
53
+
54
+ for (const layout of Array.isArray(projectMap.layouts) ? projectMap.layouts : []) {
55
+ addSource(layout && layout.path, `layout:${layout && (layout.name || layout.path)}`);
56
+ }
57
+ for (const [key, route] of Object.entries(projectMap.route_index || {})) {
58
+ if (!route || typeof route !== 'object') continue;
59
+ for (const field of ['controller', 'primary_view', 'view', 'source_file']) addSource(route[field], `route:${key}`);
60
+ for (const rel of route.partials_expanded || []) addSource(rel, `route:${key}`);
61
+ }
62
+
63
+ const candidates = [];
64
+ for (const [rel, consumers] of sources) {
65
+ let source;
66
+ try {
67
+ const stat = fs.statSync(path.join(projectDir, rel));
68
+ if (stat.size > 2 * 1024 * 1024) continue;
69
+ source = fs.readFileSync(path.join(projectDir, rel), 'utf8');
70
+ } catch (_) {
71
+ continue;
72
+ }
73
+ const signals = mountSignals.filter(([, re]) => re.test(source)).map(([name]) => name);
74
+ if (signals.length) candidates.push({ source_file: rel, signals, consumers: [...consumers].sort() });
75
+ }
76
+
77
+ const layouts = Array.isArray(projectMap.layouts) ? projectMap.layouts : [];
78
+ const errors = [];
79
+ const schemaV3 = Number(projectMap.schema_version) >= 3;
80
+ const schemaV4 = Number(projectMap.schema_version) >= 4;
81
+ const schemaV5 = Number(projectMap.schema_version) >= 5;
82
+ const ignoredValue = (projectMap.runtime_surface_coverage || {}).ignored;
83
+ const ignored = ignoredValue === undefined ? [] : ignoredValue;
84
+ if (!Array.isArray(ignored)) errors.push('runtime_surface_coverage.ignored must be an array');
85
+ const validIgnored = Array.isArray(ignored) ? ignored : [];
86
+
87
+ function nonempty(value) {
88
+ return typeof value === 'string' && value.trim().length > 0;
89
+ }
90
+
91
+ function stringArray(value, { nonemptyArray = false } = {}) {
92
+ return Array.isArray(value)
93
+ && (!nonemptyArray || value.length > 0)
94
+ && value.every(nonempty);
95
+ }
96
+
97
+ function label(layout) {
98
+ return layout && (layout.name || layout.path) || '?';
99
+ }
100
+
101
+ // Schema v3 turns runtime_chrome from a descriptive hint into an enforceable
102
+ // rendering contract. Keep older cached maps working, but reject v3 recipes
103
+ // that can be satisfied by empty tags or that omit a load-bearing app state.
104
+ if (schemaV3) {
105
+ const kebab = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
106
+ for (const layout of layouts.filter(item => item && item.runtime_chrome)) {
107
+ const owner = label(layout);
108
+ const recipe = layout.runtime_chrome;
109
+ const regions = Array.isArray(recipe.regions) ? recipe.regions : [];
110
+ const states = Array.isArray(recipe.states) ? recipe.states : [];
111
+ const regionIds = regions.map(region => region && region.id);
112
+ const stateIds = states.map(state => state && state.id);
113
+
114
+ if (!nonempty(recipe.app)) errors.push(`runtime_chrome layout '${owner}' requires a non-empty app`);
115
+ if (!nonempty(recipe.mode_note)) errors.push(`runtime_chrome layout '${owner}' requires a non-empty mode_note`);
116
+ if (!stringArray(recipe.source_files, { nonemptyArray: true })) errors.push(
117
+ `runtime_chrome layout '${owner}' requires non-empty source_files`);
118
+ const mount = norm(layout.path);
119
+ const topSources = new Set((Array.isArray(recipe.source_files) ? recipe.source_files : [])
120
+ .map(norm).filter(Boolean));
121
+ if (!mount || !topSources.has(mount)) errors.push(
122
+ `runtime_chrome layout '${owner}' source_files must include its layout mount '${layout.path || '?'}'`);
123
+ if (!regions.length) errors.push(`runtime_chrome layout '${owner}' requires at least one region`);
124
+ if (!states.length) errors.push(`runtime_chrome layout '${owner}' requires at least one state`);
125
+
126
+ if (regionIds.some(id => !nonempty(id) || !kebab.test(id))
127
+ || new Set(regionIds).size !== regionIds.length) errors.push(
128
+ `runtime_chrome layout '${owner}' requires unique kebab-case region ids`);
129
+ if (stateIds.some(id => !nonempty(id) || !kebab.test(id))
130
+ || new Set(stateIds).size !== stateIds.length) errors.push(
131
+ `runtime_chrome layout '${owner}' requires unique kebab-case state ids`);
132
+
133
+ for (const region of regions) {
134
+ const id = region && region.id || '?';
135
+ if (!region || !nonempty(region.region)) errors.push(`runtime region '${id}' requires a non-empty region label`);
136
+ if (schemaV4) {
137
+ const items = region && region.items;
138
+ const itemIds = Array.isArray(items) ? items.map(item => item && item.id) : [];
139
+ if (!Array.isArray(items) || !items.length || items.some(item => !item
140
+ || !nonempty(item.id) || !kebab.test(item.id) || !nonempty(item.description))
141
+ || new Set(itemIds).size !== itemIds.length) errors.push(
142
+ `schema-v4 runtime region '${id}' requires ordered items with unique kebab-case id and non-empty description`);
143
+ if (schemaV5 && (!Array.isArray(items) || items.some(item => !nonempty(item.kind) || !kebab.test(item.kind)
144
+ || !Array.isArray(item.required_strings) || !item.required_strings.every(nonempty)))) errors.push(
145
+ `schema-v5 runtime region '${id}' items require kebab-case kind and required_strings`);
146
+ if (schemaV5 && Array.isArray(items) && region.placement === 'right' && region.ui_strings.length >= 4
147
+ && items.length < 4) errors.push(
148
+ `schema-v5 right-panel region '${id}' must inventory its navigable rows as atomic items`);
149
+ if (schemaV5 && Array.isArray(items) && typeof region.placement === 'string' && region.placement.startsWith('overlay-')) {
150
+ const kinds = new Set(items.map(item => item.kind));
151
+ if (!kinds.has('group-heading') || !kinds.has('selection-row') || !kinds.has('actions')) errors.push(
152
+ `schema-v5 grouped overlay region '${id}' requires atomic group-heading, selection-row, and actions items`);
153
+ const itemText = items.flatMap(item => [item.description, ...(item.required_strings || [])]).join(' ');
154
+ if (/\b(?:no changes?|unchanged)\b/i.test(itemText)) errors.push(
155
+ `schema-v5 overlay region '${id}' must list concrete changed entities, not unchanged rows`);
156
+ }
157
+ } else if (!region || !stringArray(region.items, { nonemptyArray: true })) errors.push(
158
+ `runtime region '${id}' requires non-empty items`);
159
+ if (!region || !nonempty(region.appearance)) errors.push(`runtime region '${id}' requires a non-empty appearance`);
160
+ if (!region || typeof region.required !== 'boolean') errors.push(`runtime region '${id}' requires a boolean required flag`);
161
+ if (!region || !Array.isArray(region.ui_strings) || !region.ui_strings.every(nonempty)) errors.push(
162
+ `runtime region '${id}' requires a ui_strings array of non-empty strings`);
163
+ if (!region || !Array.isArray(region.required_strings) || !region.required_strings.every(nonempty)) errors.push(
164
+ `runtime region '${id}' requires a required_strings array of non-empty strings`);
165
+ if (region && typeof region.placement === 'string' && region.placement.startsWith('overlay-')) {
166
+ if (!Array.isArray(region.required_strings) || region.required_strings.length < 2) errors.push(
167
+ `overlay runtime region '${id}' requires at least two required_strings`);
168
+ if (!Array.isArray(region.items) || region.items.length < 3) errors.push(
169
+ `overlay runtime region '${id}' requires at least three items`);
170
+ }
171
+ if (schemaV4 && region && region.placement === 'canvas') {
172
+ if (!['populated', 'empty', 'loading'].includes(region.content_mode)) errors.push(
173
+ `schema-v4 canvas region '${id}' requires content_mode populated, empty, or loading`);
174
+ if (!['required', 'optional', 'none'].includes(region.media_expectation)) errors.push(
175
+ `schema-v4 canvas region '${id}' requires media_expectation required, optional, or none`);
176
+ if (!Number.isInteger(region.max_selected_outlines) || region.max_selected_outlines < 0) errors.push(
177
+ `schema-v4 canvas region '${id}' requires a non-negative max_selected_outlines integer`);
178
+ const assets = region.representative_assets;
179
+ if (!Array.isArray(assets) || assets.some(asset => !norm(asset))
180
+ || new Set(assets || []).size !== (assets || []).length) errors.push(
181
+ `schema-v4 canvas region '${id}' representative_assets must be unique existing repo files`);
182
+ if (region.media_expectation === 'required' && (!Array.isArray(assets) || !assets.length)) errors.push(
183
+ `schema-v4 canvas region '${id}' requires representative_assets when media_expectation is required`);
184
+ }
185
+ }
186
+
187
+ const regionOrder = new Map(regionIds.map((id, index) => [id, index]));
188
+ for (const state of states) {
189
+ const id = state && state.id || '?';
190
+ const visible = state && state.visible_regions;
191
+ if (!Array.isArray(visible)
192
+ || visible.some(regionId => !regionOrder.has(regionId))
193
+ || new Set(visible || []).size !== (visible || []).length
194
+ || (visible || []).some((regionId, index) => index > 0
195
+ && regionOrder.get(regionId) <= regionOrder.get(visible[index - 1]))) {
196
+ errors.push(`runtime state '${id}' visible_regions must be unique valid region ids in recipe order`);
197
+ }
198
+ if (!state || typeof state.screenshot_required !== 'boolean') errors.push(
199
+ `runtime state '${id}' requires a boolean screenshot_required flag`);
200
+ if (schemaV4 && (!stringArray(state && state.topic_tags, { nonemptyArray: true })
201
+ || !['orientation', 'primary-action', 'secondary-action', 'terminal-action']
202
+ .includes(state && state.instructional_priority))) errors.push(
203
+ `schema-v4 runtime state '${id}' requires topic_tags and a valid instructional_priority`);
204
+ if (schemaV5) {
205
+ if (!nonempty(state.canvas_presentation)) errors.push(
206
+ `schema-v5 runtime state '${id}' requires canvas_presentation`);
207
+ const hasToolbar = Array.isArray(visible) && regions.some(region =>
208
+ visible.includes(region.id) && region.placement === 'top');
209
+ if (hasToolbar && ['action', 'confirm'].includes(state.kind)
210
+ && (!nonempty(state.context_label)
211
+ || !Array.isArray(state.required_strings)
212
+ || !state.required_strings.includes(state.context_label))) errors.push(
213
+ `schema-v5 runtime state '${id}' requires a concrete context_label in required_strings`);
214
+ }
215
+ if (!state || !Array.isArray(state.required_strings) || !state.required_strings.every(nonempty)) errors.push(
216
+ `runtime state '${id}' requires a required_strings array of non-empty strings`);
217
+ if (state && state.kind === 'overview' && state.screenshot_required) errors.push(
218
+ `runtime overview state '${id}' cannot be screenshot_required`);
219
+ if (state && state.screenshot_required) {
220
+ if (!Array.isArray(visible) || !visible.length) errors.push(
221
+ `screenshot-required runtime state '${id}' must have visible_regions`);
222
+ const regionAssertions = regions.filter(region => (visible || []).includes(region.id))
223
+ .flatMap(region => Array.isArray(region.required_strings) ? region.required_strings : []);
224
+ const stateAssertions = Array.isArray(state.required_strings) ? state.required_strings : [];
225
+ if (![...stateAssertions, ...regionAssertions].some(nonempty)) errors.push(
226
+ `screenshot-required runtime state '${id}' requires a visible required string assertion`);
227
+ }
228
+ }
229
+
230
+ if (states.filter(state => state && state.screenshot_required).length > 3) errors.push(
231
+ `runtime_chrome layout '${owner}' has more than three screenshot-required states`);
232
+ for (const region of regions) {
233
+ const visibleInEveryState = states.length > 0
234
+ && states.every(state => Array.isArray(state.visible_regions) && state.visible_regions.includes(region.id));
235
+ if (typeof region.required === 'boolean' && region.required !== visibleInEveryState) errors.push(
236
+ `runtime region '${region.id || '?'}' required must be true iff it is visible in every state`);
237
+ }
238
+
239
+ const leftNav = regions.some(region => region && region.placement === 'left'
240
+ && Array.isArray(region.ui_strings) && region.ui_strings.length >= 3);
241
+ const canvas = regions.some(region => region && region.placement === 'canvas');
242
+ const confirmationOverlay = regions.some(region => region
243
+ && typeof region.placement === 'string' && region.placement.startsWith('overlay-')
244
+ && states.some(state => state && state.kind === 'confirm'
245
+ && Array.isArray(state.visible_regions) && state.visible_regions.includes(region.id)));
246
+ if (leftNav && canvas && confirmationOverlay) {
247
+ const actions = states.filter(state => state && state.kind === 'action');
248
+ const requiredActions = actions.filter(state => state.screenshot_required);
249
+ if (actions.length < 2) errors.push(
250
+ `runtime_chrome layout '${owner}' has navigation, canvas, and confirmation surfaces and requires at least two action states`);
251
+ if (!schemaV4 && requiredActions.length < 2) errors.push(
252
+ `runtime_chrome layout '${owner}' has navigation, canvas, and confirmation surfaces and requires at least two screenshot-required action states`);
253
+ }
254
+ }
255
+ }
256
+
257
+ const ignoredBySource = new Map();
258
+ for (const item of validIgnored) {
259
+ const rel = item && norm(item.source_file);
260
+ if (!item || typeof item !== 'object' || !rel || !nonempty(item.reason) || item.reason.trim().length < 12) {
261
+ errors.push(`runtime_surface_coverage.ignored entries require an existing detected source_file and a specific reason (12+ characters): ${(item && item.source_file) || '?'}`);
262
+ continue;
263
+ }
264
+ if (ignoredBySource.has(rel)) errors.push(`runtime_surface_coverage.ignored contains duplicate source_file: ${rel}`);
265
+ ignoredBySource.set(rel, item.reason.trim());
266
+ }
267
+ const ownership = new Map();
268
+ for (const candidate of candidates) {
269
+ const owners = layouts.filter(layout => {
270
+ if (!layout || !layout.runtime_chrome) return false;
271
+ if (norm(layout.path) === candidate.source_file) return true;
272
+ return (layout.runtime_chrome.source_files || []).some(rel => norm(rel) === candidate.source_file);
273
+ });
274
+ const ignoreReason = ignoredBySource.get(candidate.source_file) || null;
275
+ candidate.covered_by = owners.map(layout => layout.name || layout.path);
276
+ for (const layout of owners) {
277
+ const key = layout.name || layout.path;
278
+ if (!ownership.has(key)) ownership.set(key, []);
279
+ ownership.get(key).push(candidate.source_file);
280
+ }
281
+ candidate.ignored = ignoreReason;
282
+ if (owners.length > 1) errors.push(
283
+ `runtime mount '${candidate.source_file}' is owned by multiple runtime_chrome layouts: ${candidate.covered_by.join(', ')}`);
284
+ if (owners.length && ignoreReason) errors.push(
285
+ `runtime mount '${candidate.source_file}' cannot be both owned and ignored`);
286
+ if (!owners.length && !ignoreReason) errors.push(
287
+ `runtime mount '${candidate.source_file}' is not owned by a distinct runtime_chrome layout and has no reasoned ignore entry`);
288
+ }
289
+
290
+ for (const [owner, mounts] of ownership) {
291
+ if (mounts.length > 1) errors.push(
292
+ `runtime_chrome layout '${owner}' combines distinct mount files (${mounts.join(', ')}); give each mount its own layout/recipe`);
293
+ }
294
+
295
+ for (const item of validIgnored) {
296
+ const rel = item && norm(item.source_file);
297
+ if (!rel || !candidates.some(candidate => candidate.source_file === rel)) errors.push(
298
+ `runtime_surface_coverage.ignored references a file that is not a detected runtime mount: ${(item && item.source_file) || '?'}`);
299
+ }
300
+
301
+ const report = { checked: true, candidates, ignored: validIgnored, errors, all_passed: errors.length === 0 };
302
+ if (reportPath) {
303
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
304
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
305
+ }
306
+ if (errors.length) {
307
+ console.error(`Runtime surface coverage: FAIL (${errors.length} error(s))`);
308
+ for (const error of errors) console.error(` - ${error}`);
309
+ process.exit(1);
310
+ }
311
+ console.log(`Runtime surface coverage: OK (${candidates.length} mount(s), ${validIgnored.length} ignored)`);
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Semantic quality checks for schema-v5 runtime recipes. Coverage answers
5
+ // "does every mount have a recipe?"; this gate answers "is the recipe concrete,
6
+ // internally consistent, and source-backed enough to render faithfully?"
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ function die(message) {
12
+ console.error(`ERROR: ${message}`);
13
+ process.exit(2);
14
+ }
15
+
16
+ const mapArg = process.argv[2];
17
+ const projectArg = process.argv[3];
18
+ const jsonIndex = process.argv.indexOf('--json');
19
+ const reportArg = jsonIndex >= 0 ? process.argv[jsonIndex + 1] : null;
20
+ if (!mapArg || !projectArg) die('usage: check_runtime_recipe_quality.js <project_map.json> <project_dir> [--json report.json]');
21
+
22
+ const projectDir = path.resolve(projectArg);
23
+ let projectMap;
24
+ try {
25
+ projectMap = JSON.parse(fs.readFileSync(mapArg, 'utf8'));
26
+ } catch (error) {
27
+ die(`cannot read project map: ${error.message}`);
28
+ }
29
+
30
+ function nonempty(value) {
31
+ return typeof value === 'string' && value.trim().length > 0;
32
+ }
33
+
34
+ function filePath(value) {
35
+ if (!nonempty(value)) return null;
36
+ const clean = value.replace(/^\.\//, '').split(/[?#]/, 1)[0];
37
+ const absolute = path.resolve(projectDir, clean);
38
+ const root = `${projectDir}${path.sep}`;
39
+ if (!absolute.startsWith(root)) return null;
40
+ try {
41
+ if (!fs.statSync(absolute).isFile()) return null;
42
+ } catch (_) {
43
+ return null;
44
+ }
45
+ return path.relative(projectDir, absolute).split(path.sep).join('/');
46
+ }
47
+
48
+ function strings(value) {
49
+ return Array.isArray(value) ? value.filter(nonempty) : [];
50
+ }
51
+
52
+ function allSources(recipe) {
53
+ return [recipe.source_files, ...(recipe.regions || []).map(region => region.source_files),
54
+ ...(recipe.states || []).map(state => state.source_files)].flat().filter(nonempty);
55
+ }
56
+
57
+ function rootTokens(value) {
58
+ if (Array.isArray(value)) return value.filter(nonempty);
59
+ return nonempty(value) ? value.split(/\s+/).filter(Boolean) : [];
60
+ }
61
+
62
+ const errors = [];
63
+ const summaries = [];
64
+ const layouts = Array.isArray(projectMap.layouts) ? projectMap.layouts : [];
65
+ const routes = projectMap.route_index && typeof projectMap.route_index === 'object'
66
+ ? Object.entries(projectMap.route_index) : [];
67
+
68
+ for (const layout of layouts.filter(item => item && item.runtime_chrome)) {
69
+ const owner = layout.name || layout.path || '?';
70
+ const recipe = layout.runtime_chrome;
71
+ const regions = Array.isArray(recipe.regions) ? recipe.regions : [];
72
+ const states = Array.isArray(recipe.states) ? recipe.states : [];
73
+ const mount = filePath(layout.path);
74
+ const leftNav = regions.find(region => region && region.placement === 'left'
75
+ && /nav|destination/i.test(`${region.id || ''} ${region.region || ''}`));
76
+ const toolbar = regions.find(region => region && region.placement === 'top');
77
+ const canvas = regions.find(region => region && region.placement === 'canvas');
78
+ const overlay = regions.find(region => region && String(region.placement || '').startsWith('overlay-'));
79
+ const complexEditor = Boolean(leftNav && toolbar && canvas && overlay);
80
+
81
+ const invalidSources = [...new Set(allSources(recipe).filter(source => !filePath(source)))];
82
+ if (invalidSources.length) errors.push(
83
+ `runtime_chrome layout '${owner}' source_files must be existing regular files, not directories or symbolic placeholders: ${invalidSources.join(', ')}`);
84
+
85
+ if (recipe.source_kind === 'knowledge'
86
+ && (!Array.isArray(projectMap.default_user_assumptions) || !projectMap.default_user_assumptions.length)) {
87
+ errors.push(`knowledge-backed runtime_chrome layout '${owner}' requires explicit default_user_assumptions`);
88
+ }
89
+
90
+ const navAppearance = String(leftNav && leftNav.appearance || '');
91
+ if (leftNav && !/\b(?:light|dark|black|white|#[0-9a-f]{3,8})\b/i.test(navAppearance)) errors.push(
92
+ `runtime navigation '${leftNav.id || '?'}' must record a concrete light/dark or colour treatment`);
93
+
94
+ const shellMode = `${layout.area || ''} ${recipe.mode_note || ''}`;
95
+ const shellHidden = /fullscreen|full-screen|shell\s+(?:is\s+)?hidden|no\s+(?:classic\s+)?(?:admin\s+)?(?:menu|toolbar)|(?:menu|toolbar)[^.;]{0,30}hidden/i.test(shellMode);
96
+ if (shellHidden) {
97
+ if (!layout.root_classes || typeof layout.root_classes !== 'object') errors.push(
98
+ `shell-hiding runtime_chrome layout '${owner}' requires resolved layout root_classes`);
99
+ const appHtml = new Set(rootTokens(projectMap.app_shell && projectMap.app_shell.root_classes
100
+ && projectMap.app_shell.root_classes.html));
101
+ const layoutHtml = rootTokens(layout.root_classes && layout.root_classes.html);
102
+ const leakedOffsets = layoutHtml.filter(token => appHtml.has(token)
103
+ && /toolbar|admin-?bar|shell|header-offset/i.test(token));
104
+ if (leakedOffsets.length) errors.push(
105
+ `shell-hiding runtime_chrome layout '${owner}' retains app-shell offset class(es): ${leakedOffsets.join(', ')}`);
106
+ }
107
+
108
+ const matchingRoutes = routes.filter(([, route]) => route && (
109
+ route.layout === owner || route.layout === layout.path || filePath(route.primary_view) === mount
110
+ || filePath(route.controller) === mount || filePath(route.source_file) === mount));
111
+ if (mount && !matchingRoutes.length) errors.push(
112
+ `runtime_chrome layout '${owner}' has no route pointing to its mount`);
113
+
114
+ if (complexEditor) {
115
+ const toolbarItems = Array.isArray(toolbar.items) ? toolbar.items : [];
116
+ const toolbarKinds = new Set(toolbarItems.map(item => item && item.kind));
117
+ if (toolbarItems.length < 5 || !['identity', 'control-group', 'document-context', 'primary-action']
118
+ .every(kind => toolbarKinds.has(kind))) errors.push(
119
+ `complex editor '${owner}' toolbar must inventory identity, editing controls, concrete document context, view controls, and primary action as at least five items`);
120
+
121
+ const canvasItems = Array.isArray(canvas.items) ? canvas.items : [];
122
+ const canvasSources = [...new Set(strings(canvas.source_files).map(filePath).filter(Boolean))];
123
+ if (canvas.content_mode === 'populated' && canvasItems.length < 3) errors.push(
124
+ `populated complex-editor canvas '${canvas.id || '?'}' requires at least three source-backed anatomy items`);
125
+ if (canvas.content_mode === 'populated' && canvasSources.filter(source => source !== mount).length < 2) errors.push(
126
+ `populated complex-editor canvas '${canvas.id || '?'}' requires at least two concrete content source files beyond its runtime mount`);
127
+
128
+ const documentItems = toolbarItems.filter(item => item && item.kind === 'document-context');
129
+ const documentLabels = new Set(documentItems.flatMap(item => strings(item.required_strings)));
130
+ const featureLabels = new Set(regions.flatMap(region => [region.region])
131
+ .concat(leftNav ? strings(leftNav.ui_strings) : [])
132
+ .filter(nonempty).map(value => value.toLowerCase()));
133
+ for (const state of states.filter(state => state && ['action', 'confirm'].includes(state.kind)
134
+ && strings(state.visible_regions).includes(toolbar.id))) {
135
+ const context = String(state.context_label || '').trim();
136
+ if (!documentLabels.has(context)) errors.push(
137
+ `runtime state '${state.id || '?'}' context_label '${context || '?'}' must match the toolbar document-context item`);
138
+ if (featureLabels.has(context.toLowerCase())
139
+ || /^(?:home|page|document|template|editor|content|design|styles?|settings|untitled)$/i.test(context)) errors.push(
140
+ `runtime state '${state.id || '?'}' context_label '${context}' is a generic feature label, not a concrete open document/template title`);
141
+ }
142
+
143
+ const actionCount = states.filter(state => state && state.kind === 'action').length;
144
+ const overlayItems = Array.isArray(overlay.items) ? overlay.items : [];
145
+ const groups = overlayItems.filter(item => item && item.kind === 'group-heading');
146
+ const selections = overlayItems.filter(item => item && item.kind === 'selection-row');
147
+ const expectedGroups = Math.min(2, actionCount);
148
+ if (groups.length < expectedGroups || selections.length < expectedGroups) errors.push(
149
+ `complex editor '${owner}' confirmation must inventory at least ${expectedGroups} changed-entity group(s) and selection row(s), matching its action families`);
150
+ for (const item of selections) {
151
+ const labels = strings(item.required_strings);
152
+ if (!labels.length || labels.every(value => /^(?:custom\s+)?(?:styles?|templates?|changes?|settings?)$/i.test(value))) errors.push(
153
+ `confirmation selection '${item.id || '?'}' requires a concrete entity name, not a generic change label`);
154
+ }
155
+
156
+ for (const [key, route] of matchingRoutes) {
157
+ const partials = strings(route.partials_expanded);
158
+ const invalidPartials = partials.filter(partial => !filePath(partial));
159
+ if (!partials.length || invalidPartials.length) errors.push(
160
+ `runtime route '${key}' for complex editor '${owner}' requires recursively expanded regular-file partials`);
161
+ const closure = new Set([route.primary_view, route.controller_action, ...partials]
162
+ .map(filePath).filter(Boolean));
163
+ const missingCanvas = canvasSources.filter(source => !closure.has(source));
164
+ if (missingCanvas.length) errors.push(
165
+ `runtime route '${key}' partial closure omits canvas source(s): ${missingCanvas.join(', ')}`);
166
+ }
167
+ }
168
+
169
+ summaries.push({ layout: owner, complex_editor: complexEditor, sources: new Set(allSources(recipe)).size,
170
+ regions: regions.length, states: states.length });
171
+ }
172
+
173
+ const report = { checked: true, summaries, errors, all_passed: errors.length === 0 };
174
+ if (reportArg) {
175
+ const reportPath = path.resolve(reportArg);
176
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
177
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
178
+ }
179
+ if (errors.length) {
180
+ console.error(`Runtime recipe quality: FAIL (${errors.length} error(s))`);
181
+ errors.forEach(error => console.error(` - ${error}`));
182
+ process.exit(1);
183
+ }
184
+ console.log(`Runtime recipe quality: OK (${summaries.length} recipe(s))`);