@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
package/README.md
CHANGED
|
@@ -41,10 +41,10 @@ Current gallery captures use the Mermaid fixture in resolved dark and the chart
|
|
|
41
41
|
|
|
42
42
|
## Install
|
|
43
43
|
|
|
44
|
-
Facet is distributed as `@legionworks/facet`.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
Facet is distributed as `@legionworks/facet`. Bun is the required runtime
|
|
45
|
+
(version `1.4.0` or newer); install it first using the [official Bun
|
|
46
|
+
installation instructions](https://bun.sh/docs/installation). npm and pnpm
|
|
47
|
+
distribute Facet, but do not provide Bun. npm alone is not enough.
|
|
48
48
|
|
|
49
49
|
Recommended:
|
|
50
50
|
|
|
@@ -65,27 +65,56 @@ For a zero-install invocation:
|
|
|
65
65
|
bunx @legionworks/facet <verb>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
The
|
|
69
|
-
|
|
70
|
-
contract and [MCP reference](docs/reference/mcp.md) for
|
|
71
|
-
hosts.
|
|
68
|
+
The first visual read-back requires the pinned browser. If it is missing,
|
|
69
|
+
`facet doctor` prints the install command. See the [CLI reference](docs/reference/cli.md) for
|
|
70
|
+
the command contract and [MCP reference](docs/reference/mcp.md) for
|
|
71
|
+
structured-tool-only hosts.
|
|
72
|
+
|
|
73
|
+
## Quickstart from an installed binary
|
|
74
|
+
|
|
75
|
+
This walkthrough uses the globally installed `facet` command, not a source
|
|
76
|
+
checkout. It creates disposable runtime and export directories.
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
export FACET_HOME="$(mktemp -d)"
|
|
80
|
+
facet status --start
|
|
81
|
+
facet doctor
|
|
82
|
+
bunx --bun puppeteer browsers install chrome-headless-shell@151.0.7922.77
|
|
83
|
+
SOURCE='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 80"><rect width="240" height="80" rx="8" fill="#202638"/><text x="20" y="48" fill="white">Facet quickstart</text></svg>'
|
|
84
|
+
SOURCE_FILE="$(mktemp)"
|
|
85
|
+
printf '%s' "$SOURCE" > "$SOURCE_FILE"
|
|
86
|
+
ARTIFACT_ID="$(facet create --project-id demo --slug quickstart --title 'Quickstart' | bun -e 'const x=JSON.parse(await Bun.stdin.text()); if(!x.ok) throw new Error(x.error.code); console.log(x.data.artifact.id)')"
|
|
87
|
+
PUBLISH="$(facet publish --artifact-id "$ARTIFACT_ID" --type svg --file "$SOURCE_FILE")"
|
|
88
|
+
printf '%s\n' "$PUBLISH" | bun -e 'const x=JSON.parse(await Bun.stdin.text()); if(!x.ok || !x.data.verdict) throw new Error("publish failed"); console.log(JSON.stringify(x.data.verdict))'
|
|
89
|
+
REVISION_SHA="$(printf '%s\n' "$PUBLISH" | bun -e 'const x=JSON.parse(await Bun.stdin.text()); console.log(x.data.revision.sha256)')"
|
|
90
|
+
facet read-back --artifact-id "$ARTIFACT_ID" --revision-sha "$REVISION_SHA" --tier visual
|
|
91
|
+
EXPORT_DIR="$(mktemp -d)"
|
|
92
|
+
facet export "$ARTIFACT_ID" --format source --out "$EXPORT_DIR/artifact.svg"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`doctor` checks the local runtime and browser setup and prints the browser
|
|
96
|
+
install command if the pinned shell is missing. Install it before visual
|
|
97
|
+
read-back. Inspect `data.verdict` in the publish envelope: an `ok` command
|
|
98
|
+
envelope only confirms transport, and the stored verdict can still have
|
|
99
|
+
`status: "error"`.
|
|
72
100
|
|
|
73
101
|
## Verdict language
|
|
74
102
|
|
|
75
103
|
The gallery and CLI use the same wire enum, glyph, hue, and treatment.
|
|
76
104
|
|
|
77
|
-
| enum (wire, verbatim) | glyph | hue | treatment
|
|
78
|
-
| ---------------------------- | ----- | --------- |
|
|
79
|
-
| `ok` | `✓` | `#c3e88d` | outline — proof, not celebration
|
|
80
|
-
| `error` | `✗` | `#ff757f` | outline
|
|
81
|
-
| `partial:layout_unverified` | `◐` | `#ffc777` | outline; screenshot required
|
|
82
|
-
| `partial:opaque_content` | `◐` | `#ffc777` | outline; screenshot required
|
|
83
|
-
| `partial:external_resources` | `◐` | `#ffc777` | outline; screenshot required
|
|
84
|
-
| `partial:unstable` | `◐` | `#ffc777` | outline; screenshot required
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
105
|
+
| enum (wire, verbatim) | glyph | hue | treatment |
|
|
106
|
+
| ---------------------------- | ----- | --------- | ----------------------------------- |
|
|
107
|
+
| `ok` | `✓` | `#c3e88d` | outline — proof, not celebration |
|
|
108
|
+
| `error` | `✗` | `#ff757f` | outline |
|
|
109
|
+
| `partial:layout_unverified` | `◐` | `#ffc777` | outline; screenshot required |
|
|
110
|
+
| `partial:opaque_content` | `◐` | `#ffc777` | outline; screenshot required |
|
|
111
|
+
| `partial:external_resources` | `◐` | `#ffc777` | outline; screenshot required |
|
|
112
|
+
| `partial:unstable` | `◐` | `#ffc777` | outline; screenshot required |
|
|
113
|
+
| `partial:empty_render` | `◐` | `#ffc777` | empty TSX root; screenshot required |
|
|
114
|
+
| `tampered` | `⊘` | `#ff757f` | filled alarm badge |
|
|
115
|
+
| `timeout` | `◌` | `#737aa2` | dim outline |
|
|
116
|
+
| `shim_only` | `◇` | `#737aa2` | dim outline |
|
|
117
|
+
| `probe_only` | `◈` | `#737aa2` | dim outline |
|
|
89
118
|
|
|
90
119
|
`partial:external_resources` means an artifact references external HTTPS images the no-egress verifier could not load. An `ok` verdict can still carry `Verdict.screenshotError.code: "screenshot_unavailable"` when whole-artifact capture cannot be produced; it records the limit without changing the validation result. `insecure:unvalidated` means level 3 intentionally skipped validation. A missing verdict is `UNVERIFIED` with no tier.
|
|
91
120
|
|
|
@@ -133,7 +162,7 @@ Facet's insecure mode is an explicit, boot-only opt-in (`FACET_INSECURE=1|2|3`)
|
|
|
133
162
|
|
|
134
163
|
## MCP
|
|
135
164
|
|
|
136
|
-
On harnesses with shell access, the CLI is the integration; the MCP adapter is for structured-tool-only environments. The npm package includes
|
|
165
|
+
On harnesses with shell access, the CLI is the integration; the MCP adapter is for structured-tool-only environments. The npm package includes six CLI-backed tools: `facet_create`, `facet_publish`, `facet_read_back`, `facet_status`, `facet_export`, and `facet_open_url`. It requires Bun `1.4.0`. Register it with `bun add -g @legionworks/facet` followed by the bare `facet-mcp` command; `npx -p @legionworks/facet facet-mcp` is the no-install alternative. See the [MCP reference](docs/reference/mcp.md) for OpenCode, Claude Code, and Codex registration.
|
|
137
166
|
|
|
138
167
|
## Documentation
|
|
139
168
|
|
|
@@ -40,6 +40,9 @@ function buildFrameAttributes(src = "/gallery/frame") {
|
|
|
40
40
|
src
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
function usesArtifactStylesheet(artifactType) {
|
|
44
|
+
return artifactType === "html" || artifactType === "tsx";
|
|
45
|
+
}
|
|
43
46
|
function isLoopbackHostname(hostname) {
|
|
44
47
|
return hostname === "127.0.0.1";
|
|
45
48
|
}
|
|
@@ -4349,7 +4352,8 @@ var TemplateSchema = exports_external.object({
|
|
|
4349
4352
|
name: exports_external.string().min(1),
|
|
4350
4353
|
description: exports_external.string().nullable(),
|
|
4351
4354
|
promotedBy: exports_external.string().min(1),
|
|
4352
|
-
promotedAt: IsoTimestampSchema
|
|
4355
|
+
promotedAt: IsoTimestampSchema,
|
|
4356
|
+
promotionOverride: exports_external.string().nullable()
|
|
4353
4357
|
});
|
|
4354
4358
|
|
|
4355
4359
|
// src/shared/contracts/observed-counts.ts
|
|
@@ -4380,6 +4384,7 @@ var RenderStatusSchema = exports_external.enum([
|
|
|
4380
4384
|
"partial:opaque_content",
|
|
4381
4385
|
"partial:external_resources",
|
|
4382
4386
|
"partial:unstable",
|
|
4387
|
+
"partial:empty_render",
|
|
4383
4388
|
"tampered",
|
|
4384
4389
|
"timeout",
|
|
4385
4390
|
"shim_only",
|
|
@@ -4417,8 +4422,12 @@ var LexicalCountersSchema = exports_external.object({
|
|
|
4417
4422
|
html: HtmlStructureCountsSchema.optional()
|
|
4418
4423
|
});
|
|
4419
4424
|
var ObservedCountShape = Object.fromEntries(OBSERVED_COUNT_KEYS.map((key) => [key, exports_external.number().int().nonnegative()]));
|
|
4425
|
+
var RenderedContentShape = {
|
|
4426
|
+
emptyRendererRoot: exports_external.boolean().optional()
|
|
4427
|
+
};
|
|
4420
4428
|
var VerdictObservedSchema = exports_external.object({
|
|
4421
4429
|
...ObservedCountShape,
|
|
4430
|
+
...RenderedContentShape,
|
|
4422
4431
|
html: HtmlStructureCountsSchema.optional(),
|
|
4423
4432
|
viewBoxes: exports_external.array(exports_external.string()).optional(),
|
|
4424
4433
|
errorCount: exports_external.number().int().nonnegative(),
|
|
@@ -4472,6 +4481,7 @@ var Tier1ResultSchema = Tier0ResultSchema.extend({
|
|
|
4472
4481
|
});
|
|
4473
4482
|
var ProtocolObservationSchema = exports_external.object({
|
|
4474
4483
|
...ObservedCountShape,
|
|
4484
|
+
...RenderedContentShape,
|
|
4475
4485
|
html: HtmlStructureCountsSchema.optional(),
|
|
4476
4486
|
viewBoxes: exports_external.array(exports_external.string()),
|
|
4477
4487
|
errorCount: exports_external.number().int().nonnegative(),
|
|
@@ -4486,6 +4496,7 @@ var FAVICON_TINT_BY_STATUS = {
|
|
|
4486
4496
|
"partial:opaque_content": "amber",
|
|
4487
4497
|
"partial:external_resources": "amber",
|
|
4488
4498
|
"partial:unstable": "amber",
|
|
4499
|
+
"partial:empty_render": "amber",
|
|
4489
4500
|
tampered: "red",
|
|
4490
4501
|
timeout: "red",
|
|
4491
4502
|
shim_only: "grey",
|
|
@@ -4599,6 +4610,10 @@ var ListRequestSchema = BaseRequestSchema.extend({
|
|
|
4599
4610
|
slugPrefix: exports_external.string().optional(),
|
|
4600
4611
|
limit: exports_external.number().int().positive().max(MAX_LIST_LIMIT).optional()
|
|
4601
4612
|
});
|
|
4613
|
+
var TemplatesRequestSchema = BaseRequestSchema.extend({
|
|
4614
|
+
command: exports_external.literal("templates"),
|
|
4615
|
+
limit: exports_external.number().int().positive().max(MAX_LIST_LIMIT).optional()
|
|
4616
|
+
});
|
|
4602
4617
|
var ReadBackRequestSchema = BaseRequestSchema.extend({
|
|
4603
4618
|
command: exports_external.literal("readBack"),
|
|
4604
4619
|
artifactId: exports_external.string().min(1),
|
|
@@ -4620,7 +4635,8 @@ var PromoteRequestSchema = BaseRequestSchema.extend({
|
|
|
4620
4635
|
revisionId: exports_external.string().min(1),
|
|
4621
4636
|
name: exports_external.string().min(1),
|
|
4622
4637
|
description: exports_external.string().nullable().optional(),
|
|
4623
|
-
promotedBy: exports_external.string().min(1)
|
|
4638
|
+
promotedBy: exports_external.string().min(1),
|
|
4639
|
+
allowUnverified: exports_external.boolean().optional()
|
|
4624
4640
|
});
|
|
4625
4641
|
var InstantiateRequestSchema = BaseRequestSchema.extend({
|
|
4626
4642
|
command: exports_external.literal("instantiate"),
|
|
@@ -4678,6 +4694,19 @@ var ListResultSchema = BaseResultSchema.extend({
|
|
|
4678
4694
|
artifacts: exports_external.array(ArtifactEnvelopeSchema),
|
|
4679
4695
|
nextCursor: exports_external.string().nullable().optional()
|
|
4680
4696
|
});
|
|
4697
|
+
var TemplatesResultSchema = BaseResultSchema.extend({
|
|
4698
|
+
command: exports_external.literal("templates"),
|
|
4699
|
+
templates: exports_external.array(exports_external.object({
|
|
4700
|
+
name: exports_external.string().min(1),
|
|
4701
|
+
artifactId: exports_external.string().min(1),
|
|
4702
|
+
revisionId: exports_external.string().min(1),
|
|
4703
|
+
revisionSha: exports_external.string().regex(/^[a-f0-9]{64}$/),
|
|
4704
|
+
promotedBy: exports_external.string().min(1),
|
|
4705
|
+
promotedAt: exports_external.string().datetime({ offset: true }),
|
|
4706
|
+
promotionOverride: exports_external.string().nullable(),
|
|
4707
|
+
sourceVerdict: exports_external.object({ status: RenderStatusSchema, tier: exports_external.union([exports_external.literal(0), exports_external.literal(1)]) }).nullable()
|
|
4708
|
+
}))
|
|
4709
|
+
});
|
|
4681
4710
|
var ReadBackResultSchema = BaseResultSchema.extend({
|
|
4682
4711
|
command: exports_external.literal("readBack"),
|
|
4683
4712
|
renderer: RendererSchema,
|
|
@@ -5164,6 +5193,13 @@ function createArtifactFrame(options) {
|
|
|
5164
5193
|
renderResult = handle;
|
|
5165
5194
|
return handle;
|
|
5166
5195
|
},
|
|
5196
|
+
setTheme(theme) {
|
|
5197
|
+
const api = raw.contentWindow?.__facetFrame;
|
|
5198
|
+
if (api === null || api === undefined || typeof api.setTheme !== "function") {
|
|
5199
|
+
throw new Error("frame theme API unavailable");
|
|
5200
|
+
}
|
|
5201
|
+
api.setTheme(theme);
|
|
5202
|
+
},
|
|
5167
5203
|
get renderResult() {
|
|
5168
5204
|
return renderResult;
|
|
5169
5205
|
}
|
|
@@ -5402,7 +5438,7 @@ function setGalleryVerdict(document2, verdict) {
|
|
|
5402
5438
|
badge.dataset.status = verdict.status;
|
|
5403
5439
|
badge.childNodes[0].textContent = verdict.status.split(":")[0] ?? verdict.status;
|
|
5404
5440
|
if (tier !== null) {
|
|
5405
|
-
const detail = verdict.status === "partial:opaque_content" ? "opaque" : verdict.status === "partial:layout_unverified" ? "layout" : verdict.status === "partial:external_resources" ? "external" : verdict.status === "partial:unstable" ? "unstable" : null;
|
|
5441
|
+
const detail = verdict.status === "partial:opaque_content" ? "opaque" : verdict.status === "partial:layout_unverified" ? "layout" : verdict.status === "partial:external_resources" ? "external" : verdict.status === "partial:unstable" ? "unstable" : verdict.status === "partial:empty_render" ? "empty" : null;
|
|
5406
5442
|
const insecure = verdict.insecure === undefined ? null : `INSECURE L${verdict.insecure.level}`;
|
|
5407
5443
|
const suffix = insecure === null ? `T${verdict.tier}` : `${insecure} · T${verdict.tier}`;
|
|
5408
5444
|
tier.textContent = detail === null ? `· ${suffix}` : `· ${detail} · ${suffix}`;
|
|
@@ -5546,6 +5582,42 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5546
5582
|
let removeThemePreferenceListener = noOp;
|
|
5547
5583
|
let generation = 0;
|
|
5548
5584
|
let activeFrame = null;
|
|
5585
|
+
let removeInteractionListener = noOp;
|
|
5586
|
+
const endInteractionSignal = () => {
|
|
5587
|
+
removeInteractionListener();
|
|
5588
|
+
const badge = document2.getElementById("facet-verdict");
|
|
5589
|
+
if (badge !== null)
|
|
5590
|
+
delete badge.dataset.interactionError;
|
|
5591
|
+
};
|
|
5592
|
+
const activateInteractionSignal = (frame) => {
|
|
5593
|
+
endInteractionSignal();
|
|
5594
|
+
const badge = document2.getElementById("facet-verdict");
|
|
5595
|
+
updateGalleryStatus("displayed");
|
|
5596
|
+
const frameWindow = frame.element.raw.contentWindow;
|
|
5597
|
+
if (frameWindow === null || typeof frameWindow.addEventListener !== "function")
|
|
5598
|
+
return;
|
|
5599
|
+
const markFailure = () => {
|
|
5600
|
+
if (expired || activeFrame !== frame || frame.renderResult === null)
|
|
5601
|
+
return;
|
|
5602
|
+
if (badge !== null)
|
|
5603
|
+
badge.dataset.interactionError = "true";
|
|
5604
|
+
updateGalleryStatus("displayed · runtime error during interaction");
|
|
5605
|
+
};
|
|
5606
|
+
frameWindow.addEventListener("error", markFailure, true);
|
|
5607
|
+
frameWindow.addEventListener("unhandledrejection", markFailure, true);
|
|
5608
|
+
const frameDocument = frame.element.raw.contentDocument;
|
|
5609
|
+
const observer = frameDocument == null || typeof MutationObserver !== "function" ? null : new MutationObserver(() => {
|
|
5610
|
+
if (frameDocument.querySelector("[data-facet-error]"))
|
|
5611
|
+
markFailure();
|
|
5612
|
+
});
|
|
5613
|
+
if (frameDocument !== null)
|
|
5614
|
+
observer?.observe(frameDocument, { childList: true, subtree: true });
|
|
5615
|
+
removeInteractionListener = () => {
|
|
5616
|
+
frameWindow.removeEventListener("error", markFailure, true);
|
|
5617
|
+
frameWindow.removeEventListener("unhandledrejection", markFailure, true);
|
|
5618
|
+
observer?.disconnect();
|
|
5619
|
+
};
|
|
5620
|
+
};
|
|
5549
5621
|
const expireSession = () => {
|
|
5550
5622
|
if (expired)
|
|
5551
5623
|
return;
|
|
@@ -5556,6 +5628,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5556
5628
|
exportMenu?.sync();
|
|
5557
5629
|
activeFrame?.renderResult?.setGestureMode("native");
|
|
5558
5630
|
swaps?.close();
|
|
5631
|
+
endInteractionSignal();
|
|
5559
5632
|
removeThemePreferenceListener();
|
|
5560
5633
|
clearSession(window2.sessionStorage);
|
|
5561
5634
|
renderSessionExpired(document2, (message) => setGalleryError(document2, message), (status) => setGalleryStatus(document2, status));
|
|
@@ -5687,6 +5760,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5687
5760
|
updateGalleryFavicon(source.verdict?.status ?? "unverified");
|
|
5688
5761
|
updateGalleryVerdict(source.verdict ?? null);
|
|
5689
5762
|
updateGalleryStatus("displayed");
|
|
5763
|
+
activateInteractionSignal(current);
|
|
5690
5764
|
updateLiveState("live");
|
|
5691
5765
|
const themeToggle = document2.getElementById("facet-theme-toggle");
|
|
5692
5766
|
const updateThemeToggle = (mode) => {
|
|
@@ -5718,6 +5792,11 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5718
5792
|
const canCommit = () => themeGeneration === generation && nextMode === requestedThemeMode;
|
|
5719
5793
|
if (!canCommit())
|
|
5720
5794
|
return;
|
|
5795
|
+
if (usesArtifactStylesheet(source.artifactType)) {
|
|
5796
|
+
current.setTheme(nextResolvedTheme);
|
|
5797
|
+
commitTheme(nextMode, nextResolvedTheme);
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5721
5800
|
const next = createArtifactFrame({
|
|
5722
5801
|
artifactType: source.artifactType,
|
|
5723
5802
|
renderer: source.renderer,
|
|
@@ -5745,6 +5824,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5745
5824
|
return;
|
|
5746
5825
|
current = next;
|
|
5747
5826
|
activeFrame = next;
|
|
5827
|
+
activateInteractionSignal(next);
|
|
5748
5828
|
commitTheme(nextMode, nextResolvedTheme);
|
|
5749
5829
|
syncPanZoomToggle();
|
|
5750
5830
|
syncZoomButtons();
|
|
@@ -5815,6 +5895,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5815
5895
|
if (!result.failedNewFrameReady) {
|
|
5816
5896
|
current = frame;
|
|
5817
5897
|
activeFrame = frame;
|
|
5898
|
+
activateInteractionSignal(frame);
|
|
5818
5899
|
source = revision;
|
|
5819
5900
|
commitTheme(revisionThemeMode, revisionTheme);
|
|
5820
5901
|
syncPanZoomToggle();
|
|
@@ -5870,6 +5951,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5870
5951
|
updateSwapBar("start");
|
|
5871
5952
|
updateGalleryFavicon("unverified");
|
|
5872
5953
|
updateGalleryVerdict(null);
|
|
5954
|
+
endInteractionSignal();
|
|
5873
5955
|
swaps?.enqueue({ kind: "revision", ...event });
|
|
5874
5956
|
},
|
|
5875
5957
|
onClose: (event) => {
|
|
@@ -5885,6 +5967,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5885
5967
|
let zoomButtonPoll;
|
|
5886
5968
|
const shutdown = () => {
|
|
5887
5969
|
stream.close();
|
|
5970
|
+
removeInteractionListener();
|
|
5888
5971
|
removeThemePreferenceListener();
|
|
5889
5972
|
if (zoomButtonPoll !== undefined)
|
|
5890
5973
|
clearInterval(zoomButtonPoll);
|
|
@@ -136,6 +136,17 @@
|
|
|
136
136
|
content: "✓";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
.facet-verdict[data-interaction-error="true"] {
|
|
140
|
+
border-color: var(--facet-fail);
|
|
141
|
+
box-shadow: var(--facet-glow-danger);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.facet-verdict[data-interaction-error="true"]:after {
|
|
145
|
+
content: "✗ runtime";
|
|
146
|
+
color: var(--facet-fail);
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
}
|
|
149
|
+
|
|
139
150
|
.facet-verdict[data-status="error"] {
|
|
140
151
|
border-color: var(--facet-fail-border);
|
|
141
152
|
background: var(--facet-fail-fill);
|
|
@@ -186,6 +197,16 @@
|
|
|
186
197
|
content: "◐";
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
.facet-verdict[data-status="partial:empty_render"] {
|
|
201
|
+
border-color: var(--facet-partial-border);
|
|
202
|
+
background: var(--facet-partial-fill);
|
|
203
|
+
color: var(--facet-partial);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.facet-verdict[data-status="partial:empty_render"]:before {
|
|
207
|
+
content: "◐";
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
.facet-verdict[data-status="tampered"], .facet-verdict[data-status="insecure:unvalidated"] {
|
|
190
211
|
border-color: var(--facet-fail);
|
|
191
212
|
background: var(--facet-fail);
|
|
@@ -194,7 +215,7 @@
|
|
|
194
215
|
font-weight: 700;
|
|
195
216
|
}
|
|
196
217
|
|
|
197
|
-
.facet-verdict[data-status="tampered"]:before, .facet-verdict[data-status="
|
|
218
|
+
.facet-verdict[data-status="tampered"]:before, .facet-verdict[data-status="insecure:unvalidated"]:before {
|
|
198
219
|
content: "⊘";
|
|
199
220
|
}
|
|
200
221
|
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
function isResolvedGalleryTheme(value) {
|
|
12
12
|
return value === "dark" || value === "light";
|
|
13
13
|
}
|
|
14
|
+
function galleryDataTheme(theme) {
|
|
15
|
+
return theme === "light" ? "winter" : "night";
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
// src/gallery-web/frame/renderer-validation.ts
|
|
16
19
|
function validateRenderer(value) {
|
|
@@ -395,6 +398,11 @@ function installGalleryFrameApi(registry) {
|
|
|
395
398
|
setPointerEventsForAllChildren(container, mode === "panzoom" ? "none" : "");
|
|
396
399
|
};
|
|
397
400
|
const api = {
|
|
401
|
+
setTheme(theme) {
|
|
402
|
+
if (!isResolvedGalleryTheme(theme))
|
|
403
|
+
throw new FacetRenderError("frame theme is invalid", "invalid_request");
|
|
404
|
+
document.documentElement.dataset.theme = galleryDataTheme(theme);
|
|
405
|
+
},
|
|
398
406
|
async render(payload) {
|
|
399
407
|
if (rendered)
|
|
400
408
|
throw new FacetRenderError("frame already rendered", "invalid_request");
|
|
@@ -78,32 +78,19 @@ body {
|
|
|
78
78
|
padding-inline: max(16px, calc((100% - 92ch) / 2));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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. */
|
|
82
|
+
#artifact[data-facet-artifact-type="html"],
|
|
83
|
+
#artifact[data-facet-artifact-type="tsx"] {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
}
|
|
87
|
+
|
|
101
88
|
#artifact[data-facet-artifact-type="html"] > [data-facet-renderer-root],
|
|
102
89
|
#artifact[data-facet-artifact-type="tsx"] > [data-facet-renderer-root] {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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. */
|
|
93
|
+
flex: 1 0 auto;
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
#artifact > svg {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "../chunks/markdown-zazx9h4n.js";
|
|
4
4
|
import {
|
|
5
5
|
installGalleryFrameApi
|
|
6
|
-
} from "../chunks/markdown-
|
|
6
|
+
} from "../chunks/markdown-g3asgaw3.js";
|
|
7
7
|
import {
|
|
8
8
|
FacetRenderError,
|
|
9
9
|
createRendererRegistry,
|
|
@@ -42693,6 +42693,28 @@ function withFacetChartTheme(spec, theme) {
|
|
|
42693
42693
|
}
|
|
42694
42694
|
};
|
|
42695
42695
|
}
|
|
42696
|
+
function withFacetChartDimensions(spec) {
|
|
42697
|
+
if (spec === null || typeof spec !== "object" || Array.isArray(spec))
|
|
42698
|
+
return spec;
|
|
42699
|
+
const source4 = spec;
|
|
42700
|
+
const config = source4.config;
|
|
42701
|
+
if ("autosize" in source4 || typeof config === "object" && config !== null && !Array.isArray(config) && "view" in config)
|
|
42702
|
+
return spec;
|
|
42703
|
+
if ("width" in source4 && "height" in source4)
|
|
42704
|
+
return spec;
|
|
42705
|
+
const authoredConfig = typeof config === "object" && config !== null && !Array.isArray(config) ? config : {};
|
|
42706
|
+
const viewDefaults = {
|
|
42707
|
+
...!("width" in source4) ? { continuousWidth: 640, discreteWidth: 640 } : {},
|
|
42708
|
+
...!("height" in source4) ? { continuousHeight: 360, discreteHeight: 360 } : {}
|
|
42709
|
+
};
|
|
42710
|
+
return {
|
|
42711
|
+
...source4,
|
|
42712
|
+
config: {
|
|
42713
|
+
...authoredConfig,
|
|
42714
|
+
view: viewDefaults
|
|
42715
|
+
}
|
|
42716
|
+
};
|
|
42717
|
+
}
|
|
42696
42718
|
async function renderChart(ctx, bytes, renderer = "svg") {
|
|
42697
42719
|
const text3 = decodeArtifactBytes(bytes);
|
|
42698
42720
|
let spec;
|
|
@@ -42704,7 +42726,7 @@ async function renderChart(ctx, bytes, renderer = "svg") {
|
|
|
42704
42726
|
}
|
|
42705
42727
|
let compiled;
|
|
42706
42728
|
try {
|
|
42707
|
-
compiled = compile(withFacetChartTheme(spec, ctx.theme));
|
|
42729
|
+
compiled = compile(withFacetChartTheme(withFacetChartDimensions(spec), ctx.theme));
|
|
42708
42730
|
} catch (error3) {
|
|
42709
42731
|
const message = error3 instanceof Error ? error3.message : String(error3);
|
|
42710
42732
|
throw new FacetRenderError(`vega-lite compile failed: ${message}`, "chart_compile_error");
|
|
@@ -7,7 +7,7 @@ import"../chunks/markdown-h7pzv34e.js";
|
|
|
7
7
|
import"../chunks/markdown-zazx9h4n.js";
|
|
8
8
|
import {
|
|
9
9
|
installGalleryFrameApi
|
|
10
|
-
} from "../chunks/markdown-
|
|
10
|
+
} from "../chunks/markdown-g3asgaw3.js";
|
|
11
11
|
import {
|
|
12
12
|
createRendererRegistry
|
|
13
13
|
} from "../chunks/markdown-d1wfbs70.js";
|
package/dist/gallery/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./chunk-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./chunk-2dge4gwb.css"><script type="module" crossorigin src="./chunk-0by8qn4x.js"></script></head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="facet-shell">
|
|
14
14
|
<header id="facet-status">
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
<span class="sep" aria-hidden="true"></span>
|
|
18
18
|
<span id="facet-artifact-title"></span>
|
|
19
19
|
<span class="revision" id="facet-revision">—</span>
|
|
20
|
-
<span class="status-line" id="facet-status-line"
|
|
20
|
+
<span class="status-line" id="facet-status-line" role="status" aria-live="polite"
|
|
21
|
+
>idle</span
|
|
22
|
+
>
|
|
21
23
|
<!--
|
|
22
24
|
Verdict badge. JS wiring: set data-status to the FULL wire
|
|
23
25
|
enum, text to status.split(":")[0], and the tier span to
|
package/docs/reference/cli.md
CHANGED
|
@@ -28,7 +28,8 @@ Errors use the same top level with `ok: false` and
|
|
|
28
28
|
| `read-back` | `--artifact-id`, optional `--revision-sha` (latest when omitted), `--tier` (one of: `0` \| `1` \| `visual`) |
|
|
29
29
|
| `status` | `--artifact-id`, `--start` (valueless start-then-inspect switch) |
|
|
30
30
|
| `open` | `--artifact-id`, optional `--revision-sha` (latest when omitted), `--no-launch` |
|
|
31
|
-
| `promote` | `--artifact-id`, `--revision-id`, `--name`, `--description`, `--promoted-by`
|
|
31
|
+
| `promote` | `--artifact-id`, `--revision-id`, `--name`, `--description`, `--promoted-by`, `--allow-unverified` |
|
|
32
|
+
| `templates` | optional `--limit` |
|
|
32
33
|
| `instantiate` | `--name`, `--new-slug`, `--project-id` |
|
|
33
34
|
| `pin` | `--revision-id`, `--pinned` (`true` or `false`) |
|
|
34
35
|
| `export` | `<artifactId>`, `--revision`, `--format source\|render`, `--out`, `--force`, `--include-bytes` |
|
|
@@ -96,6 +97,12 @@ available without invoking the desktop launcher, and a launcher failure reports
|
|
|
96
97
|
defaults to `svg`; `canvas` is valid only for chart artifacts. An invalid
|
|
97
98
|
renderer value is a usage error and exits 64.
|
|
98
99
|
|
|
100
|
+
For charts, Facet defaults unspecified views to 640×360 through
|
|
101
|
+
`config.view`. These defaults apply only when the spec has no `autosize`, no
|
|
102
|
+
`config.view`, and does not set both top-level `width` and `height`. An authored
|
|
103
|
+
dimension is preserved; composite facet, concat, and repeat views receive the
|
|
104
|
+
defaults through their child view config.
|
|
105
|
+
|
|
99
106
|
`publish --type html` publishes a static, script-free HTML artifact.
|
|
100
107
|
See [HTML reference](html.md) for the static / script-free contract,
|
|
101
108
|
the denied element and attribute set, the vendored class vocabulary,
|
|
@@ -118,7 +125,7 @@ errors and next actions:
|
|
|
118
125
|
| code | next action |
|
|
119
126
|
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
120
127
|
| `artifact_not_found` | Check the artifact ID and list the project. |
|
|
121
|
-
| `revision_not_found` |
|
|
128
|
+
| `revision_not_found` | Check that the artifact has a revision; omit the SHA for latest or use an existing SHA. |
|
|
122
129
|
| `duplicate_revision` | Reuse `error.details.revisionSha`; no new revision was stored. |
|
|
123
130
|
| `output_unwritable` | Fix the output directory, permissions, or rename target. |
|
|
124
131
|
| `evidence_unavailable` | Run Tier 1 again or inspect retention and evidence paths. |
|
|
@@ -134,6 +141,27 @@ mode does not assert that its visuals always change.
|
|
|
134
141
|
`FACET_HOME/secrets/promote.token`. Neither source is accepted as an argv flag
|
|
135
142
|
or emitted in a CLI envelope.
|
|
136
143
|
|
|
144
|
+
Before promotion, request visual read-back for the exact revision. Promotion
|
|
145
|
+
requires Tier 1 visual verification. A refusal returns `promotion_refused`
|
|
146
|
+
(HTTP 409) with `error.details.reason`: `no_visual_verification` when no visual
|
|
147
|
+
run exists, `error` when Tier 0 failed, or the refused Tier 1 status. The gate refuses `error`,
|
|
148
|
+
`partial:empty_render`, `tampered`, `timeout`, `shim_only`, `probe_only`, and
|
|
149
|
+
`insecure:unvalidated`. Other Tier 1 statuses are allowed when Tier 0 is not an
|
|
150
|
+
error. `--allow-unverified` permits a refused promotion but stores the refusal
|
|
151
|
+
reason in the template's `promotionOverride`; this is an audit record, not
|
|
152
|
+
verification or a passing verdict. A duplicate template name returns
|
|
153
|
+
`template_name_taken` (HTTP 409).
|
|
154
|
+
|
|
155
|
+
`facet templates [--limit N]` lists promoted templates. It uses the install
|
|
156
|
+
token. Each row includes the source verdict, read from the stored revision when
|
|
157
|
+
the list is requested, and any `promotionOverride` reason.
|
|
158
|
+
|
|
159
|
+
A Tier 1 verdict whose `insecure.level === 3` marker is present is refused as
|
|
160
|
+
`insecure:unvalidated` even when the verdict's status would otherwise be
|
|
161
|
+
allow-listed (level 3 skips validation); `--allow-unverified` records the same
|
|
162
|
+
reason on `promotionOverride`. Levels 1 and 2 keep their status-based
|
|
163
|
+
treatment — they are real validator results under relaxed sandboxing.
|
|
164
|
+
|
|
137
165
|
## Insecure mode
|
|
138
166
|
|
|
139
167
|
`FACET_INSECURE=1|2|3` sets a boot-only forced floor. `FACET_INSECURE_AUTO=1`
|
package/docs/reference/export.md
CHANGED
|
@@ -84,6 +84,10 @@ its sidecar is not a valid export.
|
|
|
84
84
|
The sidecar describes the stored verdict. Export does not approve, promote,
|
|
85
85
|
revalidate, or otherwise change the artifact.
|
|
86
86
|
|
|
87
|
+
Evidence consumers must inspect `verdict.insecure` before treating a result as
|
|
88
|
+
validated. In particular, an explicit Tier 1 run under insecure level 3 carries
|
|
89
|
+
the insecure marker even when its status is not `insecure:unvalidated`.
|
|
90
|
+
|
|
87
91
|
## `evidence_unavailable`
|
|
88
92
|
|
|
89
93
|
Render export returns typed `evidence_unavailable` when the selected revision
|
package/docs/reference/html.md
CHANGED
|
@@ -142,15 +142,15 @@ an included Tailwind utility outside this list can still render.
|
|
|
142
142
|
|
|
143
143
|
### Recommended Tailwind utilities
|
|
144
144
|
|
|
145
|
-
`block`, `flex`, `grid`, `inline-flex`, `flex-col`, `flex-wrap`, `items-center`, `justify-between`, `justify-center`, `grid-cols-1`, `grid-cols-2`, `grid-cols-3`, `gap-2`, `gap-3`, `gap-4`, `gap-6`, `p-2`, `p-3`, `p-4`, `p-6`, `px-3`, `px-4`, `py-2`, `py-3`, `m-0`, `mt-2`, `mt-4`, `mb-2`, `mb-4`, `w-full`, `max-w-prose`, `max-w-2xl`, `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `font-medium`, `font-semibold`, `font-bold`, `leading-relaxed`, `text-left`, `text-center`, `text-right`, `text-legion-ink`, `text-legion-muted`, `text-legion-cyan`, `bg-legion-paper`, `bg-legion-ink`, `bg-legion-cyan`, `border`, `border-2`, `border-legion-line`, `rounded`, `rounded-box`, `overflow-x-auto`, `table`, `table-zebra`.
|
|
145
|
+
`block`, `flex`, `grid`, `inline-flex`, `flex-col`, `flex-wrap`, `items-center`, `justify-between`, `justify-center`, `grid-cols-1`, `grid-cols-2`, `grid-cols-3`, `gap-2`, `gap-3`, `gap-4`, `gap-6`, `p-2`, `p-3`, `p-4`, `p-6`, `px-3`, `px-4`, `py-2`, `py-3`, `m-0`, `mt-2`, `mt-4`, `mb-2`, `mb-4`, `mx-auto`, `w-full`, `max-w-prose`, `max-w-2xl`, `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `font-medium`, `font-semibold`, `font-bold`, `leading-relaxed`, `text-left`, `text-center`, `text-right`, `text-legion-ink`, `text-legion-muted`, `text-legion-cyan`, `bg-legion-paper`, `bg-legion-ink`, `bg-legion-cyan`, `border`, `border-2`, `border-legion-line`, `rounded`, `rounded-box`, `overflow-x-auto`, `table`, `table-zebra`.
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
60 recommended utilities.
|
|
148
148
|
|
|
149
149
|
### Recommended daisyUI components
|
|
150
150
|
|
|
151
151
|
`alert`, `badge`, `btn`, `card`, `stat`, `table`.
|
|
152
152
|
|
|
153
|
-
6 recommended components ·
|
|
153
|
+
6 recommended components · 65 documented recommendations.
|
|
154
154
|
|
|
155
155
|
<!-- VOCABULARY:END -->
|
|
156
156
|
|