@legionworks/facet 1.10.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 (33) hide show
  1. package/dist/gallery/{chunk-48emrxg6.js → chunk-0by8qn4x.js} +15 -0
  2. package/dist/gallery/frame/artifact.css +3 -0
  3. package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-g3asgaw3.js} +8 -0
  4. package/dist/gallery/frame/frame.css +4 -24
  5. package/dist/gallery/frame/runtime/chart.js +1 -1
  6. package/dist/gallery/frame/runtime/html.js +1 -1
  7. package/dist/gallery/frame/runtime/markdown.js +1 -1
  8. package/dist/gallery/frame/runtime/mermaid.js +1 -1
  9. package/dist/gallery/frame/runtime/svg.js +1 -1
  10. package/dist/gallery/frame/runtime/tsx.js +1 -1
  11. package/dist/gallery/index.html +1 -1
  12. package/docs/reference/cli.md +1 -1
  13. package/docs/reference/html.md +3 -3
  14. package/docs/reference/mcp.md +15 -2
  15. package/docs/reference/tsx.md +3 -1
  16. package/docs/reference/validation.md +2 -2
  17. package/package.json +1 -1
  18. package/skills/facet/SKILL.md +3 -4
  19. package/src/gallery-web/app.ts +20 -1
  20. package/src/gallery-web/frame/runtime.ts +7 -1
  21. package/src/gallery-web/frame/styles/artifact.css +3 -0
  22. package/src/gallery-web/frame/styles/frame.css +4 -24
  23. package/src/gallery-web/frame-html.ts +5 -1
  24. package/src/harness-adapters/mcp/server.ts +2 -2
  25. package/src/service/dispatcher.ts +12 -0
  26. package/src/shared/html/style-vocabulary.ts +1 -0
  27. package/templates/capacity-report.tsx +1 -1
  28. package/templates/fleet-dashboard.html +1 -1
  29. package/templates/html-release-ledger.html +1 -1
  30. package/templates/html-status-report.html +1 -1
  31. package/templates/incident-console.tsx +1 -1
  32. package/templates/tsx-interactive-counter.tsx +1 -1
  33. package/templates/tsx-status-report.tsx +1 -1
@@ -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
  }
@@ -5190,6 +5193,13 @@ function createArtifactFrame(options) {
5190
5193
  renderResult = handle;
5191
5194
  return handle;
5192
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
+ },
5193
5203
  get renderResult() {
5194
5204
  return renderResult;
5195
5205
  }
@@ -5782,6 +5792,11 @@ async function startGallery(runtime = browserGalleryRuntime()) {
5782
5792
  const canCommit = () => themeGeneration === generation && nextMode === requestedThemeMode;
5783
5793
  if (!canCommit())
5784
5794
  return;
5795
+ if (usesArtifactStylesheet(source.artifactType)) {
5796
+ current.setTheme(nextResolvedTheme);
5797
+ commitTheme(nextMode, nextResolvedTheme);
5798
+ return;
5799
+ }
5785
5800
  const next = createArtifactFrame({
5786
5801
  artifactType: source.artifactType,
5787
5802
  renderer: source.renderer,
@@ -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,26 +78,7 @@ body {
78
78
  padding-inline: max(16px, calc((100% - 92ch) / 2));
79
79
  }
80
80
 
81
- /*
82
- * Native dashboard artifacts (html, interactive tsx) read top-down
83
- * like markdown — vertical stays start-aligned — but unlike markdown
84
- * their width is whatever the fixture authored (often a fixed-width
85
- * `max-w-*` container), not a fluid readable column. Left unstyled,
86
- * that narrow content sits pinned to the container's left edge,
87
- * stranding most of a wide desktop stage as dead space.
88
- *
89
- * The centering has to land on the renderer-ROOT element
90
- * (`.facet-html-root` / `#facet-tsx-mount`), not `#artifact` itself:
91
- * `renderHtml`/`renderTsx` always mount a single full-width wrapper
92
- * as `#artifact`'s only child, and the fixture's own narrow content
93
- * (e.g. `<main class="max-w-2xl">`) is a GRANDCHILD of `#artifact`
94
- * inside that wrapper — centering `#artifact`'s single full-width
95
- * child does nothing. `safe center` on the horizontal axis only —
96
- * the same fallback-to-start mechanism the standalone-diagram rule
97
- * above uses — centers content that fits and never clips content
98
- * wider than the stage (it start-aligns and stays reachable by
99
- * scroll instead).
100
- */
81
+ /* These selectors match usesArtifactStylesheet in frame-html.ts; CSS cannot import it. */
101
82
  #artifact[data-facet-artifact-type="html"],
102
83
  #artifact[data-facet-artifact-type="tsx"] {
103
84
  display: flex;
@@ -106,10 +87,9 @@ body {
106
87
 
107
88
  #artifact[data-facet-artifact-type="html"] > [data-facet-renderer-root],
108
89
  #artifact[data-facet-artifact-type="tsx"] > [data-facet-renderer-root] {
109
- display: flex;
110
- justify-content: safe center;
111
- align-items: flex-start;
112
- /* A full-height root plus a sibling runtime error makes capture bounds grow on every viewport resize. */
90
+ /* Block flow preserves authored sibling order while keeping the renderer root a flex item of the stage. */
91
+ display: block;
92
+ /* Filling the stage keeps a sibling runtime-error line outside capture bounds as the viewport resizes. */
113
93
  flex: 1 0 auto;
114
94
  }
115
95
 
@@ -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,
@@ -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-2dge4gwb.css"><script type="module" crossorigin src="./chunk-48emrxg6.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">
@@ -125,7 +125,7 @@ errors and next actions:
125
125
  | code | next action |
126
126
  | ---------------------- | -------------------------------------------------------------------------------------------------------------- |
127
127
  | `artifact_not_found` | Check the artifact ID and list the project. |
128
- | `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. |
129
129
  | `duplicate_revision` | Reuse `error.details.revisionSha`; no new revision was stored. |
130
130
  | `output_unwritable` | Fix the output directory, permissions, or rename target. |
131
131
  | `evidence_unavailable` | Run Tier 1 again or inspect retention and evidence paths. |
@@ -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
 
@@ -11,9 +11,14 @@ bun add -g @legionworks/facet
11
11
  facet-mcp
12
12
  ```
13
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.
14
+ Releases through `1.9.0` ship a `facet-mcp` bin that cannot launch: its entry file has no `#!/usr/bin/env bun` line. Use `1.10.0` or newer. With `1.10.0` on Bun `1.4.0`, each of these answered JSON-RPC `initialize` and listed six tools:
15
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.
16
+ - `bun add -g @legionworks/facet`, then `facet-mcp`
17
+ - `npm i -g @legionworks/facet`, then `facet-mcp`
18
+ - `npx -p @legionworks/facet facet-mcp`
19
+ - `bunx -p @legionworks/facet facet-mcp`
20
+
21
+ Run `npx -p` from outside a Facet checkout. Inside one, npx resolves the local project instead of the published package and fails with `facet-mcp: command not found`.
17
22
 
18
23
  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.
19
24
 
@@ -69,6 +74,14 @@ Set `FACET_HOME` in the host configuration when the adapter must use a non-defau
69
74
 
70
75
  Exactly one of `sourceText` or `file` is required. The adapter returns `invalid_request` when both or neither are supplied.
71
76
 
77
+ Tier 0 means structure was parsed and compiled, not rendered. Request
78
+ `facet_read_back` with `tier: "visual"` before reporting a render. Visual
79
+ evidence covers the first rendered state of an interactive TSX artifact;
80
+ later interaction is not validated and does not change the stored verdict.
81
+ When testing by hand with the MCP Inspector CLI, `--tool-arg key=value` passes
82
+ `\n` literally, so multi-line `sourceText` arrives as one line; pass `file`
83
+ instead.
84
+
72
85
  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.
73
86
 
74
87
  ## Result and error handling
@@ -74,7 +74,9 @@ has no host port or service capability.
74
74
  The TSX verifier loads the vendored `artifact.css` used by the gallery frame.
75
75
  A runtime error after initial render, such as one thrown by a click handler,
76
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.
77
+ not rewrite the immutable verdict. It clears on revision swap. Switching the
78
+ theme keeps the frame mounted, including interactive component state and any
79
+ runtime error signal already displayed.
78
80
 
79
81
  Revision source remains immutable. Compilation creates derived bytes recorded at
80
82
  the run's `compiled_path`; TSX source export writes the original `.tsx` bytes.
@@ -80,8 +80,8 @@ capture it emulates `prefers-reduced-motion: reduce` and awaits
80
80
  Tier 1 uses the same artifact-type layout rules as the gallery frame: Mermaid,
81
81
  SVG, and chart roots are safely centered on both axes, with oversized content
82
82
  remaining reachable by scrolling; Markdown is top-aligned in a horizontally
83
- centered column capped at about 92ch; HTML and TSX are top-aligned while their
84
- renderer roots are safely centered across the stage. The renderer-parity gate
83
+ centered column capped at about 92ch; HTML and TSX use top-aligned document
84
+ flow, where a fixed-width layout can center itself with `mx-auto`. The renderer-parity gate
85
85
  compares both the gallery and verifier renderer-module sets and CSS/stylesheet
86
86
  sets.
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legionworks/facet",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "artifacts",
@@ -93,7 +93,7 @@ non-whitespace text; the verdict requires a screenshot.
93
93
  | ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
94
94
  | `invalid_request` | Correct the command flags, artifact type, or source bytes, then retry. |
95
95
  | `artifact_not_found` | List or create the artifact, then use its returned ID. |
96
- | `revision_not_found` | Omit the SHA for latest, or replace it with a returned revision SHA. |
96
+ | `revision_not_found` | Check that the artifact has a revision; omit the SHA for latest, or replace it with a returned revision SHA. |
97
97
  | `duplicate_revision` | Use `error.details.revisionSha` as the existing revision SHA; do not republish identical bytes. |
98
98
  | `evidence_unavailable` | Request Tier 1 evidence for that revision before a render export, or export source instead. |
99
99
  | `output_unwritable` | Choose a writable `--out` path, resolve collisions, or use `--force` deliberately. |
@@ -114,9 +114,8 @@ non-whitespace text; the verdict requires a screenshot.
114
114
  - With shell access, the CLI is the integration — MCP is for structured-tool-only hosts, even when the host owns an MCP client. On that surface, `facet_open_url` is safe because it always uses `--no-launch`.
115
115
  - For MCP registration, use `bun add -g @legionworks/facet` and the bare
116
116
  `facet-mcp` command, or the no-install fallback `npx -p @legionworks/facet
117
- facet-mcp`. The measured `bunx -p @legionworks/facet facet-mcp` form exited
118
- with status 1 under Bun `1.3.14` and the available scratch Bun `1.4.0`; treat
119
- that as an environment-specific caveat, not a universal Bun claim.
117
+ facet-mcp` (`bunx -p` also works). Use `1.10.0` or newer; earlier releases
118
+ ship a `facet-mcp` bin that cannot launch.
120
119
 
121
120
  ## Deeper reference
122
121
 
@@ -23,7 +23,12 @@
23
23
  * surfaces an error badge.
24
24
  */
25
25
 
26
- import { assertLoopbackHostname, buildFrameAttributes, type FrameAttributes } from "./frame-html";
26
+ import {
27
+ assertLoopbackHostname,
28
+ buildFrameAttributes,
29
+ usesArtifactStylesheet,
30
+ type FrameAttributes,
31
+ } from "./frame-html";
27
32
  import { planSwap, type SwapPlanStep } from "./swap";
28
33
  import { connectRevisionStream } from "./sse-client";
29
34
  import type { VerdictObserved } from "../shared/contracts/validation";
@@ -376,6 +381,7 @@ export interface CreatedArtifactFrame {
376
381
  payload: FrameRenderPayload,
377
382
  timeoutMs: number,
378
383
  ) => Promise<FrameRenderResultHandle>;
384
+ readonly setTheme: (theme: ResolvedGalleryTheme) => void;
379
385
  /** Handle from the last successful render (null before the first). */
380
386
  readonly renderResult: FrameRenderResultHandle | null;
381
387
  }
@@ -488,6 +494,14 @@ export function createArtifactFrame(options: CreateArtifactFrameOptions): Create
488
494
  renderResult = handle;
489
495
  return handle;
490
496
  },
497
+ setTheme(theme) {
498
+ // oxlint-disable-next-line no-underscore-dangle
499
+ const api = raw.contentWindow?.__facetFrame;
500
+ if (api === null || api === undefined || typeof api.setTheme !== "function") {
501
+ throw new Error("frame theme API unavailable");
502
+ }
503
+ api.setTheme(theme);
504
+ },
491
505
  get renderResult(): FrameRenderResultHandle | null {
492
506
  return renderResult;
493
507
  },
@@ -1309,6 +1323,11 @@ export async function startGallery(runtime = browserGalleryRuntime()): Promise<v
1309
1323
  const canCommit = (): boolean =>
1310
1324
  themeGeneration === generation && nextMode === requestedThemeMode;
1311
1325
  if (!canCommit()) return;
1326
+ if (usesArtifactStylesheet(source.artifactType)) {
1327
+ current.setTheme(nextResolvedTheme);
1328
+ commitTheme(nextMode, nextResolvedTheme);
1329
+ return;
1330
+ }
1312
1331
  const next = createArtifactFrame({
1313
1332
  artifactType: source.artifactType,
1314
1333
  renderer: source.renderer,
@@ -1,5 +1,5 @@
1
1
  import { isTsxExecutionMode, type TsxExecutionMode } from "../../shared/tsx/execution";
2
- import { isResolvedGalleryTheme } from "../theme";
2
+ import { galleryDataTheme, isResolvedGalleryTheme, type ResolvedGalleryTheme } from "../theme";
3
3
 
4
4
  import { validateRenderer } from "./renderer-validation";
5
5
  import {
@@ -80,6 +80,7 @@ interface DiagramRegionGestures {
80
80
 
81
81
  export interface GalleryFrameApi {
82
82
  render(payload: FrameRenderPayload): Promise<RenderResult>;
83
+ setTheme(theme: ResolvedGalleryTheme): void;
83
84
  }
84
85
 
85
86
  declare global {
@@ -410,6 +411,11 @@ export function installGalleryFrameApi(registry: RendererRegistry): void {
410
411
  };
411
412
 
412
413
  const api: GalleryFrameApi = {
414
+ setTheme(theme) {
415
+ if (!isResolvedGalleryTheme(theme))
416
+ throw new FacetRenderError("frame theme is invalid", "invalid_request");
417
+ document.documentElement.dataset.theme = galleryDataTheme(theme);
418
+ },
413
419
  async render(payload: FrameRenderPayload): Promise<RenderResult> {
414
420
  if (rendered) throw new FacetRenderError("frame already rendered", "invalid_request");
415
421
  const validated = validatePayload(payload);
@@ -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
  }
@@ -78,26 +78,7 @@ body {
78
78
  padding-inline: max(16px, calc((100% - 92ch) / 2));
79
79
  }
80
80
 
81
- /*
82
- * Native dashboard artifacts (html, interactive tsx) read top-down
83
- * like markdown — vertical stays start-aligned — but unlike markdown
84
- * their width is whatever the fixture authored (often a fixed-width
85
- * `max-w-*` container), not a fluid readable column. Left unstyled,
86
- * that narrow content sits pinned to the container's left edge,
87
- * stranding most of a wide desktop stage as dead space.
88
- *
89
- * The centering has to land on the renderer-ROOT element
90
- * (`.facet-html-root` / `#facet-tsx-mount`), not `#artifact` itself:
91
- * `renderHtml`/`renderTsx` always mount a single full-width wrapper
92
- * as `#artifact`'s only child, and the fixture's own narrow content
93
- * (e.g. `<main class="max-w-2xl">`) is a GRANDCHILD of `#artifact`
94
- * inside that wrapper — centering `#artifact`'s single full-width
95
- * child does nothing. `safe center` on the horizontal axis only —
96
- * the same fallback-to-start mechanism the standalone-diagram rule
97
- * above uses — centers content that fits and never clips content
98
- * wider than the stage (it start-aligns and stays reachable by
99
- * scroll instead).
100
- */
81
+ /* These selectors match usesArtifactStylesheet in frame-html.ts; CSS cannot import it. */
101
82
  #artifact[data-facet-artifact-type="html"],
102
83
  #artifact[data-facet-artifact-type="tsx"] {
103
84
  display: flex;
@@ -106,10 +87,9 @@ body {
106
87
 
107
88
  #artifact[data-facet-artifact-type="html"] > [data-facet-renderer-root],
108
89
  #artifact[data-facet-artifact-type="tsx"] > [data-facet-renderer-root] {
109
- display: flex;
110
- justify-content: safe center;
111
- align-items: flex-start;
112
- /* A full-height root plus a sibling runtime error makes capture bounds grow on every viewport resize. */
90
+ /* Block flow preserves authored sibling order while keeping the renderer root a flex item of the stage. */
91
+ display: block;
92
+ /* Filling the stage keeps a sibling runtime-error line outside capture bounds as the viewport resizes. */
113
93
  flex: 1 0 auto;
114
94
  }
115
95
 
@@ -31,6 +31,10 @@ export function buildFrameAttributes(src = "/gallery/frame"): FrameAttributes {
31
31
  };
32
32
  }
33
33
 
34
+ export function usesArtifactStylesheet(artifactType: string): boolean {
35
+ return artifactType === "html" || artifactType === "tsx";
36
+ }
37
+
34
38
  export function buildFrameDocument(options: {
35
39
  artifactType: string;
36
40
  runtimeUrl: string;
@@ -45,7 +49,7 @@ export function buildFrameDocument(options: {
45
49
  return (
46
50
  `<!doctype html><html data-theme="${galleryDataTheme(theme)}"><head>` +
47
51
  `<meta charset="utf-8"><link rel="stylesheet" href="/gallery/frame/frame.css">` +
48
- (artifactType === "html" || artifactType === "tsx"
52
+ (usesArtifactStylesheet(artifactType)
49
53
  ? `<link rel="stylesheet" href="/gallery/frame/artifact.css">`
50
54
  : "") +
51
55
  "</head><body>" +
@@ -145,7 +145,7 @@ export function createFacetMcpServer(): Server {
145
145
  ),
146
146
  facet_publish: defineTool(
147
147
  PublishToolSchema,
148
- "Publish exactly one sourceText or local file. Check envelope.ok separately from data.verdict.status: stored verdict status error is not a transport failure.",
148
+ 'Publish exactly one sourceText or local file. Check envelope.ok separately from data.verdict.status: a stored verdict status of error is not a transport failure. Tier 0 parses and compiles structure but is not rendered; call facet_read_back with tier: "visual" before reporting a render.',
149
149
  async (input) => {
150
150
  const source = publishSource(input);
151
151
  return invoke(buildFacetArgs("publish", { ...input, ...source }), source);
@@ -153,7 +153,7 @@ export function createFacetMcpServer(): Server {
153
153
  ),
154
154
  facet_read_back: defineTool(
155
155
  ReadBackToolSchema,
156
- "Read back the latest or revision-bound stored verdict at Tier 0, Tier 1, or visual. Tier 1 and visual need browser evidence; inspect envelope.ok before verdict status.",
156
+ "Read back a stored verdict at Tier 0, Tier 1, or visual. Tier 1 and visual need browser evidence from the pinned browser; inspect envelope.ok before verdict status. Visual covers the first rendered state; later interaction is not validated and does not change the stored verdict.",
157
157
  async (input) => invoke(buildFacetArgs("read_back", input)),
158
158
  ),
159
159
  facet_status: defineTool(
@@ -526,6 +526,12 @@ export async function dispatch(
526
526
  ? deps.repository.getLatestRevision(command.artifactId)
527
527
  : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
528
528
  if (revision === null) {
529
+ if (deps.repository.getArtifactById(command.artifactId) === null) {
530
+ throw new FacetError("artifact_not_found", "Artifact not found", {
531
+ retryable: false,
532
+ details: { artifactId: command.artifactId },
533
+ });
534
+ }
529
535
  throw new FacetError("revision_not_found", "Revision not found", {
530
536
  retryable: false,
531
537
  details: {
@@ -614,6 +620,12 @@ export async function dispatch(
614
620
  ? deps.repository.getLatestRevision(command.artifactId)
615
621
  : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
616
622
  if (revision === null) {
623
+ if (deps.repository.getArtifactById(command.artifactId) === null) {
624
+ throw new FacetError("artifact_not_found", "Artifact not found", {
625
+ retryable: false,
626
+ details: { artifactId: command.artifactId },
627
+ });
628
+ }
617
629
  throw new FacetError("revision_not_found", "Revision not found", {
618
630
  retryable: false,
619
631
  details: {
@@ -28,6 +28,7 @@ export const HTML_TAILWIND_CLASSES = [
28
28
  "mt-4",
29
29
  "mb-2",
30
30
  "mb-4",
31
+ "mx-auto",
31
32
  "w-full",
32
33
  "max-w-prose",
33
34
  "max-w-2xl",
@@ -16,7 +16,7 @@ export default function CapacityReport() {
16
16
  const capacityPercent = Math.round((readyWorkers / workerCapacity) * 100);
17
17
 
18
18
  return (
19
- <main className="max-w-2xl p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
19
+ <main className="max-w-2xl mx-auto p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
20
20
  <header className="flex flex-col gap-2">
21
21
  <div className="flex flex-wrap gap-2">
22
22
  <span className="badge">capacity report</span>
@@ -4,7 +4,7 @@
4
4
  <title>Fleet dashboard</title>
5
5
  </head>
6
6
  <body class="bg-legion-paper text-legion-ink leading-relaxed">
7
- <main class="max-w-2xl p-6 gap-6 flex flex-col">
7
+ <main class="max-w-2xl mx-auto p-6 gap-6 flex flex-col">
8
8
  <header class="flex flex-col gap-2">
9
9
  <div class="flex flex-wrap gap-2 items-center">
10
10
  <span class="badge">operations</span>
@@ -4,7 +4,7 @@
4
4
  <title>Release ledger</title>
5
5
  </head>
6
6
  <body class="bg-legion-paper text-legion-ink leading-relaxed">
7
- <main class="max-w-2xl p-6 gap-6 flex flex-col">
7
+ <main class="max-w-2xl mx-auto p-6 gap-6 flex flex-col">
8
8
  <header class="flex flex-col gap-2">
9
9
  <div class="flex flex-wrap items-center justify-between gap-2">
10
10
  <span class="badge">release ledger</span>
@@ -20,7 +20,7 @@
20
20
  <title>Status report</title>
21
21
  </head>
22
22
  <body class="bg-legion-paper text-legion-ink leading-relaxed">
23
- <main class="max-w-2xl p-6 gap-4 flex flex-col">
23
+ <main class="max-w-2xl mx-auto p-6 gap-4 flex flex-col">
24
24
  <header class="flex flex-col gap-2">
25
25
  <h1 class="text-2xl font-bold">Status report</h1>
26
26
  <p class="text-legion-muted text-sm">
@@ -44,7 +44,7 @@ export default function IncidentConsole() {
44
44
  const criticalCount = incidents.filter((incident) => incident.severity === "critical").length;
45
45
 
46
46
  return (
47
- <main className="max-w-2xl p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
47
+ <main className="max-w-2xl mx-auto p-6 gap-6 flex flex-col bg-legion-paper text-legion-ink">
48
48
  <header className="flex flex-col gap-2">
49
49
  <div className="flex flex-wrap gap-2 items-center">
50
50
  <span className="badge">incident console</span>
@@ -4,7 +4,7 @@ export default function InteractiveCounter() {
4
4
  const [count, setCount] = useState(0);
5
5
 
6
6
  return (
7
- <section className="card bg-legion-paper p-6 w-full max-w-prose">
7
+ <section className="card bg-legion-paper p-6 w-full max-w-prose mx-auto">
8
8
  <h1 className="text-2xl font-semibold text-legion-ink">Interactive counter</h1>
9
9
  <p className="text-legion-muted mt-2">Button presses: {count}</p>
10
10
  <button
@@ -2,7 +2,7 @@ import React from "react";
2
2
 
3
3
  export default function StatusReport() {
4
4
  return (
5
- <section className="card bg-legion-paper p-6 w-full max-w-prose">
5
+ <section className="card bg-legion-paper p-6 w-full max-w-prose mx-auto">
6
6
  <h1 className="text-2xl font-semibold text-legion-ink">Status report</h1>
7
7
  <p className="text-legion-muted mt-2">Verification state for the current release.</p>
8
8
  <section className="mt-4">