@reqvire-org/ads 0.3.2 → 0.3.5
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 +12 -4
- package/dist/behaviors.mjs +157 -0
- package/dist/bootstrap-ds/ADS.md +7 -0
- package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/boundaries.mjs +21 -23
- package/dist/check.mjs +25 -13
- package/dist/cli.mjs +18 -0
- package/dist/config.mjs +19 -3
- package/dist/css-values.mjs +26 -0
- package/dist/index.mjs +2 -0
- package/dist/init.mjs +1 -1
- package/dist/mcp/build-status.mjs +10 -3
- package/dist/mcp/catalog.mjs +11 -8
- package/dist/mcp/context.mjs +2 -2
- package/dist/mcp/mutations.mjs +11 -1
- package/dist/mcp/preview-owner-guard.mjs +29 -0
- package/dist/mcp/preview-watch.mjs +28 -4
- package/dist/mcp/preview-widget.bundle.js +2 -2
- package/dist/mcp/prompts.mjs +3 -3
- package/dist/mcp/server.mjs +61 -24
- package/dist/mcp/showcase-host.mjs +9 -5
- package/dist/oxlint.mjs +1 -1
- package/dist/public-api.mjs +39 -0
- package/dist/scenarios-compiler.mjs +17 -2
- package/dist/scenarios.mjs +27 -39
- package/dist/skills/README.md +3 -3
- package/dist/skills/ads-break-ui/SKILL.md +48 -4
- package/dist/skills/ads-designer/SKILL.md +49 -5
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +7 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/skills/ads-designer/references/cli.md +1 -1
- package/dist/skills/ads-designer/references/design-craft.md +1 -1
- package/dist/skills/ads-import/SKILL.md +48 -4
- package/dist/skills/ads-import/references/cli.md +1 -1
- package/dist/skills/{ads-assimilate → ads-migrate}/SKILL.md +54 -10
- package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
- package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
- package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +27 -0
- package/dist/skills/ads-motion/SKILL.md +48 -4
- package/dist/style-boundaries.mjs +59 -0
- package/dist/tokens.mjs +3 -2
- package/dist/units.mjs +56 -9
- package/dist/verification/browser.mjs +27 -0
- package/dist/verification/playwright-reporter.cjs +20 -0
- package/dist/verification.mjs +214 -0
- package/docs/ADS.md +32 -3
- package/docs/ARCHITECTURE.md +45 -4
- package/docs/BEHAVIORS.md +230 -0
- package/docs/CHANGELOG.md +48 -0
- package/docs/GUIDE.md +25 -1
- package/docs/MCP.md +76 -7
- package/package.json +4 -2
- package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/testing.md +0 -0
package/dist/mcp/server.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Server, ProtocolError, INVALID_PARAMS, ResourceNotFoundError } from "@modelcontextprotocol/server";
|
|
4
|
-
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { checkProject } from "../check.mjs";
|
|
7
7
|
import { loadConfig, sourceFiles } from "../config.mjs";
|
|
8
|
+
import { behaviorStatus, createVerificationController, BEHAVIOR_URI } from "../verification.mjs";
|
|
8
9
|
import { inspectTokens } from "../tokens.mjs";
|
|
9
10
|
import { inspectScenarios } from "../scenarios.mjs";
|
|
10
11
|
import { startShowcaseHost } from "./showcase-host.mjs";
|
|
@@ -33,6 +34,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
33
34
|
const cache = inspection ?? {};
|
|
34
35
|
const skills = cache.skills ??= createSkillStore(skillDirectory);
|
|
35
36
|
const edits = cache.edits ??= createMutationController(root);
|
|
37
|
+
const verification = cache.verification ??= createVerificationController(root);
|
|
36
38
|
let options = loadConfig(root);
|
|
37
39
|
const signature = `${root}:${JSON.stringify(options.config)}`;
|
|
38
40
|
if (cache.signature !== signature) {
|
|
@@ -43,7 +45,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
43
45
|
const project = () => { const context = projectContext(options); context.showcase.url = getShowcaseUrl(); return context; };
|
|
44
46
|
const subscribed = new Set();
|
|
45
47
|
const server = new Server({ name: "reqvire-ads", version: pkg.version }, {
|
|
46
|
-
instructions: `${serverInstructions} ${mutations ? "Guarded editing is enabled: discover one owner with ads_units, read relevant ranges with ads_read_file and send only a Git unified diff to ads_apply_patch with that unit ID. Include its own scenarios/fixtures and required re-exports together, never unrelated units.
|
|
48
|
+
instructions: `${serverInstructions} ${mutations ? "Guarded editing is enabled: discover one owner with ads_units, read relevant ranges with ads_read_file and send only a Git unified diff to ads_apply_patch with that unit ID. Include its own scenarios/fixtures and required re-exports together, never unrelated units. Manifests list companionFiles: required canonical token imports, or only the selected runtime owner's style-boundary entries in ds.config.json alongside its source. Unused style permissions fail validation. The server validates architecture, registered Oxlint and project checks before writing. Fix rejected patches in their owner. Intentional settings, project guards and builders use the configuration unit; mandatory ADS gates still run." : "Source mutation tools are disabled; source edits use the client's editor/filesystem tools."}`,
|
|
47
49
|
capabilities: { tools: { listChanged: true }, resources: { listChanged: true, subscribe: true }, prompts: {},
|
|
48
50
|
extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [APP_MIME] }, ...(era === "modern" ? { [SKILLS_EXTENSION]: { directoryRead: true } } : {}) } },
|
|
49
51
|
});
|
|
@@ -77,11 +79,13 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
77
79
|
const skillEntries = new Set(skills.skills.map((skill) => skill.uri));
|
|
78
80
|
return [
|
|
79
81
|
{ 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" },
|
|
82
|
+
{ uri: "ads://guides/behaviors", name: "Controller and behavioral verification guide", mimeType: "text/markdown" },
|
|
80
83
|
{ uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
|
|
81
84
|
{ uri: BUILD_URI, name: "Preview build status and diagnostics", mimeType: "application/json" },
|
|
82
85
|
{ uri: `${PROJECT_URI}/tokens`, name: "Project token values and themes", mimeType: "application/json" },
|
|
83
86
|
{ uri: `${PROJECT_URI}/catalog`, name: "Public components and patterns", mimeType: "application/json" },
|
|
84
87
|
{ uri: `${PROJECT_URI}/scenarios`, name: "Independently mountable scenarios", mimeType: "application/json" },
|
|
88
|
+
{ uri: BEHAVIOR_URI, name: "Behavior contracts, demonstrations and current verification evidence", mimeType: "application/json" },
|
|
85
89
|
{ uri: `${PROJECT_URI}/checks`, name: "Current ADS architecture diagnostics", mimeType: "application/json" },
|
|
86
90
|
{ uri: TOKENS_APP_URI, name: "Experimental token widget", mimeType: APP_MIME },
|
|
87
91
|
{ uri: SCENARIO_APP_URI, name: "Experimental scenario widget", mimeType: APP_MIME },
|
|
@@ -98,18 +102,20 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
98
102
|
const context = project();
|
|
99
103
|
return { projectRoot: root, workflow: workflow([...previews(), ...scenarioApps()]), project: context,
|
|
100
104
|
package: pkg.name, version: pkg.version, dsDir: options.config.dsDir, publicAlias: options.config.publicAlias,
|
|
101
|
-
components: getCatalog().filter((item) => item.kind === "components").length,
|
|
102
|
-
patterns: getCatalog().filter((item) => item.kind === "patterns").length,
|
|
105
|
+
components: getCatalog().filter((item) => item.kind === "components" && item.apiKind === "ui").length,
|
|
106
|
+
patterns: getCatalog().filter((item) => item.kind === "patterns" && item.apiKind === "ui").length,
|
|
103
107
|
tokenCount: inspectTokens(root).tokens.length, showcaseUrl: getShowcaseUrl(),
|
|
104
108
|
previewWatch: previewWatch?.status() ?? { state: "disabled" },
|
|
105
109
|
mutations: mutations ? edits.status() : { enabled: false },
|
|
106
110
|
build: summarizeBuild(builds()),
|
|
107
111
|
previews: previews(), skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description })),
|
|
108
112
|
scenarios: { count: getRegistry().scenarios.length, diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios`, tool: "ads_show_scenario" },
|
|
113
|
+
behaviors: { resourceUri: BEHAVIOR_URI, discoveryTool: "ads_behaviors", verificationTool: "ads_verify", ...verification.status() },
|
|
109
114
|
experimental: { apps: true, nativeSkills: era === "modern", nativeSkillActivation: "host-controlled" } };
|
|
110
115
|
}
|
|
111
116
|
async function read(uri, signal) {
|
|
112
117
|
if (uri === GUIDE_URI) return { contents: [{ uri, mimeType: "text/markdown", text: projectGuide(project(), [...previews(), ...scenarioApps()]) }] };
|
|
118
|
+
if (uri === "ads://guides/behaviors") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../../docs/BEHAVIORS.md", import.meta.url), "utf8") }] };
|
|
113
119
|
if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
|
|
114
120
|
let value;
|
|
115
121
|
if (uri === `${PROJECT_URI}/status`) value = status();
|
|
@@ -118,6 +124,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
118
124
|
else if (uri === `${PROJECT_URI}/catalog`) value = { items: getCatalog() };
|
|
119
125
|
else if (uri === `${PROJECT_URI}/scenarios`) value = getRegistry();
|
|
120
126
|
else if (uri === `${PROJECT_URI}/checks`) value = checkProject(root);
|
|
127
|
+
else if (uri === BEHAVIOR_URI) value = behaviorStatus(options);
|
|
121
128
|
else if (uri === PREVIEW_MANIFEST_URI && options.config.mcp.previewManifest) {
|
|
122
129
|
value = readPreviewManifest(options);
|
|
123
130
|
if (!value) throw new Error("Preview resources are not built yet. Invoke ads_show_scenario to start the managed compiler.");
|
|
@@ -153,13 +160,13 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
153
160
|
function tools() {
|
|
154
161
|
return [
|
|
155
162
|
{ name: "ads_status", description: "Start here for UI/ADS work: components, patterns, tokens/themes/branding, layout, behavior, motion and showcase review. Returns the exact project root/public API, workflow, project commands, editing mode and build diagnostics. When mutations.enabled, use guarded ads_apply_patch for ADS edits; otherwise use local editing tools.", schema: empty, call: () => json(status()) },
|
|
156
|
-
{ name: "ads_catalog", description: "Before creating/changing UI, find existing public components/patterns with a small query/limit. Returns import alias and exact source/guide URIs so you can reuse or edit the owner. Read ads_status first for workspace/workflow. Metadata only; does not render UI.", schema: query.extend({ kind: z.enum(["components", "patterns"]).optional() }), call: ({ query: search = "", limit, kind }) => {
|
|
163
|
+
{ name: "ads_catalog", description: "Before creating/changing UI or behavior, find existing public components/patterns/controllers and hooks with a small query/limit. Returns import alias and exact source/guide URIs so you can reuse or edit the owner. Read ads_status first for workspace/workflow. Metadata only; does not render UI.", schema: query.extend({ kind: z.enum(["components", "patterns", "controllers"]).optional() }), call: ({ query: search = "", limit, kind }) => {
|
|
157
164
|
const matches = getCatalog().filter((item) => (!kind || item.kind === kind) && item.name.toLowerCase().includes(search.toLowerCase()));
|
|
158
165
|
const units = edits.units();
|
|
159
166
|
return json({ total: matches.length, items: matches.slice(0, limit).map(item => ({ ...item, unit: units.find(unit => unit.files.includes(item.file))?.id ?? null })), resourceUri: `${PROJECT_URI}/catalog` });
|
|
160
167
|
} },
|
|
161
|
-
{ name: "ads_scenarios", description: "Find an independently mountable token, component, pattern or composition preview. IDs derive from source filenames; covers lists public UI and tokens lists the real token inputs. Use ads_show_scenario with an exact returned id. Showcase and MCP use this same registry.", schema: query.extend({ kind: z.enum(["tokens", "components", "patterns", "composition"]).optional(), offset: z.number().int().min(0).default(0) }), call: ({ query: search = "", limit, kind, offset }) => {
|
|
162
|
-
const matches = getRegistry().scenarios.filter(item => (!kind || item.kind === kind) && `${item.id} ${item.title} ${item.covers.join(" ")} ${item.tokens.join(" ")}`.toLowerCase().includes(search.toLowerCase()));
|
|
168
|
+
{ name: "ads_scenarios", description: "Find an independently mountable token, component, pattern or composition preview. IDs derive from source filenames; covers lists rendered public UI, exercises lists behavioral APIs and tokens lists the real token inputs. Use ads_show_scenario with an exact returned id. Showcase and MCP use this same registry.", schema: query.extend({ kind: z.enum(["tokens", "components", "patterns", "composition"]).optional(), offset: z.number().int().min(0).default(0) }), call: ({ query: search = "", limit, kind, offset }) => {
|
|
169
|
+
const matches = getRegistry().scenarios.filter(item => (!kind || item.kind === kind) && `${item.id} ${item.title} ${item.covers.join(" ")} ${(item.exercises ?? []).join(" ")} ${item.tokens.join(" ")}`.toLowerCase().includes(search.toLowerCase()));
|
|
163
170
|
const units = edits.units();
|
|
164
171
|
return json({ total: matches.length, scenarios: matches.slice(offset, offset + limit).map(item => ({ ...item, unit: units.find(unit => unit.files.includes(item.file))?.id ?? null })), ...(offset + limit < matches.length ? { nextOffset: offset + limit } : {}), diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios` });
|
|
165
172
|
} },
|
|
@@ -181,11 +188,11 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
181
188
|
const matches = inventory.tokens.filter((token) => token.name.toLowerCase().includes(search.toLowerCase()));
|
|
182
189
|
return json({ total: matches.length, tokens: matches.slice(0, limit), categories: tokenScenarios(), diagnosticCount: inventory.diagnostics.length, resourceUri: `${PROJECT_URI}/tokens` });
|
|
183
190
|
} },
|
|
184
|
-
{ name: "ads_check", description: "Run read-only core ADS architecture checks. 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 }) => {
|
|
191
|
+
{ name: "ads_check", description: "Run read-only core ADS architecture checks; returns blocking diagnostics separately from nonblocking review advisories. 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 }) => {
|
|
185
192
|
const result = checkProject(root);
|
|
186
|
-
return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
|
|
193
|
+
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` });
|
|
187
194
|
} },
|
|
188
|
-
{ name: "ads_skills", description: "Select one ADS workflow after ads_status: ads-designer for building/refining/debugging UI; ads-
|
|
195
|
+
{ 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 })) }) },
|
|
189
196
|
{ 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 }) => {
|
|
190
197
|
const skill = skills.skills.find((entry) => entry.frontmatter.name === name);
|
|
191
198
|
if (!skill) throw new Error("Unknown ADS skill name.");
|
|
@@ -214,19 +221,36 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
214
221
|
}
|
|
215
222
|
return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
|
|
216
223
|
} },
|
|
217
|
-
{ name: "
|
|
224
|
+
{ name: "ads_behaviors", description: "Discover owner-local behavioral guarantees for public hooks/APIs or private controllers, their scenario demonstrations, registered checks/profiles and fresh or stale execution evidence. Optional exercises links behavioral APIs to scenarios; covers remains rendered UI. Metadata and reachable calls do not prove a guarantee passed. Use ads_verify for selected registered checks.", schema: query.extend({ unit: z.string().max(256).optional(), api: z.string().max(200).optional() }), call: ({ query: search = "", limit, unit, api }) => {
|
|
225
|
+
const inventory = behaviorStatus(options);
|
|
226
|
+
const matches = inventory.contracts.filter(item => (!unit || item.unit === unit) && (!api || Object.values(item.subjects).some(subject => subject.api === api)) && JSON.stringify(item).toLowerCase().includes(search.toLowerCase()));
|
|
227
|
+
return json({ total: matches.length, contracts: matches.slice(0, limit).map(item => {
|
|
228
|
+
if (unit) return item;
|
|
229
|
+
const apis = Object.values(item.subjects).filter(subject => subject.api).map(subject => subject.api);
|
|
230
|
+
const demonstrations = [...new Set(Object.values(item.demonstrations).flat())];
|
|
231
|
+
return { unit: item.unit, file: item.file, apis: apis.slice(0, 20), apiCount: apis.length,
|
|
232
|
+
guarantees: item.guarantees.slice(0, 5).map(guarantee => ({ id: guarantee.id, result: guarantee.result })), guaranteeCount: item.guarantees.length,
|
|
233
|
+
demonstrations: demonstrations.slice(0, 10), demonstrationCount: demonstrations.length, checkCount: Object.keys(item.checks).length };
|
|
234
|
+
}), diagnostics: inventory.diagnostics.slice(0, 50), resourceUri: BEHAVIOR_URI });
|
|
235
|
+
} },
|
|
236
|
+
{ 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. 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) => {
|
|
237
|
+
const result = await verification.run(args, signal);
|
|
238
|
+
await refresh();
|
|
239
|
+
return { ...json(result), ...(!result.ok ? { isError: true } : {}) };
|
|
240
|
+
} },
|
|
241
|
+
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation; 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 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 }) => {
|
|
218
242
|
const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
|
|
219
243
|
return json({ total: matches.length, units: matches.slice(0, limit).map(item => ({ id: item.id, kind: item.kind, fileCount: item.files.length, ...(unit ? { files: item.files.slice(0, 200), filesTruncated: item.files.length > 200, ...(item.companionFiles ? { companionFiles: item.companionFiles } : {}) } : {}) })) });
|
|
220
244
|
} },
|
|
221
245
|
{ name: "ads_read_file", description: "Read a bounded range of ADS or registered configuration/extension source. Returns unit, full-file digest, startLine and nextOffset; default/max is 12000 UTF-16 characters. Binary assets return byte count/digest and no encoded content. Paths come from ads_units or discovery. Missing new paths return digest null. Read only relevant context before preparing a Git patch.", schema: z.object({ path: z.string().min(1).max(500), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(mutationLimits.readChars).default(mutationLimits.readChars) }).strict(), call: ({ path: file, ...range }) => json(edits.read(file, range)) },
|
|
222
|
-
...(mutations ? [{ 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
|
|
246
|
+
...(mutations ? [{ 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. Fix rejected patches and inspect current previews after acceptance.",
|
|
223
247
|
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() }).strict().refine(args => (args.patch !== undefined) !== (args.patchFile !== undefined), "Provide exactly one of patch or patchFile."),
|
|
224
248
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, _meta: { ui: { visibility: ["model"] } },
|
|
225
249
|
call: async (args, signal) => {
|
|
226
250
|
const result = await edits.apply(args, signal);
|
|
227
251
|
if (result.applied) {
|
|
228
252
|
try {
|
|
229
|
-
if (result.unit === "configuration") await previewWatch?.reconfigure?.(loadConfig(root).config.mcp, true);
|
|
253
|
+
if (result.changes.some(change => change.path === "ds.config.json") || result.unit === "configuration") await previewWatch?.reconfigure?.(loadConfig(root).config.mcp, true);
|
|
230
254
|
await refresh(true);
|
|
231
255
|
}
|
|
232
256
|
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.` }]; }
|
|
@@ -295,7 +319,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
295
319
|
try { const stat = statSync(file); return `${file}:${stat.mtimeMs}:${stat.size}`; } catch { return `${file}:missing`; }
|
|
296
320
|
}).join("\n");
|
|
297
321
|
}
|
|
298
|
-
const fingerprint = () => fileFingerprint([...sourceFiles(options.dsRoot), ...getDocuments().map((doc) => doc.file), path.join(root, "ds.config.json"), path.join(root, "package.json")]);
|
|
322
|
+
const fingerprint = () => fileFingerprint([...sourceFiles(options.dsRoot), ...getDocuments().map((doc) => doc.file), path.join(root, "ds.config.json"), path.join(root, "package.json"), path.join(root, ".ds/behavior-results.json")]);
|
|
299
323
|
const artifactFingerprint = () => fileFingerprint(allApps().map(app => path.resolve(root, app.file)));
|
|
300
324
|
const assetFingerprint = () => resourceAssets().map(asset => asset.uri).sort().join("\n");
|
|
301
325
|
let previous = fingerprint();
|
|
@@ -330,7 +354,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
330
354
|
previousAssets = assetFingerprint();
|
|
331
355
|
previousBuild = JSON.stringify(builds());
|
|
332
356
|
const targets = filesChanged ? era === "modern" ? resources().map((entry) => entry.uri).filter((uri) => !skills.files.has(uri)) : [...subscribed]
|
|
333
|
-
: [BUILD_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));
|
|
357
|
+
: [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));
|
|
334
358
|
for (const uri of targets) { if (notifications) notifications.resourceUpdated(uri); else await server.sendResourceUpdated({ uri }); }
|
|
335
359
|
return true;
|
|
336
360
|
}
|
|
@@ -338,7 +362,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
338
362
|
timer?.unref();
|
|
339
363
|
server.oninitialized = () => previewWatch?.start();
|
|
340
364
|
server.onclose = () => { if (timer) clearInterval(timer); };
|
|
341
|
-
return { server, refresh, async close() { if (timer) clearInterval(timer); await server.close(); } };
|
|
365
|
+
return { server, refresh, async close() { if (timer) clearInterval(timer); if (!inspection) await verification.close(); await server.close(); } };
|
|
342
366
|
}
|
|
343
367
|
|
|
344
368
|
export async function startAdsMcp(directory = process.cwd(), { stdio = true, showcase = true, port = 0, mutations = false } = {}) {
|
|
@@ -353,24 +377,37 @@ export async function startAdsMcp(directory = process.cwd(), { stdio = true, sho
|
|
|
353
377
|
{ port, observe: notifications => createAdsMcp(directory, { era: "modern", previewWatch, showcaseUrl, inspection, mutations, notifications }) });
|
|
354
378
|
process.stderr.write(`ADS showcase: ${host.url}\n`);
|
|
355
379
|
}
|
|
356
|
-
const
|
|
380
|
+
const transport = stdio ? new StdioServerTransport() : null;
|
|
381
|
+
const handle = stdio ? serveStdio(({ era }) => createAdsMcp(directory, { era, previewWatch, showcaseUrl, inspection, mutations }).server, { transport, legacy: "serve", onerror: (error) => process.stderr.write(`ads mcp: ${error.message}\n`) }) : null;
|
|
357
382
|
let closing;
|
|
358
383
|
function close() {
|
|
359
384
|
return closing ??= (async () => {
|
|
360
385
|
inspection.edits?.cancel();
|
|
361
|
-
|
|
362
|
-
await
|
|
363
|
-
|
|
364
|
-
|
|
386
|
+
// Stop every owned resource even if another close fails or has in-flight RPCs.
|
|
387
|
+
const results = await Promise.allSettled([
|
|
388
|
+
Promise.resolve().then(() => host?.close()),
|
|
389
|
+
previewWatch.stop(),
|
|
390
|
+
Promise.resolve().then(() => inspection.verification?.close()),
|
|
391
|
+
Promise.resolve().then(() => handle?.close()),
|
|
392
|
+
]);
|
|
393
|
+
for (const event of ["end", "close", "error"]) process.stdin.removeListener(event, disconnect);
|
|
365
394
|
for (const signal of ["SIGINT", "SIGTERM"]) process.removeListener(signal, disconnect);
|
|
366
395
|
process.removeListener("exit", forceCleanup);
|
|
396
|
+
const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
397
|
+
if (errors.length) throw new AggregateError(errors, "ADS shutdown failed");
|
|
367
398
|
})();
|
|
368
399
|
}
|
|
369
|
-
function disconnect() { void close(); }
|
|
400
|
+
function disconnect() { void close().catch(error => { process.stderr.write(`ads mcp: ${error.message}\n`); process.exitCode = 1; }); }
|
|
370
401
|
function forceCleanup() { previewWatch.kill(); }
|
|
371
402
|
// Probe instances have no managed child; a real connection activates it.
|
|
372
|
-
//
|
|
373
|
-
|
|
403
|
+
// The SDK also closes its wire on stdout failure. Closing a probe instance
|
|
404
|
+
// must not shut the runtime down, so observe the wire, not server.onclose.
|
|
405
|
+
if (transport) {
|
|
406
|
+
const wireClosed = transport.onclose;
|
|
407
|
+
transport.onclose = () => { try { wireClosed?.(); } finally { disconnect(); } };
|
|
408
|
+
for (const event of ["end", "close", "error"]) process.stdin.once(event, disconnect);
|
|
409
|
+
if (process.stdin.readableEnded || process.stdin.destroyed) disconnect();
|
|
410
|
+
}
|
|
374
411
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, disconnect);
|
|
375
412
|
process.once("exit", forceCleanup);
|
|
376
413
|
return { close, url: host?.url };
|
|
@@ -56,13 +56,17 @@ export async function startShowcaseHost(factory, { port = 0, observe } = {}) {
|
|
|
56
56
|
let closing;
|
|
57
57
|
return { url: `http://127.0.0.1:${web.address().port}/`, close() {
|
|
58
58
|
return closing ??= (async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await handler.close();
|
|
62
|
-
await sandbox?.close();
|
|
59
|
+
// Close sockets first: live subscriptions and preview requests must not
|
|
60
|
+
// hold shutdown open while protocol instances wait for cancellation.
|
|
63
61
|
const stopped = new Promise(resolve => web.close(resolve));
|
|
62
|
+
for (const request of requests) request.abort();
|
|
64
63
|
web.closeAllConnections();
|
|
65
|
-
await stopped
|
|
64
|
+
const results = await Promise.allSettled([stopped,
|
|
65
|
+
Promise.resolve().then(() => observer?.close()),
|
|
66
|
+
handler.close(), sandbox?.close(),
|
|
67
|
+
]);
|
|
68
|
+
const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
69
|
+
if (errors.length) throw new AggregateError(errors, "Showcase shutdown failed");
|
|
66
70
|
})();
|
|
67
71
|
} };
|
|
68
72
|
}
|
package/dist/oxlint.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export default {
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
"no-inline-style": {
|
|
69
|
-
meta: { type: "problem", schema, messages: { inline: "
|
|
69
|
+
meta: { type: "problem", schema, messages: { inline: "Own visual styles in component/pattern Linaria. Put demo-only sizing and framing in a public review/layout pattern. Reserve inlineStyleFiles for documented measured geometry; add component props for deliberate product API needs." } },
|
|
70
70
|
create(context) {
|
|
71
71
|
const { root, config } = options(context);
|
|
72
72
|
const allowed = config.inlineStyleFiles.some((file) => path.resolve(root, file) === path.resolve(context.getFilename()));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { isDevelopmentFile, isInside } from "./config.mjs";
|
|
4
|
+
import { uiOperation } from "./ui-framework.mjs";
|
|
5
|
+
|
|
6
|
+
export function publicApi(program, options) {
|
|
7
|
+
const checker = program.getTypeChecker();
|
|
8
|
+
const entry = program.getSourceFile(path.join(options.dsRoot, options.config.publicEntry));
|
|
9
|
+
const module = entry && checker.getSymbolAtLocation(entry);
|
|
10
|
+
const items = [];
|
|
11
|
+
for (const exported of module ? checker.getExportsOfModule(module) : []) {
|
|
12
|
+
let symbol = exported;
|
|
13
|
+
if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
14
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
15
|
+
if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
16
|
+
const file = declaration.getSourceFile().fileName;
|
|
17
|
+
if (isDevelopmentFile(file, options)) continue;
|
|
18
|
+
const kind = isInside(path.join(options.dsRoot, "components"), file) ? "components" : isInside(path.join(options.dsRoot, "patterns"), file) ? "patterns" : isInside(path.join(options.dsRoot, "controllers"), file) ? "controllers" : null;
|
|
19
|
+
if (!kind) continue;
|
|
20
|
+
let renderable = false;
|
|
21
|
+
function visit(node) {
|
|
22
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) renderable = true;
|
|
23
|
+
if (ts.isCallExpression(node) && ["createElement", "forwardRef", "memo"].includes(uiOperation(node.expression, checker))) renderable = true;
|
|
24
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
25
|
+
let tag = node.tag;
|
|
26
|
+
if (ts.isCallExpression(tag)) tag = tag.expression;
|
|
27
|
+
let base = tag;
|
|
28
|
+
while (ts.isPropertyAccessExpression(base)) base = base.expression;
|
|
29
|
+
const symbol = checker.getSymbolAtLocation(base);
|
|
30
|
+
if (symbol?.declarations?.some(item => ts.isImportSpecifier(item) && (item.propertyName ?? item.name).text === "styled" && item.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/") || ts.isNamespaceImport(item) && item.parent.parent.moduleSpecifier.text.startsWith("@linaria/") && /^\.styled(?:\.|$)/.test(tag.getText().slice(base.getText().length)))) renderable = true;
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(node, visit);
|
|
33
|
+
}
|
|
34
|
+
visit(declaration);
|
|
35
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
36
|
+
items.push({ name: exported.name, kind, file, declaration, renderable: kind !== "controllers" && renderable, behavioral: !!(type.getCallSignatures().length || type.getConstructSignatures().length) });
|
|
37
|
+
}
|
|
38
|
+
return items;
|
|
39
|
+
}
|
|
@@ -126,6 +126,16 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
126
126
|
cache.validate(stamp);
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
+
async function checkedBuild(stamp, build) {
|
|
130
|
+
const started = generation;
|
|
131
|
+
let checking = true, compiling = true;
|
|
132
|
+
function progress() { if (started === generation) reporter.progress(checking && compiling ? "validation-and-compilation" : checking ? "validation" : compiling ? "compilation" : "publication"); }
|
|
133
|
+
progress();
|
|
134
|
+
return Promise.allSettled([
|
|
135
|
+
gate(stamp).finally(() => { checking = false; progress(); }),
|
|
136
|
+
Promise.resolve().then(build).finally(() => { compiling = false; progress(); }),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
129
139
|
async function compile(entry) {
|
|
130
140
|
if (buildGraph) return compileGraph();
|
|
131
141
|
const revision = reporter.begin(entry.id);
|
|
@@ -154,16 +164,18 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
154
164
|
if (path.isAbsolute(file) && existsSync(file)) dependencies.add(file);
|
|
155
165
|
}
|
|
156
166
|
} };
|
|
157
|
-
const outcomes = await
|
|
167
|
+
const outcomes = await checkedBuild(stamp, () => reuse ? undefined : build(entry, options, track));
|
|
158
168
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
159
169
|
if (failure) throw failure.reason;
|
|
160
170
|
cache.assertCompiler();
|
|
161
171
|
if (stopped || started !== generation || stamp !== cache.inputs()) return; // Watch queued a newer revision.
|
|
162
172
|
if (!reuse) cache.record(entry, dependencies);
|
|
163
173
|
reporter.ready(entry.id, revision);
|
|
174
|
+
reporter.progress("idle");
|
|
164
175
|
} catch (error) {
|
|
165
176
|
if (started !== generation) return;
|
|
166
177
|
reporter.error(entry.id, revision, error);
|
|
178
|
+
reporter.progress("idle");
|
|
167
179
|
console.error(error);
|
|
168
180
|
if (!watched) process.exitCode = 1;
|
|
169
181
|
}
|
|
@@ -192,7 +204,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
192
204
|
cache.assertCompiler();
|
|
193
205
|
let reusable = entries.every(entry => cache.reusable(entry));
|
|
194
206
|
if (reusable) { try { reusable = Boolean(readPreviewManifest(options, { verify: true })); } catch { reusable = false; } }
|
|
195
|
-
const outcomes = await
|
|
207
|
+
const outcomes = await checkedBuild(stamp, () => reusable ? null : buildGraph(entries, options, track));
|
|
196
208
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
197
209
|
if (failure) throw failure.reason;
|
|
198
210
|
cache.assertCompiler();
|
|
@@ -209,9 +221,11 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
209
221
|
}
|
|
210
222
|
for (const entry of entries) { pending.delete(entry.id); reporter.ready(entry.id, revisions.get(entry.id)); }
|
|
211
223
|
publishedStamp = stamp;
|
|
224
|
+
reporter.progress("idle");
|
|
212
225
|
} catch (error) {
|
|
213
226
|
if (started !== generation) return;
|
|
214
227
|
for (const entry of entries) { pending.delete(entry.id); reporter.error(entry.id, revisions.get(entry.id), error); }
|
|
228
|
+
reporter.progress("idle");
|
|
215
229
|
console.error(error);
|
|
216
230
|
if (!watched) process.exitCode = 1;
|
|
217
231
|
}
|
|
@@ -261,6 +275,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
261
275
|
clearTimeout(timer);
|
|
262
276
|
timer = setTimeout(() => {
|
|
263
277
|
++generation;
|
|
278
|
+
reporter.progress("queued");
|
|
264
279
|
validation = undefined;
|
|
265
280
|
const changes = [...changed]; changed.clear();
|
|
266
281
|
try {
|
package/dist/scenarios.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import { publicApi } from "./public-api.mjs";
|
|
4
5
|
import { uiOperation } from "./ui-framework.mjs";
|
|
5
6
|
import { isDevelopmentFile, isInside, isScenarioFile, sourceFiles } from "./config.mjs";
|
|
6
7
|
import { projectCompilerOptions } from "./boundaries.mjs";
|
|
@@ -27,46 +28,17 @@ function literal(node, checker, seen = new Set()) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function publicUi(program, options) {
|
|
30
|
-
|
|
31
|
-
const entry = program.getSourceFile(path.join(options.dsRoot, options.config.publicEntry));
|
|
32
|
-
const module = entry && checker.getSymbolAtLocation(entry);
|
|
33
|
-
const items = [];
|
|
34
|
-
for (const exported of module ? checker.getExportsOfModule(module) : []) {
|
|
35
|
-
let symbol = exported;
|
|
36
|
-
if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
37
|
-
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
38
|
-
if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
39
|
-
const file = declaration.getSourceFile().fileName;
|
|
40
|
-
if (isDevelopmentFile(file, options)) continue;
|
|
41
|
-
const kind = isInside(path.join(options.dsRoot, "components"), file) ? "components" : isInside(path.join(options.dsRoot, "patterns"), file) ? "patterns" : null;
|
|
42
|
-
if (!kind) continue;
|
|
43
|
-
let renderable = false;
|
|
44
|
-
function visit(node) {
|
|
45
|
-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) renderable = true;
|
|
46
|
-
if (ts.isCallExpression(node) && ["createElement", "forwardRef", "memo"].includes(uiOperation(node.expression, checker))) renderable = true;
|
|
47
|
-
if (ts.isTaggedTemplateExpression(node)) {
|
|
48
|
-
let tag = node.tag;
|
|
49
|
-
if (ts.isCallExpression(tag)) tag = tag.expression;
|
|
50
|
-
let base = tag;
|
|
51
|
-
while (ts.isPropertyAccessExpression(base)) base = base.expression;
|
|
52
|
-
const symbol = checker.getSymbolAtLocation(base);
|
|
53
|
-
if (symbol?.declarations?.some(item => ts.isImportSpecifier(item) && (item.propertyName ?? item.name).text === "styled" && item.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/") || ts.isNamespaceImport(item) && item.parent.parent.moduleSpecifier.text.startsWith("@linaria/") && /^\.styled(?:\.|$)/.test(tag.getText().slice(base.getText().length)))) renderable = true;
|
|
54
|
-
}
|
|
55
|
-
ts.forEachChild(node, visit);
|
|
56
|
-
}
|
|
57
|
-
visit(declaration);
|
|
58
|
-
items.push({ name: exported.name, kind, file, declaration, renderable });
|
|
59
|
-
}
|
|
60
|
-
return items;
|
|
31
|
+
return publicApi(program, options).filter(item => item.kind !== "controllers");
|
|
61
32
|
}
|
|
62
33
|
|
|
63
34
|
/** Discover metadata statically. The MCP process never imports scenario modules. */
|
|
64
35
|
export function inspectScenarios(options, suppliedProgram) {
|
|
65
36
|
const { root, dsRoot } = options;
|
|
66
37
|
const files = sourceFiles(dsRoot).filter(isScenarioFile);
|
|
67
|
-
const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], projectCompilerOptions(options));
|
|
38
|
+
const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], { ...projectCompilerOptions(options), allowJs: true });
|
|
68
39
|
const known = new Map(publicUi(program, options).map((item) => [item.name, item]));
|
|
69
40
|
const checker = program.getTypeChecker();
|
|
41
|
+
const behaviorApis = new Map(publicApi(program, options).filter(item => !item.renderable && item.behavioral).map(item => [item.name, item]));
|
|
70
42
|
const tokenNames = new Set(inspectTokens(root).tokens.map(token => token.name));
|
|
71
43
|
const scenarios = [];
|
|
72
44
|
const diagnostics = [];
|
|
@@ -105,21 +77,22 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
105
77
|
}
|
|
106
78
|
const fixtureBase = path.basename(file).startsWith("Scenario.") ? path.join(path.dirname(file), "fixture") : file.replace(/\.scenario\.[^.]+$/, ".fixture");
|
|
107
79
|
if (![".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", ".json"].some(extension => existsSync(fixtureBase + extension))) report(file, "Keep this scenario's inputs beside it: fixture.ts (directory entry) or <Owner>.fixture.ts (flat entry). An empty fixture is valid for a component without inputs.");
|
|
108
|
-
if (!metadata || Object.keys(metadata).some((key) => !["title", "covers", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
|
|
80
|
+
if (!metadata || Object.keys(metadata).some((key) => !["title", "covers", "exercises", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
|
|
81
|
+
|| Object.hasOwn(metadata, "exercises") && (!Array.isArray(metadata.exercises) || metadata.exercises.some(name => typeof name !== "string" || !behaviorApis.has(name)))
|
|
109
82
|
|| Object.hasOwn(metadata, "tokens") && (!Array.isArray(metadata.tokens) || metadata.tokens.some(name => typeof name !== "string" || !name.startsWith("--")))
|
|
110
83
|
|| kind === "tokens" && (Object.hasOwn(metadata, "tokens") || !validTokenSamples(metadata.samples) || !metadata.covers.some(name => known.get(name)?.kind === "patterns"))
|
|
111
84
|
|| Object.hasOwn(metadata, "samples") && !validTokenSamples(metadata.samples)) {
|
|
112
|
-
report(file, "Export static metadata with nonempty covers. Token scenarios must render a project-owned public review pattern and declare samples: fixture.samples, a literal list of { token, property, probe } CSS samples. A plain value list or covers: [] is incomplete."); continue;
|
|
85
|
+
report(file, "Export static metadata with nonempty covers naming rendered public UI. Optional exercises names public behavioral APIs, not rendered UI. Token scenarios must render a project-owned public review pattern and declare samples: fixture.samples, a literal list of { token, property, probe } CSS samples. A plain value list or covers: [] is incomplete."); continue;
|
|
113
86
|
}
|
|
114
87
|
if (!["composition", "tokens"].includes(kind) && metadata.covers.some((name) => known.get(name).kind !== kind)) report(file, "Component scenarios cover primitives; pattern scenarios cover patterns. Use composition/ for larger interfaces.");
|
|
115
88
|
const tokens = [...new Set([...(metadata.tokens ?? []), ...(metadata.samples ?? []).map(sample => sample.token)])];
|
|
116
89
|
for (const name of tokens) if (!tokenNames.has(name)) diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule: "token-scenario-reference", line: 1, column: 1, message: `Scenario references undeclared token ${name}. Use a real foundation token.` });
|
|
117
|
-
scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
|
|
90
|
+
scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(metadata.exercises ? { exercises: [...new Set(metadata.exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
|
|
118
91
|
}
|
|
119
92
|
return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
|
|
120
93
|
}
|
|
121
94
|
|
|
122
|
-
export function inspectScenarioCoverage(options, program, report) {
|
|
95
|
+
export function inspectScenarioCoverage(options, program, report, advise = () => {}) {
|
|
123
96
|
const registry = inspectScenarios(options, program);
|
|
124
97
|
for (const item of registry.diagnostics) report(path.join(options.root, item.file), item.rule, item.message, item.line, item.column);
|
|
125
98
|
const checker = program.getTypeChecker();
|
|
@@ -153,7 +126,7 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
153
126
|
return symbol.declarations?.length ? symbol : null;
|
|
154
127
|
}
|
|
155
128
|
const identities = new Map();
|
|
156
|
-
for (const item of
|
|
129
|
+
for (const item of publicApi(program, options)) {
|
|
157
130
|
if (!identities.has(item.declaration)) identities.set(item.declaration, new Set());
|
|
158
131
|
identities.get(item.declaration).add(item.name);
|
|
159
132
|
}
|
|
@@ -165,12 +138,16 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
165
138
|
for (const scenario of registry.scenarios) {
|
|
166
139
|
const source = program.getSourceFile(path.join(options.root, scenario.file));
|
|
167
140
|
const rendered = new Set();
|
|
141
|
+
const exercised = new Set(), referencedApis = new Set();
|
|
168
142
|
const referencedTokens = new Set();
|
|
169
143
|
let samplesPassed = false;
|
|
170
144
|
const visited = new Set();
|
|
171
145
|
function visit(node, reached = false) {
|
|
172
146
|
if (!node || visited.has(node)) return;
|
|
173
|
-
if (!reached &&
|
|
147
|
+
if (!reached && ts.isFunctionDeclaration(node)) return;
|
|
148
|
+
// Calls/construction in a mounted adapter execute even when only a result
|
|
149
|
+
// property is read later. Unused literal fixture fields still do not count.
|
|
150
|
+
if (!reached && ts.isVariableDeclaration(node) && !(node.initializer && (ts.isCallExpression(node.initializer) || ts.isNewExpression(node.initializer)))) return;
|
|
174
151
|
if (!reached && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && !ts.isCallExpression(node.parent) && !ts.isJsxExpression(node.parent)) return;
|
|
175
152
|
visited.add(node);
|
|
176
153
|
if (ts.isStringLiteralLike(node) && (isSharedToken(node.text) || scenario.tokens.includes(node.text))) referencedTokens.add(node.text);
|
|
@@ -180,18 +157,25 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
180
157
|
if (expression) {
|
|
181
158
|
const symbol = canonical(symbolOf(expression));
|
|
182
159
|
const names = identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]);
|
|
183
|
-
for (const name of names ?? []) rendered.add(name);
|
|
160
|
+
for (const name of names ?? []) { rendered.add(name); exercised.add(name); }
|
|
184
161
|
if (scenario.kind === "tokens" && [...names ?? []].some(name => scenario.covers.includes(name))) {
|
|
185
162
|
const attributes = ts.isCallExpression(node) ? literal(node.arguments[1], checker) : Object.assign({}, ...node.attributes.properties.map(attribute => ts.isJsxSpreadAttribute(attribute) ? literal(attribute.expression, checker) : ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? { [attribute.name.text]: literal(attribute.initializer.expression, checker) } : {}));
|
|
186
163
|
if (JSON.stringify(attributes?.samples) === JSON.stringify(scenario.samples)) samplesPassed = true;
|
|
187
164
|
}
|
|
188
165
|
}
|
|
166
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
167
|
+
let expression = node.expression;
|
|
168
|
+
if (ts.isPropertyAccessExpression(expression) && ["call", "apply"].includes(expression.name.text)) expression = expression.expression;
|
|
169
|
+
const symbol = canonical(symbolOf(expression));
|
|
170
|
+
for (const name of identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]) ?? []) exercised.add(name);
|
|
171
|
+
}
|
|
189
172
|
// Follow local adapters reachable from the default entry. Unused example
|
|
190
173
|
// functions do not satisfy coverage; production implementations are never
|
|
191
174
|
// traversed to manufacture coverage for their internal dependencies.
|
|
192
175
|
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent))) {
|
|
193
176
|
const symbol = canonical(symbolOf(node));
|
|
194
177
|
const declaration = symbol?.valueDeclaration;
|
|
178
|
+
for (const name of identities.get(declaration ?? symbol?.declarations?.[0]) ?? []) referencedApis.add(name);
|
|
195
179
|
if (declaration && isDevelopmentFile(declaration.getSourceFile().fileName, options)) visit(declaration, true);
|
|
196
180
|
}
|
|
197
181
|
ts.forEachChild(node, child => visit(child, reached && (ts.isExportAssignment(node) || ts.isVariableDeclaration(node))));
|
|
@@ -201,6 +185,10 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
201
185
|
if (!rendered.has(name)) report(source.fileName, "scenario-render", `${name} is declared in covers but has no public render reference in this scenario. Render the real API; an import alone does not count.`);
|
|
202
186
|
else covered.add(name);
|
|
203
187
|
}
|
|
188
|
+
for (const name of scenario.exercises ?? []) {
|
|
189
|
+
if (!exercised.has(name) && !referencedApis.has(name)) report(source.fileName, "scenario-exercise", `${name} is declared in exercises but has no reachable behavioral use. Imports and unused adapters do not count.`);
|
|
190
|
+
else if (!exercised.has(name)) advise(source.fileName, "scenario-exercise-review", `${name} is referenced dynamically. Verify its invocation in the registered browser/behavior checks; a reference alone does not prove execution.`);
|
|
191
|
+
}
|
|
204
192
|
for (const name of referencedTokens) if (!declaredTokens.has(name)) report(source.fileName, "token-scenario-reference", `Reachable scenario input references undeclared token ${name}.`);
|
|
205
193
|
if (scenario.kind === "tokens" && !samplesPassed) report(source.fileName, "token-scenario-render", "Pass the declared fixture.samples into a rendered public review pattern's samples prop. Printing token names or reading unused fixtures is not sample coverage.");
|
|
206
194
|
for (const name of scenario.tokens) {
|
package/dist/skills/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install the skills you need with `npx skills`:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-
|
|
6
|
+
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
For local development, install from this checkout:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-
|
|
12
|
+
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Each skill includes its own instructions and references. `ads-designer` also
|
|
@@ -36,7 +36,7 @@ the browser, and provides a running URL or exact command for review. See the
|
|
|
36
36
|
|-------|-----|---------|
|
|
37
37
|
| [ads-designer](ads-designer/SKILL.md) | Bootstrap or build UI using the local ADS, with craft/mobile guidance | `$ads-designer build a settings page using this project's ADS` |
|
|
38
38
|
| [ads-import](ads-import/SKILL.md) | Import selected external ADS source, brand artwork, or compatible source token values | `$ads-import /path/to/source-repo component Toast` |
|
|
39
|
-
| [ads-
|
|
39
|
+
| [ads-migrate](ads-migrate/SKILL.md) | Study an external component library and implement a native counterpart | `$ads-migrate https://github.com/emilkowalski/sonner` |
|
|
40
40
|
| [ads-break-ui](ads-break-ui/SKILL.md) | Stress-test real components/patterns; optionally fix demonstrated failures | `$ads-break-ui AppShell +fix` |
|
|
41
41
|
| [ads-motion](ads-motion/SKILL.md) | Implement, review, audit, or profile motion | `$ads-motion review Toast` |
|
|
42
42
|
|