@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,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* sanitize_css.js — Strip un-compilable framework directives from a CSS file.
|
|
6
|
+
*
|
|
7
|
+
* When we fall back to inlining source CSS (rather than running the project's
|
|
8
|
+
* build pipeline), compile-time directives like @tailwind, @apply, @theme,
|
|
9
|
+
* @plugin, @custom-variant, @config, @source, @utility, @layer, and
|
|
10
|
+
* @import "tailwindcss" leak into the output. Browsers can't parse those —
|
|
11
|
+
* they silently break styling for everything after the first malformed rule.
|
|
12
|
+
* This sanitizer strips them in place.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node sanitize_css.js <css_file>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
const BAD_DIRECTIVES = [
|
|
20
|
+
/^@tailwind\b[^;]*;?\s*$/,
|
|
21
|
+
/^@theme\b/,
|
|
22
|
+
/^@plugin\b[^;]*;?\s*$/,
|
|
23
|
+
/^@custom-variant\b[^;]*;?\s*$/,
|
|
24
|
+
/^@apply\b[^;]*;?\s*$/,
|
|
25
|
+
/^@config\b[^;]*;?\s*$/,
|
|
26
|
+
/^@source\b[^;]*;?\s*$/,
|
|
27
|
+
/^@utility\b/,
|
|
28
|
+
/^@import\s+["']tailwindcss[^;]*;?\s*$/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function count(text, ch) {
|
|
32
|
+
return text.split(ch).length - 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Return { cleaned, removed }. */
|
|
36
|
+
function sanitize(css) {
|
|
37
|
+
const cleaned = [];
|
|
38
|
+
let removed = 0;
|
|
39
|
+
let skipDepth = 0; // brace depth for multi-line blocks like @theme { ... }
|
|
40
|
+
|
|
41
|
+
for (const line of css.split('\n')) {
|
|
42
|
+
const stripped = line.trim();
|
|
43
|
+
|
|
44
|
+
if (skipDepth > 0) {
|
|
45
|
+
skipDepth += count(stripped, '{') - count(stripped, '}');
|
|
46
|
+
removed += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (BAD_DIRECTIVES.some(pattern => pattern.test(stripped))) {
|
|
51
|
+
removed += 1;
|
|
52
|
+
if (stripped.includes('{')) skipDepth = count(stripped, '{') - count(stripped, '}');
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cleaned.push(line);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { cleaned: cleaned.join('\n'), removed };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
if (process.argv.length !== 3) {
|
|
64
|
+
process.stderr.write('Usage: sanitize_css.js <css_file>\n');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cssFile = process.argv[2];
|
|
69
|
+
const css = fs.readFileSync(cssFile, 'utf8');
|
|
70
|
+
const { cleaned, removed } = sanitize(css);
|
|
71
|
+
|
|
72
|
+
if (removed > 0) {
|
|
73
|
+
fs.writeFileSync(cssFile, cleaned);
|
|
74
|
+
process.stderr.write(`Sanitized ${cssFile}: removed ${removed} directive line(s) ` +
|
|
75
|
+
`(${[...css].length} → ${[...cleaned].length} bytes)\n`);
|
|
76
|
+
} else {
|
|
77
|
+
process.stderr.write('CSS clean — no directives to remove\n');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (require.main === module) main();
|
|
82
|
+
|
|
83
|
+
module.exports = { sanitize };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* classify_surface.sh — surface vs code-only repositories: a full-stack Rails
|
|
4
|
+
* app and an Express app with views are web; Rails API mode, an Express JSON
|
|
5
|
+
* API and a Go library are none (mailer templates and test fixtures don't
|
|
6
|
+
* count as an interface); a cobra CLI stays terminal and a React Native app
|
|
7
|
+
* mobile; a frontend framework dependency alone is enough for web.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
const assert = require('assert');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const SCRIPT = path.join(__dirname, 'classify_surface.sh');
|
|
17
|
+
|
|
18
|
+
function write(root, rel, text = '') { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); }
|
|
19
|
+
function classify(files) {
|
|
20
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'surface-'));
|
|
21
|
+
for (const [rel, text] of Object.entries(files)) write(root, rel, text);
|
|
22
|
+
const out = execFileSync('bash', [SCRIPT, root], { encoding: 'utf8' });
|
|
23
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
24
|
+
return Object.fromEntries(out.trim().split('\n').map(line => line.split(/=(.*)/s).slice(0, 2)));
|
|
25
|
+
}
|
|
26
|
+
const RAILS = { Gemfile: "gem 'rails'\n", 'config/routes.rb': 'Rails.application.routes.draw do\nend\n' };
|
|
27
|
+
|
|
28
|
+
// Full-stack Rails: views are an interface.
|
|
29
|
+
assert.strictEqual(classify({
|
|
30
|
+
...RAILS,
|
|
31
|
+
'app/views/layouts/application.html.erb': '<html></html>',
|
|
32
|
+
'app/views/plans/index.html.erb': '<h1>Plans</h1>',
|
|
33
|
+
'app/assets/stylesheets/application.css': 'body {}',
|
|
34
|
+
}).surface, 'web');
|
|
35
|
+
|
|
36
|
+
// Rails API mode, even with mailer templates.
|
|
37
|
+
let r = classify({
|
|
38
|
+
...RAILS,
|
|
39
|
+
'config/application.rb': 'module App\n class Application < Rails::Application\n config.api_only = true\n end\nend\n',
|
|
40
|
+
'app/views/user_mailer/welcome.html.erb': '<p>Hi</p>',
|
|
41
|
+
});
|
|
42
|
+
assert.strictEqual(r.surface, 'none');
|
|
43
|
+
assert.strictEqual(r.source, 'auto');
|
|
44
|
+
assert.match(r.reason, /api_only/);
|
|
45
|
+
|
|
46
|
+
// Rails without api_only whose only templates are mailers and test fixtures: no interface.
|
|
47
|
+
assert.strictEqual(classify({
|
|
48
|
+
...RAILS,
|
|
49
|
+
'app/views/user_mailer/welcome.html.erb': '<p>Hi</p>',
|
|
50
|
+
'app/views/user_mailer/reset.html.erb': '<p>Reset</p>',
|
|
51
|
+
'test/fixtures/files/page.html': '<p>x</p>',
|
|
52
|
+
'spec/support/page.html': '<p>x</p>',
|
|
53
|
+
}).surface, 'none');
|
|
54
|
+
|
|
55
|
+
// Express JSON API vs Express app with views.
|
|
56
|
+
const EXPRESS = { 'package.json': JSON.stringify({ dependencies: { express: '^4' } }), 'src/server.js': 'app.get("/api", ...)' };
|
|
57
|
+
r = classify(EXPRESS);
|
|
58
|
+
assert.strictEqual(r.surface, 'none');
|
|
59
|
+
assert.strictEqual(r.source, 'auto');
|
|
60
|
+
assert.strictEqual(classify({ ...EXPRESS, 'views/index.ejs': '<h1/>', 'views/login.ejs': '<form/>', 'public/app.css': 'a{}' }).surface, 'web');
|
|
61
|
+
|
|
62
|
+
// A frontend framework dependency alone is an interface (templates may live in .js).
|
|
63
|
+
assert.strictEqual(classify({ 'package.json': JSON.stringify({ dependencies: { vue: '^3' } }) }).surface, 'web');
|
|
64
|
+
|
|
65
|
+
// Go library: nothing to classify, nothing to depict.
|
|
66
|
+
r = classify({ 'go.mod': 'module example.com/lib\n', 'lib.go': 'package lib\n' });
|
|
67
|
+
assert.strictEqual(r.surface, 'none');
|
|
68
|
+
assert.strictEqual(r.source, 'default');
|
|
69
|
+
|
|
70
|
+
// Non-web verdicts are interfaces as they stand.
|
|
71
|
+
assert.strictEqual(classify({ 'go.mod': 'module example.com/cli\n\nrequire github.com/spf13/cobra v1.8.0\n', 'main.go': 'package main\n' }).surface, 'terminal');
|
|
72
|
+
assert.strictEqual(classify({ 'package.json': JSON.stringify({ dependencies: { 'react-native': '0.74.0' } }) }).surface, 'mobile');
|
|
73
|
+
|
|
74
|
+
// A Node CLI with no CLI framework (the SupportPages Writer shape): a bin, an
|
|
75
|
+
// MCP server dependency and vendored engine templates/styles is terminal.
|
|
76
|
+
r = classify({
|
|
77
|
+
'package.json': JSON.stringify({ bin: { writer: 'scripts/cli.mjs' }, dependencies: { '@modelcontextprotocol/sdk': '^1', '@clack/prompts': '1.6.0' } }),
|
|
78
|
+
'scripts/cli.mjs': '#!/usr/bin/env node',
|
|
79
|
+
'engine/detect-project/assets/tailwind-fallback.css': 'a{}',
|
|
80
|
+
'engine/detect-project/assets/webtui.css': 'b{}',
|
|
81
|
+
'engine/generate-illustrated-article/templates/page.html': '<html/>',
|
|
82
|
+
'engine/generate-illustrated-article/templates/step.html': '<html/>',
|
|
83
|
+
'.rtfm/branding.css': 'x{}', '.rtfm/previews/a.html': '<p/>', 'output/articles/x/block_a.html': '<p/>', 'output/articles/x/branding.css': 'y{}',
|
|
84
|
+
});
|
|
85
|
+
assert.strictEqual(r.surface, 'terminal');
|
|
86
|
+
assert.match(r.reason, /bin/);
|
|
87
|
+
// A bin beside a frontend framework is still a web app (e.g. a dev-server CLI).
|
|
88
|
+
assert.strictEqual(classify({ 'package.json': JSON.stringify({ bin: { app: 'cli.js' }, dependencies: { 'react-dom': '^18' } }) }).surface, 'web');
|
|
89
|
+
// A Node web app with its own views that also ships a bin stays web.
|
|
90
|
+
assert.strictEqual(classify({ ...EXPRESS, 'package.json': JSON.stringify({ bin: { app: 'bin/app' }, dependencies: { express: '^4' } }),
|
|
91
|
+
'views/index.ejs': '<h1/>', 'views/login.ejs': '<form/>', 'public/app.css': 'a{}' }).surface, 'web');
|
|
92
|
+
// Vendored assets alone never make a server a web app.
|
|
93
|
+
assert.strictEqual(classify({ ...EXPRESS, 'engine/a.css': 'a{}', 'engine/b.css': 'b{}', 'third_party/c.html': '<p/>' }).surface, 'none');
|
|
94
|
+
|
|
95
|
+
// A missing directory is none, not an error.
|
|
96
|
+
assert.strictEqual(execFileSync('bash', [SCRIPT, '/nonexistent/dir'], { encoding: 'utf8' }).split('\n')[0], 'surface=none');
|
|
97
|
+
|
|
98
|
+
// classify_workspace.js: every checkout under a root, sorted, non-repos skipped.
|
|
99
|
+
const { classifyWorkspace } = require('./classify_workspace.js');
|
|
100
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'surface-ws-'));
|
|
101
|
+
write(ws, 'acme-web/.git/HEAD'); write(ws, 'acme-web/package.json', JSON.stringify({ dependencies: { 'react-dom': '^18' } }));
|
|
102
|
+
write(ws, 'acme-api/.git/HEAD'); write(ws, 'acme-api/package.json', JSON.stringify({ dependencies: { express: '^4' } }));
|
|
103
|
+
write(ws, 'notes/readme.md', 'not a repo');
|
|
104
|
+
assert.deepStrictEqual(classifyWorkspace(ws).map(r => [r.directory, r.surface]), [['acme-api', 'none'], ['acme-web', 'web']]);
|
|
105
|
+
assert.deepStrictEqual(classifyWorkspace(path.join(ws, 'missing')), []);
|
|
106
|
+
fs.rmSync(ws, { recursive: true, force: true });
|
|
107
|
+
|
|
108
|
+
console.log('classify_surface tests passed');
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Tests for the Node helpers that replaced the Python scripts and jq filters.
|
|
5
|
+
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const { sanitize } = require('./sanitize_css.js');
|
|
13
|
+
const { overrideCss } = require('./theme_overrides.js');
|
|
14
|
+
const { merge } = require('./merge_json.js');
|
|
15
|
+
const { check } = require('./check_project_map.js');
|
|
16
|
+
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'detect-helpers-'));
|
|
18
|
+
const run = (script, args) => spawnSync(process.execPath, [path.join(__dirname, script), ...args], { encoding: 'utf8' });
|
|
19
|
+
const write = (name, value) => {
|
|
20
|
+
const file = path.join(tmp, name);
|
|
21
|
+
fs.writeFileSync(file, typeof value === 'string' ? value : JSON.stringify(value));
|
|
22
|
+
return file;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// sanitize_css: directives and their blocks go, everything else stays.
|
|
26
|
+
{
|
|
27
|
+
const { cleaned, removed } = sanitize('@import "tailwindcss";\n@theme {\n --x: 1;\n inner { }\n}\n.keep { a: b; }\n @apply foo;\n@plugin "x";\n');
|
|
28
|
+
assert.strictEqual(cleaned, '.keep { a: b; }\n');
|
|
29
|
+
assert.strictEqual(removed, 7);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// theme_overrides: expected output captured from the Python implementation.
|
|
33
|
+
{
|
|
34
|
+
const detect = {
|
|
35
|
+
theme_overrides: {
|
|
36
|
+
colors: { primary: '#3366ff', secondary: '#ff6600', dark: 'null', x: '' },
|
|
37
|
+
custom_colors: { brand_blue: '#123456', empty: '' },
|
|
38
|
+
fonts: { body: 'Inter, sans-serif', heading: 'Poppins' },
|
|
39
|
+
border_radius: '6px',
|
|
40
|
+
extra_css: '@tailwind base;\n.foo { color: red; }\n @apply p-4;\n@layer x {\n.bar{}\n',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
assert.strictEqual(overrideCss(detect, 'bootstrap'), '\n/* Theme overrides from project variables */\n:root {\n --bs-primary: #3366ff;\n --bs-primary-rgb: 51, 102, 255;\n --bs-secondary: #ff6600;\n --bs-secondary-rgb: 255, 102, 0;\n --bs-body-font-family: Inter, sans-serif;\n --bs-heading-font-family: Poppins;\n --bs-border-radius: 6px;\n}\n.text-brand-blue { color: #123456 !important; }\n.bg-brand-blue { background-color: #123456 !important; }\n.btn-primary { background-color: #3366ff; border-color: #3366ff; }\n.btn-primary:hover { background-color: #3366ff; border-color: #3366ff; opacity: 0.9; }\n.btn-outline-primary { color: #3366ff; border-color: #3366ff; }\n.btn-outline-primary:hover { background-color: #3366ff; border-color: #3366ff; color: #fff; }\na { color: #3366ff; }\n.text-primary { color: #3366ff !important; }\n.bg-primary { background-color: #3366ff !important; }\n.btn-secondary { background-color: #ff6600; border-color: #ff6600; }\n\n/* Extra project overrides */\n.foo { color: red; }\n.bar{}\n');
|
|
44
|
+
assert.strictEqual(overrideCss(detect, 'tailwind'), '\n/* Theme overrides from project variables */\n:root {\n --color-primary: #3366ff;\n --color-secondary: #ff6600;\n font-family: Inter, sans-serif;\n --font-heading: Poppins;\n --radius: 6px;\n}\nh1, h2, h3, h4, h5, h6 { font-family: Poppins; }\n.text-primary { color: #3366ff; }\n.bg-primary { background-color: #3366ff; }\n.text-secondary { color: #ff6600; }\n.bg-secondary { background-color: #ff6600; }\n.text-brand-blue { color: #123456; }\n.bg-brand-blue { background-color: #123456; }\n\n/* Extra project overrides */\n.foo { color: red; }\n.bar{}\n');
|
|
45
|
+
// Skill-style detection maps default_colors/scss_vars when no theme colours exist.
|
|
46
|
+
const skillStyle = overrideCss({ default_colors: { primary: '#0ea5e9', accent: '#f43f5e' }, scss_vars: { 'body-color': '#111' } }, 'custom');
|
|
47
|
+
assert(skillStyle.includes(' --color-secondary: #f43f5e;'));
|
|
48
|
+
assert(skillStyle.includes(' --color-body_color: #111;'));
|
|
49
|
+
// An invalid hex colour fails the run, as the caller treats that as "no overrides".
|
|
50
|
+
assert.throws(() => overrideCss({ theme_overrides: { colors: { primary: '#zz0000' } } }, 'bootstrap'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// json_get: jq-compatible reads used by the SKILL.md summaries.
|
|
54
|
+
{
|
|
55
|
+
const file = write('map.json', {
|
|
56
|
+
framework: 'bootstrap', has_tui: false, css_files: ['a.css', null, 'b.css'],
|
|
57
|
+
route_index: { a: { method: 'CANVAS' }, b: { method: 'GET' }, c: { method: 'CANVAS' } },
|
|
58
|
+
mockup_model_recommendation: { tier: 'medium', reasons: [{ code: 'r1' }, { code: 'r2' }] },
|
|
59
|
+
});
|
|
60
|
+
const out = args => run('json_get.js', args).stdout.trim();
|
|
61
|
+
assert.strictEqual(out([file, 'framework', 'none']), 'bootstrap');
|
|
62
|
+
assert.strictEqual(out([file, 'missing', 'none']), 'none');
|
|
63
|
+
assert.strictEqual(out([file, 'missing']), 'null');
|
|
64
|
+
assert.strictEqual(out([file, 'has_tui', 'false']), 'false');
|
|
65
|
+
assert.strictEqual(out([file, 'css_files[]', '']), 'a.css\nb.css');
|
|
66
|
+
assert.strictEqual(out(['--length', file, 'route_index']), '3');
|
|
67
|
+
assert.strictEqual(out(['--length', file, 'missing']), '0');
|
|
68
|
+
assert.strictEqual(out(['--join', ', ', file, 'mockup_model_recommendation.reasons[].code']), 'r1, r2');
|
|
69
|
+
assert.strictEqual(out(['--count', file, 'route_index[]', 'method=CANVAS']), '2');
|
|
70
|
+
assert.strictEqual(run('json_get.js', ['--exists', file, 'route_index.a']).status, 0);
|
|
71
|
+
assert.strictEqual(run('json_get.js', ['--exists', file, 'route_index.z']).status, 1);
|
|
72
|
+
assert.strictEqual(run('json_get.js', ['--valid', file]).status, 0);
|
|
73
|
+
assert.strictEqual(run('json_get.js', ['--valid', write('bad.json', '{bad')]).status, 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// write_branding: web merges the detect output; patch modes keep existing fields.
|
|
77
|
+
{
|
|
78
|
+
const brandDir = fs.mkdtempSync(path.join(tmp, 'brand-'));
|
|
79
|
+
fs.writeFileSync(path.join(brandDir, 'branding-detect.json'), JSON.stringify({ framework: 'tailwind', google_fonts: ['x'] }));
|
|
80
|
+
assert.strictEqual(run('write_branding.js', ['web', brandDir, 'tailwind_v4', ' 1234']).status, 0);
|
|
81
|
+
const web = JSON.parse(fs.readFileSync(path.join(brandDir, 'branding.json'), 'utf8'));
|
|
82
|
+
assert.deepStrictEqual({ ...web, generated_at: undefined }, {
|
|
83
|
+
framework: 'tailwind', google_fonts: ['x'], compiled_css_path: 'branding.css',
|
|
84
|
+
compilation_method: 'tailwind_v4', compiled_css_bytes: 1234, generated_at: undefined,
|
|
85
|
+
});
|
|
86
|
+
assert.match(web.generated_at, /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/);
|
|
87
|
+
|
|
88
|
+
const branding = path.join(brandDir, 'branding.json');
|
|
89
|
+
assert.strictEqual(run('write_branding.js', ['game', branding, 'phaser', '320', '180', '1280', '720', '55']).status, 0);
|
|
90
|
+
const game = JSON.parse(fs.readFileSync(branding, 'utf8'));
|
|
91
|
+
assert.strictEqual(game.framework, 'tailwind');
|
|
92
|
+
assert.deepStrictEqual(game.canvas_size, { width: 320, height: 180 });
|
|
93
|
+
assert.strictEqual(game.compiled_css_bytes, 55);
|
|
94
|
+
assert.strictEqual(run('write_branding.js', ['game', branding, 'phaser', '320', 'abc', '1', '1', '1']).status, 1);
|
|
95
|
+
assert.strictEqual(run('write_branding.js', ['win32', branding]).status, 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// merge_json: objects merge, arrays replace, omitted keys survive.
|
|
99
|
+
assert.deepStrictEqual(
|
|
100
|
+
merge({ app_type: 'web', route_index: { a: { x: 1 } }, layouts: [1] }, { route_index: { b: { y: 2 } }, layouts: [2, 3] }),
|
|
101
|
+
{ app_type: 'web', route_index: { a: { x: 1 }, b: { y: 2 } }, layouts: [2, 3] },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// check_project_map: the gates that replaced the jq validation.
|
|
105
|
+
{
|
|
106
|
+
const state = id => ({
|
|
107
|
+
id, kind: 'overview', topic_tags: ['t'], instructional_priority: 'orientation', canvas_presentation: 'full',
|
|
108
|
+
screenshot_required: true, visible_regions: ['top'], required_regions: [], required_strings: [], source_files: [],
|
|
109
|
+
});
|
|
110
|
+
const web = {
|
|
111
|
+
app_type: 'web', app_type_source: 'hint', schema_version: 5, dir_map: {},
|
|
112
|
+
layouts: [{
|
|
113
|
+
name: 'app',
|
|
114
|
+
runtime_chrome: {
|
|
115
|
+
source_kind: 'repo', source_files: [],
|
|
116
|
+
regions: [{ id: 'top', placement: 'top', required: true, appearance: 'bar', required_strings: [], source_files: [], items: [{ id: 'i', kind: 'k', description: 'd', required_strings: [] }] }],
|
|
117
|
+
states: [state('a'), state('b'), state('c')],
|
|
118
|
+
},
|
|
119
|
+
}],
|
|
120
|
+
};
|
|
121
|
+
assert.deepStrictEqual(check('web', web), []);
|
|
122
|
+
const tooMany = structuredClone(web);
|
|
123
|
+
tooMany.layouts[0].runtime_chrome.states.push(state('d'));
|
|
124
|
+
assert.match(check('web', tooMany).join('\n'), /at most 3 states/);
|
|
125
|
+
const badPlacement = structuredClone(web);
|
|
126
|
+
badPlacement.layouts[0].runtime_chrome.regions[0].placement = 'middle';
|
|
127
|
+
assert.match(check('web', badPlacement).join('\n'), /placement/);
|
|
128
|
+
|
|
129
|
+
const codebase = fs.mkdtempSync(path.join(tmp, 'rails-'));
|
|
130
|
+
fs.mkdirSync(path.join(codebase, 'config'));
|
|
131
|
+
fs.writeFileSync(path.join(codebase, 'config', 'routes.rb'), '');
|
|
132
|
+
assert.match(check('web', web, codebase).join('\n'), /route_index/);
|
|
133
|
+
|
|
134
|
+
const game = { app_type: 'game', app_type_source: 'hint', game_metadata: {}, app_shell: null, route_index: { play: { method: 'CANVAS', scene_anatomy: { layers: ['bg'] } } } };
|
|
135
|
+
assert.deepStrictEqual(check('game', game), []);
|
|
136
|
+
assert.match(check('game', { ...game, route_index: { play: { method: 'CANVAS', scene_anatomy: { layers: [] } } } }).join('\n'), /layers/);
|
|
137
|
+
assert.match(check('mobile', { app_type: 'web', app_type_source: 'x' }).join('\n'), /app_type must be "mobile"/);
|
|
138
|
+
assert.strictEqual(run('check_project_map.js', ['game', write('game.json', game)]).status, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
142
|
+
console.log('node helper tests passed');
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { classify, POLICY_VERSION } = require('./recommend_model_tier.js');
|
|
10
|
+
|
|
11
|
+
const classifier = path.join(__dirname, 'recommend_model_tier.js');
|
|
12
|
+
const webBranding = { framework: 'rails', compilation_method: 'source_compile' };
|
|
13
|
+
|
|
14
|
+
function shell(regionCount = 0, navItems = 0) {
|
|
15
|
+
const out = { nav_items: Array.from({ length: navItems }, (_, index) => ({ label: `Item ${index}` })) };
|
|
16
|
+
const keys = ['top_bar', 'titlebar', 'menu_bar', 'global_sidebar', 'primary_sidebar', 'secondary_sidebar'];
|
|
17
|
+
keys.slice(0, regionCount).forEach(key => { out[key] = `${key} definition`; });
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function routes(count, fanout = 0) {
|
|
22
|
+
return Object.fromEntries(Array.from({ length: count }, (_, index) => [
|
|
23
|
+
`route-${index}`,
|
|
24
|
+
{ path: `/route-${index}`, partials_expanded: Array.from({ length: fanout }, (__, part) => `part-${part}`) },
|
|
25
|
+
]));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expectTier(name, map, branding, tier) {
|
|
29
|
+
const recommendation = classify(map, branding);
|
|
30
|
+
assert.strictEqual(recommendation.tier, tier, `${name}: ${JSON.stringify(recommendation)}`);
|
|
31
|
+
assert.strictEqual(recommendation.policy_version, POLICY_VERSION);
|
|
32
|
+
assert(!JSON.stringify(recommendation).match(/claude|gpt|codex|cursor|opus|sonnet/i),
|
|
33
|
+
`${name}: recommendation leaked a concrete model or harness`);
|
|
34
|
+
return recommendation;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Representative project shapes.
|
|
38
|
+
expectTier('server-rendered web app', {
|
|
39
|
+
app_type: 'web', framework: 'rails', layouts: Array.from({ length: 7 }, () => ({})),
|
|
40
|
+
route_index: routes(6), app_shell: null,
|
|
41
|
+
}, webBranding, 'low');
|
|
42
|
+
|
|
43
|
+
expectTier('WordPress Site Editor runtime', {
|
|
44
|
+
app_type: 'web', framework: 'custom', route_index: routes(29), app_shell: shell(2, 11),
|
|
45
|
+
layouts: [{ runtime_chrome: {
|
|
46
|
+
regions: Array.from({ length: 11 }, (_, index) => ({ placement: index === 2 ? 'canvas' : 'left' })),
|
|
47
|
+
states: Array.from({ length: 8 }, () => ({})),
|
|
48
|
+
}}],
|
|
49
|
+
}, { framework: 'custom', compilation_method: 'source_compile' }, 'high');
|
|
50
|
+
|
|
51
|
+
expectTier('desktop database workspace', {
|
|
52
|
+
app_type: 'desktop', framework: 'vue', layouts: [], route_index: routes(14), app_shell: shell(6, 3),
|
|
53
|
+
}, { framework: 'vue', compilation_method: 'source_compile' }, 'high');
|
|
54
|
+
|
|
55
|
+
for (const [name, map] of [
|
|
56
|
+
['Immich-style mobile app', { app_type: 'mobile', framework: 'mobile', screen_index: routes(20), app_shell: shell(1, 4) }],
|
|
57
|
+
['Notepad++-style Win32 app', { app_type: 'win32', framework: 'win32', dialog_index: routes(20), app_shell: shell(1, 13) }],
|
|
58
|
+
['TGT-style terminal app', { app_type: 'terminal', framework: 'terminal', command_index: routes(8), app_shell: null }],
|
|
59
|
+
['Transmission-style macOS app', { app_type: 'macos', framework: 'macos', view_index: routes(12), app_shell: shell(2, 0) }],
|
|
60
|
+
]) expectTier(name, map, { framework: map.framework }, 'medium');
|
|
61
|
+
|
|
62
|
+
expectTier('canvas game', {
|
|
63
|
+
app_type: 'game', framework: 'custom', layouts: [], route_index: {
|
|
64
|
+
play: { method: 'CANVAS', scene_anatomy: { layers: ['background', 'player'] } },
|
|
65
|
+
},
|
|
66
|
+
}, { framework: 'custom' }, 'high');
|
|
67
|
+
|
|
68
|
+
expectTier('simple runtime mount', {
|
|
69
|
+
app_type: 'web', framework: 'rails', layouts: [{ runtime_chrome: { regions: [{}], states: [{}] } }],
|
|
70
|
+
route_index: routes(3),
|
|
71
|
+
}, webBranding, 'medium');
|
|
72
|
+
|
|
73
|
+
expectTier('fallback CSS floor', {
|
|
74
|
+
app_type: 'web', framework: 'tailwind', layouts: [], route_index: routes(3),
|
|
75
|
+
}, { framework: 'tailwind', compilation_method: 'fallback_synthesis' }, 'medium');
|
|
76
|
+
|
|
77
|
+
expectTier('unknown metadata is conservative', { layouts: [], route_index: {} }, {}, 'medium');
|
|
78
|
+
|
|
79
|
+
const boundaryMedium = expectTier('medium score boundary', {
|
|
80
|
+
app_type: 'web', framework: 'rails', layouts: Array.from({ length: 5 }, () => ({})),
|
|
81
|
+
route_index: routes(25), app_shell: shell(0, 8),
|
|
82
|
+
}, webBranding, 'medium');
|
|
83
|
+
assert.strictEqual(boundaryMedium.score, 3);
|
|
84
|
+
|
|
85
|
+
expectTier('high score boundary', {
|
|
86
|
+
app_type: 'web', framework: 'custom', layouts: Array.from({ length: 5 }, () => ({})),
|
|
87
|
+
route_index: routes(25, 8), app_shell: shell(0, 8),
|
|
88
|
+
}, { framework: 'custom' }, 'high');
|
|
89
|
+
|
|
90
|
+
assert.throws(() => classify([], {}), /project map must be a JSON object/);
|
|
91
|
+
assert.throws(() => classify({}, []), /branding cache must be a JSON object/);
|
|
92
|
+
|
|
93
|
+
// Exercise the actual CLI, atomic write, replacement, and byte-stable rerun.
|
|
94
|
+
{
|
|
95
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rtfm-model-tier-'));
|
|
96
|
+
const mapPath = path.join(root, 'project_map.json');
|
|
97
|
+
const brandingPath = path.join(root, 'branding.json');
|
|
98
|
+
fs.writeFileSync(mapPath, JSON.stringify({
|
|
99
|
+
app_type: 'web', framework: 'rails', layouts: [], route_index: routes(2),
|
|
100
|
+
mockup_model_recommendation: { tier: 'stale', reasons: ['stale'] },
|
|
101
|
+
}, null, 2));
|
|
102
|
+
fs.writeFileSync(brandingPath, JSON.stringify(webBranding));
|
|
103
|
+
let proc = spawnSync(process.execPath, [classifier, mapPath, brandingPath], { encoding: 'utf8' });
|
|
104
|
+
assert.strictEqual(proc.status, 0, proc.stderr);
|
|
105
|
+
const first = fs.readFileSync(mapPath, 'utf8');
|
|
106
|
+
const parsed = JSON.parse(first);
|
|
107
|
+
assert.strictEqual(parsed.mockup_model_recommendation.tier, 'low');
|
|
108
|
+
assert(!JSON.stringify(parsed.mockup_model_recommendation).includes('stale'));
|
|
109
|
+
proc = spawnSync(process.execPath, [classifier, mapPath, brandingPath], { encoding: 'utf8' });
|
|
110
|
+
assert.strictEqual(proc.status, 0, proc.stderr);
|
|
111
|
+
assert.strictEqual(fs.readFileSync(mapPath, 'utf8'), first);
|
|
112
|
+
fs.writeFileSync(mapPath, '{ malformed json');
|
|
113
|
+
proc = spawnSync(process.execPath, [classifier, mapPath, brandingPath], { encoding: 'utf8' });
|
|
114
|
+
assert.notStrictEqual(proc.status, 0);
|
|
115
|
+
assert.match(proc.stderr, /cannot read project map/);
|
|
116
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('model tier recommendation tests passed');
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* resolve_route_chains.js + the census resolver on a synthetic monorepo:
|
|
4
|
+
* tsconfig `paths` (JSONC, alias keys containing "/*"), a workspace package
|
|
5
|
+
* with `exports` + a barrel, a locally-defined sub-component, SvelteKit `$lib`,
|
|
6
|
+
* an Angular selector, the Rails controller→view convention, layout chains,
|
|
7
|
+
* kind inheritance, `partials_expanded` fill-without-overwrite, exit 0 on
|
|
8
|
+
* missing input.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
const assert = require('assert');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execFileSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const SCRIPT = path.join(__dirname, 'resolve_route_chains.js');
|
|
18
|
+
const census = require('./include_census.js');
|
|
19
|
+
|
|
20
|
+
function write(root, rel, text) { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); }
|
|
21
|
+
function run(mapPath, root, extra = []) {
|
|
22
|
+
return execFileSync('node', [SCRIPT, mapPath, root, ...extra], { encoding: 'utf8' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fixture() {
|
|
26
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-'));
|
|
27
|
+
write(root, 'package.json', JSON.stringify({ name: 'mono', workspaces: ['apps/*', 'packages/*'] }));
|
|
28
|
+
// apps/web — JSONC tsconfig whose alias key carries "/*" (a regex stripper would eat it)
|
|
29
|
+
write(root, 'apps/web/package.json', JSON.stringify({ name: '@mono/web' }));
|
|
30
|
+
write(root, 'apps/web/tsconfig.json', `{
|
|
31
|
+
/* base */
|
|
32
|
+
"compilerOptions": {
|
|
33
|
+
"baseUrl": ".", // comment after
|
|
34
|
+
"paths": { "~/*": ["modules/*"], "@ui/*": ["../../packages/ui/src/*"] },
|
|
35
|
+
},
|
|
36
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
37
|
+
}`);
|
|
38
|
+
write(root, 'apps/web/app/things/page.tsx', `import Shell from "~/shell/Shell";
|
|
39
|
+
import { DataTable, StatusBadge } from "@mono/ui/components/table";
|
|
40
|
+
import { HeaderBar } from "@ui/HeaderBar";
|
|
41
|
+
import { FancyModal } from "@mono/ui/components/modal";
|
|
42
|
+
const LocalRow = () => <tr><td>local</td></tr>;
|
|
43
|
+
export default function Page({ admin }) {
|
|
44
|
+
return (<Shell>
|
|
45
|
+
<HeaderBar />
|
|
46
|
+
<DataTable><LocalRow /><StatusBadge /></DataTable>
|
|
47
|
+
{admin && <AdminPanel />}
|
|
48
|
+
<FancyModal />
|
|
49
|
+
</Shell>);
|
|
50
|
+
}
|
|
51
|
+
import { AdminPanel } from "~/admin/AdminPanel";
|
|
52
|
+
`);
|
|
53
|
+
write(root, 'apps/web/modules/shell/Shell.tsx', `import { SideNav } from "~/shell/SideNav";
|
|
54
|
+
export default function Shell({ children }) { return <div><SideNav /><main>{children}</main></div>; }`);
|
|
55
|
+
write(root, 'apps/web/modules/shell/SideNav.tsx', `export const SideNav = () => <nav><a>Things</a></nav>;`);
|
|
56
|
+
write(root, 'apps/web/modules/admin/AdminPanel.tsx', `import { DangerZone } from "~/admin/DangerZone";
|
|
57
|
+
export const AdminPanel = () => <section><DangerZone /></section>;`);
|
|
58
|
+
write(root, 'apps/web/modules/admin/DangerZone.tsx', `export const DangerZone = () => <p>danger</p>;`);
|
|
59
|
+
write(root, 'apps/web/app/layout.tsx', `import Shell from "~/shell/Shell";
|
|
60
|
+
export default function L({ children }) { return <Shell>{children}</Shell>; }`);
|
|
61
|
+
// packages/ui — workspace package with exports + barrel
|
|
62
|
+
write(root, 'packages/ui/package.json', JSON.stringify({ name: '@mono/ui', exports: { './components/table': './components/table/index.ts', './components/modal': './components/modal/index.ts' } }));
|
|
63
|
+
write(root, 'packages/ui/components/table/index.ts', `export { DataTable } from "./DataTable";\nexport * from "./StatusBadge";`);
|
|
64
|
+
write(root, 'packages/ui/components/table/DataTable.tsx', `export const DataTable = ({ children }) => <table>{children}</table>;`);
|
|
65
|
+
write(root, 'packages/ui/components/table/StatusBadge.tsx', `export const StatusBadge = () => <span/>;`);
|
|
66
|
+
write(root, 'packages/ui/components/modal/index.ts', `export { FancyModal } from "./FancyModal";`);
|
|
67
|
+
write(root, 'packages/ui/components/modal/FancyModal.tsx', `import { ModalFooter } from "./ModalFooter";\nexport const FancyModal = () => <div role="dialog"><ModalFooter/></div>;`);
|
|
68
|
+
write(root, 'packages/ui/components/modal/ModalFooter.tsx', `export const ModalFooter = () => <footer/>;`);
|
|
69
|
+
write(root, 'packages/ui/src/HeaderBar.tsx', `export const HeaderBar = () => <header>Things</header>;`);
|
|
70
|
+
// apps/kit — SvelteKit with $lib
|
|
71
|
+
write(root, 'apps/kit/package.json', JSON.stringify({ name: '@mono/kit' }));
|
|
72
|
+
write(root, 'apps/kit/src/routes/albums/+page.svelte', `<script>\n import Albums from '$lib/components/Albums.svelte';\n</script>\n<Albums />`);
|
|
73
|
+
write(root, 'apps/kit/src/lib/components/Albums.svelte', `<ul><li>Album</li></ul>`);
|
|
74
|
+
// apps/ng — Angular selectors
|
|
75
|
+
write(root, 'apps/ng/package.json', JSON.stringify({ name: '@mono/ng' }));
|
|
76
|
+
write(root, 'apps/ng/src/main.ts', ``);
|
|
77
|
+
write(root, 'apps/ng/src/app/dash/dash.component.html', `<pngx-page-header></pngx-page-header>\n<ng-container></ng-container>\n<pngx-stats-widget />`);
|
|
78
|
+
write(root, 'apps/ng/src/app/common/page-header/page-header.component.ts', `@Component({ selector: 'pngx-page-header', templateUrl: './page-header.component.html' })\nexport class PageHeaderComponent {}`);
|
|
79
|
+
write(root, 'apps/ng/src/app/common/page-header/page-header.component.html', `<h1>Header</h1>`);
|
|
80
|
+
write(root, 'apps/ng/src/app/dash/widgets/stats/stats.component.ts', `@Component({ selector: 'pngx-stats-widget', templateUrl: './stats.component.html' })\nexport class StatsComponent {}`);
|
|
81
|
+
write(root, 'apps/ng/src/app/dash/widgets/stats/stats.component.html', `<p>Stats</p>`);
|
|
82
|
+
// Rails convention
|
|
83
|
+
write(root, 'app/views/things/index.html.erb', `<%= render "layouts/header" %>\n<h1>Things</h1>`);
|
|
84
|
+
write(root, 'app/views/layouts/_header.html.erb', `<header/>`);
|
|
85
|
+
const map = {
|
|
86
|
+
route_index: {
|
|
87
|
+
things: { method: 'GET', path: '/things', primary_view: 'apps/web/app/things/page.tsx', layout: 'root' },
|
|
88
|
+
albums: { method: 'GET', path: '/albums', primary_view: 'apps/kit/src/routes/albums/+page.svelte', partials_expanded: ['already/listed.svelte'] },
|
|
89
|
+
dash: { method: 'GET', path: '/dash', primary_view: 'apps/ng/src/app/dash/dash.component.html' },
|
|
90
|
+
rails_things: { method: 'GET', path: '/rails/things', controller: 'things#index' },
|
|
91
|
+
ghost: { method: 'GET', path: '/ghost', primary_view: 'apps/web/app/missing.tsx' },
|
|
92
|
+
},
|
|
93
|
+
layouts: [{ name: 'root', path: 'apps/web/app/layout.tsx', chrome: [] }],
|
|
94
|
+
};
|
|
95
|
+
const mapPath = path.join(root, 'project_map.json');
|
|
96
|
+
fs.writeFileSync(mapPath, JSON.stringify(map, null, 2));
|
|
97
|
+
return { root, mapPath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── resolver unit checks ────────────────────────────────────────────────────
|
|
101
|
+
{
|
|
102
|
+
const { root } = fixture();
|
|
103
|
+
const from = path.join(root, 'apps/web/app/things');
|
|
104
|
+
assert.strictEqual(census.resolveModule('~/shell/Shell', from, root), path.join(root, 'apps/web/modules/shell/Shell.tsx'), 'tsconfig paths alias (JSONC with "/*" keys)');
|
|
105
|
+
assert.strictEqual(census.resolveModule('@ui/HeaderBar', from, root), path.join(root, 'packages/ui/src/HeaderBar.tsx'), 'alias into a sibling package');
|
|
106
|
+
assert.strictEqual(census.resolveModule('@mono/ui/components/table', from, root), path.join(root, 'packages/ui/components/table/index.ts'), 'workspace package exports entry');
|
|
107
|
+
assert.strictEqual(census.followBarrel(census.resolveModule('@mono/ui/components/table', from, root), 'DataTable', root), path.join(root, 'packages/ui/components/table/DataTable.tsx'), 'barrel export { X } from');
|
|
108
|
+
assert.strictEqual(census.followBarrel(census.resolveModule('@mono/ui/components/table', from, root), 'StatusBadge', root), path.join(root, 'packages/ui/components/table/StatusBadge.tsx'), 'barrel export * from');
|
|
109
|
+
assert.strictEqual(census.resolveModule('react', from, root), null, 'bare external stays unresolved');
|
|
110
|
+
const refs = census.includeRefs(root, 'apps/web/app/things/page.tsx');
|
|
111
|
+
const byName = Object.fromEntries(refs.map(r => [r.name, r]));
|
|
112
|
+
assert(!('LocalRow' in byName), 'a component defined in the same file is not an include');
|
|
113
|
+
assert.strictEqual(byName.Shell.file, 'apps/web/modules/shell/Shell.tsx');
|
|
114
|
+
assert.strictEqual(byName.AdminPanel.guarded, true, '&& guard');
|
|
115
|
+
assert.strictEqual(byName.FancyModal.overlay, true, 'modal by name');
|
|
116
|
+
const ng = Object.fromEntries(census.includeRefs(root, 'apps/ng/src/app/dash/dash.component.html').map(r => [r.name, r]));
|
|
117
|
+
assert.strictEqual(ng['pngx-page-header'].file, 'apps/ng/src/app/common/page-header/page-header.component.html', 'angular selector → template');
|
|
118
|
+
assert.strictEqual(ng['pngx-stats-widget'].file, 'apps/ng/src/app/dash/widgets/stats/stats.component.html');
|
|
119
|
+
assert(!('ng-container' in ng), 'framework tags skipped');
|
|
120
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── script end-to-end ───────────────────────────────────────────────────────
|
|
124
|
+
{
|
|
125
|
+
const { root, mapPath } = fixture();
|
|
126
|
+
const out = run(mapPath, root, ['--json', path.join(root, 'report.json')]);
|
|
127
|
+
assert(/4 route\(s\) resolved/.test(out), out);
|
|
128
|
+
assert(/1 skipped/.test(out), out);
|
|
129
|
+
assert(/1 layout chain\(s\)/.test(out), out);
|
|
130
|
+
const pm = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
|
131
|
+
const things = pm.route_index.things;
|
|
132
|
+
const files = things.render_chain.map(c => c.file);
|
|
133
|
+
const kind = f => things.render_chain.find(c => c.file === f).kind;
|
|
134
|
+
assert(files.includes('apps/web/modules/shell/Shell.tsx'), 'alias import in chain');
|
|
135
|
+
assert(files.includes('apps/web/modules/shell/SideNav.tsx'), 'depth-2 through the alias');
|
|
136
|
+
assert(files.includes('packages/ui/components/table/DataTable.tsx'), 'barrel followed to the component');
|
|
137
|
+
assert(files.includes('packages/ui/components/table/StatusBadge.tsx'), 'export * barrel followed');
|
|
138
|
+
assert(!files.includes('packages/ui/components/table/index.ts'), 'the barrel itself is not a chain file');
|
|
139
|
+
assert.strictEqual(kind('apps/web/modules/admin/AdminPanel.tsx'), 'conditional');
|
|
140
|
+
assert.strictEqual(kind('apps/web/modules/admin/DangerZone.tsx'), 'conditional', 'inherits the parent guard');
|
|
141
|
+
assert.strictEqual(kind('packages/ui/components/modal/FancyModal.tsx'), 'overlay');
|
|
142
|
+
assert.strictEqual(kind('packages/ui/components/modal/ModalFooter.tsx'), 'overlay', 'inherits overlay-ness');
|
|
143
|
+
assert.strictEqual(kind('apps/web/modules/shell/Shell.tsx'), 'unconditional');
|
|
144
|
+
assert.deepStrictEqual(things.partials_expanded, things.render_chain.filter(c => c.kind === 'unconditional').map(c => c.file), 'partials_expanded filled from the unconditional chain');
|
|
145
|
+
assert.strictEqual(things.render_chain_stats.files, things.render_chain.length);
|
|
146
|
+
assert.strictEqual(things.render_chain_stats.truncated, false);
|
|
147
|
+
assert(things.render_chain_stats.unresolved >= 0);
|
|
148
|
+
assert.deepStrictEqual(pm.route_index.albums.partials_expanded, ['already/listed.svelte'], 'existing partials_expanded never overwritten');
|
|
149
|
+
assert.deepStrictEqual(pm.route_index.albums.render_chain.map(c => c.file), ['apps/kit/src/lib/components/Albums.svelte'], '$lib resolves against the owning package');
|
|
150
|
+
assert.deepStrictEqual(pm.route_index.dash.render_chain.map(c => c.file).sort(), ['apps/ng/src/app/common/page-header/page-header.component.html', 'apps/ng/src/app/dash/widgets/stats/stats.component.html'].sort(), 'angular chain');
|
|
151
|
+
assert.strictEqual(pm.route_index.rails_things.primary_view, 'app/views/things/index.html.erb', 'rails convention fallback');
|
|
152
|
+
assert.strictEqual(pm.route_index.rails_things.primary_view_source, 'convention');
|
|
153
|
+
assert.deepStrictEqual(pm.route_index.rails_things.render_chain.map(c => c.file), ['app/views/layouts/_header.html.erb']);
|
|
154
|
+
assert(!pm.route_index.ghost.render_chain, 'unreadable primary_view is skipped untouched');
|
|
155
|
+
assert.deepStrictEqual(pm.layouts[0].render_chain.map(c => c.file), ['apps/web/modules/shell/Shell.tsx', 'apps/web/modules/shell/SideNav.tsx'], 'layout chain');
|
|
156
|
+
assert.strictEqual(pm.route_chain_resolver.version, 1);
|
|
157
|
+
const report = JSON.parse(fs.readFileSync(path.join(root, 'report.json'), 'utf8'));
|
|
158
|
+
assert.strictEqual(report.resolved, 4); assert.strictEqual(report.skipped, 1);
|
|
159
|
+
// caps
|
|
160
|
+
const again = run(mapPath, root, ['--max-files', '2']);
|
|
161
|
+
const capped = JSON.parse(fs.readFileSync(mapPath, 'utf8')).route_index.things;
|
|
162
|
+
assert.strictEqual(capped.render_chain.length, 2); assert.strictEqual(capped.render_chain_stats.truncated, true);
|
|
163
|
+
assert(again.includes('resolve_route_chains'));
|
|
164
|
+
// idempotent re-run keeps partials_expanded from the first (uncapped) pass
|
|
165
|
+
assert(capped.partials_expanded.length > 2, 'filled partials_expanded survives a re-run');
|
|
166
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── never fails a detect ────────────────────────────────────────────────────
|
|
170
|
+
{
|
|
171
|
+
const out = run('/nonexistent/project_map.json', os.tmpdir());
|
|
172
|
+
assert(/usage/.test(out));
|
|
173
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-'));
|
|
174
|
+
const mp = path.join(root, 'project_map.json');
|
|
175
|
+
fs.writeFileSync(mp, '{"framework":"rails"}');
|
|
176
|
+
assert(/no route_index/.test(run(mp, root)));
|
|
177
|
+
fs.writeFileSync(mp, '{not json');
|
|
178
|
+
assert(/unreadable/.test(run(mp, root)));
|
|
179
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('resolve_route_chains tests passed');
|