@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,653 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const lint = path.join(__dirname, 'lint_mockup_fidelity.js');
|
|
9
|
+
const renderAll = path.join(__dirname, 'render_all.js');
|
|
10
|
+
|
|
11
|
+
function write(file, value) {
|
|
12
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
13
|
+
fs.writeFileSync(file, typeof value === 'string' ? value : JSON.stringify(value, null, 2));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fixture(mutator = () => {}) {
|
|
17
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-lint-'));
|
|
18
|
+
const out = path.join(root, 'out');
|
|
19
|
+
const source = 'views/runtime.html';
|
|
20
|
+
const projectMap = {
|
|
21
|
+
schema_version: 5,
|
|
22
|
+
app_type: 'web',
|
|
23
|
+
app_shell: null,
|
|
24
|
+
layouts: [{
|
|
25
|
+
name: 'runtime', path: source, chrome: [], root_classes: { html: [], body: [] },
|
|
26
|
+
runtime_chrome: {
|
|
27
|
+
app: 'Editor', source_kind: 'repo', source_files: [source],
|
|
28
|
+
regions: [
|
|
29
|
+
{ id: 'toolbar', region: 'toolbar', placement: 'top', required: true,
|
|
30
|
+
items: [{ id: 'actions', kind: 'control-group', description: 'Editor actions', required_strings: ['Save'] }],
|
|
31
|
+
appearance: 'Light compact command bar', ui_strings: ['Save'],
|
|
32
|
+
required_strings: ['Save'], source_files: [source] },
|
|
33
|
+
{ id: 'canvas', region: 'canvas', placement: 'canvas', required: true,
|
|
34
|
+
items: [{ id: 'document', kind: 'content-section', description: 'Populated document', required_strings: ['Document'] }],
|
|
35
|
+
appearance: 'Large white document canvas', content_mode: 'populated',
|
|
36
|
+
media_expectation: 'none', representative_assets: [], max_selected_outlines: 1,
|
|
37
|
+
ui_strings: ['Document'],
|
|
38
|
+
required_strings: ['Document'], source_files: [source] },
|
|
39
|
+
],
|
|
40
|
+
states: [{ id: 'editing', name: 'editing', kind: 'action', screenshot_required: true,
|
|
41
|
+
topic_tags: ['edit'], instructional_priority: 'primary-action',
|
|
42
|
+
context_label: 'Document', canvas_presentation: 'Populated full editing canvas.',
|
|
43
|
+
entered_by: 'Open', shows: 'Document open', required_regions: [],
|
|
44
|
+
visible_regions: ['toolbar', 'canvas'], required_strings: ['Document'], source_files: [source] }],
|
|
45
|
+
},
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
const viewSources = {
|
|
49
|
+
framework: 'test',
|
|
50
|
+
runtime_state_selection: { layout: 'runtime', selected: ['editing'], dropped: [] },
|
|
51
|
+
steps: [{ index: 0, url_or_route: '/edit', controller_action: source,
|
|
52
|
+
primary_view: source, layout: 'runtime', partials_expanded: [source],
|
|
53
|
+
runtime_state_id: 'editing', runtime_source_files: [source],
|
|
54
|
+
depicted_state: 'Document open for editing', default_user_assumptions: [],
|
|
55
|
+
verbatim_evidence: ['Save', 'Document', 'Editor'] }],
|
|
56
|
+
};
|
|
57
|
+
let html = '<!doctype html><html><head><style>body{color:#111}</style></head>' +
|
|
58
|
+
'<body data-rtfm-state="editing"><header data-rtfm-region="toolbar" data-rtfm-placement="top"><span data-rtfm-item="toolbar:actions">Save Editor</span></header>' +
|
|
59
|
+
'<main data-rtfm-region="canvas" data-rtfm-placement="canvas"><article data-rtfm-item="canvas:document">Document</article></main></body></html>';
|
|
60
|
+
const state = { projectMap, viewSources, generatedManifest: null, article: null, skipHtml: false, blockId: null, extraFiles: {},
|
|
61
|
+
get html() { return html; }, set html(v) { html = v; } };
|
|
62
|
+
mutator(state);
|
|
63
|
+
write(path.join(root, '.rtfm', 'project_map.json'), projectMap);
|
|
64
|
+
write(path.join(root, source), 'Save Document Editor');
|
|
65
|
+
for (const [rel, text] of Object.entries(state.extraFiles || {})) write(path.join(root, rel), text);
|
|
66
|
+
write(path.join(out, 'view_sources.json'), viewSources);
|
|
67
|
+
if (state.article) write(path.join(out, 'article.json'), state.article);
|
|
68
|
+
if (state.generatedManifest) write(path.join(out, 'generated_images.json'), state.generatedManifest);
|
|
69
|
+
if (!state.skipHtml) write(path.join(out, state.blockId ? `block_${state.blockId}.html` : 'step_0.html'), html);
|
|
70
|
+
write(path.join(out, 'branding.css'), 'body{color:#111}');
|
|
71
|
+
const env = { ...process.env, ...(state.env || {}) };
|
|
72
|
+
if (!state.env || !('RTFM_MAX_IMAGES' in state.env)) delete env.RTFM_MAX_IMAGES;
|
|
73
|
+
const proc = spawnSync(process.execPath, [lint, out, root], { encoding: 'utf8', env });
|
|
74
|
+
const report = JSON.parse(fs.readFileSync(path.join(out, 'lint_report.json'), 'utf8'));
|
|
75
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
76
|
+
return { status: proc.status, report };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let result = fixture();
|
|
80
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
81
|
+
|
|
82
|
+
result = fixture(s => {
|
|
83
|
+
s.blockId = 'edit-document';
|
|
84
|
+
s.article = { schema_version: 2, title: 'How to edit', article_type: 'how-to', blocks: [
|
|
85
|
+
{ id: 'edit-document', type: 'section', presentation: 'numbered', title: 'Edit the document', content: 'Review the document.', has_image: true },
|
|
86
|
+
] };
|
|
87
|
+
s.viewSources.blocks = [{ ...s.viewSources.steps[0], block_id: 'edit-document' }];
|
|
88
|
+
delete s.viewSources.steps;
|
|
89
|
+
});
|
|
90
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
91
|
+
assert.strictEqual(result.report.blocks[0].block_id, 'edit-document');
|
|
92
|
+
|
|
93
|
+
// view_sources lists only illustrated sections; blocks are numbered by their
|
|
94
|
+
// section's position in article.json, so a screenshot on a later section (and
|
|
95
|
+
// coverage from a text-only section) lines up with the article's steps.
|
|
96
|
+
const laterSection = s => {
|
|
97
|
+
s.blockId = 'edit-document';
|
|
98
|
+
s.article = { schema_version: 2, title: 'How to edit', article_type: 'how-to', blocks: [
|
|
99
|
+
{ id: 'intro', type: 'prose', presentation: 'lead', content: 'Edit a document in the editor.' },
|
|
100
|
+
{ id: 'before-you-start', type: 'section', presentation: 'numbered', title: 'Before you start', content: 'Documents open in the editor.', has_image: false },
|
|
101
|
+
{ id: 'edit-document', type: 'section', presentation: 'numbered', title: 'Edit the document', content: 'Review the document.', has_image: true },
|
|
102
|
+
{ id: 'save-changes', type: 'section', presentation: 'numbered', title: 'Save your changes', content: 'Select **Save**.', has_image: false },
|
|
103
|
+
] };
|
|
104
|
+
s.viewSources.blocks = [{ ...s.viewSources.steps[0], block_id: 'edit-document' }];
|
|
105
|
+
delete s.viewSources.steps;
|
|
106
|
+
s.viewSources.action_coverage = [{ article_block_id: 'save-changes', screenshot_block_id: 'edit-document',
|
|
107
|
+
kind: 'click', target: 'Save', state: 'action-ready' }];
|
|
108
|
+
s.html = s.html.replace('data-rtfm-item="toolbar:actions"',
|
|
109
|
+
'data-rtfm-item="toolbar:actions" data-rtfm-action-target="save-changes"');
|
|
110
|
+
};
|
|
111
|
+
result = fixture(laterSection);
|
|
112
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
113
|
+
assert.strictEqual(result.report.blocks[0].block_id, 'edit-document');
|
|
114
|
+
|
|
115
|
+
result = fixture(s => { laterSection(s); s.viewSources.blocks[0].block_id = 'edit-doc'; s.blockId = 'edit-doc'; });
|
|
116
|
+
assert.strictEqual(result.status, 1);
|
|
117
|
+
assert(result.report.global_errors.some(e => e.includes("block 'edit-doc' names no section in article.json")),
|
|
118
|
+
JSON.stringify(result.report.global_errors));
|
|
119
|
+
|
|
120
|
+
result = fixture(s => { s.html = s.html.replace(' data-rtfm-region="canvas"', ''); });
|
|
121
|
+
assert.strictEqual(result.status, 1);
|
|
122
|
+
assert(result.report.steps[0].errors.some(e => e.includes("runtime region 'canvas'")));
|
|
123
|
+
|
|
124
|
+
result = fixture(s => {
|
|
125
|
+
s.projectMap.layouts[0].runtime_chrome.regions.push({
|
|
126
|
+
id: 'inspector', region: 'inspector', placement: 'right', required: false,
|
|
127
|
+
items: [{ id: 'settings', kind: 'settings-list', description: 'Settings', required_strings: [] }],
|
|
128
|
+
appearance: 'Right settings inspector', ui_strings: [],
|
|
129
|
+
required_strings: [], source_files: [s.projectMap.layouts[0].path],
|
|
130
|
+
});
|
|
131
|
+
s.html = s.html.replace('</body>', '<aside data-rtfm-region="inspector" data-rtfm-placement="right">Inspector</aside></body>');
|
|
132
|
+
});
|
|
133
|
+
assert.strictEqual(result.status, 1);
|
|
134
|
+
assert(result.report.steps[0].errors.some(e => e.includes("is not visible in state 'editing'")));
|
|
135
|
+
|
|
136
|
+
result = fixture(s => {
|
|
137
|
+
const canvas = s.projectMap.layouts[0].runtime_chrome.regions[1];
|
|
138
|
+
canvas.media_expectation = 'required';
|
|
139
|
+
canvas.representative_assets = [s.projectMap.layouts[0].path];
|
|
140
|
+
});
|
|
141
|
+
assert.strictEqual(result.status, 1);
|
|
142
|
+
assert(result.report.steps[0].errors.some(e => e.includes('runtime canvas asset')));
|
|
143
|
+
|
|
144
|
+
result = fixture(s => { s.viewSources.steps[0].runtime_state_id = 'invented'; });
|
|
145
|
+
assert.strictEqual(result.status, 1);
|
|
146
|
+
assert(result.report.steps[0].errors.some(e => e.includes('valid runtime_state_id')));
|
|
147
|
+
|
|
148
|
+
result = fixture(s => { s.viewSources.steps[0].runtime_source_files = ['views/adjacent.html']; });
|
|
149
|
+
assert.strictEqual(result.status, 1);
|
|
150
|
+
assert(result.report.steps[0].errors.some(e => e.includes('runtime_source_files must exactly match')));
|
|
151
|
+
|
|
152
|
+
result = fixture(s => {
|
|
153
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, model: 'gpt-image-2', assets: {
|
|
154
|
+
'participant-alex': { status: 'generated', path: 'generated-assets/participant-alex.png',
|
|
155
|
+
kind: 'video-frame', reason: 'A live camera feed is required and no repo image exists',
|
|
156
|
+
used_in_steps: [0] },
|
|
157
|
+
} };
|
|
158
|
+
s.html = s.html.replace('</article>', '<img src="{{generated:participant-alex}}" data-rtfm-generated-asset="participant-alex" alt="Fictional participant"></article>');
|
|
159
|
+
});
|
|
160
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
161
|
+
|
|
162
|
+
result = fixture(s => {
|
|
163
|
+
s.projectMap.layouts = [];
|
|
164
|
+
delete s.viewSources.runtime_state_selection;
|
|
165
|
+
s.viewSources.steps = [];
|
|
166
|
+
s.viewSources.action_coverage = [];
|
|
167
|
+
s.viewSources.generation_omissions = [{
|
|
168
|
+
asset_id: 'confirm-email', article_step_indexes: [0],
|
|
169
|
+
reason: 'Image generation failed; the external step is text-only.',
|
|
170
|
+
}];
|
|
171
|
+
s.article = { title: 'How to confirm', article_type: 'how-to',
|
|
172
|
+
introduction: 'Confirm your account.', prerequisites: [],
|
|
173
|
+
steps: [{ title: 'Confirm', content: 'Select **Confirm email**.', has_image: false }],
|
|
174
|
+
tips: [], summary: 'The account is confirmed.' };
|
|
175
|
+
s.generatedManifest = { version: 1, max_assets: 5, requested_count: 1,
|
|
176
|
+
count: 0, failure_count: 1, model: 'gpt-image-2', assets: {}, failures: {
|
|
177
|
+
'confirm-email': { status: 'failed', kind: 'external-surface',
|
|
178
|
+
reason: 'External email required', used_in_steps: [0] },
|
|
179
|
+
} };
|
|
180
|
+
s.skipHtml = true;
|
|
181
|
+
});
|
|
182
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
183
|
+
|
|
184
|
+
result = fixture(s => {
|
|
185
|
+
s.projectMap.layouts = [];
|
|
186
|
+
delete s.viewSources.runtime_state_selection;
|
|
187
|
+
s.viewSources.steps = [];
|
|
188
|
+
s.viewSources.action_coverage = [];
|
|
189
|
+
s.viewSources.generation_omissions = [{
|
|
190
|
+
asset_id: 'confirm-email', article_step_indexes: [0], reason: 'Missing image.',
|
|
191
|
+
}];
|
|
192
|
+
s.article = { title: 'How to confirm', article_type: 'how-to',
|
|
193
|
+
introduction: 'Confirm your account.', prerequisites: [],
|
|
194
|
+
steps: [{ title: 'Confirm', content: 'Select **Confirm email**.', has_image: false }],
|
|
195
|
+
tips: [], summary: 'The account is confirmed.' };
|
|
196
|
+
s.skipHtml = true;
|
|
197
|
+
});
|
|
198
|
+
assert.strictEqual(result.status, 1);
|
|
199
|
+
assert(result.report.global_errors.some(e => e.includes('must match a failed external-surface request')));
|
|
200
|
+
|
|
201
|
+
result = fixture(s => {
|
|
202
|
+
const step = s.viewSources.steps[0];
|
|
203
|
+
delete step.primary_view;
|
|
204
|
+
delete step.layout;
|
|
205
|
+
delete step.partials_expanded;
|
|
206
|
+
delete step.runtime_state_id;
|
|
207
|
+
delete step.runtime_source_files;
|
|
208
|
+
step.url_or_route = 'external:confirmation-email';
|
|
209
|
+
step.verbatim_evidence = [];
|
|
210
|
+
step.external_surface = {
|
|
211
|
+
asset_id: 'confirm-email',
|
|
212
|
+
surface: 'confirmation-email',
|
|
213
|
+
source_basis: 'user-description',
|
|
214
|
+
source_files: [],
|
|
215
|
+
required_text: ['Confirm your email address', 'Confirm email'],
|
|
216
|
+
};
|
|
217
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, model: 'gpt-image-2', assets: {
|
|
218
|
+
'confirm-email': { status: 'generated', path: 'generated-assets/confirm-email.png',
|
|
219
|
+
kind: 'external-surface', surface: 'confirmation-email',
|
|
220
|
+
source_basis: 'user-description', source_files: [],
|
|
221
|
+
required_text: ['Confirm your email address', 'Confirm email'],
|
|
222
|
+
text_fidelity: { enforcement: 'prompt-constrained', required_text_count: 2 },
|
|
223
|
+
reason: 'The email is outside the product and no renderable template exists',
|
|
224
|
+
used_in_steps: [0] },
|
|
225
|
+
} };
|
|
226
|
+
s.html = '<!doctype html><html><head></head><body data-rtfm-surface="external">' +
|
|
227
|
+
'<img style="position:fixed;inset:0;width:100vw;height:100vh" ' +
|
|
228
|
+
'src="{{generated:confirm-email}}" data-rtfm-generated-asset="confirm-email" ' +
|
|
229
|
+
'data-rtfm-external-surface="confirmation-email" alt="Confirmation email"></body></html>';
|
|
230
|
+
});
|
|
231
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
232
|
+
|
|
233
|
+
result = fixture(s => {
|
|
234
|
+
const step = s.viewSources.steps[0];
|
|
235
|
+
step.external_surface = { asset_id: 'confirm-email', surface: 'confirmation-email',
|
|
236
|
+
source_basis: 'user-description', source_files: [], required_text: ['Wrong text'] };
|
|
237
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, model: 'gpt-image-2', assets: {
|
|
238
|
+
'confirm-email': { kind: 'external-surface', surface: 'confirmation-email',
|
|
239
|
+
source_basis: 'user-description', source_files: [],
|
|
240
|
+
required_text: ['Confirm email'],
|
|
241
|
+
text_fidelity: { enforcement: 'prompt-constrained', required_text_count: 1 },
|
|
242
|
+
reason: 'External email required', used_in_steps: [0] },
|
|
243
|
+
} };
|
|
244
|
+
s.html = s.html.replace('<body ', '<body data-rtfm-surface="external" ')
|
|
245
|
+
.replace('</article>', '<img src="{{generated:confirm-email}}" data-rtfm-generated-asset="confirm-email" data-rtfm-external-surface="confirmation-email"></article>');
|
|
246
|
+
});
|
|
247
|
+
assert.strictEqual(result.status, 1);
|
|
248
|
+
assert(result.report.steps[0].errors.some(e => e.includes('does not exactly match')));
|
|
249
|
+
assert(result.report.steps[0].errors.some(e => e.includes('standalone external step')));
|
|
250
|
+
|
|
251
|
+
result = fixture(s => {
|
|
252
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, assets: {
|
|
253
|
+
'participant-alex': { kind: 'video-frame', reason: 'Camera content required', used_in_steps: [0] },
|
|
254
|
+
} };
|
|
255
|
+
s.html = s.html.replace('</article>', '<div src="{{generated:participant-alex}}" data-rtfm-generated-asset="participant-alex"></div></article>');
|
|
256
|
+
});
|
|
257
|
+
assert.strictEqual(result.status, 1);
|
|
258
|
+
assert(result.report.steps[0].errors.some(e => e.includes('must appear on an <img>')));
|
|
259
|
+
|
|
260
|
+
result = fixture(s => {
|
|
261
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, assets: {
|
|
262
|
+
'participant-alex': { kind: 'video-frame', reason: 'Camera content required', used_in_steps: [] },
|
|
263
|
+
} };
|
|
264
|
+
s.html = s.html.replace('</head>', '<style>body{background-image:url({{generated:participant-alex}})}</style></head>');
|
|
265
|
+
});
|
|
266
|
+
assert.strictEqual(result.status, 1);
|
|
267
|
+
assert(result.report.steps[0].errors.some(e => e.includes('CSS backgrounds')));
|
|
268
|
+
|
|
269
|
+
result = fixture(s => {
|
|
270
|
+
s.generatedManifest = { version: 1, max_assets: 5, count: 1, assets: {
|
|
271
|
+
'participant-alex': { kind: 'video-frame', reason: 'Camera content required', used_in_steps: [0] },
|
|
272
|
+
} };
|
|
273
|
+
s.html = s.html.replace('</article>', '<img style="position:fixed;inset:0" src="{{generated:participant-alex}}" data-rtfm-generated-asset="participant-alex"></article>');
|
|
274
|
+
});
|
|
275
|
+
assert.strictEqual(result.status, 1);
|
|
276
|
+
assert(result.report.steps[0].errors.some(e => e.includes('screen/region-sized')));
|
|
277
|
+
|
|
278
|
+
result = fixture(s => {
|
|
279
|
+
const recipe = s.projectMap.layouts[0].runtime_chrome;
|
|
280
|
+
recipe.source_kind = 'knowledge';
|
|
281
|
+
recipe.source_files = [];
|
|
282
|
+
recipe.regions.forEach(r => { r.source_files = []; });
|
|
283
|
+
recipe.states.forEach(st => { st.source_files = []; });
|
|
284
|
+
s.viewSources.steps[0].runtime_source_files = [];
|
|
285
|
+
});
|
|
286
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
287
|
+
|
|
288
|
+
result = fixture(s => {
|
|
289
|
+
s.article = { title: 'How to save', article_type: 'how-to', introduction: 'Save a document.',
|
|
290
|
+
prerequisites: [], steps: [{ title: 'Save', content: 'Select **Save**.', has_image: true }],
|
|
291
|
+
tips: [], summary: 'The document is saved.' };
|
|
292
|
+
s.viewSources.action_coverage = [{ article_step_index: 0, screenshot_step_index: 0,
|
|
293
|
+
kind: 'click', target: 'Save', state: 'action-ready' }];
|
|
294
|
+
s.html = s.html.replace('data-rtfm-item="toolbar:actions"',
|
|
295
|
+
'data-rtfm-item="toolbar:actions" data-rtfm-action-target="0"');
|
|
296
|
+
});
|
|
297
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
298
|
+
|
|
299
|
+
result = fixture(s => {
|
|
300
|
+
s.article = { title: 'How to type', article_type: 'how-to', introduction: 'Edit a document.',
|
|
301
|
+
prerequisites: [], steps: [{ title: 'Enter text',
|
|
302
|
+
content: 'In the editor, enter text in **Document**.', has_image: true }],
|
|
303
|
+
tips: [], summary: 'The document contains the text.' };
|
|
304
|
+
s.viewSources.action_coverage = [{ article_step_index: 0, screenshot_step_index: 0,
|
|
305
|
+
kind: 'type', target: 'Document', state: 'action-ready' }];
|
|
306
|
+
s.html = s.html.replace('data-rtfm-item="canvas:document"',
|
|
307
|
+
'data-rtfm-item="canvas:document" data-rtfm-action-target="0"');
|
|
308
|
+
});
|
|
309
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
310
|
+
|
|
311
|
+
result = fixture(s => {
|
|
312
|
+
s.article = { title: 'How to save', article_type: 'how-to', introduction: 'Save a document.',
|
|
313
|
+
prerequisites: [], steps: [{ title: 'Save', content: 'Select **Save**.', has_image: true }],
|
|
314
|
+
tips: [], summary: 'The document is saved.' };
|
|
315
|
+
s.viewSources.action_coverage = [{ article_step_index: 0, screenshot_step_index: 0,
|
|
316
|
+
kind: 'click', target: 'Save', state: 'after' }];
|
|
317
|
+
s.html = s.html.replace('data-rtfm-item="toolbar:actions"',
|
|
318
|
+
'data-rtfm-item="toolbar:actions" data-rtfm-action-target="0"');
|
|
319
|
+
});
|
|
320
|
+
assert.strictEqual(result.status, 1);
|
|
321
|
+
assert(result.report.global_errors.some(e => e.includes('must use state "action-ready"')));
|
|
322
|
+
|
|
323
|
+
result = fixture(s => {
|
|
324
|
+
s.article = { title: 'How to save', article_type: 'how-to', introduction: 'Save a document.',
|
|
325
|
+
prerequisites: [], steps: [{ title: 'Save', content: 'Select **Save**.', has_image: true }],
|
|
326
|
+
tips: [], summary: 'The document is saved.' };
|
|
327
|
+
});
|
|
328
|
+
assert.strictEqual(result.status, 1);
|
|
329
|
+
assert(result.report.global_errors.some(e => e.includes('requires an action_coverage array')));
|
|
330
|
+
assert(result.report.global_errors.some(e => e.includes('lack action-ready screenshot coverage')));
|
|
331
|
+
|
|
332
|
+
result = fixture(s => {
|
|
333
|
+
s.article = { title: 'How to record', article_type: 'how-to', introduction: 'Record a meeting.',
|
|
334
|
+
prerequisites: [], steps: [{ title: 'Start', content: 'Select **Start Recording**.', has_image: true }],
|
|
335
|
+
tips: [], summary: 'Recording is active.' };
|
|
336
|
+
s.viewSources.action_coverage = [{ article_step_index: 0, screenshot_step_index: 0,
|
|
337
|
+
kind: 'click', target: 'Start Recording', state: 'action-ready' }];
|
|
338
|
+
s.html = s.html.replace('data-rtfm-item="toolbar:actions"',
|
|
339
|
+
'data-rtfm-item="toolbar:actions" data-rtfm-action-target="0"');
|
|
340
|
+
});
|
|
341
|
+
assert.strictEqual(result.status, 1);
|
|
342
|
+
assert(result.report.global_errors.some(e => e.includes('does not contain visible/accessibility text "Start Recording"')));
|
|
343
|
+
assert(result.report.global_errors.some(e => e.includes('must also appear exactly in verbatim_evidence')));
|
|
344
|
+
|
|
345
|
+
result = fixture(s => {
|
|
346
|
+
s.article = { title: 'How to edit', article_type: 'how-to', introduction: 'Edit a document.',
|
|
347
|
+
prerequisites: [], steps: [
|
|
348
|
+
{ title: 'Open', content: 'Open **Document**.', has_image: true },
|
|
349
|
+
{ title: 'Save', content: 'Select **Save**.', has_image: false },
|
|
350
|
+
], tips: [], summary: 'The document is saved.' };
|
|
351
|
+
s.viewSources.action_coverage = [
|
|
352
|
+
{ article_step_index: 0, screenshot_step_index: 0, kind: 'click', target: 'Document', state: 'action-ready' },
|
|
353
|
+
{ article_step_index: 1, screenshot_step_index: 0, kind: 'click', target: 'Save', state: 'action-ready' },
|
|
354
|
+
];
|
|
355
|
+
s.html = s.html
|
|
356
|
+
.replace('data-rtfm-item="toolbar:actions"',
|
|
357
|
+
'data-rtfm-item="toolbar:actions" data-rtfm-action-target="1"')
|
|
358
|
+
.replace('data-rtfm-item="canvas:document"',
|
|
359
|
+
'data-rtfm-item="canvas:document" data-rtfm-action-target="0"');
|
|
360
|
+
});
|
|
361
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
362
|
+
|
|
363
|
+
result = fixture(s => {
|
|
364
|
+
s.article = { title: 'How to record', article_type: 'how-to', introduction: 'Record a meeting.',
|
|
365
|
+
prerequisites: [], steps: [
|
|
366
|
+
{ title: 'Open', content: 'Review Home.', has_image: false },
|
|
367
|
+
{ title: 'Start', content: 'Recording begins.', has_image: false },
|
|
368
|
+
{ title: 'Confirm', content: 'Confirm recording is active.', has_image: true },
|
|
369
|
+
], tips: [], summary: 'Recording is active.' };
|
|
370
|
+
s.viewSources.steps[0].index = 3;
|
|
371
|
+
});
|
|
372
|
+
assert.strictEqual(result.status, 1);
|
|
373
|
+
assert(result.report.global_errors.some(e => e.includes('outside article.json\'s zero-based step range')));
|
|
374
|
+
assert(result.report.global_errors.some(e => e.includes('has no zero-based step 2')));
|
|
375
|
+
|
|
376
|
+
// ─── Deterministic metrics contract (additive; never affects errors/exit) ────
|
|
377
|
+
const METRIC_KEYS = ['shell_nav_labels', 'chrome_files_expanded', 'runtime_ui_strings', 'runtime_regions_rendered',
|
|
378
|
+
'root_classes_html', 'layout_root_classes', 'verbatim_evidence_source', 'verbatim_evidence_html',
|
|
379
|
+
'partials_present', 'body_class_tokens', 'styled_coverage', 'inline_classes_verified',
|
|
380
|
+
'invented_colours', 'invented_copy', 'screen_closure'];
|
|
381
|
+
const ratio = (hits, total) => ({ hits, total, ratio: Math.round((hits / total) * 1000) / 1000 });
|
|
382
|
+
const allNull = () => Object.fromEntries(METRIC_KEYS.map(k => [k, null]));
|
|
383
|
+
function errorsTotal(report) {
|
|
384
|
+
return report.global_errors.length + (report.steps || report.blocks).reduce((n, s) => n + s.errors.length, 0);
|
|
385
|
+
}
|
|
386
|
+
function warningsTotal(report) {
|
|
387
|
+
return report.global_warnings.length + (report.steps || report.blocks).reduce((n, s) => n + s.warnings.length, 0);
|
|
388
|
+
}
|
|
389
|
+
function checkTotals(report) {
|
|
390
|
+
assert.strictEqual(report.metrics.errors_total, errorsTotal(report));
|
|
391
|
+
assert.strictEqual(report.metrics.warnings_total, warningsTotal(report));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Baseline: key order is the contract; data-gated checks are null, never 0/0.
|
|
395
|
+
result = fixture();
|
|
396
|
+
assert.deepStrictEqual(Object.keys(result.report),
|
|
397
|
+
['framework', 'project_dir', 'global_errors', 'global_warnings', 'steps', 'all_passed', 'metrics']);
|
|
398
|
+
const baselineMetrics = result.report.steps[0].metrics;
|
|
399
|
+
assert.deepStrictEqual(Object.keys(baselineMetrics), METRIC_KEYS);
|
|
400
|
+
assert.deepStrictEqual(baselineMetrics, { ...allNull(),
|
|
401
|
+
runtime_ui_strings: ratio(2, 2), runtime_regions_rendered: ratio(2, 2),
|
|
402
|
+
verbatim_evidence_source: ratio(3, 3), verbatim_evidence_html: ratio(3, 3),
|
|
403
|
+
partials_present: ratio(1, 1), invented_colours: { count: 0 }, invented_copy: { count: 0 } });
|
|
404
|
+
assert.deepStrictEqual(result.report.metrics, { action_coverage: null,
|
|
405
|
+
runtime_states_selected: ratio(1, 1), undefined_css_vars: { count: 0 }, errors_total: 0, warnings_total: 0 });
|
|
406
|
+
|
|
407
|
+
// Block mode carries the same per-entry metrics.
|
|
408
|
+
result = fixture(s => {
|
|
409
|
+
s.blockId = 'edit-document';
|
|
410
|
+
s.article = { schema_version: 2, title: 'How to edit', article_type: 'how-to', blocks: [
|
|
411
|
+
{ id: 'edit-document', type: 'section', presentation: 'numbered', title: 'Edit the document', content: 'Review the document.', has_image: true },
|
|
412
|
+
] };
|
|
413
|
+
s.viewSources.blocks = [{ ...s.viewSources.steps[0], block_id: 'edit-document' }];
|
|
414
|
+
delete s.viewSources.steps;
|
|
415
|
+
});
|
|
416
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
417
|
+
assert.deepStrictEqual(result.report.blocks[0].metrics, baselineMetrics);
|
|
418
|
+
assert(Object.keys(result.report).includes('blocks') && !Object.keys(result.report).includes('steps'));
|
|
419
|
+
checkTotals(result.report);
|
|
420
|
+
|
|
421
|
+
// Region removed: ratio drops, totals agree with the error lists.
|
|
422
|
+
result = fixture(s => { s.html = s.html.replace(' data-rtfm-region="canvas"', ''); });
|
|
423
|
+
assert.strictEqual(result.status, 1);
|
|
424
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.runtime_regions_rendered, ratio(1, 2));
|
|
425
|
+
checkTotals(result.report);
|
|
426
|
+
|
|
427
|
+
// Evidence miss: both evidence ratios drop; still two errors.
|
|
428
|
+
result = fixture(s => { s.viewSources.steps[0].verbatim_evidence = ['Save', 'Document', 'Missing']; });
|
|
429
|
+
assert.strictEqual(result.status, 1);
|
|
430
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.verbatim_evidence_source, ratio(2, 3));
|
|
431
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.verbatim_evidence_html, ratio(2, 3));
|
|
432
|
+
assert.strictEqual(result.report.metrics.errors_total, 2);
|
|
433
|
+
checkTotals(result.report);
|
|
434
|
+
|
|
435
|
+
// Invented-copy cap: the message keeps its 10-entry cap, the metric counts all 12.
|
|
436
|
+
result = fixture(s => {
|
|
437
|
+
const phrases = ['quantum lattice harmonics resonate', 'velvet orbital cadence shimmer',
|
|
438
|
+
'granite pendulum whispers linger', 'crimson tidal vectors unfold', 'saffron glacier murmurs drift',
|
|
439
|
+
'obsidian meadow currents ripple', 'cobalt thunder ribbons weave', 'amber canyon echoes tumble',
|
|
440
|
+
'violet ember spirals ascend', 'silver monsoon lanterns flicker', 'copper zephyr mosaics glisten',
|
|
441
|
+
'indigo harbor whistles scatter'];
|
|
442
|
+
s.html = s.html.replace('</main>', '</main><p>' + phrases.map(p => p + '.').join(' ') + '</p>');
|
|
443
|
+
});
|
|
444
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
445
|
+
assert(result.report.steps[0].warnings.some(w => w.startsWith('10 multi-word phrase(s)')), JSON.stringify(result.report.steps[0].warnings));
|
|
446
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.invented_copy, { count: 12 });
|
|
447
|
+
checkTotals(result.report);
|
|
448
|
+
|
|
449
|
+
// Shell + chrome positive fixture: nav labels, chrome checklist, root classes.
|
|
450
|
+
const shellMutator = htmlClass => s => {
|
|
451
|
+
const source = s.projectMap.layouts[0].path;
|
|
452
|
+
s.projectMap.app_shell = { nav_items: [{ label: 'Dashboard' }, { label: 'Pages' }, { label: 'Settings' }],
|
|
453
|
+
root_classes: { html: ['wp-toolbar'], body: [] } };
|
|
454
|
+
delete s.projectMap.layouts[0].root_classes;
|
|
455
|
+
s.projectMap.layouts[0].chrome = ['views/_header.html', 'views/_footer.html', 'sidebar', 'the top bar prose'];
|
|
456
|
+
s.extraFiles = { 'views/_header.html': 'Header', 'views/_footer.html': 'Footer' };
|
|
457
|
+
s.viewSources.steps[0].partials_expanded = [source, 'views/_header.html'];
|
|
458
|
+
s.html = s.html.replace('<header data-rtfm-region="toolbar"', '<nav>Dashboard Pages Settings</nav><header data-rtfm-region="toolbar"');
|
|
459
|
+
if (htmlClass) s.html = s.html.replace('<html>', `<html class="${htmlClass}">`);
|
|
460
|
+
};
|
|
461
|
+
result = fixture(shellMutator(null));
|
|
462
|
+
assert.strictEqual(result.status, 1);
|
|
463
|
+
let m = result.report.steps[0].metrics;
|
|
464
|
+
assert.deepStrictEqual(m.shell_nav_labels, ratio(3, 3));
|
|
465
|
+
assert.deepStrictEqual(m.chrome_files_expanded, ratio(1, 3));
|
|
466
|
+
assert.deepStrictEqual(m.root_classes_html, ratio(0, 1));
|
|
467
|
+
assert.deepStrictEqual(m.layout_root_classes, null);
|
|
468
|
+
assert.deepStrictEqual(m.partials_present, ratio(2, 2));
|
|
469
|
+
assert(result.report.steps[0].errors.some(e => e.includes('never expanded its recorded chrome file(s): views/_footer.html')));
|
|
470
|
+
assert(result.report.steps[0].errors.some(e => e.includes('missing class token(s) recorded in app_shell.root_classes: wp-toolbar')));
|
|
471
|
+
assert(result.report.steps[0].warnings.some(e => e.includes('records chrome sidebar')));
|
|
472
|
+
checkTotals(result.report);
|
|
473
|
+
result = fixture(shellMutator('wp-toolbar'));
|
|
474
|
+
m = result.report.steps[0].metrics;
|
|
475
|
+
assert.deepStrictEqual(m.root_classes_html, ratio(1, 1));
|
|
476
|
+
assert(!result.report.steps[0].errors.some(e => e.includes('app_shell.root_classes')));
|
|
477
|
+
checkTotals(result.report);
|
|
478
|
+
|
|
479
|
+
// Missing mockup: the step's metrics are all null (nothing measurable).
|
|
480
|
+
result = fixture(s => { s.skipHtml = true; });
|
|
481
|
+
assert.strictEqual(result.status, 1);
|
|
482
|
+
assert(result.report.steps[0].errors.some(e => e.includes('mockup file not found')));
|
|
483
|
+
assert.deepStrictEqual(result.report.steps[0].metrics, allNull());
|
|
484
|
+
checkTotals(result.report);
|
|
485
|
+
|
|
486
|
+
// Action coverage metric mirrors the coverage ledger.
|
|
487
|
+
const saveArticle = () => ({ title: 'How to save', article_type: 'how-to', introduction: 'Save a document.',
|
|
488
|
+
prerequisites: [], steps: [{ title: 'Save', content: 'Select **Save**.', has_image: true }],
|
|
489
|
+
tips: [], summary: 'The document is saved.' });
|
|
490
|
+
result = fixture(s => {
|
|
491
|
+
s.article = saveArticle();
|
|
492
|
+
s.viewSources.action_coverage = [{ article_step_index: 0, screenshot_step_index: 0,
|
|
493
|
+
kind: 'click', target: 'Save', state: 'action-ready' }];
|
|
494
|
+
s.html = s.html.replace('data-rtfm-item="toolbar:actions"', 'data-rtfm-item="toolbar:actions" data-rtfm-action-target="0"');
|
|
495
|
+
});
|
|
496
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
497
|
+
assert.deepStrictEqual(result.report.metrics.action_coverage, ratio(1, 1));
|
|
498
|
+
result = fixture(s => { s.article = saveArticle(); });
|
|
499
|
+
assert.strictEqual(result.status, 1);
|
|
500
|
+
assert.deepStrictEqual(result.report.metrics.action_coverage, ratio(0, 1));
|
|
501
|
+
checkTotals(result.report);
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
function renderFixture(leftIsWrong) {
|
|
505
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-render-'));
|
|
506
|
+
const leftStyle = leftIsWrong ? 'position:absolute;left:850px;top:60px' : 'position:absolute;left:0;top:60px';
|
|
507
|
+
write(path.join(root, 'step_0.html'), '<!doctype html><html><head><style>' +
|
|
508
|
+
'html,body{margin:0;width:1200px;height:800px}.toolbar{height:60px;width:1200px}' +
|
|
509
|
+
'.canvas{position:absolute;left:240px;top:60px;width:960px;height:740px}.side{width:240px;height:740px}' +
|
|
510
|
+
'</style></head><body data-viewport="wide" data-rtfm-state="editing">' +
|
|
511
|
+
'<header class="toolbar" data-rtfm-region="toolbar" data-rtfm-placement="top">Toolbar</header>' +
|
|
512
|
+
`<aside class="side" style="${leftStyle}" data-rtfm-region="sidebar" data-rtfm-placement="left">Sidebar</aside>` +
|
|
513
|
+
'<main class="canvas" data-rtfm-region="canvas" data-rtfm-placement="canvas">Canvas</main>' +
|
|
514
|
+
'</body></html>');
|
|
515
|
+
const proc = spawnSync(process.execPath, [renderAll, root], {
|
|
516
|
+
encoding: 'utf8', env: { ...process.env, RTFM_WATERMARK: 'off' }, timeout: 30000,
|
|
517
|
+
});
|
|
518
|
+
const timingPath = path.join(root, 'render_timing.json');
|
|
519
|
+
if (!fs.existsSync(timingPath)) {
|
|
520
|
+
const diagnostic = `renderer produced no timing report (status ${proc.status})\n${proc.stdout}${proc.stderr}`;
|
|
521
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
522
|
+
throw new Error(diagnostic);
|
|
523
|
+
}
|
|
524
|
+
const timing = JSON.parse(fs.readFileSync(timingPath, 'utf8'));
|
|
525
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
526
|
+
return { status: proc.status, timing, stdout: proc.stdout, stderr: proc.stderr };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let rendered = renderFixture(false);
|
|
530
|
+
assert.strictEqual(rendered.status, 0, rendered.stdout + rendered.stderr);
|
|
531
|
+
assert.strictEqual(rendered.timing.all_passed, true);
|
|
532
|
+
|
|
533
|
+
rendered = renderFixture(true);
|
|
534
|
+
assert.strictEqual(rendered.status, 1);
|
|
535
|
+
assert.strictEqual(rendered.timing.all_passed, false);
|
|
536
|
+
assert(rendered.timing.steps[0].geometry_errors.some(e => e.includes('not anchored on the left')));
|
|
537
|
+
|
|
538
|
+
// ── 6b. Screen closure (warn-only): the primary view's unconditional includes and
|
|
539
|
+
// inline sections must leave a trace; overlays and guarded pieces are exempt.
|
|
540
|
+
const closureView = [
|
|
541
|
+
'<%= render partial: "shared/toolbar" %>',
|
|
542
|
+
'<h3>Document details</h3><label>Document title</label>',
|
|
543
|
+
'<h3>Sharing options</h3><label>Allow comments</label>',
|
|
544
|
+
'<% if @admin %><h3>Danger zone</h3><button>Delete everything</button><% end %>',
|
|
545
|
+
'<%= render partial: "modals/share_modal" %>',
|
|
546
|
+
].join('\n');
|
|
547
|
+
const closureFiles = {
|
|
548
|
+
'views/closure.html.erb': closureView,
|
|
549
|
+
'views/shared/_toolbar.html.erb': '<button>Save changes</button>',
|
|
550
|
+
'views/modals/_share_modal.html.erb': '<div class="modal"><h5>Share with people</h5></div>',
|
|
551
|
+
};
|
|
552
|
+
const closureStep = s => {
|
|
553
|
+
s.viewSources.steps[0].primary_view = 'views/closure.html.erb';
|
|
554
|
+
s.viewSources.steps[0].partials_expanded = ['views/runtime.html', 'views/shared/_toolbar.html.erb', 'views/modals/_share_modal.html.erb'];
|
|
555
|
+
s.viewSources.steps[0].verbatim_evidence = ['Document details', 'Document title'];
|
|
556
|
+
s.extraFiles = closureFiles;
|
|
557
|
+
};
|
|
558
|
+
result = fixture(s => {
|
|
559
|
+
closureStep(s);
|
|
560
|
+
s.html = s.html.replace('<article data-rtfm-item="canvas:document">Document</article>',
|
|
561
|
+
'<article data-rtfm-item="canvas:document">Document <button>Save changes</button><h3>Document details</h3>Document title <h3>Sharing options</h3>Allow comments</article>');
|
|
562
|
+
});
|
|
563
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
564
|
+
assert(!result.report.steps[0].warnings.some(w => w.includes('renders unconditionally')), 'complete screen draws no closure warning');
|
|
565
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.screen_closure, ratio(3, 3), 'toolbar + 2 sections; the guarded section and the modal are exempt');
|
|
566
|
+
|
|
567
|
+
result = fixture(s => {
|
|
568
|
+
closureStep(s);
|
|
569
|
+
s.html = s.html.replace('<article data-rtfm-item="canvas:document">Document</article>',
|
|
570
|
+
'<article data-rtfm-item="canvas:document">Document <h3>Document details</h3>Document title</article>');
|
|
571
|
+
});
|
|
572
|
+
assert.strictEqual(result.status, 0, 'screen closure is warn-only');
|
|
573
|
+
const closureWarn = result.report.steps[0].warnings.find(w => w.includes('renders unconditionally'));
|
|
574
|
+
assert(closureWarn, JSON.stringify(result.report.steps[0].warnings));
|
|
575
|
+
assert(closureWarn.includes('shared/toolbar') && closureWarn.includes('Sharing options'), closureWarn);
|
|
576
|
+
assert(!closureWarn.includes('Danger zone') && !closureWarn.includes('share_modal'), 'guarded and overlay pieces never warn');
|
|
577
|
+
assert.deepStrictEqual(result.report.steps[0].metrics.screen_closure, ratio(1, 3));
|
|
578
|
+
|
|
579
|
+
result = fixture(s => { // a modal step: primary_view is the overlay → exempt
|
|
580
|
+
closureStep(s);
|
|
581
|
+
s.viewSources.steps[0].primary_view = 'views/modals/_share_modal.html.erb';
|
|
582
|
+
s.viewSources.steps[0].verbatim_evidence = ['Share with people'];
|
|
583
|
+
s.html = s.html.replace('Document</article>', 'Document Share with people</article>');
|
|
584
|
+
});
|
|
585
|
+
assert.strictEqual(result.report.steps[0].metrics.screen_closure, null);
|
|
586
|
+
|
|
587
|
+
// Code in step content is never a UI action: a fenced command whose first word
|
|
588
|
+
// is an action verb must not demand action coverage, while the same words as
|
|
589
|
+
// prose still do.
|
|
590
|
+
result = fixture(s => {
|
|
591
|
+
s.article = { title: 'How to start', article_type: 'how-to', introduction: 'Start the app.',
|
|
592
|
+
prerequisites: [], steps: [{ title: 'Start', content: 'Review the output below.\n```\nstart app --port 3000\n```\nThen check `start --help`.', has_image: true }],
|
|
593
|
+
tips: [], summary: 'The app is running.' };
|
|
594
|
+
});
|
|
595
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
596
|
+
result = fixture(s => {
|
|
597
|
+
s.article = { title: 'How to start', article_type: 'how-to', introduction: 'Start the app.',
|
|
598
|
+
prerequisites: [], steps: [{ title: 'Start', content: 'Review the output below. Start app.', has_image: true }],
|
|
599
|
+
tips: [], summary: 'The app is running.' };
|
|
600
|
+
});
|
|
601
|
+
assert.strictEqual(result.status, 1);
|
|
602
|
+
assert(result.report.global_errors.some(e => /action/i.test(e)), JSON.stringify(result.report.global_errors));
|
|
603
|
+
|
|
604
|
+
console.log('lint_mockup_fidelity schema-v5 tests passed');
|
|
605
|
+
|
|
606
|
+
// A Rails-generated submit label is checked against the real form/model binding.
|
|
607
|
+
function generatedSubmitFixture(wrongLabel = false) {
|
|
608
|
+
return fixture(s => {
|
|
609
|
+
const label = wrongLabel ? 'Create invented label' : 'Create API key';
|
|
610
|
+
s.extraFiles = {
|
|
611
|
+
'form.erb':'<%= form_with(model: @record) do |f| %><%= f.submit %><% end %>',
|
|
612
|
+
'controller.rb':'@record = APIKey.new',
|
|
613
|
+
'model.rb':'class APIKey < ActiveRecord::Base\nend',
|
|
614
|
+
'config/initializers/inflections.rb':"ActiveSupport::Inflector.inflections(:en) do |i|\n i.acronym 'API'\nend",
|
|
615
|
+
};
|
|
616
|
+
s.viewSources.steps[0].verbatim_evidence.push({string:label,found_in:'form.erb',generated:{kind:'rails_submit',locale:'en',action:'create',binding:'@record',binding_source:'controller.rb',model_source:'model.rb'}});
|
|
617
|
+
s.html = s.html.replace('</main>',`<input type="submit" value="${label}" data-rtfm-action-target="0"></main>`);
|
|
618
|
+
s.viewSources.action_coverage = [{article_step_index:0,screenshot_step_index:0,kind:'click',state:'action-ready',target:label}];
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
result = generatedSubmitFixture();
|
|
622
|
+
assert.equal(result.status,0,JSON.stringify(result.report));
|
|
623
|
+
result = generatedSubmitFixture(true);
|
|
624
|
+
assert.equal(result.status,1);
|
|
625
|
+
assert(result.report.steps[0].errors.some(e => e.includes('contradicts source')));
|
|
626
|
+
|
|
627
|
+
// Screenshot limit (max_images / RTFM_MAX_IMAGES): hard, and once reached the
|
|
628
|
+
// remaining actions are text-only warnings rather than coverage errors.
|
|
629
|
+
function limitFixture(limit, stepImages) {
|
|
630
|
+
return fixture(st => {
|
|
631
|
+
st.env = { RTFM_MAX_IMAGES: String(limit) };
|
|
632
|
+
st.article = { title: 'How to edit', article_type: 'how-to', introduction: 'Edit a document.',
|
|
633
|
+
prerequisites: [], steps: [
|
|
634
|
+
{ title: 'Open', content: 'Open **Document**.', has_image: true },
|
|
635
|
+
{ title: 'Save', content: 'Select **Save**.', has_image: stepImages > 1 },
|
|
636
|
+
], tips: [], summary: 'The document is saved.' };
|
|
637
|
+
st.viewSources.action_coverage = [
|
|
638
|
+
{ article_step_index: 0, screenshot_step_index: 0, kind: 'click', target: 'Document', state: 'action-ready' },
|
|
639
|
+
];
|
|
640
|
+
st.html = st.html.replace('data-rtfm-item="canvas:document"',
|
|
641
|
+
'data-rtfm-item="canvas:document" data-rtfm-action-target="0"');
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
result = limitFixture(1, 1);
|
|
645
|
+
assert.strictEqual(result.status, 0, JSON.stringify(result.report));
|
|
646
|
+
assert(result.report.global_warnings.some(w => w.includes('step(s) 1 are text-only: the 1-screenshot limit is reached')));
|
|
647
|
+
result = limitFixture(2, 1);
|
|
648
|
+
assert.strictEqual(result.status, 1);
|
|
649
|
+
assert(result.report.global_errors.some(e => e.includes('lack action-ready screenshot coverage: 1') && e.includes('1 of 2 screenshot(s) remain')));
|
|
650
|
+
result = limitFixture(1, 2);
|
|
651
|
+
assert.strictEqual(result.status, 1);
|
|
652
|
+
assert(result.report.global_errors.some(e => e.includes('article has 2 screenshots; the limit is 1')));
|
|
653
|
+
console.log('screenshot limit tests passed');
|