@reqvire-org/ads 0.3.16 → 0.3.18
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 +3 -0
- package/dist/atomic-composition.mjs +1 -1
- package/dist/check.mjs +43 -11
- package/dist/evidence.mjs +18 -0
- package/dist/investigations.mjs +101 -0
- package/dist/linaria/preserve-values.cjs +21 -2
- package/dist/mcp/behavior-pages.mjs +36 -0
- package/dist/mcp/context.mjs +3 -1
- package/dist/mcp/execution-results.mjs +146 -0
- package/dist/mcp/mutations.mjs +34 -17
- package/dist/mcp/preview-widget.bundle.js +12 -12
- package/dist/mcp/resource-loader.js +3 -3
- package/dist/mcp/result-pages.mjs +87 -0
- package/dist/mcp/server.mjs +67 -30
- package/dist/property-registrations.mjs +39 -0
- package/dist/scenario-selectors.mjs +140 -0
- package/dist/scenario-trace.mjs +1 -1
- package/dist/scoped-tokens.mjs +6 -0
- package/dist/selector-match.mjs +94 -0
- package/dist/showcase/client.bundle.js +2 -2
- package/dist/skills/ads-break-ui/SKILL.md +8 -3
- package/dist/skills/ads-break-ui/references/atomic-styles.md +48 -0
- package/dist/skills/ads-break-ui/references/captures.md +6 -1
- package/dist/skills/ads-break-ui/references/evidence.md +120 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +8 -5
- package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
- package/dist/skills/ads-designer/SKILL.md +8 -3
- package/dist/skills/ads-designer/references/atomic-styles.md +48 -0
- package/dist/skills/ads-designer/references/captures.md +6 -1
- package/dist/skills/ads-designer/references/evidence.md +120 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +8 -5
- package/dist/skills/ads-designer/references/scenarios.md +32 -0
- package/dist/skills/ads-import/SKILL.md +8 -3
- package/dist/skills/ads-import/references/atomic-styles.md +48 -0
- package/dist/skills/ads-import/references/captures.md +6 -1
- package/dist/skills/ads-import/references/evidence.md +120 -0
- package/dist/skills/ads-import/references/guarded-editing.md +8 -5
- package/dist/skills/ads-import/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/SKILL.md +8 -3
- package/dist/skills/ads-migrate/references/atomic-styles.md +48 -0
- package/dist/skills/ads-migrate/references/captures.md +6 -1
- package/dist/skills/ads-migrate/references/evidence.md +120 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +8 -5
- package/dist/skills/ads-migrate/references/scenarios.md +32 -0
- package/dist/skills/ads-motion/SKILL.md +8 -3
- package/dist/skills/ads-motion/references/atomic-styles.md +48 -0
- package/dist/skills/ads-motion/references/captures.md +6 -1
- package/dist/skills/ads-motion/references/evidence.md +120 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +8 -5
- package/dist/skills/ads-motion/references/scenarios.md +32 -0
- package/dist/verification/browser.mjs +22 -7
- package/dist/verification/playwright-reporter.cjs +23 -5
- package/dist/verification.mjs +31 -9
- package/docs/ADS.md +10 -0
- package/docs/ARCHITECTURE.md +47 -3
- package/docs/BEHAVIORS.md +30 -0
- package/docs/CHANGELOG.md +30 -0
- package/docs/MCP.md +66 -7
- package/package.json +2 -1
|
@@ -31,14 +31,14 @@ export class PreviewResourceLoader {
|
|
|
31
31
|
if (!record) throw new Error(`Unknown preview dependency: ${uri}`);
|
|
32
32
|
let response;
|
|
33
33
|
try { response = await this.read(uri); }
|
|
34
|
-
catch (error) { throw new Error(`Preview resource read failed for ${uri}: ${error.message}`, { cause: error }); }
|
|
34
|
+
catch (error) { throw Object.assign(new Error(`Preview resource read failed for ${uri}: ${error.message}`, { cause: error }), { code: "ADS_RESOURCE_READ", category: "transport", resourceUri: uri }); }
|
|
35
35
|
if (this.closed) throw new Error("The preview App was closed.");
|
|
36
36
|
const content = response.contents.find(item => item.uri === uri);
|
|
37
|
-
if (!content) throw new Error(`Missing preview dependency: ${uri}`);
|
|
37
|
+
if (!content) throw Object.assign(new Error(`Missing preview dependency: ${uri}`), { code: "ADS_RESOURCE_CONTENT", category: "build", resourceUri: uri });
|
|
38
38
|
const bytes = typeof content.text === "string" ? new TextEncoder().encode(content.text) : Uint8Array.from(atob(content.blob), letter => letter.charCodeAt(0));
|
|
39
39
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
40
40
|
const actual = "sha256:" + [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
41
|
-
if (bytes.length !== record.size || actual !== record.digest) throw new Error("Preview resource integrity check failed. Rebuild and reopen the preview.");
|
|
41
|
+
if (bytes.length !== record.size || actual !== record.digest) throw Object.assign(new Error("Preview resource integrity check failed. Rebuild and reopen the preview."), { code: "ADS_RESOURCE_INTEGRITY", category: "build", resourceUri: uri });
|
|
42
42
|
return bytes;
|
|
43
43
|
})();
|
|
44
44
|
this.bytes.set(uri, task);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const resultBudgets = { summary: 4096, page: 8192, row: 2048, defaultLimit: 10, maxLimit: 20 };
|
|
4
|
+
export const jsonBytes = value => Buffer.byteLength(JSON.stringify(value));
|
|
5
|
+
|
|
6
|
+
/** Projection is only for delivery; validation always uses the complete result. */
|
|
7
|
+
export function compactValue(input, { string = 500, array = 8, keys = 24, depth = 5 } = {}) {
|
|
8
|
+
let truncated = false;
|
|
9
|
+
function visit(value, level) {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
if (value.length <= string) return value;
|
|
12
|
+
truncated = true; return `${value.slice(0, string)}…`;
|
|
13
|
+
}
|
|
14
|
+
if (value === null || typeof value === "number" || typeof value === "boolean" || value === undefined) return value;
|
|
15
|
+
if (level >= depth) { truncated = true; return "[omitted]"; }
|
|
16
|
+
if (Array.isArray(value)) { if (value.length > array) truncated = true; return value.slice(0, array).map(item => visit(item, level + 1)); }
|
|
17
|
+
const entries = Object.entries(value);
|
|
18
|
+
if (entries.length > keys) truncated = true;
|
|
19
|
+
return Object.fromEntries(entries.slice(0, keys).map(([key, item]) => [key, visit(item, level + 1)]));
|
|
20
|
+
}
|
|
21
|
+
const value = visit(input, 0);
|
|
22
|
+
return { value, truncated };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function compactRow(input, bytes = resultBudgets.row) {
|
|
26
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) input = { value: input };
|
|
27
|
+
for (const limits of [{}, { string: 160, array: 3, keys: 18 }, { string: 60, array: 1, keys: 12, depth: 3 }]) {
|
|
28
|
+
const { value, truncated } = compactValue(input, limits);
|
|
29
|
+
const row = { ...value, ...(truncated ? { detailTruncated: true } : {}) };
|
|
30
|
+
if (jsonBytes(row) <= bytes) return row;
|
|
31
|
+
}
|
|
32
|
+
const row = Object.fromEntries(["recordIndex", "id", "unit", "check", "status", "passed", "rule", "file", "path", "name"].filter(key => input[key] !== undefined).slice(0, 4).map(key => [key, typeof input[key] === "string" ? input[key].slice(0, 20) : input[key]]));
|
|
33
|
+
const result = { ...row, detailTruncated: true };
|
|
34
|
+
return jsonBytes(result) <= bytes ? result : { detailTruncated: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactAcceptance(value) {
|
|
38
|
+
if (!value) return undefined;
|
|
39
|
+
const establishedCount=value.establishedCount??value.established?.length??0,unverifiedCount=value.unverifiedCount??value.unverified?.length??0;
|
|
40
|
+
return { scope: value.scope, status: value.status, establishedCount, unverifiedCount,
|
|
41
|
+
established: (value.established ?? []).slice(0, 3).map(item => String(item).slice(0, 100)),
|
|
42
|
+
unverified: (value.unverified ?? []).slice(0, 12).map(item => String(item).slice(0, 100)),
|
|
43
|
+
...(value.detailTruncated || establishedCount > 3 || unverifiedCount > 12 ? { detailTruncated: true } : {}) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The cursor binds the immutable result/query, not just an array offset. */
|
|
47
|
+
export function resultPage(items, { cursor, limit = resultBudgets.defaultLimit, identity, header = {}, key = "items" } = {}) {
|
|
48
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > resultBudgets.maxLimit) throw new Error("Use a page limit from 1 to 20.");
|
|
49
|
+
const stamp = createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 24);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
if (cursor !== undefined) {
|
|
52
|
+
if (typeof cursor !== "string" || cursor.length > 300) throw new Error("Invalid result cursor.");
|
|
53
|
+
const match = /^([a-f0-9]{24}):(\d+)$/.exec(cursor);
|
|
54
|
+
offset = match && match[1] === stamp ? Number(match[2]) : -1;
|
|
55
|
+
if (!Number.isSafeInteger(offset) || offset <= 0 || offset >= items.length) throw new Error("Invalid or stale result cursor; restart this listing.");
|
|
56
|
+
}
|
|
57
|
+
const selected = [];
|
|
58
|
+
const envelope = () => ({ ...header, total: items.length, returned: selected.length, byteLimit: resultBudgets.page, [key]: selected,
|
|
59
|
+
...(offset + selected.length < items.length ? { nextCursor: `${stamp}:${offset + selected.length}` } : {}) });
|
|
60
|
+
for (const item of items.slice(offset, offset + limit)) {
|
|
61
|
+
selected.push(compactRow(item));
|
|
62
|
+
if (jsonBytes(envelope()) > resultBudgets.page) { selected.pop(); break; }
|
|
63
|
+
}
|
|
64
|
+
if (offset < items.length && !selected.length) throw new Error("The page envelope exceeds the response budget.");
|
|
65
|
+
const result = envelope();
|
|
66
|
+
if (jsonBytes(result) > resultBudgets.page) throw new Error("The page exceeds its response budget.");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function compactProgress(value) {
|
|
71
|
+
if (!value) return undefined;
|
|
72
|
+
const { stage, profile, elapsedMs, timeoutMs, total, completed, pending, workers } = value;
|
|
73
|
+
return { stage, profile, elapsedMs, timeoutMs, total, completed, pending, workers, activeCount: value.active?.length ?? 0,
|
|
74
|
+
active: (value.active ?? []).slice(0, 3).map(item => compactRow(item, 512)) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Exact JSON ranges complement lossy row previews without unbounded replies. */
|
|
78
|
+
export function resultRecord(rows, { recordIndex, offset = 0, chars = 2000, header = {} } = {}) {
|
|
79
|
+
if (!Number.isInteger(recordIndex) || recordIndex < 0 || recordIndex >= rows.length || !Number.isInteger(offset) || offset < 0 || !Number.isInteger(chars) || chars < 1 || chars > 2000) throw new Error("Read a valid recordIndex with an offset and 1–2000 characters.");
|
|
80
|
+
const source=JSON.stringify(rows[recordIndex]);
|
|
81
|
+
if(offset>source.length)throw new Error("Offset exceeds the selected record.");
|
|
82
|
+
let end=Math.min(source.length,offset+chars);
|
|
83
|
+
const result=()=>({...header,recordIndex,offset,totalChars:source.length,text:source.slice(offset,end),nextOffset:end<source.length?end:null,byteLimit:resultBudgets.page});
|
|
84
|
+
while(jsonBytes(result())>resultBudgets.page&&end>offset)end=offset+Math.floor((end-offset)*0.8);
|
|
85
|
+
if(jsonBytes(result())>resultBudgets.page||end===offset&&offset<source.length)throw new Error("Record identity exceeds the response budget.");
|
|
86
|
+
return result();
|
|
87
|
+
}
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -5,7 +5,9 @@ import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/s
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { checkProject } from "../check.mjs";
|
|
7
7
|
import { loadConfig, sourceFiles } from "../config.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { createVerificationController, BEHAVIOR_URI } from "../verification.mjs";
|
|
9
|
+
import { INVESTIGATIONS_URI, investigationIndex, investigationPage, investigationSections, readInvestigationResource } from "../investigations.mjs";
|
|
10
|
+
import { failure } from "../evidence.mjs";
|
|
9
11
|
import { inspectTokens } from "../tokens.mjs";
|
|
10
12
|
import { inspectScenarios } from "../scenarios.mjs";
|
|
11
13
|
import { startShowcaseHost } from "./showcase-host.mjs";
|
|
@@ -24,6 +26,9 @@ import { reconcileTokenSamples } from "../token-fixtures.mjs";
|
|
|
24
26
|
import { createCaptureController } from "../capture.mjs";
|
|
25
27
|
import { CAPTURES_URI, captureSpec, screenshotFields } from "../capture-store.mjs";
|
|
26
28
|
import { previewSourceRevision } from "../preview-inputs.mjs";
|
|
29
|
+
import { createExecutionResults, RESULTS_URI, resultSections } from "./execution-results.mjs";
|
|
30
|
+
import { behaviorPage, behaviorSections } from "./behavior-pages.mjs";
|
|
31
|
+
import { compactProgress, resultBudgets } from "./result-pages.mjs";
|
|
27
32
|
|
|
28
33
|
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
29
34
|
class InvalidParamsError extends ProtocolError {
|
|
@@ -38,10 +43,18 @@ function captureResponse(value, resourceLinks = false) {
|
|
|
38
43
|
}
|
|
39
44
|
const empty = z.object({}).strict();
|
|
40
45
|
const query = z.object({ query: z.string().max(200).optional(), limit: z.number().int().min(1).max(50).default(20) }).strict();
|
|
46
|
+
const recordRange = { recordIndex: z.number().int().min(0).optional(), offset: z.number().int().min(0).optional(), chars: z.number().int().min(1).max(2000).optional() };
|
|
47
|
+
const paged = { cursor: z.string().max(300).optional(), limit: z.number().int().min(1).max(resultBudgets.maxLimit).optional() };
|
|
41
48
|
const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
42
49
|
const schemas = { params: z.object({ cursor: z.string().optional() }).passthrough(), result: z.object({ resultType: z.literal("complete") }).passthrough() };
|
|
43
50
|
const ownerPatch = z.object({ unit: z.string().min(1).max(256), patch: z.string().min(1).max(mutationLimits.patchBytes).optional(), patchFile: z.string().min(1).max(500).optional() }).strict().refine(args => (args.patch !== undefined) !== (args.patchFile !== undefined), "Provide exactly one of patch or patchFile.");
|
|
44
51
|
|
|
52
|
+
function resourceQuery(uri, allowed) {
|
|
53
|
+
const url=new URL(uri);
|
|
54
|
+
if([...url.searchParams.keys()].some(key=>!allowed.includes(key)))throw new Error("Unknown paged resource query.");
|
|
55
|
+
return Object.fromEntries([...url.searchParams].map(([key,value])=>[key,["limit","recordIndex","offset","chars"].includes(key)?Number(value):value]));
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollInterval = 1000, previewWatch, showcaseUrl, notifications, inspection, mutations = false } = {}) {
|
|
46
59
|
const root = path.resolve(directory);
|
|
47
60
|
const cache = inspection ?? {};
|
|
@@ -49,6 +62,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
49
62
|
const captures = cache.captures ??= createCaptureController(root);
|
|
50
63
|
const edits = cache.edits ??= createMutationController(root, { captures });
|
|
51
64
|
const verification = cache.verification ??= createVerificationController(root);
|
|
65
|
+
const executionResults = cache.executionResults ??= createExecutionResults(root);
|
|
52
66
|
let options = loadConfig(root);
|
|
53
67
|
const signature = `${root}:${JSON.stringify(options.config)}`;
|
|
54
68
|
if (cache.signature !== signature) {
|
|
@@ -108,6 +122,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
108
122
|
return [
|
|
109
123
|
{ uri: GUIDE_URI, name: "Start here: design-system workflow", description: "Read first for UI/ADS tasks, especially when only resource access is exposed. Explains the exact workspace, tool sequence, skill selection, previews, project commands and fallback.", mimeType: "text/markdown" },
|
|
110
124
|
{ uri: "ads://guides/behaviors", name: "Controller and behavioral verification guide", mimeType: "text/markdown" },
|
|
125
|
+
{ uri: "ads://guides/evidence", name: "Failure boundaries and compact investigations", mimeType: "text/markdown" },
|
|
111
126
|
{ uri: "ads://guides/atomic-styles", name: "Atomic styles: composition conflicts and custom-property extraction", mimeType: "text/markdown" },
|
|
112
127
|
{ uri: "ads://guides/scenarios", name: "Scenario coverage and public class-helper contracts", mimeType: "text/markdown" },
|
|
113
128
|
{ uri: "ads://guides/captures", name: "On-demand captures and candidate visual review", mimeType: "text/markdown" },
|
|
@@ -117,38 +132,42 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
117
132
|
{ uri: `${PROJECT_URI}/catalog`, name: "Public components and patterns", mimeType: "application/json" },
|
|
118
133
|
{ uri: `${PROJECT_URI}/scenarios`, name: "Independently mountable scenarios", mimeType: "application/json" },
|
|
119
134
|
{ uri: BEHAVIOR_URI, name: "Behavior contracts, demonstrations and current verification evidence", mimeType: "application/json" },
|
|
135
|
+
{ uri: RESULTS_URI, name: "Latest execution summaries and paged outcomes", mimeType: "application/json" },
|
|
136
|
+
{ uri: INVESTIGATIONS_URI, name: "Latest per-check investigation evidence", mimeType: "application/json" },
|
|
120
137
|
{ uri: CAPTURES_URI, name: "On-demand current and candidate screenshots", mimeType: "application/json" },
|
|
121
138
|
{ uri: `${PROJECT_URI}/checks`, name: "Current ADS architecture diagnostics", mimeType: "application/json" },
|
|
139
|
+
// Keep fallback workflow metadata discoverable in the bounded first page.
|
|
140
|
+
...skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description, mimeType: "text/markdown" })),
|
|
122
141
|
...documents.filter(document => promoted.has(document.uri)).sort((a, b) => Number(b.uri === context.publicEntryUri) - Number(a.uri === context.publicEntryUri)).map(({ file: _file, ...document }) => document),
|
|
123
142
|
{ uri: TOKENS_APP_URI, name: "Experimental token widget", mimeType: APP_MIME },
|
|
124
143
|
{ uri: SCENARIO_APP_URI, name: "Experimental scenario widget", mimeType: APP_MIME },
|
|
125
144
|
...(options.config.mcp.previewManifest ? [{ uri: PREVIEW_APP_URI, name: "Shared-resource preview App", mimeType: APP_MIME }, { uri: PREVIEW_MANIFEST_URI, name: "Preview dependency graph", mimeType: "application/json" }] : []),
|
|
126
145
|
...(options.config.mcp.previewManifest ? resourceAssets() : []),
|
|
127
146
|
...allApps().map((app) => ({ uri: appUri(app.id), name: app.title, description: app.description, mimeType: APP_MIME })),
|
|
128
|
-
...skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description, mimeType: "text/markdown" })),
|
|
129
147
|
...documents.filter((document) => !promoted.has(document.uri)).map(({ file: _file, ...document }) => document),
|
|
130
148
|
...[...skills.files.values()].filter((file) => !skillEntries.has(file.uri)).map(({ uri, name, mimeType }) => ({ uri, name, mimeType })),
|
|
131
149
|
];
|
|
132
150
|
}
|
|
133
151
|
function status() {
|
|
134
|
-
const context = project();
|
|
152
|
+
const context = project(), mutationState = mutations ? edits.status() : null, behaviorState = verification.status();
|
|
135
153
|
return { projectRoot: root, workflow: workflow([...previews(), ...scenarioApps()]), project: context,
|
|
136
154
|
package: pkg.name, version: pkg.version, dsDir: options.config.dsDir, publicAlias: options.config.publicAlias,
|
|
137
155
|
components: getCatalog().filter((item) => item.kind === "components" && item.apiKind === "ui").length,
|
|
138
156
|
patterns: getCatalog().filter((item) => item.kind === "patterns" && item.apiKind === "ui").length,
|
|
139
157
|
tokenCount: inspectTokens(root).tokens.length, showcaseUrl: getShowcaseUrl(),
|
|
140
158
|
previewWatch: previewWatch?.status() ?? { state: "disabled" },
|
|
141
|
-
mutations: mutations ?
|
|
159
|
+
mutations: mutations ? { ...mutationState, progress: compactProgress(mutationState.progress), lastResult: executionResults.latest("mutation") } : { enabled: false },
|
|
142
160
|
build: summarizeBuild(builds()),
|
|
143
161
|
previews: previews(), skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description })),
|
|
144
162
|
scenarios: { count: getRegistry().scenarios.length, diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios`, tool: "ads_show_scenario" },
|
|
145
|
-
behaviors: { resourceUri: BEHAVIOR_URI, discoveryTool: "ads_behaviors", verificationTool: "ads_verify", ...
|
|
163
|
+
behaviors: { resourceUri: BEHAVIOR_URI, discoveryTool: "ads_behaviors", verificationTool: "ads_verify", investigationTool: "ads_investigate", investigationResourceUri: INVESTIGATIONS_URI, ...behaviorState, progress: compactProgress(behaviorState.progress), lastResult: executionResults.latest("verification"), resultsTool: "ads_results", resultsResourceUri: RESULTS_URI },
|
|
146
164
|
captures: { resourceUri: CAPTURES_URI, tool: "ads_capture", clearTool: "ads_capture_clear", ...captures.status() },
|
|
147
165
|
experimental: { apps: true, nativeSkills: era === "modern", nativeSkillActivation: "host-controlled" } };
|
|
148
166
|
}
|
|
149
167
|
async function read(uri, signal) {
|
|
150
168
|
if (uri === GUIDE_URI) return { contents: [{ uri, mimeType: "text/markdown", text: projectGuide(project(), [...previews(), ...scenarioApps()]) }] };
|
|
151
169
|
if (uri === "ads://guides/behaviors") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../../docs/BEHAVIORS.md", import.meta.url), "utf8") }] };
|
|
170
|
+
if (uri === "ads://guides/evidence") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/evidence.md", import.meta.url), "utf8") }] };
|
|
152
171
|
if (uri === "ads://guides/atomic-styles") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/atomic-styles.md", import.meta.url), "utf8") }] };
|
|
153
172
|
if (uri === "ads://guides/scenarios") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/scenarios.md", import.meta.url), "utf8") }] };
|
|
154
173
|
if (uri === "ads://guides/captures") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/captures.md", import.meta.url), "utf8") }] };
|
|
@@ -160,13 +179,19 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
160
179
|
else if (uri === `${PROJECT_URI}/catalog`) value = { items: getCatalog() };
|
|
161
180
|
else if (uri === `${PROJECT_URI}/scenarios`) value = getRegistry();
|
|
162
181
|
else if (uri === `${PROJECT_URI}/checks`) value = checkProject(root);
|
|
163
|
-
else if (uri === BEHAVIOR_URI) value =
|
|
182
|
+
else if (uri === BEHAVIOR_URI || uri.startsWith(`${BEHAVIOR_URI}?`)) value = behaviorPage(options, resourceQuery(uri, ["unit", "api", "query", "section", "cursor", "limit"]));
|
|
183
|
+
else if (uri === INVESTIGATIONS_URI || uri.startsWith(`${INVESTIGATIONS_URI}?`)) value = investigationPage(root, resourceQuery(uri, ["unit", "check", "runId", "section", "cursor", "limit", "recordIndex", "offset", "chars"]));
|
|
184
|
+
else if (uri === RESULTS_URI || uri.startsWith("ads://results/")) return { contents: [executionResults.readResource(uri)] };
|
|
164
185
|
else if (uri === CAPTURES_URI) value = captureIndex();
|
|
165
186
|
else if (uri === PREVIEW_MANIFEST_URI && options.config.mcp.previewManifest) {
|
|
166
187
|
value = readPreviewManifest(previewOptions());
|
|
167
188
|
if (!value) throw new Error("Preview resources are not built yet. Invoke ads_show_scenario to start the managed compiler.");
|
|
168
189
|
}
|
|
169
190
|
if (value !== undefined) return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(value) }] };
|
|
191
|
+
if (uri.startsWith("ads://investigations/") || uri.startsWith("ads://verification-artifacts/")) {
|
|
192
|
+
try { return {contents:[readInvestigationResource(root,uri)]}; }
|
|
193
|
+
catch(error){throw new ResourceNotFoundError(uri,error.message);}
|
|
194
|
+
}
|
|
170
195
|
if (uri.startsWith("ads://captures/")) {
|
|
171
196
|
try { const inventory = captureInventory(); return { contents: [captures.store.readResource(uri, inventory.scenarios, inventory.revision, { allowUnknownTargets: !!inventory.diagnostics.length })] }; }
|
|
172
197
|
catch (error) { if (error.code === "ADS_CAPTURE_NOT_FOUND") throw new ResourceNotFoundError(uri, error.message); throw error; }
|
|
@@ -198,6 +223,11 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
198
223
|
return build;
|
|
199
224
|
}
|
|
200
225
|
|
|
226
|
+
async function finishExecution(kind, result) {
|
|
227
|
+
const summary = executionResults.save(kind, result);
|
|
228
|
+
try { await refresh(); } catch (error) { process.stderr.write(`ads mcp: result saved; notification failed: ${error.message}\n`); }
|
|
229
|
+
return { ...json(summary), ...(!result.ok ? { isError: true } : {}) };
|
|
230
|
+
}
|
|
201
231
|
async function applyEdits(args,signal,progress) {
|
|
202
232
|
const result = await edits.apply(args,signal,progress);
|
|
203
233
|
if (result.screenshots) await refresh();
|
|
@@ -208,7 +238,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
208
238
|
await refresh(true);
|
|
209
239
|
} catch (error) {result.warnings=[...(result.warnings ?? []),{rule:"mutation-notification",message:`Changes were applied, but preview refresh failed: ${error.message.slice(0,2000)}. Recheck ads_status and reopen the scenario.`}];}
|
|
210
240
|
}
|
|
211
|
-
return
|
|
241
|
+
return finishExecution("mutation", { ...(args.patches ? {units:args.patches.map(item=>item.unit),transaction:true} : {unit:args.unit}), ...result });
|
|
212
242
|
}
|
|
213
243
|
function tools() {
|
|
214
244
|
return [
|
|
@@ -244,7 +274,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
244
274
|
} },
|
|
245
275
|
{ name: "ads_check", description: "Run read-only core ADS architecture checks; returns blocking diagnostics separately from nonblocking advisories, with warnings before informational limits. Atomic warnings include helper/forwarded-class paths and ads://guides/atomic-styles guidance; unresolved paths do not establish safe composition. Project Oxlint, typecheck, build and browser tests remain separate.", schema: z.object({ limit: z.number().int().min(1).max(50).default(20) }).strict(), call: ({ limit }) => {
|
|
246
276
|
const result = checkProject(root);
|
|
247
|
-
return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), advisoryCount: result.advisories?.length ?? 0, advisories: (result.advisories ?? []).slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
|
|
277
|
+
return json({ ok: result.ok, acceptance:result.acceptance, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), advisoryCount: result.advisories?.length ?? 0, advisories: (result.advisories ?? []).slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
|
|
248
278
|
} },
|
|
249
279
|
{ name: "ads_skills", description: "Select one ADS workflow after ads_status: ads-designer for building/refining/debugging UI; ads-migrate for a library reference; ads-import for another project's ADS; ads-motion for animation; ads-break-ui for stress testing. Then read that workflow with ads_read_skill. Listing does not activate a native skill.", schema: empty, call: () => json({ skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, ...frontmatter })) }) },
|
|
250
280
|
{ name: "ads_read_skill", description: "Read a selected skill or relative supporting text file in bounded chunks. This is explicit instruction access, not automatic native skill activation/install.", schema: z.object({ name: z.string().max(64), file: z.string().max(300).default("SKILL.md"), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(8000).default(6000) }).strict(), call: ({ name, file, offset, limit }) => {
|
|
@@ -272,22 +302,12 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
272
302
|
}
|
|
273
303
|
return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
|
|
274
304
|
} },
|
|
275
|
-
{ name: "ads_behaviors", description: "Discover
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (unit) return item;
|
|
280
|
-
const apis = Object.values(item.subjects).filter(subject => subject.api).map(subject => subject.api);
|
|
281
|
-
const demonstrations = [...new Set(Object.values(item.demonstrations).flat())];
|
|
282
|
-
return { unit: item.unit, file: item.file, apis: apis.slice(0, 20), apiCount: apis.length,
|
|
283
|
-
guarantees: item.guarantees.slice(0, 5).map(guarantee => ({ id: guarantee.id, result: guarantee.result })), guaranteeCount: item.guarantees.length,
|
|
284
|
-
demonstrations: demonstrations.slice(0, 10), demonstrationCount: demonstrations.length, checkCount: Object.keys(item.checks).length };
|
|
285
|
-
}), diagnostics: inventory.diagnostics.slice(0, 50), resourceUri: BEHAVIOR_URI });
|
|
286
|
-
} },
|
|
287
|
-
{ name: "ads_verify", description: "Run one owner's registered development behavior checks using its configured runner/profile. Executes trusted project commands with bounded output, cancellation and process-tree cleanup; installs nothing. Progress and per-check timings are available through ads_status; a profile timeout bounds the whole run. Browser profiles use an isolated managed showcase/MCP App. React StrictMode requires a project development harness, not a production widget. Missing/skipped cases, stale inputs and process exit alone cannot produce passing evidence. Discover exact unit/check IDs with ads_behaviors first.", schema: z.object({ unit: z.string().min(1).max(256), checks: z.array(z.string().min(1).max(100)).min(1).max(100).optional() }).strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, call: async (args, signal, progress) => {
|
|
305
|
+
{ name: "ads_behaviors", description: "Discover behavior owners, then page an exact unit's checks, guarantees, subjects, demonstrations or profiles. Summary listings never embed runner logs. Default 10/max 20 entries and 8 KiB JSON per page; continue with nextCursor and identical filters. Cursors expire when discovery/evidence changes. Read sourceFile with ads_read_file for a complete contract. Metadata and reachable calls do not prove execution; use ads_verify for registered checks.", schema: z.object({ query: z.string().max(200).optional(), unit: z.string().min(1).max(256).optional(), api: z.string().max(200).optional(), section: z.enum(behaviorSections).optional(), ...paged }).strict(), call: args => json(behaviorPage(options,args)) },
|
|
306
|
+
{ name: "ads_results", title: "Read execution outcomes", description: "Read saved ads_verify/ads_apply_patch/ads_apply_transaction outcomes without rerunning anything. Pass the returned resultId; select outcomes, checks, diagnostics, advisories, warnings, changes, captures, acceptance, investigation or selection. Default 10/max 20 rows, at most 8 KiB JSON per page. Follow nextCursor with identical section/status. Rows marked detailTruncated can be read in full using recordIndex and offset/chars (max 2000); follow nextOffset. Only the latest verification and mutation snapshots are retained; replaced IDs fail explicitly. For one outcome's browser evidence use ads_investigate with its exact unit/check/runId.", schema: z.object({ resultId: z.string().uuid(), section: z.enum(resultSections).optional(), status: z.enum(["all","passed","not_passed"]).optional(), ...paged, ...recordRange }).strict(), call: args => json(executionResults.page(args)) },
|
|
307
|
+
{ name: "ads_investigate", title: "Inspect check evidence", description: "Read a summary for an exact unit/check, or list latest investigations when no check is selected. Pass the outcome's runId to reject superseded evidence. Summary is at most 4 KiB JSON; details, requests, browserErrors and artifacts are separate paged sections (default 10/max 20 entries, 8 KiB). Read truncated rows with recordIndex/offset/chars and the exact runId. Artifact URIs are read separately. Does not rerun checks or infer root cause. Only the latest bundle per check is retained, including candidate runs.", schema: z.object({unit:z.string().min(1).max(256).optional(),check:z.string().min(1).max(100).optional(),runId:z.string().min(1).max(100).optional(),section:z.enum(investigationSections).optional(),...paged,...recordRange}).strict(), call: args => json(investigationPage(root,args)) },
|
|
308
|
+
{ name: "ads_verify", description: "Run one owner's registered development behavior checks using its configured runner/profile. Executes trusted project commands with bounded output, cancellation and process-tree cleanup; installs nothing. Progress and per-check timings are available through ads_status; a profile timeout bounds the whole run. Browser profiles use an isolated managed showcase/MCP App. React StrictMode requires a project development harness, not a production widget. Missing/skipped cases, stale inputs and process exit alone cannot produce passing evidence. Replies contain counts, acceptance, at most three failure summaries and a resultId (4 KiB JSON maximum). Page outcomes with ads_results; inspect one exact unit/check/runId with ads_investigate. Discover exact unit/check IDs with ads_behaviors first.", schema: z.object({ unit: z.string().min(1).max(256), checks: z.array(z.string().min(1).max(100)).min(1).max(100).optional() }).strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, call: async (args, signal, progress) => {
|
|
288
309
|
const result = await verification.run(args, signal, progress);
|
|
289
|
-
|
|
290
|
-
return { ...json(result), ...(!result.ok ? { isError: true } : {}) };
|
|
310
|
+
return finishExecution("verification", {unit:args.unit,...result});
|
|
291
311
|
} },
|
|
292
312
|
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation including shared credits/license notices; assets/brand, assets/fonts and asset groups; configuration with ds.config.json, package/build/lint configuration and registered project guard/compiler sources. List summaries with query, or pass an exact unit ID for its manifest. Manifests include filePolicy for supported new documentation paths and companionFiles with permitted shared-entry or owner-only style-boundary bookkeeping; preserve peer permissions and unrelated configuration. Imported peers stay separate. New owners follow the existing colocation convention.", schema: query.extend({ unit: z.string().min(1).max(256).optional() }), call: ({ unit, query: search = "", limit }) => {
|
|
293
313
|
const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
|
|
@@ -299,11 +319,11 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
299
319
|
const {patch,...result}=reconcileTokenSamples(options,args);
|
|
300
320
|
return json({...result,removed:result.removed.slice(0,50),unresolved:result.unresolved.slice(0,50),unresolvedCount:result.unresolved.length,...storePreparedPatch(options,patch)});
|
|
301
321
|
}},
|
|
302
|
-
{ name: "ads_apply_patch", description: "Apply one Git diff to one ADS unit from ads_units. Pass exactly one of patch (inline) or patchFile (a regular .patch/.diff under .ds/patches/, for large/binary diffs without model-context bytes). Text uses --- a/path, +++ b/path and exact @@ hunks; /dev/null creates/deletes. Git binary patches require git diff --binary --full-index and installed Git. A unit owns its implementation, previews and fixtures; imported peers are separate. Foundation, documentation, assets and configuration are editable too. Configuration changes may register/edit project guards and builders; the proposed schema, mandatory core/Oxlint and candidate project checks must pass. Only selected-unit public re-exports may change; preserve unrelated statements. A real selected-unit re-export may remove a redundant export {}; placeholder. A tokens patch may include its companionFiles foundation entry only to add/remove canonical imports required by token files created/deleted in this patch, preserving other imports/comments and canonical order. A runtime owner patch may also include its ds.config.json companion solely for that owner's inlineStyleFiles/styleWrites entries alongside its source. Unused permissions fail validation. Set dryRun true to validate without writing. Optional screenshot:true plus captures:[{scenario,target?,theme?,viewport?}] captures current/candidate only after gates pass. For genuinely coupled owner changes use ads_apply_transaction with one patch per owner. Fix rejected patches and inspect current previews after acceptance.",
|
|
322
|
+
{ name: "ads_apply_patch", description: "Apply one Git diff to one ADS unit from ads_units. Pass exactly one of patch (inline) or patchFile (a regular .patch/.diff under .ds/patches/, for large/binary diffs without model-context bytes). Text uses --- a/path, +++ b/path and exact @@ hunks; /dev/null creates/deletes. Git binary patches require git diff --binary --full-index and installed Git. A unit owns its implementation, previews and fixtures; imported peers are separate. Foundation, documentation, assets and configuration are editable too. Configuration changes may register/edit project guards and builders; the proposed schema, mandatory core/Oxlint and candidate project checks must pass. Only selected-unit public re-exports may change; preserve unrelated statements. A real selected-unit re-export may remove a redundant export {}; placeholder. A tokens patch may include its companionFiles foundation entry only to add/remove canonical imports required by token files created/deleted in this patch, preserving other imports/comments and canonical order. A runtime owner patch may also include its ds.config.json companion solely for that owner's inlineStyleFiles/styleWrites entries alongside its source. Unused permissions fail validation. Set dryRun true to validate without writing. Optional screenshot:true plus captures:[{scenario,target?,theme?,viewport?}] captures current/candidate only after gates pass. For genuinely coupled owner changes use ads_apply_transaction with one patch per owner. Fix rejected patches and inspect current previews after acceptance. Reply is a 4 KiB summary; read diagnostics, outcomes and requested captures through ads_results using its resultId.",
|
|
303
323
|
schema: z.object({ unit: z.string().min(1).max(256), dryRun: z.boolean(), patch: z.string().min(1).max(mutationLimits.patchBytes).optional(), patchFile: z.string().min(1).max(500).optional(), ...screenshotFields }).strict().refine(args => (args.patch !== undefined) !== (args.patchFile !== undefined), "Provide exactly one of patch or patchFile."),
|
|
304
324
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, _meta: { ui: { visibility: ["model"] } },
|
|
305
325
|
call: applyEdits,
|
|
306
|
-
}, {name:"ads_apply_transaction",description:"Apply genuinely coupled ADS changes as 2–8 distinct owner Git patches. Each patch must obey its unit and narrow companion rules. Patches run in order; later patches use the earlier candidate's shared-entry content. Core guards, registered Oxlint/project checks and affected behavior checks validate the complete final candidate before any live source is written. Maximum 32 distinct files/16 MiB combined. Dry runs, failures and cancellation publish nothing; publication rolls back on write failure. Optional screenshot:true with dryRun:true and explicit captures selects current/candidate images. Prefer ads_apply_patch for independent edits.",schema:z.object({patches:z.array(ownerPatch).min(2).max(mutationLimits.units),dryRun:z.boolean(),...screenshotFields}).strict(),annotations:{readOnlyHint:false,destructiveHint:true,idempotentHint:false,openWorldHint:false},_meta:{ui:{visibility:["model"]}},call:applyEdits}] : []),
|
|
326
|
+
}, {name:"ads_apply_transaction",description:"Apply genuinely coupled ADS changes as 2–8 distinct owner Git patches. Each patch must obey its unit and narrow companion rules. Patches run in order; later patches use the earlier candidate's shared-entry content. Core guards, registered Oxlint/project checks and affected behavior checks validate the complete final candidate before any live source is written. Maximum 32 distinct files/16 MiB combined. Dry runs, failures and cancellation publish nothing; publication rolls back on write failure. Optional screenshot:true with dryRun:true and explicit captures selects current/candidate images. Prefer ads_apply_patch for independent edits. Reply is a bounded summary; page diagnostics, outcomes and captures with ads_results using the returned resultId.",schema:z.object({patches:z.array(ownerPatch).min(2).max(mutationLimits.units),dryRun:z.boolean(),...screenshotFields}).strict(),annotations:{readOnlyHint:false,destructiveHint:true,idempotentHint:false,openWorldHint:false},_meta:{ui:{visibility:["model"]}},call:applyEdits}] : []),
|
|
307
327
|
...options.config.mcp.apps.map((app) => ({ name: `ads_show_${app.id.replaceAll("-", "_")}`, title: app.title, description: `${app.description ?? app.title}. Renders the project's built ${app.kind} examples through MCP Apps; text fallback provides the showcase URL.`, schema: empty, _meta: { ui: { resourceUri: appUri(app.id) } }, call: async (_args, signal) => {
|
|
308
328
|
// Validate readiness without embedding executable HTML in model results.
|
|
309
329
|
const build = await previewBuild(app, signal);
|
|
@@ -327,10 +347,10 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
327
347
|
let progress = 0;
|
|
328
348
|
const token = context.mcpReq._meta?.progressToken;
|
|
329
349
|
const notify = token === undefined ? undefined : value => {
|
|
330
|
-
void context.mcpReq.notify({ method: "notifications/progress", params: { progressToken: token, progress: ++progress, message: value.message ?? `${value.profile}: ${value.completed}/${value.total} completed; ${value.active.map(item => `${item.unit}#${item.check}`).join(", ") || "no active check reported"}; ${value.elapsedMs} ms elapsed` } }).catch(() => {});
|
|
350
|
+
void context.mcpReq.notify({ method: "notifications/progress", params: { progressToken: token, progress: ++progress, message: value.message ?? `${value.profile}: ${value.completed}/${value.total} completed; ${(value.active ?? []).slice(0,3).map(item => `${item.unit}#${item.check}`).join(", ") || "no active check reported"}; ${value.elapsedMs} ms elapsed` } }).catch(() => {});
|
|
331
351
|
};
|
|
332
352
|
try { return await tool.call(input.data, context.mcpReq.signal, notify); }
|
|
333
|
-
catch (error) { return { isError: true, content: [{ type: "text", text: error.message }],
|
|
353
|
+
catch (error) { return { isError: true, content: [{ type: "text", text: error.message }], structuredContent: {ok:false,message:error.message,failure:failure(error.build?"build":"runner",error.build?"preview-build":"tool-execution",error.build?.code??"ADS_TOOL_ERROR",{certainty:"observed-boundary"}),...(error.build?{build:error.build}:{})} }; }
|
|
334
354
|
});
|
|
335
355
|
server.setRequestHandler("resources/list", ({ params = {} }) => {
|
|
336
356
|
previewWatch?.start();
|
|
@@ -338,10 +358,20 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
338
358
|
const listing = page(resources(), cursor, "resources");
|
|
339
359
|
return { resources: listing.items, ...(listing.nextCursor ? { nextCursor: listing.nextCursor } : {}) };
|
|
340
360
|
});
|
|
341
|
-
server.setRequestHandler("resources/templates/list", () => ({ resourceTemplates: [
|
|
361
|
+
server.setRequestHandler("resources/templates/list", () => ({ resourceTemplates: [
|
|
362
|
+
{ uriTemplate: "ads://captures/{key}/{slot}", name: "Current/candidate screenshot", description: "Exact keys and available slots come from ads://project/captures. Slots are replaced in place, not revision history.", mimeType: "image/png" },
|
|
363
|
+
{ uriTemplate: "ads://results/{resultId}", name: "Saved execution summary", description: "Use resultId from execution or ads://project/results. Query section/status/cursor/limit for pages, or section/recordIndex/offset/chars for exact record ranges. Only the latest result of each kind remains available.", mimeType: "application/json" },
|
|
364
|
+
{ uriTemplate: "ads://investigations/{unit}/{check}", name: "Latest check investigation", description: "Use encoded unit/check segments from ads://project/investigations. One latest bundle per check, including candidate verification.", mimeType: "application/json" },
|
|
365
|
+
] }));
|
|
342
366
|
server.setRequestHandler("resources/read", ({ params: { uri } }, context) => read(uri, context.mcpReq.signal));
|
|
343
367
|
server.setRequestHandler("resources/subscribe", ({ params: { uri } }) => {
|
|
344
368
|
if (!resources().some((entry) => entry.uri === uri)) {
|
|
369
|
+
if(uri.startsWith("ads://results/")) {
|
|
370
|
+
try {executionResults.readResource(uri);subscribed.add(uri);return {};}catch{throw new ResourceNotFoundError(uri,"Unknown or superseded execution result.");}
|
|
371
|
+
}
|
|
372
|
+
if(uri.startsWith("ads://investigations/")) {
|
|
373
|
+
try {readInvestigationResource(root,uri);subscribed.add(uri);return {};}catch{throw new ResourceNotFoundError(uri,"Unknown check investigation.");}
|
|
374
|
+
}
|
|
345
375
|
if (!uri.startsWith("ads://captures/")) throw new ResourceNotFoundError(uri, "Unknown ADS resource subscription.");
|
|
346
376
|
try { const inventory = captureInventory(); captures.store.readResource(uri, inventory.scenarios, inventory.revision, { allowUnknownTargets: !!inventory.diagnostics.length }); } catch { throw new ResourceNotFoundError(uri, "Unknown or absent screenshot slot."); }
|
|
347
377
|
}
|
|
@@ -382,6 +412,9 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
382
412
|
let previousBuild = JSON.stringify(builds());
|
|
383
413
|
let previousCaptures = captures.store.fingerprint();
|
|
384
414
|
let previousCaptureUris = captures.store.resourceUris();
|
|
415
|
+
const investigationFingerprint=()=>fileFingerprint([path.join(root,".ds/investigations.json")]);
|
|
416
|
+
let previousInvestigations=investigationFingerprint();
|
|
417
|
+
let previousResults=executionResults.fingerprint();
|
|
385
418
|
async function refresh(force = false) {
|
|
386
419
|
// Polling covers replaced files and new directories consistently across OSes.
|
|
387
420
|
cache.documents = undefined;
|
|
@@ -394,7 +427,9 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
394
427
|
const artifactsChanged = artifacts !== previousArtifacts;
|
|
395
428
|
const assetsChanged = assets !== previousAssets;
|
|
396
429
|
const capturesChanged = captures.store.fingerprint() !== previousCaptures;
|
|
397
|
-
|
|
430
|
+
const investigationsChanged=investigationFingerprint()!==previousInvestigations;
|
|
431
|
+
const resultsChanged=executionResults.fingerprint()!==previousResults;
|
|
432
|
+
if (!filesChanged && !buildChanged && !artifactsChanged && !assetsChanged && !capturesChanged && !investigationsChanged && !resultsChanged) return false;
|
|
398
433
|
if (filesChanged) {
|
|
399
434
|
const updated = loadConfig(root);
|
|
400
435
|
await previewWatch?.reconfigure?.(updated.config.mcp);
|
|
@@ -411,7 +446,9 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
411
446
|
previousAssets = assetFingerprint();
|
|
412
447
|
previousBuild = JSON.stringify(builds());
|
|
413
448
|
const targets = filesChanged ? era === "modern" ? resources().map((entry) => entry.uri).filter((uri) => !skills.files.has(uri)) : [...subscribed]
|
|
414
|
-
: [BUILD_URI, BEHAVIOR_URI, `${PROJECT_URI}/status`, ...(options.config.mcp.previewManifest ? [PREVIEW_MANIFEST_URI] : []), ...(artifactsChanged ? allApps().map(app => appUri(app.id)) : [])].filter((uri) => era === "modern" || subscribed.has(uri));
|
|
449
|
+
: [BUILD_URI, BEHAVIOR_URI, INVESTIGATIONS_URI, `${PROJECT_URI}/status`, ...(options.config.mcp.previewManifest ? [PREVIEW_MANIFEST_URI] : []), ...(artifactsChanged ? allApps().map(app => appUri(app.id)) : [])].filter((uri) => era === "modern" || subscribed.has(uri));
|
|
450
|
+
if(resultsChanged){targets.push(...[RESULTS_URI,...executionResults.index().results.map(entry=>entry.resourceUri),...[...subscribed].filter(uri=>uri.startsWith("ads://results/"))].filter(uri=>era==="modern"||subscribed.has(uri)));previousResults=executionResults.fingerprint();}
|
|
451
|
+
if(investigationsChanged){targets.push(...[INVESTIGATIONS_URI,...investigationIndex(root).entries.map(entry=>entry.resourceUri),...[...subscribed].filter(uri=>uri.startsWith("ads://investigations/"))].filter(uri=>era==="modern"||subscribed.has(uri)));previousInvestigations=investigationFingerprint();}
|
|
415
452
|
if (capturesChanged || filesChanged) {
|
|
416
453
|
const current = (captures.store.fingerprint() ? captureIndex().captures : []).flatMap(record => [record.current?.resourceUri, record.candidate?.resourceUri].filter(Boolean));
|
|
417
454
|
targets.push(...[CAPTURES_URI, ...previousCaptureUris, ...current].filter(uri => era === "modern" || subscribed.has(uri)));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ident, lexer, parse, string, walk, generate } from "css-tree";
|
|
2
|
+
|
|
3
|
+
// CSS Properties and Values API Level 1 syntax types, not project token roles.
|
|
4
|
+
const types=new Set(["angle","color","custom-ident","image","integer","length","length-percentage","number","percentage","resolution","string","time","transform-function","transform-list","url"]);
|
|
5
|
+
const relative=/^(?:r?em|r?ex|r?cap|r?ch|r?ic|r?lh|[sld]?v(?:w|h|i|b|min|max)|cq(?:w|h|i|b|min|max))$/i;
|
|
6
|
+
const descriptors=new Set(["syntax","inherits","initial-value"]);
|
|
7
|
+
|
|
8
|
+
/** Registration metadata has its own CSS grammar; it is not an element style. */
|
|
9
|
+
export function propertyRegistration(rule) {
|
|
10
|
+
const name=ident.decode(rule.params.trim());
|
|
11
|
+
if(!name.startsWith("--")||name==="--"||lexer.matchType("custom-property-name",rule.params).error)throw new Error("@property needs one literal custom-property name.");
|
|
12
|
+
for(let parent=rule.parent;parent&&parent.type!=="root";parent=parent.parent)if(parent.type!=="atrule"||["property","font-face"].includes(parent.name.toLowerCase())||/keyframes$/i.test(parent.name))throw new Error("@property is document-global. Place it at template/sheet level or inside conditional at-rules, not element selectors or another descriptor block.");
|
|
13
|
+
const values={};
|
|
14
|
+
for(const node of rule.nodes??[]){
|
|
15
|
+
if(node.type==="comment")continue;
|
|
16
|
+
if(node.type!=="decl"||!descriptors.has(node.prop.toLowerCase())||node.important||Object.hasOwn(values,node.prop.toLowerCase()))throw new Error("@property accepts only one syntax, inherits and initial-value descriptor; no styles, nested rules or !important.");
|
|
17
|
+
values[node.prop.toLowerCase()]=node.value;
|
|
18
|
+
}
|
|
19
|
+
const syntaxAst=parse(values.syntax??"",{context:"value"}),syntaxNode=syntaxAst.children.first;
|
|
20
|
+
if(syntaxAst.children.size!==1||syntaxNode?.type!=="String")throw new Error("@property syntax must be one CSS string.");
|
|
21
|
+
const syntax=string.decode(generate(syntaxNode)).trim();
|
|
22
|
+
if(syntax!=="*"&&(!syntax||!syntax.split("|").every(part=>{
|
|
23
|
+
part=part.trim();const multiplier=/[+#]$/.test(part)?part.at(-1):"",base=multiplier?part.slice(0,-1).trim():part,match=/^<([a-z-]+)>$/.exec(base);
|
|
24
|
+
return match?types.has(match[1])&&!(match[1]==="transform-list"&&multiplier):!!base&&!lexer.matchType("custom-ident",base).error;
|
|
25
|
+
})))throw new Error("Invalid @property syntax. Use the CSS Properties and Values API grammar, not a project-specific type.");
|
|
26
|
+
if(!/^(?:true|false)$/i.test(values.inherits??""))throw new Error("@property requires inherits: true or false.");
|
|
27
|
+
if(values["initial-value"]===undefined&&syntax!=="*")throw new Error("Typed @property registrations require initial-value.");
|
|
28
|
+
if(values["initial-value"]!==undefined){
|
|
29
|
+
const initial=parse(values["initial-value"],{context:"value"});
|
|
30
|
+
if(syntax!=="*"){
|
|
31
|
+
walk(initial,node=>{
|
|
32
|
+
if(node.type==="Function"&&/^(?:var|env|attr)$/i.test(node.name)||node.type==="Dimension"&&relative.test(node.unit)||node.type==="Identifier"&&/^(?:currentcolor|inherit|initial|unset|revert(?:-layer)?)$/i.test(node.name))throw new Error("Typed @property initial-value must be computationally independent: no var(), relative units or currentColor. Keep design assignments token-backed outside this descriptor.");
|
|
33
|
+
});
|
|
34
|
+
const match=lexer.match(syntax,initial);
|
|
35
|
+
if(match.error)throw new Error("@property initial-value must match its declared syntax.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {name,signature:JSON.stringify([syntax,values.inherits.toLowerCase(),values["initial-value"]===undefined?null:generate(parse(values["initial-value"],{context:"value"}))])};
|
|
39
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import { isDevelopmentFile, isInside } from "./config.mjs";
|
|
5
|
+
import { publicApi } from "./public-api.mjs";
|
|
6
|
+
import { createSourceValues, reference, literal, isFunction } from "./source-values.mjs";
|
|
7
|
+
import { uiOperation } from "./ui-framework.mjs";
|
|
8
|
+
import { nestedSelectors, selectorWitness } from "./selector-match.mjs";
|
|
9
|
+
|
|
10
|
+
/** Native semantics may be composed through actual public runtime styles.
|
|
11
|
+
* A matching tag relationship is evidence of ownership, not visual correctness. */
|
|
12
|
+
export function createScenarioSelectors(program, options) {
|
|
13
|
+
const checker = program.getTypeChecker(), templates = new WeakMap(), cache = new WeakMap();
|
|
14
|
+
function imported(node, name, seen = new Set()) {
|
|
15
|
+
if (!node || seen.has(node)) return false;
|
|
16
|
+
seen.add(node);
|
|
17
|
+
const base = ts.isPropertyAccessExpression(node) ? node.expression : node;
|
|
18
|
+
return checker.getSymbolAtLocation(base)?.declarations?.some(declaration => {
|
|
19
|
+
if (ts.isImportSpecifier(declaration)) return (declaration.propertyName ?? declaration.name).text === name && ["@linaria/atomic", "@linaria/core"].includes(declaration.parent.parent.parent.moduleSpecifier.text);
|
|
20
|
+
if (ts.isNamespaceImport(declaration) && ts.isPropertyAccessExpression(node)) return node.name.text === name && ["@linaria/atomic", "@linaria/core"].includes(declaration.parent.parent.moduleSpecifier.text);
|
|
21
|
+
return ts.isVariableDeclaration(declaration) && imported(declaration.initializer, name, seen);
|
|
22
|
+
}) ?? false;
|
|
23
|
+
}
|
|
24
|
+
const isCx = node => imported(node, "cx");
|
|
25
|
+
const values = createSourceValues(program, options, { strict: true, preserveCall: node => isCx(node) || uiOperation(node, checker) === "createElement" });
|
|
26
|
+
const { state, next, resolve, get, keys, props, returns } = values;
|
|
27
|
+
const publicNodes = new Set(publicApi(program, options).filter(item => item.renderable).map(item => resolve(reference(item.declaration.name ?? item.declaration), state())).filter(value => value.kind === "node").map(value => value.node));
|
|
28
|
+
const runtime = node => !isDevelopmentFile(node.getSourceFile().fileName, options) && ["components", "patterns"].some(folder => isInside(path.join(options.dsRoot, folder), node.getSourceFile().fileName));
|
|
29
|
+
function selectors(node) {
|
|
30
|
+
if (templates.has(node)) return templates.get(node);
|
|
31
|
+
const result = []; templates.set(node, result);
|
|
32
|
+
if (!runtime(node) || !ts.isNoSubstitutionTemplateLiteral(node.template)) return result;
|
|
33
|
+
try {
|
|
34
|
+
function walk(container, parents) {
|
|
35
|
+
for (const child of container.nodes ?? []) {
|
|
36
|
+
if (child.type === "rule") {
|
|
37
|
+
const selected = nestedSelectors(parents, child.selector);
|
|
38
|
+
if (child.nodes.some(value => value.type === "decl" && !value.prop.startsWith("--"))) result.push(...selected);
|
|
39
|
+
walk(child, selected);
|
|
40
|
+
} else if (child.type === "atrule" && !/(?:^|-)keyframes$/.test(child.name)) walk(child, parents);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(postcss.parse(node.template.text), ["&"]);
|
|
44
|
+
} catch { /* The CSS syntax gate owns malformed templates. */ }
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function classes(input, context) {
|
|
48
|
+
const value = resolve(input, next(context));
|
|
49
|
+
if (value.kind === "union") {
|
|
50
|
+
const all = value.values.map(branch => classes(branch, next(context)));
|
|
51
|
+
return all[0]?.filter(entry => all.every(branch => branch.includes(entry))) ?? [];
|
|
52
|
+
}
|
|
53
|
+
if (value.kind !== "node") return [];
|
|
54
|
+
if (ts.isTaggedTemplateExpression(value.node) && imported(value.node.tag, "css")) return [value.node];
|
|
55
|
+
if (ts.isCallExpression(value.node) && isCx(value.node.expression)) return value.node.arguments.flatMap(argument => classes(reference(argument, value.env, value.via), next(context)));
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
function element(node, env, via) {
|
|
59
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
60
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
61
|
+
const attributes = props(opening, env, via);
|
|
62
|
+
const children = ts.isJsxElement(node) ? node.children.filter(child => !ts.isJsxText(child) || child.text.trim()).filter(child => !ts.isJsxExpression(child) || child.expression) : [];
|
|
63
|
+
if (children.length) attributes.layers.push(new Map([["children", children.length === 1 ? reference(children[0], env, via) : { kind: "array", values: children.map(child => reference(child, env, via)) }]]));
|
|
64
|
+
return { tag: reference(opening.tagName, env, via), attributes, intrinsic: ts.isIdentifier(opening.tagName) && /^[a-z]/.test(opening.tagName.text) ? opening.tagName.text : null };
|
|
65
|
+
}
|
|
66
|
+
if (ts.isCallExpression(node) && uiOperation(node.expression, checker) === "createElement") {
|
|
67
|
+
const attributes = { kind: "object", layers: node.arguments[1] && node.arguments[1].kind !== ts.SyntaxKind.NullKeyword ? [reference(node.arguments[1], env, via)] : [] };
|
|
68
|
+
const children = node.arguments.slice(2).map(child => reference(child, env, via));
|
|
69
|
+
if (children.length) attributes.layers.push(new Map([["children", children.length === 1 ? children[0] : { kind: "array", values: children }]]));
|
|
70
|
+
return { tag: reference(node.arguments[0], env, via), attributes, intrinsic: ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null };
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return site => {
|
|
75
|
+
if (cache.has(site)) return cache.get(site);
|
|
76
|
+
const native = ts.isJsxOpeningElement(site) ? site.parent : site;
|
|
77
|
+
let steps = 0, limited = false;
|
|
78
|
+
const witnesses = [];
|
|
79
|
+
function render(input, parent, context, owned = false) {
|
|
80
|
+
if (++steps > 700 || context.depth > 40) { limited = true; return []; }
|
|
81
|
+
const value = resolve(input, next(context));
|
|
82
|
+
context = value.context ?? input.context ?? context;
|
|
83
|
+
if (value.kind === "union") {
|
|
84
|
+
// Unknown render alternatives cannot silently authorize a native control.
|
|
85
|
+
limited = true; return [];
|
|
86
|
+
}
|
|
87
|
+
if (value.kind === "array") return value.values.flatMap(child => render(child, parent, next(context), owned));
|
|
88
|
+
if (value.kind !== "node") return [];
|
|
89
|
+
const {node, env, via} = value;
|
|
90
|
+
if (ts.isJsxExpression(node)) return render(reference(node.expression, env, via), parent, next(context), owned);
|
|
91
|
+
if (ts.isJsxFragment(node)) return node.children.flatMap(child => render(reference(child, env, via), parent, next(context), owned));
|
|
92
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.flatMap(child => render(reference(child, env, via), parent, next(context), owned));
|
|
93
|
+
const view = element(node, env, via);
|
|
94
|
+
if (!view) return [];
|
|
95
|
+
const target = view.intrinsic ? literal(view.intrinsic) : resolve(view.tag, next(context));
|
|
96
|
+
if (["provider", "context"].includes(target.kind)) return render(get(view.attributes, "children", next(context)), parent, next(context), owned);
|
|
97
|
+
if (target.kind === "node" && isFunction(target.node)) {
|
|
98
|
+
const outputs = returns(target, [view.attributes], next(context), "component");
|
|
99
|
+
if (outputs.length !== 1) { limited = true; return []; }
|
|
100
|
+
return render(outputs[0], parent, next(context), owned || publicNodes.has(target.node));
|
|
101
|
+
}
|
|
102
|
+
if (target.kind !== "literal" || typeof target.value !== "string") return [];
|
|
103
|
+
const attributes = new Map(),ownedAttributes=new Set();
|
|
104
|
+
for (const name of keys(view.attributes, next(context))) {
|
|
105
|
+
const input=get(view.attributes,name,next(context));
|
|
106
|
+
const value = resolve(input, next(context));
|
|
107
|
+
attributes.set(name, value.kind === "literal" ? value.value : undefined);
|
|
108
|
+
if(owned&&runtime(node)&&input.kind==="node"&&ts.isStringLiteralLike(input.node)&&runtime(input.node))ownedAttributes.add(name);
|
|
109
|
+
}
|
|
110
|
+
const result = {tag:target.value.toLowerCase(),attributes,ownedAttributes,parent,children:[],site:node,styles:owned ? classes(get(view.attributes,"className",next(context)),next(context)) : []};
|
|
111
|
+
if (node === native) witnesses.push(result);
|
|
112
|
+
result.children = render(get(view.attributes,"children",next(context)),result,next(context),owned);
|
|
113
|
+
return [result];
|
|
114
|
+
}
|
|
115
|
+
function proves(roots) {
|
|
116
|
+
function all(node) { return [node,...node.children.flatMap(all)]; }
|
|
117
|
+
const nodes = roots.flatMap(all);
|
|
118
|
+
return witnesses.some(witness => nodes.some(anchor => anchor.styles.some(template => selectors(template).some(selector => nodes.some(target => {
|
|
119
|
+
// Styles must target the native node itself or public UI inside it.
|
|
120
|
+
let inside = target === witness;
|
|
121
|
+
for (let parent = target.parent; parent && !inside; parent = parent.parent) inside = parent === witness;
|
|
122
|
+
return inside && selectorWitness(selector,target,anchor,witness);
|
|
123
|
+
})))));
|
|
124
|
+
}
|
|
125
|
+
// First follow the native wrapper into its public contents. This also works
|
|
126
|
+
// when a scenario helper returns it and is invoked under a public container.
|
|
127
|
+
let roots = render(reference(native),null,state());
|
|
128
|
+
let ok = proves(roots);
|
|
129
|
+
if (!ok) {
|
|
130
|
+
let parent = native.parent;
|
|
131
|
+
while (parent && (ts.isJsxExpression(parent) || ts.isParenthesizedExpression(parent))) parent = parent.parent;
|
|
132
|
+
if (parent && element(parent,new Map(),[])) {
|
|
133
|
+
const view = element(parent,new Map(),[]), target = resolve(view.tag,state());
|
|
134
|
+
if (target.kind === "node" && publicNodes.has(target.node)) { witnesses.length=0; steps=0; limited=false; roots=render(reference(parent),null,state()); ok=proves(roots); }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const result = {ok, reasons:ok ? [] : [limited ? "Selector composition reached an unresolved render branch or source work limit." : "No attached public-runtime Linaria selector proves this native tag or runtime-part relationship."], evidence:ok ? {kind:"selector-composition", established:["public-runtime-style-provenance","native-position-ownership"], unverified:["computed-styles","interaction","accessibility"]} : undefined};
|
|
138
|
+
cache.set(site,result);return result;
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/scenario-trace.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function createScenarioTrace(program, options) {
|
|
|
120
120
|
let declarations = 0;
|
|
121
121
|
const template = node.template;
|
|
122
122
|
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text : template.head.text + template.templateSpans.map(span => `__ads_expression__${span.literal.text}`).join("");
|
|
123
|
-
try { postcss.parse(text).walkDecls(()
|
|
123
|
+
try { postcss.parse(text).walkDecls(declaration => { for(let parent=declaration.parent;parent;parent=parent.parent)if(parent.type==="atrule"&&/^(?:property|font-face|(?:-\w+-)?keyframes)$/i.test(parent.name))return; ++declarations; }); } catch { /* Source CSS guards own malformed templates. */ }
|
|
124
124
|
if (owned(source, via) && declarations) return result(true, true);
|
|
125
125
|
reasons.add("The returned style must originate in runtime component/pattern Linaria through a public callable helper.");
|
|
126
126
|
return result(false);
|