@reqvire-org/ads 0.3.18 → 0.3.20

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 (78) hide show
  1. package/dist/behaviors.mjs +5 -5
  2. package/dist/bootstrap-ds/ADS.md +2 -2
  3. package/dist/bootstrap-ds/README.md +1 -1
  4. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  5. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  6. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  7. package/dist/bootstrap-ds/ds.config.json +1 -1
  8. package/dist/bootstrap-ds/tsconfig.json +18 -3
  9. package/dist/bootstrap-ds/vite.config.ts +1 -1
  10. package/dist/boundaries.mjs +4 -2
  11. package/dist/check.mjs +20 -13
  12. package/dist/config.mjs +13 -2
  13. package/dist/init.mjs +17 -14
  14. package/dist/layers.mjs +288 -0
  15. package/dist/lint-config.mjs +1 -1
  16. package/dist/mcp/context.mjs +2 -1
  17. package/dist/mcp/edit-preparation.mjs +3 -3
  18. package/dist/mcp/mutations.mjs +1 -1
  19. package/dist/mcp/prompts.mjs +1 -1
  20. package/dist/mcp/server.mjs +6 -2
  21. package/dist/mcp/widget.bundle.js +1 -1
  22. package/dist/oxlint.mjs +3 -1
  23. package/dist/preview-resources.mjs +11 -1
  24. package/dist/public-api.mjs +13 -6
  25. package/dist/scenario-trace.mjs +6 -6
  26. package/dist/scenarios-vite.mjs +2 -1
  27. package/dist/scenarios.mjs +10 -7
  28. package/dist/showcase/client.bundle.js +1 -1
  29. package/dist/showcase/sandbox.bundle.js +1 -1
  30. package/dist/skills/ads-break-ui/SKILL.md +21 -6
  31. package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
  32. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  33. package/dist/skills/ads-break-ui/references/review.md +96 -0
  34. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  35. package/dist/skills/ads-designer/SKILL.md +22 -7
  36. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  37. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
  38. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  39. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  40. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  41. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  42. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  43. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  44. package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
  45. package/dist/skills/ads-designer/references/layers.md +123 -0
  46. package/dist/skills/ads-designer/references/review.md +96 -0
  47. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  48. package/dist/skills/ads-import/SKILL.md +21 -6
  49. package/dist/skills/ads-import/references/guarded-editing.md +1 -1
  50. package/dist/skills/ads-import/references/layers.md +123 -0
  51. package/dist/skills/ads-import/references/review.md +96 -0
  52. package/dist/skills/ads-import/references/scenarios.md +24 -3
  53. package/dist/skills/ads-migrate/SKILL.md +21 -6
  54. package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
  55. package/dist/skills/ads-migrate/references/layers.md +123 -0
  56. package/dist/skills/ads-migrate/references/review.md +96 -0
  57. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  58. package/dist/skills/ads-motion/SKILL.md +22 -7
  59. package/dist/skills/ads-motion/references/audit.md +1 -1
  60. package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
  61. package/dist/skills/ads-motion/references/layers.md +123 -0
  62. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  63. package/dist/skills/ads-motion/references/review.md +96 -35
  64. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  65. package/dist/units.mjs +17 -14
  66. package/docs/ADS.md +32 -5
  67. package/docs/ARCHITECTURE.md +37 -4
  68. package/docs/BEHAVIORS.md +7 -0
  69. package/docs/CHANGELOG.md +23 -0
  70. package/docs/GUIDE.md +3 -2
  71. package/docs/MCP.md +27 -6
  72. package/package.json +1 -1
  73. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  74. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  75. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  76. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  77. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  78. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
@@ -0,0 +1,288 @@
1
+ import path from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import ts from "typescript";
4
+ import selectorParser from "postcss-selector-parser";
5
+ import valueParser from "postcss-value-parser";
6
+ import { isDevelopmentFile, isInside, isStateVariable, isTestFile, isReviewFile } from "./config.mjs";
7
+ import { importResolver, styleAccessProperty } from "./boundaries.mjs";
8
+
9
+ const guidance = "See ads://guides/layers. A component provides one cohesive UI capability; a pattern arranges capabilities into a reusable structure or workflow. Component count does not decide the layer.";
10
+
11
+ /** Include statically addressed local helpers outside the configured source roots.
12
+ * Dependency packages remain external; no project module is imported/executed. */
13
+ export function layerSourceFiles(initial, options) {
14
+ const files = new Set(initial), resolve = importResolver(options);
15
+ for (const file of files) {
16
+ const source = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
17
+ function include(literal) {
18
+ if (!literal || !ts.isStringLiteralLike(literal)) return;
19
+ for (const dependency of resolve(literal.text, file)) {
20
+ if (isInside(options.root, dependency) && !dependency.split(path.sep).includes("node_modules") && /\.[cm]?[jt]sx?$/.test(dependency) && existsSync(dependency)) files.add(dependency);
21
+ }
22
+ }
23
+ function visit(node) {
24
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) include(node.moduleSpecifier);
25
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) include(node.moduleReference.expression);
26
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) include(node.argument.literal);
27
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require")) include(node.arguments[0]);
28
+ ts.forEachChild(node, visit);
29
+ }
30
+ visit(source);
31
+ }
32
+ return [...files];
33
+ }
34
+
35
+ /** Source-only dependency and private-style evidence; never evaluates project code. */
36
+ export function createLayerBoundaries(program, options, report) {
37
+ const { root, dsRoot, sourceRoot, config } = options;
38
+ const checker = program.getTypeChecker(), resolve = importResolver(options);
39
+ const sources = program.getSourceFiles().filter(source => isInside(root, source.fileName) && !source.isDeclarationFile && !source.fileName.split(path.sep).includes("node_modules"));
40
+ const runtime = file => !isTestFile(file) && !isDevelopmentFile(file, options);
41
+ const layer = file => ["components", "patterns", "controllers", "composition", "review"].find(name => isInside(path.join(dsRoot, name), file)) ?? (isInside(sourceRoot, file) && !isInside(dsRoot, file) ? "application" : "shared");
42
+ const unalias = symbol => symbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
43
+ const entry = program.getSourceFile(path.join(dsRoot, config.publicEntry));
44
+ const module = entry && checker.getSymbolAtLocation(entry);
45
+ const publicSymbols = new Set((module ? checker.getExportsOfModule(module) : []).map(unalias));
46
+ const publicFiles = new Set([...publicSymbols].filter(symbol => symbol?.flags & ts.SymbolFlags.Value).flatMap(symbol => symbol.declarations?.map(node => node.getSourceFile().fileName) ?? []));
47
+ const edges = new Map(), emitted = new Set(), classOwners = new Map(), attributeOwners = new Map(), componentVariables = new Set(), patternVariables = new Set(), publicHooks = new Set();
48
+
49
+ function emit(node, rule, message) {
50
+ const source = node.getSourceFile(), position = source.getLineAndCharacterOfPosition(node.getStart(source));
51
+ const key = `${source.fileName}:${node.pos}:${rule}:${message}`;
52
+ if (emitted.has(key)) return;
53
+ emitted.add(key);
54
+ report(source.fileName, rule, `${message} ${guidance}`, position.line + 1, position.character + 1, { guidanceUri: "ads://guides/layers" });
55
+ }
56
+ function target(specifier, source) {
57
+ return resolve(specifier, source.fileName).find(file => program.getSourceFile(file));
58
+ }
59
+ function exportsOf(file) {
60
+ const source = file && program.getSourceFile(file), symbol = source && checker.getSymbolAtLocation(source);
61
+ return symbol ? checker.getExportsOfModule(symbol) : [];
62
+ }
63
+ function origins(symbol) {
64
+ symbol = unalias(symbol);
65
+ return [...new Set(symbol?.declarations?.map(node => node.getSourceFile().fileName) ?? [])].filter(file => isInside(root, file) && !file.split(path.sep).includes("node_modules")).map(file => ({ file, symbol }));
66
+ }
67
+ function bindings(pattern) {
68
+ return ts.isObjectBindingPattern(pattern) ? pattern.elements.filter(node => !node.dotDotDotToken).map(node => (node.propertyName ?? node.name).text).filter(Boolean) : null;
69
+ }
70
+ function members(identifier, source) {
71
+ const symbol = checker.getSymbolAtLocation(identifier), names = new Set();
72
+ let opaque = false;
73
+ function visit(node) {
74
+ if (ts.isIdentifier(node) && node !== identifier && checker.getSymbolAtLocation(node) === symbol) {
75
+ const parent = node.parent;
76
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === node) names.add(parent.name.text);
77
+ else if (ts.isElementAccessExpression(parent) && parent.expression === node && ts.isStringLiteralLike(parent.argumentExpression)) names.add(parent.argumentExpression.text);
78
+ else if (ts.isVariableDeclaration(parent) && parent.initializer === node && bindings(parent.name)) for (const name of bindings(parent.name)) names.add(name);
79
+ else opaque = true;
80
+ }
81
+ ts.forEachChild(node, visit);
82
+ }
83
+ visit(source);
84
+ return opaque ? null : [...names];
85
+ }
86
+ function selectedImport(call, source) {
87
+ let expression = call;
88
+ while (ts.isAwaitExpression(expression.parent) || ts.isParenthesizedExpression(expression.parent)) expression = expression.parent;
89
+ const parent = expression.parent;
90
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === expression && parent.name.text !== "then") return [parent.name.text];
91
+ if (ts.isElementAccessExpression(parent) && parent.expression === expression && ts.isStringLiteralLike(parent.argumentExpression)) return [parent.argumentExpression.text];
92
+ if (ts.isVariableDeclaration(parent)) return bindings(parent.name) ?? (ts.isIdentifier(parent.name) ? members(parent.name, source) : null);
93
+ if (ts.isPropertyAccessExpression(parent) && parent.name.text === "then" && ts.isCallExpression(parent.parent)) {
94
+ const callback = parent.parent.arguments[0];
95
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && callback.parameters[0]) {
96
+ const parameter = callback.parameters[0].name;
97
+ return bindings(parameter) ?? (ts.isIdentifier(parameter) ? members(parameter, source) : null);
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ function add(source, node, dependency, typeOnly = false) {
103
+ if (!dependency?.file || !isInside(root, dependency.file) || dependency.file.split(path.sep).includes("node_modules")) return;
104
+ const list = edges.get(source.fileName) ?? [];
105
+ list.push({ ...dependency, node, typeOnly });
106
+ edges.set(source.fileName, list);
107
+ }
108
+ function addSymbol(source, node, symbol, typeOnly = false) {
109
+ for (const origin of origins(symbol)) add(source, node, origin, typeOnly);
110
+ }
111
+ function addModule(source, node, file, names, typeOnly = false) {
112
+ if (!file) return;
113
+ const values = exportsOf(file).filter(symbol => names === null || names.includes(symbol.name));
114
+ for (const symbol of values) if (typeOnly || unalias(symbol)?.flags & ts.SymbolFlags.Value) addSymbol(source, node, symbol, typeOnly);
115
+ if (!values.length || layer(file) === "composition" || isDevelopmentFile(file, options)) add(source, node, { file }, typeOnly);
116
+ // Explicit side effects in a forwarding barrel still run for a selected export.
117
+ if (!typeOnly) for (const statement of program.getSourceFile(file)?.statements ?? []) {
118
+ if (ts.isImportDeclaration(statement) && !statement.importClause && ts.isStringLiteralLike(statement.moduleSpecifier)) {
119
+ const sideEffect = target(statement.moduleSpecifier.text, statement.getSourceFile());
120
+ if (sideEffect) add(source, node, { file: sideEffect });
121
+ }
122
+ }
123
+ }
124
+ for (const source of sources) {
125
+ function visit(node) {
126
+ if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
127
+ const file = target(node.moduleSpecifier.text, source), clause = node.importClause;
128
+ if (!clause) add(source, node, { file: file ?? resolve(node.moduleSpecifier.text, source.fileName)[0] });
129
+ else {
130
+ if (clause.name) addSymbol(source, node, checker.getSymbolAtLocation(clause.name), !!clause.isTypeOnly);
131
+ const named = clause.namedBindings;
132
+ if (named && ts.isNamedImports(named)) for (const item of named.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(clause.isTypeOnly || item.isTypeOnly));
133
+ if (named && ts.isNamespaceImport(named)) addModule(source, node, file, members(named.name, source), !!clause.isTypeOnly);
134
+ }
135
+ }
136
+ if (ts.isExportDeclaration(node)) {
137
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) for (const item of node.exportClause.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(node.isTypeOnly || item.isTypeOnly));
138
+ else if (node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) addModule(source, node, target(node.moduleSpecifier.text, source), null, !!node.isTypeOnly);
139
+ }
140
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
141
+ addModule(source, node, target(node.moduleReference.expression.text, source), members(node.name, source), !!node.isTypeOnly);
142
+ }
143
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal)) addModule(source, node, target(node.argument.literal.text, source), null, true);
144
+ const requireSymbol = ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" ? checker.getSymbolAtLocation(node.expression) : null;
145
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require" && (!requireSymbol || requireSymbol.declarations?.every(declaration => declaration.getSourceFile().isDeclarationFile))) && ts.isStringLiteralLike(node.arguments[0])) {
146
+ addModule(source, node, target(node.arguments[0].text, source), selectedImport(node, source));
147
+ }
148
+ ts.forEachChild(node, visit);
149
+ }
150
+ visit(source);
151
+ }
152
+
153
+ function runtimeHelpers() {
154
+ const found = new Set(sources.filter(source => isReviewFile(source.fileName, options) || runtime(source.fileName) && ["components", "patterns", "controllers"].includes(layer(source.fileName))).map(source => source.fileName));
155
+ for (const file of found) for (const edge of edges.get(file) ?? []) if (!edge.typeOnly && runtime(edge.file) && program.getSourceFile(edge.file)) found.add(edge.file);
156
+ return [...found];
157
+ }
158
+
159
+ function inspectDependencies() {
160
+ for (const source of sources.filter(source => runtime(source.fileName) || isReviewFile(source.fileName, options))) {
161
+ const ownLayer = layer(source.fileName);
162
+ // Check resolved declaration ownership too: a barrel cannot launder fixtures/app code.
163
+ if (isInside(dsRoot, source.fileName) || ownLayer === "application") for (const edge of edges.get(source.fileName) ?? []) {
164
+ if (runtime(source.fileName) && layer(edge.file) === "review") emit(edge.node, "review-import", "Production code and its public entry cannot depend on review implementations or their types, including through aliases and re-exports. Use the development review entry only from scenarios, fixtures and tests.");
165
+ if (edge.typeOnly) continue;
166
+ if (runtime(source.fileName) && !runtime(edge.file)) emit(edge.node, "showcase-import", "Runtime code cannot depend on review helpers, scenarios, fixtures or checks, including through re-exports. Keep demonstrations beside their owner and integration harnesses in tests.");
167
+ if (isInside(dsRoot, source.fileName) && layer(edge.file) === "application") emit(edge.node, "no-app-import", "ADS runtime cannot import application stores, routing or services, including through forwarding modules. Receive data and navigation callbacks through public APIs.");
168
+ }
169
+ if (!["components", "patterns", "review"].includes(ownLayer)) continue;
170
+ for (const initial of edges.get(source.fileName) ?? []) {
171
+ if (initial.typeOnly && ownLayer === "components") continue;
172
+ const queue = [{ edge: initial, trail: [source.fileName] }], seen = new Set();
173
+ while (queue.length) {
174
+ const { edge, trail } = queue.shift();
175
+ const key = `${edge.file}:${edge.symbol?.name ?? ""}`;
176
+ if (seen.has(key)) continue;
177
+ seen.add(key);
178
+ const dependencyLayer = layer(edge.file), chain = [...trail, edge.file];
179
+ const via = chain.map(file => path.relative(root, file).replaceAll("\\", "/")).join(" → ");
180
+ if (ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer)) {
181
+ emit(initial.node, "component-dependency", `Component runtime depends on ${dependencyLayer}: ${via}. Move the arrangement to a pattern or extract a lower-level component/controller; do not change a valid API just to satisfy classification.`);
182
+ break;
183
+ }
184
+ if (["patterns", "review"].includes(ownLayer) && dependencyLayer === "components") {
185
+ if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "component-contract", `Pattern accesses private component ${edge.symbol?.name ?? "module"}: ${via}. Export an intentional public contract or keep the helper/state/style inside its component; importing a module directly is allowed when the selected symbol is public.`);
186
+ continue; // Public component implementations may use their own private files.
187
+ }
188
+ if (ownLayer === "review" && ["patterns", "controllers"].includes(dependencyLayer)) {
189
+ if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "review-contract", `Review accesses private ${dependencyLayer} ${edge.symbol?.name ?? "module"}: ${via}. Demonstrate the production API instead of depending on private implementation details.`);
190
+ continue;
191
+ }
192
+ if (dependencyLayer === "application" || !runtime(edge.file)) continue;
193
+ if (["patterns", "review"].includes(ownLayer) && dependencyLayer !== "shared") continue;
194
+ for (const next of edges.get(edge.file) ?? []) if (!next.typeOnly) queue.push({ edge: next, trail: chain });
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ function strings(node, seen = new Set()) {
201
+ if (!node || seen.has(node)) return [];
202
+ seen.add(node);
203
+ if (ts.isStringLiteralLike(node)) return [node.text];
204
+ if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
205
+ const symbol = unalias(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node));
206
+ return (symbol?.declarations ?? []).flatMap(declaration => ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration) ? strings(declaration.initializer, seen) : []);
207
+ }
208
+ if (ts.isTaggedTemplateExpression(node)) return [];
209
+ const found = [];
210
+ ts.forEachChild(node, child => { found.push(...strings(child, seen)); });
211
+ return found;
212
+ }
213
+ function own(map, key, file) {
214
+ if (!map.has(key)) map.set(key, new Set());
215
+ map.get(key).add(file);
216
+ }
217
+ for (const symbol of publicSymbols) for (const node of symbol?.declarations ?? []) if (ts.isVariableDeclaration(node)) for (const value of strings(node.initializer)) publicHooks.add(value);
218
+ for (const source of sources.filter(source => runtime(source.fileName))) {
219
+ function visit(node) {
220
+ let name, value;
221
+ if (["components", "patterns"].includes(layer(source.fileName))) {
222
+ const write = styleAccessProperty(node);
223
+ if (typeof write === "string" && write.startsWith("--")) (layer(source.fileName) === "components" ? componentVariables : patternVariables).add(write);
224
+ }
225
+ if (ts.isJsxAttribute(node)) { name = node.name.getText(); value = node.initializer; }
226
+ if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name))) { name = node.name.text; value = node.initializer; }
227
+ if (name === "className" || name === "class") for (const text of strings(value)) for (const token of text.split(/\s+/).filter(Boolean)) own(classOwners, token, source.fileName);
228
+ if (name?.startsWith("data-")) for (const text of strings(value)) own(attributeOwners, `${name}=${text}`, source.fileName);
229
+ ts.forEachChild(node, visit);
230
+ }
231
+ visit(source);
232
+ }
233
+ function privateHook(map, key, kinds = ["components"]) {
234
+ const owners = map.get(key);
235
+ return !publicHooks.has(key) && owners?.size && (map !== attributeOwners || owners.size === 1) && [...owners].every(file => kinds.includes(layer(file)));
236
+ }
237
+ // Called first for all component styles, then for patterns by checkProject.
238
+ function collectComponentStyles(file, css) {
239
+ if (runtime(file) && ["components", "patterns"].includes(layer(file))) css.walkDecls(declaration => {
240
+ if (declaration.prop.startsWith("--")) (layer(file) === "components" ? componentVariables : patternVariables).add(declaration.prop);
241
+ });
242
+ }
243
+ function inspectStyles(file, css, offset = { line: 1 }) {
244
+ if (!runtime(file) && !isReviewFile(file, options)) return;
245
+ const ownLayer = layer(file);
246
+ css.walkAtRules("import", rule => {
247
+ const first = valueParser(rule.params).nodes.find(node => node.type !== "space" && node.type !== "comment");
248
+ const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
249
+ if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
250
+ for (const dependency of resolve(specifier, file)) {
251
+ const dependencyLayer = layer(dependency);
252
+ const boundary = ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer) ? "component-dependency"
253
+ : ["patterns", "review"].includes(ownLayer) && dependencyLayer === "components" ? "component-contract" : null;
254
+ if (boundary) report(file, boundary, `Stylesheet import crosses the ${ownLayer} → ${dependencyLayer} boundary: ${specifier}. Consume public component contracts instead of importing private styles. ${guidance}`, offset.line + (rule.source?.start.line ?? 1) - 1, rule.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
255
+ }
256
+ });
257
+ if (!["patterns", "review"].includes(ownLayer)) return;
258
+ const failures = new Set();
259
+ const styleReport = (node, message) => {
260
+ const line = offset.line + (node.source?.start.line ?? 1) - 1;
261
+ const key = `${line}:${message}`;
262
+ if (failures.has(key)) return;
263
+ failures.add(key);
264
+ report(file, "component-style", `${message} Use component props, public class helpers or registered non-state customization variables; patterns own arrangement and spacing. ${guidance}`, line, node.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
265
+ };
266
+ css.walkRules(rule => {
267
+ try {
268
+ selectorParser(selectors => {
269
+ function subject(selector) {
270
+ const nodes = selector.nodes ?? [], lastCombinator = nodes.findLastIndex(node => node.type === "combinator");
271
+ for (const node of nodes.slice(lastCombinator + 1)) {
272
+ if (node.type === "class" && privateHook(classOwners, node.value, ownLayer === "review" ? ["components", "patterns"] : undefined)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} class .${node.value}.`);
273
+ if (node.type === "attribute" && node.operator === "=" && privateHook(attributeOwners, `${node.attribute}=${node.value}`, ownLayer === "review" ? ["components", "patterns"] : undefined) && !publicHooks.has(node.value)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} marker [${node.attribute}="${node.value}"].`);
274
+ if (node.type === "pseudo" && [":is", ":where"].includes(node.value)) for (const branch of node.nodes ?? []) subject(branch);
275
+ }
276
+ }
277
+ for (const selector of selectors.nodes) subject(selector);
278
+ }).processSync(rule.selector);
279
+ } catch { /* CSS syntax diagnostics are owned by the existing parser/build. */ }
280
+ });
281
+ css.walkDecls(declaration => valueParser(declaration.value).walk(node => {
282
+ if (node.type !== "function" || node.value !== "var") return;
283
+ const name = node.nodes.find(child => child.type === "word")?.value;
284
+ if ((componentVariables.has(name) || ownLayer === "review" && patternVariables.has(name) && !publicHooks.has(name)) && (isStateVariable(name) || !config.boundaries.componentVariables.includes(name))) styleReport(declaration, `${ownLayer === "review" ? "Review reads private product" : "Pattern reads private component"} variable ${name}.`);
285
+ }));
286
+ }
287
+ return { inspectDependencies, runtimeHelpers, collectComponentStyles, inspectStyles };
288
+ }
@@ -13,7 +13,7 @@ export function createLintConfig({ root, config }) {
13
13
  if (!existsSync(specifier)) throw new Error(`Project plugin not found: ${plugin.path}`);
14
14
  return { name: plugin.name, specifier };
15
15
  });
16
- const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
16
+ const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, publicEntry: config.publicEntry, reviewEntry: config.reviewEntry, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
17
17
  configurationFiles: [...configurationFiles({ root, config, sourceRoot: path.resolve(root,config.sourceDir), dsRoot: path.resolve(root,config.dsDir) })].sort() };
18
18
  return {
19
19
  jsPlugins: [{ name: "ads", specifier: fileURLToPath(new URL("./oxlint.mjs", import.meta.url)) }, ...projectPlugins],
@@ -24,6 +24,7 @@ export function projectContext(options) {
24
24
  const guides = [...projectGuideFiles(root), path.join(config.dsDir, "README.md"), path.join(config.dsDir, "readme.md"), path.join(config.dsDir, "SKILL.md")].filter((file) => existsSync(path.join(root, file))).map((file) => ({ file: file.replaceAll("\\", "/"), uri: sourceUri(file.replaceAll("\\", "/")) }));
25
25
  return { name: pkg.name ?? path.basename(root), projectRoot: root, dsRoot, sourceRoot, framework: config.framework,
26
26
  publicEntry: path.join(dsRoot, config.publicEntry), publicEntryUri: existsSync(path.join(dsRoot, config.publicEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.publicEntry)).replaceAll("\\", "/")) : null,
27
+ review: { importFrom: `${config.publicAlias}/review`, entry: path.join(dsRoot, config.reviewEntry), sourceUri: existsSync(path.join(dsRoot, config.reviewEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.reviewEntry)).replaceAll("\\", "/")) : null, developmentOnly: true },
27
28
  stylesEntry: path.join(dsRoot, config.stylesEntry), packageManager, commands, guides,
28
29
  showcase: { client: "tool-owned-mcp", url: config.mcp.showcaseUrl, title: config.mcp.showcaseTitle ?? `${pkg.name ?? path.basename(root)} · Design System` },
29
30
  runtime: { node: process.execPath, nodeBinDir: path.dirname(process.execPath), nodeVersion: process.version,
@@ -56,7 +57,7 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
56
57
  3. Select one workflow with ads_skills, then ads_read_skill. ${route.skills.map((skill) => `${skill.task}: ${skill.name}`).join("; ")}. Native skill activation is host-controlled. Relative skill references resolve from its SKILL.md URI. Read support files only when relevant.
57
58
  4. ${route.preview} Token samples: ads_scenarios(kind: tokens), then ads_show_scenario or ads_show_tokens(category: id). Token source inspection: ads_tokens with a focused query. Registered previews: ${previews.map((preview) => `${preview.id}: ${preview.tool ?? "ads_show_scenario"}`).join(", ") || "none"}. Showcase URL: ${context.showcase.url ?? "not configured; use the project's showcase command"}.
58
59
  Inspection connects a public export to its owning source, available guides, token values and discovered preview IDs. Bounded tools keep discovery focused; explicit resource reads supply detail. Source metadata does not prove layout, interaction or motion. A scenario renders the real public implementation and its fixture context in either review surface.
59
- 5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
60
+ 5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for guarded owners, aliases and migration. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
60
61
  6. ${route.validation} Registered commands: ${context.commands.map((command) => command.args?.join(" ") ?? `choose the project package manager for ${command.script}`).join("; ") || "none discovered; inspect the project's build setup"}. Run them from ${context.projectRoot}.
61
62
  Execution replies from ads_verify and guarded edits contain counts, acceptance and resultId, not full reports. Read only needed outcomes/diagnostics/captures with ads_results, following nextCursor with identical filters. A truncated row has recordIndex for bounded text ranges. For one failure, use ads_investigate with its unit/check/runId; read request/error/artifact sections only when needed. Only the latest verification and mutation results, and latest investigation per check, are available. Notifications do not establish freshness.
62
63
 
@@ -30,9 +30,9 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
30
30
  const owner = inventory.units.find(entry => entry.id === unit);
31
31
  const files = [...new Set([...(owner?.files ?? []), ...(owner?.companionFiles ?? []).map(entry => entry.path),
32
32
  ...(!owner ? ["ds.config.json"] : []),
33
- ...(/^(?:components|patterns|controllers)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,options.config.publicEntry)).replaceAll("\\","/")] : [])])];
34
- if (!owner && !/^(?:components|patterns|controllers|composition)\/.+/.test(unit)) throw new Error("Discover an existing unit or choose a new unoccupied component/pattern/controller/composition owner.");
35
- if (/^(?:components|patterns|controllers|composition)\//.test(unit)) for (let directory=unit;directory!==".";directory=path.posix.dirname(directory)) {
33
+ ...(/^(?:components|patterns|controllers|review)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,unit.startsWith("review/")?options.config.reviewEntry:options.config.publicEntry)).replaceAll("\\","/")] : [])])];
34
+ if (!owner && !/^(?:components|patterns|controllers|composition|review)\/.+/.test(unit)) throw new Error("Discover an existing unit or choose a new unoccupied component/pattern/controller/composition/review owner.");
35
+ if (/^(?:components|patterns|controllers|composition|review)\//.test(unit)) for (let directory=unit;directory!==".";directory=path.posix.dirname(directory)) {
36
36
  const marker=path.join(options.dsRoot,directory,".gitkeep");
37
37
  if (existsSync(marker) && realpathSync(marker)===marker && lstatSync(marker).isFile() && lstatSync(marker).size===0) {
38
38
  const relative=path.relative(options.root,marker).replaceAll("\\","/");
@@ -178,7 +178,7 @@ export function createMutationController(directory, { captures = createCaptureCo
178
178
  if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > mutationLimits.readChars) throw fail("mutation-range", "Use a non-negative character offset and a limit between 1 and 12000.", file);
179
179
  const options = loadConfig(root), source = read(options, file);
180
180
  const inventory = inspectUnits(options), unit = inventory.owner(inventory.relative(file));
181
- if (!unit && inventory.relative(file) !== inventory.publicEntry) throw fail("mutation-unit", "This file is outside the project's ADS and registered configuration/extension units.", file);
181
+ if (!unit && inventory.relative(file) !== inventory.publicEntry && inventory.relative(file) !== inventory.reviewEntry) throw fail("mutation-unit", "This file is outside the project's ADS and registered configuration/extension units.", file);
182
182
  if (!source.exists) return { ...source, offset: 0, nextOffset: null, totalChars: 0, startLine: 1 };
183
183
  if (source.binary) return { ...source, unit, content: null, note: "Binary asset: inspect its metadata or preview; edit using a Git binary patch, optionally through patchFile." };
184
184
  if (offset > source.content.length) throw fail("mutation-range", "Read offset exceeds the current file length.", 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-migrate; 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. 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.
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 development-only review/ helper, exported through the separate review entry rather than the production barrel; 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.
@@ -117,7 +117,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
117
117
  function resources() {
118
118
  const context = project();
119
119
  const documents = getDocuments();
120
- const promoted = new Set([context.publicEntryUri, ...context.guides.map((guide) => guide.uri)]);
120
+ const promoted = new Set([context.publicEntryUri, context.review.sourceUri, ...context.guides.map((guide) => guide.uri)]);
121
121
  const skillEntries = new Set(skills.skills.map((skill) => skill.uri));
122
122
  return [
123
123
  { uri: GUIDE_URI, name: "Start here: design-system workflow", description: "Read first for UI/ADS tasks, especially when only resource access is exposed. Explains the exact workspace, tool sequence, skill selection, previews, project commands and fallback.", mimeType: "text/markdown" },
@@ -125,6 +125,8 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
125
125
  { uri: "ads://guides/evidence", name: "Failure boundaries and compact investigations", mimeType: "text/markdown" },
126
126
  { uri: "ads://guides/atomic-styles", name: "Atomic styles: composition conflicts and custom-property extraction", mimeType: "text/markdown" },
127
127
  { uri: "ads://guides/scenarios", name: "Scenario coverage and public class-helper contracts", mimeType: "text/markdown" },
128
+ { uri: "ads://guides/layers", name: "Component capabilities, pattern composition and dependency ownership", mimeType: "text/markdown" },
129
+ { uri: "ads://guides/review", name: "Development-only review UI, token renderers and migration", mimeType: "text/markdown" },
128
130
  { uri: "ads://guides/captures", name: "On-demand captures and candidate visual review", mimeType: "text/markdown" },
129
131
  { uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
130
132
  { uri: BUILD_URI, name: "Preview build status and diagnostics", mimeType: "application/json" },
@@ -170,6 +172,8 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
170
172
  if (uri === "ads://guides/evidence") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/evidence.md", import.meta.url), "utf8") }] };
171
173
  if (uri === "ads://guides/atomic-styles") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/atomic-styles.md", import.meta.url), "utf8") }] };
172
174
  if (uri === "ads://guides/scenarios") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/scenarios.md", import.meta.url), "utf8") }] };
175
+ if (uri === "ads://guides/layers") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/layers.md", import.meta.url), "utf8") }] };
176
+ if (uri === "ads://guides/review") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/review.md", import.meta.url), "utf8") }] };
173
177
  if (uri === "ads://guides/captures") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/captures.md", import.meta.url), "utf8") }] };
174
178
  if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
175
179
  let value;
@@ -309,7 +313,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
309
313
  const result = await verification.run(args, signal, progress);
310
314
  return finishExecution("verification", {unit:args.unit,...result});
311
315
  } },
312
- { name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation including shared credits/license notices; 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 filePolicy for supported new documentation paths and 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 }) => {
316
+ { name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; development review/ helpers with a separate export entry; composition; tokens; foundation shared files; documentation including shared credits/license notices; 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 filePolicy for supported new documentation paths and 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 }) => {
313
317
  const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
314
318
  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 } : {}), ...(item.filePolicy ? { filePolicy: item.filePolicy } : {}) } : {}) })) });
315
319
  } },
@@ -138,7 +138,7 @@ Boolean requesting whether a visible border and background is provided by the ho
138
138
  - omitted: host decides border`)}),q_=i.object({method:i.literal("ui/request-display-mode"),params:i.object({mode:Sa.describe("The display mode being requested.")})}),N_=i.object({mode:Sa.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),wR=i.union([i.literal("model"),i.literal("app")]).describe("Tool visibility scope - who can access the tool."),KT=i.object({resourceUri:i.string().optional(),visibility:i.array(wR).optional().describe(`Who can access this tool. Default: ["model", "app"]
139
139
  - "model": Tool visible to and callable by the agent
140
140
  - "app": Tool callable by the app from this server only`),csp:i.never().optional(),permissions:i.never().optional()}),GT=i.object({mimeTypes:i.array(i.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')}),xR=i.object({method:i.literal("ui/download-file"),params:i.object({contents:i.array(i.union([ht,mt])).describe("Resource contents to download \u2014 embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})}),TR=i.object({method:i.literal("ui/message"),params:i.object({role:i.literal("user").describe('Message role, currently only "user" is supported.'),content:i.array(Ee).describe("Message content blocks (text, image, etc.).")})}),YT=i.object({method:i.literal("ui/notifications/sandbox-resource-ready"),params:i.object({html:i.string().describe("HTML content to load into the inner iframe."),sandbox:i.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:Ef.optional().describe("CSP configuration from resource metadata."),permissions:kf.optional().describe("Sandbox permissions from resource metadata.")})}),QT=i.object({method:i.literal("ui/notifications/tool-result"),params:Ye.describe("Standard MCP tool execution result.")}),j_=i.object({toolInfo:i.object({id:Te.optional().describe("JSON-RPC id of the tools/call request."),tool:Ge.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:cR.optional().describe("Current color theme preference."),styles:_R.optional().describe("Style configuration for theming the app."),displayMode:Sa.optional().describe("How the UI is currently displayed."),availableDisplayModes:i.array(Sa).optional().describe("Display modes the host supports."),containerDimensions:i.union([i.object({height:i.number().describe("Fixed container height in pixels.")}),i.object({maxHeight:i.union([i.number(),i.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(i.union([i.object({width:i.number().describe("Fixed container width in pixels.")}),i.object({maxWidth:i.union([i.number(),i.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
141
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:i.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:i.string().optional().describe("User's timezone in IANA format."),userAgent:i.string().optional().describe("Host application identifier."),platform:i.union([i.literal("web"),i.literal("desktop"),i.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:i.object({touch:i.boolean().optional().describe("Whether the device supports touch input."),hover:i.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:i.object({top:i.number().describe("Top safe area inset in pixels."),right:i.number().describe("Right safe area inset in pixels."),bottom:i.number().describe("Bottom safe area inset in pixels."),left:i.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),XT=i.object({method:i.literal("ui/notifications/host-context-changed"),params:j_.describe("Partial context update containing only changed fields.")}),PR=i.object({method:i.literal("ui/update-model-context"),params:i.object({content:i.array(Ee).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:i.record(i.string(),i.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),ER=i.object({method:i.literal("ui/initialize"),params:i.object({appInfo:Pe.describe("App identification (name and version)."),appCapabilities:zR.describe("Features and capabilities this app provides."),protocolVersion:i.string().describe("Protocol version this app supports.")})}),kR=i.object({protocolVersion:i.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Pe.describe("Host application identification and version."),hostCapabilities:vR.describe("Features and capabilities provided by the host."),hostContext:j_.describe("Rich context about the host environment.")}).passthrough();var Ql=class{eventTarget;eventSource;messageListener;constructor(t=window.parent,o){this.eventTarget=t,this.eventSource=o,this.messageListener=r=>{if(o&&r.source!==this.eventSource){console.debug("Ignoring message from unknown source",r);return}let n=ct.safeParse(r.data);n.success?(console.debug("Parsed message",n.data),this.onmessage?.(n.data)):r.data?.jsonrpc!=="2.0"?console.debug("Ignoring non-JSON-RPC message",n.error.message,r):(console.error("Failed to parse message",n.error.message,r),this.onerror?.(Error("Invalid JSON-RPC message received: "+n.error.message)))}}async start(){window.addEventListener("message",this.messageListener)}async send(t,o){t.method!==aR&&console.debug("Sending message",t),this.eventTarget.postMessage(t,"*")}async close(){window.removeEventListener("message",this.messageListener),this.onclose?.()}onclose;onerror;onmessage;sessionId;setProtocolVersion};var $R=[A_],Xl=class extends ma{_client;_hostInfo;_hostCapabilities;_appCapabilities;_hostContext={};_appInfo;_initializedReceived=!1;_registeredEvents=new Set;_events=new Tf;_methods=new Pf;setRequestHandler=(...t)=>{let[o,...r]=O_("request",t)??t;this._methods.claim(o,"setRequestHandler"),super.setRequestHandler.call(this,o,...r)};setNotificationHandler=(...t)=>{let[o,...r]=O_("notification",t)??t;this._methods.claim(o,"setNotificationHandler"),super.setNotificationHandler.call(this,o,...r)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...o)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...o)};_wrapHandler(t,o){let r=super._wrapHandler(t,o);return t==="ui/initialize"||t==="ping"?r:async(n,s)=>(this._initializedReceived||console.warn(`[ext-apps] AppBridge received '${t}' before ui/notifications/initialized. The View is calling host methods before completing the handshake; it should await app.connect() first.`),r(n,s))}eventSchemas={sizechange:gR,sandboxready:fR,initialized:RR,requestteardown:yR,loggingmessage:bo};setEventHandler(t,o){this._ensureEventSlot(t),this._events.setHandler(t,o)}getEventHandler(t){return this._events.getHandler(t)}addEventListener(t,o){this._ensureEventSlot(t),this._events.addEventListener(t,o)}removeEventListener(t,o){this._events.removeEventListener(t,o)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let o=this.eventSchemas[t];if(!o)throw Error(`Unknown event: ${String(t)}`);let r=n=>{t==="initialized"&&(this._initializedReceived=!0),this._events.dispatch(t,n)};t==="loggingmessage"?this.setNotificationHandler("notifications/message",n=>{r(n.params)}):this.setNotificationHandler(o.shape.method.value,{params:o.shape.params},n=>r(n)),this._registeredEvents.add(t)}warnIfRequestHandlerReplaced(t,o,r){o&&r&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,o,r,n){super(n),this._client=t,this._hostInfo=o,this._hostCapabilities=r,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),this._ensureEventSlot("initialized"),this._hostContext=n?.hostContext||{},this.replaceRequestHandler("ui/initialize",{params:ER.shape.params,result:kR},s=>this._onAppsInitialize(s)),this.replaceRequestHandler("ping",(s,a)=>(this.onping?.(s.params,a),{})),this.replaceRequestHandler("ui/request-display-mode",{params:q_.shape.params,result:N_},()=>({mode:this._hostContext.displayMode??"inline"}))}buildContext(t){return t}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){}assertNotificationCapability(t){}getAppCapabilities(){return this._appCapabilities}getAppVersion(){return this._appInfo}onping;get onsizechange(){return this.getEventHandler("sizechange")}set onsizechange(t){this.setEventHandler("sizechange",t)}get onsandboxready(){return this.getEventHandler("sandboxready")}set onsandboxready(t){this.setEventHandler("sandboxready",t)}get oninitialized(){return this.getEventHandler("initialized")}set oninitialized(t){this.setEventHandler("initialized",t)}_onmessage;get onmessage(){return this._onmessage}set onmessage(t){this.warnIfRequestHandlerReplaced("onmessage",this._onmessage,t),this._onmessage=t,this.replaceRequestHandler("ui/message",{params:TR.shape.params,result:mR},async(o,r)=>{if(!this._onmessage)throw Error("No onmessage handler set");return this._onmessage(o,r)})}_onopenlink;get onopenlink(){return this._onopenlink}set onopenlink(t){this.warnIfRequestHandlerReplaced("onopenlink",this._onopenlink,t),this._onopenlink=t,this.replaceRequestHandler("ui/open-link",{params:dR.shape.params,result:pR},async(o,r)=>{if(!this._onopenlink)throw Error("No onopenlink handler set");return this._onopenlink(o,r)})}_ondownloadfile;get ondownloadfile(){return this._ondownloadfile}set ondownloadfile(t){this.warnIfRequestHandlerReplaced("ondownloadfile",this._ondownloadfile,t),this._ondownloadfile=t,this.replaceRequestHandler("ui/download-file",{params:xR.shape.params,result:hR},async(o,r)=>{if(!this._ondownloadfile)throw Error("No ondownloadfile handler set");return this._ondownloadfile(o,r)})}get onrequestteardown(){return this.getEventHandler("requestteardown")}set onrequestteardown(t){this.setEventHandler("requestteardown",t)}_onrequestdisplaymode;get onrequestdisplaymode(){return this._onrequestdisplaymode}set onrequestdisplaymode(t){this.warnIfRequestHandlerReplaced("onrequestdisplaymode",this._onrequestdisplaymode,t),this._onrequestdisplaymode=t,this.replaceRequestHandler("ui/request-display-mode",{params:q_.shape.params,result:N_},async(o,r)=>{if(!this._onrequestdisplaymode)throw Error("No onrequestdisplaymode handler set");return this._onrequestdisplaymode(o,r)})}get onloggingmessage(){return this.getEventHandler("loggingmessage")}set onloggingmessage(t){this.setEventHandler("loggingmessage",t)}_onupdatemodelcontext;get onupdatemodelcontext(){return this._onupdatemodelcontext}set onupdatemodelcontext(t){this.warnIfRequestHandlerReplaced("onupdatemodelcontext",this._onupdatemodelcontext,t),this._onupdatemodelcontext=t,this.replaceRequestHandler("ui/update-model-context",{params:PR.shape.params,result:We},async(o,r)=>{if(!this._onupdatemodelcontext)throw Error("No onupdatemodelcontext handler set");return this._onupdatemodelcontext(o,r)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",async(o,r)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,r)})}_oncreatesamplingmessage;get oncreatesamplingmessage(){return this._oncreatesamplingmessage}set oncreatesamplingmessage(t){this.warnIfRequestHandlerReplaced("oncreatesamplingmessage",this._oncreatesamplingmessage,t),this._oncreatesamplingmessage=t,this.replaceRequestHandler("sampling/createMessage",async(o,r)=>{if(!this._oncreatesamplingmessage)throw Error("No oncreatesamplingmessage handler set");return this._oncreatesamplingmessage(o.params,r)})}sendToolListChanged(t={}){return this.notification({method:"notifications/tools/list_changed",params:t})}_onlistresources;get onlistresources(){return this._onlistresources}set onlistresources(t){this.warnIfRequestHandlerReplaced("onlistresources",this._onlistresources,t),this._onlistresources=t,this.replaceRequestHandler("resources/list",async(o,r)=>{if(!this._onlistresources)throw Error("No onlistresources handler set");return this._onlistresources(o.params,r)})}_onlistresourcetemplates;get onlistresourcetemplates(){return this._onlistresourcetemplates}set onlistresourcetemplates(t){this.warnIfRequestHandlerReplaced("onlistresourcetemplates",this._onlistresourcetemplates,t),this._onlistresourcetemplates=t,this.replaceRequestHandler("resources/templates/list",async(o,r)=>{if(!this._onlistresourcetemplates)throw Error("No onlistresourcetemplates handler set");return this._onlistresourcetemplates(o.params,r)})}_onreadresource;get onreadresource(){return this._onreadresource}set onreadresource(t){this.warnIfRequestHandlerReplaced("onreadresource",this._onreadresource,t),this._onreadresource=t,this.replaceRequestHandler("resources/read",async(o,r)=>{if(!this._onreadresource)throw Error("No onreadresource handler set");return this._onreadresource(o.params,r)})}sendResourceListChanged(t={}){return this.notification({method:"notifications/resources/list_changed",params:t})}_onlistprompts;get onlistprompts(){return this._onlistprompts}set onlistprompts(t){this.warnIfRequestHandlerReplaced("onlistprompts",this._onlistprompts,t),this._onlistprompts=t,this.replaceRequestHandler("prompts/list",async(o,r)=>{if(!this._onlistprompts)throw Error("No onlistprompts handler set");return this._onlistprompts(o.params,r)})}sendPromptListChanged(t={}){return this.notification({method:"notifications/prompts/list_changed",params:t})}getCapabilities(){return this._hostCapabilities}async _onAppsInitialize(t){let o=t.protocolVersion;return this._appInfo!==void 0&&console.warn("[ext-apps] AppBridge received a second ui/initialize. The View may be double-mounting (e.g. React StrictMode in dev) without closing the previous App instance. Responding normally; the latest appInfo/appCapabilities replace the previous values."),this._appCapabilities=t.appCapabilities,this._appInfo=t.appInfo,{protocolVersion:$R.includes(o)?o:A_,hostCapabilities:this.getCapabilities(),hostInfo:this._hostInfo,hostContext:this._hostContext}}setHostContext(t){let o={},r=!1;for(let n of Object.keys(t)){let s=this._hostContext[n],a=t[n];CR(s,a)||(o[n]=a,r=!0)}r&&(this._hostContext=t,this.sendHostContextChange(o))}sendHostContextChange(t){return this.notification({method:"ui/notifications/host-context-changed",params:t})}sendToolInput(t){return this.notification({method:"ui/notifications/tool-input",params:t})}sendToolInputPartial(t){return this.notification({method:"ui/notifications/tool-input-partial",params:t})}sendToolResult(t){return this.notification({method:"ui/notifications/tool-result",params:t})}sendToolCancelled(t){return this.notification({method:"ui/notifications/tool-cancelled",params:t})}sendSandboxResourceReady(t){return this.notification({method:"ui/notifications/sandbox-resource-ready",params:t})}teardownResource(t,o){return this.request({method:"ui/resource-teardown",params:t},bR,o)}sendResourceTeardown=this.teardownResource;callTool(t,o){return this.request({method:"tools/call",params:t},o)}listTools(t,o){return this.request({method:"tools/list",params:t},o)}async connect(t){if(this.transport)throw Error("AppBridge is already connected. Call close() before connecting again.");if(this._initializedReceived=!1,this._client){let o=this._client.getServerCapabilities();if(!o)throw Error("Client server capabilities not available");o.tools&&(this.oncalltool=async(r,n)=>this._client.request({method:"tools/call",params:r},{signal:n.mcpReq.signal}),o.tools.listChanged&&this._client.setNotificationHandler("notifications/tools/list_changed",r=>this.sendToolListChanged(r.params))),o.resources&&(this.onlistresources=async(r,n)=>this._client.request({method:"resources/list",params:r},{signal:n.mcpReq.signal}),this.onlistresourcetemplates=async(r,n)=>this._client.request({method:"resources/templates/list",params:r},{signal:n.mcpReq.signal}),this.onreadresource=async(r,n)=>this._client.request({method:"resources/read",params:r},{signal:n.mcpReq.signal}),o.resources.listChanged&&this._client.setNotificationHandler("notifications/resources/list_changed",r=>this.sendResourceListChanged(r.params))),o.prompts&&(this.onlistprompts=async(r,n)=>this._client.request({method:"prompts/list",params:r},{signal:n.mcpReq.signal}),o.prompts.listChanged&&this._client.setNotificationHandler("notifications/prompts/list_changed",r=>this.sendPromptListChanged(r.params)))}return super.connect(t)}};function CR(e,t){return JSON.stringify(e)===JSON.stringify(t)}function L_(e,t){let o=new DOMParser().parseFromString(e,"text/html"),r=o.createElement("meta");if(r.httpEquiv="Content-Security-Policy",r.content=t,o.head.prepend(r),!o.querySelector("base[href]")){let n=o.querySelector("base")??o.createElement("base");n.setAttribute("href","about:srcdoc"),r.after(n)}return"<!doctype html>"+o.documentElement.outerHTML}var $e=new Yl({name:"ADS preview",version:"1.0.0"},{},{autoResize:!0}),Wr=document.getElementById("ads-token-root"),Lt=document.getElementById("ads-widget-status"),tu=document.getElementById("ads-token-theme"),eu=document.getElementById("ads-scenario-root"),To,Me=0,U_="",Br=30,Z_=document.createElement("style");Wr&&document.head.append(Z_);function ou(e){e!=="light"&&e!=="dark"||(document.documentElement.dataset.theme=e,tu&&(tu.value=e),Wr&&To&&wo())}function Oe(e,t){let o=document.createElement(e);return o.textContent=t,o}function IR(e){let t=Oe("li","");t.dataset.token=e.name;let o=getComputedStyle(document.documentElement).getPropertyValue(e.name).trim(),r=Oe("details","");r.className="token-source";let n=Oe("summary","");n.title=`${e.name}: ${o||"Conditional or unset"}. Click to inspect source definitions.`;let s=Oe("code",e.name);s.className="token-name";let a=Oe("span",o||"Conditional or unset");n.append(s,a);let l=Oe("dl","");l.append(Oe("dt","Computed in preview"),Oe("dd",o||"Conditional or unset")),e.meaning&&l.append(Oe("dt","Meaning"),Oe("dd",e.meaning));for(let u of e.definitions)l.append(Oe("dt",u.scope),Oe("dd",`${u.value} \xB7 ${u.file}:${u.line}`));return r.append(n,l),t.append(r),t}function wo(){Z_.textContent=To.css;let e=To.tokens.filter(t=>`${t.name} ${t.type??""}`.toLowerCase().includes(U_.toLowerCase()));document.getElementById("ads-token-list").replaceChildren(...e.slice(Me,Me+Br).map(IR)),document.querySelector("footer").hidden=e.length<=Br,document.getElementById("ads-token-count").textContent=e.length?`${Me+1}\u2013${Math.min(Me+Br,e.length)} of ${e.length}`:"No matching tokens",document.getElementById("ads-token-prev").disabled=Me===0,document.getElementById("ads-token-next").disabled=Me+Br>=e.length,Lt.textContent=`${e.length} matching definitions \xB7 ${To.diagnostics.length} diagnostics`}Wr&&($e.ontoolinput=()=>{Me=0,To&&wo()},To=JSON.parse(document.getElementById("ads-token-seed").textContent),ou("light"),document.getElementById("ads-token-search").addEventListener("input",e=>{U_=e.target.value,Me=0,wo()}),tu.addEventListener("change",()=>ou(tu.value)),document.getElementById("ads-token-prev").addEventListener("click",()=>{Me=Math.max(0,Me-Br),wo()}),document.getElementById("ads-token-next").addEventListener("click",()=>{Me+=Br,wo()}),document.getElementById("ads-token-refresh").addEventListener("click",async()=>{try{let e=await $e.readServerResource({uri:"ads://project/tokens"});To=JSON.parse(e.contents.find(t=>"text"in t).text),Me=0,wo()}catch(e){Lt.textContent=`Refresh failed: ${e.message}`}}));var xo,$f,D_=!1,_a=0;async function H_(e){if($f=e,!D_||!eu)return;let t=e?.structuredContent?.scenario?.resourceUri;if(!t||!t.startsWith("ui://ads/previews/"))return;let o=++_a;Wr&&(Wr.hidden=!0,Lt=document.getElementById("ads-scenario-status")),eu.hidden=!1;try{if(!$e.getHostCapabilities()?.serverResources)throw new Error("This Apps host does not forward server resources. Use the project showcase URL from the tool result.");let n=(await $e.readServerResource({uri:t})).contents.find(u=>u.mimeType==="text/html;profile=mcp-app"&&typeof u.text=="string");if(!n)throw new Error("Scenario HTML is unavailable.");if(o!==_a)return;xo&&(await xo.close(),xo=void 0);let s=document.createElement("iframe");s.title=e.structuredContent.scenario.title,s.setAttribute("sandbox","allow-scripts"),eu.replaceChildren(s);let a=new Xl(null,{name:"ADS scenario host",version:"1.0.0"},{serverResources:{},serverTools:{}},{hostContext:$e.getHostContext()});xo=a,a.onreadresource=u=>$e.readServerResource(u),a.oncalltool=u=>$e.callServerTool(u),a.onsizechange=({height:u})=>{u&&(s.height=String(Math.min(1e4,Math.max(120,u))))},a.oninitialized=async()=>{await a.sendToolInput({arguments:{}}),await a.sendToolResult(e),o===_a&&window.parent.postMessage({type:"ads-preview-ready"},"*")},await a.connect(new Ql(s.contentWindow,s.contentWindow));let l="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; worker-src blob: data:; connect-src 'none'; base-uri about:; object-src 'none'";s.srcdoc=L_(n.text,l),Lt.hidden=!0}catch(r){o===_a&&(Lt.hidden=!1,Lt.textContent=r.message,window.parent.postMessage({type:"ads-preview-ready"},"*"))}}eu&&($e.ontoolresult=e=>{H_(e)},$e.onteardown=async()=>{++_a;let e=xo;xo=void 0;try{await e?.teardownResource({},{timeout:500})}catch{}return await e?.close(),{}});$e.onhostcontextchanged=e=>{ou(e.theme),xo?.setHostContext(e)};try{await $e.connect(),D_=!0,$f&&H_($f),ou($e.getHostContext()?.theme),Wr&&(document.getElementById("ads-token-refresh").disabled=!$e.getHostCapabilities()?.serverResources,wo())}catch{Lt&&(Lt.textContent="Apps bridge unavailable. Samples remain usable; inspect project data through ADS tools/resources.")}
141
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:i.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:i.string().optional().describe("User's timezone in IANA format."),userAgent:i.string().optional().describe("Host application identifier."),platform:i.union([i.literal("web"),i.literal("desktop"),i.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:i.object({touch:i.boolean().optional().describe("Whether the device supports touch input."),hover:i.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:i.object({top:i.number().describe("Top safe area inset in pixels."),right:i.number().describe("Right safe area inset in pixels."),bottom:i.number().describe("Bottom safe area inset in pixels."),left:i.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),XT=i.object({method:i.literal("ui/notifications/host-context-changed"),params:j_.describe("Partial context update containing only changed fields.")}),PR=i.object({method:i.literal("ui/update-model-context"),params:i.object({content:i.array(Ee).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:i.record(i.string(),i.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),ER=i.object({method:i.literal("ui/initialize"),params:i.object({appInfo:Pe.describe("App identification (name and version)."),appCapabilities:zR.describe("Features and capabilities this app provides."),protocolVersion:i.string().describe("Protocol version this app supports.")})}),kR=i.object({protocolVersion:i.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Pe.describe("Host application identification and version."),hostCapabilities:vR.describe("Features and capabilities provided by the host."),hostContext:j_.describe("Rich context about the host environment.")}).passthrough();var Ql=class{eventTarget;eventSource;messageListener;constructor(t=window.parent,o){this.eventTarget=t,this.eventSource=o,this.messageListener=r=>{if(o&&r.source!==this.eventSource){console.debug("Ignoring message from unknown source",r);return}let n=ct.safeParse(r.data);n.success?(console.debug("Parsed message",n.data),this.onmessage?.(n.data)):r.data?.jsonrpc!=="2.0"?console.debug("Ignoring non-JSON-RPC message",n.error.message,r):(console.error("Failed to parse message",n.error.message,r),this.onerror?.(Error("Invalid JSON-RPC message received: "+n.error.message)))}}async start(){window.addEventListener("message",this.messageListener)}async send(t,o){t.method!==aR&&console.debug("Sending message",t),this.eventTarget.postMessage(t,"*")}async close(){window.removeEventListener("message",this.messageListener),this.onclose?.()}onclose;onerror;onmessage;sessionId;setProtocolVersion};var $R=[A_],Xl=class extends ma{_client;_hostInfo;_hostCapabilities;_appCapabilities;_hostContext={};_appInfo;_initializedReceived=!1;_registeredEvents=new Set;_events=new Tf;_methods=new Pf;setRequestHandler=(...t)=>{let[o,...r]=O_("request",t)??t;this._methods.claim(o,"setRequestHandler"),super.setRequestHandler.call(this,o,...r)};setNotificationHandler=(...t)=>{let[o,...r]=O_("notification",t)??t;this._methods.claim(o,"setNotificationHandler"),super.setNotificationHandler.call(this,o,...r)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...o)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...o)};_wrapHandler(t,o){let r=super._wrapHandler(t,o);return t==="ui/initialize"||t==="ping"?r:async(n,s)=>(this._initializedReceived||console.warn(`[ext-apps] AppBridge received '${t}' before ui/notifications/initialized. The View is calling host methods before completing the handshake; it should await app.connect() first.`),r(n,s))}eventSchemas={sizechange:gR,sandboxready:fR,initialized:RR,requestteardown:yR,loggingmessage:bo};setEventHandler(t,o){this._ensureEventSlot(t),this._events.setHandler(t,o)}getEventHandler(t){return this._events.getHandler(t)}addEventListener(t,o){this._ensureEventSlot(t),this._events.addEventListener(t,o)}removeEventListener(t,o){this._events.removeEventListener(t,o)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let o=this.eventSchemas[t];if(!o)throw Error(`Unknown event: ${String(t)}`);let r=n=>{t==="initialized"&&(this._initializedReceived=!0),this._events.dispatch(t,n)};t==="loggingmessage"?this.setNotificationHandler("notifications/message",n=>{r(n.params)}):this.setNotificationHandler(o.shape.method.value,{params:o.shape.params},n=>r(n)),this._registeredEvents.add(t)}warnIfRequestHandlerReplaced(t,o,r){o&&r&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,o,r,n){super(n),this._client=t,this._hostInfo=o,this._hostCapabilities=r,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),this._ensureEventSlot("initialized"),this._hostContext=n?.hostContext||{},this.replaceRequestHandler("ui/initialize",{params:ER.shape.params,result:kR},s=>this._onAppsInitialize(s)),this.replaceRequestHandler("ping",(s,a)=>(this.onping?.(s.params,a),{})),this.replaceRequestHandler("ui/request-display-mode",{params:q_.shape.params,result:N_},()=>({mode:this._hostContext.displayMode??"inline"}))}buildContext(t){return t}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){}assertNotificationCapability(t){}getAppCapabilities(){return this._appCapabilities}getAppVersion(){return this._appInfo}onping;get onsizechange(){return this.getEventHandler("sizechange")}set onsizechange(t){this.setEventHandler("sizechange",t)}get onsandboxready(){return this.getEventHandler("sandboxready")}set onsandboxready(t){this.setEventHandler("sandboxready",t)}get oninitialized(){return this.getEventHandler("initialized")}set oninitialized(t){this.setEventHandler("initialized",t)}_onmessage;get onmessage(){return this._onmessage}set onmessage(t){this.warnIfRequestHandlerReplaced("onmessage",this._onmessage,t),this._onmessage=t,this.replaceRequestHandler("ui/message",{params:TR.shape.params,result:mR},async(o,r)=>{if(!this._onmessage)throw Error("No onmessage handler set");return this._onmessage(o,r)})}_onopenlink;get onopenlink(){return this._onopenlink}set onopenlink(t){this.warnIfRequestHandlerReplaced("onopenlink",this._onopenlink,t),this._onopenlink=t,this.replaceRequestHandler("ui/open-link",{params:dR.shape.params,result:pR},async(o,r)=>{if(!this._onopenlink)throw Error("No onopenlink handler set");return this._onopenlink(o,r)})}_ondownloadfile;get ondownloadfile(){return this._ondownloadfile}set ondownloadfile(t){this.warnIfRequestHandlerReplaced("ondownloadfile",this._ondownloadfile,t),this._ondownloadfile=t,this.replaceRequestHandler("ui/download-file",{params:xR.shape.params,result:hR},async(o,r)=>{if(!this._ondownloadfile)throw Error("No ondownloadfile handler set");return this._ondownloadfile(o,r)})}get onrequestteardown(){return this.getEventHandler("requestteardown")}set onrequestteardown(t){this.setEventHandler("requestteardown",t)}_onrequestdisplaymode;get onrequestdisplaymode(){return this._onrequestdisplaymode}set onrequestdisplaymode(t){this.warnIfRequestHandlerReplaced("onrequestdisplaymode",this._onrequestdisplaymode,t),this._onrequestdisplaymode=t,this.replaceRequestHandler("ui/request-display-mode",{params:q_.shape.params,result:N_},async(o,r)=>{if(!this._onrequestdisplaymode)throw Error("No onrequestdisplaymode handler set");return this._onrequestdisplaymode(o,r)})}get onloggingmessage(){return this.getEventHandler("loggingmessage")}set onloggingmessage(t){this.setEventHandler("loggingmessage",t)}_onupdatemodelcontext;get onupdatemodelcontext(){return this._onupdatemodelcontext}set onupdatemodelcontext(t){this.warnIfRequestHandlerReplaced("onupdatemodelcontext",this._onupdatemodelcontext,t),this._onupdatemodelcontext=t,this.replaceRequestHandler("ui/update-model-context",{params:PR.shape.params,result:We},async(o,r)=>{if(!this._onupdatemodelcontext)throw Error("No onupdatemodelcontext handler set");return this._onupdatemodelcontext(o,r)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",async(o,r)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,r)})}_oncreatesamplingmessage;get oncreatesamplingmessage(){return this._oncreatesamplingmessage}set oncreatesamplingmessage(t){this.warnIfRequestHandlerReplaced("oncreatesamplingmessage",this._oncreatesamplingmessage,t),this._oncreatesamplingmessage=t,this.replaceRequestHandler("sampling/createMessage",async(o,r)=>{if(!this._oncreatesamplingmessage)throw Error("No oncreatesamplingmessage handler set");return this._oncreatesamplingmessage(o.params,r)})}sendToolListChanged(t={}){return this.notification({method:"notifications/tools/list_changed",params:t})}_onlistresources;get onlistresources(){return this._onlistresources}set onlistresources(t){this.warnIfRequestHandlerReplaced("onlistresources",this._onlistresources,t),this._onlistresources=t,this.replaceRequestHandler("resources/list",async(o,r)=>{if(!this._onlistresources)throw Error("No onlistresources handler set");return this._onlistresources(o.params,r)})}_onlistresourcetemplates;get onlistresourcetemplates(){return this._onlistresourcetemplates}set onlistresourcetemplates(t){this.warnIfRequestHandlerReplaced("onlistresourcetemplates",this._onlistresourcetemplates,t),this._onlistresourcetemplates=t,this.replaceRequestHandler("resources/templates/list",async(o,r)=>{if(!this._onlistresourcetemplates)throw Error("No onlistresourcetemplates handler set");return this._onlistresourcetemplates(o.params,r)})}_onreadresource;get onreadresource(){return this._onreadresource}set onreadresource(t){this.warnIfRequestHandlerReplaced("onreadresource",this._onreadresource,t),this._onreadresource=t,this.replaceRequestHandler("resources/read",async(o,r)=>{if(!this._onreadresource)throw Error("No onreadresource handler set");return this._onreadresource(o.params,r)})}sendResourceListChanged(t={}){return this.notification({method:"notifications/resources/list_changed",params:t})}_onlistprompts;get onlistprompts(){return this._onlistprompts}set onlistprompts(t){this.warnIfRequestHandlerReplaced("onlistprompts",this._onlistprompts,t),this._onlistprompts=t,this.replaceRequestHandler("prompts/list",async(o,r)=>{if(!this._onlistprompts)throw Error("No onlistprompts handler set");return this._onlistprompts(o.params,r)})}sendPromptListChanged(t={}){return this.notification({method:"notifications/prompts/list_changed",params:t})}getCapabilities(){return this._hostCapabilities}async _onAppsInitialize(t){let o=t.protocolVersion;return this._appInfo!==void 0&&console.warn("[ext-apps] AppBridge received a second ui/initialize. The View may be double-mounting (e.g. React StrictMode in dev) without closing the previous App instance. Responding normally; the latest appInfo/appCapabilities replace the previous values."),this._appCapabilities=t.appCapabilities,this._appInfo=t.appInfo,{protocolVersion:$R.includes(o)?o:A_,hostCapabilities:this.getCapabilities(),hostInfo:this._hostInfo,hostContext:this._hostContext}}setHostContext(t){let o={},r=!1;for(let n of Object.keys(t)){let s=this._hostContext[n],a=t[n];CR(s,a)||(o[n]=a,r=!0)}r&&(this._hostContext=t,this.sendHostContextChange(o))}sendHostContextChange(t){return this.notification({method:"ui/notifications/host-context-changed",params:t})}sendToolInput(t){return this.notification({method:"ui/notifications/tool-input",params:t})}sendToolInputPartial(t){return this.notification({method:"ui/notifications/tool-input-partial",params:t})}sendToolResult(t){return this.notification({method:"ui/notifications/tool-result",params:t})}sendToolCancelled(t){return this.notification({method:"ui/notifications/tool-cancelled",params:t})}sendSandboxResourceReady(t){return this.notification({method:"ui/notifications/sandbox-resource-ready",params:t})}teardownResource(t,o){return this.request({method:"ui/resource-teardown",params:t},bR,o)}sendResourceTeardown=this.teardownResource;callTool(t,o){return this.request({method:"tools/call",params:t},o)}listTools(t,o){return this.request({method:"tools/list",params:t},o)}async connect(t){if(this.transport)throw Error("AppBridge is already connected. Call close() before connecting again.");if(this._initializedReceived=!1,this._client){let o=this._client.getServerCapabilities();if(!o)throw Error("Client server capabilities not available");o.tools&&(this.oncalltool=async(r,n)=>this._client.request({method:"tools/call",params:r},{signal:n.mcpReq.signal}),o.tools.listChanged&&this._client.setNotificationHandler("notifications/tools/list_changed",r=>this.sendToolListChanged(r.params))),o.resources&&(this.onlistresources=async(r,n)=>this._client.request({method:"resources/list",params:r},{signal:n.mcpReq.signal}),this.onlistresourcetemplates=async(r,n)=>this._client.request({method:"resources/templates/list",params:r},{signal:n.mcpReq.signal}),this.onreadresource=async(r,n)=>this._client.request({method:"resources/read",params:r},{signal:n.mcpReq.signal}),o.resources.listChanged&&this._client.setNotificationHandler("notifications/resources/list_changed",r=>this.sendResourceListChanged(r.params))),o.prompts&&(this.onlistprompts=async(r,n)=>this._client.request({method:"prompts/list",params:r},{signal:n.mcpReq.signal}),o.prompts.listChanged&&this._client.setNotificationHandler("notifications/prompts/list_changed",r=>this.sendPromptListChanged(r.params)))}return super.connect(t)}};function CR(e,t){return JSON.stringify(e)===JSON.stringify(t)}function L_(e,t){let o=new DOMParser().parseFromString(e,"text/html"),r=o.createElement("meta");if(r.httpEquiv="Content-Security-Policy",r.content=t,o.head.prepend(r),!o.querySelector("base[href]")){let n=o.querySelector("base")??o.createElement("base");n.setAttribute("href","about:srcdoc"),r.after(n)}return"<!doctype html>"+o.documentElement.outerHTML}var $e=new Yl({name:"ADS preview",version:"1.0.0"},{},{autoResize:!0}),Wr=document.getElementById("ads-token-root"),Lt=document.getElementById("ads-widget-status"),tu=document.getElementById("ads-token-theme"),eu=document.getElementById("ads-scenario-root"),To,Me=0,U_="",Br=30,Z_=document.createElement("style");Wr&&document.head.append(Z_);function ou(e){e!=="light"&&e!=="dark"||(document.documentElement.dataset.theme=e,tu&&(tu.value=e),Wr&&To&&wo())}function Oe(e,t){let o=document.createElement(e);return o.textContent=t,o}function IR(e){let t=Oe("li","");t.dataset.token=e.name;let o=getComputedStyle(document.documentElement).getPropertyValue(e.name).trim(),r=Oe("details","");r.className="token-source";let n=Oe("summary","");n.title=`${e.name}: ${o||"Conditional or unset"}. Click to inspect source definitions.`;let s=Oe("code",e.name);s.className="token-name";let a=Oe("span",o||"Conditional or unset");n.append(s,a);let l=Oe("dl","");l.append(Oe("dt","Computed in preview"),Oe("dd",o||"Conditional or unset")),e.meaning&&l.append(Oe("dt","Meaning"),Oe("dd",e.meaning));for(let u of e.definitions)l.append(Oe("dt",u.scope),Oe("dd",`${u.value} \xB7 ${u.file}:${u.line}`));return r.append(n,l),t.append(r),t}function wo(){Z_.textContent=To.css;let e=To.tokens.filter(t=>`${t.name} ${t.type??""}`.toLowerCase().includes(U_.toLowerCase()));document.getElementById("ads-token-list").replaceChildren(...e.slice(Me,Me+Br).map(IR)),document.querySelector("footer").hidden=e.length<=Br,document.getElementById("ads-token-count").textContent=e.length?`${Me+1}\u2013${Math.min(Me+Br,e.length)} of ${e.length}`:"No matching tokens",document.getElementById("ads-token-prev").disabled=Me===0,document.getElementById("ads-token-next").disabled=Me+Br>=e.length,Lt.textContent=`${e.length} matching definitions \xB7 ${To.diagnostics.length} diagnostics`}Wr&&($e.ontoolinput=()=>{Me=0,To&&wo()},To=JSON.parse(document.getElementById("ads-token-seed").textContent),ou("light"),document.getElementById("ads-token-search").addEventListener("input",e=>{U_=e.target.value,Me=0,wo()}),tu.addEventListener("change",()=>ou(tu.value)),document.getElementById("ads-token-prev").addEventListener("click",()=>{Me=Math.max(0,Me-Br),wo()}),document.getElementById("ads-token-next").addEventListener("click",()=>{Me+=Br,wo()}),document.getElementById("ads-token-refresh").addEventListener("click",async()=>{try{let e=await $e.readServerResource({uri:"ads://project/tokens"});To=JSON.parse(e.contents.find(t=>"text"in t).text),Me=0,wo()}catch(e){Lt.textContent=`Refresh failed: ${e.message}`}}));var xo,$f,D_=!1,_a=0;async function H_(e){if($f=e,!D_||!eu)return;let t=e?.structuredContent?.scenario?.resourceUri;if(!t||!t.startsWith("ui://ads/previews/"))return;let o=++_a;Wr&&(Wr.hidden=!0,Lt=document.getElementById("ads-scenario-status")),eu.hidden=!1;try{if(!$e.getHostCapabilities()?.serverResources)throw new Error("This Apps host does not forward server resources. Use the project showcase URL from the tool result.");let n=(await $e.readServerResource({uri:t})).contents.find(u=>u.mimeType==="text/html;profile=mcp-app"&&typeof u.text=="string");if(!n)throw new Error("Scenario HTML is unavailable.");if(o!==_a)return;xo&&(await xo.close(),xo=void 0);let s=document.createElement("iframe");s.title=e.structuredContent.scenario.title,s.setAttribute("sandbox","allow-scripts allow-forms"),eu.replaceChildren(s);let a=new Xl(null,{name:"ADS scenario host",version:"1.0.0"},{serverResources:{},serverTools:{}},{hostContext:$e.getHostContext()});xo=a,a.onreadresource=u=>$e.readServerResource(u),a.oncalltool=u=>$e.callServerTool(u),a.onsizechange=({height:u})=>{u&&(s.height=String(Math.min(1e4,Math.max(120,u))))},a.oninitialized=async()=>{await a.sendToolInput({arguments:{}}),await a.sendToolResult(e),o===_a&&window.parent.postMessage({type:"ads-preview-ready"},"*")},await a.connect(new Ql(s.contentWindow,s.contentWindow));let l="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; worker-src blob: data:; connect-src 'none'; base-uri about:; object-src 'none'; form-action 'none'";s.srcdoc=L_(n.text,l),Lt.hidden=!0}catch(r){o===_a&&(Lt.hidden=!1,Lt.textContent=r.message,window.parent.postMessage({type:"ads-preview-ready"},"*"))}}eu&&($e.ontoolresult=e=>{H_(e)},$e.onteardown=async()=>{++_a;let e=xo;xo=void 0;try{await e?.teardownResource({},{timeout:500})}catch{}return await e?.close(),{}});$e.onhostcontextchanged=e=>{ou(e.theme),xo?.setHostContext(e)};try{await $e.connect(),D_=!0,$f&&H_($f),ou($e.getHostContext()?.theme),Wr&&(document.getElementById("ads-token-refresh").disabled=!$e.getHostCapabilities()?.serverResources,wo())}catch{Lt&&(Lt.textContent="Apps bridge unavailable. Samples remain usable; inspect project data through ADS tools/resources.")}
142
142
  /*! Bundled license information:
143
143
 
144
144
  @modelcontextprotocol/client/dist/src-WCy6ifGf.mjs:
package/dist/oxlint.mjs CHANGED
@@ -9,6 +9,8 @@ const schema = [{
9
9
  dsDir: { type: "string" },
10
10
  sourceDir: { type: "string" },
11
11
  publicAlias: { type: "string" },
12
+ publicEntry: { type: "string" },
13
+ reviewEntry: { type: "string" },
12
14
  stylesEntry: { type: "string" },
13
15
  inlineStyleFiles: { type: "array", items: { type: "string" } },
14
16
  configurationFiles: { type: "array", items: { type: "string" } },
@@ -66,7 +68,7 @@ export default {
66
68
  },
67
69
  },
68
70
  "no-inline-style": {
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." } },
71
+ meta: { type: "problem", schema, messages: { inline: "Own visual styles in component/pattern Linaria. Put demo-only sizing and framing in a development-only review/ helper. Reserve inlineStyleFiles for documented measured geometry; add component props for deliberate product API needs." } },
70
72
  create(context) {
71
73
  const { root, config } = options(context);
72
74
  const allowed = config.inlineStyleFiles.some((file) => path.resolve(root, file) === path.resolve(context.getFilename()));
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameS
3
3
  import path from "node:path";
4
4
  import ts from "typescript";
5
5
  import valueParser from "postcss-value-parser";
6
- import { isInside } from "./config.mjs";
6
+ import { isInside, loadConfig } from "./config.mjs";
7
7
  import { previewArtifactPath, previewManifestPath } from "./preview-scope.mjs";
8
8
 
9
9
  export const PREVIEW_APP_URI = "ui://ads/preview";
@@ -21,9 +21,19 @@ function atomic(file, bytes) {
21
21
  }
22
22
 
23
23
  /** Registry entries remain project-owned; these virtual entries normalize mount/unmount. */
24
+ export function configureReviewAlias(config, { dsRoot, config: ads }) {
25
+ const aliases = config.resolve?.alias ?? [];
26
+ const existing = Array.isArray(aliases) ? aliases : Object.entries(aliases).map(([find, replacement]) => ({ find, replacement }));
27
+ config.resolve = { ...config.resolve, alias: [{ find: `${ads.publicAlias}/review`, replacement: path.join(dsRoot, ads.reviewEntry) }, ...existing] };
28
+ }
29
+
24
30
  export function previewEntriesPlugin(entries, mountEntry) {
25
31
  const ids = new Map(entries.map((entry, index) => [`ads-preview-entry:${index}`, entry]));
26
32
  return { name: "ads-preview-resource-entries",
33
+ config(config) {
34
+ if (!entries[0]?.root) return;
35
+ configureReviewAlias(config, loadConfig(entries[0].root));
36
+ },
27
37
  resolveId(id) { if (ids.has(id)) return `\0${id}`; },
28
38
  load(id) {
29
39
  const entry = ids.get(id.slice(1));