@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,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Prepare and publish one article screenshot as soon as its mockup is ready.
4
+ *
5
+ * The injector mutates step HTML in place, while later steps need the original
6
+ * source (including <!-- INJECT_CSS --> and image tokens) for clone-and-edit.
7
+ * This helper snapshots every authored step, injects the current set, validates
8
+ * and renders one requested step, then restores all source HTML. The PNG is
9
+ * rendered to a temporary path and renamed only after the quality, evidence,
10
+ * and geometry gates pass, so observers never consume a half-written asset.
11
+ *
12
+ * Usage: render_ready.js <article_dir> <step_index> [project_root]
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { spawnSync } = require('child_process');
18
+ const { puppeteer, launchOptions, renderOnBrowser, probeStringsFor } = require('./render_mockup.js');
19
+
20
+ function existingPath(candidates) {
21
+ return candidates.find(candidate => fs.existsSync(candidate));
22
+ }
23
+
24
+ function evidenceForStep(articleDir, location) {
25
+ try {
26
+ const viewSources = JSON.parse(
27
+ fs.readFileSync(path.join(articleDir, 'view_sources.json'), 'utf8')
28
+ );
29
+ const entries = viewSources.blocks || viewSources.steps || [];
30
+ const step = entries.find(item => item.block_id === location || Number(item.index) === Number(location));
31
+ return (step && step.verbatim_evidence || [])
32
+ .map(item => typeof item === 'string' ? item : (item && item.string) || '')
33
+ .filter(Boolean);
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function run(command, args, options = {}) {
40
+ const result = spawnSync(command, args, {
41
+ encoding: 'utf8',
42
+ maxBuffer: 10 * 1024 * 1024,
43
+ ...options,
44
+ });
45
+ if (result.stdout) process.stdout.write(result.stdout);
46
+ if (result.stderr) process.stderr.write(result.stderr);
47
+ if (result.error) throw result.error;
48
+ if (result.status !== 0) {
49
+ throw new Error(`${path.basename(command)} exited with status ${result.status}`);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ async function main() {
55
+ const articleDir = path.resolve(process.argv[2] || '');
56
+ const rawIndex = process.argv[3];
57
+ const projectRoot = path.resolve(process.argv[4] || process.cwd());
58
+ if (!articleDir || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rawIndex || '') || !fs.existsSync(articleDir)) {
59
+ throw new Error('Usage: render_ready.js <article_dir> <block_id|step_index> [project_root]');
60
+ }
61
+
62
+ const legacy = /^\d+$/.test(rawIndex);
63
+ const index = legacy ? Number(rawIndex) : null;
64
+ const stepName = legacy ? `step_${index}` : `block_${rawIndex}`;
65
+ const htmlPath = path.join(articleDir, `${stepName}.html`);
66
+ const finalPng = path.join(articleDir, `${stepName}.png`);
67
+ const finalDiagnostics = path.join(articleDir, `${stepName}_diagnostics.json`);
68
+ if (!fs.existsSync(htmlPath)) throw new Error(`Missing ${htmlPath}`);
69
+
70
+ const authoredHtml = fs.readdirSync(articleDir)
71
+ .filter(name => /^(?:step_\d+|block_[a-z0-9]+(?:-[a-z0-9]+)*)\.html$/.test(name))
72
+ .map(name => path.join(articleDir, name));
73
+ const originals = new Map(
74
+ authoredHtml.map(file => [file, fs.readFileSync(file, 'utf8')])
75
+ );
76
+
77
+ const brandDir = existingPath([
78
+ path.join(projectRoot, '.rtfm', 'branding.json'),
79
+ path.join(projectRoot, '.rtfm-branding', 'branding.json'),
80
+ ]);
81
+ const brandingJson = brandDir || null;
82
+ const brandingCss = brandingJson
83
+ ? path.join(path.dirname(brandingJson), 'branding.css')
84
+ : null;
85
+ if (brandingCss && fs.existsSync(brandingCss)) {
86
+ fs.copyFileSync(brandingCss, path.join(articleDir, 'branding.css'));
87
+ }
88
+
89
+ const imagesJson = existingPath([
90
+ path.join(projectRoot, '.rtfm', 'images_base64.json'),
91
+ path.join(projectRoot, '.rtfm-branding', 'images_base64.json'),
92
+ ]) || path.join(projectRoot, '.rtfm', 'images_base64.json');
93
+ const generatedImages = path.join(articleDir, 'generated_images.json');
94
+ const injectArgs = [
95
+ path.join(__dirname, 'inject_assets.js'), articleDir, '', imagesJson,
96
+ ];
97
+ if (brandingJson) injectArgs.push('--branding', brandingJson);
98
+ if (fs.existsSync(generatedImages)) {
99
+ injectArgs.push('--generated-images', generatedImages);
100
+ }
101
+ injectArgs.push('--refresh-jit');
102
+
103
+ const tempPng = path.join(articleDir, `.${stepName}.rendering-${process.pid}.png`);
104
+ const tempDiagnostics = tempPng.replace(/\.png$/, '_diagnostics.json');
105
+ let browser;
106
+ try {
107
+ run(process.execPath, injectArgs, { cwd: projectRoot });
108
+
109
+ const validation = spawnSync(process.execPath, [
110
+ path.join(__dirname, 'validate_html.js'), htmlPath,
111
+ ], { encoding: 'utf8', maxBuffer: 1024 * 1024 });
112
+ const validationText = `${validation.stdout || ''}${validation.stderr || ''}`;
113
+ fs.writeFileSync(path.join(articleDir, `${stepName}_validation.json`), validationText);
114
+ if (validation.error) throw validation.error;
115
+ if (validation.status !== 0) {
116
+ throw new Error(`${stepName}.html is not ready: HTML validation failed`);
117
+ }
118
+
119
+ const evidence = evidenceForStep(articleDir, legacy ? index : rawIndex);
120
+ let viewSources = null;
121
+ try { viewSources = JSON.parse(fs.readFileSync(path.join(articleDir, 'view_sources.json'), 'utf8')); } catch { /* optional */ }
122
+ const probe = probeStringsFor(projectRoot, viewSources, legacy ? index : rawIndex, evidence);
123
+ browser = await puppeteer.launch(launchOptions());
124
+ const started = Date.now();
125
+ const diagnostics = await renderOnBrowser(browser, tempPng, htmlPath, { evidence, probe });
126
+ const quality = diagnostics.qualityScore || {};
127
+ const invisibleEvidence = ((diagnostics.metrics || {}).evidenceVisibility || [])
128
+ .filter(item => !item.visible).map(item => item.string);
129
+ const evidenceChecked = ((diagnostics.metrics || {}).evidenceVisibility || []).length;
130
+ const geometryErrors = (((diagnostics.metrics || {}).regionGeometry || {}).errors || []);
131
+
132
+ if (quality.rating === 'poor') {
133
+ throw new Error(`${stepName} rendered POOR (score ${quality.score})`);
134
+ }
135
+ if (evidenceChecked && invisibleEvidence.length * 2 > evidenceChecked) {
136
+ throw new Error(`${stepName} hides ${invisibleEvidence.length}/${evidenceChecked} evidence strings`);
137
+ }
138
+ if (geometryErrors.length) {
139
+ throw new Error(`${stepName} violates runtime geometry: ${geometryErrors.join('; ')}`);
140
+ }
141
+
142
+ fs.renameSync(tempPng, finalPng);
143
+ fs.renameSync(tempDiagnostics, finalDiagnostics);
144
+ fs.writeFileSync(path.join(articleDir, `${stepName}_progress.json`), JSON.stringify({
145
+ ...(legacy ? { step_index: index } : { block_id: rawIndex }),
146
+ published: true,
147
+ render_ms: Date.now() - started,
148
+ quality: quality.rating,
149
+ score: quality.score,
150
+ }, null, 2));
151
+ console.log(`Published ${path.basename(finalPng)} progressively (quality: ${quality.rating}, ${quality.score})`);
152
+ } finally {
153
+ if (browser) await browser.close();
154
+ for (const [file, contents] of originals) fs.writeFileSync(file, contents);
155
+ for (const temporary of [tempPng, tempDiagnostics]) {
156
+ try { fs.unlinkSync(temporary); } catch { /* already published or absent */ }
157
+ }
158
+ }
159
+ }
160
+
161
+ main().catch(error => {
162
+ console.error(`render_ready failed: ${error.message}`);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # Recover the project workspace when a shell tool starts in a non-project
3
+ # directory (Antigravity scratch, $HOME, …). The CLI process was launched in
4
+ # the target repo — walk parent pids until we find a directory that looks like
5
+ # a project (.git or .rtfm). No project-specific paths.
6
+ #
7
+ # Prefer $RTFM_WORKSPACE when the harness already set it.
8
+ # Prints one absolute path to stdout. Always exits 0.
9
+ # bash 3.2 / macOS + Linux.
10
+
11
+ if [ -n "$RTFM_WORKSPACE" ] && [ -d "$RTFM_WORKSPACE" ]; then
12
+ (cd "$RTFM_WORKSPACE" && pwd -P)
13
+ exit 0
14
+ fi
15
+
16
+ scratch_default="${HOME}/.gemini/antigravity-cli/scratch"
17
+ here=$(pwd -P 2>/dev/null || pwd)
18
+ home_real=$(cd "$HOME" && pwd -P 2>/dev/null || true)
19
+
20
+ is_scratch() {
21
+ local p="$1" s
22
+ [ -n "$p" ] || return 1
23
+ [ -d "$scratch_default" ] || return 1
24
+ s=$(cd "$scratch_default" && pwd -P 2>/dev/null) || return 1
25
+ [ "$p" = "$s" ]
26
+ }
27
+
28
+ is_home() {
29
+ [ -n "$home_real" ] && [ "$1" = "$home_real" ]
30
+ }
31
+
32
+ looks_like_project() {
33
+ [ -d "$1/.git" ] || [ -d "$1/.rtfm" ]
34
+ }
35
+
36
+ is_untrusted() {
37
+ is_scratch "$1" && return 0
38
+ is_home "$1" && return 0
39
+ looks_like_project "$1" && return 1
40
+ return 0
41
+ }
42
+
43
+ pid_cwd() {
44
+ local pid="$1" cand=""
45
+ [ -n "$pid" ] || return 1
46
+ [ "$pid" -gt 1 ] 2>/dev/null || return 1
47
+ if command -v lsof >/dev/null 2>&1; then
48
+ cand=$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | awk '/^n\// {print substr($0,2); exit}')
49
+ fi
50
+ if [ -z "$cand" ] && [ -L "/proc/$pid/cwd" ]; then
51
+ cand=$(readlink "/proc/$pid/cwd" 2>/dev/null || true)
52
+ fi
53
+ [ -n "$cand" ] && [ -d "$cand" ] || return 1
54
+ (cd "$cand" && pwd -P)
55
+ }
56
+
57
+ if ! is_untrusted "$here"; then
58
+ printf '%s\n' "$here"
59
+ exit 0
60
+ fi
61
+
62
+ pid=${PPID:-$$}
63
+ i=0
64
+ found=""
65
+ while [ "$i" -lt 12 ]; do
66
+ cand=$(pid_cwd "$pid" || true)
67
+ if [ -n "$cand" ] && ! is_untrusted "$cand"; then
68
+ printf '%s\n' "$cand"
69
+ exit 0
70
+ fi
71
+ if [ -z "$found" ] && [ -n "$cand" ] && ! is_scratch "$cand" && ! is_home "$cand"; then
72
+ found=$cand
73
+ fi
74
+ next=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
75
+ [ -n "$next" ] && [ "$next" != "$pid" ] && [ "$next" -gt 1 ] 2>/dev/null || break
76
+ pid=$next
77
+ i=$((i + 1))
78
+ done
79
+
80
+ if [ -n "$found" ]; then
81
+ printf '%s\n' "$found"
82
+ exit 0
83
+ fi
84
+
85
+ printf '%s\n' "$here"
86
+ exit 0
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ // This function is deliberately self-contained: Puppeteer serialises it into
4
+ // the browser for page.evaluate(). Keep every helper inside the function so the
5
+ // article renderer and walkthrough recorder measure the exact same contract.
6
+ function measureRuntimeRegionGeometry() {
7
+ const nodes = [...document.querySelectorAll('[data-rtfm-region]')];
8
+ if (!nodes.length) return { checked: false, regions: [], errors: [] };
9
+ const vw = window.innerWidth;
10
+ const vh = window.innerHeight;
11
+ const regions = nodes.map(el => {
12
+ const r = el.getBoundingClientRect();
13
+ return {
14
+ id: el.getAttribute('data-rtfm-region'),
15
+ placement: el.getAttribute('data-rtfm-placement') || '',
16
+ left: r.left, top: r.top, right: r.right, bottom: r.bottom,
17
+ width: r.width, height: r.height, area: r.width * r.height,
18
+ };
19
+ });
20
+ const errors = [];
21
+ const canvas = regions.find(r => r.placement === 'canvas');
22
+ const nonOverlays = regions.filter(r => !r.placement.startsWith('overlay-'));
23
+ if (canvas && nonOverlays.some(r => r.id !== canvas.id && r.area > canvas.area)) {
24
+ errors.push(`canvas region '${canvas.id}' is not the dominant non-overlay region`);
25
+ }
26
+ const overlapArea = (a, b) => Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left))
27
+ * Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
28
+ for (const r of regions) {
29
+ if (r.width <= 1 || r.height <= 1) {
30
+ errors.push(`region '${r.id}' is collapsed`);
31
+ continue;
32
+ }
33
+ if (r.placement === 'top' && (r.top > vh * 0.3 || r.width < vw * 0.45))
34
+ errors.push(`top region '${r.id}' is not positioned across the upper screen`);
35
+ if (r.placement === 'bottom' && r.bottom < vh * 0.7)
36
+ errors.push(`bottom region '${r.id}' is not positioned near the lower screen`);
37
+ if (r.placement === 'left' && (r.left > vw * 0.25 || (r.left + r.right) / 2 >= vw * 0.5))
38
+ errors.push(`left region '${r.id}' is not anchored on the left`);
39
+ if (r.placement === 'right' && (r.right < vw * 0.75 || (r.left + r.right) / 2 <= vw * 0.5))
40
+ errors.push(`right region '${r.id}' is not anchored on the right`);
41
+ if (r.placement.startsWith('overlay-')) {
42
+ if (!canvas || overlapArea(r, canvas) < r.area * 0.2)
43
+ errors.push(`overlay region '${r.id}' does not visibly overlap the canvas`);
44
+ if (r.placement === 'overlay-left' && (r.left + r.right) / 2 >= vw * 0.5)
45
+ errors.push(`overlay-left region '${r.id}' is on the wrong side`);
46
+ if (r.placement === 'overlay-right' && (r.left + r.right) / 2 <= vw * 0.5)
47
+ errors.push(`overlay-right region '${r.id}' is on the wrong side`);
48
+ if (r.placement === 'overlay-bottom' && r.bottom < vh * 0.65)
49
+ errors.push(`overlay-bottom region '${r.id}' is not bottom-anchored`);
50
+ }
51
+ }
52
+ return { checked: true, viewport: { width: vw, height: vh }, regions, errors };
53
+ }
54
+
55
+ module.exports = { measureRuntimeRegionGeometry };
@@ -0,0 +1,49 @@
1
+ // Cited source paths: project-relative, or `../<sibling>/<path>` into another
2
+ // checkout of a multi-repo product. Siblings are only reachable when the
3
+ // harness set RTFM_REPOS_ROOT and the project directory sits directly inside
4
+ // it (see related_repos.sh) — interactive single-repo runs keep the strict
5
+ // project-relative rule. The resolved file must stay inside its checkout.
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function reposRoot() {
10
+ const root = process.env.RTFM_REPOS_ROOT;
11
+ return typeof root === 'string' && root.trim() ? root : null;
12
+ }
13
+
14
+ // Syntactic check, no filesystem: a plain relative path, or (with
15
+ // RTFM_REPOS_ROOT set) exactly one leading `..` followed by a sibling name.
16
+ function isSafeSourcePath(value) {
17
+ if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) return false;
18
+ const parts = value.split(/[\\/]/);
19
+ if (!parts.includes('..')) return true;
20
+ return Boolean(reposRoot()) && parts[0] === '..' && parts.length > 2 &&
21
+ Boolean(parts[1]) && parts[1] !== '.' && !parts.slice(1).includes('..');
22
+ }
23
+
24
+ // Absolute real path of a cited file, or throws. Sibling citations must land
25
+ // in a git checkout that is a direct child of RTFM_REPOS_ROOT, next to the
26
+ // project directory.
27
+ function resolveSourcePath(projectDir, file) {
28
+ if (!isSafeSourcePath(file)) throw Error('cite a project-relative source file (or ../<repo>/<path> for a related repository)');
29
+ const base = fs.realpathSync(projectDir);
30
+ const parts = file.split(/[\\/]/);
31
+ let checkout = base;
32
+ if (parts[0] === '..') {
33
+ const root = fs.realpathSync(reposRoot());
34
+ if (path.dirname(base) !== root) throw Error('related repositories are only reachable from a checkout inside RTFM_REPOS_ROOT');
35
+ checkout = fs.realpathSync(path.join(root, parts[1]));
36
+ if (path.dirname(checkout) !== root || checkout === base || !fs.existsSync(path.join(checkout, '.git'))) {
37
+ throw Error(`${parts[0]}/${parts[1]} is not a related repository`);
38
+ }
39
+ }
40
+ const resolved = fs.realpathSync(path.join(base, file));
41
+ if (!resolved.startsWith(checkout + path.sep)) throw Error('source resolves outside the project');
42
+ return resolved;
43
+ }
44
+
45
+ function sourceExists(projectDir, file) {
46
+ try { resolveSourcePath(projectDir, file); return true; } catch { return false; }
47
+ }
48
+
49
+ module.exports = { isSafeSourcePath, resolveSourcePath, sourceExists };
@@ -0,0 +1,30 @@
1
+ const assert = require('assert');
2
+ const {puppeteer, launchOptions, measureStringVisibility} = require('./render_mockup');
3
+ (async () => {
4
+ const browser = await puppeteer.launch(launchOptions());
5
+ try {
6
+ const page = await browser.newPage();
7
+ await page.setViewport({width:800,height:600});
8
+ await page.setContent(`<style>button,input{width:140px;height:35px}</style>
9
+ <button>Text button</button><input type="submit" value="Create API key">
10
+ <input type="button" value="Input button"><input type="reset" value="Reset fields">
11
+ <button aria-label="Admin menu"><svg width="20" height="20"><rect width="20" height="20"/></svg></button>
12
+ <span id="control-label" hidden>Named control</span><button aria-labelledby="control-label">⚙</button>
13
+ <a data-rtfm-action-target="open-menu" aria-label="Scripted menu"><svg width="20" height="20"><rect width="20" height="20"/></svg></a>
14
+ <input type="hidden" value="Hidden value"><input type="password" value="Secret value">
15
+ <div aria-label="Not a control" style="width:80px;height:30px"></div>
16
+ <button style="display:none" aria-label="Hidden control"></button>
17
+ <div style="opacity:0"><input type="submit" value="Transparent submit"></div>
18
+ <div style="overflow:hidden;height:0"><button aria-label="Clipped control"></button></div>
19
+ <input type="submit" value="Below crop" style="position:absolute;top:1000px">
20
+ <input type="submit" value="Right of crop" style="position:absolute;left:1000px">
21
+ <button style="visibility:hidden">Invisible text</button>`);
22
+ const visible = ['Text button','Create API key','Input button','Reset fields','Admin menu','Named control','Scripted menu'];
23
+ const hidden = ['Hidden value','Secret value','Not a control','Hidden control','Transparent submit','Clipped control','Below crop','Right of crop','Invisible text','Absent'];
24
+ const results = await measureStringVisibility(page,[...visible,...hidden],600,800);
25
+ for (const item of results) assert.equal(item.visible,visible.includes(item.string),JSON.stringify(item));
26
+ assert.equal(results.find(r => r.string === 'Create API key').match_kind, 'input_value');
27
+ assert.equal(results.find(r => r.string === 'Admin menu').match_kind, 'accessible_control');
28
+ console.log('Text, input values and named-control visibility tests passed');
29
+ } finally { await browser.close(); }
30
+ })().catch(error => { console.error(error); process.exitCode=1; });
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+ const { computeSignals } = require('./emit_walkthrough_signals.js');
10
+
11
+ const SCRIPT = path.join(__dirname, 'emit_walkthrough_signals.js');
12
+
13
+ function articleV2(overrides = {}) {
14
+ return {
15
+ schema_version: 2,
16
+ title: 'How to invite a teammate',
17
+ article_type: 'how-to',
18
+ blocks: [
19
+ { id: 'intro', type: 'prose', presentation: 'lead', content: 'Invite teammates.' },
20
+ { id: 'open-settings', type: 'section', presentation: 'numbered', title: 'Open settings', content: 'Click **Settings**.', has_image: true },
21
+ { id: 'enter-email', type: 'section', presentation: 'numbered', title: 'Enter the email', content: 'Type the address.', has_image: true },
22
+ { id: 'confirm', type: 'section', presentation: 'numbered', title: 'Confirm', content: 'Click **Send**.', has_image: false },
23
+ ],
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function viewSourcesBlocks(overrides = {}) {
29
+ return {
30
+ framework: 'rails',
31
+ action_coverage: [
32
+ { article_block_id: 'open-settings', screenshot_block_id: 'open-settings', kind: 'click', target: 'Settings', state: 'action-ready' },
33
+ { article_block_id: 'enter-email', screenshot_block_id: 'enter-email', kind: 'type', target: 'Email', state: 'action-ready' },
34
+ ],
35
+ blocks: [
36
+ { block_id: 'open-settings', primary_view: 'app/views/settings/index.html.erb' },
37
+ { block_id: 'enter-email', primary_view: 'app/views/settings/invites.html.erb' },
38
+ ],
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ function articleLegacy() {
44
+ return {
45
+ title: 'How to invite a teammate',
46
+ article_type: 'how-to',
47
+ introduction: 'Invite teammates.',
48
+ prerequisites: [],
49
+ steps: [
50
+ { title: 'Open settings', content: 'Click **Settings**.', has_image: true },
51
+ { title: 'Enter the email', content: 'Type the address.', has_image: false },
52
+ ],
53
+ tips: [],
54
+ summary: 'Done.',
55
+ };
56
+ }
57
+
58
+ function viewSourcesSteps() {
59
+ return {
60
+ framework: 'rails',
61
+ action_coverage: [
62
+ { article_step_index: 0, screenshot_step_index: 0, kind: 'click', target: 'Settings', state: 'action-ready' },
63
+ ],
64
+ steps: [
65
+ { index: 0, primary_view: 'app/views/settings/index.html.erb' },
66
+ { index: 1, primary_view: 'app/views/settings/index.html.erb' },
67
+ ],
68
+ };
69
+ }
70
+
71
+ function run() {
72
+ // Block-keyed schema-v2 artifacts.
73
+ let signals = computeSignals(articleV2(), viewSourcesBlocks(), 'web');
74
+ assert.deepStrictEqual(signals, {
75
+ schema_version: 1,
76
+ article_type: 'how-to',
77
+ sections_total: 3,
78
+ sections_with_images: 2,
79
+ distinct_surfaces: 2,
80
+ action_count: 2,
81
+ action_kinds: ['click', 'type'],
82
+ typed_input: true,
83
+ unsupported_action_kinds: [],
84
+ external_surfaces: 0,
85
+ app_type: 'web',
86
+ });
87
+
88
+ // Legacy index-keyed artifacts (article_blocks.js synthesizes sections).
89
+ signals = computeSignals(articleLegacy(), viewSourcesSteps(), 'web');
90
+ assert.strictEqual(signals.sections_total, 2);
91
+ assert.strictEqual(signals.sections_with_images, 1);
92
+ assert.strictEqual(signals.distinct_surfaces, 1);
93
+ assert.strictEqual(signals.action_count, 1);
94
+ assert.strictEqual(signals.typed_input, false);
95
+
96
+ // External-surface entries count separately and never contribute a surface.
97
+ signals = computeSignals(articleV2(), viewSourcesBlocks({
98
+ blocks: [
99
+ { block_id: 'open-settings', primary_view: 'app/views/settings/index.html.erb' },
100
+ { block_id: 'enter-email', url_or_route: 'external:confirmation-email',
101
+ external_surface: { asset_id: 'confirmation-email', surface: 'email' } },
102
+ ],
103
+ }), 'web');
104
+ assert.strictEqual(signals.external_surfaces, 1);
105
+ assert.strictEqual(signals.distinct_surfaces, 1);
106
+
107
+ // Recorder-unsupported kinds are surfaced; the supported set is not.
108
+ signals = computeSignals(articleV2(), viewSourcesBlocks({
109
+ action_coverage: [
110
+ { article_block_id: 'open-settings', kind: 'drag', target: 'Card', state: 'action-ready' },
111
+ { article_block_id: 'enter-email', kind: 'upload', target: 'Attach', state: 'action-ready' },
112
+ { article_block_id: 'confirm', kind: 'toggle', target: 'Notify', state: 'action-ready' },
113
+ ],
114
+ }), 'mobile');
115
+ assert.deepStrictEqual(signals.unsupported_action_kinds, ['drag', 'upload']);
116
+ assert.strictEqual(signals.app_type, 'mobile');
117
+
118
+ // Tolerates absent action_coverage / malformed entries (pre-v1.41 artifacts).
119
+ signals = computeSignals(articleLegacy(), { framework: 'rails', steps: [null, { index: 1 }] }, 'web');
120
+ assert.strictEqual(signals.action_count, 0);
121
+ assert.deepStrictEqual(signals.action_kinds, []);
122
+ assert.strictEqual(signals.distinct_surfaces, 0);
123
+
124
+ // CLI: writes the sidecar for a valid artifact dir.
125
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-walkthrough-signals-'));
126
+ try {
127
+ fs.writeFileSync(path.join(root, 'article.json'), JSON.stringify(articleV2()));
128
+ fs.writeFileSync(path.join(root, 'view_sources.json'), JSON.stringify(viewSourcesBlocks()));
129
+ let cli = spawnSync(process.execPath, [SCRIPT, root], { encoding: 'utf8' });
130
+ assert.strictEqual(cli.status, 0, cli.stderr);
131
+ const written = JSON.parse(fs.readFileSync(path.join(root, 'walkthrough_signals.json'), 'utf8'));
132
+ assert.strictEqual(written.schema_version, 1);
133
+ assert.strictEqual(written.distinct_surfaces, 2);
134
+
135
+ // CLI: missing view_sources.json is a warning + exit 0, no file written.
136
+ const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-walkthrough-signals-'));
137
+ try {
138
+ fs.writeFileSync(path.join(empty, 'article.json'), JSON.stringify(articleV2()));
139
+ cli = spawnSync(process.execPath, [SCRIPT, empty], { encoding: 'utf8' });
140
+ assert.strictEqual(cli.status, 0, cli.stderr);
141
+ assert.match(cli.stderr, /walkthrough signals skipped/);
142
+ assert.strictEqual(fs.existsSync(path.join(empty, 'walkthrough_signals.json')), false);
143
+ } finally {
144
+ fs.rmSync(empty, { recursive: true, force: true });
145
+ }
146
+
147
+ // CLI: no argument is a usage error.
148
+ cli = spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8' });
149
+ assert.strictEqual(cli.status, 2);
150
+ } finally {
151
+ fs.rmSync(root, { recursive: true, force: true });
152
+ }
153
+
154
+ console.log('walkthrough signals tests passed');
155
+ }
156
+
157
+ run();