@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
@@ -10,6 +10,7 @@ export const FAVICON_TINT_BY_STATUS = {
10
10
  "partial:opaque_content": "amber",
11
11
  "partial:external_resources": "amber",
12
12
  "partial:unstable": "amber",
13
+ "partial:empty_render": "amber",
13
14
  tampered: "red",
14
15
  timeout: "red",
15
16
  shim_only: "grey",
@@ -128,6 +128,36 @@ export function withFacetChartTheme(spec: unknown, theme: ResolvedGalleryTheme):
128
128
  };
129
129
  }
130
130
 
131
+ export function withFacetChartDimensions(spec: unknown): unknown {
132
+ if (spec === null || typeof spec !== "object" || Array.isArray(spec)) return spec;
133
+ const source = spec as Readonly<Record<string, unknown>>;
134
+ const config = source.config;
135
+ if (
136
+ "autosize" in source ||
137
+ (typeof config === "object" && config !== null && !Array.isArray(config) && "view" in config)
138
+ )
139
+ return spec;
140
+ if ("width" in source && "height" in source) return spec;
141
+ const authoredConfig =
142
+ typeof config === "object" && config !== null && !Array.isArray(config)
143
+ ? (config as Readonly<Record<string, unknown>>)
144
+ : {};
145
+ const viewDefaults = {
146
+ ...(!("width" in source) ? { continuousWidth: 640, discreteWidth: 640 } : {}),
147
+ ...(!("height" in source) ? { continuousHeight: 360, discreteHeight: 360 } : {}),
148
+ };
149
+ // Composite views ignore top-level dimensions; config.view reaches each child view. An
150
+ // authored config.view wins because its sizing is an explicit author choice. The 640×360
151
+ // plot defaults leave charts readable in the 1280×800 verifier viewport.
152
+ return {
153
+ ...source,
154
+ config: {
155
+ ...authoredConfig,
156
+ view: viewDefaults,
157
+ },
158
+ };
159
+ }
160
+
131
161
  /** Render a chart artifact (artifactType "chart"): Vega-Lite JSON bytes. */
132
162
  export async function renderChart(
133
163
  ctx: RenderContext,
@@ -144,7 +174,7 @@ export async function renderChart(
144
174
  }
145
175
  let compiled: { spec: unknown };
146
176
  try {
147
- compiled = compile(withFacetChartTheme(spec, ctx.theme) as never);
177
+ compiled = compile(withFacetChartTheme(withFacetChartDimensions(spec), ctx.theme) as never);
148
178
  } catch (error) {
149
179
  const message = error instanceof Error ? error.message : String(error);
150
180
  throw new FacetRenderError(`vega-lite compile failed: ${message}`, "chart_compile_error");
@@ -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,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 {
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { galleryDataTheme, type ResolvedGalleryTheme } from "./theme";
12
+ import { artifactMainAttributes } from "../shared/html/artifact-main";
12
13
 
13
14
  /**
14
15
  * Frame element attributes. The shell applies these to every iframe it
@@ -30,6 +31,10 @@ export function buildFrameAttributes(src = "/gallery/frame"): FrameAttributes {
30
31
  };
31
32
  }
32
33
 
34
+ export function usesArtifactStylesheet(artifactType: string): boolean {
35
+ return artifactType === "html" || artifactType === "tsx";
36
+ }
37
+
33
38
  export function buildFrameDocument(options: {
34
39
  artifactType: string;
35
40
  runtimeUrl: string;
@@ -44,11 +49,11 @@ export function buildFrameDocument(options: {
44
49
  return (
45
50
  `<!doctype html><html data-theme="${galleryDataTheme(theme)}"><head>` +
46
51
  `<meta charset="utf-8"><link rel="stylesheet" href="/gallery/frame/frame.css">` +
47
- (artifactType === "html" || artifactType === "tsx"
52
+ (usesArtifactStylesheet(artifactType)
48
53
  ? `<link rel="stylesheet" href="/gallery/frame/artifact.css">`
49
54
  : "") +
50
55
  "</head><body>" +
51
- `<main id="artifact" data-facet-artifact-type="${artifactType}"></main>` +
56
+ `<main ${artifactMainAttributes(artifactType)}></main>` +
52
57
  `<script type="module" src="${escapedRuntimeUrl}"></script>` +
53
58
  "</body></html>"
54
59
  );
@@ -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
@@ -57,6 +57,16 @@
57
57
  content: "✓";
58
58
  }
59
59
 
60
+ .facet-verdict[data-interaction-error="true"] {
61
+ border-color: var(--facet-fail);
62
+ box-shadow: var(--facet-glow-danger);
63
+ }
64
+ .facet-verdict[data-interaction-error="true"]::after {
65
+ content: "✗ runtime";
66
+ color: var(--facet-fail);
67
+ font-weight: 700;
68
+ }
69
+
60
70
  .facet-verdict[data-status="error"] {
61
71
  border-color: var(--facet-fail-border);
62
72
  background: var(--facet-fail-fill);
@@ -100,6 +110,15 @@
100
110
  content: "◐";
101
111
  }
102
112
 
113
+ .facet-verdict[data-status="partial:empty_render"] {
114
+ border-color: var(--facet-partial-border);
115
+ background: var(--facet-partial-fill);
116
+ color: var(--facet-partial);
117
+ }
118
+ .facet-verdict[data-status="partial:empty_render"]::before {
119
+ content: "◐";
120
+ }
121
+
103
122
  /* The alarm. The only fill, the only glow. Never animated. */
104
123
  .facet-verdict[data-status="tampered"],
105
124
  .facet-verdict[data-status="insecure:unvalidated"] {
@@ -109,10 +128,6 @@
109
128
  font-weight: 700;
110
129
  box-shadow: var(--facet-glow-danger);
111
130
  }
112
- .facet-verdict[data-status="tampered"]::before {
113
- content: "⊘";
114
- }
115
-
116
131
  .facet-verdict[data-status="tampered"]::before,
117
132
  .facet-verdict[data-status="insecure:unvalidated"]::before {
118
133
  content: "⊘";
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  const STDERR_DETAIL_LIMIT = 4_096;
11
11
 
12
- export type FacetToolName = "publish" | "read_back" | "status" | "export" | "open";
12
+ export type FacetToolName = "create" | "publish" | "read_back" | "status" | "export" | "open";
13
13
 
14
14
  export interface FacetCliInvocation {
15
15
  readonly command: string;
@@ -75,6 +75,16 @@ export function buildFacetArgs(
75
75
  input: Readonly<Record<string, unknown>>,
76
76
  ): string[] {
77
77
  switch (tool) {
78
+ case "create":
79
+ return [
80
+ "create",
81
+ "--project-id",
82
+ requiredString(input, "projectId"),
83
+ "--slug",
84
+ requiredString(input, "slug"),
85
+ "--title",
86
+ requiredString(input, "title"),
87
+ ];
78
88
  case "publish": {
79
89
  const args = [
80
90
  "publish",
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env bun
2
+
1
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
4
 
3
5
  import { createFacetMcpServer } from "./server";
@@ -1,11 +1,15 @@
1
1
  import { mkdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
2
3
 
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { z, ZodError } from "zod";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
6
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
+ import { type ZodType, ZodError } from "zod";
5
8
  import { FACET_VERSION } from "../../shared/version";
6
9
 
7
10
  import { buildFacetArgs, FacetBridgeError, invokeFacet } from "./cli-bridge";
8
11
  import {
12
+ CreateToolSchema,
9
13
  ExportToolSchema,
10
14
  OpenUrlToolSchema,
11
15
  PublishToolSchema,
@@ -41,12 +45,10 @@ function errorEnvelope(cause: unknown): FacetEnvelope<never> {
41
45
  return errEnvelope(requestId(), body);
42
46
  }
43
47
 
44
- const LooseToolInputSchema = z.record(z.unknown());
45
-
46
48
  function toolResult(envelope: FacetEnvelope<unknown>) {
47
49
  return {
48
50
  content: [{ type: "text" as const, text: JSON.stringify(envelope) }],
49
- ...(envelope.ok ? {} : { isError: true }),
51
+ isError: !envelope.ok,
50
52
  };
51
53
  }
52
54
 
@@ -90,75 +92,96 @@ function publishSource(input: PublishToolInput): {
90
92
  });
91
93
  }
92
94
 
93
- export function createFacetMcpServer(): McpServer {
94
- const server = new McpServer({ name: "facet", version: FACET_VERSION });
95
-
96
- server.registerTool(
97
- "facet_export",
98
- {
99
- description:
100
- "Export source or stored render evidence into outDir. Check envelope.ok for transport success; a successful publish verdict remains a separate data.verdict.status decision.",
101
- inputSchema: LooseToolInputSchema,
102
- },
103
- async (args) =>
104
- withInput(args, ExportToolSchema.parse, async (input) => {
105
- mkdirSync(input.outDir, { recursive: true });
106
- return invoke(buildFacetArgs("export", input), { cwd: input.outDir });
107
- }),
108
- );
95
+ function defineTool<T>(
96
+ schema: ZodType<T>,
97
+ description: string,
98
+ run: (input: T) => Promise<ReturnType<typeof toolResult>>,
99
+ ) {
100
+ return {
101
+ schema,
102
+ description,
103
+ execute: (args: unknown) => withInput(args, (input) => schema.parse(input), run),
104
+ };
105
+ }
109
106
 
110
- server.registerTool(
111
- "facet_open_url",
112
- {
113
- description:
114
- "Return a Facet gallery frameUrl without launching a browser. This always invokes facet open --no-launch; agents must not launch desktop display.",
115
- inputSchema: LooseToolInputSchema,
116
- },
117
- async (args) =>
118
- withInput(args, OpenUrlToolSchema.parse, async (input) =>
119
- invoke(buildFacetArgs("open", input)),
120
- ),
107
+ export function createFacetMcpServer(): Server {
108
+ const server = new Server(
109
+ { name: "facet", version: FACET_VERSION },
110
+ { capabilities: { tools: {} } },
121
111
  );
122
-
123
- server.registerTool(
124
- "facet_publish",
125
- {
126
- description:
127
- "Publish exactly one sourceText or local file. Check envelope.ok separately from data.verdict.status: stored verdict status error is not a transport failure.",
128
- inputSchema: LooseToolInputSchema,
129
- },
130
- async (args) =>
131
- withInput(args, PublishToolSchema.parse, async (input) => {
112
+ // The high-level SDK validates arguments before the handler and emits plain-text errors.
113
+ // Wire schemas and handler parsing are separate so invalid calls retain Facet envelopes.
114
+ const tools = {
115
+ facet_create: defineTool(
116
+ CreateToolSchema,
117
+ "Create an artifact from projectId, slug, and title before publishing source.",
118
+ async (input) => invoke(buildFacetArgs("create", input)),
119
+ ),
120
+ facet_export: defineTool(
121
+ ExportToolSchema,
122
+ "Export source or stored render evidence into outDir. Check envelope.ok for transport success; a successful publish verdict remains a separate data.verdict.status decision.",
123
+ async (input) => {
124
+ const outDir = resolve(input.outDir);
125
+ try {
126
+ mkdirSync(outDir, { recursive: true });
127
+ } catch (cause) {
128
+ throw new FacetBridgeError(
129
+ {
130
+ code: "output_unwritable",
131
+ message: `Cannot write export output: ${outDir}`,
132
+ retryable: false,
133
+ details: { out: outDir },
134
+ },
135
+ cause,
136
+ );
137
+ }
138
+ return invoke(buildFacetArgs("export", input), { cwd: outDir });
139
+ },
140
+ ),
141
+ facet_open_url: defineTool(
142
+ OpenUrlToolSchema,
143
+ "Return a Facet gallery frameUrl without launching a browser. This always invokes facet open --no-launch; agents must not launch desktop display.",
144
+ async (input) => invoke(buildFacetArgs("open", input)),
145
+ ),
146
+ facet_publish: defineTool(
147
+ PublishToolSchema,
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
+ async (input) => {
132
150
  const source = publishSource(input);
133
151
  return invoke(buildFacetArgs("publish", { ...input, ...source }), source);
152
+ },
153
+ ),
154
+ facet_read_back: defineTool(
155
+ ReadBackToolSchema,
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
+ async (input) => invoke(buildFacetArgs("read_back", input)),
158
+ ),
159
+ facet_status: defineTool(
160
+ StatusToolSchema,
161
+ "Read Facet status. Set start only when activation is intended; envelope.ok reports command transport and never replaces verdict.status inspection.",
162
+ async (input) => invoke(buildFacetArgs("status", input)),
163
+ ),
164
+ };
165
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
166
+ tools: Object.entries(tools).map(([name, tool]) => ({
167
+ name,
168
+ description: tool.description,
169
+ inputSchema: toJsonSchemaCompat(tool.schema, { pipeStrategy: "input" }),
170
+ })),
171
+ }));
172
+ server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
173
+ const tool = Object.hasOwn(tools, params.name)
174
+ ? tools[params.name as keyof typeof tools]
175
+ : null;
176
+ if (tool) return tool.execute(params.arguments);
177
+ return toolResult(
178
+ errEnvelope(requestId(), {
179
+ code: "invalid_request",
180
+ message: `Unknown MCP tool '${params.name}'`,
181
+ retryable: false,
134
182
  }),
135
- );
136
-
137
- server.registerTool(
138
- "facet_read_back",
139
- {
140
- description:
141
- "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.",
142
- inputSchema: LooseToolInputSchema,
143
- },
144
- async (args) =>
145
- withInput(args, ReadBackToolSchema.parse, async (input) =>
146
- invoke(buildFacetArgs("read_back", input)),
147
- ),
148
- );
149
-
150
- server.registerTool(
151
- "facet_status",
152
- {
153
- description:
154
- "Read Facet status. Set start only when activation is intended; envelope.ok reports command transport and never replaces verdict.status inspection.",
155
- inputSchema: LooseToolInputSchema,
156
- },
157
- async (args) =>
158
- withInput(args, StatusToolSchema.parse, async (input) =>
159
- invoke(buildFacetArgs("status", input)),
160
- ),
161
- );
183
+ );
184
+ });
162
185
 
163
186
  return server;
164
187
  }
@@ -6,6 +6,13 @@ const RevisionShaSchema = z.string().regex(/^[a-f0-9]{64}$/);
6
6
  const ExecutionSchema = z.enum(["static", "interactive"]);
7
7
  const ReadBackTierSchema = z.union([z.literal(0), z.literal(1), z.literal("visual")]);
8
8
 
9
+ export const CreateToolShape = {
10
+ projectId: z.string().min(1),
11
+ slug: z.string().min(1),
12
+ title: z.string().min(1),
13
+ };
14
+ export const CreateToolSchema = z.object(CreateToolShape).strict();
15
+
9
16
  export const PublishToolShape = {
10
17
  artifactId: z.string().min(1),
11
18
  type: ArtifactTypeSchema,
@@ -513,12 +513,25 @@ export async function dispatch(
513
513
  nextCursor: null,
514
514
  };
515
515
  }
516
+ case "templates": {
517
+ return {
518
+ command: "templates",
519
+ requestId,
520
+ templates: deps.repository.listTemplates(command.limit),
521
+ };
522
+ }
516
523
  case "readBack": {
517
524
  const revision =
518
525
  command.revisionSha === undefined
519
526
  ? deps.repository.getLatestRevision(command.artifactId)
520
527
  : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
521
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
+ }
522
535
  throw new FacetError("revision_not_found", "Revision not found", {
523
536
  retryable: false,
524
537
  details: {
@@ -607,6 +620,12 @@ export async function dispatch(
607
620
  ? deps.repository.getLatestRevision(command.artifactId)
608
621
  : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
609
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
+ }
610
629
  throw new FacetError("revision_not_found", "Revision not found", {
611
630
  retryable: false,
612
631
  details: {
@@ -642,6 +661,9 @@ export async function dispatch(
642
661
  revisionId: command.revisionId,
643
662
  name: command.name,
644
663
  promotedBy: command.promotedBy,
664
+ ...(command.allowUnverified !== undefined
665
+ ? { allowUnverified: command.allowUnverified }
666
+ : {}),
645
667
  ...(command.description !== undefined ? { description: command.description } : {}),
646
668
  });
647
669
  return { command: "promote", requestId, template };
@@ -222,6 +222,8 @@ export function statusFor(error: FacetError): number {
222
222
  case "tier0_unavailable":
223
223
  return 503;
224
224
  case "duplicate_revision":
225
+ case "template_name_taken":
226
+ case "promotion_refused":
225
227
  case "constraint":
226
228
  case "foreign_key":
227
229
  case "immutable_revision":
@@ -12,8 +12,8 @@ import {
12
12
  V7_SCHEMA_FRAGMENT,
13
13
  V8_SCHEMA_FRAGMENT,
14
14
  V9_SCHEMA_FRAGMENT,
15
+ V10_SCHEMA_FRAGMENT,
15
16
  } from "./schema";
16
- import { CURRENT_STORAGE_VERSION } from "../../shared/storage-version";
17
17
 
18
18
  export interface MigrationOptions {
19
19
  readonly beforeRecordVersion?: (version: number) => void;
@@ -106,13 +106,23 @@ const MIGRATION_STEPS: readonly MigrationStep[] = [
106
106
  requiresForeignKeyDisable: true,
107
107
  },
108
108
  {
109
- version: CURRENT_STORAGE_VERSION,
109
+ version: 9,
110
110
  apply: (db) => {
111
111
  db.exec(V9_SCHEMA_FRAGMENT);
112
112
  },
113
113
  },
114
+ {
115
+ version: 10,
116
+ apply: (db) => {
117
+ db.exec(V10_SCHEMA_FRAGMENT);
118
+ },
119
+ },
114
120
  ];
115
121
 
122
+ export function latestMigrationVersion(): number {
123
+ return MIGRATION_STEPS.at(-1)?.version ?? 0;
124
+ }
125
+
116
126
  export function runMigrations(db: Database, options: MigrationOptions = {}): void {
117
127
  try {
118
128
  db.exec(