@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.
Files changed (77) hide show
  1. package/README.md +50 -21
  2. package/dist/gallery/{chunk-c6s9k9ty.js → chunk-0by8qn4x.js} +86 -3
  3. package/dist/gallery/{chunk-4g8rem85.css → chunk-2dge4gwb.css} +22 -1
  4. package/dist/gallery/frame/artifact.css +3 -0
  5. package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-g3asgaw3.js} +8 -0
  6. package/dist/gallery/frame/frame.css +11 -24
  7. package/dist/gallery/frame/runtime/chart.js +24 -2
  8. package/dist/gallery/frame/runtime/html.js +1 -1
  9. package/dist/gallery/frame/runtime/markdown.js +1 -1
  10. package/dist/gallery/frame/runtime/mermaid.js +1 -1
  11. package/dist/gallery/frame/runtime/svg.js +1 -1
  12. package/dist/gallery/frame/runtime/tsx.js +1 -1
  13. package/dist/gallery/index.html +4 -2
  14. package/docs/reference/cli.md +30 -2
  15. package/docs/reference/export.md +4 -0
  16. package/docs/reference/html.md +3 -3
  17. package/docs/reference/mcp.md +29 -8
  18. package/docs/reference/security.md +7 -0
  19. package/docs/reference/storage.md +6 -1
  20. package/docs/reference/tsx.md +16 -0
  21. package/docs/reference/validation.md +34 -6
  22. package/package.json +4 -4
  23. package/skills/facet/SKILL.md +24 -2
  24. package/src/cli/commands/promote.ts +1 -0
  25. package/src/cli/commands/templates.ts +25 -0
  26. package/src/cli/main.ts +4 -0
  27. package/src/cli/parser.ts +4 -0
  28. package/src/cli/presenter.ts +48 -0
  29. package/src/gallery-web/app.ts +65 -2
  30. package/src/gallery-web/favicon.ts +1 -0
  31. package/src/gallery-web/frame/renderers/chart.ts +31 -1
  32. package/src/gallery-web/frame/runtime.ts +7 -1
  33. package/src/gallery-web/frame/styles/artifact.css +3 -0
  34. package/src/gallery-web/frame/styles/frame.css +11 -24
  35. package/src/gallery-web/frame-html.ts +7 -2
  36. package/src/gallery-web/index.html +3 -1
  37. package/src/gallery-web/styles/verdict.css +19 -4
  38. package/src/harness-adapters/mcp/cli-bridge.ts +11 -1
  39. package/src/harness-adapters/mcp/main.ts +2 -0
  40. package/src/harness-adapters/mcp/server.ts +92 -69
  41. package/src/harness-adapters/mcp/tool-schemas.ts +7 -0
  42. package/src/service/dispatcher.ts +22 -0
  43. package/src/service/router-guards.ts +2 -0
  44. package/src/service/store/migrations.ts +12 -2
  45. package/src/service/store/repository-lifecycle.ts +91 -19
  46. package/src/service/store/repository.ts +60 -2
  47. package/src/service/store/schema.ts +4 -0
  48. package/src/service/stored-verdict.ts +3 -0
  49. package/src/shared/contracts/artifact.ts +1 -0
  50. package/src/shared/contracts/commands/index.ts +10 -0
  51. package/src/shared/contracts/commands/names.ts +2 -0
  52. package/src/shared/contracts/commands/requests.ts +7 -0
  53. package/src/shared/contracts/commands/results.ts +20 -1
  54. package/src/shared/contracts/promotion.ts +16 -0
  55. package/src/shared/contracts/validation.ts +7 -0
  56. package/src/shared/errors/facet-error.ts +2 -0
  57. package/src/shared/errors/store-error.ts +4 -0
  58. package/src/shared/html/artifact-main.ts +3 -0
  59. package/src/shared/html/style-vocabulary.ts +1 -0
  60. package/src/shared/storage-version.ts +1 -1
  61. package/src/validation/tier0/dom-shim.ts +3 -18
  62. package/src/validation/tier0/markdown.ts +28 -5
  63. package/src/validation/tier0/mermaid.ts +17 -8
  64. package/src/validation/tier0/worker-dispatch.ts +1 -1
  65. package/src/validation/tier1/entries/tsx.ts +1 -0
  66. package/src/validation/tier1/harness.ts +2 -1
  67. package/src/validation/tier1/isolated-probe.ts +4 -0
  68. package/src/validation/tier1/protocol-probe.ts +37 -0
  69. package/src/validation/tier1/runner.ts +21 -4
  70. package/src/validation/tier1/verdict.ts +24 -39
  71. package/templates/capacity-report.tsx +1 -1
  72. package/templates/fleet-dashboard.html +1 -1
  73. package/templates/html-release-ledger.html +1 -1
  74. package/templates/html-status-report.html +1 -1
  75. package/templates/incident-console.tsx +1 -1
  76. package/templates/tsx-interactive-counter.tsx +1 -1
  77. 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`. **Bun is the required runtime**:
45
- the CLI runs on Bun `1.4.0` or newer. npm and pnpm are distribution channels,
46
- not Node runtime support; a machine without Bun cannot run the installed CLI and
47
- will receive a runtime failure.
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 pinned browser used for visual read-back downloads on the first visual
69
- read-back. See the [CLI reference](docs/reference/cli.md) for the command
70
- contract and [MCP reference](docs/reference/mcp.md) for structured-tool-only
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
- | `tampered` | `⊘` | `#ff757f` | filled alarm badge |
86
- | `timeout` | `◌` | `#737aa2` | dim outline |
87
- | `shim_only` | `◇` | `#737aa2` | dim outline |
88
- | `probe_only` | `◈` | `#737aa2` | dim outline |
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 a stdio MCP adapter with five CLI-backed tools: publish, read-back, status, export, and a no-launch frame URL lookup. It requires Bun `1.4.0`. See the [MCP reference](docs/reference/mcp.md) for OpenCode, Claude Code, and Codex registration.
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="tampered"]:before, .facet-verdict[data-status="insecure:unvalidated"]:before {
218
+ .facet-verdict[data-status="tampered"]:before, .facet-verdict[data-status="insecure:unvalidated"]:before {
198
219
  content: "⊘";
199
220
  }
200
221
 
@@ -5256,6 +5256,9 @@
5256
5256
  .mx-16 {
5257
5257
  margin-inline: calc(var(--spacing) * 16);
5258
5258
  }
5259
+ .mx-auto {
5260
+ margin-inline: auto;
5261
+ }
5259
5262
  .my-0 {
5260
5263
  margin-block: 0px;
5261
5264
  }
@@ -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
- * 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. */
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
- display: flex;
104
- justify-content: safe center;
105
- align-items: flex-start;
106
- min-height: 100%;
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-fvfncadf.js";
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");
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunks/markdown-rd8w0ng9.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-g3asgaw3.js";
7
7
  import {
8
8
  createRendererRegistry
9
9
  } from "../chunks/markdown-d1wfbs70.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  installGalleryFrameApi
3
- } from "../chunks/markdown-fvfncadf.js";
3
+ } from "../chunks/markdown-g3asgaw3.js";
4
4
  import {
5
5
  createRendererRegistry,
6
6
  decodeArtifactBytes
@@ -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-fvfncadf.js";
10
+ } from "../chunks/markdown-g3asgaw3.js";
11
11
  import {
12
12
  createRendererRegistry
13
13
  } from "../chunks/markdown-d1wfbs70.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunks/markdown-zazx9h4n.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-g3asgaw3.js";
7
7
  import {
8
8
  createRendererRegistry
9
9
  } from "../chunks/markdown-d1wfbs70.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunks/markdown-rd8w0ng9.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-g3asgaw3.js";
7
7
  import {
8
8
  RENDER_ERROR_ATTRIBUTE,
9
9
  appendRenderError,
@@ -8,7 +8,7 @@
8
8
 
9
9
 
10
10
 
11
- <link rel="stylesheet" crossorigin href="./chunk-4g8rem85.css"><script type="module" crossorigin src="./chunk-c6s9k9ty.js"></script></head>
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">idle</span>
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
@@ -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` | Omit the SHA for latest read-back or use an existing SHA. |
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`
@@ -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
@@ -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
- 59 recommended utilities.
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 · 64 documented recommendations.
153
+ 6 recommended components · 65 documented recommendations.
154
154
 
155
155
  <!-- VOCABULARY:END -->
156
156