@legionworks/facet 1.9.0 → 1.10.1
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 +50 -21
- package/dist/gallery/{chunk-c6s9k9ty.js → chunk-0by8qn4x.js} +86 -3
- package/dist/gallery/{chunk-4g8rem85.css → chunk-2dge4gwb.css} +22 -1
- package/dist/gallery/frame/artifact.css +3 -0
- package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-g3asgaw3.js} +8 -0
- package/dist/gallery/frame/frame.css +11 -24
- package/dist/gallery/frame/runtime/chart.js +24 -2
- package/dist/gallery/frame/runtime/html.js +1 -1
- package/dist/gallery/frame/runtime/markdown.js +1 -1
- package/dist/gallery/frame/runtime/mermaid.js +1 -1
- package/dist/gallery/frame/runtime/svg.js +1 -1
- package/dist/gallery/frame/runtime/tsx.js +1 -1
- package/dist/gallery/index.html +4 -2
- package/docs/reference/cli.md +30 -2
- package/docs/reference/export.md +4 -0
- package/docs/reference/html.md +3 -3
- package/docs/reference/mcp.md +29 -8
- package/docs/reference/security.md +7 -0
- package/docs/reference/storage.md +6 -1
- package/docs/reference/tsx.md +16 -0
- package/docs/reference/validation.md +34 -6
- package/package.json +4 -4
- package/skills/facet/SKILL.md +24 -2
- package/src/cli/commands/promote.ts +1 -0
- package/src/cli/commands/templates.ts +25 -0
- package/src/cli/main.ts +4 -0
- package/src/cli/parser.ts +4 -0
- package/src/cli/presenter.ts +48 -0
- package/src/gallery-web/app.ts +65 -2
- package/src/gallery-web/favicon.ts +1 -0
- package/src/gallery-web/frame/renderers/chart.ts +31 -1
- package/src/gallery-web/frame/runtime.ts +7 -1
- package/src/gallery-web/frame/styles/artifact.css +3 -0
- package/src/gallery-web/frame/styles/frame.css +11 -24
- package/src/gallery-web/frame-html.ts +7 -2
- package/src/gallery-web/index.html +3 -1
- package/src/gallery-web/styles/verdict.css +19 -4
- package/src/harness-adapters/mcp/cli-bridge.ts +11 -1
- package/src/harness-adapters/mcp/main.ts +2 -0
- package/src/harness-adapters/mcp/server.ts +92 -69
- package/src/harness-adapters/mcp/tool-schemas.ts +7 -0
- package/src/service/dispatcher.ts +22 -0
- package/src/service/router-guards.ts +2 -0
- package/src/service/store/migrations.ts +12 -2
- package/src/service/store/repository-lifecycle.ts +91 -19
- package/src/service/store/repository.ts +60 -2
- package/src/service/store/schema.ts +4 -0
- package/src/service/stored-verdict.ts +3 -0
- package/src/shared/contracts/artifact.ts +1 -0
- package/src/shared/contracts/commands/index.ts +10 -0
- package/src/shared/contracts/commands/names.ts +2 -0
- package/src/shared/contracts/commands/requests.ts +7 -0
- package/src/shared/contracts/commands/results.ts +20 -1
- package/src/shared/contracts/promotion.ts +16 -0
- package/src/shared/contracts/validation.ts +7 -0
- package/src/shared/errors/facet-error.ts +2 -0
- package/src/shared/errors/store-error.ts +4 -0
- package/src/shared/html/artifact-main.ts +3 -0
- package/src/shared/html/style-vocabulary.ts +1 -0
- package/src/shared/storage-version.ts +1 -1
- package/src/validation/tier0/dom-shim.ts +3 -18
- package/src/validation/tier0/markdown.ts +28 -5
- package/src/validation/tier0/mermaid.ts +17 -8
- package/src/validation/tier0/worker-dispatch.ts +1 -1
- package/src/validation/tier1/entries/tsx.ts +1 -0
- package/src/validation/tier1/harness.ts +2 -1
- package/src/validation/tier1/isolated-probe.ts +4 -0
- package/src/validation/tier1/protocol-probe.ts +37 -0
- package/src/validation/tier1/runner.ts +21 -4
- package/src/validation/tier1/verdict.ts +24 -39
- package/templates/capacity-report.tsx +1 -1
- package/templates/fleet-dashboard.html +1 -1
- package/templates/html-release-ledger.html +1 -1
- package/templates/html-status-report.html +1 -1
- package/templates/incident-console.tsx +1 -1
- package/templates/tsx-interactive-counter.tsx +1 -1
- package/templates/tsx-status-report.tsx +1 -1
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* DOM (DOMPurify, etc.) at IMPORT TIME.
|
|
9
9
|
*
|
|
10
10
|
* `dom-shim.ts` installs a linkedom-based structural DOM stub BEFORE
|
|
11
|
-
* this module imports mermaid, so the library
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* this module imports mermaid, so the library can initialize. DOMPurify
|
|
12
|
+
* deliberately reports itself unsupported under the stub and passes label
|
|
13
|
+
* text through unsanitized: Tier 0 never renders or keeps that text, and
|
|
14
|
+
* sanitization is verified at Tier 1 in a real browser. The shim never
|
|
15
|
+
* executes artifact source against the DOM. netns ensures any
|
|
16
|
+
* library-initiated network egress cannot reach a host.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import "./dom-shim";
|
|
@@ -33,6 +35,15 @@ export interface MermaidParseFail {
|
|
|
33
35
|
|
|
34
36
|
export type MermaidParseResult = MermaidParseOk | MermaidParseFail;
|
|
35
37
|
|
|
38
|
+
export async function parseMermaidText(text: string): Promise<string | null> {
|
|
39
|
+
try {
|
|
40
|
+
await (mermaid as { parse: (s: string) => Promise<MermaidResolved> }).parse(text);
|
|
41
|
+
return null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
/**
|
|
37
48
|
* Count node declarations across the source. This is the lexical
|
|
38
49
|
* counter the parser agrees with when the parse succeeds and disagrees
|
|
@@ -58,10 +69,8 @@ export async function parseMermaid(bytes: Uint8Array): Promise<MermaidParseResul
|
|
|
58
69
|
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
59
70
|
const lexicalNodes = countMermaidNodes(bytes);
|
|
60
71
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
)) as MermaidResolved;
|
|
64
|
-
void resolved;
|
|
72
|
+
const message = await parseMermaidText(text);
|
|
73
|
+
if (message !== null) throw new Error(message);
|
|
65
74
|
return {
|
|
66
75
|
status: "ok",
|
|
67
76
|
observed: {
|
|
@@ -15,7 +15,7 @@ export async function runParser(input: WorkerInput): Promise<Tier0WorkerResult>
|
|
|
15
15
|
};
|
|
16
16
|
switch (input.artifactType) {
|
|
17
17
|
case "markdown": {
|
|
18
|
-
const result = parseMarkdown(input.source);
|
|
18
|
+
const result = await parseMarkdown(input.source);
|
|
19
19
|
const externalImageCount = result.observed.externalImageCount;
|
|
20
20
|
return {
|
|
21
21
|
...base,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "../../../gallery-web/frame/styles/artifact.css";
|
|
1
2
|
import { renderTsx } from "../../../gallery-web/frame/renderers/tsx";
|
|
2
3
|
import { createRendererRegistry } from "../../../gallery-web/frame/renderers/registry";
|
|
3
4
|
import { startTier1Harness } from "../harness-entry";
|
|
@@ -24,6 +24,7 @@ import { ARTIFACT_TYPES, type ArtifactType } from "../../gallery-web/frame/rende
|
|
|
24
24
|
import frameChromeStyles from "../../gallery-web/frame/styles/frame.css" with { type: "text" };
|
|
25
25
|
import { freshHarnessNonce } from "./nonce";
|
|
26
26
|
import { frameBundlePlugins } from "../../shared/build/frame-bundle-plugins";
|
|
27
|
+
import { artifactMainAttributes } from "../../shared/html/artifact-main";
|
|
27
28
|
import { FROZEN_CSP_TEMPLATE as HARNESS_CSP } from "../../shared/security/frozen-csp";
|
|
28
29
|
|
|
29
30
|
export { FROZEN_CSP_TEMPLATE as HARNESS_CSP } from "../../shared/security/frozen-csp";
|
|
@@ -135,7 +136,7 @@ export async function buildHarnessSrcdoc(artifactType: string): Promise<{
|
|
|
135
136
|
`<style>${frameChromeStyles.replace(/<\/style/gi, "<\\/style")}</style>` +
|
|
136
137
|
(styles.length === 0 ? "" : `<style>${styles.replace(/<\/style/gi, "<\\/style")}</style>`) +
|
|
137
138
|
"</head><body>" +
|
|
138
|
-
`<main
|
|
139
|
+
`<main ${artifactMainAttributes(rendererType)} data-facet-nonce="${nonce}"></main>` +
|
|
139
140
|
`<script type="module" nonce="${nonce}">${escaped}</script>` +
|
|
140
141
|
"</body></html>";
|
|
141
142
|
return { srcdoc, nonce, bundleBytes: bytes };
|
|
@@ -26,6 +26,7 @@ import type { VerifierCdpSession } from "./browser-process";
|
|
|
26
26
|
export async function probeIsolatedCounts(
|
|
27
27
|
session: VerifierCdpSession,
|
|
28
28
|
executionContextId: number,
|
|
29
|
+
observeContent = false,
|
|
29
30
|
): Promise<ProtocolObservation | null> {
|
|
30
31
|
try {
|
|
31
32
|
const selectors = Object.fromEntries(
|
|
@@ -47,6 +48,8 @@ export async function probeIsolatedCounts(
|
|
|
47
48
|
" var svgRoots = roots.filter(function(root){ return String(root.nodeName).toLowerCase() === 'svg'; });",
|
|
48
49
|
" var graphRoots = svgRoots.filter(function(root){ return root.getAttribute('data-facet-renderer-graph') === 'true'; });",
|
|
49
50
|
" var htmlRoots = roots.filter(function(root){ return String(root.nodeName).toLowerCase() !== 'svg'; });",
|
|
51
|
+
` var observeContent = ${JSON.stringify(observeContent)};`,
|
|
52
|
+
" var emptyRendererRoot = observeContent && htmlRoots.length === 1 ? Array.prototype.every.call(htmlRoots[0].childNodes, function(node){ return node.nodeType !== 1 && (node.nodeType !== 3 || !node.textContent.trim()); }) : undefined;",
|
|
50
53
|
` var selectors = ${JSON.stringify(selectors)};`,
|
|
51
54
|
" var html = htmlRoots.length === 0 ? null : {rendererRootCount:htmlRoots.length,headingCount:0,tableCount:0,listCount:0,imageCount:0,canvasCount:0,externalImageCount:0};",
|
|
52
55
|
" var externalImageCount = 0;",
|
|
@@ -82,6 +85,7 @@ export async function probeIsolatedCounts(
|
|
|
82
85
|
" opaqueRegionCount: opaqueRegionCount,",
|
|
83
86
|
" externalImageCount: externalImageCount,",
|
|
84
87
|
" discriminativeErrors: [],",
|
|
88
|
+
" ...(emptyRendererRoot === undefined ? {} : {emptyRendererRoot: emptyRendererRoot}),",
|
|
85
89
|
" ...(html === null ? {} : {html: html})",
|
|
86
90
|
" };",
|
|
87
91
|
"})()",
|
|
@@ -30,6 +30,8 @@ export interface SnapshotDocument {
|
|
|
30
30
|
readonly frameId: number;
|
|
31
31
|
readonly nodes: {
|
|
32
32
|
readonly nodeName: number[];
|
|
33
|
+
readonly nodeType?: readonly number[];
|
|
34
|
+
readonly nodeValue?: readonly number[];
|
|
33
35
|
/** Parent node index for every entry in nodeName. */
|
|
34
36
|
readonly parentIndex: readonly number[];
|
|
35
37
|
/**
|
|
@@ -44,6 +46,23 @@ export interface SnapshotDocument {
|
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function snapshotRootEmpty(snapshot: SnapshotResponse, documentIndex: number): boolean | undefined {
|
|
50
|
+
const document = snapshot.documents[documentIndex];
|
|
51
|
+
if (document === undefined) return undefined;
|
|
52
|
+
const roots = htmlRootIndexes(snapshot, documentIndex);
|
|
53
|
+
if (roots.length !== 1) return undefined;
|
|
54
|
+
const root = roots[0];
|
|
55
|
+
if (root === undefined) return undefined;
|
|
56
|
+
for (let index = 0; index < document.nodes.nodeName.length; index += 1) {
|
|
57
|
+
if (document.nodes.parentIndex[index] !== root) continue;
|
|
58
|
+
const type = document.nodes.nodeType?.[index];
|
|
59
|
+
if (type === 1) return false;
|
|
60
|
+
if (type === 3 && readString(snapshot.strings, document.nodes.nodeValue?.[index] ?? -1).trim())
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
export interface SnapshotResponse {
|
|
48
67
|
readonly documents: readonly SnapshotDocument[];
|
|
49
68
|
readonly strings: readonly string[];
|
|
@@ -328,6 +347,7 @@ function findDocumentIndex(snapshot: SnapshotResponse, childFrameId: string): nu
|
|
|
328
347
|
export async function probeProtocolSnapshot(
|
|
329
348
|
session: VerifierCdpSession,
|
|
330
349
|
childFrame: ResolvedChildFrame,
|
|
350
|
+
observeContent = false,
|
|
331
351
|
): Promise<ProtocolObservation> {
|
|
332
352
|
const snapshot = (await session.send("DOMSnapshot.captureSnapshot", {
|
|
333
353
|
computedStyles: [],
|
|
@@ -352,6 +372,7 @@ export async function probeProtocolSnapshot(
|
|
|
352
372
|
const discriminativeErrors = collectDiscriminativeErrors(snapshot, documentIndex);
|
|
353
373
|
const errorCount = discriminativeErrors.length;
|
|
354
374
|
const htmlCounts = countSnapshotHtml(snapshot, documentIndex);
|
|
375
|
+
const emptyRendererRoot = observeContent ? snapshotRootEmpty(snapshot, documentIndex) : undefined;
|
|
355
376
|
return {
|
|
356
377
|
rendererRootSvgCount: rendererRoots.length,
|
|
357
378
|
graphCount: graphRoots.length,
|
|
@@ -360,6 +381,7 @@ export async function probeProtocolSnapshot(
|
|
|
360
381
|
opaqueRegionCount: countByName(snapshot, documentIndex, "canvas"),
|
|
361
382
|
externalImageCount: htmlCounts?.externalImageCount ?? 0,
|
|
362
383
|
...(htmlCounts === undefined ? {} : { html: htmlCounts }),
|
|
384
|
+
...(emptyRendererRoot === undefined ? {} : { emptyRendererRoot }),
|
|
363
385
|
viewBoxes,
|
|
364
386
|
errorCount,
|
|
365
387
|
discriminativeErrors: discriminativeErrors.map((entry) => ({
|
|
@@ -378,6 +400,7 @@ export async function probeProtocolSnapshot(
|
|
|
378
400
|
export async function probeProtocolGetDocument(
|
|
379
401
|
session: VerifierCdpSession,
|
|
380
402
|
childFrame: ResolvedChildFrame,
|
|
403
|
+
observeContent = false,
|
|
381
404
|
): Promise<ProtocolObservation> {
|
|
382
405
|
const result = (await session.send("DOM.getDocument", {
|
|
383
406
|
depth: -1,
|
|
@@ -402,6 +425,7 @@ export async function probeProtocolGetDocument(
|
|
|
402
425
|
let opaqueRegionCount = 0;
|
|
403
426
|
let visibleSvgCount = 0;
|
|
404
427
|
let html: HtmlStructureCounts | undefined;
|
|
428
|
+
let emptyRendererRoot: boolean | undefined;
|
|
405
429
|
const viewBoxes: string[] = [];
|
|
406
430
|
const visit = (
|
|
407
431
|
node: unknown,
|
|
@@ -413,6 +437,8 @@ export async function probeProtocolGetDocument(
|
|
|
413
437
|
if (node === null || typeof node !== "object") return;
|
|
414
438
|
const record = node as {
|
|
415
439
|
nodeName?: string;
|
|
440
|
+
nodeType?: number;
|
|
441
|
+
nodeValue?: string;
|
|
416
442
|
// DOM.Node attributes arrive as a FLAT string array
|
|
417
443
|
// [name1, value1, name2, value2, …] — not {name, value} objects.
|
|
418
444
|
attributes?: string[];
|
|
@@ -435,6 +461,16 @@ export async function probeProtocolGetDocument(
|
|
|
435
461
|
const htmlRoot = name !== "svg" && markedRoot && !withinMarkedRoot;
|
|
436
462
|
const graphRoot = rendererRoot && findAttr("data-facet-renderer-graph") === "true";
|
|
437
463
|
if (htmlRoot) {
|
|
464
|
+
if (observeContent) {
|
|
465
|
+
const empty = !(record.children ?? []).some((child) => {
|
|
466
|
+
const direct = child as ProtocolDomNode & { nodeType?: number; nodeValue?: string };
|
|
467
|
+
return (
|
|
468
|
+
direct.nodeType === 1 ||
|
|
469
|
+
(direct.nodeType === 3 && (direct.nodeValue ?? "").trim().length > 0)
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
emptyRendererRoot = emptyRendererRoot === undefined ? empty : false;
|
|
473
|
+
}
|
|
438
474
|
html ??= {
|
|
439
475
|
rendererRootCount: 0,
|
|
440
476
|
headingCount: 0,
|
|
@@ -511,6 +547,7 @@ export async function probeProtocolGetDocument(
|
|
|
511
547
|
opaqueRegionCount,
|
|
512
548
|
externalImageCount: html?.externalImageCount ?? 0,
|
|
513
549
|
...(html === undefined ? {} : { html }),
|
|
550
|
+
...(emptyRendererRoot === undefined ? {} : { emptyRendererRoot }),
|
|
514
551
|
viewBoxes,
|
|
515
552
|
errorCount,
|
|
516
553
|
discriminativeErrors:
|
|
@@ -327,6 +327,7 @@ async function runTier1Attempt(
|
|
|
327
327
|
artifactFrame,
|
|
328
328
|
isolated.executionContextId,
|
|
329
329
|
interactiveTsx ? runtimeExceptions!.errorsForFrame(artifactFrame.frameId) : [],
|
|
330
|
+
input.artifactType === "tsx",
|
|
330
331
|
);
|
|
331
332
|
const secondObservation = interactiveTsx
|
|
332
333
|
? await waitForStabilityObservation(
|
|
@@ -334,6 +335,7 @@ async function runTier1Attempt(
|
|
|
334
335
|
artifactFrame,
|
|
335
336
|
isolated.executionContextId,
|
|
336
337
|
() => runtimeExceptions!.errorsForFrame(artifactFrame.frameId),
|
|
338
|
+
true,
|
|
337
339
|
)
|
|
338
340
|
: firstObservation;
|
|
339
341
|
const protocolObservation = secondObservation.protocol;
|
|
@@ -350,6 +352,7 @@ async function runTier1Attempt(
|
|
|
350
352
|
bootReady: shim.bootReady,
|
|
351
353
|
renderComplete: shim.renderComplete,
|
|
352
354
|
interactive: interactiveTsx,
|
|
355
|
+
tsx: input.artifactType === "tsx",
|
|
353
356
|
channelDivergence,
|
|
354
357
|
structureChanged:
|
|
355
358
|
interactiveTsx && countsDiffer(firstObservation.protocol, secondObservation.protocol),
|
|
@@ -390,6 +393,9 @@ async function runTier1Attempt(
|
|
|
390
393
|
errorCount: observed.errorCount,
|
|
391
394
|
opaqueRegionCount: observed.opaqueRegionCount,
|
|
392
395
|
externalImageCount: observed.externalImageCount,
|
|
396
|
+
...(observed.emptyRendererRoot === undefined
|
|
397
|
+
? {}
|
|
398
|
+
: { emptyRendererRoot: observed.emptyRendererRoot }),
|
|
393
399
|
...(observed.html === undefined ? {} : { html: observed.html }),
|
|
394
400
|
discriminativeErrors: observed.discriminativeErrors,
|
|
395
401
|
},
|
|
@@ -604,6 +610,12 @@ function mergeProtocol(
|
|
|
604
610
|
message: `DOMSnapshot.externalImageCount=${snapshot.externalImageCount} vs DOM.getDocument.externalImageCount=${getDocument.externalImageCount}`,
|
|
605
611
|
});
|
|
606
612
|
}
|
|
613
|
+
if (snapshot.emptyRendererRoot !== getDocument.emptyRendererRoot) {
|
|
614
|
+
errors.push({
|
|
615
|
+
code: "protocol_divergence",
|
|
616
|
+
message: `DOMSnapshot.emptyRendererRoot=${snapshot.emptyRendererRoot} vs DOM.getDocument.emptyRendererRoot=${getDocument.emptyRendererRoot}`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
607
619
|
const observedCounts = Object.fromEntries(
|
|
608
620
|
OBSERVED_COUNT_KEYS.map((key) => [key, snapshot[key]]),
|
|
609
621
|
) as Pick<ProtocolObservation, ObservedCountKey>;
|
|
@@ -612,6 +624,9 @@ function mergeProtocol(
|
|
|
612
624
|
viewBoxes: snapshot.viewBoxes,
|
|
613
625
|
errorCount: snapshot.errorCount,
|
|
614
626
|
...(snapshot.html === undefined ? {} : { html: snapshot.html }),
|
|
627
|
+
...(snapshot.emptyRendererRoot === undefined
|
|
628
|
+
? {}
|
|
629
|
+
: { emptyRendererRoot: snapshot.emptyRendererRoot }),
|
|
615
630
|
discriminativeErrors: errors,
|
|
616
631
|
};
|
|
617
632
|
}
|
|
@@ -717,10 +732,11 @@ async function observeArtifact(
|
|
|
717
732
|
frame: { readonly frameId: string; readonly url: string },
|
|
718
733
|
executionContextId: number,
|
|
719
734
|
runtimeErrors: readonly { readonly code: string; readonly message: string }[],
|
|
735
|
+
observeContent = false,
|
|
720
736
|
): Promise<ArtifactObservation> {
|
|
721
|
-
const snapshot = await probeProtocolSnapshot(session, frame);
|
|
722
|
-
const document = await probeProtocolGetDocument(session, frame);
|
|
723
|
-
const isolated = await probeIsolatedCounts(session, executionContextId);
|
|
737
|
+
const snapshot = await probeProtocolSnapshot(session, frame, observeContent);
|
|
738
|
+
const document = await probeProtocolGetDocument(session, frame, observeContent);
|
|
739
|
+
const isolated = await probeIsolatedCounts(session, executionContextId, observeContent);
|
|
724
740
|
return { protocol: mergeProtocol(snapshot, document, runtimeErrors), isolated };
|
|
725
741
|
}
|
|
726
742
|
|
|
@@ -729,9 +745,10 @@ async function waitForStabilityObservation(
|
|
|
729
745
|
frame: { readonly frameId: string; readonly url: string },
|
|
730
746
|
executionContextId: number,
|
|
731
747
|
runtimeErrors: () => readonly { readonly code: string; readonly message: string }[],
|
|
748
|
+
observeContent = false,
|
|
732
749
|
): Promise<ArtifactObservation> {
|
|
733
750
|
await Bun.sleep(TSX_STABILITY_WINDOW_MS);
|
|
734
|
-
return observeArtifact(session, frame, executionContextId, runtimeErrors());
|
|
751
|
+
return observeArtifact(session, frame, executionContextId, runtimeErrors(), observeContent);
|
|
735
752
|
}
|
|
736
753
|
|
|
737
754
|
interface EvidenceCapture {
|
|
@@ -44,12 +44,12 @@ export type { ProtocolObservation };
|
|
|
44
44
|
*/
|
|
45
45
|
export type PageShim = Pick<
|
|
46
46
|
ProtocolObservation,
|
|
47
|
-
(typeof COUNT_COMPARISON_KEYS)[number] | "html" | "errorCount"
|
|
47
|
+
(typeof COUNT_COMPARISON_KEYS)[number] | "html" | "errorCount" | "emptyRendererRoot"
|
|
48
48
|
>;
|
|
49
49
|
|
|
50
50
|
export type CountsLike = Pick<
|
|
51
51
|
ProtocolObservation,
|
|
52
|
-
(typeof COUNT_COMPARISON_KEYS)[number] | "html" | "errorCount"
|
|
52
|
+
(typeof COUNT_COMPARISON_KEYS)[number] | "html" | "errorCount" | "emptyRendererRoot"
|
|
53
53
|
>;
|
|
54
54
|
|
|
55
55
|
const COUNT_COMPARISON_KEYS = OBSERVED_COUNT_KEYS;
|
|
@@ -88,47 +88,21 @@ export interface LifecycleSummary {
|
|
|
88
88
|
readonly channelDivergence?: boolean;
|
|
89
89
|
/** Interactive TSX has no lexical HTML prediction and no trusted outer shim. */
|
|
90
90
|
readonly interactive?: boolean;
|
|
91
|
+
/** Only TSX has a renderer-root emptiness claim from Tier 1. */
|
|
92
|
+
readonly tsx?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
|
-
* Compute the final `RenderStatus`.
|
|
96
|
+
* Compute the final `RenderStatus`. Precedence, highest first:
|
|
97
|
+
* timeout → channel divergence/tampered → missing channels → protocol
|
|
98
|
+
* errors or lexical mismatch → missing TSX content observation →
|
|
99
|
+
* unstable → empty TSX root → missing declared opaque content/error →
|
|
100
|
+
* opaque content → external resources → unobservable SVG layout → ok.
|
|
95
101
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* 5. Channel availability: only shim missing → `probe_only`
|
|
101
|
-
* 6. Channel availability: only isolated missing → `shim_only`
|
|
102
|
-
* 7. Opaque content: expected > 0 but protocol observed 0 → `error`
|
|
103
|
-
* 8. Opaque content: protocol observed > 0 → `partial:opaque_content`
|
|
104
|
-
* 9. External resources: expected external images > 0 → `partial:external_resources`
|
|
105
|
-
* 10. Layout observability (non-HTML only): protocol visibleSvgCount === 0
|
|
106
|
-
* AND every viewBox is zeroed → `partial:layout_unverified`. The
|
|
107
|
-
* branch is gated on `expected.html === undefined` because HTML
|
|
108
|
-
* artifacts carry no viewBox axis — they have no SVG layout to
|
|
109
|
-
* verify against, so `partial:layout_unverified` is structurally
|
|
110
|
-
* unreachable for HTML. A clean HTML artifact with zero
|
|
111
|
-
* `visibleSvgCount` and zero `viewBoxes` returns `ok` when its
|
|
112
|
-
* counts match.
|
|
113
|
-
* 11. Counts: protocol discriminativeErrors non-empty → `error`
|
|
114
|
-
* 12. Counts: protocol observed !== expected lexical → `error`
|
|
115
|
-
* 13. Otherwise → `ok`
|
|
116
|
-
*
|
|
117
|
-
* Tampered wins over partial: a forge attempt that hides layout
|
|
118
|
-
* observability (no viewBoxes) is still a forge attempt.
|
|
119
|
-
*
|
|
120
|
-
* D11 addition: `partial:unstable` slots between step 6 and step 7,
|
|
121
|
-
* i.e. AFTER the catastrophic statuses (timeout, tampered, channel
|
|
122
|
-
* availability) and the count-mismatch error, but BEFORE the
|
|
123
|
-
* single-snapshot partials (opaque_content, external_resources,
|
|
124
|
-
* layout_unverified). The reasoning: when structure is changing
|
|
125
|
-
* between the two observation snapshots, the verifier cannot
|
|
126
|
-
* honestly claim "this artifact has structure X" — every
|
|
127
|
-
* single-snapshot claim is moot. Unstable is a meta-claim about the
|
|
128
|
-
* page's runtime behavior that dominates the structural claims.
|
|
129
|
-
* Tampered stays above it because channel divergence (the page
|
|
130
|
-
* contradicting protocol authority) is the more catastrophic
|
|
131
|
-
* reading of the page's behavior.
|
|
102
|
+
* A changing page cannot support any single-snapshot content claim,
|
|
103
|
+
* including emptiness. HTML/TSX has no SVG viewBox axis, while TSX
|
|
104
|
+
* emptiness needs agreement from the two protocol paths and isolated
|
|
105
|
+
* world; the outer page shim has no authority to make that claim.
|
|
132
106
|
*/
|
|
133
107
|
export function deriveVerdict(
|
|
134
108
|
expected: LexicalCounters,
|
|
@@ -164,11 +138,19 @@ export function deriveVerdict(
|
|
|
164
138
|
|
|
165
139
|
if (protocolObservation.discriminativeErrors.length > 0) return "error";
|
|
166
140
|
if (!lifecycle.interactive && !matchesExpected(expected, protocolObservation)) return "error";
|
|
141
|
+
if (
|
|
142
|
+
lifecycle.tsx &&
|
|
143
|
+
(protocolObservation.emptyRendererRoot === undefined ||
|
|
144
|
+
isolatedObservation?.emptyRendererRoot === undefined)
|
|
145
|
+
)
|
|
146
|
+
return "probe_only";
|
|
167
147
|
// D11: structure changed between the barrier and the stability
|
|
168
148
|
// window. This is the only path that does not also depend on a
|
|
169
149
|
// single observation — it depends on TWO observations, so it
|
|
170
150
|
// dominates the single-snapshot partial statuses below.
|
|
171
151
|
if (lifecycle.structureChanged === true) return "partial:unstable";
|
|
152
|
+
if (lifecycle.tsx && protocolObservation.emptyRendererRoot === true)
|
|
153
|
+
return "partial:empty_render";
|
|
172
154
|
if (expected.opaqueRegionCount > 0 && protocolObservation.opaqueRegionCount === 0) {
|
|
173
155
|
return "error";
|
|
174
156
|
}
|
|
@@ -224,6 +206,9 @@ function htmlCountsDiffer(
|
|
|
224
206
|
export function countsDiffer(left: CountsLike, right: CountsLike): boolean {
|
|
225
207
|
return (
|
|
226
208
|
COUNT_COMPARISON_KEYS.some((key) => left[key] !== right[key]) ||
|
|
209
|
+
(left.emptyRendererRoot !== undefined &&
|
|
210
|
+
right.emptyRendererRoot !== undefined &&
|
|
211
|
+
left.emptyRendererRoot !== right.emptyRendererRoot) ||
|
|
227
212
|
left.errorCount !== right.errorCount ||
|
|
228
213
|
htmlCountsDiffer(left.html, right.html)
|
|
229
214
|
);
|
|
@@ -16,7 +16,7 @@ export default function CapacityReport() {
|
|
|
16
16
|
const capacityPercent = Math.round((readyWorkers / workerCapacity) * 100);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<main className="max-w-2xl p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
|
|
19
|
+
<main className="max-w-2xl mx-auto p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
|
|
20
20
|
<header className="flex flex-col gap-2">
|
|
21
21
|
<div className="flex flex-wrap gap-2">
|
|
22
22
|
<span className="badge">capacity report</span>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<title>Fleet dashboard</title>
|
|
5
5
|
</head>
|
|
6
6
|
<body class="bg-legion-paper text-legion-ink leading-relaxed">
|
|
7
|
-
<main class="max-w-2xl p-6 gap-6 flex flex-col">
|
|
7
|
+
<main class="max-w-2xl mx-auto p-6 gap-6 flex flex-col">
|
|
8
8
|
<header class="flex flex-col gap-2">
|
|
9
9
|
<div class="flex flex-wrap gap-2 items-center">
|
|
10
10
|
<span class="badge">operations</span>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<title>Release ledger</title>
|
|
5
5
|
</head>
|
|
6
6
|
<body class="bg-legion-paper text-legion-ink leading-relaxed">
|
|
7
|
-
<main class="max-w-2xl p-6 gap-6 flex flex-col">
|
|
7
|
+
<main class="max-w-2xl mx-auto p-6 gap-6 flex flex-col">
|
|
8
8
|
<header class="flex flex-col gap-2">
|
|
9
9
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
10
10
|
<span class="badge">release ledger</span>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<title>Status report</title>
|
|
21
21
|
</head>
|
|
22
22
|
<body class="bg-legion-paper text-legion-ink leading-relaxed">
|
|
23
|
-
<main class="max-w-2xl p-6 gap-4 flex flex-col">
|
|
23
|
+
<main class="max-w-2xl mx-auto p-6 gap-4 flex flex-col">
|
|
24
24
|
<header class="flex flex-col gap-2">
|
|
25
25
|
<h1 class="text-2xl font-bold">Status report</h1>
|
|
26
26
|
<p class="text-legion-muted text-sm">
|
|
@@ -44,7 +44,7 @@ export default function IncidentConsole() {
|
|
|
44
44
|
const criticalCount = incidents.filter((incident) => incident.severity === "critical").length;
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
<main className="max-w-2xl p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
|
|
47
|
+
<main className="max-w-2xl mx-auto p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
|
|
48
48
|
<header className="flex flex-col gap-2">
|
|
49
49
|
<div className="flex flex-wrap gap-2 items-center">
|
|
50
50
|
<span className="badge">incident console</span>
|
|
@@ -4,7 +4,7 @@ export default function InteractiveCounter() {
|
|
|
4
4
|
const [count, setCount] = useState(0);
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
|
-
<section className="card bg-legion-paper p-6 w-full max-w-prose">
|
|
7
|
+
<section className="card bg-legion-paper p-6 w-full max-w-prose mx-auto">
|
|
8
8
|
<h1 className="text-2xl font-semibold text-legion-ink">Interactive counter</h1>
|
|
9
9
|
<p className="text-legion-muted mt-2">Button presses: {count}</p>
|
|
10
10
|
<button
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
export default function StatusReport() {
|
|
4
4
|
return (
|
|
5
|
-
<section className="card bg-legion-paper p-6 w-full max-w-prose">
|
|
5
|
+
<section className="card bg-legion-paper p-6 w-full max-w-prose mx-auto">
|
|
6
6
|
<h1 className="text-2xl font-semibold text-legion-ink">Status report</h1>
|
|
7
7
|
<p className="text-legion-muted mt-2">Verification state for the current release.</p>
|
|
8
8
|
<section className="mt-4">
|