@reqvire-org/ads 0.3.9 → 0.3.11

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,8 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import { isInside, projectPath } from "../config.mjs";
5
+ import { isInside, projectPath, loadConfig } from "../config.mjs";
6
+ import { previewSourceRevision } from "../preview-inputs.mjs";
6
7
  import { readPreviewManifest } from "../preview-resources.mjs";
7
8
  import { previewArtifactPath, withPreviewSession } from "../preview-scope.mjs";
8
9
 
@@ -10,7 +11,7 @@ export const BUILD_URI = "ads://project/build";
10
11
  export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json");
11
12
  const maxBytes = 256 * 1024;
12
13
  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();
13
- 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();
14
+ 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(), sourceRevision:z.string().regex(/^[a-f0-9]{64}$/).optional(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
14
15
  const progressSchema = z.object({ phase: z.enum(["queued", "validation", "compilation", "validation-and-compilation", "publication", "idle"]), startedAt: z.string().datetime(), updatedAt: z.string().datetime() }).strict();
15
16
  const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), progress: progressSchema.optional(), 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.");
16
17
  export const artifactDigest = (file) => {
@@ -93,8 +94,9 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
93
94
  apps = next;
94
95
  save();
95
96
  },
96
- begin(id) {
97
+ begin(id,{sourceRevision = previewSourceRevision(loadConfig(root),apps)} = {}) {
97
98
  const value = entry(id);
99
+ value.sourceRevision = sourceRevision;
98
100
  value.state = "building";
99
101
  value.revision++;
100
102
  value.diagnostics = [];
@@ -137,6 +139,11 @@ export function inspectBuildStatus(root, config, { session, previewSession, proc
137
139
  }
138
140
  } catch (error) { reportError = `Invalid preview build status: ${error.message.slice(0, 500)}`; }
139
141
  let graphError;
142
+ let sourceRevision, sourceError;
143
+ if (report?.previews.some(entry=>entry.state==="ready")) {
144
+ try { sourceRevision = previewSourceRevision(loadConfig(root),config.apps); }
145
+ catch (error) { sourceError = `Cannot verify preview inputs: ${error.message}`; }
146
+ }
140
147
  if (config.previewManifest && report?.previews.some(entry => entry.state === "ready")) {
141
148
  try {
142
149
  if (!readPreviewManifest(options, { verify: true })) throw new Error("Preview resource manifest is missing.");
@@ -154,13 +161,16 @@ export function inspectBuildStatus(root, config, { session, previewSession, proc
154
161
  if (state === "ready") {
155
162
  try {
156
163
  if (value.lastSuccessfulRevision !== value.revision || artifactDigest(artifact) !== value.artifactDigest) throw new Error("Preview output does not match its successful build. Rebuild before reviewing it.");
164
+ if (sourceError) throw new Error(sourceError);
165
+ if (!value.sourceRevision) throw new Error("Preview builder did not record its source revision. Rebuild using the current ADS build reporter.");
166
+ if (value.sourceRevision !== sourceRevision) { state = "idle"; diagnostics = [{code:"ADS_PREVIEW_STALE_SOURCE",message:"Project inputs changed after this preview was built. Waiting for a build of the current source."}]; }
157
167
  } catch (error) { state = "error"; diagnostics = [{ message: error.message }]; }
158
168
  }
159
169
  if (processState?.state === "failed") { state = "error"; diagnostics = [{ message: processState.error ?? "Preview watcher exited." }, ...diagnostics].slice(0, 20); }
160
170
  if (["stopped", "stopping", "idle"].includes(processState?.state)) state = "unreported";
161
171
  const current = state === "ready";
162
172
  return { id: app.id, state, session: report?.session ?? null, revision: value?.revision ?? null, lastSuccessfulRevision: value?.lastSuccessfulRevision ?? null,
163
- current, stale: state === "unreported" ? null : !current, available, diagnostics };
173
+ current, stale: state === "unreported" ? null : !current, available, ...(value?.sourceRevision ? {sourceRevision:value.sourceRevision} : {}), diagnostics };
164
174
  });
165
175
  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";
166
176
  return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, ...(report?.progress ? { progress: report.progress } : {}), previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
@@ -31,16 +31,17 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
31
31
  const ready = new Set();
32
32
  const requests = new Set();
33
33
  let active;
34
+ let invalidation;
34
35
  let requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
35
36
  let reportFile = path.join(root, ".ds", `build-status-${session}.json`);
36
- function request(file) {
37
- if (!command || requests.has(file) && active === file) return;
37
+ function request(file,force = false) {
38
+ if (!command || !force && requests.has(file) && active === file) return;
38
39
  requests.add(file);
39
40
  active = file;
40
41
  mkdirSync(path.dirname(requestFile), { recursive: true });
41
42
  if (realpathSync(path.dirname(requestFile)) !== path.dirname(requestFile)) throw new Error("Preview requests cannot resolve through a symlink.");
42
43
  const temporary = `${requestFile}.tmp`;
43
- writeFileSync(temporary, JSON.stringify({ session, active: path.relative(root, active).replaceAll("\\", "/"), files: [...requests].map((file) => path.relative(root, file).replaceAll("\\", "/")) }));
44
+ writeFileSync(temporary, JSON.stringify({ session, active: path.relative(root, active).replaceAll("\\", "/"), files: [...requests].map((file) => path.relative(root, file).replaceAll("\\", "/")), ...(invalidation ? {invalidation} : {}) }));
44
45
  renameSync(temporary, requestFile);
45
46
  }
46
47
 
@@ -135,7 +136,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
135
136
  if (state === "failed" || restarting || stalled) await restart();
136
137
  if (disposed) throw new Error("Preview watcher is stopped.");
137
138
  start();
138
- request(file);
139
+ request(file,buildStatus().previews.some(entry=>entry.diagnostics.some(item=>item.code==="ADS_PREVIEW_STALE_SOURCE")));
139
140
  if (signal?.aborted) throw new Error("Preview request cancelled.");
140
141
  const app = config.apps.find((entry) => path.resolve(root, entry.file) === file);
141
142
  let baseline = baselines.get(file) ?? null;
@@ -181,7 +182,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
181
182
  })();
182
183
  return stopping;
183
184
  }
184
- return { start, ensure, async stop() { disposed = true; await restarting; return stopChild(); }, kill: () => kill("SIGKILL"), status, resourceSession, buildStatus, setApps(apps) {
185
+ return { start, ensure, invalidate() { invalidation = randomUUID(); if (active) request(active,true); }, async stop() { disposed = true; await restarting; return stopChild(); }, kill: () => kill("SIGKILL"), status, resourceSession, buildStatus, setApps(apps) {
185
186
  for (const app of apps) {
186
187
  const file = path.resolve(root, app.file);
187
188
  if (!baselines.has(file)) baselines.set(file, stamp(file));
@@ -202,6 +203,7 @@ export function createReconfigurablePreviewWatch(root, config, options) {
202
203
  status() { return replacing ? { state: "reconfiguring" } : current.status(); },
203
204
  resourceSession() { return current.resourceSession(); },
204
205
  buildStatus() { return current.buildStatus(); },
206
+ async invalidate() { await pending; if (!stopped) current.invalidate(); },
205
207
  setApps(next) { apps = next; current.setApps(next); },
206
208
  kill() { current.kill(); },
207
209
  reconfigure(next, force = false) {
@@ -46,6 +46,14 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
46
46
  cache.registry = cache.catalog = cache.documents = undefined;
47
47
  }
48
48
  const getShowcaseUrl = () => showcaseUrl?.() ?? options.config.mcp.showcaseUrl;
49
+ function scenarioUrl(id) {
50
+ const base = getShowcaseUrl();
51
+ if (!base) return null;
52
+ const url = new URL(base);
53
+ url.searchParams.delete("category");
54
+ url.searchParams.set("scenario", id);
55
+ return url.href;
56
+ }
49
57
  const project = () => { const context = projectContext(options); context.showcase.url = getShowcaseUrl(); return context; };
50
58
  const subscribed = new Set();
51
59
  const server = new Server({ name: "reqvire-ads", version: pkg.version }, {
@@ -171,6 +179,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
171
179
  if (result.applied) {
172
180
  try {
173
181
  if (result.changes.some(change => change.path === "ds.config.json") || result.unit === "configuration" || result.units?.includes("configuration")) await previewWatch?.reconfigure?.(loadConfig(root).config.mcp,true);
182
+ await previewWatch?.invalidate?.();
174
183
  await refresh(true);
175
184
  } 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.`}];}
176
185
  }
@@ -197,10 +206,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
197
206
  const build = await previewBuild(app, signal);
198
207
  const graph = graphSelection(id);
199
208
  if (!options.config.mcp.previewManifest) projectApp(options, app, build);
200
- const url = getShowcaseUrl() ? new URL(getShowcaseUrl()) : null;
201
- url?.searchParams.delete("category");
202
- url?.searchParams.set("scenario", id);
203
- return json({ widget: { resourceUri: scenarioWidget() }, scenario: { ...entry, resourceUri: appUri(id) }, ...graph, build, showcaseUrl: url?.href ?? null });
209
+ return json({ widget: { resourceUri: scenarioWidget() }, scenario: { ...entry, resourceUri: appUri(id) }, ...graph, build, showcaseUrl: scenarioUrl(id) });
204
210
  } },
205
211
  { name: "ads_tokens", description: "Use before styling, theme, brand or motion-token changes to inspect existing semantic definitions/values. Query specific token names with a small limit. ads_show_tokens renders project token scenarios; definition inspection does not validate component layout.", schema: query, call: ({ query: search = "", limit }) => {
206
212
  const inventory = inspectTokens(root);
@@ -233,10 +239,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
233
239
  const build = await previewBuild(app, signal);
234
240
  const graph = graphSelection(category);
235
241
  if (!options.config.mcp.previewManifest) projectApp(options, app, build);
236
- const url = getShowcaseUrl() ? new URL(getShowcaseUrl()) : null;
237
- url?.searchParams.delete("scenario");
238
- url?.searchParams.set("category", category);
239
- return json({ widget: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI }, category, scenario: { ...entry, resourceUri: appUri(category) }, ...graph, build, showcaseUrl: url?.href ?? null });
242
+ return json({ widget: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI }, category, scenario: { ...entry, resourceUri: appUri(category) }, ...graph, build, showcaseUrl: scenarioUrl(category) });
240
243
  }
241
244
  return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
242
245
  } },
@@ -0,0 +1,24 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { isInside } from "./config.mjs";
5
+
6
+ /** Shared source evidence for compilation, watch recovery and status reads. */
7
+ export function previewSourceRevision({root,config}, apps = config.mcp.apps ?? []) {
8
+ const rows = [], generated = config.assets.generatedPaths.map(file => path.resolve(root,file));
9
+ const outputs = new Set(apps.map(app => path.resolve(root,app.file)));
10
+ function digest(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
11
+ function visit(directory) {
12
+ for (const entry of readdirSync(directory,{withFileTypes:true})) {
13
+ if (entry.isSymbolicLink() || ["node_modules",".git",".ds",".vite","coverage"].includes(entry.name)) continue;
14
+ const file = path.join(directory,entry.name);
15
+ if (outputs.has(file) || generated.some(base => isInside(base,file))) continue;
16
+ if (entry.isDirectory()) visit(file);
17
+ else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) rows.push([path.relative(root,file),digest(file)]);
18
+ }
19
+ }
20
+ visit(root);
21
+ const installed = path.join(root,"node_modules/.package-lock.json");
22
+ rows.push(["installed-packages",existsSync(installed) ? digest(installed) : null]);
23
+ return createHash("sha256").update(JSON.stringify(rows.sort((a,b)=>a[0].localeCompare(b[0])))).digest("hex");
24
+ }
@@ -28,7 +28,8 @@ export function previewEntriesPlugin(entries, mountEntry) {
28
28
  load(id) {
29
29
  const entry = ids.get(id.slice(1));
30
30
  if (!entry) return;
31
- return `import { mountScenario } from ${JSON.stringify(mountEntry)};import Scenario from ${JSON.stringify(path.resolve(entry.root, entry.file))};export const mount = container => mountScenario(Scenario,container);`;
31
+ const styles = (entry.stylesheets ?? []).map(file => `import ${JSON.stringify(path.resolve(entry.root, file))};`).join("");
32
+ return `import { mountScenario } from ${JSON.stringify(mountEntry)};${styles}import Scenario from ${JSON.stringify(path.resolve(entry.root, entry.file))};export const mount = container => mountScenario(Scenario,container);`;
32
33
  },
33
34
  };
34
35
  }
@@ -6,6 +6,7 @@ import { inspectScenarios } from "./scenarios.mjs";
6
6
  import { artifactDigest, createBuildReporter } from "./mcp/build-status.mjs";
7
7
  import { cachedPreviewPublication, previewArtifactHtml, readPreviewManifest } from "./preview-resources.mjs";
8
8
  import { previewArtifactPath, withPreviewSession } from "./preview-scope.mjs";
9
+ import { previewSourceRevision } from "./preview-inputs.mjs";
9
10
 
10
11
  const digest = value => createHash("sha256").update(value).digest("hex");
11
12
  const fileDigest = file => { try { return digest(readFileSync(file)); } catch { return null; } };
@@ -13,8 +14,8 @@ const fileDigest = file => { try { return digest(readFileSync(file)); } catch {
13
14
  /** Cache only successful validation + matching source/dependency/output content.
14
15
  * Cache state is development output, never proof that a failed build is current. */
15
16
  export function createPreviewCache(options, compilerFiles = []) {
16
- const { root, config } = options;
17
- const adapterFiles = [import.meta.filename, ...["preview-resources.mjs", "preview-scope.mjs", "scenarios-vite.mjs", "linaria.mjs", "linaria/atomic-css.cjs", "linaria/atomic-styled.cjs", "linaria/preserve-values.cjs"].map(file => path.join(import.meta.dirname, file)), ...compilerFiles,
17
+ const { root } = options;
18
+ const adapterFiles = [import.meta.filename, ...["preview-inputs.mjs", "preview-resources.mjs", "preview-scope.mjs", "scenarios-vite.mjs", "linaria.mjs", "linaria/atomic-css.cjs", "linaria/atomic-styled.cjs", "linaria/preserve-values.cjs"].map(file => path.join(import.meta.dirname, file)), ...compilerFiles,
18
19
  ...["package.json", "package-lock.json", "bun.lock", "bun.lockb", "node_modules/.package-lock.json"].map(file => path.join(root, file))];
19
20
  const adapterDigest = () => digest(JSON.stringify(adapterFiles.map(file => [file, fileDigest(file)])));
20
21
  const loadedAdapter = adapterDigest();
@@ -27,20 +28,7 @@ export function createPreviewCache(options, compilerFiles = []) {
27
28
  }
28
29
  } catch { /* Missing or corrupt cache means validate and rebuild. */ }
29
30
  function inputs() {
30
- const rows = [];
31
- const generated = config.assets.generatedPaths.map(file => path.resolve(root, file));
32
- function visit(directory) {
33
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
34
- if (entry.isSymbolicLink() || ["node_modules", ".git", ".ds", ".vite", "coverage"].includes(entry.name)) continue;
35
- const file = path.join(directory, entry.name);
36
- if (generated.some(base => isInside(base, file))) continue;
37
- if (entry.isDirectory()) visit(file);
38
- else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|css|html|json|md|svg|png|jpe?g|webp|gif|woff2?|ttf|otf|lock|lockb)$/.test(file)) rows.push([path.relative(root, file), fileDigest(file)]);
39
- }
40
- }
41
- visit(root);
42
- rows.push(["installed-packages", fileDigest(path.join(root, "node_modules/.package-lock.json"))]);
43
- rows.push(["node-runtime", `${process.version}/${process.platform}/${process.arch}`]);
31
+ const rows = [["project-inputs",previewSourceRevision(options)],["node-runtime",`${process.version}/${process.platform}/${process.arch}`]];
44
32
  for (const file of compilerFiles) rows.push([file, fileDigest(file)]);
45
33
  // Tool upgrades invalidate validation even when the project's lockfile is unchanged.
46
34
  for (const name of readdirSync(import.meta.dirname).filter(name => name.endsWith(".mjs"))) {
@@ -119,6 +107,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
119
107
  let generation = 0;
120
108
  let publishedStamp;
121
109
  let validation;
110
+ let observedSourceRevision = previewSourceRevision(options);
111
+ let invalidation;
122
112
  function failure(error) {
123
113
  try { reporter.failure(error); } catch (reportError) { console.error(reportError); }
124
114
  console.error(error);
@@ -144,7 +134,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
144
134
  }
145
135
  async function compile(entry) {
146
136
  if (buildGraph) return compileGraph();
147
- const revision = reporter.begin(entry.id);
137
+ const revision = reporter.begin(entry.id,{sourceRevision:previewSourceRevision(options)});
148
138
  const started = generation;
149
139
  try {
150
140
  const stamp = cache.inputs();
@@ -187,7 +177,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
187
177
  }
188
178
  }
189
179
  async function compileGraph() {
190
- const revisions = new Map(entries.map(entry => [entry.id, reporter.begin(entry.id)]));
180
+ const sourceRevision = previewSourceRevision(options);
181
+ const revisions = new Map(entries.map(entry => [entry.id, reporter.begin(entry.id,{sourceRevision})]));
191
182
  const started = generation;
192
183
  try {
193
184
  const stamp = cache.inputs();
@@ -257,6 +248,10 @@ export async function runScenarioCompiler(directory, { watched = false, validate
257
248
  try {
258
249
  const value = JSON.parse(readFileSync(file, "utf8"));
259
250
  if (value.session !== process.env.ADS_MCP_BUILD_SESSION) return;
251
+ if (value.invalidation !== invalidation) {
252
+ invalidation = value.invalidation;
253
+ if (requested.size) sourceChanged("invalidate",options.config.dsDir,true);
254
+ }
260
255
  active = entries.find(entry => entry.artifact === value.active)?.id;
261
256
  for (const file of value.files) {
262
257
  const entry = entries.find(entry => entry.artifact === file);
@@ -288,6 +283,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
288
283
  const changes = [...changed]; changed.clear();
289
284
  try {
290
285
  entries = registry();
286
+ observedSourceRevision = previewSourceRevision(options);
291
287
  registryFailure = undefined;
292
288
  reporter.setApps(buildApps());
293
289
  reporter.clearFailure();
@@ -297,7 +293,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
297
293
  // compilation. Check active first, then rebuild only changed dependencies.
298
294
  for (const id of requested) {
299
295
  const entry = entries.find(entry => entry.id === id);
300
- if (entry) { reporter.begin(id); pending.add(id); }
296
+ if (entry) { reporter.begin(id,{sourceRevision:observedSourceRevision}); pending.add(id); }
301
297
  }
302
298
  if (changes.length) void drain();
303
299
  } catch (error) {
@@ -344,7 +340,17 @@ export async function runScenarioCompiler(directory, { watched = false, validate
344
340
  } catch (error) { failure(error); }
345
341
  }
346
342
  reconcileWatches(true);
347
- const reconcileTimer = setInterval(reconcileWatches, 1000);
343
+ const reconcileTimer = setInterval(() => {
344
+ reconcileWatches();
345
+ try {
346
+ const current = previewSourceRevision(options);
347
+ if (current !== observedSourceRevision) {
348
+ observedSourceRevision = current;
349
+ sourceChanged("reconcile",options.config.dsDir,true);
350
+ }
351
+ request();
352
+ } catch (error) { failure(error); }
353
+ }, 1000);
348
354
  request();
349
355
  console.log(`Scenario compiler ready: ${entries.length} entries; builds are requested on demand.`);
350
356
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
@@ -110,7 +110,8 @@ export function inspectScenarios(options, suppliedProgram) {
110
110
  const owner = scoped.inventory.owner(relative);
111
111
  for (const name of tokens) if (tokenInventory.tokens.find(token => token.name === name)?.owner !== owner) report(file, `Token ${name} needs a preview beside its defining owner. Shared tokens belong under the configured token directory; local tokens stay with their component/pattern.`);
112
112
  }
113
- scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(Object.hasOwn(metadata, "exercises") ? { exercises: [...new Set(exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
113
+ const stylesheets = kind === "tokens" ? scoped.definitions.filter(entry => entry.unit === scoped.inventory.owner(relative)).map(entry => entry.file) : [];
114
+ scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(Object.hasOwn(metadata, "exercises") ? { exercises: [...new Set(exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), ...(stylesheets.length ? { stylesheets } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
114
115
  }
115
116
  return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
116
117
  }