@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.
Files changed (35) hide show
  1. package/dist/bootstrap-ds/ADS.md +29 -0
  2. package/dist/bootstrap-ds/design-system/tokens/base.css +4 -1
  3. package/dist/bootstrap-ds/design-system/tokens/colors.css +2 -2
  4. package/dist/boundaries.mjs +21 -23
  5. package/dist/check.mjs +45 -15
  6. package/dist/cli.mjs +1 -0
  7. package/dist/contracts/foundation.json +211 -0
  8. package/dist/foundation.mjs +114 -0
  9. package/dist/mcp/build-status.mjs +22 -9
  10. package/dist/mcp/mutations.mjs +1 -1
  11. package/dist/mcp/preview-watch.mjs +35 -12
  12. package/dist/mcp/prompts.mjs +1 -1
  13. package/dist/mcp/server.mjs +6 -6
  14. package/dist/oxlint.mjs +1 -1
  15. package/dist/scenarios-compiler.mjs +86 -43
  16. package/dist/skills/ads-assimilate/SKILL.md +43 -2
  17. package/dist/skills/ads-assimilate/references/cli.md +3 -2
  18. package/dist/skills/ads-assimilate/references/native-implementation.md +16 -0
  19. package/dist/skills/ads-break-ui/SKILL.md +43 -2
  20. package/dist/skills/ads-designer/SKILL.md +43 -2
  21. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +29 -0
  22. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +4 -1
  23. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +2 -2
  24. package/dist/skills/ads-designer/references/cli.md +3 -2
  25. package/dist/skills/ads-import/SKILL.md +43 -2
  26. package/dist/skills/ads-import/references/cli.md +3 -2
  27. package/dist/skills/ads-motion/SKILL.md +43 -2
  28. package/dist/style-boundaries.mjs +59 -0
  29. package/dist/units.mjs +37 -1
  30. package/docs/ADS.md +34 -1
  31. package/docs/ARCHITECTURE.md +38 -6
  32. package/docs/CHANGELOG.md +28 -0
  33. package/docs/GUIDE.md +18 -1
  34. package/docs/MCP.md +48 -7
  35. 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) {
@@ -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, buildStatusFile, inspectBuildStatus } from "./build-status.mjs";
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
- const session = randomUUID();
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
- const requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
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: buildStatusFile(root), ADS_MCP_REQUEST_FILE: requestFile, PATH: [path.join(root, "node_modules/.bin"), path.dirname(process.execPath), process.env.PATH ?? ""].join(path.delimiter) };
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}). Restart MCP after fixing the configured command.`;
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
- const baseline = baselines.get(file) ?? null;
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 stop() {
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));
@@ -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.
@@ -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. Token manifests list companionFiles for canonical foundation import changes required by token files created/deleted in the same patch. 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."}`,
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. Token manifests include companionFiles with the foundation entry path and allowed import changes. 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 }) => {
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 including export {};. 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. Set dryRun true to validate without writing. Fix rejected patches and inspect current previews after acceptance.",
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: "Move visual policy into the local ADS, or document a narrow exception in inlineStyleFiles." } },
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. Restart ADS MCP/showcase to load the updated compiler before reviewing previews.");
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 = registry();
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
- } finally { compiling = false; }
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) { console.error(`Preview requests: ${error.message}`); }
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
- for (const id of requested) { const revision = reporter.begin(id); reporter.error(id, revision, error); }
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 = [watch(root, sourceChanged)];
273
- for (const entry of readdirSync(root, { withFileTypes: true })) {
274
- const directory = path.join(root, entry.name);
275
- if (!entry.isDirectory() || entry.isSymbolicLink() || ["node_modules", ".git", ".ds", ".vite", "coverage"].includes(entry.name)
276
- || options.config.assets.generatedPaths.some(base => isInside(path.resolve(root, base), directory))) continue;
277
- watchers.push(watch(directory, { recursive: true }, (event, file) => sourceChanged(event, file && path.join(entry.name, file))));
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
- const requestWatch = watch(path.join(root, ".ds"), (_event, file) => {
280
- if (file === path.basename(process.env.ADS_MCP_REQUEST_FILE ?? "")) request();
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) watcher.close(); requestWatch.close();
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. Use `dryRun: true` to
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. Add generic architectural protections to
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