@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.
- package/README.md +50 -21
- package/dist/gallery/{chunk-c6s9k9ty.js → chunk-0by8qn4x.js} +86 -3
- package/dist/gallery/{chunk-4g8rem85.css → chunk-2dge4gwb.css} +22 -1
- package/dist/gallery/frame/artifact.css +3 -0
- package/dist/gallery/frame/chunks/{markdown-fvfncadf.js → markdown-g3asgaw3.js} +8 -0
- package/dist/gallery/frame/frame.css +11 -24
- package/dist/gallery/frame/runtime/chart.js +24 -2
- package/dist/gallery/frame/runtime/html.js +1 -1
- package/dist/gallery/frame/runtime/markdown.js +1 -1
- package/dist/gallery/frame/runtime/mermaid.js +1 -1
- package/dist/gallery/frame/runtime/svg.js +1 -1
- package/dist/gallery/frame/runtime/tsx.js +1 -1
- package/dist/gallery/index.html +4 -2
- package/docs/reference/cli.md +30 -2
- package/docs/reference/export.md +4 -0
- package/docs/reference/html.md +3 -3
- package/docs/reference/mcp.md +29 -8
- package/docs/reference/security.md +7 -0
- package/docs/reference/storage.md +6 -1
- package/docs/reference/tsx.md +16 -0
- package/docs/reference/validation.md +34 -6
- package/package.json +4 -4
- package/skills/facet/SKILL.md +24 -2
- package/src/cli/commands/promote.ts +1 -0
- package/src/cli/commands/templates.ts +25 -0
- package/src/cli/main.ts +4 -0
- package/src/cli/parser.ts +4 -0
- package/src/cli/presenter.ts +48 -0
- package/src/gallery-web/app.ts +65 -2
- package/src/gallery-web/favicon.ts +1 -0
- package/src/gallery-web/frame/renderers/chart.ts +31 -1
- package/src/gallery-web/frame/runtime.ts +7 -1
- package/src/gallery-web/frame/styles/artifact.css +3 -0
- package/src/gallery-web/frame/styles/frame.css +11 -24
- package/src/gallery-web/frame-html.ts +7 -2
- package/src/gallery-web/index.html +3 -1
- package/src/gallery-web/styles/verdict.css +19 -4
- package/src/harness-adapters/mcp/cli-bridge.ts +11 -1
- package/src/harness-adapters/mcp/main.ts +2 -0
- package/src/harness-adapters/mcp/server.ts +92 -69
- package/src/harness-adapters/mcp/tool-schemas.ts +7 -0
- package/src/service/dispatcher.ts +22 -0
- package/src/service/router-guards.ts +2 -0
- package/src/service/store/migrations.ts +12 -2
- package/src/service/store/repository-lifecycle.ts +91 -19
- package/src/service/store/repository.ts +60 -2
- package/src/service/store/schema.ts +4 -0
- package/src/service/stored-verdict.ts +3 -0
- package/src/shared/contracts/artifact.ts +1 -0
- package/src/shared/contracts/commands/index.ts +10 -0
- package/src/shared/contracts/commands/names.ts +2 -0
- package/src/shared/contracts/commands/requests.ts +7 -0
- package/src/shared/contracts/commands/results.ts +20 -1
- package/src/shared/contracts/promotion.ts +16 -0
- package/src/shared/contracts/validation.ts +7 -0
- package/src/shared/errors/facet-error.ts +2 -0
- package/src/shared/errors/store-error.ts +4 -0
- package/src/shared/html/artifact-main.ts +3 -0
- package/src/shared/html/style-vocabulary.ts +1 -0
- package/src/shared/storage-version.ts +1 -1
- package/src/validation/tier0/dom-shim.ts +3 -18
- package/src/validation/tier0/markdown.ts +28 -5
- package/src/validation/tier0/mermaid.ts +17 -8
- package/src/validation/tier0/worker-dispatch.ts +1 -1
- package/src/validation/tier1/entries/tsx.ts +1 -0
- package/src/validation/tier1/harness.ts +2 -1
- package/src/validation/tier1/isolated-probe.ts +4 -0
- package/src/validation/tier1/protocol-probe.ts +37 -0
- package/src/validation/tier1/runner.ts +21 -4
- package/src/validation/tier1/verdict.ts +24 -39
- package/templates/capacity-report.tsx +1 -1
- package/templates/fleet-dashboard.html +1 -1
- package/templates/html-release-ledger.html +1 -1
- package/templates/html-status-report.html +1 -1
- package/templates/incident-console.tsx +1 -1
- package/templates/tsx-interactive-counter.tsx +1 -1
- package/templates/tsx-status-report.tsx +1 -1
|
@@ -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);
|
|
@@ -78,32 +78,19 @@ body {
|
|
|
78
78
|
padding-inline: max(16px, calc((100% - 92ch) / 2));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
52
|
+
(usesArtifactStylesheet(artifactType)
|
|
48
53
|
? `<link rel="stylesheet" href="/gallery/frame/artifact.css">`
|
|
49
54
|
: "") +
|
|
50
55
|
"</head><body>" +
|
|
51
|
-
`<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"
|
|
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,11 +1,15 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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:
|
|
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(
|