@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,168 @@
|
|
|
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 {
|
|
10
|
+
MAX_ASSETS,
|
|
11
|
+
buildPrompt,
|
|
12
|
+
generateAll,
|
|
13
|
+
validateRequests,
|
|
14
|
+
} = require('./generate_content_images.js');
|
|
15
|
+
|
|
16
|
+
function request(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
id: 'participant-alex',
|
|
19
|
+
kind: 'video-frame',
|
|
20
|
+
purpose: 'A fictional participant visible inside the real call tile',
|
|
21
|
+
reason: 'The source requires a live camera feed and provides no suitable image asset',
|
|
22
|
+
prompt: 'Natural webcam view of a fictional adult in a bright home office',
|
|
23
|
+
aspect_ratio: 'landscape',
|
|
24
|
+
alt: 'Fictional participant on camera',
|
|
25
|
+
used_in_steps: [1, 2],
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function manifest(assets = [request()]) { return { version: 1, assets }; }
|
|
31
|
+
|
|
32
|
+
function externalRequest(overrides = {}) {
|
|
33
|
+
return request({
|
|
34
|
+
id: 'confirm-email',
|
|
35
|
+
kind: 'external-surface',
|
|
36
|
+
surface: 'confirmation-email',
|
|
37
|
+
source_basis: 'user-description',
|
|
38
|
+
source_files: [],
|
|
39
|
+
required_text: ['Confirm your email address', 'Confirm email'],
|
|
40
|
+
purpose: 'The confirmation email the reader must recognise and act in',
|
|
41
|
+
reason: 'The step happens outside the product and no renderable email template exists',
|
|
42
|
+
prompt: 'A clean transactional confirmation email on a neutral mail canvas',
|
|
43
|
+
alt: 'Confirmation email with a Confirm email button',
|
|
44
|
+
used_in_steps: [2],
|
|
45
|
+
...overrides,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function run() {
|
|
50
|
+
assert.strictEqual(validateRequests(manifest()).length, 1);
|
|
51
|
+
const blockAsset = validateRequests(manifest([request({ used_in_steps: undefined, used_in_blocks: ['join-call'] })]))[0];
|
|
52
|
+
assert.deepStrictEqual(blockAsset.used_in_blocks, ['join-call']);
|
|
53
|
+
assert.throws(() => validateRequests(manifest(Array.from({ length: MAX_ASSETS + 1 }, (_, i) =>
|
|
54
|
+
request({ id: `asset-${i}` })))), /maximum is 5/);
|
|
55
|
+
assert.throws(() => validateRequests(manifest([request(), request()])), /duplicates/);
|
|
56
|
+
assert.throws(() => validateRequests(manifest([request({ kind: 'ui-mockup' })])), /kind must be/);
|
|
57
|
+
assert.strictEqual(validateRequests(manifest([externalRequest()]))[0].surface, 'confirmation-email');
|
|
58
|
+
assert.throws(() => validateRequests(manifest([externalRequest({ surface: 'product-screen' })])), /surface must be/);
|
|
59
|
+
assert.throws(() => validateRequests(manifest([externalRequest({ required_text: [] })])), /required_text/);
|
|
60
|
+
assert.throws(() => validateRequests(manifest([externalRequest({ source_basis: 'repository', source_files: [] })])), /must not be empty/);
|
|
61
|
+
assert.throws(() => validateRequests(manifest([request({ required_text: ['Not allowed'] })])), /only valid/);
|
|
62
|
+
|
|
63
|
+
const prompt = buildPrompt(request());
|
|
64
|
+
assert(prompt.includes('Do not draw application UI'));
|
|
65
|
+
assert(prompt.includes('must not depict or imitate an identifiable real person'));
|
|
66
|
+
|
|
67
|
+
const externalPrompt = buildPrompt(validateRequests(manifest([externalRequest()]))[0]);
|
|
68
|
+
assert(externalPrompt.includes('External surface type: confirmation-email'));
|
|
69
|
+
assert(externalPrompt.includes('"Confirm your email address"'));
|
|
70
|
+
assert(externalPrompt.includes('authorised to contain interface elements and legible text'));
|
|
71
|
+
assert(externalPrompt.includes('Do not draw, extend, or imitate the product application UI'));
|
|
72
|
+
|
|
73
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-content-images-'));
|
|
74
|
+
const out = path.join(root, 'out');
|
|
75
|
+
const cache = path.join(root, 'cache');
|
|
76
|
+
let calls = 0;
|
|
77
|
+
const fakeFetch = async () => {
|
|
78
|
+
calls += 1;
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
json: async () => ({ data: [{ b64_json: Buffer.from('fake-png').toString('base64') }] }),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let result = await generateAll({ requests: manifest(), outDir: out, cacheDir: cache,
|
|
86
|
+
apiKey: 'test-key', fetchImpl: fakeFetch });
|
|
87
|
+
assert.strictEqual(result.assets['participant-alex'].status, 'generated');
|
|
88
|
+
assert.strictEqual(calls, 1);
|
|
89
|
+
assert.strictEqual(fs.readFileSync(path.join(out, 'generated-assets', 'participant-alex.png'), 'utf8'), 'fake-png');
|
|
90
|
+
|
|
91
|
+
result = await generateAll({ requests: manifest(), outDir: out, cacheDir: cache,
|
|
92
|
+
apiKey: 'test-key', fetchImpl: fakeFetch });
|
|
93
|
+
assert.strictEqual(result.assets['participant-alex'].status, 'cached');
|
|
94
|
+
assert.strictEqual(calls, 1);
|
|
95
|
+
|
|
96
|
+
result = await generateAll({ requests: manifest(), outDir: out, cacheDir: cache,
|
|
97
|
+
model: 'gpt-image-1', apiKey: 'test-key', fetchImpl: fakeFetch });
|
|
98
|
+
assert.strictEqual(result.assets['participant-alex'].status, 'generated');
|
|
99
|
+
assert.strictEqual(calls, 2);
|
|
100
|
+
|
|
101
|
+
const externalOut = path.join(root, 'external');
|
|
102
|
+
result = await generateAll({ requests: manifest([externalRequest()]), outDir: externalOut,
|
|
103
|
+
cacheDir: path.join(root, 'external-cache'), apiKey: 'test-key', fetchImpl: fakeFetch });
|
|
104
|
+
assert.strictEqual(result.assets['confirm-email'].status, 'generated');
|
|
105
|
+
assert.strictEqual(result.assets['confirm-email'].surface, 'confirmation-email');
|
|
106
|
+
assert.deepStrictEqual(result.assets['confirm-email'].required_text,
|
|
107
|
+
['Confirm your email address', 'Confirm email']);
|
|
108
|
+
assert.deepStrictEqual(result.assets['confirm-email'].text_fidelity,
|
|
109
|
+
{ enforcement: 'prompt-constrained', required_text_count: 2 });
|
|
110
|
+
|
|
111
|
+
const missingKeyOut = path.join(root, 'missing-key');
|
|
112
|
+
result = await generateAll({ requests: manifest(), outDir: missingKeyOut, cacheDir: path.join(root, 'empty-cache'),
|
|
113
|
+
apiKey: '' });
|
|
114
|
+
assert.strictEqual(result.count, 0);
|
|
115
|
+
assert.strictEqual(result.failure_count, 1);
|
|
116
|
+
assert.strictEqual(result.assets['participant-alex'], undefined);
|
|
117
|
+
assert.strictEqual(result.failures['participant-alex'].status, 'failed');
|
|
118
|
+
assert(result.failures['participant-alex'].warning.includes('no image was produced'));
|
|
119
|
+
assert(!fs.existsSync(path.join(missingKeyOut, 'generated-assets')));
|
|
120
|
+
|
|
121
|
+
const failedOut = path.join(root, 'api-failure');
|
|
122
|
+
const failedFetch = async () => ({ ok: false, status: 429, text: async () => 'rate limited' });
|
|
123
|
+
result = await generateAll({ requests: manifest(), outDir: failedOut, cacheDir: path.join(root, 'failed-cache'),
|
|
124
|
+
apiKey: 'test-key', fetchImpl: failedFetch });
|
|
125
|
+
assert.strictEqual(result.count, 0);
|
|
126
|
+
assert.strictEqual(result.failure_count, 1);
|
|
127
|
+
assert.strictEqual(result.assets['participant-alex'], undefined);
|
|
128
|
+
assert(result.failures['participant-alex'].warning.includes('429'));
|
|
129
|
+
assert(result.failures['participant-alex'].warning.includes('no image was produced'));
|
|
130
|
+
assert(!fs.existsSync(path.join(failedOut, 'generated-assets')));
|
|
131
|
+
|
|
132
|
+
const injectOut = path.join(root, 'inject');
|
|
133
|
+
fs.mkdirSync(injectOut, { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(injectOut, 'step_0.html'),
|
|
135
|
+
'<!doctype html><html><head><!-- INJECT_CSS --></head><body><img src="{{generated:participant-alex}}" data-rtfm-generated-asset="participant-alex"></body></html>');
|
|
136
|
+
fs.writeFileSync(path.join(injectOut, 'images.json'), JSON.stringify({ images: {} }));
|
|
137
|
+
fs.copyFileSync(path.join(out, 'generated_images.json'), path.join(injectOut, 'generated_images.json'));
|
|
138
|
+
fs.cpSync(path.join(out, 'generated-assets'), path.join(injectOut, 'generated-assets'), { recursive: true });
|
|
139
|
+
const injected = spawnSync(process.execPath, [path.join(__dirname, 'inject_assets.js'), injectOut, '',
|
|
140
|
+
path.join(injectOut, 'images.json'), '--generated-images', path.join(injectOut, 'generated_images.json')],
|
|
141
|
+
{ encoding: 'utf8' });
|
|
142
|
+
assert.strictEqual(injected.status, 0, injected.stdout + injected.stderr);
|
|
143
|
+
const injectedHtml = fs.readFileSync(path.join(injectOut, 'step_0.html'), 'utf8');
|
|
144
|
+
assert(injectedHtml.includes('src="data:image/png;base64,'));
|
|
145
|
+
assert(injectedHtml.includes('data-rtfm-generated-asset="participant-alex"'));
|
|
146
|
+
|
|
147
|
+
const omittedInjectOut = path.join(root, 'inject-omitted');
|
|
148
|
+
fs.mkdirSync(omittedInjectOut, { recursive: true });
|
|
149
|
+
fs.writeFileSync(path.join(omittedInjectOut, 'step_0.html'),
|
|
150
|
+
'<!doctype html><html><head><!-- INJECT_CSS --></head><body><img src="{{generated:participant-alex}}" data-rtfm-generated-asset="participant-alex"></body></html>');
|
|
151
|
+
fs.writeFileSync(path.join(omittedInjectOut, 'images.json'), JSON.stringify({ images: {} }));
|
|
152
|
+
fs.copyFileSync(path.join(missingKeyOut, 'generated_images.json'), path.join(omittedInjectOut, 'generated_images.json'));
|
|
153
|
+
const omittedInjection = spawnSync(process.execPath, [path.join(__dirname, 'inject_assets.js'), omittedInjectOut, '',
|
|
154
|
+
path.join(omittedInjectOut, 'images.json'), '--generated-images', path.join(omittedInjectOut, 'generated_images.json')],
|
|
155
|
+
{ encoding: 'utf8' });
|
|
156
|
+
assert.strictEqual(omittedInjection.status, 0, omittedInjection.stdout + omittedInjection.stderr);
|
|
157
|
+
const omittedHtml = fs.readFileSync(path.join(omittedInjectOut, 'step_0.html'), 'utf8');
|
|
158
|
+
assert(omittedHtml.includes('src="{{generated:participant-alex}}"'));
|
|
159
|
+
assert(!omittedHtml.includes('src="data:image/'));
|
|
160
|
+
|
|
161
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
162
|
+
console.log('content image generation tests passed');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
run().catch(error => {
|
|
166
|
+
console.error(error);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* include_census.js — classification of includes by render condition on synthetic views
|
|
4
|
+
* (Rails ERB, JSX, Blade, Django), plus marker extraction. No app-specific fixtures.
|
|
5
|
+
*/
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { census, includeRefs, extractMarkers } = require('./include_census.js');
|
|
11
|
+
|
|
12
|
+
function project(files) {
|
|
13
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-census-'));
|
|
14
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
15
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
16
|
+
fs.writeFileSync(path.join(root, rel), text);
|
|
17
|
+
}
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
const byName = c => Object.fromEntries(c.includes.map(i => [i.name, i]));
|
|
21
|
+
|
|
22
|
+
// ── Rails ERB: unconditional partial, guarded partial, overlay partials, inline sections ──
|
|
23
|
+
{
|
|
24
|
+
const root = project({
|
|
25
|
+
'app/views/things/new.html.erb': [
|
|
26
|
+
'<%= render partial: "layouts/header" %>',
|
|
27
|
+
'<main>',
|
|
28
|
+
' <% if @account.premium? %>',
|
|
29
|
+
' <%= render partial: "partials/upsell", locals: { feature: "x" } %>',
|
|
30
|
+
' <% end %>',
|
|
31
|
+
' <h3>Item details</h3>',
|
|
32
|
+
' <%= f.label :title, "Title (Required)" %>',
|
|
33
|
+
' <%= f.text_field :title %>',
|
|
34
|
+
' <h3>Item options</h3>',
|
|
35
|
+
' <label>Enable alerts</label>',
|
|
36
|
+
' <% if current_user.admin? %>',
|
|
37
|
+
' <h3>Admin controls</h3>',
|
|
38
|
+
' <button>Purge</button>',
|
|
39
|
+
' <% end %>',
|
|
40
|
+
' <%= f.submit "Create item" %>',
|
|
41
|
+
' <%= render partial: "modals/pick_owner_modal", locals: { title: "Pick" } %>',
|
|
42
|
+
' <%= render partial: "shared/footer_actions" %>',
|
|
43
|
+
'</main>',
|
|
44
|
+
].join('\n'),
|
|
45
|
+
'app/views/layouts/_header.html.erb': '<nav><a href="/">Home</a><a href="/things">Things</a></nav>',
|
|
46
|
+
'app/views/partials/_upsell.html.erb': '<div><h4>Upgrade to unlock</h4></div>',
|
|
47
|
+
'app/views/modals/_pick_owner_modal.html.erb': '<div class="modal"><h5>Pick an owner</h5><button>Close</button></div>',
|
|
48
|
+
'app/views/shared/_footer_actions.html.erb': '<div><button>Close</button><button>Save and continue</button></div>',
|
|
49
|
+
});
|
|
50
|
+
const c = census(root, 'app/views/things/new.html.erb');
|
|
51
|
+
const inc = byName(c);
|
|
52
|
+
assert.strictEqual(inc['layouts/header'].kind, 'unconditional');
|
|
53
|
+
assert.deepStrictEqual(inc['layouts/header'].markers, ['Home', 'Things']);
|
|
54
|
+
assert.strictEqual(inc['partials/upsell'].kind, 'conditional', 'guarded by the surrounding if');
|
|
55
|
+
assert.strictEqual(inc['modals/pick_owner_modal'].kind, 'overlay', 'modal by name');
|
|
56
|
+
assert.strictEqual(inc['shared/footer_actions'].kind, 'unconditional');
|
|
57
|
+
assert(inc['shared/footer_actions'].markers.includes('Save and continue'));
|
|
58
|
+
const secs = Object.fromEntries(c.inline_sections.map(s => [s.heading, s]));
|
|
59
|
+
assert.strictEqual(secs['Item details'].guarded, false);
|
|
60
|
+
assert(secs['Item details'].markers.includes('Title (Required)'), secs['Item details'].markers);
|
|
61
|
+
assert.strictEqual(secs['Item options'].guarded, false);
|
|
62
|
+
assert.strictEqual(secs['Admin controls'].guarded, true, 'heading directly under an if is guarded');
|
|
63
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── JSX: resolved component with markers, ternary-guarded component, Dialog primitive skipped ──
|
|
67
|
+
{
|
|
68
|
+
const root = project({
|
|
69
|
+
'src/pages/List.tsx': [
|
|
70
|
+
'import Toolbar from "./Toolbar";',
|
|
71
|
+
'import EmptyState from "./EmptyState";',
|
|
72
|
+
'import { Dialog } from "@ui/dialog";',
|
|
73
|
+
'export default function List({ items }) {',
|
|
74
|
+
' return (<div>',
|
|
75
|
+
' <Toolbar />',
|
|
76
|
+
' {items.length === 0 ? <EmptyState /> : null}',
|
|
77
|
+
' <Dialog open={false}><p>hidden</p></Dialog>',
|
|
78
|
+
' </div>);',
|
|
79
|
+
'}',
|
|
80
|
+
].join('\n'),
|
|
81
|
+
'src/pages/Toolbar.tsx': 'export default () => <div><button>New item</button><input placeholder="Search items" /></div>;',
|
|
82
|
+
'src/pages/EmptyState.tsx': 'export default () => <h2>Nothing here yet</h2>;',
|
|
83
|
+
});
|
|
84
|
+
const c = census(root, 'src/pages/List.tsx');
|
|
85
|
+
const inc = byName(c);
|
|
86
|
+
assert.strictEqual(inc.Toolbar.kind, 'unconditional');
|
|
87
|
+
assert.deepStrictEqual(inc.Toolbar.markers.sort(), ['New item', 'Search items']);
|
|
88
|
+
assert.strictEqual(inc.EmptyState.kind, 'conditional', 'ternary on the same line is a guard');
|
|
89
|
+
assert(!('Dialog' in inc), 'UI primitives are not includes');
|
|
90
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Blade + Django: include syntaxes resolve and classify ──
|
|
94
|
+
{
|
|
95
|
+
const root = project({
|
|
96
|
+
'resources/views/orders/show.blade.php': [
|
|
97
|
+
'@include("partials.summary")',
|
|
98
|
+
'@if($order->refundable)',
|
|
99
|
+
' @include("partials.refund")',
|
|
100
|
+
'@endif',
|
|
101
|
+
'<x-drawer name="notes">…</x-drawer>',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
'resources/views/partials/summary.blade.php': '<h3>Order summary</h3><th>Total</th>',
|
|
104
|
+
'resources/views/partials/refund.blade.php': '<button>Refund order</button>',
|
|
105
|
+
'templates/reports/index.html': '{% include "reports/_filters.html" %}\n{% if perms.export %}{% include "reports/_export.html" %}{% endif %}',
|
|
106
|
+
'templates/reports/_filters.html': '<label>Date range</label>',
|
|
107
|
+
'templates/reports/_export.html': '<button>Export CSV</button>',
|
|
108
|
+
});
|
|
109
|
+
const b = byName(census(root, 'resources/views/orders/show.blade.php'));
|
|
110
|
+
assert.strictEqual(b['partials.summary'].kind, 'unconditional');
|
|
111
|
+
assert.deepStrictEqual(b['partials.summary'].markers, ['Order summary', 'Total']);
|
|
112
|
+
assert.strictEqual(b['partials.refund'].kind, 'conditional');
|
|
113
|
+
assert.strictEqual(b['x-drawer'].kind, 'overlay');
|
|
114
|
+
const d = byName(census(root, 'templates/reports/index.html'));
|
|
115
|
+
assert.strictEqual(d['reports/_filters.html'].kind, 'unconditional');
|
|
116
|
+
assert.deepStrictEqual(d['reports/_filters.html'].markers, ['Date range']);
|
|
117
|
+
assert.strictEqual(d['reports/_export.html'].kind, 'conditional');
|
|
118
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── markers: sentences, templating and identifiers are not markers ──
|
|
122
|
+
assert.deepStrictEqual(extractMarkers('<h2>Settings</h2><p>You are not allowed to do this.</p><button>{{ t("x") }}</button><th>createdAt</th><label>Display name</label>'),
|
|
123
|
+
['Settings', 'Display name']);
|
|
124
|
+
assert.deepStrictEqual(includeRefs('/nonexistent-root', 'nope.erb'), []);
|
|
125
|
+
|
|
126
|
+
console.log('include_census tests passed');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { verifyLabelEvidence } = require('./label_evidence');
|
|
6
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-labels-'));
|
|
7
|
+
const write = (file, text) => { fs.mkdirSync(path.dirname(path.join(root, file)), {recursive:true}); fs.writeFileSync(path.join(root, file), text); };
|
|
8
|
+
const template = '<%= form_with(model: @new_record, url: records_path) do |form| %>\n<%= form.submit class: "btn" %>\n<% end %>';
|
|
9
|
+
const model = 'class APIKey < ActiveRecord::Base\n belongs_to :account\nend';
|
|
10
|
+
const binding = 'def index\n @new_record = APIKey.new\nend';
|
|
11
|
+
const evidence = {string:'Create API key', found_in:'app/views/form.erb', generated:{kind:'rails_submit',locale:'en',action:'create',binding:'@new_record',binding_source:'app/controllers/records_controller.rb',model_source:'app/models/api_key.rb'}};
|
|
12
|
+
function reset() {
|
|
13
|
+
fs.rmSync(root, {recursive:true,force:true}); fs.mkdirSync(root);
|
|
14
|
+
write(evidence.found_in, template); write(evidence.generated.binding_source, binding); write(evidence.generated.model_source, model);
|
|
15
|
+
write('config/initializers/inflections.rb', "# inflect.human /bad/, 'Wrong'\nActiveSupport::Inflector.inflections(:en) do |inflect|\n inflect.acronym 'API'\nend");
|
|
16
|
+
}
|
|
17
|
+
const check = (entry=evidence) => verifyLabelEvidence(entry, null, root);
|
|
18
|
+
try {
|
|
19
|
+
reset(); assert.equal(check().ok, true, JSON.stringify(check()));
|
|
20
|
+
assert.match(check({...evidence,string:'Create a banana'}).error, /contradicts source/);
|
|
21
|
+
assert.match(check({...evidence,generated:{...evidence.generated,action:'update'}}).error, /create\/update state/);
|
|
22
|
+
write(evidence.generated.binding_source, '@new_record = APIKey.find(params[:id])');
|
|
23
|
+
assert.equal(check({...evidence,string:'Update API key',generated:{...evidence.generated,action:'update'}}).ok, true);
|
|
24
|
+
for (const [file, text, expected] of [
|
|
25
|
+
[evidence.found_in, template.replace('form.submit class:', 'form.submit "Delete", class:'), /default submit/],
|
|
26
|
+
[evidence.found_in, template.replace('@new_record','@unrelated'), /bound to this model/],
|
|
27
|
+
[evidence.found_in, template + template, /single form_with/],
|
|
28
|
+
[evidence.generated.model_source, model.replace('APIKey', 'DifferentModel'), /create\/update state/],
|
|
29
|
+
[evidence.generated.binding_source, binding + '\n@new_record = APIKey.find(1)', /unambiguous/],
|
|
30
|
+
[evidence.generated.model_source, model.replace('belongs_to :account', 'def self.model_name; other; end'), /custom model/],
|
|
31
|
+
['config/locales/en.yml', 'en:\n helpers:\n submit:\n create: Add %{model}', /runtime resolver/],
|
|
32
|
+
['config/locales/en.yml', 'en:\n activerecord:\n models:\n api_key: Credential', /runtime resolver/],
|
|
33
|
+
['config/application.rb', 'config.i18n.default_locale = :fr', /runtime resolver/],
|
|
34
|
+
['config/initializers/inflections.rb', "ActiveSupport::Inflector.inflections(:en) do |i|\n i.human 'api_key', 'Credential'\nend", /custom inflections/],
|
|
35
|
+
]) { reset(); write(file,text); assert.equal(check().ok,false, file); assert.match(check().error,expected); }
|
|
36
|
+
reset();
|
|
37
|
+
assert.equal(check({...evidence,generated:{kind:'trust_me'}}).ok,false);
|
|
38
|
+
assert.equal(check({...evidence,found_in:'../outside'}).ok,false);
|
|
39
|
+
assert.equal(check({...evidence,generated:undefined,derivation:'Rails does this'}).ok,false);
|
|
40
|
+
write('config/locales/en.yml','en:\n button: Create API key');
|
|
41
|
+
assert.equal(check({string:'Create API key',found_in:'config/locales/en.yml'}).ok,true);
|
|
42
|
+
assert.equal(check({string:'Invented',found_in:'config/locales/en.yml'}).ok,false);
|
|
43
|
+
reset(); write(evidence.generated.model_source,'class InvoiceLine < ApplicationRecord\nend'); write(evidence.generated.binding_source,'@new_record = InvoiceLine.new');
|
|
44
|
+
assert.equal(check({...evidence,string:'Create Invoice line'}).ok,true);
|
|
45
|
+
reset(); write(evidence.generated.model_source,'class OAuthToken < ApplicationRecord\nend'); write(evidence.generated.binding_source,'@new_record = OAuthToken.new');
|
|
46
|
+
write('config/initializers/inflections.rb', "ActiveSupport::Inflector.inflections(:en) do |i|\n i.acronym 'OAuth'\nend");
|
|
47
|
+
assert.equal(check({...evidence,string:'Create OAuth token'}).ok,true);
|
|
48
|
+
console.log('Generated and literal label evidence tests passed');
|
|
49
|
+
} finally {fs.rmSync(root,{recursive:true,force:true});}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
// `dir` doubles as the output dir and the project dir, so a project map lands
|
|
9
|
+
// at <dir>/.rtfm/project_map.json and mockups at <dir>/block_<id>.html.
|
|
10
|
+
function run(article, { viewSources = { blocks: [] }, projectMap = null, files = {}, env = {} } = {}) {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-block-copy-'));
|
|
12
|
+
fs.writeFileSync(path.join(dir, 'article.json'), JSON.stringify(article));
|
|
13
|
+
fs.writeFileSync(path.join(dir, 'view_sources.json'), JSON.stringify(viewSources));
|
|
14
|
+
if (projectMap) { fs.mkdirSync(path.join(dir, '.rtfm')); fs.writeFileSync(path.join(dir, '.rtfm', 'project_map.json'), JSON.stringify(projectMap)); }
|
|
15
|
+
for (const [name, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, name)), { recursive: true }); fs.writeFileSync(path.join(dir, name), text); }
|
|
16
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, 'lint_article_copy.js'), dir, dir], { encoding: 'utf8', env: { ...process.env, ...env } });
|
|
17
|
+
const report = JSON.parse(fs.readFileSync(path.join(dir, 'article_lint_report.json'), 'utf8'));
|
|
18
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
return { result, report };
|
|
20
|
+
}
|
|
21
|
+
const warningsOf = (check, id) => check.report.blocks.find(block => block.block_id === id).warnings;
|
|
22
|
+
const section = (id, content, extra = {}) => ({ id, type: 'section', presentation: 'numbered', title: 'Run it', content, has_image: false, ...extra });
|
|
23
|
+
const howTo = (...middle) => ({
|
|
24
|
+
schema_version: 2, title: 'How to send a message', article_type: 'how-to',
|
|
25
|
+
blocks: [
|
|
26
|
+
{ id: 'introduction', type: 'prose', presentation: 'lead', content: 'Send a message from the terminal.' },
|
|
27
|
+
...middle,
|
|
28
|
+
{ id: 'summary', type: 'prose', presentation: 'summary', title: 'Summary', content: 'The message is sent.' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
const FENCE = '```';
|
|
32
|
+
|
|
33
|
+
const concept = {
|
|
34
|
+
schema_version: 2,
|
|
35
|
+
title: 'What is Meetily?',
|
|
36
|
+
article_type: 'concept',
|
|
37
|
+
blocks: [
|
|
38
|
+
{ id: 'introduction', type: 'prose', presentation: 'lead', content: 'Meetily records and organises meetings.' },
|
|
39
|
+
{ id: 'how-it-works', type: 'section', presentation: 'plain', title: 'How it works', content: 'Meetily turns conversations into useful notes.', has_image: false },
|
|
40
|
+
{ id: 'summary', type: 'prose', presentation: 'summary', title: 'Summary', content: 'Use Meetily to keep meeting knowledge accessible.' },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
let check = run(concept);
|
|
44
|
+
assert.strictEqual(check.result.status, 0, check.result.stdout + check.result.stderr);
|
|
45
|
+
assert.strictEqual(check.report.all_passed, true);
|
|
46
|
+
assert.deepStrictEqual(check.report.blocks.map(block => block.block_id), ['introduction', 'how-it-works', 'summary']);
|
|
47
|
+
|
|
48
|
+
check = run({ title: 'Legacy', article_type: 'how-to', introduction: 'Old', steps: [] });
|
|
49
|
+
assert.strictEqual(check.result.status, 1);
|
|
50
|
+
assert(check.report.global_errors.some(error => error.includes('schema_version')));
|
|
51
|
+
|
|
52
|
+
check = run({ ...concept, blocks: [...concept.blocks, { ...concept.blocks[1] }] });
|
|
53
|
+
assert.strictEqual(check.result.status, 1);
|
|
54
|
+
assert(check.report.global_errors.some(error => error.includes('duplicated')));
|
|
55
|
+
|
|
56
|
+
// ---- Fence placement is schema-hard ---------------------------------------
|
|
57
|
+
const leadFence = howTo(section('run', 'Run the command.'));
|
|
58
|
+
leadFence.blocks[0].content = `Install it first.\n${FENCE}bash\nnpm install\n${FENCE}`;
|
|
59
|
+
check = run(leadFence);
|
|
60
|
+
assert.strictEqual(check.result.status, 1);
|
|
61
|
+
assert(check.report.global_errors.some(error => error.includes('lead prose') && error.includes('fenced code block')));
|
|
62
|
+
check = run(leadFence, { env: { RTFM_LINT_MODE: 'warn' } });
|
|
63
|
+
assert.strictEqual(check.result.status, 1, 'fence placement stays hard under warn mode');
|
|
64
|
+
|
|
65
|
+
check = run(howTo(section('run', 'Run the command.'), { id: 'prerequisites', type: 'list', presentation: 'checklist', title: 'Prerequisites', items: ['Node installed', `${FENCE}\nnpm -v\n${FENCE}`] }));
|
|
66
|
+
assert.strictEqual(check.result.status, 1);
|
|
67
|
+
assert(check.report.global_errors.some(error => error.includes('items[1]') && error.includes('inline-only')));
|
|
68
|
+
|
|
69
|
+
check = run(howTo(section('run', `Run the command.\n${FENCE}bash\ntgt --help`)));
|
|
70
|
+
assert.strictEqual(check.result.status, 1);
|
|
71
|
+
assert(check.report.global_errors.some(error => error.includes('unclosed')));
|
|
72
|
+
|
|
73
|
+
// ---- Fences in sections and the summary are fine; code never counts as prose --
|
|
74
|
+
const fenced = howTo(section('run', `Open a terminal. Run the command below. Wait for it to finish.\n${FENCE}bash\necho a.b. c. d.\n${FENCE}`));
|
|
75
|
+
fenced.blocks[2].content = `Wrap up with:\n${FENCE}bash\ntgt --send-message "Alice" "Hi"\n${FENCE}`;
|
|
76
|
+
check = run(fenced);
|
|
77
|
+
assert.strictEqual(check.result.status, 0, check.result.stdout + check.result.stderr);
|
|
78
|
+
assert(!check.report.global_warnings.some(warning => warning.includes('exceeds')), JSON.stringify(check.report.global_warnings));
|
|
79
|
+
assert.deepStrictEqual(check.report.metrics, { inline_code_spans: 0, fenced_blocks: 2, untagged_fences: 0 });
|
|
80
|
+
|
|
81
|
+
check = run(howTo(section('run', `Run the command.\n${FENCE}\ntgt --help\n${FENCE}`)));
|
|
82
|
+
assert.strictEqual(check.result.status, 0);
|
|
83
|
+
assert(warningsOf(check, 'run').some(warning => warning.includes('without a language tag')));
|
|
84
|
+
assert(check.result.stdout.includes('[WARN] block run:'), 'block warnings are printed for the model');
|
|
85
|
+
|
|
86
|
+
check = run(howTo(section('run', `${FENCE}bash\ntgt --help\n${FENCE}\nThis prints the options.`)));
|
|
87
|
+
assert.strictEqual(check.result.status, 0);
|
|
88
|
+
assert(warningsOf(check, 'run').some(warning => warning.includes('starts with a code block')));
|
|
89
|
+
|
|
90
|
+
// ---- Code-as-bold probe ----------------------------------------------------
|
|
91
|
+
const terminalMap = { app_type: 'terminal', cli_metadata: { binary: 'tgt' } };
|
|
92
|
+
check = run(howTo(section('run', 'Run **tgt --help** and pass **--send-message**; edit **~/.config/tgt/config.toml**. Select **Send**.')), { projectMap: terminalMap });
|
|
93
|
+
assert.strictEqual(check.result.status, 0);
|
|
94
|
+
const bold = warningsOf(check, 'run');
|
|
95
|
+
assert(bold.some(warning => warning.includes('"tgt --help" looks like a command')), JSON.stringify(bold));
|
|
96
|
+
assert(bold.some(warning => warning.includes('"--send-message" looks like a flag')));
|
|
97
|
+
assert(bold.some(warning => warning.includes('config.toml" looks like a path')));
|
|
98
|
+
assert(bold.some(warning => warning.includes('bold UI name "Send"')), 'plain labels still go through the bold check');
|
|
99
|
+
|
|
100
|
+
check = run(howTo(section('run', 'Run **`tgt --help`**.')));
|
|
101
|
+
assert(warningsOf(check, 'run').some(warning => warning.includes('bolds inline code')));
|
|
102
|
+
|
|
103
|
+
// ---- Inline-code fidelity: warn only, dash/underscore-insensitive ----------
|
|
104
|
+
const sources = { blocks: [{ block_id: 'run', verbatim_evidence: ['send_message', 'Usage: tgt [OPTIONS]'] }] };
|
|
105
|
+
check = run(howTo(section('run', 'Pass `--send-message` to `tgt`, then try `--dry-run`.')), { viewSources: sources });
|
|
106
|
+
assert.strictEqual(check.result.status, 0);
|
|
107
|
+
const code = warningsOf(check, 'run');
|
|
108
|
+
assert(!code.some(warning => warning.includes('--send-message')), JSON.stringify(code));
|
|
109
|
+
assert(code.some(warning => warning.includes('"--dry-run" appears in none')), JSON.stringify(code));
|
|
110
|
+
assert(!code.some(warning => warning.includes('"tgt"')), 'plain single words are not grounded');
|
|
111
|
+
check = run(howTo(section('run', 'Try `--dry-run`.')));
|
|
112
|
+
assert.strictEqual(warningsOf(check, 'run').length, 0, 'no sources resolved, nothing to check against');
|
|
113
|
+
|
|
114
|
+
// ---- Redundant command screenshot on terminal projects ----------------------
|
|
115
|
+
const cmdOnly = '<div class="terminal-line"><span class="terminal-prompt">$</span><span class="terminal-cmd">tgt --help</span></div>';
|
|
116
|
+
check = run(howTo(section('run', 'Run the command.', { has_image: true })), { projectMap: terminalMap, files: { 'block_run.html': cmdOnly } });
|
|
117
|
+
assert(warningsOf(check, 'run').some(warning => warning.includes('spends a screenshot on a typed command')));
|
|
118
|
+
check = run(howTo(section('run', 'Run the command.', { has_image: true })), { projectMap: terminalMap, files: { 'block_run.html': cmdOnly + '<span class="terminal-output">Usage: tgt [OPTIONS]</span>' } });
|
|
119
|
+
assert(!warningsOf(check, 'run').some(warning => warning.includes('spends a screenshot')));
|
|
120
|
+
check = run(howTo(section('run', 'Run the command.', { has_image: true })), { files: { 'block_run.html': cmdOnly } });
|
|
121
|
+
assert(!warningsOf(check, 'run').some(warning => warning.includes('spends a screenshot')), 'web projects are not checked');
|
|
122
|
+
|
|
123
|
+
console.log('lint_article_copy block-contract tests passed');
|
|
124
|
+
|
|
125
|
+
// Structured generated evidence must pass both copy and screenshot source checks.
|
|
126
|
+
const generatedLabel = {string:'Create API key',found_in:'form.erb',generated:{kind:'rails_submit',locale:'en',action:'create',binding:'@record',binding_source:'controller.rb',model_source:'model.rb'}};
|
|
127
|
+
const generatedFiles = {
|
|
128
|
+
'form.erb': '<%= form_with(model: @record) do |f| %><%= f.submit %><% end %>',
|
|
129
|
+
'controller.rb': '@record = APIKey.new',
|
|
130
|
+
'model.rb': 'class APIKey < ActiveRecord::Base\nend',
|
|
131
|
+
'config/initializers/inflections.rb': "ActiveSupport::Inflector.inflections(:en) do |i|\n i.acronym 'API'\nend",
|
|
132
|
+
};
|
|
133
|
+
const generatedSources = {blocks:[{block_id:'create',primary_view:'form.erb',verbatim_evidence:[generatedLabel]}]};
|
|
134
|
+
check = run(howTo(section('create','Click **Create API key**.',{has_image:true})),{viewSources:generatedSources,files:generatedFiles});
|
|
135
|
+
assert.equal(check.result.status,0,JSON.stringify(check.report));
|
|
136
|
+
generatedLabel.string = 'Delete everything';
|
|
137
|
+
check = run(howTo(section('create','Click **Delete everything**.',{has_image:true})),{viewSources:generatedSources,files:generatedFiles});
|
|
138
|
+
assert.equal(check.result.status,1,'unverified generated claims must not enter the copy corpus');
|