@legionworks/facet 1.9.0 → 1.10.0
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-4g8rem85.css → chunk-2dge4gwb.css} +22 -1
- package/dist/gallery/{chunk-c6s9k9ty.js → chunk-48emrxg6.js} +71 -3
- package/dist/gallery/frame/frame.css +8 -1
- package/dist/gallery/frame/runtime/chart.js +23 -1
- package/dist/gallery/index.html +4 -2
- package/docs/reference/cli.md +29 -1
- package/docs/reference/export.md +4 -0
- package/docs/reference/mcp.md +16 -8
- package/docs/reference/security.md +7 -0
- package/docs/reference/storage.md +6 -1
- package/docs/reference/tsx.md +14 -0
- package/docs/reference/validation.md +34 -6
- package/package.json +4 -4
- package/skills/facet/SKILL.md +24 -1
- 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 +45 -1
- package/src/gallery-web/favicon.ts +1 -0
- package/src/gallery-web/frame/renderers/chart.ts +31 -1
- package/src/gallery-web/frame/styles/frame.css +8 -1
- package/src/gallery-web/frame-html.ts +2 -1
- 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 +10 -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/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/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
|
|
|
@@ -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
|
|
|
@@ -4349,7 +4349,8 @@ var TemplateSchema = exports_external.object({
|
|
|
4349
4349
|
name: exports_external.string().min(1),
|
|
4350
4350
|
description: exports_external.string().nullable(),
|
|
4351
4351
|
promotedBy: exports_external.string().min(1),
|
|
4352
|
-
promotedAt: IsoTimestampSchema
|
|
4352
|
+
promotedAt: IsoTimestampSchema,
|
|
4353
|
+
promotionOverride: exports_external.string().nullable()
|
|
4353
4354
|
});
|
|
4354
4355
|
|
|
4355
4356
|
// src/shared/contracts/observed-counts.ts
|
|
@@ -4380,6 +4381,7 @@ var RenderStatusSchema = exports_external.enum([
|
|
|
4380
4381
|
"partial:opaque_content",
|
|
4381
4382
|
"partial:external_resources",
|
|
4382
4383
|
"partial:unstable",
|
|
4384
|
+
"partial:empty_render",
|
|
4383
4385
|
"tampered",
|
|
4384
4386
|
"timeout",
|
|
4385
4387
|
"shim_only",
|
|
@@ -4417,8 +4419,12 @@ var LexicalCountersSchema = exports_external.object({
|
|
|
4417
4419
|
html: HtmlStructureCountsSchema.optional()
|
|
4418
4420
|
});
|
|
4419
4421
|
var ObservedCountShape = Object.fromEntries(OBSERVED_COUNT_KEYS.map((key) => [key, exports_external.number().int().nonnegative()]));
|
|
4422
|
+
var RenderedContentShape = {
|
|
4423
|
+
emptyRendererRoot: exports_external.boolean().optional()
|
|
4424
|
+
};
|
|
4420
4425
|
var VerdictObservedSchema = exports_external.object({
|
|
4421
4426
|
...ObservedCountShape,
|
|
4427
|
+
...RenderedContentShape,
|
|
4422
4428
|
html: HtmlStructureCountsSchema.optional(),
|
|
4423
4429
|
viewBoxes: exports_external.array(exports_external.string()).optional(),
|
|
4424
4430
|
errorCount: exports_external.number().int().nonnegative(),
|
|
@@ -4472,6 +4478,7 @@ var Tier1ResultSchema = Tier0ResultSchema.extend({
|
|
|
4472
4478
|
});
|
|
4473
4479
|
var ProtocolObservationSchema = exports_external.object({
|
|
4474
4480
|
...ObservedCountShape,
|
|
4481
|
+
...RenderedContentShape,
|
|
4475
4482
|
html: HtmlStructureCountsSchema.optional(),
|
|
4476
4483
|
viewBoxes: exports_external.array(exports_external.string()),
|
|
4477
4484
|
errorCount: exports_external.number().int().nonnegative(),
|
|
@@ -4486,6 +4493,7 @@ var FAVICON_TINT_BY_STATUS = {
|
|
|
4486
4493
|
"partial:opaque_content": "amber",
|
|
4487
4494
|
"partial:external_resources": "amber",
|
|
4488
4495
|
"partial:unstable": "amber",
|
|
4496
|
+
"partial:empty_render": "amber",
|
|
4489
4497
|
tampered: "red",
|
|
4490
4498
|
timeout: "red",
|
|
4491
4499
|
shim_only: "grey",
|
|
@@ -4599,6 +4607,10 @@ var ListRequestSchema = BaseRequestSchema.extend({
|
|
|
4599
4607
|
slugPrefix: exports_external.string().optional(),
|
|
4600
4608
|
limit: exports_external.number().int().positive().max(MAX_LIST_LIMIT).optional()
|
|
4601
4609
|
});
|
|
4610
|
+
var TemplatesRequestSchema = BaseRequestSchema.extend({
|
|
4611
|
+
command: exports_external.literal("templates"),
|
|
4612
|
+
limit: exports_external.number().int().positive().max(MAX_LIST_LIMIT).optional()
|
|
4613
|
+
});
|
|
4602
4614
|
var ReadBackRequestSchema = BaseRequestSchema.extend({
|
|
4603
4615
|
command: exports_external.literal("readBack"),
|
|
4604
4616
|
artifactId: exports_external.string().min(1),
|
|
@@ -4620,7 +4632,8 @@ var PromoteRequestSchema = BaseRequestSchema.extend({
|
|
|
4620
4632
|
revisionId: exports_external.string().min(1),
|
|
4621
4633
|
name: exports_external.string().min(1),
|
|
4622
4634
|
description: exports_external.string().nullable().optional(),
|
|
4623
|
-
promotedBy: exports_external.string().min(1)
|
|
4635
|
+
promotedBy: exports_external.string().min(1),
|
|
4636
|
+
allowUnverified: exports_external.boolean().optional()
|
|
4624
4637
|
});
|
|
4625
4638
|
var InstantiateRequestSchema = BaseRequestSchema.extend({
|
|
4626
4639
|
command: exports_external.literal("instantiate"),
|
|
@@ -4678,6 +4691,19 @@ var ListResultSchema = BaseResultSchema.extend({
|
|
|
4678
4691
|
artifacts: exports_external.array(ArtifactEnvelopeSchema),
|
|
4679
4692
|
nextCursor: exports_external.string().nullable().optional()
|
|
4680
4693
|
});
|
|
4694
|
+
var TemplatesResultSchema = BaseResultSchema.extend({
|
|
4695
|
+
command: exports_external.literal("templates"),
|
|
4696
|
+
templates: exports_external.array(exports_external.object({
|
|
4697
|
+
name: exports_external.string().min(1),
|
|
4698
|
+
artifactId: exports_external.string().min(1),
|
|
4699
|
+
revisionId: exports_external.string().min(1),
|
|
4700
|
+
revisionSha: exports_external.string().regex(/^[a-f0-9]{64}$/),
|
|
4701
|
+
promotedBy: exports_external.string().min(1),
|
|
4702
|
+
promotedAt: exports_external.string().datetime({ offset: true }),
|
|
4703
|
+
promotionOverride: exports_external.string().nullable(),
|
|
4704
|
+
sourceVerdict: exports_external.object({ status: RenderStatusSchema, tier: exports_external.union([exports_external.literal(0), exports_external.literal(1)]) }).nullable()
|
|
4705
|
+
}))
|
|
4706
|
+
});
|
|
4681
4707
|
var ReadBackResultSchema = BaseResultSchema.extend({
|
|
4682
4708
|
command: exports_external.literal("readBack"),
|
|
4683
4709
|
renderer: RendererSchema,
|
|
@@ -5402,7 +5428,7 @@ function setGalleryVerdict(document2, verdict) {
|
|
|
5402
5428
|
badge.dataset.status = verdict.status;
|
|
5403
5429
|
badge.childNodes[0].textContent = verdict.status.split(":")[0] ?? verdict.status;
|
|
5404
5430
|
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;
|
|
5431
|
+
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
5432
|
const insecure = verdict.insecure === undefined ? null : `INSECURE L${verdict.insecure.level}`;
|
|
5407
5433
|
const suffix = insecure === null ? `T${verdict.tier}` : `${insecure} · T${verdict.tier}`;
|
|
5408
5434
|
tier.textContent = detail === null ? `· ${suffix}` : `· ${detail} · ${suffix}`;
|
|
@@ -5546,6 +5572,42 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5546
5572
|
let removeThemePreferenceListener = noOp;
|
|
5547
5573
|
let generation = 0;
|
|
5548
5574
|
let activeFrame = null;
|
|
5575
|
+
let removeInteractionListener = noOp;
|
|
5576
|
+
const endInteractionSignal = () => {
|
|
5577
|
+
removeInteractionListener();
|
|
5578
|
+
const badge = document2.getElementById("facet-verdict");
|
|
5579
|
+
if (badge !== null)
|
|
5580
|
+
delete badge.dataset.interactionError;
|
|
5581
|
+
};
|
|
5582
|
+
const activateInteractionSignal = (frame) => {
|
|
5583
|
+
endInteractionSignal();
|
|
5584
|
+
const badge = document2.getElementById("facet-verdict");
|
|
5585
|
+
updateGalleryStatus("displayed");
|
|
5586
|
+
const frameWindow = frame.element.raw.contentWindow;
|
|
5587
|
+
if (frameWindow === null || typeof frameWindow.addEventListener !== "function")
|
|
5588
|
+
return;
|
|
5589
|
+
const markFailure = () => {
|
|
5590
|
+
if (expired || activeFrame !== frame || frame.renderResult === null)
|
|
5591
|
+
return;
|
|
5592
|
+
if (badge !== null)
|
|
5593
|
+
badge.dataset.interactionError = "true";
|
|
5594
|
+
updateGalleryStatus("displayed · runtime error during interaction");
|
|
5595
|
+
};
|
|
5596
|
+
frameWindow.addEventListener("error", markFailure, true);
|
|
5597
|
+
frameWindow.addEventListener("unhandledrejection", markFailure, true);
|
|
5598
|
+
const frameDocument = frame.element.raw.contentDocument;
|
|
5599
|
+
const observer = frameDocument == null || typeof MutationObserver !== "function" ? null : new MutationObserver(() => {
|
|
5600
|
+
if (frameDocument.querySelector("[data-facet-error]"))
|
|
5601
|
+
markFailure();
|
|
5602
|
+
});
|
|
5603
|
+
if (frameDocument !== null)
|
|
5604
|
+
observer?.observe(frameDocument, { childList: true, subtree: true });
|
|
5605
|
+
removeInteractionListener = () => {
|
|
5606
|
+
frameWindow.removeEventListener("error", markFailure, true);
|
|
5607
|
+
frameWindow.removeEventListener("unhandledrejection", markFailure, true);
|
|
5608
|
+
observer?.disconnect();
|
|
5609
|
+
};
|
|
5610
|
+
};
|
|
5549
5611
|
const expireSession = () => {
|
|
5550
5612
|
if (expired)
|
|
5551
5613
|
return;
|
|
@@ -5556,6 +5618,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5556
5618
|
exportMenu?.sync();
|
|
5557
5619
|
activeFrame?.renderResult?.setGestureMode("native");
|
|
5558
5620
|
swaps?.close();
|
|
5621
|
+
endInteractionSignal();
|
|
5559
5622
|
removeThemePreferenceListener();
|
|
5560
5623
|
clearSession(window2.sessionStorage);
|
|
5561
5624
|
renderSessionExpired(document2, (message) => setGalleryError(document2, message), (status) => setGalleryStatus(document2, status));
|
|
@@ -5687,6 +5750,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5687
5750
|
updateGalleryFavicon(source.verdict?.status ?? "unverified");
|
|
5688
5751
|
updateGalleryVerdict(source.verdict ?? null);
|
|
5689
5752
|
updateGalleryStatus("displayed");
|
|
5753
|
+
activateInteractionSignal(current);
|
|
5690
5754
|
updateLiveState("live");
|
|
5691
5755
|
const themeToggle = document2.getElementById("facet-theme-toggle");
|
|
5692
5756
|
const updateThemeToggle = (mode) => {
|
|
@@ -5745,6 +5809,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5745
5809
|
return;
|
|
5746
5810
|
current = next;
|
|
5747
5811
|
activeFrame = next;
|
|
5812
|
+
activateInteractionSignal(next);
|
|
5748
5813
|
commitTheme(nextMode, nextResolvedTheme);
|
|
5749
5814
|
syncPanZoomToggle();
|
|
5750
5815
|
syncZoomButtons();
|
|
@@ -5815,6 +5880,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5815
5880
|
if (!result.failedNewFrameReady) {
|
|
5816
5881
|
current = frame;
|
|
5817
5882
|
activeFrame = frame;
|
|
5883
|
+
activateInteractionSignal(frame);
|
|
5818
5884
|
source = revision;
|
|
5819
5885
|
commitTheme(revisionThemeMode, revisionTheme);
|
|
5820
5886
|
syncPanZoomToggle();
|
|
@@ -5870,6 +5936,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5870
5936
|
updateSwapBar("start");
|
|
5871
5937
|
updateGalleryFavicon("unverified");
|
|
5872
5938
|
updateGalleryVerdict(null);
|
|
5939
|
+
endInteractionSignal();
|
|
5873
5940
|
swaps?.enqueue({ kind: "revision", ...event });
|
|
5874
5941
|
},
|
|
5875
5942
|
onClose: (event) => {
|
|
@@ -5885,6 +5952,7 @@ async function startGallery(runtime = browserGalleryRuntime()) {
|
|
|
5885
5952
|
let zoomButtonPoll;
|
|
5886
5953
|
const shutdown = () => {
|
|
5887
5954
|
stream.close();
|
|
5955
|
+
removeInteractionListener();
|
|
5888
5956
|
removeThemePreferenceListener();
|
|
5889
5957
|
if (zoomButtonPoll !== undefined)
|
|
5890
5958
|
clearInterval(zoomButtonPoll);
|
|
@@ -98,12 +98,19 @@ body {
|
|
|
98
98
|
* wider than the stage (it start-aligns and stays reachable by
|
|
99
99
|
* scroll instead).
|
|
100
100
|
*/
|
|
101
|
+
#artifact[data-facet-artifact-type="html"],
|
|
102
|
+
#artifact[data-facet-artifact-type="tsx"] {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
#artifact[data-facet-artifact-type="html"] > [data-facet-renderer-root],
|
|
102
108
|
#artifact[data-facet-artifact-type="tsx"] > [data-facet-renderer-root] {
|
|
103
109
|
display: flex;
|
|
104
110
|
justify-content: safe center;
|
|
105
111
|
align-items: flex-start;
|
|
106
|
-
|
|
112
|
+
/* A full-height root plus a sibling runtime error makes capture bounds grow on every viewport resize. */
|
|
113
|
+
flex: 1 0 auto;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
#artifact > svg {
|
|
@@ -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");
|
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-48emrxg6.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,
|
|
@@ -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/mcp.md
CHANGED
|
@@ -4,12 +4,17 @@ On harnesses with shell access, the CLI is the integration; the MCP adapter is f
|
|
|
4
4
|
|
|
5
5
|
Facet's npm package includes the `facet-mcp` bin. It needs Bun `1.4.0`; npm and pnpm install the package, but Bun remains the runtime.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install globally, then run the bin with Bun `1.4.0` or newer:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
|
|
10
|
+
bun add -g @legionworks/facet
|
|
11
|
+
facet-mcp
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
Releases through `1.9.0` ship a `facet-mcp` bin that cannot launch directly. Use the next release or newer. A local package built with this fix answered JSON-RPC `initialize` after a global install. The `npx -p @legionworks/facet facet-mcp` path still needs verification against that published release before it is recommended.
|
|
15
|
+
|
|
16
|
+
Do not use `bunx -p @legionworks/facet facet-mcp` as the registration command without verifying the local Bun release. It exited immediately with status 1 on Bun `1.3.14` on the measured host; the same form also exited with status 1 under the available scratch Bun `1.4.0` runtime. These observations are environment-specific, so this caveat is not a universal claim about every Bun release or installation.
|
|
17
|
+
|
|
13
18
|
The adapter resolves the CLI in this order: `FACET_CLI`, then `bun <adapter-relative-repository>/src/cli/main.ts`, then `facet` on `PATH`. Set `FACET_CLI` to an absolute CLI executable when the adapter should use another installation.
|
|
14
19
|
|
|
15
20
|
## Register the server
|
|
@@ -21,7 +26,7 @@ OpenCode config:
|
|
|
21
26
|
"mcp": {
|
|
22
27
|
"facet": {
|
|
23
28
|
"type": "local",
|
|
24
|
-
"command": ["
|
|
29
|
+
"command": ["facet-mcp"],
|
|
25
30
|
"enabled": true
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -34,8 +39,7 @@ Claude Code project config (`.mcp.json`):
|
|
|
34
39
|
{
|
|
35
40
|
"mcpServers": {
|
|
36
41
|
"facet": {
|
|
37
|
-
"command": "
|
|
38
|
-
"args": ["-p", "@legionworks/facet", "facet-mcp"]
|
|
42
|
+
"command": "facet-mcp"
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
@@ -45,8 +49,7 @@ Codex config (`~/.codex/config.toml`):
|
|
|
45
49
|
|
|
46
50
|
```toml
|
|
47
51
|
[mcp_servers.facet]
|
|
48
|
-
command = "
|
|
49
|
-
args = ["-p", "@legionworks/facet", "facet-mcp"]
|
|
52
|
+
command = "facet-mcp"
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
Set `FACET_HOME` in the host configuration when the adapter must use a non-default Facet runtime directory. Set `FACET_CLI` only for an alternate CLI executable.
|
|
@@ -55,6 +58,7 @@ Set `FACET_HOME` in the host configuration when the adapter must use a non-defau
|
|
|
55
58
|
|
|
56
59
|
| Tool | Inputs | Effect |
|
|
57
60
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
61
|
+
| `facet_create` | `projectId`, `slug`, `title` | Creates an artifact; pass its returned `data.artifact.id` to publish. |
|
|
58
62
|
| `facet_publish` | `artifactId`, `type`, exactly one of `sourceText` or `file`; optional `execution`, `renderer`, `note`, `parentRevisionId` | Publishes inline source through CLI stdin or reads the named local file. |
|
|
59
63
|
| `facet_read_back` | `artifactId`; optional `revisionSha`, `tier` (`0` \| `1` \| `visual`) | Reads the latest or named revision. Tier 1 and visual need browser evidence. |
|
|
60
64
|
| `facet_status` | optional `artifactId`, `start` | Reads status. Set `start` only when activation is intended. |
|
|
@@ -65,7 +69,7 @@ Set `FACET_HOME` in the host configuration when the adapter must use a non-defau
|
|
|
65
69
|
|
|
66
70
|
Exactly one of `sourceText` or `file` is required. The adapter returns `invalid_request` when both or neither are supplied.
|
|
67
71
|
|
|
68
|
-
The MCP surface
|
|
72
|
+
The MCP surface has six artifact tools. Start a cold-home flow with `facet_create`, then call `facet_publish`, `facet_read_back`, and `facet_export`. Each tool advertises its input schema in `tools/list`; run `facet doctor` through the CLI.
|
|
69
73
|
|
|
70
74
|
## Result and error handling
|
|
71
75
|
|
|
@@ -73,6 +77,10 @@ Every tool returns one text content item containing the complete versioned Facet
|
|
|
73
77
|
|
|
74
78
|
Typed Facet failures return that same envelope with `isError: true`. The JSON body preserves `error.code`, `error.message`, `error.retryable`, and `error.details`. The adapter converts malformed CLI stdout and subprocess failures into typed `invalid_envelope` errors instead of throwing raw process text through MCP.
|
|
75
79
|
|
|
80
|
+
For `output_unwritable`, `error.details.out` is the absolute path whose write
|
|
81
|
+
failed. A malformed Mermaid fence is a verdict result, not a tool failure: the
|
|
82
|
+
call returns `isError: false` with `data.verdict.status: "error"`.
|
|
83
|
+
|
|
76
84
|
## Boundary
|
|
77
85
|
|
|
78
86
|
The adapter only shells out to `facet` and parses the shared wire envelope. It does not import service, validation, or gallery code. The boundary checker permits only the MCP SDK, Zod, Node builtins, adapter-local modules, shared contracts, and the shared product version in `src/harness-adapters/mcp/`.
|
|
@@ -107,6 +107,13 @@ service.
|
|
|
107
107
|
| `2` | Removes Tier 0 and Tier 1 network-namespace isolation. Real validators still run. |
|
|
108
108
|
| `3` | Performs no validation and records `insecure:unvalidated`. |
|
|
109
109
|
|
|
110
|
+
Level 3 skips publish-time validation, not an explicit visual read-back.
|
|
111
|
+
`facet read-back --tier visual` still runs Tier 1, and that verdict carries the
|
|
112
|
+
insecure marker. Evidence consumers must check the marker, not infer trust from
|
|
113
|
+
`status` alone. A level-3 visual verdict does not count toward `facet promote`
|
|
114
|
+
either — the gate refuses it as `insecure:unvalidated` unless the operator
|
|
115
|
+
passes `--allow-unverified`.
|
|
116
|
+
|
|
110
117
|
Levels compose as a forced floor: the effective level is never below the
|
|
111
118
|
operator's `FACET_INSECURE` value. `FACET_INSECURE_AUTO=1` may raise a level when
|
|
112
119
|
startup probes fail, but it never selects level 3. With auto mode off, hard
|
|
@@ -23,7 +23,7 @@ the evidence root and every per-run evidence directory.
|
|
|
23
23
|
## Schema migrations
|
|
24
24
|
|
|
25
25
|
`runMigrations` records applied versions in `schema_migrations` and applies
|
|
26
|
-
additive fragments in order. The current schema is
|
|
26
|
+
additive fragments in order. The current schema is v10:
|
|
27
27
|
|
|
28
28
|
| version | change |
|
|
29
29
|
| ------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -36,6 +36,7 @@ additive fragments in order. The current schema is v9:
|
|
|
36
36
|
| v7 | Backfills HTML observation defaults. |
|
|
37
37
|
| v8 | Adds `tsx`, declared revision execution, and nullable `render_runs.compiled_path`. |
|
|
38
38
|
| v9 | Adds `render_runs.screenshot_format`, recorded as `png` or `webp` for retained evidence. |
|
|
39
|
+
| v10 | Adds nullable `templates.promotion_override`; null for pre-v10 templates and for promotions that passed the verification gate. |
|
|
39
40
|
|
|
40
41
|
Migrations are additive and transactional. Existing revisions are not rewritten
|
|
41
42
|
when a later schema version is applied.
|
|
@@ -81,6 +82,10 @@ artifact and unlinks older screenshot and console files. Rows marked
|
|
|
81
82
|
`retained = 1` are exempt. Cleanup is best-effort; the database row remains the
|
|
82
83
|
authority and the orphan sweep can recover from stale files.
|
|
83
84
|
|
|
85
|
+
Facet targets one operator on a local machine. The evidence bound is the last
|
|
86
|
+
`EVIDENCE_LAST_N_PER_ARTIFACT` Tier 1 runs per artifact, plus any retained runs.
|
|
87
|
+
There is no quota; retained evidence can grow without a fixed limit.
|
|
88
|
+
|
|
84
89
|
## Templates
|
|
85
90
|
|
|
86
91
|
A template records one immutable revision ID. Later publication to the source
|
package/docs/reference/tsx.md
CHANGED
|
@@ -29,6 +29,15 @@ and must not call `createRoot` or self-mount.
|
|
|
29
29
|
|
|
30
30
|
## Imports and styling
|
|
31
31
|
|
|
32
|
+
Artifacts cannot fetch data (`connect-src 'none'`). Generate source with the
|
|
33
|
+
data embedded, then publish it. For repeated updates, regenerate the file and
|
|
34
|
+
use `facet publish --watch` to publish each changed version:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
node -e 'const data = [{label:"A", value:3}]; require("node:fs").writeFileSync("report.tsx", `export default function Report(){ return <pre>{JSON.stringify(${JSON.stringify(data)})}</pre> }`)'
|
|
38
|
+
facet publish --artifact-id <id> --type tsx --file report.tsx
|
|
39
|
+
```
|
|
40
|
+
|
|
32
41
|
The compiler accepts only vendored modules: `react`, `react-dom`,
|
|
33
42
|
`react-dom/client`, `react/jsx-runtime`, and `react/jsx-dev-runtime`.
|
|
34
43
|
Pinned versions are React and React DOM `19.2.8`, TypeScript `5.7.3`.
|
|
@@ -62,6 +71,11 @@ has no host port or service capability.
|
|
|
62
71
|
|
|
63
72
|
## Storage and export
|
|
64
73
|
|
|
74
|
+
The TSX verifier loads the vendored `artifact.css` used by the gallery frame.
|
|
75
|
+
A runtime error after initial render, such as one thrown by a click handler,
|
|
76
|
+
appears as a gallery signal in an `aria-live="polite"` status region. It does
|
|
77
|
+
not rewrite the immutable verdict and clears on revision swap or theme change.
|
|
78
|
+
|
|
65
79
|
Revision source remains immutable. Compilation creates derived bytes recorded at
|
|
66
80
|
the run's `compiled_path`; TSX source export writes the original `.tsx` bytes.
|
|
67
81
|
Render export remains the retained Tier 1 screenshot. Evidence retains the last
|