@reqvire-org/ads 0.3.1 → 0.3.3
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/dist/bootstrap-ds/ADS.md +29 -0
- package/dist/bootstrap-ds/design-system/tokens/base.css +4 -1
- package/dist/bootstrap-ds/design-system/tokens/colors.css +2 -2
- package/dist/boundaries.mjs +21 -23
- package/dist/check.mjs +45 -15
- package/dist/cli.mjs +1 -0
- package/dist/contracts/foundation.json +211 -0
- package/dist/foundation.mjs +114 -0
- package/dist/mcp/build-status.mjs +22 -9
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/preview-watch.mjs +35 -12
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/server.mjs +6 -6
- package/dist/oxlint.mjs +1 -1
- package/dist/scenarios-compiler.mjs +86 -43
- package/dist/skills/ads-assimilate/SKILL.md +43 -2
- package/dist/skills/ads-assimilate/references/cli.md +3 -2
- package/dist/skills/ads-assimilate/references/native-implementation.md +16 -0
- package/dist/skills/ads-break-ui/SKILL.md +43 -2
- package/dist/skills/ads-designer/SKILL.md +43 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +29 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +4 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +2 -2
- package/dist/skills/ads-designer/references/cli.md +3 -2
- package/dist/skills/ads-import/SKILL.md +43 -2
- package/dist/skills/ads-import/references/cli.md +3 -2
- package/dist/skills/ads-motion/SKILL.md +43 -2
- package/dist/style-boundaries.mjs +59 -0
- package/dist/units.mjs +37 -1
- package/docs/ADS.md +34 -1
- package/docs/ARCHITECTURE.md +38 -6
- package/docs/CHANGELOG.md +28 -0
- package/docs/GUIDE.md +18 -1
- package/docs/MCP.md +48 -7
- package/package.json +2 -1
|
@@ -8,9 +8,9 @@ import { readPreviewManifest } from "../preview-resources.mjs";
|
|
|
8
8
|
export const BUILD_URI = "ads://project/build";
|
|
9
9
|
export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json");
|
|
10
10
|
const maxBytes = 256 * 1024;
|
|
11
|
-
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional() }).strict();
|
|
11
|
+
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional(), code: z.string().max(100).optional() }).strict();
|
|
12
12
|
const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
|
|
13
|
-
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), previews: z.array(preview), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
13
|
+
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), previews: z.array(preview), diagnostics: z.array(diagnostic).max(20).optional(), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
14
14
|
export const artifactDigest = (file) => {
|
|
15
15
|
if (realpathSync(file) !== file) throw new Error("Preview output cannot resolve through a symlink.");
|
|
16
16
|
if (statSync(file).size > 2 * 1024 * 1024) throw new Error("Preview exceeds 2 MiB.");
|
|
@@ -26,7 +26,7 @@ function normalizeDiagnostic(root, error) {
|
|
|
26
26
|
return { message: String(error?.message ?? error).slice(0, 4000),
|
|
27
27
|
...(file ? { file: (path.isAbsolute(file) ? path.relative(root, file) : file).replaceAll("\\", "/").slice(0, 1000) } : {}),
|
|
28
28
|
...(Number.isInteger(line) && line > 0 ? { line } : {}), ...(Number.isInteger(column) && column > 0 ? { column } : {}),
|
|
29
|
-
...(typeof error?.plugin === "string" ? { plugin: error.plugin.slice(0, 200) } : {}) };
|
|
29
|
+
...(typeof error?.plugin === "string" ? { plugin: error.plugin.slice(0, 200) } : {}), ...(typeof error?.code === "string" ? { code: error.code.slice(0, 100) } : {}) };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** One publisher per project compiler; no dependency on a UI framework. */
|
|
@@ -36,8 +36,9 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
36
36
|
if (!isInside(root, target)) throw new Error("Build status must stay inside the project.");
|
|
37
37
|
const entries = new Map(apps.map((app) => [app.id, { id: app.id, state: "idle", revision: 0, lastSuccessfulRevision: null, artifactDigest: null, diagnostics: [] }]));
|
|
38
38
|
let sequence = 0;
|
|
39
|
+
let diagnostics = [];
|
|
39
40
|
function save() {
|
|
40
|
-
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()] };
|
|
41
|
+
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()], ...(diagnostics.length ? { diagnostics } : {}) };
|
|
41
42
|
let text = JSON.stringify(snapshot.parse(value));
|
|
42
43
|
// A shared graph failure applies to every scenario. Store repeated details
|
|
43
44
|
// once only when necessary; ordinary reports retain inline diagnostics.
|
|
@@ -66,6 +67,17 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
66
67
|
save();
|
|
67
68
|
return {
|
|
68
69
|
session,
|
|
70
|
+
failure(error) {
|
|
71
|
+
diagnostics = (Array.isArray(error) ? error : [error]).slice(0, 20).map(error => normalizeDiagnostic(root, error));
|
|
72
|
+
for (const value of entries.values()) { value.state = "error"; ++value.revision; value.diagnostics = []; }
|
|
73
|
+
save();
|
|
74
|
+
},
|
|
75
|
+
clearFailure() {
|
|
76
|
+
if (!diagnostics.length) return;
|
|
77
|
+
diagnostics = [];
|
|
78
|
+
for (const value of entries.values()) if (value.state === "error" && !value.diagnostics.length) value.state = "idle";
|
|
79
|
+
save();
|
|
80
|
+
},
|
|
69
81
|
setApps(next) {
|
|
70
82
|
const ids = new Set(next.map(app => app.id));
|
|
71
83
|
for (const id of entries.keys()) if (!ids.has(id)) entries.delete(id);
|
|
@@ -104,11 +116,11 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/** Read durable build events; a running process alone never means a fresh build. */
|
|
107
|
-
export function inspectBuildStatus(root, config, { session, processState } = {}) {
|
|
119
|
+
export function inspectBuildStatus(root, config, { session, processState, file = buildStatusFile(root) } = {}) {
|
|
108
120
|
let report;
|
|
109
121
|
let reportError;
|
|
110
|
-
const file = buildStatusFile(root);
|
|
111
122
|
try {
|
|
123
|
+
if (!isInside(root, path.resolve(file))) throw new Error("Build status must stay inside the project.");
|
|
112
124
|
if (existsSync(file)) {
|
|
113
125
|
if (realpathSync(file) !== file || statSync(file).size > maxBytes) throw new Error("Build status must be a real project file under 256 KiB.");
|
|
114
126
|
report = snapshot.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
@@ -125,6 +137,7 @@ export function inspectBuildStatus(root, config, { session, processState } = {})
|
|
|
125
137
|
const value = report?.previews.find((entry) => entry.id === app.id);
|
|
126
138
|
let state = value?.state ?? "unreported";
|
|
127
139
|
let diagnostics = value?.diagnosticSet !== undefined ? report.diagnosticSets[value.diagnosticSet] : value?.diagnostics ?? [];
|
|
140
|
+
if (report?.diagnostics?.length) { state = "error"; diagnostics = [...report.diagnostics, ...diagnostics].slice(0, 20); }
|
|
128
141
|
const available = existsSync(path.resolve(root, app.file));
|
|
129
142
|
if (reportError) { state = "error"; diagnostics = [{ message: reportError }]; }
|
|
130
143
|
if (graphError && app.id.includes("/")) { state = "error"; diagnostics = [{ message: graphError }]; }
|
|
@@ -139,8 +152,8 @@ export function inspectBuildStatus(root, config, { session, processState } = {})
|
|
|
139
152
|
return { id: app.id, state, session: report?.session ?? null, revision: value?.revision ?? null, lastSuccessfulRevision: value?.lastSuccessfulRevision ?? null,
|
|
140
153
|
current, stale: state === "unreported" ? null : !current, available, diagnostics };
|
|
141
154
|
});
|
|
142
|
-
const state = previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
143
|
-
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, resourceUri: BUILD_URI };
|
|
155
|
+
const state = reportError || report?.diagnostics?.length || previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
156
|
+
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
export class PreviewBuildError extends Error {
|
|
@@ -154,7 +167,7 @@ export function summarizeBuild(build) {
|
|
|
154
167
|
return { ...value, diagnostics, diagnosticCount: value.diagnostics.length,
|
|
155
168
|
diagnosticsTruncated: value.diagnostics.length > 3 || value.diagnostics.some((entry) => entry.message.length > 1000) };
|
|
156
169
|
}
|
|
157
|
-
return build.previews ? { ...build, previews: build.previews.map(bounded) } : { ...bounded(build), resourceUri: BUILD_URI };
|
|
170
|
+
return build.previews ? { ...build, ...(build.diagnostics ? bounded(build) : {}), previews: build.previews.map(bounded) } : { ...bounded(build), resourceUri: BUILD_URI };
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
export function buildFailure(build) {
|
package/dist/mcp/mutations.mjs
CHANGED
|
@@ -291,7 +291,7 @@ export function createMutationController(directory) {
|
|
|
291
291
|
}
|
|
292
292
|
const warnings = [];
|
|
293
293
|
if (!dryRun && (unit.startsWith("assets") || unit === "configuration")) { try { await generateBrandAssets(root); } catch (error) { warnings.push({ rule: "mutation-assets", message: `Source was applied; generated brand output needs recovery: ${error.message}` }); } }
|
|
294
|
-
return lastResult = { ok: true, unit, applied: !dryRun, changes: changes.map(change => ({ path: change.path, operation: change.content === null ? "delete" : change.expectedDigest === null ? "create" : "update", digest: change.content === null ? null : digest(Buffer.from(change.content)) })), checks: verified, ...(warnings.length ? { warnings } : {}),
|
|
294
|
+
return lastResult = { ok: true, unit, applied: !dryRun, changes: changes.map(change => ({ path: change.path, operation: change.content === null ? "delete" : change.expectedDigest === null ? "create" : "update", digest: change.content === null ? null : digest(Buffer.from(change.content)) })), checks: verified, ...(result.advisories?.length ? { advisories: result.advisories.slice(0, 50), advisoryCount: result.advisories.length } : {}), ...(warnings.length ? { warnings } : {}),
|
|
295
295
|
preview: "Source validation passed. Inspect the managed preview build and rendered scenario after applying; validation is not visual verification." };
|
|
296
296
|
} catch (error) {
|
|
297
297
|
return lastResult = { ok: false, applied: false, diagnostics: [error.diagnostic ?? { rule: "mutation-error", message: error.message }], checks: verified };
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { statSync, mkdirSync, writeFileSync, renameSync, rmSync, realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { buildFailure,
|
|
5
|
+
import { buildFailure, inspectBuildStatus } from "./build-status.mjs";
|
|
6
6
|
|
|
7
7
|
function stamp(file) {
|
|
8
8
|
try { const stat = statSync(file); return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`; }
|
|
@@ -13,7 +13,7 @@ function stamp(file) {
|
|
|
13
13
|
export function createPreviewWatch(root, config, { log = process.stderr, buildTimeout = 30000 } = {}) {
|
|
14
14
|
config = { ...config, apps: [...config.apps] };
|
|
15
15
|
const command = config.previewWatch;
|
|
16
|
-
|
|
16
|
+
let session = randomUUID();
|
|
17
17
|
let state = command ? "idle" : "disabled";
|
|
18
18
|
let child;
|
|
19
19
|
let error;
|
|
@@ -21,11 +21,14 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
21
21
|
let childClosed = false;
|
|
22
22
|
let killTimer;
|
|
23
23
|
let stopping;
|
|
24
|
+
let restarting;
|
|
25
|
+
let disposed = false;
|
|
24
26
|
const baselines = new Map();
|
|
25
27
|
const ready = new Set();
|
|
26
28
|
const requests = new Set();
|
|
27
29
|
let active;
|
|
28
|
-
|
|
30
|
+
let requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
|
|
31
|
+
let reportFile = path.join(root, ".ds", `build-status-${session}.json`);
|
|
29
32
|
function request(file) {
|
|
30
33
|
if (!command || requests.has(file) && active === file) return;
|
|
31
34
|
requests.add(file);
|
|
@@ -48,7 +51,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
48
51
|
} catch (failure) { if (failure.code !== "ESRCH") throw failure; }
|
|
49
52
|
}
|
|
50
53
|
function start() {
|
|
51
|
-
if (state !== "idle") return;
|
|
54
|
+
if (disposed || state !== "idle") return;
|
|
52
55
|
for (const app of config.apps) {
|
|
53
56
|
const file = path.resolve(root, app.file);
|
|
54
57
|
baselines.set(file, stamp(file));
|
|
@@ -57,7 +60,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
57
60
|
// No shell: configured arguments remain literal. Child stdin cannot consume
|
|
58
61
|
// the MCP channel, and both output streams go exclusively to diagnostics.
|
|
59
62
|
const executable = command.command === "node" ? process.execPath : command.command;
|
|
60
|
-
const env = { ...process.env, ADS_MCP_BUILD_SESSION: session, ADS_MCP_BUILD_STATUS:
|
|
63
|
+
const env = { ...process.env, ADS_MCP_BUILD_SESSION: session, ADS_MCP_BUILD_STATUS: reportFile, ADS_MCP_REQUEST_FILE: requestFile, PATH: [path.join(root, "node_modules/.bin"), path.dirname(process.execPath), process.env.PATH ?? ""].join(path.delimiter) };
|
|
61
64
|
try {
|
|
62
65
|
child = spawn(executable, command.args, { cwd: root, env, shell: false, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
63
66
|
} catch (failure) {
|
|
@@ -76,7 +79,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
76
79
|
clearTimeout(killTimer);
|
|
77
80
|
if (state !== "stopping") {
|
|
78
81
|
state = "failed";
|
|
79
|
-
error ??= `Preview watcher exited (${signal ?? code}).
|
|
82
|
+
error ??= `Preview watcher exited (${signal ?? code}). Fix the builder and invoke the preview again to restart it; MCP inspection remains available.`;
|
|
80
83
|
log.write(`ads mcp: ${error}\n`);
|
|
81
84
|
} else state = "stopped";
|
|
82
85
|
resolve();
|
|
@@ -91,16 +94,36 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
91
94
|
child.once("error", (failure) => { error = `Cannot start preview watcher: ${failure.message}`; });
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
function restart() {
|
|
98
|
+
return restarting ??= (async () => {
|
|
99
|
+
await stopChild();
|
|
100
|
+
if (disposed) return;
|
|
101
|
+
session = randomUUID();
|
|
102
|
+
requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
|
|
103
|
+
reportFile = path.join(root, ".ds", `build-status-${session}.json`);
|
|
104
|
+
state = "idle"; error = undefined; child = closed = stopping = undefined; childClosed = false;
|
|
105
|
+
ready.clear(); baselines.clear(); active = undefined;
|
|
106
|
+
start();
|
|
107
|
+
})().finally(() => { restarting = undefined; });
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
async function ensure(file, signal) {
|
|
111
|
+
if (signal?.aborted) throw new Error("Preview request cancelled.");
|
|
112
|
+
if (state === "failed" || restarting) await restart();
|
|
113
|
+
if (disposed) throw new Error("Preview watcher is stopped.");
|
|
95
114
|
start();
|
|
96
115
|
request(file);
|
|
97
116
|
if (signal?.aborted) throw new Error("Preview request cancelled.");
|
|
98
117
|
const app = config.apps.find((entry) => path.resolve(root, entry.file) === file);
|
|
99
|
-
|
|
118
|
+
let baseline = baselines.get(file) ?? null;
|
|
100
119
|
const deadline = Date.now() + buildTimeout;
|
|
120
|
+
let restarted = false;
|
|
101
121
|
while (Date.now() < deadline) {
|
|
102
122
|
if (signal?.aborted) throw new Error("Preview request cancelled.");
|
|
103
123
|
const build = buildStatus().previews.find((entry) => entry.id === app?.id);
|
|
124
|
+
if (!restarted && build?.diagnostics.some(item => item.code === "ADS_PREVIEW_RESTART")) {
|
|
125
|
+
restarted = true; await restart(); request(file); baseline = baselines.get(file) ?? null; continue;
|
|
126
|
+
}
|
|
104
127
|
if (build?.state === "error") throw buildFailure(build);
|
|
105
128
|
if (command && state !== "running") throw new Error(error ?? "Preview watcher is stopped.");
|
|
106
129
|
if (build?.current) return build;
|
|
@@ -113,23 +136,23 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
|
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
const status = () => ({ state, ...(child?.pid ? { pid: child.pid } : {}), ...(error ? { error } : {}) });
|
|
116
|
-
const buildStatus = () => inspectBuildStatus(root, config, command ? { session, processState: status() } : {});
|
|
139
|
+
const buildStatus = () => inspectBuildStatus(root, config, command ? { session, file: reportFile, processState: status() } : {});
|
|
117
140
|
|
|
118
|
-
async function
|
|
141
|
+
async function stopChild() {
|
|
119
142
|
rmSync(requestFile, { force: true });
|
|
120
143
|
if (stopping) return stopping;
|
|
121
|
-
if (!child || childClosed) { if (command) state = "stopped"; return; }
|
|
144
|
+
if (!child || childClosed) { if (command) state = "stopped"; rmSync(reportFile, { force: true }); return; }
|
|
122
145
|
if (state === "stopped") return;
|
|
123
146
|
state = "stopping";
|
|
124
147
|
stopping = (async () => {
|
|
125
148
|
kill("SIGTERM");
|
|
126
149
|
clearTimeout(killTimer);
|
|
127
150
|
const timer = setTimeout(() => kill("SIGKILL"), 1500);
|
|
128
|
-
try { await closed; } finally { clearTimeout(timer); }
|
|
151
|
+
try { await closed; } finally { clearTimeout(timer); rmSync(reportFile, { force: true }); }
|
|
129
152
|
})();
|
|
130
153
|
return stopping;
|
|
131
154
|
}
|
|
132
|
-
return { start, ensure, stop, kill: () => kill("SIGKILL"), status, buildStatus, setApps(apps) {
|
|
155
|
+
return { start, ensure, async stop() { disposed = true; await restarting; return stopChild(); }, kill: () => kill("SIGKILL"), status, buildStatus, setApps(apps) {
|
|
133
156
|
for (const app of apps) {
|
|
134
157
|
const file = path.resolve(root, app.file);
|
|
135
158
|
if (!baselines.has(file)) baselines.set(file, stamp(file));
|
package/dist/mcp/prompts.mjs
CHANGED
|
@@ -2,7 +2,7 @@ export const reasoning = `ADS is development tooling. The project owns its compo
|
|
|
2
2
|
Start with ads_status and use its projectRoot, workflow and project commands; the MCP workspace may be nested inside the client repository. If ADS tools are not exposed, read ads://project/guide then ads://project/status. Find existing public UI with ads_catalog and load one relevant ADS workflow via ads_skills/ads_read_skill. UI implementation/refinement uses ads-designer; library adaptation ads-assimilate; ADS transfer ads-import; motion ads-motion; stress testing ads-break-ui. Domain-model workflows are separate. Inspect the relevant public API, local guide, token definitions and real showcase fixture before proposing changes. Keep source/instruction discovery scoped to the target project, ancestry and selected external sources; broad scans of /mnt or home are not needed to discover this ADS.
|
|
3
3
|
When ads_status.mutations.enabled, discover one owner with ads_units, read relevant ranges with ads_read_file and send only a Git unified diff through ads_apply_patch({unit,patch,dryRun}). A unit includes its own scenarios/fixtures; imported peers remain separate. tokens is the token foundation unit. Only that owner's public re-exports may change in the shared entry; preserve unrelated statements including export {};. Token manifests list companionFiles: include required foundation import changes only for canonical token files created/deleted in the same patch, preserving other imports/comments and canonical order. Use dryRun to review validation. Rejected patches leave source unchanged; fix diagnostics rather than bypassing checks with direct writes. Shared globals use foundation; guides use documentation; assets use discovered assets/* units. Intentional configuration, registered project guards/builders and rule exceptions use configuration, which validates the proposed settings and refreshes its compiler. Keep mandatory ADS gates. For large/binary diffs, pass patchFile pointing to .ds/patches/<name>.patch rather than loading bytes into context; Git binary patches require installed Git. When editing is disabled, use local editing tools.
|
|
4
4
|
Reason through Data (design-token values, semantic roles and relationships), UX (structure, behavior, accessibility, states and layout), and Skin (token-backed identity and visual states). Business data is a separate application-owned input, supplied through typed props or injected callbacks/services. The project uses Linaria extraction for owned styles; MCP adds no production runtime.
|
|
5
|
-
Choose an existing public component or pattern first. Keep application data/routing/effects in the application and fixture data/state beside its scenario. Change presentation in its owning ADS implementation, then demonstrate the same public API in the showcase. Do not create a widget-only replica or override component internals.
|
|
5
|
+
Choose an existing public component or pattern first. Keep application data/routing/effects in the application and fixture data/state beside its scenario. Change presentation in its owning ADS implementation, then demonstrate the same public API in the showcase. Put demo-only sizing and framing in a public review/layout pattern; add component props for deliberate runtime API needs and preserve required behavior. Do not create a widget-only replica or override component internals.
|
|
6
6
|
For token source inspection, call ads_tokens with a focused query. For token samples, components, patterns and composition, use ads_scenarios to select a path-derived id and invoke ads_show_scenario. Showcase and MCP mount that exact entry and its colocated fixture; no page initialization is required. The showcase is a minimal tool-owned shell plus an MCP client and Apps host. Its menus come from tools; it invokes the same entry tools and refreshes the active scenario through notifications. The reusable App loads framework runtime, foundation CSS, fonts and selected modules via MCP resources inside its sandbox. Keep project execution and styles out of the outer host; preserve mount/unmount cleanup and lazy dependencies. Use the existing ads_status showcase URL, or ads showcase when no MCP runtime is connected. Projects supply scenarios/fixtures and the compiler, not browser page files or another shell. Tool metadata selects an MCP App; a resources/read of HTML alone is not proof of rendered UI. If the host has no Apps view, give the configured showcase URL and use text/resources.
|
|
7
7
|
Check ads_status.previewWatch before launching a separate preview builder. A configured watcher is managed by ads mcp. Check ads_status.build or ads://project/build after changes: running does not mean ready, and unreported freshness is not evidence of current source. Reported preview failures include structured diagnostics; fix the owning source and wait for a successful rebuild, then invoke the preview tool again. Notifications may not reach the agent, so read status explicitly. Build revisions identify reported compilation attempts, not proof of runtime/visual correctness. Do not invent replacement UI to bypass a failed preview.
|
|
8
8
|
Skills: use ads_skills to select a workflow and read its SKILL.md with ads_read_skill. Resolve relative supporting files from that skill URI. Reading instructions does not prove native skill activation or authorize unrelated actions.
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
43
43
|
const project = () => { const context = projectContext(options); context.showcase.url = getShowcaseUrl(); return context; };
|
|
44
44
|
const subscribed = new Set();
|
|
45
45
|
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.
|
|
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. 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
47
|
capabilities: { tools: { listChanged: true }, resources: { listChanged: true, subscribe: true }, prompts: {},
|
|
48
48
|
extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [APP_MIME] }, ...(era === "modern" ? { [SKILLS_EXTENSION]: { directoryRead: true } } : {}) } },
|
|
49
49
|
});
|
|
@@ -181,9 +181,9 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
181
181
|
const matches = inventory.tokens.filter((token) => token.name.toLowerCase().includes(search.toLowerCase()));
|
|
182
182
|
return json({ total: matches.length, tokens: matches.slice(0, limit), categories: tokenScenarios(), diagnosticCount: inventory.diagnostics.length, resourceUri: `${PROJECT_URI}/tokens` });
|
|
183
183
|
} },
|
|
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 }) => {
|
|
184
|
+
{ 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
185
|
const result = checkProject(root);
|
|
186
|
-
return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
|
|
186
|
+
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
187
|
} },
|
|
188
188
|
{ name: "ads_skills", description: "Select one ADS workflow after ads_status: ads-designer for building/refining/debugging UI; ads-assimilate 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
189
|
{ 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 }) => {
|
|
@@ -214,19 +214,19 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
|
|
|
214
214
|
}
|
|
215
215
|
return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
|
|
216
216
|
} },
|
|
217
|
-
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; 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.
|
|
217
|
+
{ name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; 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
218
|
const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
|
|
219
219
|
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
220
|
} },
|
|
221
221
|
{ 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
|
|
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. 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
223
|
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
224
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, _meta: { ui: { visibility: ["model"] } },
|
|
225
225
|
call: async (args, signal) => {
|
|
226
226
|
const result = await edits.apply(args, signal);
|
|
227
227
|
if (result.applied) {
|
|
228
228
|
try {
|
|
229
|
-
if (result.unit === "configuration") await previewWatch?.reconfigure?.(loadConfig(root).config.mcp, true);
|
|
229
|
+
if (result.changes.some(change => change.path === "ds.config.json") || result.unit === "configuration") await previewWatch?.reconfigure?.(loadConfig(root).config.mcp, true);
|
|
230
230
|
await refresh(true);
|
|
231
231
|
}
|
|
232
232
|
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.` }]; }
|
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()));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { loadConfig, isInside } from "./config.mjs";
|
|
5
5
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
@@ -71,7 +71,7 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
71
71
|
}
|
|
72
72
|
return { inputs, reusable, validated: stamp => value.validation === stamp,
|
|
73
73
|
assertCompiler() {
|
|
74
|
-
if (adapterDigest() !== loadedAdapter) throw new Error("Preview compiler code or dependencies changed.
|
|
74
|
+
if (adapterDigest() !== loadedAdapter) throw Object.assign(new Error("Preview compiler code or dependencies changed. Invoke the preview again to restart the builder and load current code."), { code: "ADS_PREVIEW_RESTART" });
|
|
75
75
|
},
|
|
76
76
|
validate(stamp) { value.validation = stamp; save(); },
|
|
77
77
|
record(entry, dependencies) {
|
|
@@ -93,12 +93,17 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
93
93
|
function registry() {
|
|
94
94
|
options = loadConfig(root);
|
|
95
95
|
const result = inspectScenarios(options);
|
|
96
|
-
if (result.diagnostics.length) throw new Error(result.diagnostics.map(item => `${item.file}: ${item.message}`).join("\n"));
|
|
96
|
+
if (result.diagnostics.length) throw Object.assign(new Error(result.diagnostics.map(item => `${item.file}: ${item.message}`).join("\n")), { scenarios: result.scenarios });
|
|
97
97
|
mkdirSync(path.join(root, ".ds/scenarios"), { recursive: true });
|
|
98
98
|
writeFileSync(path.join(root, ".ds/scenarios/manifest.json"), JSON.stringify({ schemaVersion: 1, scenarios: result.scenarios }, null, 2));
|
|
99
99
|
return result.scenarios;
|
|
100
100
|
}
|
|
101
|
-
let entries =
|
|
101
|
+
let entries = [], registryFailure;
|
|
102
|
+
try { entries = registry(); }
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!watched) throw error;
|
|
105
|
+
registryFailure = error; entries = error.scenarios ?? [];
|
|
106
|
+
}
|
|
102
107
|
const reporter = createBuildReporter(root, { apps: entries.map(entry => ({ id: entry.id, file: entry.artifact })) });
|
|
103
108
|
const requested = new Set();
|
|
104
109
|
const pending = new Set();
|
|
@@ -108,6 +113,12 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
108
113
|
let generation = 0;
|
|
109
114
|
let publishedStamp;
|
|
110
115
|
let validation;
|
|
116
|
+
function failure(error) {
|
|
117
|
+
try { reporter.failure(error); } catch (reportError) { console.error(reportError); }
|
|
118
|
+
console.error(error);
|
|
119
|
+
if (!watched) process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
if (registryFailure) failure(registryFailure);
|
|
111
122
|
function gate(stamp) {
|
|
112
123
|
if (cache.validated(stamp)) return Promise.resolve();
|
|
113
124
|
return validation ??= Promise.resolve().then(validate).then(() => {
|
|
@@ -119,8 +130,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
119
130
|
if (buildGraph) return compileGraph();
|
|
120
131
|
const revision = reporter.begin(entry.id);
|
|
121
132
|
const started = generation;
|
|
122
|
-
const stamp = cache.inputs();
|
|
123
133
|
try {
|
|
134
|
+
const stamp = cache.inputs();
|
|
124
135
|
cache.assertCompiler();
|
|
125
136
|
const reuse = cache.reusable(entry);
|
|
126
137
|
const dependencies = new Set();
|
|
@@ -147,7 +158,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
147
158
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
148
159
|
if (failure) throw failure.reason;
|
|
149
160
|
cache.assertCompiler();
|
|
150
|
-
if (started !== generation || stamp !== cache.inputs()) return; // Watch queued a newer revision.
|
|
161
|
+
if (stopped || started !== generation || stamp !== cache.inputs()) return; // Watch queued a newer revision.
|
|
151
162
|
if (!reuse) cache.record(entry, dependencies);
|
|
152
163
|
reporter.ready(entry.id, revision);
|
|
153
164
|
} catch (error) {
|
|
@@ -160,24 +171,24 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
160
171
|
async function compileGraph() {
|
|
161
172
|
const revisions = new Map(entries.map(entry => [entry.id, reporter.begin(entry.id)]));
|
|
162
173
|
const started = generation;
|
|
163
|
-
const stamp = cache.inputs();
|
|
164
|
-
const dependencies = new Set();
|
|
165
|
-
function assets(directory) {
|
|
166
|
-
for (const item of readdirSync(directory, { withFileTypes: true })) {
|
|
167
|
-
if (item.isSymbolicLink() || ["node_modules", ".ds", ".vite", "dist"].includes(item.name)) continue;
|
|
168
|
-
const file = path.join(directory, item.name);
|
|
169
|
-
if (item.isDirectory()) assets(file);
|
|
170
|
-
else if (/\.(?:css|svg|png|jpe?g|webp|gif|woff2?|ttf|otf)$/.test(file)) dependencies.add(file);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
assets(options.dsRoot);
|
|
174
|
-
const track = { name: "ads-preview-graph-dependencies", generateBundle() {
|
|
175
|
-
for (const id of this.getModuleIds()) {
|
|
176
|
-
const file = id.split("?")[0];
|
|
177
|
-
if (path.isAbsolute(file) && existsSync(file)) dependencies.add(file);
|
|
178
|
-
}
|
|
179
|
-
} };
|
|
180
174
|
try {
|
|
175
|
+
const stamp = cache.inputs();
|
|
176
|
+
const dependencies = new Set();
|
|
177
|
+
function assets(directory) {
|
|
178
|
+
for (const item of readdirSync(directory, { withFileTypes: true })) {
|
|
179
|
+
if (item.isSymbolicLink() || ["node_modules", ".ds", ".vite", "dist"].includes(item.name)) continue;
|
|
180
|
+
const file = path.join(directory, item.name);
|
|
181
|
+
if (item.isDirectory()) assets(file);
|
|
182
|
+
else if (/\.(?:css|svg|png|jpe?g|webp|gif|woff2?|ttf|otf)$/.test(file)) dependencies.add(file);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
assets(options.dsRoot);
|
|
186
|
+
const track = { name: "ads-preview-graph-dependencies", generateBundle() {
|
|
187
|
+
for (const id of this.getModuleIds()) {
|
|
188
|
+
const file = id.split("?")[0];
|
|
189
|
+
if (path.isAbsolute(file) && existsSync(file)) dependencies.add(file);
|
|
190
|
+
}
|
|
191
|
+
} };
|
|
181
192
|
cache.assertCompiler();
|
|
182
193
|
let reusable = entries.every(entry => cache.reusable(entry));
|
|
183
194
|
if (reusable) { try { reusable = Boolean(readPreviewManifest(options, { verify: true })); } catch { reusable = false; } }
|
|
@@ -185,7 +196,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
185
196
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
186
197
|
if (failure) throw failure.reason;
|
|
187
198
|
cache.assertCompiler();
|
|
188
|
-
if (started !== generation || stamp !== cache.inputs()) return;
|
|
199
|
+
if (stopped || started !== generation || stamp !== cache.inputs()) return;
|
|
189
200
|
const publication = outcomes[1].value;
|
|
190
201
|
if (publication) {
|
|
191
202
|
publication.publish(reporter.session);
|
|
@@ -209,15 +220,16 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
209
220
|
if (compiling) return;
|
|
210
221
|
compiling = true;
|
|
211
222
|
try {
|
|
212
|
-
while (pending.size && !stopped) {
|
|
223
|
+
while (pending.size && !registryFailure && !stopped) {
|
|
213
224
|
const id = pending.has(active) ? active : pending.values().next().value;
|
|
214
225
|
pending.delete(id);
|
|
215
226
|
const entry = entries.find(entry => entry.id === id);
|
|
216
227
|
if (entry) await compile(entry);
|
|
217
228
|
}
|
|
218
|
-
}
|
|
229
|
+
} catch (error) { failure(error); }
|
|
230
|
+
finally { compiling = false; }
|
|
219
231
|
}
|
|
220
|
-
function request() {
|
|
232
|
+
function request(startBuild = true) {
|
|
221
233
|
const file = process.env.ADS_MCP_REQUEST_FILE;
|
|
222
234
|
if (!file || !existsSync(file)) return;
|
|
223
235
|
try {
|
|
@@ -231,8 +243,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
231
243
|
if (!buildGraph || publishedStamp !== cache.inputs()) pending.add(entry.id);
|
|
232
244
|
}
|
|
233
245
|
}
|
|
234
|
-
void drain();
|
|
235
|
-
} catch (error) {
|
|
246
|
+
if (startBuild) void drain();
|
|
247
|
+
} catch (error) { failure(error); }
|
|
236
248
|
}
|
|
237
249
|
if (!watched) {
|
|
238
250
|
for (const entry of entries) pending.add(entry.id);
|
|
@@ -241,8 +253,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
241
253
|
}
|
|
242
254
|
let timer;
|
|
243
255
|
const changed = new Set();
|
|
244
|
-
function sourceChanged(_event, file) {
|
|
245
|
-
if (!file || !/\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) return;
|
|
256
|
+
function sourceChanged(_event, file, topology = false) {
|
|
257
|
+
if (!file || !topology && !/\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) return;
|
|
246
258
|
const absolute = path.join(root, file);
|
|
247
259
|
if (file.split(path.sep).some(name => ["node_modules", ".git", ".ds", ".vite", "coverage"].includes(name)) || options.config.assets.generatedPaths.some(base => isInside(path.resolve(root, base), absolute))) return;
|
|
248
260
|
changed.add(absolute);
|
|
@@ -253,7 +265,10 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
253
265
|
const changes = [...changed]; changed.clear();
|
|
254
266
|
try {
|
|
255
267
|
entries = registry();
|
|
268
|
+
registryFailure = undefined;
|
|
256
269
|
reporter.setApps(entries.map(entry => ({ id: entry.id, file: entry.artifact })));
|
|
270
|
+
reporter.clearFailure();
|
|
271
|
+
request(false);
|
|
257
272
|
for (const id of requested) if (!entries.some(entry => entry.id === id)) { requested.delete(id); pending.delete(id); }
|
|
258
273
|
// Every source change needs validation; unaffected artifacts need no
|
|
259
274
|
// compilation. Check active first, then rebuild only changed dependencies.
|
|
@@ -263,25 +278,53 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
263
278
|
}
|
|
264
279
|
if (changes.length) void drain();
|
|
265
280
|
} catch (error) {
|
|
266
|
-
|
|
267
|
-
console.error(error);
|
|
281
|
+
registryFailure = error; failure(error);
|
|
268
282
|
}
|
|
269
283
|
}, 100);
|
|
270
284
|
}
|
|
271
285
|
// Do not allocate recursive OS watches for node_modules or generated trees.
|
|
272
|
-
const watchers =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
const watchers = new Map();
|
|
287
|
+
function addWatch(directory, callback, recursive = false) {
|
|
288
|
+
if (stopped) return false;
|
|
289
|
+
try {
|
|
290
|
+
const stat = statSync(directory), identity = `${stat.dev}:${stat.ino}`;
|
|
291
|
+
if (watchers.get(directory)?.identity === identity) return false;
|
|
292
|
+
watchers.get(directory)?.watcher.close();
|
|
293
|
+
const watcher = watch(directory, { recursive }, callback);
|
|
294
|
+
watchers.set(directory, { watcher, identity });
|
|
295
|
+
watcher.on("error", error => {
|
|
296
|
+
watcher.close();
|
|
297
|
+
if (watchers.get(directory)?.watcher === watcher) watchers.delete(directory);
|
|
298
|
+
failure(error);
|
|
299
|
+
});
|
|
300
|
+
return true;
|
|
301
|
+
} catch (error) { failure(error); }
|
|
278
302
|
}
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
303
|
+
function reconcileWatches(initial = false) {
|
|
304
|
+
if (stopped) return;
|
|
305
|
+
try {
|
|
306
|
+
addWatch(root, sourceChanged);
|
|
307
|
+
const wanted = new Set([root, path.join(root, ".ds")]);
|
|
308
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
309
|
+
const directory = path.join(root, entry.name);
|
|
310
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || ["node_modules", ".git", ".ds", ".vite", "coverage"].includes(entry.name)
|
|
311
|
+
|| options.config.assets.generatedPaths.some(base => isInside(path.resolve(root, base), directory))) continue;
|
|
312
|
+
wanted.add(directory);
|
|
313
|
+
if (addWatch(directory, (event, file) => sourceChanged(event, file && path.join(entry.name, file)), true) && !initial) sourceChanged("rename", entry.name, true);
|
|
314
|
+
}
|
|
315
|
+
mkdirSync(path.join(root, ".ds"), { recursive: true });
|
|
316
|
+
addWatch(path.join(root, ".ds"), (_event, file) => { if (file === path.basename(process.env.ADS_MCP_REQUEST_FILE ?? "")) request(); });
|
|
317
|
+
for (const [directory, { watcher }] of watchers) if (!wanted.has(directory) || !existsSync(directory)) {
|
|
318
|
+
watcher.close(); watchers.delete(directory);
|
|
319
|
+
if (!initial && directory !== root && directory !== path.join(root, ".ds")) sourceChanged("rename", path.relative(root, directory), true);
|
|
320
|
+
}
|
|
321
|
+
} catch (error) { failure(error); }
|
|
322
|
+
}
|
|
323
|
+
reconcileWatches(true);
|
|
324
|
+
const reconcileTimer = setInterval(reconcileWatches, 1000);
|
|
282
325
|
request();
|
|
283
326
|
console.log(`Scenario compiler ready: ${entries.length} entries; builds are requested on demand.`);
|
|
284
327
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
285
|
-
stopped = true; clearTimeout(timer); for (const watcher of watchers
|
|
328
|
+
stopped = true; clearTimeout(timer); clearInterval(reconcileTimer); for (const { watcher } of watchers.values()) watcher.close();
|
|
286
329
|
});
|
|
287
330
|
}
|
|
@@ -27,7 +27,11 @@ unrelated statements. Its real re-export may accompany removal of a redundant
|
|
|
27
27
|
`export {};` placeholder; do not add empty exports. A `tokens` manifest lists the foundation
|
|
28
28
|
entry in `companionFiles`: when creating/deleting a canonical token file such as
|
|
29
29
|
`fonts.css`, include only its required import change there in the same patch.
|
|
30
|
-
Preserve other imports/comments and the canonical order.
|
|
30
|
+
Preserve other imports/comments and the canonical order. Runtime owner manifests
|
|
31
|
+
also list a `ds.config.json` companion: change only that owner's `inlineStyleFiles`
|
|
32
|
+
and `boundaries.styleWrites` entries together with its source. Unused permissions
|
|
33
|
+
fail the core checks; do not pre-register a boundary without its implementation.
|
|
34
|
+
Use `dryRun: true` to
|
|
31
35
|
inspect validation; applying re-runs the gates. Fix rejected patches in their
|
|
32
36
|
owner. Report unsupported coupled changes instead of bypassing a rejection with
|
|
33
37
|
direct source writes. Shared styles/helpers use `foundation`, root guides use
|
|
@@ -241,6 +245,15 @@ source definitions and values; inspecting definitions does not prove UI parity.
|
|
|
241
245
|
The tool keeps the shared naming contract; sample templates and their token
|
|
242
246
|
lists belong to the project.
|
|
243
247
|
|
|
248
|
+
A public review pattern may be covered by real token or composition scenarios;
|
|
249
|
+
it needs no redundant standalone entry. Remove an optional scenario only while
|
|
250
|
+
retaining reachable render coverage. Keep directory ownership anchored by its
|
|
251
|
+
runtime entry (`index` or a file matching the directory name) or colocated fixture.
|
|
252
|
+
Import colocated image/font fixtures so MCP serves them through asset resources.
|
|
253
|
+
For a broken-image case, use a local failing fixture, rather than an external
|
|
254
|
+
network failure or a domain-specific CSP exception. Production asset APIs remain
|
|
255
|
+
unrestricted by this fixture guidance.
|
|
256
|
+
|
|
244
257
|
Scenarios, colocated fixtures and helpers are development-only. Never export them
|
|
245
258
|
from the runtime barrel or import them in production, including through aliases,
|
|
246
259
|
re-exports or dynamic imports. They may not import application stores, routers,
|
|
@@ -251,6 +264,27 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
|
|
|
251
264
|
foundation CSS rather than assigning shared token values in JavaScript. Keep
|
|
252
265
|
full-app integration harnesses and workflow coverage in application test support.
|
|
253
266
|
|
|
267
|
+
Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
|
|
268
|
+
on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
|
|
269
|
+
three closed roles: root/body typography, canvas and theme defaults; limited native
|
|
270
|
+
normalization; and global focus indicators/reduced-motion rules. Every selector
|
|
271
|
+
branch, property and value must fit its role. Token-backed control skins, layout,
|
|
272
|
+
interactive state rules and owner hooks are rejected even through `:where()`.
|
|
273
|
+
Do not copy an unchecked upstream preflight or turn a component repair into a reset.
|
|
274
|
+
Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
|
|
275
|
+
in a public review/layout pattern; add component props for a deliberate runtime
|
|
276
|
+
API need. Preserve required capabilities while following ownership rules.
|
|
277
|
+
Browser review still verifies the effects.
|
|
278
|
+
|
|
279
|
+
Invalid metadata and source failures keep the managed compiler alive for repairs.
|
|
280
|
+
Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
|
|
281
|
+
successful output. If the builder exited, fix its command and invoke the preview
|
|
282
|
+
again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
|
|
283
|
+
also reload on the next preview invocation. Reconnect the client when upgrading
|
|
284
|
+
the ADS server package itself. Persistent SSE/notification streams make
|
|
285
|
+
`networkidle` unsuitable: wait for the App's current ready/error state instead.
|
|
286
|
+
|
|
287
|
+
|
|
254
288
|
|
|
255
289
|
|
|
256
290
|
Register component/project Oxlint plugins and rules in `ds.config.json`. Use
|
|
@@ -258,7 +292,14 @@ scoped `oxlint.overrides` for justified file-specific settings; never edit
|
|
|
258
292
|
`.ds/oxlintrc.json`. These overrides configure Oxlint, not the independent
|
|
259
293
|
architecture checks in `ads check`. Measured renderer properties and documented
|
|
260
294
|
non-state hooks use explicit `boundaries`; shared token writes and primitive
|
|
261
|
-
state overrides cannot be allowlisted.
|
|
295
|
+
state overrides cannot be allowlisted. When migrating JSX inline geometry to
|
|
296
|
+
imperative writes, port the source and replace its own boundary entries together
|
|
297
|
+
in the owner's patch. Remove obsolete `inlineStyleFiles` entries and unused
|
|
298
|
+
`styleWrites` properties; do not broaden permissions to keep a migration passing.
|
|
299
|
+
Pointer-capture advisories call for review of cancellation, capture loss and
|
|
300
|
+
teardown, including cleanup delegated to hooks. Verify actual cleanup in a browser;
|
|
301
|
+
a locally present handler alone does not prove correctness.
|
|
302
|
+
Add generic architectural protections to
|
|
262
303
|
ADS core and keep domain/component-specific rules registered in project setup.
|
|
263
304
|
|
|
264
305
|
Update meaningful variants, states, content, themes, and container sizes in the
|