@reqvire-org/ads 0.3.1 → 0.3.2

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.
@@ -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));
@@ -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
  }
@@ -251,6 +251,25 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
251
251
  foundation CSS rather than assigning shared token values in JavaScript. Keep
252
252
  full-app integration harnesses and workflow coverage in application test support.
253
253
 
254
+ Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
255
+ on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
256
+ three closed roles: root/body typography, canvas and theme defaults; limited native
257
+ normalization; and global focus indicators/reduced-motion rules. Every selector
258
+ branch, property and value must fit its role. Token-backed control skins, layout,
259
+ interactive state rules and owner hooks are rejected even through `:where()`.
260
+ Do not copy an unchecked upstream preflight or turn a component repair into a reset.
261
+ Put skin/state/animation in the Linaria owner, and scenario layout variations in
262
+ the public review pattern's typed API. Browser review still verifies the effects.
263
+
264
+ Invalid metadata and source failures keep the managed compiler alive for repairs.
265
+ Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
266
+ successful output. If the builder exited, fix its command and invoke the preview
267
+ again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
268
+ also reload on the next preview invocation. Reconnect the client when upgrading
269
+ the ADS server package itself. Persistent SSE/notification streams make
270
+ `networkidle` unsuitable: wait for the App's current ready/error state instead.
271
+
272
+
254
273
 
255
274
 
256
275
  Register component/project Oxlint plugins and rules in `ds.config.json`. Use
@@ -55,7 +55,7 @@ If no published distribution exists, build and pack the tooling checkout with
55
55
  From inside an existing test project:
56
56
 
57
57
  ```sh
58
- ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.1.tgz"
58
+ ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.2.tgz"
59
59
  npx --yes --package "$ads_tool_package" ads --help
60
60
  npx --yes --package "$ads_tool_package" ads init --framework react --tool-package "$ads_tool_package"
61
61
  npm install
@@ -93,7 +93,8 @@ component rules through the project's plugin adapter and `ds.config.json`, then
93
93
  run its actual build. Use `lint-config` directly for custom pipeline integration
94
94
  or debugging, not as a separate routine setup step.
95
95
 
96
- Use ADS 0.3.1 or later for the shared-resource preview compiler. `mcp` starts stdio
96
+ Use ADS 0.3.2 or later for the closed foundation contract and recoverable
97
+ shared-resource preview compiler. `mcp` starts stdio
97
98
  plus its local website; `showcase` starts the browser-only MCP runtime. Open the existing runtime URL from `ads_status` when
98
99
  connected; otherwise use `npm run dev:showcase` or the installed `ads showcase`.
99
100
  The tool client discovers entries and renders MCP Apps. Init adds the launch
@@ -203,6 +203,25 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
203
203
  foundation CSS rather than assigning shared token values in JavaScript. Keep
204
204
  full-app integration harnesses and workflow coverage in application test support.
205
205
 
206
+ Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
207
+ on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
208
+ three closed roles: root/body typography, canvas and theme defaults; limited native
209
+ normalization; and global focus indicators/reduced-motion rules. Every selector
210
+ branch, property and value must fit its role. Token-backed control skins, layout,
211
+ interactive state rules and owner hooks are rejected even through `:where()`.
212
+ Do not copy an unchecked upstream preflight or turn a component repair into a reset.
213
+ Put skin/state/animation in the Linaria owner, and scenario layout variations in
214
+ the public review pattern's typed API. Browser review still verifies the effects.
215
+
216
+ Invalid metadata and source failures keep the managed compiler alive for repairs.
217
+ Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
218
+ successful output. If the builder exited, fix its command and invoke the preview
219
+ again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
220
+ also reload on the next preview invocation. Reconnect the client when upgrading
221
+ the ADS server package itself. Persistent SSE/notification streams make
222
+ `networkidle` unsuitable: wait for the App's current ready/error state instead.
223
+
224
+
206
225
 
207
226
 
208
227
  Register component/project Oxlint plugins and rules in `ds.config.json`. Use
@@ -341,6 +341,25 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
341
341
  foundation CSS rather than assigning shared token values in JavaScript. Keep
342
342
  full-app integration harnesses and workflow coverage in application test support.
343
343
 
344
+ Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
345
+ on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
346
+ three closed roles: root/body typography, canvas and theme defaults; limited native
347
+ normalization; and global focus indicators/reduced-motion rules. Every selector
348
+ branch, property and value must fit its role. Token-backed control skins, layout,
349
+ interactive state rules and owner hooks are rejected even through `:where()`.
350
+ Do not copy an unchecked upstream preflight or turn a component repair into a reset.
351
+ Put skin/state/animation in the Linaria owner, and scenario layout variations in
352
+ the public review pattern's typed API. Browser review still verifies the effects.
353
+
354
+ Invalid metadata and source failures keep the managed compiler alive for repairs.
355
+ Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
356
+ successful output. If the builder exited, fix its command and invoke the preview
357
+ again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
358
+ also reload on the next preview invocation. Reconnect the client when upgrading
359
+ the ADS server package itself. Persistent SSE/notification streams make
360
+ `networkidle` unsuitable: wait for the App's current ready/error state instead.
361
+
362
+
344
363
 
345
364
 
346
365
  Register component/project Oxlint plugins and rules in `ds.config.json`. Use
@@ -40,6 +40,35 @@ Token names and prefixes are fixed by ADS architecture; foreign brand prefixes
40
40
  are rejected. Shared assignments belong in token files. Components own
41
41
  `--ds-*`, patterns `--ux-*`, and the showcase `--showcase-*`; token files cannot
42
42
  assign those local variables or override component state.
43
+
44
+ Foundation roles are enforced by `foundation-role`, `foundation-selector` and
45
+ `foundation-import`, alongside the regular token checks. Shared definitions
46
+ belong on the document root or explicit root themes; ordinary declarations belong
47
+ in `base.css`, and font-face descriptors in optional `fonts.css`. The entry imports
48
+ canonical sheets; those sheets cannot import another styling layer. Component
49
+ classes, IDs, data/ARIA hooks, contextual selectors, keyframes and scoped UI rules
50
+ are rejected in the foundation, including nested functional selectors. Raw visual
51
+ values and undefined token references are checked in native defaults too; literal
52
+ font-face metadata keeps its separate asset role.
53
+
54
+ `base.css` has three closed roles:
55
+
56
+ - Document defaults: root/body typography, canvas colors and theme behavior,
57
+ global text selection and document scrollbars.
58
+ - Native resets: permitted normalization properties and values, such as zero
59
+ spacing, inherited control typography, list resets and responsive replaced media.
60
+ - Global accessibility: universal `:focus-visible` outline rules, and reduced-motion
61
+ overrides inside `@media (prefers-reduced-motion: reduce)`.
62
+
63
+ The `foundation-role` check validates every selector branch, property and value.
64
+ Native selectors are not a bypass: token-backed button backgrounds, flex/grid
65
+ layout, component-specific focus rules and hover/disabled skins are rejected.
66
+ Shared token definitions belong in value sheets, not `base.css`. No project token
67
+ names are embedded in this policy. A zero-specificity `:where()` wrapper does not
68
+ change ownership. Put component design and animation in Linaria owners, and
69
+ scenario layout variations in typed public review-pattern props. Browser review
70
+ still checks the effects and accessibility of permitted defaults.
71
+
43
72
  Own primitive markup, focus, hover, selected, disabled, loading, and motion in
44
73
  the primitive. Use typed props for variations. Keep reusable structure and skin
45
74
  in colocated Linaria blocks using `css` and `cx` from `@linaria/atomic`.
@@ -1,3 +1,6 @@
1
+ :root { color-scheme: light; }
2
+ :root[data-theme="dark"] { color-scheme: dark; }
3
+
1
4
  *,
2
5
  *::before,
3
6
  *::after {
@@ -5,7 +8,7 @@
5
8
  }
6
9
 
7
10
  body {
8
- margin: var(--space-0);
11
+ margin: 0;
9
12
  background: var(--bg-canvas);
10
13
  color: var(--text-body);
11
14
  font-family: var(--font-sans);
@@ -1,5 +1,5 @@
1
1
  :root {
2
- color-scheme: light;
2
+
3
3
  --bg-canvas: #faf9f6;
4
4
  --bg-surface: #fff;
5
5
  --bg-hover: #f1efea;
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  :root[data-theme="dark"] {
18
- color-scheme: dark;
18
+
19
19
  --bg-canvas: #17191e;
20
20
  --bg-surface: #20232a;
21
21
  --bg-hover: #2c3039;
@@ -55,7 +55,7 @@ If no published distribution exists, build and pack the tooling checkout with
55
55
  From inside an existing test project:
56
56
 
57
57
  ```sh
58
- ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.1.tgz"
58
+ ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.2.tgz"
59
59
  npx --yes --package "$ads_tool_package" ads --help
60
60
  npx --yes --package "$ads_tool_package" ads init --framework react --tool-package "$ads_tool_package"
61
61
  npm install
@@ -93,7 +93,8 @@ component rules through the project's plugin adapter and `ds.config.json`, then
93
93
  run its actual build. Use `lint-config` directly for custom pipeline integration
94
94
  or debugging, not as a separate routine setup step.
95
95
 
96
- Use ADS 0.3.1 or later for the shared-resource preview compiler. `mcp` starts stdio
96
+ Use ADS 0.3.2 or later for the closed foundation contract and recoverable
97
+ shared-resource preview compiler. `mcp` starts stdio
97
98
  plus its local website; `showcase` starts the browser-only MCP runtime. Open the existing runtime URL from `ads_status` when
98
99
  connected; otherwise use `npm run dev:showcase` or the installed `ads showcase`.
99
100
  The tool client discovers entries and renders MCP Apps. Init adds the launch
@@ -308,6 +308,25 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
308
308
  foundation CSS rather than assigning shared token values in JavaScript. Keep
309
309
  full-app integration harnesses and workflow coverage in application test support.
310
310
 
311
+ Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
312
+ on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
313
+ three closed roles: root/body typography, canvas and theme defaults; limited native
314
+ normalization; and global focus indicators/reduced-motion rules. Every selector
315
+ branch, property and value must fit its role. Token-backed control skins, layout,
316
+ interactive state rules and owner hooks are rejected even through `:where()`.
317
+ Do not copy an unchecked upstream preflight or turn a component repair into a reset.
318
+ Put skin/state/animation in the Linaria owner, and scenario layout variations in
319
+ the public review pattern's typed API. Browser review still verifies the effects.
320
+
321
+ Invalid metadata and source failures keep the managed compiler alive for repairs.
322
+ Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
323
+ successful output. If the builder exited, fix its command and invoke the preview
324
+ again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
325
+ also reload on the next preview invocation. Reconnect the client when upgrading
326
+ the ADS server package itself. Persistent SSE/notification streams make
327
+ `networkidle` unsuitable: wait for the App's current ready/error state instead.
328
+
329
+
311
330
 
312
331
 
313
332
  Register component/project Oxlint plugins and rules in `ds.config.json`. Use
@@ -55,7 +55,7 @@ If no published distribution exists, build and pack the tooling checkout with
55
55
  From inside an existing test project:
56
56
 
57
57
  ```sh
58
- ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.1.tgz"
58
+ ads_tool_package="/absolute/path/to/reqvire-org-ads-0.3.2.tgz"
59
59
  npx --yes --package "$ads_tool_package" ads --help
60
60
  npx --yes --package "$ads_tool_package" ads init --framework react --tool-package "$ads_tool_package"
61
61
  npm install
@@ -93,7 +93,8 @@ component rules through the project's plugin adapter and `ds.config.json`, then
93
93
  run its actual build. Use `lint-config` directly for custom pipeline integration
94
94
  or debugging, not as a separate routine setup step.
95
95
 
96
- Use ADS 0.3.1 or later for the shared-resource preview compiler. `mcp` starts stdio
96
+ Use ADS 0.3.2 or later for the closed foundation contract and recoverable
97
+ shared-resource preview compiler. `mcp` starts stdio
97
98
  plus its local website; `showcase` starts the browser-only MCP runtime. Open the existing runtime URL from `ads_status` when
98
99
  connected; otherwise use `npm run dev:showcase` or the installed `ads showcase`.
99
100
  The tool client discovers entries and renders MCP Apps. Init adds the launch
@@ -198,6 +198,25 @@ Use local simulated services in fixtures. Tests may import fixtures; load real
198
198
  foundation CSS rather than assigning shared token values in JavaScript. Keep
199
199
  full-app integration harnesses and workflow coverage in application test support.
200
200
 
201
+ Foundation CSS is checked, not an escape hatch. Value sheets define shared tokens
202
+ on the document root/themes; `fonts.css` holds font-face metadata. `base.css` has
203
+ three closed roles: root/body typography, canvas and theme defaults; limited native
204
+ normalization; and global focus indicators/reduced-motion rules. Every selector
205
+ branch, property and value must fit its role. Token-backed control skins, layout,
206
+ interactive state rules and owner hooks are rejected even through `:where()`.
207
+ Do not copy an unchecked upstream preflight or turn a component repair into a reset.
208
+ Put skin/state/animation in the Linaria owner, and scenario layout variations in
209
+ the public review pattern's typed API. Browser review still verifies the effects.
210
+
211
+ Invalid metadata and source failures keep the managed compiler alive for repairs.
212
+ Read `ads_status.build` or `ads://project/build`, fix the owner, then wait for current
213
+ successful output. If the builder exited, fix its command and invoke the preview
214
+ again; ADS starts one fresh child without an MCP reconnect. Compiler-code changes
215
+ also reload on the next preview invocation. Reconnect the client when upgrading
216
+ the ADS server package itself. Persistent SSE/notification streams make
217
+ `networkidle` unsuitable: wait for the App's current ready/error state instead.
218
+
219
+
201
220
 
202
221
 
203
222
  Register component/project Oxlint plugins and rules in `ds.config.json`. Use