@12ui/design 0.2.36 → 0.2.37
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/SKILL.md +1 -1
- package/dist/branch-client.d.ts +12 -0
- package/dist/branch-client.d.ts.map +1 -1
- package/dist/branch-client.js.map +1 -1
- package/dist/branch-completion.d.ts.map +1 -1
- package/dist/branch-completion.js +1 -0
- package/dist/branch-completion.js.map +1 -1
- package/dist/branch-execution-types.d.ts +3 -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 +13 -0
- package/dist/branch-execution.js.map +1 -1
- package/dist/branch-identity.d.ts +1 -1
- package/dist/branch-identity.js +1 -1
- package/dist/branch-plan.d.ts +3 -1
- package/dist/branch-plan.d.ts.map +1 -1
- package/dist/branch-plan.js +2 -0
- package/dist/branch-plan.js.map +1 -1
- package/dist/branch-run-record.d.ts +18 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/branch-winner.d.ts.map +1 -1
- package/dist/branch-winner.js +7 -0
- package/dist/branch-winner.js.map +1 -1
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +61 -7
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-help/design.d.ts +1 -1
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +1 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +1 -1
- package/dist/cli-prototype-command.d.ts +3 -2
- package/dist/cli-prototype-command.d.ts.map +1 -1
- package/dist/cli-prototype-command.js +9 -3
- package/dist/cli-prototype-command.js.map +1 -1
- package/dist/cli-review-command.js +2 -2
- package/dist/cli-review-command.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prototype-branch-contract.d.ts +67 -0
- package/dist/prototype-branch-contract.d.ts.map +1 -0
- package/dist/prototype-branch-contract.js +403 -0
- package/dist/prototype-branch-contract.js.map +1 -0
- package/dist/prototype-poststep.d.ts +27 -0
- package/dist/prototype-poststep.d.ts.map +1 -0
- package/dist/prototype-poststep.js +97 -0
- package/dist/prototype-poststep.js.map +1 -0
- package/dist/prototype-run-record.d.ts +92 -0
- package/dist/prototype-run-record.d.ts.map +1 -0
- package/dist/prototype-run-record.js +236 -0
- package/dist/prototype-run-record.js.map +1 -0
- package/dist/prototype-runner.d.ts +13 -1
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +79 -20
- package/dist/prototype-runner.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/prototype-kit/analyze.mjs +48 -6
- package/prototype-kit/build.mjs +249 -65
- package/prototype-kit/inventory.mjs +290 -2
- package/prototype-kit/lib/holding-composition.mjs +67 -0
- package/prototype-kit/lib/nav-active-state.mjs +67 -0
- package/prototype-kit/lib/review-framing.mjs +72 -0
- package/prototype-kit/lib/runtime-check.mjs +361 -28
- package/prototype-kit/lib/shell-consistency.mjs +68 -0
- package/prototype-kit/lib/shell-stitching.mjs +221 -0
- package/prototype-kit/runtime/proto.css +8 -3
- package/prototype-kit/tests/README.md +4 -1
- package/prototype-kit/tests/adversarial-runtime.mjs +85 -2
- package/prototype-kit/tests/fixtures/holding-runtime/PROVENANCE.md +22 -0
- package/prototype-kit/tests/fixtures/holding-runtime/band-ownership-fire.html +10 -0
- package/prototype-kit/tests/fixtures/holding-runtime/band-ownership-silent.html +10 -0
- package/prototype-kit/tests/fixtures/holding-runtime/declared-width-fire.html +11 -0
- package/prototype-kit/tests/fixtures/holding-runtime/declared-width-silent.html +11 -0
- package/prototype-kit/tests/fixtures/holding-runtime/ground-registration-mismatch-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/ground-registration-mismatch-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/holding-contrast-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/holding-contrast-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/positioned-ground-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/positioned-ground-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-geometry-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-geometry-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-intersection-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-intersection-silent.html +5 -0
- package/prototype-kit/tests/fixtures/runtime/history.html +1 -1
- package/prototype-kit/tests/fixtures/shell-nav-runtime/PROVENANCE.md +6 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/active-fire.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/proto-manifest.json +20 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/shell-fire.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/shell-silent.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/unlinked-silent.html +3 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p0-home.html +3 -3
- package/prototype-kit/tests/fixtures/three-shell/pages/p1-reports.html +3 -3
- package/prototype-kit/tests/lib/run-process.mjs +8 -2
- package/prototype-kit/tests/offline-check.mjs +143 -0
- package/prototype-kit/tests/population-sweep.mjs +26 -7
- package/prototype-kit/tests/rules-check.mjs +80 -0
- package/prototype-kit/verify-runtime.mjs +5 -1
|
@@ -4,8 +4,8 @@ html,body,#page-flow{margin:0;width:1536px;height:1024px;font-family:Arial,sans-
|
|
|
4
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
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
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
|
|
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>
|
|
8
|
+
<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></aside>
|
|
8
9
|
<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
|
-
<
|
|
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>
|
|
10
|
+
<div id="viewport-1-a-main"><h1 id="viewport-1-a-main-title" style="color:#eee">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
11
|
</section></main></body></html>
|
|
@@ -4,8 +4,8 @@ html,body,#page-flow{margin:0;width:1536px;height:1024px;font-family:Arial,sans-
|
|
|
4
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
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
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
|
|
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>
|
|
8
|
+
<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></aside>
|
|
8
9
|
<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
|
-
<
|
|
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>
|
|
10
|
+
<div id="viewport-1-b-main"><h1 id="viewport-1-b-main-title" style="color:#eee">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
11
|
</section></main></body></html>
|
|
@@ -26,12 +26,13 @@ function runNode(script, args, logFile) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function buildPrototype({ kitDir, run, runWorkDir, labelsDir = '-' }) {
|
|
29
|
+
export function buildPrototype({ kitDir, run, runWorkDir, labelsDir = '-', review = null }) {
|
|
30
30
|
runWorkDir = path.resolve(runWorkDir);
|
|
31
31
|
fs.rmSync(runWorkDir, { recursive: true, force: true });
|
|
32
32
|
fs.mkdirSync(runWorkDir, { recursive: true });
|
|
33
33
|
const inventoryFile = path.join(runWorkDir, 'inventory.json');
|
|
34
34
|
const modelFile = path.join(runWorkDir, 'model.json');
|
|
35
|
+
const reviewFile = path.join(runWorkDir, 'review.json');
|
|
35
36
|
const protoDir = path.join(runWorkDir, 'proto');
|
|
36
37
|
fs.mkdirSync(protoDir, { recursive: true });
|
|
37
38
|
|
|
@@ -39,7 +40,12 @@ export function buildPrototype({ kitDir, run, runWorkDir, labelsDir = '-' }) {
|
|
|
39
40
|
stages.inventory = runNode(path.join(kitDir, 'inventory.mjs'), [run.runDir, inventoryFile], path.join(runWorkDir, 'inventory.log'));
|
|
40
41
|
if (!stages.inventory.ok) return { ok: false, failedStage: 'inventory', stages, inventoryFile, modelFile, protoDir };
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
if (review) fs.writeFileSync(reviewFile, JSON.stringify(review, null, 2));
|
|
44
|
+
stages.analyze = runNode(
|
|
45
|
+
path.join(kitDir, 'analyze.mjs'),
|
|
46
|
+
[inventoryFile, modelFile, ...(review ? [reviewFile] : [])],
|
|
47
|
+
path.join(runWorkDir, 'analyze.log'),
|
|
48
|
+
);
|
|
43
49
|
if (!stages.analyze.ok) return { ok: false, failedStage: 'analyze', stages, inventoryFile, modelFile, protoDir };
|
|
44
50
|
|
|
45
51
|
stages.build = runNode(path.join(kitDir, 'build.mjs'), [run.runDir, inventoryFile, modelFile, labelsDir, protoDir], path.join(runWorkDir, 'build.log'));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
7
|
+
import { reviewFramingMarkup } from '../lib/review-framing.mjs';
|
|
6
8
|
import { buildPrototype } from './lib/run-process.mjs';
|
|
7
9
|
|
|
8
10
|
const testsDir = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -21,8 +23,149 @@ assert.equal(partialManifest.units.length, 1);
|
|
|
21
23
|
|
|
22
24
|
const threeShell = buildFixture('three-shell');
|
|
23
25
|
assert.equal(threeShell.ok, true, 'must stay silent: a three-region shell still builds');
|
|
26
|
+
const outputDigest = (directory) => {
|
|
27
|
+
const files = [];
|
|
28
|
+
const visit = (current) => {
|
|
29
|
+
for (const name of fs.readdirSync(current).sort()) {
|
|
30
|
+
const file = path.join(current, name);
|
|
31
|
+
if (fs.statSync(file).isDirectory()) visit(file);
|
|
32
|
+
else files.push(file);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
visit(directory);
|
|
36
|
+
const digest = createHash('sha256');
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
digest.update(path.relative(directory, file));
|
|
39
|
+
digest.update('\0');
|
|
40
|
+
digest.update(fs.readFileSync(file));
|
|
41
|
+
digest.update('\0');
|
|
42
|
+
}
|
|
43
|
+
return digest.digest('hex');
|
|
44
|
+
};
|
|
45
|
+
assert.equal(
|
|
46
|
+
outputDigest(threeShell.protoDir),
|
|
47
|
+
'5168cc6926988d3bee32d5e890792e989417d9ca78376282f988ebf7875a57bc',
|
|
48
|
+
'must stay silent: review support leaves the accepted shell/navigation complete output byte-identical',
|
|
49
|
+
);
|
|
24
50
|
const threeShellModel = JSON.parse(fs.readFileSync(threeShell.modelFile, 'utf8'));
|
|
25
51
|
assert.deepEqual(Object.keys(threeShellModel.canonicalShell).sort(), ['footer', 'header', 'sidebar']);
|
|
52
|
+
const threeShellInventory = JSON.parse(fs.readFileSync(threeShell.inventoryFile, 'utf8'));
|
|
53
|
+
const absentGroundBand = threeShellInventory.pages.find((page) => page.pageId === 'p0-home').bands[0];
|
|
54
|
+
assert.equal(absentGroundBand.groundRegistration, null, 'must stay silent: a flat donor without a ground fact remains unregistered');
|
|
55
|
+
assert.equal(absentGroundBand.heading.color, 'rgb(238, 238, 238)', 'must stay silent: inventory preserves the donor heading color when the fact is absent');
|
|
56
|
+
assert.equal(absentGroundBand.readableTextColor, absentGroundBand.heading.color, 'must stay silent: inventory does not synthesize neutral-surface ink when the fact is absent');
|
|
57
|
+
const holdingSettings = fs.readFileSync(path.join(threeShell.protoDir, 'holding-settings.html'), 'utf8');
|
|
58
|
+
assert.equal(holdingSettings.includes('Prototype holding page'), false, 'must fire: the internal holding label is absent from shipped output');
|
|
59
|
+
assert.equal((holdingSettings.match(/data-proto-holding-surface="true"/g) || []).length, 1, 'must fire: the fixed fixture receives exactly one holding surface');
|
|
60
|
+
assert.equal(holdingSettings.includes('data-proto-holding-layout="mobile"'), false, 'must stay silent: a fixed canvas does not reflow its holding merely because the browser is narrow');
|
|
61
|
+
assert.equal((holdingSettings.match(/data-proto-shell-region=/g) || []).length, 3, 'must stay silent: base-state header, sidebar, and footer are all retained');
|
|
62
|
+
assert.equal((holdingSettings.match(/id="viewport-1-a-footer"/g) || []).length, 1, 'must stay silent: a nested retained shell is measured and placed without cloning a duplicate');
|
|
63
|
+
assert.match(holdingSettings, /Settings is planned, but it is not included in this prototype yet\./, 'must stay silent: planned-destination copy remains sentence case');
|
|
64
|
+
assert.match(holdingSettings, /<a class="proto-clickable" href="[^"]+"[^>]+aria-label="Back to [^"]+">Back<\/a>/, 'must stay silent: the bare Back action ships as a native, keyboard-operable link with an accessible destination');
|
|
65
|
+
assert.equal(holdingSettings.includes('data-proto-ground-neutralized'), false, 'must stay silent: build does not neutralize an absent ground fact');
|
|
66
|
+
assert.match(holdingSettings, /color:rgb\(238, 238, 238\)/, 'must stay silent: build preserves absent-fact donor ink even when contrast is low');
|
|
67
|
+
|
|
68
|
+
const review = {
|
|
69
|
+
terminalStatus: 'partial',
|
|
70
|
+
winnerPageFailed: true,
|
|
71
|
+
expectedPageCount: 4,
|
|
72
|
+
availablePageCount: 2,
|
|
73
|
+
expectedRoutes: ['p0-home', 'p1-reports', 'p2-settings', 'p3-billing'],
|
|
74
|
+
realRoutes: [
|
|
75
|
+
{ id: 'p0-home', title: 'Home', file: 'p0-home.html', status: 'succeeded' },
|
|
76
|
+
{ id: 'p1-reports', title: 'Reports', file: 'p1-reports.html', status: 'succeeded' },
|
|
77
|
+
],
|
|
78
|
+
missingRoutes: [
|
|
79
|
+
{ id: 'p2-settings', title: 'Settings', holding: 'holding-p2-settings.html', file: 'holding-p2-settings.html' },
|
|
80
|
+
{ id: 'p3-billing', title: 'Billing', holding: 'holding-p3-billing.html', file: 'holding-p3-billing.html' },
|
|
81
|
+
],
|
|
82
|
+
partialRoutes: [],
|
|
83
|
+
entry: { kind: 'holding', id: 'p2-settings', title: 'Settings', file: 'holding-p2-settings.html' },
|
|
84
|
+
};
|
|
85
|
+
const reviewed = buildPrototype({
|
|
86
|
+
kitDir,
|
|
87
|
+
run: { runDir: path.join(testsDir, 'fixtures', 'three-shell') },
|
|
88
|
+
runWorkDir: path.join(scratch, 'review-mode'),
|
|
89
|
+
review,
|
|
90
|
+
});
|
|
91
|
+
assert.equal(reviewed.ok, true, 'must fire: review mode builds the available pages and explicit missing routes');
|
|
92
|
+
const reviewedModel = JSON.parse(fs.readFileSync(reviewed.modelFile, 'utf8'));
|
|
93
|
+
assert.deepEqual(reviewedModel.review, review, 'must fire: analysis carries the review facts without reinterpretation');
|
|
94
|
+
const settingsNav = reviewedModel.nav.find((item) => item.text === 'Settings');
|
|
95
|
+
assert.equal(settingsNav.target, null, 'must fire: an exact missing planned route cannot target a surviving page');
|
|
96
|
+
assert.equal(settingsNav.holding, 'holding-p2-settings.html', 'must fire: an exact missing planned route owns its explicit holding');
|
|
97
|
+
assert.equal(settingsNav.reviewRoute, 'p2-settings', 'must fire: analysis retains the exact planned route identity');
|
|
98
|
+
assert.equal(fs.existsSync(path.join(reviewed.protoDir, 'holding-p3-billing.html')), true, 'must fire: a missing route with no source navigation is still emitted');
|
|
99
|
+
assert.match(fs.readFileSync(path.join(reviewed.protoDir, 'index.html'), 'utf8'), /url=holding-p2-settings\.html/, 'must fire: review index opens the explicit entry route');
|
|
100
|
+
const reviewedManifest = JSON.parse(fs.readFileSync(path.join(reviewed.protoDir, 'proto-manifest.json'), 'utf8'));
|
|
101
|
+
assert.deepEqual(reviewedManifest.review, review, 'must fire: review facts remain inspectable in the kit manifest');
|
|
102
|
+
assert.ok(reviewedManifest.holdings.some(([holding, title]) => holding === 'holding-p3-billing' && title === 'Billing'), 'must fire: the manifest keeps the established extension-free holding identity and owner-facing title');
|
|
103
|
+
for (const file of ['p0-home.html', 'p1-reports.html', 'holding-p2-settings.html', 'holding-p3-billing.html']) {
|
|
104
|
+
const html = fs.readFileSync(path.join(reviewed.protoDir, file), 'utf8');
|
|
105
|
+
assert.match(html, /Branch status: Partial/, `must fire: ${file} carries the visible branch status`);
|
|
106
|
+
assert.match(html, /2 of 4 routes are available in this review\./, `must fire: ${file} carries the available-route count`);
|
|
107
|
+
assert.match(html, /href="holding-p2-settings\.html">Settings<\/a>/, `must fire: ${file} links the exact unavailable Settings route`);
|
|
108
|
+
assert.match(html, /href="holding-p3-billing\.html">Billing<\/a>/, `must fire: ${file} links the exact unavailable Billing route`);
|
|
109
|
+
assert.match(html, /The approved starting page could not be built\./, `must fire: ${file} explains the missing approved start`);
|
|
110
|
+
assert.equal(html.includes('terminalStatus'), false, `must stay silent: ${file} does not expose internal review field names`);
|
|
111
|
+
}
|
|
112
|
+
assert.match(fs.readFileSync(path.join(reviewed.protoDir, 'p0-home.html'), 'utf8'), /data-proto-target="holding-p2-settings\.html"/, 'must fire: real-page navigation uses the explicit missing-route file');
|
|
113
|
+
|
|
114
|
+
const missingSourceReview = {
|
|
115
|
+
...review,
|
|
116
|
+
realRoutes: [
|
|
117
|
+
{ id: 'p1-reports', title: 'Reports', file: 'p1-reports.html', status: 'succeeded' },
|
|
118
|
+
{ id: 'p2-activity', title: 'Activity', file: 'p2-activity.html', status: 'succeeded' },
|
|
119
|
+
],
|
|
120
|
+
missingRoutes: [{ id: 'p0-home', title: 'Home', holding: 'holding-p0-home.html', file: 'holding-p0-home.html' }],
|
|
121
|
+
expectedPageCount: 3,
|
|
122
|
+
availablePageCount: 2,
|
|
123
|
+
expectedRoutes: ['p0-home', 'p1-reports', 'p2-activity'],
|
|
124
|
+
entry: { kind: 'holding', id: 'p0-home', title: 'Home', file: 'holding-p0-home.html' },
|
|
125
|
+
};
|
|
126
|
+
const missingSourceDir = path.join(scratch, 'missing-source-input');
|
|
127
|
+
fs.mkdirSync(path.join(missingSourceDir, 'pages'), { recursive: true });
|
|
128
|
+
fs.copyFileSync(path.join(testsDir, 'fixtures', 'three-shell', 'pages', 'p0-home.html'), path.join(missingSourceDir, 'pages', 'p1-reports.html'));
|
|
129
|
+
fs.copyFileSync(path.join(testsDir, 'fixtures', 'three-shell', 'pages', 'p1-reports.html'), path.join(missingSourceDir, 'pages', 'p2-activity.html'));
|
|
130
|
+
fs.writeFileSync(path.join(missingSourceDir, 'branch.plan.json'), JSON.stringify({
|
|
131
|
+
version: 1,
|
|
132
|
+
scope: 'site',
|
|
133
|
+
concept: 'A partial branch whose approved starting route did not survive.',
|
|
134
|
+
pages: [
|
|
135
|
+
{ index: 0, id: 'p0-home', title: 'Home', surfaceType: 'web_app', purpose: 'Start the application.', startsFromWinner: true, slots: ['z'] },
|
|
136
|
+
{ index: 1, id: 'p1-reports', title: 'Reports', surfaceType: 'web_app', purpose: 'Show reports.', startsFromWinner: false, slots: ['a'] },
|
|
137
|
+
{ index: 2, id: 'p2-activity', title: 'Activity', surfaceType: 'web_app', purpose: 'Show activity.', startsFromWinner: false, slots: ['b'] },
|
|
138
|
+
],
|
|
139
|
+
screens: [
|
|
140
|
+
{ slot: 'z', pageIndex: 0, content: 'Home content', dependsOnSlot: null },
|
|
141
|
+
{ slot: 'a', pageIndex: 1, content: 'Reports content', dependsOnSlot: null },
|
|
142
|
+
{ slot: 'b', pageIndex: 2, content: 'Activity content', dependsOnSlot: null },
|
|
143
|
+
],
|
|
144
|
+
}, null, 2));
|
|
145
|
+
const missingSource = buildPrototype({
|
|
146
|
+
kitDir,
|
|
147
|
+
run: { runDir: missingSourceDir },
|
|
148
|
+
runWorkDir: path.join(scratch, 'missing-source-work'),
|
|
149
|
+
review: missingSourceReview,
|
|
150
|
+
});
|
|
151
|
+
assert.equal(missingSource.ok, true, 'must fire: review mode builds when the approved starting route has no source HTML');
|
|
152
|
+
const missingSourceModel = JSON.parse(fs.readFileSync(missingSource.modelFile, 'utf8'));
|
|
153
|
+
assert.equal(missingSourceModel.states.every((state) => state.isSource === false), true, 'fixture contract: no surviving state aliases the missing approved source');
|
|
154
|
+
const homeNav = missingSourceModel.nav.find((item) => item.text === 'Home');
|
|
155
|
+
assert.equal(homeNav.target, null, 'must fire: missing approved Home is never aliased to the first surviving state');
|
|
156
|
+
assert.equal(homeNav.holding, 'holding-p0-home.html', 'must fire: missing approved Home keeps its explicit planned holding');
|
|
157
|
+
assert.match(fs.readFileSync(path.join(missingSource.protoDir, 'index.html'), 'utf8'), /url=holding-p0-home\.html/, 'must fire: the missing approved route remains the declared review entry');
|
|
158
|
+
assert.match(fs.readFileSync(path.join(missingSource.protoDir, 'holding-p0-home.html'), 'utf8'), /The approved starting page could not be built\./, 'must fire: the entry holding plainly explains the approved-page failure');
|
|
159
|
+
assert.match(
|
|
160
|
+
reviewFramingMarkup({ ...review, terminalStatus: 'failed', terminalClass: 'required_winner_page_failed' }),
|
|
161
|
+
/Branch status: Failed/,
|
|
162
|
+
'must fire: a failed branch is named plainly without exposing its internal class',
|
|
163
|
+
);
|
|
164
|
+
assert.match(
|
|
165
|
+
reviewFramingMarkup({ ...review, terminalClass: 'required_winner_page_failed' }),
|
|
166
|
+
/Branch status: Partial/,
|
|
167
|
+
'must stay silent: an internal failure class does not overstate a partial branch',
|
|
168
|
+
);
|
|
26
169
|
|
|
27
170
|
const legacy = buildFixture('no-semantic');
|
|
28
171
|
assert.equal(legacy.ok, false, 'must fire: a legacy export without semantic ids is gated');
|
|
@@ -18,7 +18,11 @@ function parseArgs(argv) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function maxShellDelta(rows) {
|
|
21
|
-
return rows.reduce((max, row) => Math.max(
|
|
21
|
+
return rows.reduce((max, row) => Math.max(
|
|
22
|
+
max,
|
|
23
|
+
Math.abs(row.own[0] - row.canonical[0]),
|
|
24
|
+
Math.abs(row.own[1] - row.canonical[1]),
|
|
25
|
+
), 0);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -41,7 +45,7 @@ const server = await startStaticServer(args.workDir);
|
|
|
41
45
|
const browser = await chromium.launch();
|
|
42
46
|
let ordinal = 0;
|
|
43
47
|
for (const run of selected) {
|
|
44
|
-
ordinal
|
|
48
|
+
ordinal += 1;
|
|
45
49
|
if (completed.has(run.fingerprint)) {
|
|
46
50
|
process.stdout.write(`[${ordinal}/${selected.length}] skip ${run.fingerprint.slice(0, 10)} ${run.runDir}\n`);
|
|
47
51
|
continue;
|
|
@@ -55,12 +59,23 @@ for (const run of selected) {
|
|
|
55
59
|
process.stdout.write(`[${ordinal}/${selected.length}] build ${runId} ${run.runDir}\n`);
|
|
56
60
|
const built = buildPrototype({ kitDir, run, runWorkDir });
|
|
57
61
|
if (!built.ok) {
|
|
58
|
-
row = {
|
|
62
|
+
row = {
|
|
63
|
+
...run,
|
|
64
|
+
runId,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
failedStage: built.failedStage,
|
|
67
|
+
error: built.stages[built.failedStage].error,
|
|
68
|
+
stages: built.stages,
|
|
69
|
+
};
|
|
59
70
|
} else {
|
|
60
71
|
const model = JSON.parse(fs.readFileSync(built.modelFile, 'utf8'));
|
|
61
72
|
const protoManifest = JSON.parse(fs.readFileSync(path.join(built.protoDir, 'proto-manifest.json'), 'utf8'));
|
|
62
|
-
const
|
|
63
|
-
|
|
73
|
+
const runtime = await checkRuntime({
|
|
74
|
+
browser,
|
|
75
|
+
baseUrl: server.baseUrl,
|
|
76
|
+
relativeProtoPath: `${runId}/proto`,
|
|
77
|
+
protoDir: built.protoDir,
|
|
78
|
+
});
|
|
64
79
|
const realPages = protoManifest.units.length;
|
|
65
80
|
const holdingPages = protoManifest.holdings.length;
|
|
66
81
|
row = {
|
|
@@ -74,21 +89,25 @@ for (const run of selected) {
|
|
|
74
89
|
holdingPages,
|
|
75
90
|
holdingRatio: holdingPages / Math.max(1, realPages + holdingPages),
|
|
76
91
|
actionsBound: protoManifest.units.reduce((sum, unit) => sum + unit.actions + unit.fields, 0),
|
|
92
|
+
zeroActionPages: protoManifest.units.filter((unit) => unit.actions + unit.fields === 0).length,
|
|
77
93
|
actionCounts: protoManifest.counts,
|
|
78
94
|
unboundActions: protoManifest.unbound.length,
|
|
95
|
+
invalidDropdowns: protoManifest.invalidDropdowns.length,
|
|
79
96
|
shellRegions: Object.keys(protoManifest.canonicalShell),
|
|
80
97
|
shellResiduals: protoManifest.shellSizeDelta.length,
|
|
81
98
|
maxShellDeltaPx: maxShellDelta(protoManifest.shellSizeDelta),
|
|
82
99
|
shellAmbiguities: protoManifest.shellAmbiguities.length,
|
|
100
|
+
suppressedShellOverlaps: protoManifest.suppressedShellOverlaps.length,
|
|
101
|
+
skippedShellTransplants: protoManifest.skippedShellTransplants.length,
|
|
83
102
|
replacedShellActions: protoManifest.replacedShellActions,
|
|
84
103
|
shellActionRemaps: protoManifest.shellActionRemaps,
|
|
85
104
|
runtime,
|
|
86
105
|
};
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
|
-
fs.appendFileSync(args.results, JSON.stringify(row)
|
|
108
|
+
fs.appendFileSync(args.results, `${JSON.stringify(row)}\n`);
|
|
90
109
|
completed.set(run.fingerprint, row);
|
|
91
|
-
process.stdout.write(` ${row.status}${row.failedStage ? ` @ ${row.failedStage}` : ` real=${row.realPages} holding=${row.holdingPages}
|
|
110
|
+
process.stdout.write(` ${row.status}${row.failedStage ? ` @ ${row.failedStage}` : ` real=${row.realPages} holding=${row.holdingPages} errors=${row.runtime.pageErrors} composition=${row.runtime.compositionFailures.length}`}\n`);
|
|
92
111
|
}
|
|
93
112
|
await browser.close();
|
|
94
113
|
await server.close();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseHTML } from 'linkedom';
|
|
3
4
|
import { usableDropdownOptions } from '../lib/actions.mjs';
|
|
5
|
+
import { declaredActiveMarkers, linkedNavForUnit, markerOverlapsNav } from '../lib/nav-active-state.mjs';
|
|
4
6
|
import { matchPlannedPage } from '../lib/navigation.mjs';
|
|
7
|
+
import { needsCanonicalShell, selectCanonicalShellPage } from '../lib/shell-consistency.mjs';
|
|
8
|
+
import { stitchUnitShells } from '../lib/shell-stitching.mjs';
|
|
5
9
|
import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion, shouldSuppressDroppedShell, shouldTransplantShell } from '../lib/shells.mjs';
|
|
6
10
|
import { slugText } from '../lib/text.mjs';
|
|
7
11
|
|
|
@@ -47,4 +51,80 @@ assert.equal(shouldTransplantShell([0, 0, 1215, 70], [0, 0, 1342, 68]), true, 'm
|
|
|
47
51
|
assert.equal(isWithinShell(['row', 'sidebar', 'viewport'], new Set(['sidebar'])), true, 'must fire: a nested shell row is not treated as page content');
|
|
48
52
|
assert.equal(isWithinShell(['row', 'main', 'viewport'], new Set(['sidebar'])), false, 'must stay silent: a content row remains interactive');
|
|
49
53
|
|
|
54
|
+
const canonicalPage = selectCanonicalShellPage({
|
|
55
|
+
pages: [
|
|
56
|
+
{ pageId: 'menu', planPage: { index: 0 }, bands: [{ key: 'native', shells: [{ region: 'header' }] }] },
|
|
57
|
+
{ pageId: 'journal', planPage: { index: 2 }, bands: [{ key: 'mobile', shells: [{ region: 'header' }, { region: 'footer' }] }, { key: 'native', shells: [{ region: 'header' }, { region: 'footer' }] }] },
|
|
58
|
+
],
|
|
59
|
+
states: [
|
|
60
|
+
{ stateId: 'menu', pageId: 'menu', isSource: true },
|
|
61
|
+
{ stateId: 'journal', pageId: 'journal', isSource: false },
|
|
62
|
+
],
|
|
63
|
+
canonicalShell: {
|
|
64
|
+
header: { stateId: 'journal' },
|
|
65
|
+
footer: { stateId: 'journal' },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
assert.equal(canonicalPage?.pageId, 'journal', 'must fire: the most complete measured shell page becomes the one real donor');
|
|
69
|
+
assert.equal(needsCanonicalShell({ signature: 'source' }, { signature: 'canonical' }), true, 'must fire: a declared shell fingerprint difference requests canonicalization');
|
|
70
|
+
assert.equal(needsCanonicalShell({ signature: 'same' }, { signature: 'same' }), false, 'must stay silent: an already canonical shell is a literal no-write');
|
|
71
|
+
|
|
72
|
+
const markerShell = {
|
|
73
|
+
items: [
|
|
74
|
+
{ id: 'menu', text: 'Menu', x: 20, y: 10, w: 60, h: 20 },
|
|
75
|
+
{ id: 'journal', text: 'Journal', x: 100, y: 10, w: 70, h: 20 },
|
|
76
|
+
{ id: 'underline', label: 'rust brush underline beneath Menu', x: 20, y: 32, w: 60, h: 4, chain: [] },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
const markers = declaredActiveMarkers(markerShell, markerShell.items.slice(0, 2));
|
|
80
|
+
assert.deepEqual(markers.map((marker) => marker.targetText), ['Menu'], 'must fire: an explicitly declared raster underline identifies its nav entity');
|
|
81
|
+
assert.equal(markerOverlapsNav(markerShell.items[2], markerShell.items[0]), true, 'must fire: the active marker is geometrically below the linked item');
|
|
82
|
+
const menuUnit = { states: [{ stateId: 'menu-page' }] };
|
|
83
|
+
const linkedMenu = linkedNavForUnit({
|
|
84
|
+
navRows: [
|
|
85
|
+
{ region: 'header', text: 'Menu', target: 'menu-page' },
|
|
86
|
+
{ region: 'header', text: 'Journal', target: 'journal-page' },
|
|
87
|
+
],
|
|
88
|
+
unit: menuUnit,
|
|
89
|
+
sourceMarkers: markers,
|
|
90
|
+
region: 'header',
|
|
91
|
+
});
|
|
92
|
+
assert.equal(linkedMenu?.text, 'Menu', 'must fire: active state follows the entity link for the current page');
|
|
93
|
+
assert.equal(linkedNavForUnit({ navRows: [{ region: 'header', text: 'Journal', target: 'journal-page' }], unit: menuUnit, sourceMarkers: markers, region: 'header' }), null, 'must stay silent: a page without a nav entity match keeps its source ink');
|
|
94
|
+
|
|
95
|
+
const rulesForFixture = () => '';
|
|
96
|
+
const styleTextFixture = () => '';
|
|
97
|
+
const noWriteDocument = parseHTML('<html><head></head><body><header id="detail-header"><span id="detail-home">Home</span><img id="detail-ink" alt="Home navigation underline"></header></body></html>').document;
|
|
98
|
+
const noWriteBefore = noWriteDocument.documentElement.outerHTML;
|
|
99
|
+
const noWriteResult = stitchUnitShells({
|
|
100
|
+
document: noWriteDocument,
|
|
101
|
+
donorDocument: parseHTML('<html><head></head><body><header id="home-header"><span id="home-home">Home</span></header></body></html>').document,
|
|
102
|
+
donorPage: { pageId: 'home', bands: [{ key: 'native', shells: [{ containerId: 'home-header', region: 'header', box: [0, 0, 100, 20], signature: 'same', items: [{ id: 'home-home', text: 'Home', x: 0, y: 0, w: 40, h: 20 }] }] }] },
|
|
103
|
+
recipientPage: { pageId: 'detail', bands: [{ key: 'native', shells: [{ containerId: 'detail-header', region: 'header', box: [0, 0, 100, 20], signature: 'same', items: [{ id: 'detail-home', text: 'Home', x: 0, y: 0, w: 40, h: 20 }, { id: 'detail-ink', label: 'Home navigation underline', x: 0, y: 20, w: 40, h: 3, chain: [] }] }] }] },
|
|
104
|
+
unit: { states: [{ stateId: 'detail', shell: [{ region: 'header', box: [0, 0, 100, 20] }] }] },
|
|
105
|
+
canonicalShell: { header: { stateId: 'home', containerId: 'home-header', box: [0, 0, 100, 20] } },
|
|
106
|
+
navRows: [{ region: 'header', text: 'Home', target: 'home' }],
|
|
107
|
+
rulesFor: rulesForFixture,
|
|
108
|
+
styleText: styleTextFixture,
|
|
109
|
+
});
|
|
110
|
+
assert.equal(noWriteResult.changes, 0);
|
|
111
|
+
assert.equal(noWriteDocument.documentElement.outerHTML, noWriteBefore, 'must stay silent: a consistent shell with unlinked nav replays byte-identically through the new pass');
|
|
112
|
+
|
|
113
|
+
const fireDocument = parseHTML('<html><head></head><body><header id="menu-header"><span id="menu-menu">Menu</span><span id="menu-journal">Journal</span><img id="menu-ink" alt="rust brush underline beneath Menu"></header></body></html>').document;
|
|
114
|
+
const fireResult = stitchUnitShells({
|
|
115
|
+
document: fireDocument,
|
|
116
|
+
donorDocument: parseHTML('<html><head></head><body><header id="journal-header"><span id="journal-menu">Menu</span><span id="journal-journal">Journal</span><img id="journal-ink" alt="Journal navigation underline"></header></body></html>').document,
|
|
117
|
+
donorPage: { pageId: 'journal', bands: [{ key: 'native', shells: [{ containerId: 'journal-header', region: 'header', box: [0, 0, 200, 40], signature: 'journal', items: [{ id: 'journal-menu', text: 'Menu', x: 10, y: 5, w: 50, h: 20 }, { id: 'journal-journal', text: 'Journal', x: 90, y: 5, w: 60, h: 20 }, { id: 'journal-ink', label: 'Journal navigation underline', x: 90, y: 27, w: 60, h: 3, chain: [] }] }] }] },
|
|
118
|
+
recipientPage: { pageId: 'menu', bands: [{ key: 'native', shells: [{ containerId: 'menu-header', region: 'header', box: [0, 0, 200, 40], signature: 'menu', items: [{ id: 'menu-menu', text: 'Menu', x: 10, y: 5, w: 50, h: 20 }, { id: 'menu-journal', text: 'Journal', x: 90, y: 5, w: 60, h: 20 }, { id: 'menu-ink', label: 'rust brush underline beneath Menu', x: 10, y: 27, w: 50, h: 3, chain: [] }] }] }] },
|
|
119
|
+
unit: { states: [{ stateId: 'menu', shell: [{ region: 'header', box: [0, 0, 200, 40] }] }] },
|
|
120
|
+
canonicalShell: { header: { stateId: 'journal', containerId: 'journal-header', box: [0, 0, 200, 40] } },
|
|
121
|
+
navRows: [{ region: 'header', text: 'Menu', target: 'menu' }, { region: 'header', text: 'Journal', target: 'journal' }],
|
|
122
|
+
rulesFor: rulesForFixture,
|
|
123
|
+
styleText: styleTextFixture,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(fireResult.changes, 1, 'must fire: a measured incompatible fingerprint commits the declared donor shell');
|
|
126
|
+
assert.ok(fireDocument.getElementById('journal-header'), 'must fire: the canonical donor outer shell replaces the recipient shell');
|
|
127
|
+
assert.ok(fireDocument.getElementById('menu-ink'), 'must fire: the page-declared Menu underline is retained and moved into the donor shell');
|
|
128
|
+
assert.equal(fireResult.bands[0].regions[0].active.navText, 'Menu');
|
|
129
|
+
|
|
50
130
|
process.stdout.write('rules: ok\n');
|
|
@@ -31,6 +31,10 @@ if (audit.consoleErrors > 0) unsafe.push(`${audit.consoleErrors} console error(s
|
|
|
31
31
|
if (audit.duplicateIds > 0) unsafe.push(`${audit.duplicateIds} duplicate id(s)`);
|
|
32
32
|
if (audit.brokenTargets.length > 0) unsafe.push(`${audit.brokenTargets.length} broken navigation target(s)`);
|
|
33
33
|
if (audit.interactionFailures.length > 0) unsafe.push(`${audit.interactionFailures.length} page(s) with broken dropdown/modal interaction`);
|
|
34
|
+
if (audit.compositionFailures.length > 0) unsafe.push(`${audit.compositionFailures.length} holding composition failure(s)`);
|
|
35
|
+
if (audit.shellConsistencyFailures.length > 0) unsafe.push(`${audit.shellConsistencyFailures.length} canonical shell consistency failure(s)`);
|
|
36
|
+
if (audit.activeNavigationFailures.length > 0) unsafe.push(`${audit.activeNavigationFailures.length} active navigation failure(s)`);
|
|
34
37
|
if (audit.deepLinkBack.present && !audit.deepLinkBack.ok) unsafe.push('deep-link history did not return to its source');
|
|
35
38
|
if (unsafe.length > 0) throw new Error(`Prototype runtime gate failed: ${unsafe.join('; ')}`);
|
|
36
|
-
|
|
39
|
+
const viewportWidths = [...new Set(audit.pages.flatMap((page) => page.viewports.map((viewport) => viewport.width)))].sort((left, right) => left - right);
|
|
40
|
+
console.error(`runtime gate passed: ${audit.pages.length} pages at ${viewportWidths.join('/')}, ${audit.brokenTargets.length} broken targets, ${audit.interactionFailures.length} interaction failures, ${audit.shellConsistencyFailures.length} shell failures, ${audit.activeNavigationFailures.length} active navigation failures`);
|