@12ui/design 0.2.29 → 0.2.30
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/README.md +31 -6
- package/SKILL.md +1 -1
- package/dist/branch-execution-types.d.ts +2 -0
- package/dist/branch-execution-types.d.ts.map +1 -1
- package/dist/branch-execution.d.ts.map +1 -1
- package/dist/branch-execution.js +3 -0
- package/dist/branch-execution.js.map +1 -1
- package/dist/branch-run-record.d.ts +1 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/cli-arguments.d.ts.map +1 -1
- package/dist/cli-arguments.js +1 -0
- package/dist/cli-arguments.js.map +1 -1
- package/dist/cli-branch-command.d.ts +4 -0
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +30 -3
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-capabilities.d.ts +9 -0
- package/dist/cli-capabilities.d.ts.map +1 -1
- package/dist/cli-capabilities.js +9 -0
- package/dist/cli-capabilities.js.map +1 -1
- package/dist/cli-help/design.d.ts +8 -1
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +12 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +8 -1
- package/dist/cli-help/index.d.ts.map +1 -1
- package/dist/cli-prototype-command.d.ts +12 -0
- package/dist/cli-prototype-command.d.ts.map +1 -0
- package/dist/cli-prototype-command.js +29 -0
- package/dist/cli-prototype-command.js.map +1 -0
- package/dist/cli-review-command.d.ts.map +1 -1
- package/dist/cli-review-command.js +20 -1
- package/dist/cli-review-command.js.map +1 -1
- package/dist/cli-run.d.ts.map +1 -1
- package/dist/cli-run.js +8 -0
- package/dist/cli-run.js.map +1 -1
- package/dist/cli-usage.d.ts.map +1 -1
- package/dist/cli-usage.js +6 -1
- package/dist/cli-usage.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/legacy-skill-catalog.d.ts.map +1 -1
- package/dist/legacy-skill-catalog.js +8 -0
- package/dist/legacy-skill-catalog.js.map +1 -1
- package/dist/prototype-gap-html.d.ts +10 -0
- package/dist/prototype-gap-html.d.ts.map +1 -0
- package/dist/prototype-gap-html.js +55 -0
- package/dist/prototype-gap-html.js.map +1 -0
- package/dist/prototype-gap-source.d.ts +14 -0
- package/dist/prototype-gap-source.d.ts.map +1 -0
- package/dist/prototype-gap-source.js +87 -0
- package/dist/prototype-gap-source.js.map +1 -0
- package/dist/prototype-gates.d.ts +24 -0
- package/dist/prototype-gates.d.ts.map +1 -0
- package/dist/prototype-gates.js +76 -0
- package/dist/prototype-gates.js.map +1 -0
- package/dist/prototype-label-client.d.ts +51 -0
- package/dist/prototype-label-client.d.ts.map +1 -0
- package/dist/prototype-label-client.js +53 -0
- package/dist/prototype-label-client.js.map +1 -0
- package/dist/prototype-label-score.d.ts +26 -0
- package/dist/prototype-label-score.d.ts.map +1 -0
- package/dist/prototype-label-score.js +51 -0
- package/dist/prototype-label-score.js.map +1 -0
- package/dist/prototype-process.d.ts +13 -0
- package/dist/prototype-process.d.ts.map +1 -0
- package/dist/prototype-process.js +26 -0
- package/dist/prototype-process.js.map +1 -0
- package/dist/prototype-requests.d.ts +29 -0
- package/dist/prototype-requests.d.ts.map +1 -0
- package/dist/prototype-requests.js +34 -0
- package/dist/prototype-requests.js.map +1 -0
- package/dist/prototype-runner.d.ts +22 -0
- package/dist/prototype-runner.d.ts.map +1 -0
- package/dist/prototype-runner.js +159 -0
- package/dist/prototype-runner.js.map +1 -0
- package/dist/prototype-source.d.ts +9 -0
- package/dist/prototype-source.d.ts.map +1 -0
- package/dist/prototype-source.js +48 -0
- package/dist/prototype-source.js.map +1 -0
- package/dist/review-discovery.d.ts +1 -0
- package/dist/review-discovery.d.ts.map +1 -1
- package/dist/review-discovery.js +49 -3
- package/dist/review-discovery.js.map +1 -1
- package/dist/review-page-assets.d.ts +2 -2
- package/dist/review-page-assets.d.ts.map +1 -1
- package/dist/review-page-assets.js +20 -1
- package/dist/review-page-assets.js.map +1 -1
- package/dist/review-page.d.ts.map +1 -1
- package/dist/review-page.js +38 -0
- package/dist/review-page.js.map +1 -1
- package/dist/review-prototype-gap.d.ts +48 -0
- package/dist/review-prototype-gap.d.ts.map +1 -0
- package/dist/review-prototype-gap.js +192 -0
- package/dist/review-prototype-gap.js.map +1 -0
- package/dist/review-server.d.ts +7 -0
- package/dist/review-server.d.ts.map +1 -1
- package/dist/review-server.js +64 -0
- package/dist/review-server.js.map +1 -1
- package/dist/review-types.d.ts +12 -0
- package/dist/review-types.d.ts.map +1 -1
- package/dist/run-journal-commands.d.ts.map +1 -1
- package/dist/run-journal-commands.js +13 -0
- package/dist/run-journal-commands.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +7 -1
- package/prototype-kit/analyze.mjs +275 -0
- package/prototype-kit/build.mjs +393 -0
- package/prototype-kit/inventory.mjs +104 -0
- package/prototype-kit/label-briefs.mjs +47 -0
- package/prototype-kit/lib/actions.mjs +15 -0
- package/prototype-kit/lib/navigation.mjs +33 -0
- package/prototype-kit/lib/runtime-check.mjs +126 -0
- package/prototype-kit/lib/shells.mjs +68 -0
- package/prototype-kit/lib/static-server.mjs +40 -0
- package/prototype-kit/lib/text.mjs +20 -0
- package/prototype-kit/runtime/proto.css +46 -0
- package/prototype-kit/runtime/proto.js +173 -0
- package/prototype-kit/score-labels.mjs +42 -0
- package/prototype-kit/tests/README.md +28 -0
- package/prototype-kit/tests/adversarial-runtime.mjs +90 -0
- package/prototype-kit/tests/discover-runs.mjs +29 -0
- package/prototype-kit/tests/fixtures/no-semantic/branch.plan.json +7 -0
- package/prototype-kit/tests/fixtures/no-semantic/pages/legacy.html +1 -0
- package/prototype-kit/tests/fixtures/partial-plan/branch.plan.json +5 -0
- package/prototype-kit/tests/fixtures/partial-plan/pages/orphan.html +4 -0
- package/prototype-kit/tests/fixtures/runtime/double.html +5 -0
- package/prototype-kit/tests/fixtures/runtime/history-destination.html +1 -0
- package/prototype-kit/tests/fixtures/runtime/history.html +6 -0
- package/prototype-kit/tests/fixtures/runtime/hover-stack.html +15 -0
- package/prototype-kit/tests/fixtures/runtime/invalid-dropdown.html +6 -0
- package/prototype-kit/tests/fixtures/runtime/nested.html +7 -0
- package/prototype-kit/tests/fixtures/three-shell/branch.plan.json +30 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p0-home.html +11 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p1-reports.html +11 -0
- package/prototype-kit/tests/lib/run-discovery.mjs +87 -0
- package/prototype-kit/tests/lib/run-process.mjs +49 -0
- package/prototype-kit/tests/offline-check.mjs +41 -0
- package/prototype-kit/tests/population-sweep.mjs +94 -0
- package/prototype-kit/tests/rules-check.mjs +50 -0
- package/prototype-kit/verify-runtime.mjs +36 -0
- package/skills/12ui-design/SKILL.md +8 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { discoverRuns } from './lib/run-discovery.mjs';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const outIndex = args.indexOf('--out');
|
|
8
|
+
const outFile = outIndex >= 0 ? args[outIndex + 1] : null;
|
|
9
|
+
const roots = args.filter((arg, index) => index !== outIndex && index !== outIndex + 1);
|
|
10
|
+
if (!roots.length) throw new Error('usage: discover-runs.mjs <root>... [--out manifest.json]');
|
|
11
|
+
|
|
12
|
+
const runs = discoverRuns(roots);
|
|
13
|
+
const result = {
|
|
14
|
+
generatedAt: new Date().toISOString(),
|
|
15
|
+
roots: roots.map((root) => path.resolve(root)),
|
|
16
|
+
uniqueRuns: runs.length,
|
|
17
|
+
aliasRuns: runs.reduce((sum, run) => sum + run.aliasCount, 0),
|
|
18
|
+
recoverableRuns: runs.filter((run) => run.recoverable).length,
|
|
19
|
+
planOnlyRuns: runs.filter((run) => !run.recoverable).length,
|
|
20
|
+
runs,
|
|
21
|
+
};
|
|
22
|
+
const json = JSON.stringify(result, null, 2) + '\n';
|
|
23
|
+
if (outFile) {
|
|
24
|
+
fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true });
|
|
25
|
+
fs.writeFileSync(outFile, json);
|
|
26
|
+
process.stdout.write(`unique=${result.uniqueRuns} aliases=${result.aliasRuns} recoverable=${result.recoverableRuns} plan-only=${result.planOnlyRuns}\n`);
|
|
27
|
+
} else {
|
|
28
|
+
process.stdout.write(json);
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "page",
|
|
4
|
+
"concept": "Legacy export without semantic ids",
|
|
5
|
+
"pages": [{ "index": 0, "id": "legacy", "title": "Legacy", "surfaceType": "web_page", "startsFromWinner": true, "slots": ["a"] }],
|
|
6
|
+
"screens": [{ "slot": "a", "pageIndex": 0, "content": "Legacy content" }]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><style>section{padding:40px}</style></head><body><section><h1>Legacy export</h1><p>No semantic viewport wrapper or element ids.</p></section></body></html>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow,#viewport-1-a{margin:0;width:1536px;height:1024px;font-family:Arial,sans-serif}.text{position:absolute}#viewport-1-a-card{position:absolute;left:100px;top:140px;width:600px;height:180px;background:#fff;border:1px solid #ddd}
|
|
4
|
+
</style></head><body><main id="page-flow"><section id="viewport-1-a"><div id="viewport-1-a-card"><h1 id="viewport-1-a-card-title">Recovered screen</h1><button id="viewport-1-a-card-button"><span id="viewport-1-a-card-button-text" class="text">Continue</span></button></div></section></main></body></html>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"></head><body>
|
|
2
|
+
<button id="open" data-proto-action="modal" data-proto-modal='{"title":"One modal","body":"Bound once.","confirm":"OK"}'>Open</button>
|
|
3
|
+
<script id="proto-state" type="application/json">{"unit":"double","tokens":{},"contentFile":null}</script>
|
|
4
|
+
<script src="../../../runtime/proto.js"></script><script src="../../../runtime/proto.js"></script>
|
|
5
|
+
</body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"></head><body><h1>Destination</h1></body></html>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0}section{height:900px;padding:20px}</style></head><body>
|
|
2
|
+
<section><button id="jump" data-proto-action="navigate" data-proto-target="history.html#target">Jump</button><button id="next" data-proto-action="navigate" data-proto-target="history-destination.html">Next page</button></section>
|
|
3
|
+
<section id="target"><h1>Target</h1></section>
|
|
4
|
+
<script id="proto-state" type="application/json">{"unit":"history","tokens":{},"contentFile":null}</script>
|
|
5
|
+
<script src="../../../runtime/proto.js"></script>
|
|
6
|
+
</body></html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><link rel="stylesheet" href="../../../runtime/proto.css"><style>
|
|
2
|
+
#stage{position:relative;width:400px;height:180px}
|
|
3
|
+
#stacked{position:absolute;left:20px;top:20px;width:240px;height:60px}
|
|
4
|
+
#stacked-button{position:absolute;inset:0;z-index:10}
|
|
5
|
+
#cover{position:absolute;left:20px;top:20px;width:240px;height:60px;z-index:1;background:transparent}
|
|
6
|
+
#plain{position:absolute;left:20px;top:110px;width:240px;height:44px}
|
|
7
|
+
</style></head><body>
|
|
8
|
+
<section id="stage">
|
|
9
|
+
<div id="stacked" data-proto-action="modal" data-proto-modal='{"title":"Stacked action","body":"Hover must not reorder it.","confirm":"OK"}'><button id="stacked-button">Stacked action</button></div>
|
|
10
|
+
<div id="cover"></div>
|
|
11
|
+
<button id="plain" data-proto-action="modal" data-proto-modal='{"title":"Plain action","body":"Normal actions stay live.","confirm":"OK"}'>Plain action</button>
|
|
12
|
+
</section>
|
|
13
|
+
<script id="proto-state" type="application/json">{"unit":"hover-stack","tokens":{},"contentFile":null}</script>
|
|
14
|
+
<script src="../../../runtime/proto.js"></script>
|
|
15
|
+
</body></html>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"></head><body>
|
|
2
|
+
<button id="invalid" data-proto-action="dropdown" data-proto-options='["Only choice"]'>Invalid dropdown</button>
|
|
3
|
+
<button id="valid" data-proto-action="dropdown" data-proto-options='["All","Open","Closed"]'>Valid dropdown</button>
|
|
4
|
+
<script id="proto-state" type="application/json">{"unit":"invalid-dropdown","tokens":{},"contentFile":null}</script>
|
|
5
|
+
<script src="../../../runtime/proto.js"></script>
|
|
6
|
+
</body></html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"></head><body>
|
|
2
|
+
<div id="parent" data-proto-action="dropdown" data-proto-options='["Parent","Other","Third"]'>
|
|
3
|
+
<button id="child" data-proto-action="modal" data-proto-modal='{"title":"Child modal","body":"Only the inner action fired.","confirm":"OK"}'>Open child</button>
|
|
4
|
+
</div>
|
|
5
|
+
<script id="proto-state" type="application/json">{"unit":"nested","tokens":{},"contentFile":null}</script>
|
|
6
|
+
<script src="../../../runtime/proto.js"></script>
|
|
7
|
+
</body></html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "site",
|
|
4
|
+
"concept": "Three-region application shell fixture",
|
|
5
|
+
"sitePurpose": "Exercise a sidebar, header, and footer together.",
|
|
6
|
+
"pages": [
|
|
7
|
+
{
|
|
8
|
+
"index": 0,
|
|
9
|
+
"id": "p0-home",
|
|
10
|
+
"title": "Home",
|
|
11
|
+
"surfaceType": "web_app",
|
|
12
|
+
"purpose": "Show the home state.",
|
|
13
|
+
"startsFromWinner": true,
|
|
14
|
+
"slots": ["a"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"index": 1,
|
|
18
|
+
"id": "p1-reports",
|
|
19
|
+
"title": "Reports",
|
|
20
|
+
"surfaceType": "web_app",
|
|
21
|
+
"purpose": "Show reports.",
|
|
22
|
+
"startsFromWinner": false,
|
|
23
|
+
"slots": ["b"]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"screens": [
|
|
27
|
+
{ "slot": "a", "pageIndex": 0, "content": "Home content", "dependsOnSlot": null },
|
|
28
|
+
{ "slot": "b", "pageIndex": 1, "content": "Reports content", "dependsOnSlot": null }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow{margin:0;width:1536px;height:1024px;font-family:Arial,sans-serif}#viewport-1-a{position:relative;width:1536px;height:1024px;background:#f4f4ef}
|
|
4
|
+
#viewport-1-a-sidebar{position:absolute;left:0;top:0;width:220px;height:1024px;background:#202820}#viewport-1-a-header{position:absolute;left:220px;top:0;width:1316px;height:70px;background:#fff}#viewport-1-a-footer{position:absolute;left:220px;top:924px;width:1316px;height:100px;background:#eee}#viewport-1-a-main{position:absolute;left:270px;top:120px;width:1150px;height:700px}
|
|
5
|
+
.text{position:absolute;font-size:16px}.side{left:30px;color:#fff}.head{top:24px}.foot{top:35px}.card{top:90px;width:600px;height:80px;background:#fff;padding:20px}
|
|
6
|
+
</style></head><body><main id="page-flow"><section id="viewport-1-a">
|
|
7
|
+
<aside id="viewport-1-a-sidebar"><p id="viewport-1-a-sidebar-home" class="text side" style="top:120px">Home</p><p id="viewport-1-a-sidebar-reports" class="text side" style="top:170px">Reports</p><p id="viewport-1-a-sidebar-settings" class="text side" style="top:220px">Settings</p></aside>
|
|
8
|
+
<header id="viewport-1-a-header"><p id="viewport-1-a-header-brand" class="text head" style="left:30px">Atlas</p><p id="viewport-1-a-header-search" class="text head" style="left:600px">Search</p><p id="viewport-1-a-header-account" class="text head" style="left:1160px">Account</p></header>
|
|
9
|
+
<footer id="viewport-1-a-footer"><p id="viewport-1-a-footer-status" class="text foot" style="left:30px">System ready</p><p id="viewport-1-a-footer-help" class="text foot" style="left:600px">Help</p><p id="viewport-1-a-footer-privacy" class="text foot" style="left:1160px">Privacy</p></footer>
|
|
10
|
+
<div id="viewport-1-a-main"><h1 id="viewport-1-a-main-title">Home</h1><button id="viewport-1-a-main-button" class="card"><span id="viewport-1-a-main-button-text" class="text" style="left:20px;top:20px">Open report</span></button></div>
|
|
11
|
+
</section></main></body></html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html><head><meta charset="utf-8"><style>
|
|
3
|
+
html,body,#page-flow{margin:0;width:1536px;height:1024px;font-family:Arial,sans-serif}#viewport-1-b{position:relative;width:1536px;height:1024px;background:#f4f4ef}
|
|
4
|
+
#viewport-1-b-sidebar{position:absolute;left:0;top:0;width:220px;height:1024px;background:#273027}#viewport-1-b-header{position:absolute;left:220px;top:0;width:1316px;height:70px;background:#fafafa}#viewport-1-b-footer{position:absolute;left:220px;top:924px;width:1316px;height:100px;background:#e8e8e2}#viewport-1-b-main{position:absolute;left:270px;top:120px;width:1150px;height:700px}
|
|
5
|
+
.text{position:absolute;font-size:16px}.side{left:30px;color:#fff}.head{top:24px}.foot{top:35px}.card{top:90px;width:600px;height:80px;background:#fff;padding:20px}
|
|
6
|
+
</style></head><body><main id="page-flow"><section id="viewport-1-b">
|
|
7
|
+
<aside id="viewport-1-b-sidebar"><p id="viewport-1-b-sidebar-home" class="text side" style="top:120px">Home</p><p id="viewport-1-b-sidebar-reports" class="text side" style="top:170px">Reports</p><p id="viewport-1-b-sidebar-settings" class="text side" style="top:220px">Settings</p></aside>
|
|
8
|
+
<header id="viewport-1-b-header"><p id="viewport-1-b-header-brand" class="text head" style="left:30px">Atlas</p><p id="viewport-1-b-header-search" class="text head" style="left:600px">Search</p><p id="viewport-1-b-header-account" class="text head" style="left:1160px">Account</p></header>
|
|
9
|
+
<footer id="viewport-1-b-footer"><p id="viewport-1-b-footer-status" class="text foot" style="left:30px">System ready</p><p id="viewport-1-b-footer-help" class="text foot" style="left:600px">Help</p><p id="viewport-1-b-footer-privacy" class="text foot" style="left:1160px">Privacy</p></footer>
|
|
10
|
+
<div id="viewport-1-b-main"><h1 id="viewport-1-b-main-title">Reports</h1><button id="viewport-1-b-main-button" class="card"><span id="viewport-1-b-main-button-text" class="text" style="left:20px;top:20px">Export report</span></button></div>
|
|
11
|
+
</section></main></body></html>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
6
|
+
|
|
7
|
+
function sha(value) {
|
|
8
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function walk(root, found) {
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
15
|
+
} catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) walk(path.join(root, entry.name), found);
|
|
20
|
+
else if (entry.isFile() && entry.name === 'branch.plan.json') found.push(path.join(root, entry.name));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shapePlan(plan) {
|
|
25
|
+
return {
|
|
26
|
+
scope: plan?.scope ?? null,
|
|
27
|
+
concept: plan?.concept ?? null,
|
|
28
|
+
sitePurpose: plan?.sitePurpose ?? null,
|
|
29
|
+
pages: Array.isArray(plan?.pages) ? plan.pages : null,
|
|
30
|
+
screens: Array.isArray(plan?.screens) ? plan.screens : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inspectPlan(planFile) {
|
|
35
|
+
const runDir = path.dirname(planFile);
|
|
36
|
+
const pagesDir = path.join(runDir, 'pages');
|
|
37
|
+
let plan = null;
|
|
38
|
+
let planError = null;
|
|
39
|
+
try {
|
|
40
|
+
plan = JSON.parse(fs.readFileSync(planFile, 'utf8'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
planError = error.message;
|
|
43
|
+
}
|
|
44
|
+
const htmlFiles = fs.existsSync(pagesDir)
|
|
45
|
+
? fs.readdirSync(pagesDir).filter((name) => name.endsWith('.html')).sort()
|
|
46
|
+
: [];
|
|
47
|
+
const htmlHashes = htmlFiles.map((name) => [name, sha(fs.readFileSync(path.join(pagesDir, name)))]);
|
|
48
|
+
const pageIds = Array.isArray(plan?.pages) ? plan.pages.map((page) => page?.id).filter(Boolean) : [];
|
|
49
|
+
const missingPlannedPages = pageIds.filter((pageId) => !htmlFiles.includes(`${pageId}.html`));
|
|
50
|
+
const unplannedHtml = htmlFiles.filter((name) => !pageIds.includes(name.replace(/\.html$/, '')));
|
|
51
|
+
const shape = {
|
|
52
|
+
plan: shapePlan(plan),
|
|
53
|
+
html: htmlHashes,
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
runDir,
|
|
57
|
+
planFile,
|
|
58
|
+
pagesDir,
|
|
59
|
+
scope: plan?.scope ?? null,
|
|
60
|
+
planPages: Array.isArray(plan?.pages) ? plan.pages.length : null,
|
|
61
|
+
planScreens: Array.isArray(plan?.screens) ? plan.screens.length : null,
|
|
62
|
+
htmlPages: htmlFiles.length,
|
|
63
|
+
htmlFiles,
|
|
64
|
+
missingPlannedPages,
|
|
65
|
+
unplannedHtml,
|
|
66
|
+
planError,
|
|
67
|
+
recoverable: !planError && htmlFiles.length > 0,
|
|
68
|
+
fingerprint: sha(JSON.stringify(shape)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function discoverRuns(roots) {
|
|
73
|
+
const planFiles = [];
|
|
74
|
+
for (const root of roots) walk(path.resolve(root), planFiles);
|
|
75
|
+
const inspected = [...new Set(planFiles)].sort().map(inspectPlan);
|
|
76
|
+
const groups = new Map();
|
|
77
|
+
for (const run of inspected) {
|
|
78
|
+
const list = groups.get(run.fingerprint) || [];
|
|
79
|
+
list.push(run);
|
|
80
|
+
groups.set(run.fingerprint, list);
|
|
81
|
+
}
|
|
82
|
+
return [...groups.values()].map((aliases) => ({
|
|
83
|
+
...aliases[0],
|
|
84
|
+
aliases: aliases.map((run) => run.runDir),
|
|
85
|
+
aliasCount: aliases.length,
|
|
86
|
+
})).sort((a, b) => a.runDir.localeCompare(b.runDir));
|
|
87
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
function runNode(script, args, logFile) {
|
|
6
|
+
const started = Date.now();
|
|
7
|
+
const result = spawnSync(process.execPath, [script, ...args], {
|
|
8
|
+
cwd: path.dirname(script),
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
11
|
+
timeout: 180_000,
|
|
12
|
+
});
|
|
13
|
+
const log = [
|
|
14
|
+
`$ node ${path.basename(script)} ${args.join(' ')}`,
|
|
15
|
+
result.stdout || '',
|
|
16
|
+
result.stderr || '',
|
|
17
|
+
result.error ? `spawn error: ${result.error.message}` : '',
|
|
18
|
+
].filter(Boolean).join('\n');
|
|
19
|
+
fs.writeFileSync(logFile, log);
|
|
20
|
+
return {
|
|
21
|
+
ok: result.status === 0 && !result.error,
|
|
22
|
+
status: result.status,
|
|
23
|
+
signal: result.signal,
|
|
24
|
+
durationMs: Date.now() - started,
|
|
25
|
+
error: result.error?.message || (result.status === 0 ? null : (result.stderr || result.stdout || `exit ${result.status}`).trim().split('\n').slice(-20).join('\n')),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildPrototype({ kitDir, run, runWorkDir, labelsDir = '-' }) {
|
|
30
|
+
runWorkDir = path.resolve(runWorkDir);
|
|
31
|
+
fs.rmSync(runWorkDir, { recursive: true, force: true });
|
|
32
|
+
fs.mkdirSync(runWorkDir, { recursive: true });
|
|
33
|
+
const inventoryFile = path.join(runWorkDir, 'inventory.json');
|
|
34
|
+
const modelFile = path.join(runWorkDir, 'model.json');
|
|
35
|
+
const protoDir = path.join(runWorkDir, 'proto');
|
|
36
|
+
fs.mkdirSync(protoDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const stages = {};
|
|
39
|
+
stages.inventory = runNode(path.join(kitDir, 'inventory.mjs'), [run.runDir, inventoryFile], path.join(runWorkDir, 'inventory.log'));
|
|
40
|
+
if (!stages.inventory.ok) return { ok: false, failedStage: 'inventory', stages, inventoryFile, modelFile, protoDir };
|
|
41
|
+
|
|
42
|
+
stages.analyze = runNode(path.join(kitDir, 'analyze.mjs'), [inventoryFile, modelFile], path.join(runWorkDir, 'analyze.log'));
|
|
43
|
+
if (!stages.analyze.ok) return { ok: false, failedStage: 'analyze', stages, inventoryFile, modelFile, protoDir };
|
|
44
|
+
|
|
45
|
+
stages.build = runNode(path.join(kitDir, 'build.mjs'), [run.runDir, inventoryFile, modelFile, labelsDir, protoDir], path.join(runWorkDir, 'build.log'));
|
|
46
|
+
if (!stages.build.ok) return { ok: false, failedStage: 'build', stages, inventoryFile, modelFile, protoDir };
|
|
47
|
+
|
|
48
|
+
return { ok: true, failedStage: null, stages, inventoryFile, modelFile, protoDir };
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { buildPrototype } from './lib/run-process.mjs';
|
|
7
|
+
|
|
8
|
+
const testsDir = path.dirname(new URL(import.meta.url).pathname);
|
|
9
|
+
const kitDir = path.resolve(testsDir, '..');
|
|
10
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'proto-kit-offline-'));
|
|
11
|
+
|
|
12
|
+
function buildFixture(name) {
|
|
13
|
+
const runDir = path.join(testsDir, 'fixtures', name);
|
|
14
|
+
return buildPrototype({ kitDir, run: { runDir }, runWorkDir: path.join(scratch, name) });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const partial = buildFixture('partial-plan');
|
|
18
|
+
assert.equal(partial.ok, true, 'must fire: a valid partial plan recovers from semantic HTML');
|
|
19
|
+
const partialManifest = JSON.parse(fs.readFileSync(path.join(partial.protoDir, 'proto-manifest.json'), 'utf8'));
|
|
20
|
+
assert.equal(partialManifest.units.length, 1);
|
|
21
|
+
|
|
22
|
+
const threeShell = buildFixture('three-shell');
|
|
23
|
+
assert.equal(threeShell.ok, true, 'must stay silent: a three-region shell still builds');
|
|
24
|
+
const threeShellModel = JSON.parse(fs.readFileSync(threeShell.modelFile, 'utf8'));
|
|
25
|
+
assert.deepEqual(Object.keys(threeShellModel.canonicalShell).sort(), ['footer', 'header', 'sidebar']);
|
|
26
|
+
|
|
27
|
+
const legacy = buildFixture('no-semantic');
|
|
28
|
+
assert.equal(legacy.ok, false, 'must fire: a legacy export without semantic ids is gated');
|
|
29
|
+
assert.equal(legacy.failedStage, 'analyze');
|
|
30
|
+
assert.match(legacy.stages.analyze.error, /no semantic viewport sections found/);
|
|
31
|
+
|
|
32
|
+
const missingPlanDir = path.join(scratch, 'missing-plan-input');
|
|
33
|
+
fs.mkdirSync(path.join(missingPlanDir, 'pages'), { recursive: true });
|
|
34
|
+
fs.copyFileSync(path.join(testsDir, 'fixtures', 'partial-plan', 'pages', 'orphan.html'), path.join(missingPlanDir, 'pages', 'orphan.html'));
|
|
35
|
+
const missingPlan = buildPrototype({ kitDir, run: { runDir: missingPlanDir }, runWorkDir: path.join(scratch, 'missing-plan-work') });
|
|
36
|
+
assert.equal(missingPlan.ok, false, 'must fire: missing plan_json is gated');
|
|
37
|
+
assert.equal(missingPlan.failedStage, 'inventory');
|
|
38
|
+
assert.match(missingPlan.stages.inventory.error, /Prototype input error: missing .*branch\.plan\.json/);
|
|
39
|
+
|
|
40
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
41
|
+
process.stdout.write('offline pipeline: ok\n');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { chromium } from '@playwright/test';
|
|
5
|
+
import { buildPrototype } from './lib/run-process.mjs';
|
|
6
|
+
import { checkRuntime } from '../lib/runtime-check.mjs';
|
|
7
|
+
import { startStaticServer } from '../lib/static-server.mjs';
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const value = (flag) => argv[argv.indexOf(flag) + 1];
|
|
11
|
+
return {
|
|
12
|
+
manifest: value('--manifest'),
|
|
13
|
+
workDir: value('--work-dir'),
|
|
14
|
+
results: value('--results'),
|
|
15
|
+
match: argv.includes('--match') ? new RegExp(value('--match')) : null,
|
|
16
|
+
limit: argv.includes('--limit') ? Number(value('--limit')) : Infinity,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function maxShellDelta(rows) {
|
|
21
|
+
return rows.reduce((max, row) => Math.max(max, Math.abs(row.own[0] - row.canonical[0]), Math.abs(row.own[1] - row.canonical[1])), 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const args = parseArgs(process.argv.slice(2));
|
|
25
|
+
if (!args.manifest || !args.workDir || !args.results) {
|
|
26
|
+
throw new Error('usage: population-sweep.mjs --manifest runs.json --work-dir dir --results results.jsonl [--match regex] [--limit n]');
|
|
27
|
+
}
|
|
28
|
+
const kitDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
29
|
+
const manifest = JSON.parse(fs.readFileSync(args.manifest, 'utf8'));
|
|
30
|
+
const selected = manifest.runs.filter((run) => !args.match || args.match.test(run.runDir)).slice(0, args.limit);
|
|
31
|
+
fs.mkdirSync(args.workDir, { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.dirname(path.resolve(args.results)), { recursive: true });
|
|
33
|
+
const completed = new Map();
|
|
34
|
+
if (fs.existsSync(args.results)) {
|
|
35
|
+
for (const line of fs.readFileSync(args.results, 'utf8').split('\n').filter(Boolean)) {
|
|
36
|
+
const row = JSON.parse(line);
|
|
37
|
+
completed.set(row.fingerprint, row);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const server = await startStaticServer(args.workDir);
|
|
41
|
+
const browser = await chromium.launch();
|
|
42
|
+
let ordinal = 0;
|
|
43
|
+
for (const run of selected) {
|
|
44
|
+
ordinal++;
|
|
45
|
+
if (completed.has(run.fingerprint)) {
|
|
46
|
+
process.stdout.write(`[${ordinal}/${selected.length}] skip ${run.fingerprint.slice(0, 10)} ${run.runDir}\n`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const runId = run.fingerprint.slice(0, 16);
|
|
50
|
+
const runWorkDir = path.join(args.workDir, runId);
|
|
51
|
+
let row;
|
|
52
|
+
if (!run.recoverable) {
|
|
53
|
+
row = { ...run, runId, status: 'failed', failedStage: 'input', error: run.planError || 'no exported HTML pages' };
|
|
54
|
+
} else {
|
|
55
|
+
process.stdout.write(`[${ordinal}/${selected.length}] build ${runId} ${run.runDir}\n`);
|
|
56
|
+
const built = buildPrototype({ kitDir, run, runWorkDir });
|
|
57
|
+
if (!built.ok) {
|
|
58
|
+
row = { ...run, runId, status: 'failed', failedStage: built.failedStage, error: built.stages[built.failedStage].error, stages: built.stages };
|
|
59
|
+
} else {
|
|
60
|
+
const model = JSON.parse(fs.readFileSync(built.modelFile, 'utf8'));
|
|
61
|
+
const protoManifest = JSON.parse(fs.readFileSync(path.join(built.protoDir, 'proto-manifest.json'), 'utf8'));
|
|
62
|
+
const relativeProtoPath = `${runId}/proto`;
|
|
63
|
+
const runtime = await checkRuntime({ browser, baseUrl: server.baseUrl, relativeProtoPath, protoDir: built.protoDir });
|
|
64
|
+
const realPages = protoManifest.units.length;
|
|
65
|
+
const holdingPages = protoManifest.holdings.length;
|
|
66
|
+
row = {
|
|
67
|
+
...run,
|
|
68
|
+
runId,
|
|
69
|
+
status: 'built',
|
|
70
|
+
failedStage: null,
|
|
71
|
+
stages: built.stages,
|
|
72
|
+
states: model.states.length,
|
|
73
|
+
realPages,
|
|
74
|
+
holdingPages,
|
|
75
|
+
holdingRatio: holdingPages / Math.max(1, realPages + holdingPages),
|
|
76
|
+
actionsBound: protoManifest.units.reduce((sum, unit) => sum + unit.actions + unit.fields, 0),
|
|
77
|
+
actionCounts: protoManifest.counts,
|
|
78
|
+
unboundActions: protoManifest.unbound.length,
|
|
79
|
+
shellRegions: Object.keys(protoManifest.canonicalShell),
|
|
80
|
+
shellResiduals: protoManifest.shellSizeDelta.length,
|
|
81
|
+
maxShellDeltaPx: maxShellDelta(protoManifest.shellSizeDelta),
|
|
82
|
+
shellAmbiguities: protoManifest.shellAmbiguities.length,
|
|
83
|
+
replacedShellActions: protoManifest.replacedShellActions,
|
|
84
|
+
shellActionRemaps: protoManifest.shellActionRemaps,
|
|
85
|
+
runtime,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
fs.appendFileSync(args.results, JSON.stringify(row) + '\n');
|
|
90
|
+
completed.set(run.fingerprint, row);
|
|
91
|
+
process.stdout.write(` ${row.status}${row.failedStage ? ` @ ${row.failedStage}` : ` real=${row.realPages} holding=${row.holdingPages} actions=${row.actionsBound} errors=${row.runtime.pageErrors}`}\n`);
|
|
92
|
+
}
|
|
93
|
+
await browser.close();
|
|
94
|
+
await server.close();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { usableDropdownOptions } from '../lib/actions.mjs';
|
|
4
|
+
import { matchPlannedPage } from '../lib/navigation.mjs';
|
|
5
|
+
import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion, shouldSuppressDroppedShell, shouldTransplantShell } from '../lib/shells.mjs';
|
|
6
|
+
import { slugText } from '../lib/text.mjs';
|
|
7
|
+
|
|
8
|
+
const pages = [
|
|
9
|
+
{ stateId: 'incident-overview', pageId: 'p0-incident-overview', title: 'INC-4821 Incident Overview' },
|
|
10
|
+
{ stateId: 'incidents', pageId: 'p1-incidents', title: 'Incidents' },
|
|
11
|
+
{ stateId: 'service-detail', pageId: 'p2-checkout-api-service-detail', title: 'checkout-api Service Detail' },
|
|
12
|
+
];
|
|
13
|
+
assert.equal(matchPlannedPage('Incidents', pages)?.stateId, 'incidents', 'must fire: exact title beats an earlier partial title');
|
|
14
|
+
assert.equal(matchPlannedPage('Services', pages)?.stateId, 'service-detail', 'must stay silent: a unique useful partial match remains');
|
|
15
|
+
assert.equal(matchPlannedPage('Reports', [
|
|
16
|
+
{ stateId: 'team', title: 'Team reports' },
|
|
17
|
+
{ stateId: 'usage', title: 'Usage reports' },
|
|
18
|
+
]), null, 'must fire: an ambiguous partial title does not silently choose the first page');
|
|
19
|
+
assert.equal(matchPlannedPage('التقارير', [{ stateId: 'reports-ar', title: 'التقارير' }])?.stateId, 'reports-ar', 'must fire: non-Latin exact navigation remains matchable');
|
|
20
|
+
assert.notEqual(slugText('التقارير'), slugText('الإعدادات'), 'must stay silent: distinct non-Latin holdings do not collide');
|
|
21
|
+
assert.equal(usableDropdownOptions(['Only choice']), null, 'must fire: a one-option fallback is not bound as a fake dropdown');
|
|
22
|
+
assert.deepEqual(usableDropdownOptions(['All', 'Open', 'Closed']), ['All', 'Open', 'Closed'], 'must stay silent: a real three-option dropdown remains bindable');
|
|
23
|
+
|
|
24
|
+
const headerNav = { containerId: 'nav', region: 'header', semantic: true, items: 8, share: 0.8, box: [0, 40, 1536, 76] };
|
|
25
|
+
const announcement = { containerId: 'announcement', region: 'header', semantic: false, items: 3, share: 1, box: [0, 0, 1536, 40] };
|
|
26
|
+
const footer = { containerId: 'footer', region: 'footer', semantic: true, items: 5, share: 0.8, box: [0, 900, 1536, 124] };
|
|
27
|
+
const duplicate = selectOneShellPerRegion([announcement, headerNav, footer]);
|
|
28
|
+
assert.deepEqual(duplicate.selected.map((shell) => shell.containerId).sort(), ['footer', 'nav'], 'must fire: one shell per region prevents double transplant');
|
|
29
|
+
assert.deepEqual(duplicate.dropped.map((shell) => shell.containerId), ['announcement']);
|
|
30
|
+
const distinct = selectOneShellPerRegion([
|
|
31
|
+
{ ...headerNav, containerId: 'header' },
|
|
32
|
+
{ ...footer },
|
|
33
|
+
{ containerId: 'sidebar', region: 'sidebar', semantic: true, items: 5, share: 1, box: [0, 0, 220, 1024] },
|
|
34
|
+
]);
|
|
35
|
+
assert.equal(distinct.selected.length, 3, 'must stay silent: distinct sidebar/header/footer regions are all retained');
|
|
36
|
+
assert.equal(distinct.dropped.length, 0);
|
|
37
|
+
assert.equal(shouldSuppressDroppedShell({ droppedBox: [175, 25, 397, 88], keptBox: [35, 31, 1461, 87] }), true, 'must fire: an overlapping duplicate shell is suppressed after transplant');
|
|
38
|
+
assert.equal(shouldSuppressDroppedShell({ droppedBox: [0, 0, 1536, 40], keptBox: [0, 40, 1536, 76] }), false, 'must stay silent: an adjacent announcement is preserved');
|
|
39
|
+
assert.equal(qualifiesShellCandidate({ region: 'header', semantic: true, share: 0.67, items: 9, box: [1162, 24, 341, 100] }, 1536), false, 'must fire: a narrow utility topbar is not stretched into a page shell');
|
|
40
|
+
assert.equal(qualifiesShellCandidate({ region: 'header', semantic: true, share: 0.75, items: 4, box: [15, 0, 902, 155] }, 1536), true, 'must stay silent: a wide edge header remains eligible');
|
|
41
|
+
const sourceShell = { share: 0.75, matches: 0, isSource: true, items: 4 };
|
|
42
|
+
const matchedShell = { share: 0.89, matches: 2, isSource: false, items: 9 };
|
|
43
|
+
assert.equal([sourceShell, matchedShell].sort(compareCanonicalShells)[0], matchedShell, 'must fire: a shell with real planned-page matches beats a source shell with none');
|
|
44
|
+
assert.equal([{ ...sourceShell, matches: 2 }, matchedShell].sort(compareCanonicalShells)[0].isSource, true, 'must stay silent: source preference breaks a navigation-match tie');
|
|
45
|
+
assert.equal(shouldTransplantShell([15, 0, 902, 155], [2, 0, 1424, 155]), false, 'must fire: a strongly mismatched shell is preserved instead of stretched');
|
|
46
|
+
assert.equal(shouldTransplantShell([0, 0, 1215, 70], [0, 0, 1342, 68]), true, 'must stay silent: a near-size shell still receives canonical children');
|
|
47
|
+
assert.equal(isWithinShell(['row', 'sidebar', 'viewport'], new Set(['sidebar'])), true, 'must fire: a nested shell row is not treated as page content');
|
|
48
|
+
assert.equal(isWithinShell(['row', 'main', 'viewport'], new Set(['sidebar'])), false, 'must stay silent: a content row remains interactive');
|
|
49
|
+
|
|
50
|
+
process.stdout.write('rules: ok\n');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { chromium } from '@playwright/test';
|
|
5
|
+
|
|
6
|
+
import { checkRuntime } from './lib/runtime-check.mjs';
|
|
7
|
+
import { startStaticServer } from './lib/static-server.mjs';
|
|
8
|
+
|
|
9
|
+
const [protoDirArg, auditFileArg] = process.argv.slice(2);
|
|
10
|
+
if (!protoDirArg || !auditFileArg) throw new Error('usage: verify-runtime.mjs <prototype-dir> <audit.json>');
|
|
11
|
+
const protoDir = path.resolve(protoDirArg);
|
|
12
|
+
const auditFile = path.resolve(auditFileArg);
|
|
13
|
+
const server = await startStaticServer(path.dirname(protoDir));
|
|
14
|
+
const browser = await chromium.launch();
|
|
15
|
+
let audit;
|
|
16
|
+
try {
|
|
17
|
+
audit = await checkRuntime({
|
|
18
|
+
browser,
|
|
19
|
+
baseUrl: server.baseUrl,
|
|
20
|
+
relativeProtoPath: path.basename(protoDir),
|
|
21
|
+
protoDir,
|
|
22
|
+
});
|
|
23
|
+
} finally {
|
|
24
|
+
await browser.close();
|
|
25
|
+
await server.close();
|
|
26
|
+
}
|
|
27
|
+
fs.writeFileSync(auditFile, JSON.stringify(audit, null, 2) + '\n');
|
|
28
|
+
const unsafe = [];
|
|
29
|
+
if (audit.pageErrors > 0) unsafe.push(`${audit.pageErrors} page/load error(s)`);
|
|
30
|
+
if (audit.consoleErrors > 0) unsafe.push(`${audit.consoleErrors} console error(s)`);
|
|
31
|
+
if (audit.duplicateIds > 0) unsafe.push(`${audit.duplicateIds} duplicate id(s)`);
|
|
32
|
+
if (audit.brokenTargets.length > 0) unsafe.push(`${audit.brokenTargets.length} broken navigation target(s)`);
|
|
33
|
+
if (audit.interactionFailures.length > 0) unsafe.push(`${audit.interactionFailures.length} page(s) with broken dropdown/modal interaction`);
|
|
34
|
+
if (audit.deepLinkBack.present && !audit.deepLinkBack.ok) unsafe.push('deep-link history did not return to its source');
|
|
35
|
+
if (unsafe.length > 0) throw new Error(`Prototype runtime gate failed: ${unsafe.join('; ')}`);
|
|
36
|
+
console.error(`runtime gate passed: ${audit.pages.length} pages, ${audit.brokenTargets.length} broken targets, ${audit.interactionFailures.length} interaction failures`);
|
|
@@ -29,7 +29,7 @@ Only if needed, read `inspire.md` for how to search the corpus directly.
|
|
|
29
29
|
|
|
30
30
|
Use whenever the deliverable is more than one viewport — a full page, a multi-route site, an app with several states — expand the approved design rather than re-deriving it per screen.
|
|
31
31
|
|
|
32
|
-
12ui branch execute --start <image.png> --scope page|site --convert html --concept "<what the rest of the surface must cover; 1200 chars max>"
|
|
32
|
+
12ui branch execute --start <image.png> --scope page|site --convert html --prototype --concept "<what the rest of the surface must cover; 1200 chars max>"
|
|
33
33
|
|
|
34
34
|
`--scope page` grows that screen into the rest of its page and `--scope site` adds sibling routes and app states. With `--convert html`, ordered `web_page` viewports stay in one continuous page; complete `web_app`, `mobile_app`, and `immersive` states become separate pages so recurring application shells are never stacked into one long document.
|
|
35
35
|
|
|
@@ -37,6 +37,12 @@ Use whenever the deliverable is more than one viewport — a full page, a multi-
|
|
|
37
37
|
|
|
38
38
|
Execute returns immediately; `next` reports progress and the next command, `--wait` blocks instead of polling, and `12ui branch resume <run-dir>` continues an interrupted run, replaying already-settled screens for free. Wait for it rather than building the same pages by hand in the meantime — they are already being produced and paid for. Screens land in `<run-dir>/branch/screens/`; HTML lands in `<run-dir>/branch/pages/`, one file per continuous web page or complete app state.
|
|
39
39
|
|
|
40
|
+
Use `--prototype` with `--convert html` when the deliverable should be clickable. After export it canonically splits application states, preserves shared shells, derives navigation from the branch plan, adds content-backed holding pages for unbuilt routes, and writes a verified `<run-dir>/branch/prototype/`. It labels ambiguous controls through one cheap product call per exported screen, but deterministic link and form facts take precedence. Missing HTML or plan, legacy viewport ids, runtime failures, zero-action real pages, dropdowns with fewer than three distinct options after labeling, and more than 50% holdings stop publication. Any holding page, skipped shell transplant, shell size residual, or untested script direction remains a warning and measured fact in `proto-manifest.json`.
|
|
41
|
+
|
|
42
|
+
For a completed branch that was exported without the flag, run:
|
|
43
|
+
|
|
44
|
+
12ui prototype <run-dir>
|
|
45
|
+
|
|
40
46
|
## 3. Convert
|
|
41
47
|
|
|
42
48
|
When you have a single image to convert (or for example only designing a single viewport), you can directly convert the image instead of branching it.
|
|
@@ -51,7 +57,7 @@ All but `html` are free, deterministic, and parallel; reusing a key replays fini
|
|
|
51
57
|
|
|
52
58
|
## 4. Integrate
|
|
53
59
|
|
|
54
|
-
Avoid rebuilding from scratch because you will lose pixel precision.
|
|
60
|
+
Avoid rebuilding from scratch because you will lose pixel precision. For a multi-screen HTML branch, use its generated prototype as the interaction and routing baseline before adding application-specific data and behavior. In `12ui review`, Prototype builds or rebuilds that artifact explicitly. Holding pages offer Fill this gap for a visible $0.51+ claim-and-convert operation; it never runs automatically. Open the integrated result, follow every transition, resize it, and compare it with the source; fix visible drift rather than accepting a merely functional approximation.
|
|
55
61
|
|
|
56
62
|
## 5. Search
|
|
57
63
|
|