@legionworks/facet 1.10.0 → 1.10.2
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/dist/gallery/{chunk-48emrxg6.js → chunk-0by8qn4x.js} +15 -0
- package/dist/gallery/frame/artifact.css +3 -0
- package/dist/gallery/frame/chunks/{markdown-zazx9h4n.js → markdown-31ee7zgx.js} +1 -1
- package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-7scxp44j.js} +9 -1
- package/dist/gallery/frame/chunks/{markdown-z1wc0v0d.js → markdown-mksxbxx8.js} +2 -2
- package/dist/gallery/frame/chunks/{markdown-d1wfbs70.js → markdown-n9xcxy80.js} +14 -9
- package/dist/gallery/frame/chunks/{markdown-rd8w0ng9.js → markdown-rkt612xp.js} +1 -1
- package/dist/gallery/frame/chunks/{mermaid-de7p4v0x.js → mermaid-b8fxhsgg.js} +3 -3
- package/dist/gallery/frame/frame.css +4 -24
- package/dist/gallery/frame/runtime/chart.js +3 -3
- package/dist/gallery/frame/runtime/html.js +3 -3
- package/dist/gallery/frame/runtime/markdown.js +8 -4
- package/dist/gallery/frame/runtime/mermaid.js +4 -4
- package/dist/gallery/frame/runtime/svg.js +3 -3
- package/dist/gallery/frame/runtime/tsx.js +3 -3
- package/dist/gallery/index.html +1 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/html.md +3 -3
- package/docs/reference/mcp.md +15 -2
- package/docs/reference/tsx.md +3 -1
- package/docs/reference/validation.md +12 -4
- package/package.json +1 -1
- package/skills/facet/SKILL.md +3 -4
- package/src/gallery-web/app.ts +20 -1
- package/src/gallery-web/frame/renderers/markdown.ts +5 -1
- package/src/gallery-web/frame/renderers/registry.ts +17 -10
- 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 +4 -24
- package/src/gallery-web/frame-html.ts +5 -1
- package/src/harness-adapters/mcp/server.ts +2 -2
- package/src/service/dispatcher.ts +12 -0
- package/src/shared/html/policy.ts +9 -0
- package/src/shared/html/style-vocabulary.ts +1 -0
- package/src/validation/tier0/html.ts +6 -9
- package/src/validation/tier0/markdown.ts +2 -12
- package/src/validation/tier1/isolated-probe.ts +20 -11
- package/src/validation/tier1/limits.ts +11 -6
- package/src/validation/tier1/protocol-probe.ts +60 -26
- package/src/validation/tier1/runner.ts +173 -10
- package/src/validation/tier1/verdict.ts +9 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ArtifactType } from "../../../shared/contracts/artifact-types";
|
|
2
2
|
import { isRenderer, type Renderer as RendererKind } from "../../../shared/contracts/renderers";
|
|
3
|
-
import { HTML_STRUCTURAL_GROUPS } from "../../../shared/html/policy";
|
|
3
|
+
import { HTML_STRUCTURAL_GROUPS, isExternalHttpsImageSource } from "../../../shared/html/policy";
|
|
4
4
|
import type { VerdictObserved } from "../../../shared/contracts/validation";
|
|
5
5
|
import type { ObservedCountKey } from "../../../shared/contracts/observed-counts";
|
|
6
6
|
import { isTsxExecutionMode, type TsxExecutionMode } from "../../../shared/tsx/execution";
|
|
@@ -140,9 +140,12 @@ export function countPageShim(): PageShimCounts {
|
|
|
140
140
|
const graphRoots = roots.filter((root) => root.getAttribute(RENDERER_GRAPH_ATTRIBUTE) === "true");
|
|
141
141
|
const allMarkedCandidates = safeSelectorElements(MARKED_ROOT_SELECTOR);
|
|
142
142
|
const markedSet = new Set(allMarkedCandidates);
|
|
143
|
-
const
|
|
143
|
+
const contentRoots = allMarkedCandidates.filter(
|
|
144
144
|
(root) => root.nodeName.toLowerCase() !== "svg" && !hasMarkedRootAncestor(root, markedSet),
|
|
145
145
|
);
|
|
146
|
+
const htmlRoots = contentRoots.filter(
|
|
147
|
+
(root) => root.getAttribute("data-facet-renderer-kind") !== "markdown",
|
|
148
|
+
);
|
|
146
149
|
const html =
|
|
147
150
|
htmlRoots.length === 0
|
|
148
151
|
? undefined
|
|
@@ -170,13 +173,9 @@ export function countPageShim(): PageShimCounts {
|
|
|
170
173
|
).length;
|
|
171
174
|
const images = safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(","));
|
|
172
175
|
html!.imageCount += images.length;
|
|
173
|
-
html!.externalImageCount += images.filter((image) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} catch {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
}).length;
|
|
176
|
+
html!.externalImageCount += images.filter((image) =>
|
|
177
|
+
isExternalHttpsImageSource(image.getAttribute("src")),
|
|
178
|
+
).length;
|
|
180
179
|
html!.canvasCount += safeSelectorElementsWithin(
|
|
181
180
|
root,
|
|
182
181
|
HTML_STRUCTURAL_GROUPS.canvases.join(","),
|
|
@@ -192,13 +191,21 @@ export function countPageShim(): PageShimCounts {
|
|
|
192
191
|
const opaqueRegionCount = safeSelectorElements("*").filter(
|
|
193
192
|
(element) => element.nodeName.toLowerCase() === "canvas",
|
|
194
193
|
).length;
|
|
194
|
+
const externalImageCount = contentRoots.reduce(
|
|
195
|
+
(count, root) =>
|
|
196
|
+
count +
|
|
197
|
+
safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(",")).filter((image) =>
|
|
198
|
+
isExternalHttpsImageSource(image.getAttribute("src")),
|
|
199
|
+
).length,
|
|
200
|
+
0,
|
|
201
|
+
);
|
|
195
202
|
return {
|
|
196
203
|
rendererRootSvgCount: roots.length,
|
|
197
204
|
graphCount: graphRoots.length,
|
|
198
205
|
mermaidNodeCount,
|
|
199
206
|
visibleSvgCount: roots.filter(nonDegenerateViewBox).length,
|
|
200
207
|
opaqueRegionCount,
|
|
201
|
-
externalImageCount
|
|
208
|
+
externalImageCount,
|
|
202
209
|
errorCount: safeSelectorElements("[data-facet-error]").length,
|
|
203
210
|
...(html === undefined ? {} : { html }),
|
|
204
211
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isTsxExecutionMode, type TsxExecutionMode } from "../../shared/tsx/execution";
|
|
2
|
-
import { isResolvedGalleryTheme } from "../theme";
|
|
2
|
+
import { galleryDataTheme, isResolvedGalleryTheme, type ResolvedGalleryTheme } from "../theme";
|
|
3
3
|
|
|
4
4
|
import { validateRenderer } from "./renderer-validation";
|
|
5
5
|
import {
|
|
@@ -80,6 +80,7 @@ interface DiagramRegionGestures {
|
|
|
80
80
|
|
|
81
81
|
export interface GalleryFrameApi {
|
|
82
82
|
render(payload: FrameRenderPayload): Promise<RenderResult>;
|
|
83
|
+
setTheme(theme: ResolvedGalleryTheme): void;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
declare global {
|
|
@@ -410,6 +411,11 @@ export function installGalleryFrameApi(registry: RendererRegistry): void {
|
|
|
410
411
|
};
|
|
411
412
|
|
|
412
413
|
const api: GalleryFrameApi = {
|
|
414
|
+
setTheme(theme) {
|
|
415
|
+
if (!isResolvedGalleryTheme(theme))
|
|
416
|
+
throw new FacetRenderError("frame theme is invalid", "invalid_request");
|
|
417
|
+
document.documentElement.dataset.theme = galleryDataTheme(theme);
|
|
418
|
+
},
|
|
413
419
|
async render(payload: FrameRenderPayload): Promise<RenderResult> {
|
|
414
420
|
if (rendered) throw new FacetRenderError("frame already rendered", "invalid_request");
|
|
415
421
|
const validated = validatePayload(payload);
|
|
@@ -78,26 +78,7 @@ body {
|
|
|
78
78
|
padding-inline: max(16px, calc((100% - 92ch) / 2));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/*
|
|
82
|
-
* Native dashboard artifacts (html, interactive tsx) read top-down
|
|
83
|
-
* like markdown — vertical stays start-aligned — but unlike markdown
|
|
84
|
-
* their width is whatever the fixture authored (often a fixed-width
|
|
85
|
-
* `max-w-*` container), not a fluid readable column. Left unstyled,
|
|
86
|
-
* that narrow content sits pinned to the container's left edge,
|
|
87
|
-
* stranding most of a wide desktop stage as dead space.
|
|
88
|
-
*
|
|
89
|
-
* The centering has to land on the renderer-ROOT element
|
|
90
|
-
* (`.facet-html-root` / `#facet-tsx-mount`), not `#artifact` itself:
|
|
91
|
-
* `renderHtml`/`renderTsx` always mount a single full-width wrapper
|
|
92
|
-
* as `#artifact`'s only child, and the fixture's own narrow content
|
|
93
|
-
* (e.g. `<main class="max-w-2xl">`) is a GRANDCHILD of `#artifact`
|
|
94
|
-
* inside that wrapper — centering `#artifact`'s single full-width
|
|
95
|
-
* child does nothing. `safe center` on the horizontal axis only —
|
|
96
|
-
* the same fallback-to-start mechanism the standalone-diagram rule
|
|
97
|
-
* above uses — centers content that fits and never clips content
|
|
98
|
-
* wider than the stage (it start-aligns and stays reachable by
|
|
99
|
-
* scroll instead).
|
|
100
|
-
*/
|
|
81
|
+
/* These selectors match usesArtifactStylesheet in frame-html.ts; CSS cannot import it. */
|
|
101
82
|
#artifact[data-facet-artifact-type="html"],
|
|
102
83
|
#artifact[data-facet-artifact-type="tsx"] {
|
|
103
84
|
display: flex;
|
|
@@ -106,10 +87,9 @@ body {
|
|
|
106
87
|
|
|
107
88
|
#artifact[data-facet-artifact-type="html"] > [data-facet-renderer-root],
|
|
108
89
|
#artifact[data-facet-artifact-type="tsx"] > [data-facet-renderer-root] {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/* A full-height root plus a sibling runtime error makes capture bounds grow on every viewport resize. */
|
|
90
|
+
/* Block flow preserves authored sibling order while keeping the renderer root a flex item of the stage. */
|
|
91
|
+
display: block;
|
|
92
|
+
/* Filling the stage keeps a sibling runtime-error line outside capture bounds as the viewport resizes. */
|
|
113
93
|
flex: 1 0 auto;
|
|
114
94
|
}
|
|
115
95
|
|
|
@@ -31,6 +31,10 @@ export function buildFrameAttributes(src = "/gallery/frame"): FrameAttributes {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function usesArtifactStylesheet(artifactType: string): boolean {
|
|
35
|
+
return artifactType === "html" || artifactType === "tsx";
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
export function buildFrameDocument(options: {
|
|
35
39
|
artifactType: string;
|
|
36
40
|
runtimeUrl: string;
|
|
@@ -45,7 +49,7 @@ export function buildFrameDocument(options: {
|
|
|
45
49
|
return (
|
|
46
50
|
`<!doctype html><html data-theme="${galleryDataTheme(theme)}"><head>` +
|
|
47
51
|
`<meta charset="utf-8"><link rel="stylesheet" href="/gallery/frame/frame.css">` +
|
|
48
|
-
(artifactType
|
|
52
|
+
(usesArtifactStylesheet(artifactType)
|
|
49
53
|
? `<link rel="stylesheet" href="/gallery/frame/artifact.css">`
|
|
50
54
|
: "") +
|
|
51
55
|
"</head><body>" +
|
|
@@ -145,7 +145,7 @@ export function createFacetMcpServer(): Server {
|
|
|
145
145
|
),
|
|
146
146
|
facet_publish: defineTool(
|
|
147
147
|
PublishToolSchema,
|
|
148
|
-
|
|
148
|
+
'Publish exactly one sourceText or local file. Check envelope.ok separately from data.verdict.status: a stored verdict status of error is not a transport failure. Tier 0 parses and compiles structure but is not rendered; call facet_read_back with tier: "visual" before reporting a render.',
|
|
149
149
|
async (input) => {
|
|
150
150
|
const source = publishSource(input);
|
|
151
151
|
return invoke(buildFacetArgs("publish", { ...input, ...source }), source);
|
|
@@ -153,7 +153,7 @@ export function createFacetMcpServer(): Server {
|
|
|
153
153
|
),
|
|
154
154
|
facet_read_back: defineTool(
|
|
155
155
|
ReadBackToolSchema,
|
|
156
|
-
"Read back
|
|
156
|
+
"Read back a stored verdict at Tier 0, Tier 1, or visual. Tier 1 and visual need browser evidence from the pinned browser; inspect envelope.ok before verdict status. Visual covers the first rendered state; later interaction is not validated and does not change the stored verdict.",
|
|
157
157
|
async (input) => invoke(buildFacetArgs("read_back", input)),
|
|
158
158
|
),
|
|
159
159
|
facet_status: defineTool(
|
|
@@ -526,6 +526,12 @@ export async function dispatch(
|
|
|
526
526
|
? deps.repository.getLatestRevision(command.artifactId)
|
|
527
527
|
: deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
|
|
528
528
|
if (revision === null) {
|
|
529
|
+
if (deps.repository.getArtifactById(command.artifactId) === null) {
|
|
530
|
+
throw new FacetError("artifact_not_found", "Artifact not found", {
|
|
531
|
+
retryable: false,
|
|
532
|
+
details: { artifactId: command.artifactId },
|
|
533
|
+
});
|
|
534
|
+
}
|
|
529
535
|
throw new FacetError("revision_not_found", "Revision not found", {
|
|
530
536
|
retryable: false,
|
|
531
537
|
details: {
|
|
@@ -614,6 +620,12 @@ export async function dispatch(
|
|
|
614
620
|
? deps.repository.getLatestRevision(command.artifactId)
|
|
615
621
|
: deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
|
|
616
622
|
if (revision === null) {
|
|
623
|
+
if (deps.repository.getArtifactById(command.artifactId) === null) {
|
|
624
|
+
throw new FacetError("artifact_not_found", "Artifact not found", {
|
|
625
|
+
retryable: false,
|
|
626
|
+
details: { artifactId: command.artifactId },
|
|
627
|
+
});
|
|
628
|
+
}
|
|
617
629
|
throw new FacetError("revision_not_found", "Revision not found", {
|
|
618
630
|
retryable: false,
|
|
619
631
|
details: {
|
|
@@ -61,6 +61,15 @@ export function isHtmlInlineStyleAttribute(name: string): boolean {
|
|
|
61
61
|
return name.toLowerCase() === "style";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function isExternalHttpsImageSource(src: string | null | undefined): boolean {
|
|
65
|
+
if (typeof src !== "string") return false;
|
|
66
|
+
try {
|
|
67
|
+
return new URL(src).protocol === "https:";
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
export function isAllowedHtmlUrl(
|
|
65
74
|
elementName: string,
|
|
66
75
|
attributeName: string,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isHtmlInlineStyleAttribute,
|
|
11
11
|
isHtmlUrlAttributeName,
|
|
12
12
|
isHtmlUrlBearingAttribute,
|
|
13
|
+
isExternalHttpsImageSource,
|
|
13
14
|
} from "../../shared/html/policy";
|
|
14
15
|
|
|
15
16
|
export interface HtmlParseOk {
|
|
@@ -131,14 +132,6 @@ function templateContent(node: unknown): unknown | null {
|
|
|
131
132
|
return isRecord(node) && isRecord(node.content) ? node.content : null;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
function isHttpsUrl(value: string): boolean {
|
|
135
|
-
try {
|
|
136
|
-
return new URL(value.trim()).protocol.toLowerCase() === "https:";
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
135
|
function isWhitespace(character: string): boolean {
|
|
143
136
|
return (
|
|
144
137
|
character === " " ||
|
|
@@ -190,7 +183,11 @@ function validateUrl(
|
|
|
190
183
|
);
|
|
191
184
|
continue;
|
|
192
185
|
}
|
|
193
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
countStructure &&
|
|
188
|
+
(tagName === "img" || tagName === "source") &&
|
|
189
|
+
isExternalHttpsImageSource(candidate)
|
|
190
|
+
) {
|
|
194
191
|
counts.externalImageCount += 1;
|
|
195
192
|
}
|
|
196
193
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { Lexer, type Token, type Tokens } from "marked";
|
|
30
30
|
|
|
31
31
|
import type { DiscriminativeError, VerdictObserved } from "../../shared/contracts/validation";
|
|
32
|
+
import { isExternalHttpsImageSource } from "../../shared/html/policy";
|
|
32
33
|
import { countMermaidNodeDeclarations } from "../../shared/util/mermaid-nodes";
|
|
33
34
|
import { parseMermaidText } from "./mermaid";
|
|
34
35
|
|
|
@@ -58,17 +59,6 @@ interface MarkdownCounts {
|
|
|
58
59
|
mermaidBodies: string[];
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function isExternalHttpsUrl(value: string): boolean {
|
|
62
|
-
// The URL constructor canonicalizes the protocol; malformed URLs
|
|
63
|
-
// (e.g. `https://[`) return false here rather than throwing, so the
|
|
64
|
-
// token walk surfaces a typed zero instead of crashing Tier 0.
|
|
65
|
-
try {
|
|
66
|
-
return new URL(value.trim()).protocol.toLowerCase() === "https:";
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
62
|
/**
|
|
73
63
|
* Walk the marked token tree and tally the surfaces that matter for
|
|
74
64
|
* the Tier 0 verdict: fenced blocks, mermaid blocks, raw HTML blocks,
|
|
@@ -122,7 +112,7 @@ function walkTokens(tokens: Token[], counts: MarkdownCounts): void {
|
|
|
122
112
|
if (/\b(?:href|src)\s*=\s*["']https?:/i.test(raw)) counts.hasExternalRef = true;
|
|
123
113
|
} else if (token.type === "image") {
|
|
124
114
|
const image = token as Tokens.Image;
|
|
125
|
-
if (
|
|
115
|
+
if (isExternalHttpsImageSource(image.href)) counts.externalImageCount += 1;
|
|
126
116
|
}
|
|
127
117
|
// Recurse every container Marked actually emits with children.
|
|
128
118
|
const recurseTokens = (sub: Token[] | undefined): void => {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { ProtocolObservation } from "../../shared/contracts/validation";
|
|
22
|
-
import { HTML_STRUCTURAL_GROUPS } from "../../shared/html/policy";
|
|
22
|
+
import { HTML_STRUCTURAL_GROUPS, isExternalHttpsImageSource } from "../../shared/html/policy";
|
|
23
23
|
|
|
24
24
|
import type { VerifierCdpSession } from "./browser-process";
|
|
25
25
|
|
|
@@ -32,27 +32,37 @@ export async function probeIsolatedCounts(
|
|
|
32
32
|
const selectors = Object.fromEntries(
|
|
33
33
|
Object.entries(HTML_STRUCTURAL_GROUPS).map(([group, names]) => [group, names.join(",")]),
|
|
34
34
|
);
|
|
35
|
+
const externalHttpsImageSource = isExternalHttpsImageSource.toString();
|
|
35
36
|
const result = (await session.send("Runtime.evaluate", {
|
|
36
37
|
contextId: executionContextId,
|
|
37
38
|
returnByValue: true,
|
|
38
39
|
expression: [
|
|
39
40
|
"(function(){",
|
|
40
41
|
" var candidates = Array.prototype.slice.call(document.querySelectorAll('[data-facet-renderer-root=\"true\"]'));",
|
|
41
|
-
" var
|
|
42
|
-
" var
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
" }",
|
|
42
|
+
" var svgCandidates = candidates.filter(function(root){ return String(root.nodeName).toLowerCase() === 'svg'; });",
|
|
43
|
+
" var svgCandidateSet = new Set(svgCandidates);",
|
|
44
|
+
" var svgRoots = svgCandidates.filter(function(root){",
|
|
45
|
+
" for (var parent = root.parentElement; parent; parent = parent.parentElement) if (svgCandidateSet.has(parent)) return false;",
|
|
46
46
|
" return true;",
|
|
47
47
|
" });",
|
|
48
|
-
" var
|
|
48
|
+
" var contentCandidates = candidates.filter(function(root){ return String(root.nodeName).toLowerCase() !== 'svg'; });",
|
|
49
|
+
" var contentCandidateSet = new Set(contentCandidates);",
|
|
50
|
+
" var contentRoots = contentCandidates.filter(function(root){",
|
|
51
|
+
" for (var parent = root.parentElement; parent; parent = parent.parentElement) if (contentCandidateSet.has(parent)) return false;",
|
|
52
|
+
" return true;",
|
|
53
|
+
" });",
|
|
54
|
+
" var htmlRoots = contentRoots.filter(function(root){ return root.getAttribute('data-facet-renderer-kind') !== 'markdown'; });",
|
|
49
55
|
" var graphRoots = svgRoots.filter(function(root){ return root.getAttribute('data-facet-renderer-graph') === 'true'; });",
|
|
50
|
-
" var htmlRoots = roots.filter(function(root){ return String(root.nodeName).toLowerCase() !== 'svg'; });",
|
|
51
56
|
` var observeContent = ${JSON.stringify(observeContent)};`,
|
|
52
|
-
|
|
57
|
+
` var isExternalHttpsImageSource = (${externalHttpsImageSource});`,
|
|
58
|
+
" var emptyRendererRoot = observeContent && contentRoots.length === 1 ? Array.prototype.every.call(contentRoots[0].childNodes, function(node){ return node.nodeType !== 1 && (node.nodeType !== 3 || !node.textContent.trim()); }) : undefined;",
|
|
53
59
|
` var selectors = ${JSON.stringify(selectors)};`,
|
|
54
60
|
" var html = htmlRoots.length === 0 ? null : {rendererRootCount:htmlRoots.length,headingCount:0,tableCount:0,listCount:0,imageCount:0,canvasCount:0,externalImageCount:0};",
|
|
55
61
|
" var externalImageCount = 0;",
|
|
62
|
+
" for (var c = 0; c < contentRoots.length; c++) {",
|
|
63
|
+
" var contentImages = Array.prototype.slice.call(contentRoots[c].querySelectorAll(selectors.images));",
|
|
64
|
+
" externalImageCount += contentImages.filter(function(image){ return isExternalHttpsImageSource(image.getAttribute('src')); }).length;",
|
|
65
|
+
" }",
|
|
56
66
|
" for (var h = 0; h < htmlRoots.length; h++) {",
|
|
57
67
|
" var htmlRoot = htmlRoots[h];",
|
|
58
68
|
" html.headingCount += htmlRoot.querySelectorAll(selectors.headings).length;",
|
|
@@ -60,9 +70,8 @@ export async function probeIsolatedCounts(
|
|
|
60
70
|
" html.listCount += htmlRoot.querySelectorAll(selectors.lists).length;",
|
|
61
71
|
" var images = Array.prototype.slice.call(htmlRoot.querySelectorAll(selectors.images));",
|
|
62
72
|
" html.imageCount += images.length;",
|
|
63
|
-
" var rootExternal = images.filter(function(image){
|
|
73
|
+
" var rootExternal = images.filter(function(image){ return isExternalHttpsImageSource(image.getAttribute('src')); }).length;",
|
|
64
74
|
" html.externalImageCount += rootExternal;",
|
|
65
|
-
" externalImageCount += rootExternal;",
|
|
66
75
|
" html.canvasCount += htmlRoot.querySelectorAll(selectors.canvases).length;",
|
|
67
76
|
" }",
|
|
68
77
|
" var nodeCount = 0;",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resource caps for the Tier 1 verifier.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Named limits keep the runner, harness builder, and penetration test
|
|
5
|
+
* aligned. Enforced caps produce typed `FacetError`s with matching
|
|
6
|
+
* `tier1_*` codes; TIER1_TIMEOUT_MS remains an intended total-budget
|
|
7
|
+
* target until outer enforcement lands.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Intended total budget target for one Tier 1 verifier invocation; it
|
|
12
|
+
* is not enforced yet. The harness
|
|
11
13
|
* bundles the REAL renderers (mermaid + marked + vega, ~8 MB inline);
|
|
12
14
|
* parse + first render of a 40-node fixture needs real headroom.
|
|
13
15
|
*/
|
|
@@ -36,10 +38,13 @@ export const TIER1_USER_DATA_DIR_MODE = 0o700;
|
|
|
36
38
|
*/
|
|
37
39
|
export const TIER1_RENDER_BARRIER_MS = 30_000;
|
|
38
40
|
|
|
41
|
+
/** Keep tiled evidence within the intended total-budget target after render and stability windows. */
|
|
42
|
+
export const TIER1_TILED_CAPTURE_DEADLINE_MS = 15_000;
|
|
43
|
+
|
|
39
44
|
/**
|
|
40
45
|
* Time between the first interactive TSX observation and its bounded
|
|
41
|
-
* stability re-check. It stays below the
|
|
42
|
-
*
|
|
46
|
+
* stability re-check. It stays below the intended total-budget target after
|
|
47
|
+
* the render barrier; that outer target is not enforced yet.
|
|
43
48
|
*/
|
|
44
49
|
export const TSX_STABILITY_WINDOW_MS = 1_000;
|
|
45
50
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { HtmlStructureCounts, ProtocolObservation } from "../../shared/contracts/validation";
|
|
23
|
-
import { HTML_STRUCTURAL_GROUPS } from "../../shared/html/policy";
|
|
23
|
+
import { HTML_STRUCTURAL_GROUPS, isExternalHttpsImageSource } from "../../shared/html/policy";
|
|
24
24
|
|
|
25
25
|
import type { VerifierCdpSession } from "./browser-process";
|
|
26
26
|
import type { ResolvedChildFrame } from "./frame-target";
|
|
@@ -49,7 +49,7 @@ export interface SnapshotDocument {
|
|
|
49
49
|
function snapshotRootEmpty(snapshot: SnapshotResponse, documentIndex: number): boolean | undefined {
|
|
50
50
|
const document = snapshot.documents[documentIndex];
|
|
51
51
|
if (document === undefined) return undefined;
|
|
52
|
-
const roots =
|
|
52
|
+
const roots = contentRootIndexes(snapshot, documentIndex);
|
|
53
53
|
if (roots.length !== 1) return undefined;
|
|
54
54
|
const root = roots[0];
|
|
55
55
|
if (root === undefined) return undefined;
|
|
@@ -166,6 +166,18 @@ function hasAncestorIn(
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function htmlRootIndexes(snapshot: SnapshotResponse, documentIndex: number): number[] {
|
|
169
|
+
return contentRootIndexes(snapshot, documentIndex).filter(
|
|
170
|
+
(nodeIndex) =>
|
|
171
|
+
attributeValue(
|
|
172
|
+
snapshot,
|
|
173
|
+
snapshot.documents[documentIndex]!,
|
|
174
|
+
nodeIndex,
|
|
175
|
+
"data-facet-renderer-kind",
|
|
176
|
+
) !== "markdown",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function contentRootIndexes(snapshot: SnapshotResponse, documentIndex: number): number[] {
|
|
169
181
|
const document = snapshot.documents[documentIndex];
|
|
170
182
|
if (document === undefined) return [];
|
|
171
183
|
const candidates = new Set<number>();
|
|
@@ -181,6 +193,27 @@ function htmlRootIndexes(snapshot: SnapshotResponse, documentIndex: number): num
|
|
|
181
193
|
});
|
|
182
194
|
}
|
|
183
195
|
|
|
196
|
+
function countSnapshotExternalImages(snapshot: SnapshotResponse, documentIndex: number): number {
|
|
197
|
+
const document = snapshot.documents[documentIndex];
|
|
198
|
+
if (document === undefined) return 0;
|
|
199
|
+
const roots = new Set(contentRootIndexes(snapshot, documentIndex));
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (let nodeIndex = 0; nodeIndex < document.nodes.nodeName.length; nodeIndex += 1) {
|
|
202
|
+
if (!isDescendantOf(document, nodeIndex, roots)) continue;
|
|
203
|
+
const name = readString(
|
|
204
|
+
snapshot.strings,
|
|
205
|
+
document.nodes.nodeName[nodeIndex] ?? 0,
|
|
206
|
+
).toLowerCase();
|
|
207
|
+
if (
|
|
208
|
+
(HTML_STRUCTURAL_GROUPS.images as readonly string[]).includes(name) &&
|
|
209
|
+
isExternalHttpsImageSource(attributeValue(snapshot, document, nodeIndex, "src"))
|
|
210
|
+
) {
|
|
211
|
+
count += 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return count;
|
|
215
|
+
}
|
|
216
|
+
|
|
184
217
|
function isDescendantOf(
|
|
185
218
|
document: SnapshotDocument,
|
|
186
219
|
nodeIndex: number,
|
|
@@ -194,15 +227,6 @@ function isDescendantOf(
|
|
|
194
227
|
return false;
|
|
195
228
|
}
|
|
196
229
|
|
|
197
|
-
function isExternalHttps(value: string | undefined): boolean {
|
|
198
|
-
if (value === undefined) return false;
|
|
199
|
-
try {
|
|
200
|
-
return new URL(value).protocol === "https:";
|
|
201
|
-
} catch {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
230
|
export function countSnapshotHtml(
|
|
207
231
|
snapshot: SnapshotResponse,
|
|
208
232
|
documentIndex: number,
|
|
@@ -233,7 +257,7 @@ export function countSnapshotHtml(
|
|
|
233
257
|
if ((HTML_STRUCTURAL_GROUPS.lists as readonly string[]).includes(name)) counts.listCount += 1;
|
|
234
258
|
if ((HTML_STRUCTURAL_GROUPS.images as readonly string[]).includes(name)) {
|
|
235
259
|
counts.imageCount += 1;
|
|
236
|
-
if (
|
|
260
|
+
if (isExternalHttpsImageSource(attributeValue(snapshot, document, nodeIndex, "src"))) {
|
|
237
261
|
counts.externalImageCount += 1;
|
|
238
262
|
}
|
|
239
263
|
}
|
|
@@ -372,6 +396,7 @@ export async function probeProtocolSnapshot(
|
|
|
372
396
|
const discriminativeErrors = collectDiscriminativeErrors(snapshot, documentIndex);
|
|
373
397
|
const errorCount = discriminativeErrors.length;
|
|
374
398
|
const htmlCounts = countSnapshotHtml(snapshot, documentIndex);
|
|
399
|
+
const externalImageCount = countSnapshotExternalImages(snapshot, documentIndex);
|
|
375
400
|
const emptyRendererRoot = observeContent ? snapshotRootEmpty(snapshot, documentIndex) : undefined;
|
|
376
401
|
return {
|
|
377
402
|
rendererRootSvgCount: rendererRoots.length,
|
|
@@ -379,7 +404,7 @@ export async function probeProtocolSnapshot(
|
|
|
379
404
|
mermaidNodeCount: countGNode(snapshot, documentIndex, graphRoots),
|
|
380
405
|
visibleSvgCount: viewBoxes.filter(isNonDegenerateViewBox).length,
|
|
381
406
|
opaqueRegionCount: countByName(snapshot, documentIndex, "canvas"),
|
|
382
|
-
externalImageCount
|
|
407
|
+
externalImageCount,
|
|
383
408
|
...(htmlCounts === undefined ? {} : { html: htmlCounts }),
|
|
384
409
|
...(emptyRendererRoot === undefined ? {} : { emptyRendererRoot }),
|
|
385
410
|
viewBoxes,
|
|
@@ -424,6 +449,7 @@ export async function probeProtocolGetDocument(
|
|
|
424
449
|
let gNodeCount = 0;
|
|
425
450
|
let opaqueRegionCount = 0;
|
|
426
451
|
let visibleSvgCount = 0;
|
|
452
|
+
let externalImageCount = 0;
|
|
427
453
|
let html: HtmlStructureCounts | undefined;
|
|
428
454
|
let emptyRendererRoot: boolean | undefined;
|
|
429
455
|
const viewBoxes: string[] = [];
|
|
@@ -458,19 +484,21 @@ export async function probeProtocolGetDocument(
|
|
|
458
484
|
const name = (record.nodeName ?? "").toLowerCase();
|
|
459
485
|
const markedRoot = findAttr("data-facet-renderer-root") === "true";
|
|
460
486
|
const rendererRoot = name === "svg" && markedRoot && !withinRendererRoot;
|
|
461
|
-
const
|
|
487
|
+
const contentRoot = name !== "svg" && markedRoot && !withinMarkedRoot;
|
|
488
|
+
const markdownRoot = contentRoot && findAttr("data-facet-renderer-kind") === "markdown";
|
|
489
|
+
const htmlRoot = contentRoot && !markdownRoot;
|
|
462
490
|
const graphRoot = rendererRoot && findAttr("data-facet-renderer-graph") === "true";
|
|
491
|
+
if (contentRoot && observeContent) {
|
|
492
|
+
const empty = !(record.children ?? []).some((child) => {
|
|
493
|
+
const direct = child as ProtocolDomNode & { nodeType?: number; nodeValue?: string };
|
|
494
|
+
return (
|
|
495
|
+
direct.nodeType === 1 ||
|
|
496
|
+
(direct.nodeType === 3 && (direct.nodeValue ?? "").trim().length > 0)
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
emptyRendererRoot = emptyRendererRoot === undefined ? empty : false;
|
|
500
|
+
}
|
|
463
501
|
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
|
-
}
|
|
474
502
|
html ??= {
|
|
475
503
|
rendererRootCount: 0,
|
|
476
504
|
headingCount: 0,
|
|
@@ -498,6 +526,12 @@ export async function probeProtocolGetDocument(
|
|
|
498
526
|
// covers the entire child-frame document so smuggled canvases stay visible.
|
|
499
527
|
// getContext() would create a context and make the observation self-fulfilling.
|
|
500
528
|
if (name === "canvas") opaqueRegionCount += 1;
|
|
529
|
+
if (
|
|
530
|
+
(withinMarkedRoot || contentRoot) &&
|
|
531
|
+
(HTML_STRUCTURAL_GROUPS.images as readonly string[]).includes(name)
|
|
532
|
+
) {
|
|
533
|
+
if (isExternalHttpsImageSource(findAttr("src"))) externalImageCount += 1;
|
|
534
|
+
}
|
|
501
535
|
if (withinHtmlRoot && html !== undefined) {
|
|
502
536
|
if ((HTML_STRUCTURAL_GROUPS.headings as readonly string[]).includes(name))
|
|
503
537
|
html.headingCount += 1;
|
|
@@ -505,7 +539,7 @@ export async function probeProtocolGetDocument(
|
|
|
505
539
|
if ((HTML_STRUCTURAL_GROUPS.lists as readonly string[]).includes(name)) html.listCount += 1;
|
|
506
540
|
if ((HTML_STRUCTURAL_GROUPS.images as readonly string[]).includes(name)) {
|
|
507
541
|
html.imageCount += 1;
|
|
508
|
-
if (
|
|
542
|
+
if (isExternalHttpsImageSource(findAttr("src"))) html.externalImageCount += 1;
|
|
509
543
|
}
|
|
510
544
|
if ((HTML_STRUCTURAL_GROUPS.canvases as readonly string[]).includes(name))
|
|
511
545
|
html.canvasCount += 1;
|
|
@@ -545,7 +579,7 @@ export async function probeProtocolGetDocument(
|
|
|
545
579
|
mermaidNodeCount: gNodeCount,
|
|
546
580
|
visibleSvgCount,
|
|
547
581
|
opaqueRegionCount,
|
|
548
|
-
externalImageCount
|
|
582
|
+
externalImageCount,
|
|
549
583
|
...(html === undefined ? {} : { html }),
|
|
550
584
|
...(emptyRendererRoot === undefined ? {} : { emptyRendererRoot }),
|
|
551
585
|
viewBoxes,
|