@legionworks/facet 1.10.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) 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-zazx9h4n.js → markdown-31ee7zgx.js} +1 -1
  4. package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-7scxp44j.js} +9 -1
  5. package/dist/gallery/frame/chunks/{markdown-z1wc0v0d.js → markdown-mksxbxx8.js} +2 -2
  6. package/dist/gallery/frame/chunks/{markdown-d1wfbs70.js → markdown-n9xcxy80.js} +14 -9
  7. package/dist/gallery/frame/chunks/{markdown-rd8w0ng9.js → markdown-rkt612xp.js} +1 -1
  8. package/dist/gallery/frame/chunks/{mermaid-de7p4v0x.js → mermaid-b8fxhsgg.js} +3 -3
  9. package/dist/gallery/frame/frame.css +4 -24
  10. package/dist/gallery/frame/runtime/chart.js +3 -3
  11. package/dist/gallery/frame/runtime/html.js +3 -3
  12. package/dist/gallery/frame/runtime/markdown.js +8 -4
  13. package/dist/gallery/frame/runtime/mermaid.js +4 -4
  14. package/dist/gallery/frame/runtime/svg.js +3 -3
  15. package/dist/gallery/frame/runtime/tsx.js +3 -3
  16. package/dist/gallery/index.html +1 -1
  17. package/docs/reference/cli.md +1 -1
  18. package/docs/reference/html.md +3 -3
  19. package/docs/reference/mcp.md +15 -2
  20. package/docs/reference/tsx.md +3 -1
  21. package/docs/reference/validation.md +12 -4
  22. package/package.json +1 -1
  23. package/skills/facet/SKILL.md +3 -4
  24. package/src/gallery-web/app.ts +20 -1
  25. package/src/gallery-web/frame/renderers/markdown.ts +5 -1
  26. package/src/gallery-web/frame/renderers/registry.ts +17 -10
  27. package/src/gallery-web/frame/runtime.ts +7 -1
  28. package/src/gallery-web/frame/styles/artifact.css +3 -0
  29. package/src/gallery-web/frame/styles/frame.css +4 -24
  30. package/src/gallery-web/frame-html.ts +5 -1
  31. package/src/harness-adapters/mcp/server.ts +2 -2
  32. package/src/service/dispatcher.ts +12 -0
  33. package/src/shared/html/policy.ts +9 -0
  34. package/src/shared/html/style-vocabulary.ts +1 -0
  35. package/src/validation/tier0/html.ts +6 -9
  36. package/src/validation/tier0/markdown.ts +2 -12
  37. package/src/validation/tier1/isolated-probe.ts +20 -11
  38. package/src/validation/tier1/limits.ts +11 -6
  39. package/src/validation/tier1/protocol-probe.ts +60 -26
  40. package/src/validation/tier1/runner.ts +173 -10
  41. package/src/validation/tier1/verdict.ts +9 -0
  42. package/templates/capacity-report.tsx +1 -1
  43. package/templates/fleet-dashboard.html +1 -1
  44. package/templates/html-release-ledger.html +1 -1
  45. package/templates/html-status-report.html +1 -1
  46. package/templates/incident-console.tsx +1 -1
  47. package/templates/tsx-interactive-counter.tsx +1 -1
  48. 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
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FacetRenderError,
3
3
  decodeArtifactBytes
4
- } from "./markdown-d1wfbs70.js";
4
+ } from "./markdown-n9xcxy80.js";
5
5
 
6
6
  // src/gallery-web/frame/renderers/svg.ts
7
7
  var STRIPPED_TAGS = new Set([
@@ -5,12 +5,15 @@ import {
5
5
  dispatchRender,
6
6
  isRenderer,
7
7
  isTsxExecutionMode
8
- } from "./markdown-d1wfbs70.js";
8
+ } from "./markdown-n9xcxy80.js";
9
9
 
10
10
  // src/gallery-web/theme.ts
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");
@@ -10,11 +10,11 @@ import {
10
10
  } from "./markdown-h7pzv34e.js";
11
11
  import {
12
12
  importSanitizedSvgText
13
- } from "./markdown-zazx9h4n.js";
13
+ } from "./markdown-31ee7zgx.js";
14
14
  import {
15
15
  FacetRenderError,
16
16
  decodeArtifactBytes
17
- } from "./markdown-d1wfbs70.js";
17
+ } from "./markdown-n9xcxy80.js";
18
18
  import {
19
19
  dedent
20
20
  } from "./markdown-qx3cngz2.js";
@@ -51,6 +51,15 @@ function isHtmlEventHandlerAttribute(name) {
51
51
  function isHtmlInlineStyleAttribute(name) {
52
52
  return name.toLowerCase() === "style";
53
53
  }
54
+ function isExternalHttpsImageSource(src) {
55
+ if (typeof src !== "string")
56
+ return false;
57
+ try {
58
+ return new URL(src).protocol === "https:";
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
54
63
  function isAllowedHtmlUrl(elementName, attributeName, value) {
55
64
  const element = htmlUrlBearingElement(elementName);
56
65
  if (element === null || !isHtmlUrlBearingAttribute(element, attributeName))
@@ -177,7 +186,8 @@ function countPageShim() {
177
186
  const graphRoots = roots.filter((root) => root.getAttribute(RENDERER_GRAPH_ATTRIBUTE) === "true");
178
187
  const allMarkedCandidates = safeSelectorElements(MARKED_ROOT_SELECTOR);
179
188
  const markedSet = new Set(allMarkedCandidates);
180
- const htmlRoots = allMarkedCandidates.filter((root) => root.nodeName.toLowerCase() !== "svg" && !hasMarkedRootAncestor(root, markedSet));
189
+ const contentRoots = allMarkedCandidates.filter((root) => root.nodeName.toLowerCase() !== "svg" && !hasMarkedRootAncestor(root, markedSet));
190
+ const htmlRoots = contentRoots.filter((root) => root.getAttribute("data-facet-renderer-kind") !== "markdown");
181
191
  const html = htmlRoots.length === 0 ? undefined : {
182
192
  rendererRootCount: htmlRoots.length,
183
193
  headingCount: 0,
@@ -193,24 +203,19 @@ function countPageShim() {
193
203
  html.listCount += safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.lists.join(",")).length;
194
204
  const images = safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(","));
195
205
  html.imageCount += images.length;
196
- html.externalImageCount += images.filter((image) => {
197
- try {
198
- return new URL(image.getAttribute("src") ?? "").protocol === "https:";
199
- } catch {
200
- return false;
201
- }
202
- }).length;
206
+ html.externalImageCount += images.filter((image) => isExternalHttpsImageSource(image.getAttribute("src"))).length;
203
207
  html.canvasCount += safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.canvases.join(",")).length;
204
208
  }
205
209
  const mermaidNodeCount = graphRoots.reduce((count, root) => count + safeSelectorElementsWithin(root, "g.node").length, 0);
206
210
  const opaqueRegionCount = safeSelectorElements("*").filter((element) => element.nodeName.toLowerCase() === "canvas").length;
211
+ const externalImageCount = contentRoots.reduce((count, root) => count + safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(",")).filter((image) => isExternalHttpsImageSource(image.getAttribute("src"))).length, 0);
207
212
  return {
208
213
  rendererRootSvgCount: roots.length,
209
214
  graphCount: graphRoots.length,
210
215
  mermaidNodeCount,
211
216
  visibleSvgCount: roots.filter(nonDegenerateViewBox).length,
212
217
  opaqueRegionCount,
213
- externalImageCount: html?.externalImageCount ?? 0,
218
+ externalImageCount,
214
219
  errorCount: safeSelectorElements("[data-facet-error]").length,
215
220
  ...html === undefined ? {} : { html }
216
221
  };
@@ -6,7 +6,7 @@ import {
6
6
  isHtmlInlineStyleAttribute,
7
7
  isHtmlUrlAttributeName,
8
8
  isHtmlUrlBearingAttribute
9
- } from "./markdown-d1wfbs70.js";
9
+ } from "./markdown-n9xcxy80.js";
10
10
 
11
11
  // src/gallery-web/frame/renderers/html.ts
12
12
  function shouldKeepAttribute(elementName, name, value) {
@@ -2,12 +2,12 @@ import {
2
2
  mermaidInitializeConfig,
3
3
  renderMermaidDocument,
4
4
  renderMermaidInto
5
- } from "./markdown-z1wc0v0d.js";
5
+ } from "./markdown-mksxbxx8.js";
6
6
  import"./markdown-6zz8efx2.js";
7
7
  import"./markdown-rm2yxfjj.js";
8
8
  import"./markdown-h7pzv34e.js";
9
- import"./markdown-zazx9h4n.js";
10
- import"./markdown-d1wfbs70.js";
9
+ import"./markdown-31ee7zgx.js";
10
+ import"./markdown-n9xcxy80.js";
11
11
  import"./markdown-s1jatrnk.js";
12
12
  import"./markdown-m7frq8x8.js";
13
13
  import"./markdown-tvdpppw8.js";
@@ -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
 
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  importSanitizedSvgText
3
- } from "../chunks/markdown-zazx9h4n.js";
3
+ } from "../chunks/markdown-31ee7zgx.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-7scxp44j.js";
7
7
  import {
8
8
  FacetRenderError,
9
9
  createRendererRegistry,
10
10
  decodeArtifactBytes
11
- } from "../chunks/markdown-d1wfbs70.js";
11
+ } from "../chunks/markdown-n9xcxy80.js";
12
12
  import {
13
13
  Accent_default,
14
14
  Dark2_default,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  renderHtml
3
- } from "../chunks/markdown-rd8w0ng9.js";
3
+ } from "../chunks/markdown-rkt612xp.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-7scxp44j.js";
7
7
  import {
8
8
  createRendererRegistry
9
- } from "../chunks/markdown-d1wfbs70.js";
9
+ } from "../chunks/markdown-n9xcxy80.js";
10
10
  import"../chunks/markdown-809k7q41.js";
11
11
 
12
12
  // src/gallery-web/frame/entries/html.ts
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  installGalleryFrameApi
3
- } from "../chunks/markdown-fvfncadf.js";
3
+ } from "../chunks/markdown-7scxp44j.js";
4
4
  import {
5
5
  createRendererRegistry,
6
6
  decodeArtifactBytes
7
- } from "../chunks/markdown-d1wfbs70.js";
7
+ } from "../chunks/markdown-n9xcxy80.js";
8
8
  import {
9
9
  __require
10
10
  } from "../chunks/markdown-809k7q41.js";
@@ -1380,7 +1380,7 @@ async function renderMarkdown(ctx, bytes) {
1380
1380
  const template = document.createElement("template");
1381
1381
  template.innerHTML = html;
1382
1382
  const mermaidBlocks = Array.from(template.content.querySelectorAll("pre > code.language-mermaid"));
1383
- const renderMermaidInto = mermaidBlocks.length === 0 ? null : (await import("../chunks/mermaid-de7p4v0x.js")).renderMermaidInto;
1383
+ const renderMermaidInto = mermaidBlocks.length === 0 ? null : (await import("../chunks/mermaid-b8fxhsgg.js")).renderMermaidInto;
1384
1384
  for (const code of mermaidBlocks) {
1385
1385
  const source = code.textContent ?? "";
1386
1386
  const pre = code.parentElement;
@@ -1395,7 +1395,11 @@ async function renderMarkdown(ctx, bytes) {
1395
1395
  else
1396
1396
  pre.remove();
1397
1397
  }
1398
- ctx.container.appendChild(template.content);
1398
+ const root = document.createElement("div");
1399
+ root.setAttribute("data-facet-renderer-root", "true");
1400
+ root.setAttribute("data-facet-renderer-kind", "markdown");
1401
+ root.appendChild(template.content);
1402
+ ctx.container.appendChild(root);
1399
1403
  }
1400
1404
 
1401
1405
  // src/gallery-web/frame/entries/markdown.ts
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  renderMermaidDocument
3
- } from "../chunks/markdown-z1wc0v0d.js";
3
+ } from "../chunks/markdown-mksxbxx8.js";
4
4
  import"../chunks/markdown-6zz8efx2.js";
5
5
  import"../chunks/markdown-rm2yxfjj.js";
6
6
  import"../chunks/markdown-h7pzv34e.js";
7
- import"../chunks/markdown-zazx9h4n.js";
7
+ import"../chunks/markdown-31ee7zgx.js";
8
8
  import {
9
9
  installGalleryFrameApi
10
- } from "../chunks/markdown-fvfncadf.js";
10
+ } from "../chunks/markdown-7scxp44j.js";
11
11
  import {
12
12
  createRendererRegistry
13
- } from "../chunks/markdown-d1wfbs70.js";
13
+ } from "../chunks/markdown-n9xcxy80.js";
14
14
  import"../chunks/markdown-s1jatrnk.js";
15
15
  import"../chunks/markdown-m7frq8x8.js";
16
16
  import"../chunks/markdown-tvdpppw8.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  renderSvgDocument
3
- } from "../chunks/markdown-zazx9h4n.js";
3
+ } from "../chunks/markdown-31ee7zgx.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-7scxp44j.js";
7
7
  import {
8
8
  createRendererRegistry
9
- } from "../chunks/markdown-d1wfbs70.js";
9
+ } from "../chunks/markdown-n9xcxy80.js";
10
10
  import"../chunks/markdown-809k7q41.js";
11
11
 
12
12
  // src/gallery-web/frame/entries/svg.ts
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  renderHtml
3
- } from "../chunks/markdown-rd8w0ng9.js";
3
+ } from "../chunks/markdown-rkt612xp.js";
4
4
  import {
5
5
  installGalleryFrameApi
6
- } from "../chunks/markdown-fvfncadf.js";
6
+ } from "../chunks/markdown-7scxp44j.js";
7
7
  import {
8
8
  RENDER_ERROR_ATTRIBUTE,
9
9
  appendRenderError,
10
10
  createRendererRegistry
11
- } from "../chunks/markdown-d1wfbs70.js";
11
+ } from "../chunks/markdown-n9xcxy80.js";
12
12
  import"../chunks/markdown-809k7q41.js";
13
13
 
14
14
  // src/gallery-web/frame/renderers/tsx.ts
@@ -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.
@@ -14,7 +14,7 @@ Every other layer is bound to it through `VerdictSchema.status`.
14
14
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
15
15
  | `ok` | Counts agree across protocol + shim + isolated worlds, layout observable, no discriminative errors. |
16
16
  | `error` | Counts disagree with the lexical expectation OR the protocol surfaced a discriminative error. |
17
- | `partial:layout_unverified` | Counts agree but the layout pass is unverified (no SVG rendered with a non-degenerate viewBox). MUST carry a screenshot path on the wire. |
17
+ | `partial:layout_unverified` | Counts agree but the layout pass is unverified (no SVG rendered with a non-degenerate viewBox). Markdown with zero expected renderer roots and opaque regions instead reaches `ok` when protocol and isolated observations agree that its renderer root is non-empty. Empty Markdown remains unverified. MUST carry a screenshot path on the wire. |
18
18
  | `partial:opaque_content` | An opaque DOM region was observed, so structural contents were not verified. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
19
19
  | `partial:external_resources` | The artifact references external HTTPS images the no-egress verifier could not observe. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
20
20
  | `partial:unstable` | TSX interactive mode: the structure observed at the render barrier differed from the structure observed after a bounded stability window. Deliberately NOT `tampered` — a legitimately animated or async-loading component also changes structure between observations, and branding that a forgery would manufacture the false-verdict class this project has spent three arcs eliminating. `tampered` stays reserved for channel divergence. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
@@ -48,7 +48,7 @@ ordering, top to bottom:
48
48
  7. `error` (declared-opaque, observed-zero) — the artifact declared opaque regions and none were seen.
49
49
  8. `partial:opaque_content` — single-snapshot claim: structure has opaque regions.
50
50
  9. `partial:external_resources` — single-snapshot claim: structure has external HTTP references.
51
- 10. `partial:layout_unverified` — single-snapshot claim: visible SVG with zeroed viewBoxes.
51
+ 10. `partial:layout_unverified` — single-snapshot claim: visible SVG with zeroed viewBoxes; it also applies to empty Markdown, while diagram-free Markdown with matching non-empty observations has no layout to verify.
52
52
  11. `ok`.
53
53
 
54
54
  The rule generalizing rows 4 and 6: a claim that depends on ONE observation
@@ -77,11 +77,19 @@ WebP; legacy PNG evidence remains readable and exportable. Before static
77
77
  capture it emulates `prefers-reduced-motion: reduce` and awaits
78
78
  `document.fonts.ready`; these pre-flights keep repeated captures byte-identical.
79
79
 
80
+ If resizing the capture viewport makes the artifact grow, Tier 1 restores the
81
+ 1280×800 evidence viewport and captures the artifact's scroll area in tiles.
82
+ The tiles form one still WebP image, including for interactive or animated
83
+ artifacts. This avoids changing viewport-sized elements such as `100vh` while
84
+ retaining content below and to the right. Fixed and sticky elements repeat in
85
+ each tile. Content that grows while the tiles are captured gets
86
+ `screenshot_unavailable` instead of an incomplete image.
87
+
80
88
  Tier 1 uses the same artifact-type layout rules as the gallery frame: Mermaid,
81
89
  SVG, and chart roots are safely centered on both axes, with oversized content
82
90
  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
91
+ centered column capped at about 92ch; HTML and TSX use top-aligned document
92
+ flow, where a fixed-width layout can center itself with `mx-auto`. The renderer-parity gate
85
93
  compares both the gallery and verifier renderer-module sets and CSS/stylesheet
86
94
  sets.
87
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legionworks/facet",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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,
@@ -77,5 +77,9 @@ export async function renderMarkdown(ctx: RenderContext, bytes: Uint8Array): Pro
77
77
  if (region.firstElementChild !== null) pre.replaceWith(region);
78
78
  else pre.remove();
79
79
  }
80
- ctx.container.appendChild(template.content);
80
+ const root = document.createElement("div");
81
+ root.setAttribute("data-facet-renderer-root", "true");
82
+ root.setAttribute("data-facet-renderer-kind", "markdown");
83
+ root.appendChild(template.content);
84
+ ctx.container.appendChild(root);
81
85
  }