@reqvire-org/ads 0.0.0-stage → 0.2.9

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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/assets.mjs +52 -0
  4. package/dist/bootstrap-ds/ADS.md +203 -0
  5. package/dist/bootstrap-ds/README.md +98 -0
  6. package/dist/bootstrap-ds/biome.css.json +21 -0
  7. package/dist/bootstrap-ds/design-system/assets/brand/README.md +28 -0
  8. package/dist/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
  9. package/dist/bootstrap-ds/design-system/components/.gitkeep +0 -0
  10. package/dist/bootstrap-ds/design-system/composition/.gitkeep +0 -0
  11. package/dist/bootstrap-ds/design-system/index.ts +5 -0
  12. package/dist/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
  13. package/dist/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
  14. package/dist/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
  15. package/dist/bootstrap-ds/design-system/styles.css +5 -0
  16. package/dist/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
  17. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
  18. package/dist/bootstrap-ds/design-system/tokens/base.css +27 -0
  19. package/dist/bootstrap-ds/design-system/tokens/colors.css +29 -0
  20. package/dist/bootstrap-ds/design-system/tokens/elevation.css +15 -0
  21. package/dist/bootstrap-ds/design-system/tokens/spacing.css +15 -0
  22. package/dist/bootstrap-ds/design-system/tokens/typography.css +13 -0
  23. package/dist/bootstrap-ds/ds.config.json +13 -0
  24. package/dist/bootstrap-ds/gitignore.template +8 -0
  25. package/dist/bootstrap-ds/package-lock.json +4660 -0
  26. package/dist/bootstrap-ds/package.json +41 -0
  27. package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
  28. package/dist/bootstrap-ds/src/vite-env.d.ts +7 -0
  29. package/dist/bootstrap-ds/tsconfig.build.json +12 -0
  30. package/dist/bootstrap-ds/tsconfig.json +20 -0
  31. package/dist/bootstrap-ds/vite.config.ts +21 -0
  32. package/dist/boundaries.mjs +130 -0
  33. package/dist/brand-assets.mjs +82 -0
  34. package/dist/check.mjs +222 -0
  35. package/dist/cli.mjs +106 -0
  36. package/dist/config.mjs +181 -0
  37. package/dist/contracts/tokens.json +52 -0
  38. package/dist/html.mjs +30 -0
  39. package/dist/index.mjs +9 -0
  40. package/dist/init.mjs +194 -0
  41. package/dist/lint-config.mjs +40 -0
  42. package/dist/mcp/THIRD_PARTY_NOTICES.md +786 -0
  43. package/dist/mcp/apps.mjs +53 -0
  44. package/dist/mcp/build-status.mjs +164 -0
  45. package/dist/mcp/catalog.mjs +65 -0
  46. package/dist/mcp/context.mjs +69 -0
  47. package/dist/mcp/mutations.mjs +301 -0
  48. package/dist/mcp/patch.mjs +114 -0
  49. package/dist/mcp/preview-watch.mjs +169 -0
  50. package/dist/mcp/preview-widget.bundle.js +113 -0
  51. package/dist/mcp/project-widget.bundle.js +113 -0
  52. package/dist/mcp/prompts.mjs +28 -0
  53. package/dist/mcp/resource-loader.js +138 -0
  54. package/dist/mcp/sandbox-host.mjs +22 -0
  55. package/dist/mcp/server.mjs +377 -0
  56. package/dist/mcp/showcase-host.mjs +68 -0
  57. package/dist/mcp/skills.mjs +77 -0
  58. package/dist/mcp/widget.bundle.js +150 -0
  59. package/dist/oxlint.mjs +81 -0
  60. package/dist/preview-resources.mjs +198 -0
  61. package/dist/project-documents.mjs +27 -0
  62. package/dist/public-props.mjs +74 -0
  63. package/dist/scenarios-compiler.mjs +287 -0
  64. package/dist/scenarios-vite.d.mts +3 -0
  65. package/dist/scenarios-vite.mjs +71 -0
  66. package/dist/scenarios.mjs +212 -0
  67. package/dist/showcase/client.bundle.js +127 -0
  68. package/dist/showcase/index.html +17 -0
  69. package/dist/showcase/sandbox.js +30 -0
  70. package/dist/showcase/style.css +7 -0
  71. package/dist/showcase-coverage.mjs +118 -0
  72. package/dist/showcase.mjs +77 -0
  73. package/dist/skills/README.md +68 -0
  74. package/dist/skills/ads-assimilate/SKILL.md +292 -0
  75. package/dist/skills/ads-assimilate/agents/openai.yaml +4 -0
  76. package/dist/skills/ads-assimilate/references/cli.md +111 -0
  77. package/dist/skills/ads-assimilate/references/lint-and-docs.md +87 -0
  78. package/dist/skills/ads-assimilate/references/native-implementation.md +98 -0
  79. package/dist/skills/ads-assimilate/references/testing.md +107 -0
  80. package/dist/skills/ads-break-ui/LICENSE.upstream +21 -0
  81. package/dist/skills/ads-break-ui/README.md +16 -0
  82. package/dist/skills/ads-break-ui/SKILL.md +235 -0
  83. package/dist/skills/ads-break-ui/agents/openai.yaml +4 -0
  84. package/dist/skills/ads-break-ui/references/catalog.md +135 -0
  85. package/dist/skills/ads-break-ui/references/verification.md +48 -0
  86. package/dist/skills/ads-designer/LICENSE.upstream +21 -0
  87. package/dist/skills/ads-designer/README.md +19 -0
  88. package/dist/skills/ads-designer/SKILL.md +400 -0
  89. package/dist/skills/ads-designer/agents/openai.yaml +4 -0
  90. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +203 -0
  91. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +98 -0
  92. package/dist/skills/ads-designer/assets/bootstrap-ds/biome.css.json +21 -0
  93. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/brand/README.md +28 -0
  94. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
  95. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/components/.gitkeep +0 -0
  96. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/composition/.gitkeep +0 -0
  97. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +5 -0
  98. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
  100. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
  101. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/styles.css +5 -0
  102. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
  103. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
  104. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +27 -0
  105. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +29 -0
  106. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/elevation.css +15 -0
  107. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/spacing.css +15 -0
  108. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/typography.css +13 -0
  109. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +13 -0
  110. package/dist/skills/ads-designer/assets/bootstrap-ds/gitignore.template +8 -0
  111. package/dist/skills/ads-designer/assets/bootstrap-ds/package-lock.json +4660 -0
  112. package/dist/skills/ads-designer/assets/bootstrap-ds/package.json +41 -0
  113. package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
  114. package/dist/skills/ads-designer/assets/bootstrap-ds/src/vite-env.d.ts +7 -0
  115. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +12 -0
  116. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +20 -0
  117. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +21 -0
  118. package/dist/skills/ads-designer/references/cli.md +111 -0
  119. package/dist/skills/ads-designer/references/design-craft.md +65 -0
  120. package/dist/skills/ads-designer/references/mobile.md +73 -0
  121. package/dist/skills/ads-import/SKILL.md +335 -0
  122. package/dist/skills/ads-import/agents/openai.yaml +4 -0
  123. package/dist/skills/ads-import/references/branding.md +44 -0
  124. package/dist/skills/ads-import/references/cli.md +111 -0
  125. package/dist/skills/ads-import/references/implementation.md +111 -0
  126. package/dist/skills/ads-import/references/tokens.md +96 -0
  127. package/dist/skills/ads-motion/LICENSE.upstream +21 -0
  128. package/dist/skills/ads-motion/README.md +22 -0
  129. package/dist/skills/ads-motion/SKILL.md +217 -0
  130. package/dist/skills/ads-motion/agents/openai.yaml +4 -0
  131. package/dist/skills/ads-motion/references/audit.md +44 -0
  132. package/dist/skills/ads-motion/references/build.md +81 -0
  133. package/dist/skills/ads-motion/references/performance.md +55 -0
  134. package/dist/skills/ads-motion/references/review.md +35 -0
  135. package/dist/skills/ads-motion/references/verification.md +39 -0
  136. package/dist/token-policy.mjs +10 -0
  137. package/dist/tokens.mjs +142 -0
  138. package/dist/ui-framework.mjs +59 -0
  139. package/dist/units.mjs +168 -0
  140. package/docs/ADS.md +310 -0
  141. package/docs/ARCHITECTURE.md +311 -0
  142. package/docs/CHANGELOG.md +106 -0
  143. package/docs/GUIDE.md +722 -0
  144. package/docs/MCP.md +690 -0
  145. package/package.json +107 -4
@@ -0,0 +1,53 @@
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { isInside, projectPath } from "../config.mjs";
3
+ import { PREVIEW_APP_URI } from "../preview-resources.mjs";
4
+ export { PREVIEW_APP_URI };
5
+
6
+ export const APP_MIME = "text/html;profile=mcp-app";
7
+ export const TOKENS_APP_URI = "ui://ads/tokens";
8
+ export const SCENARIO_APP_URI = "ui://ads/scenario";
9
+ export const appUri = (id) => `ui://ads/previews/${id.split("/").map(encodeURIComponent).join("/")}`;
10
+ const safeJson = (value) => JSON.stringify(value).replaceAll("<", "\\u003c");
11
+ const bridge = (project = false) => readFileSync(new URL(project ? "./project-widget.bundle.js" : "./widget.bundle.js", import.meta.url), "utf8").replaceAll("</script", "<\\/script");
12
+
13
+ export function previewApp(selection) {
14
+ const script = readFileSync(new URL("./preview-widget.bundle.js", import.meta.url), "utf8").replaceAll("</script", "<\\/script");
15
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ADS preview</title><style>html,body,#root{height:100%;margin:0}body{overflow:hidden}#ads-preview-status{position:fixed;inset:12px 12px auto;z-index:2147483647;padding:12px;border:1px solid GrayText;background:Canvas;color:CanvasText;font:13px system-ui;white-space:pre-wrap}#ads-preview-status[hidden]{display:none}</style></head><body><aside id="ads-preview-status" role="status" hidden></aside><div id="root"></div>${selection ? `<script type="application/json" id="ads-preview-selection">${safeJson(selection)}</script>` : ""}<script type="module">${script}</script></body></html>`;
16
+ }
17
+
18
+ export function tokenWidgetData(inventory) {
19
+ const scopes = new Map();
20
+ for (const token of inventory.tokens) for (const definition of token.definitions) {
21
+ // Conditional definitions are displayed verbatim; simple theme selectors
22
+ // also drive the in-frame browser samples, including alias resolution.
23
+ if (definition.scope.includes("@") || definition.scope.includes(" > ")) continue;
24
+ if (!scopes.has(definition.scope)) scopes.set(definition.scope, []);
25
+ scopes.get(definition.scope).push(`${token.name}:${definition.value};`);
26
+ }
27
+ return { ...inventory, css: [...scopes].map(([selector, declarations]) => `${selector}{${declarations.join("")}}`).join("\n") };
28
+ }
29
+
30
+ export function tokensApp(data) {
31
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ADS tokens</title><style>
32
+ html{color-scheme:light dark;background:Canvas;color:CanvasText;font:13px/1.55 system-ui}html,body{height:100%}body{margin:0}#ads-token-root{padding:32px}#ads-scenario-root{height:100%}header{display:flex;gap:8px;flex-wrap:wrap;align-items:center}h1{font-size:18px;margin:0 auto 0 0}button,input,select{font:inherit;color:CanvasText;background:Canvas;border:1px solid GrayText;border-radius:4px;padding:6px 8px}ul{padding:0;list-style:none}li{margin:12px 0}summary{cursor:pointer;display:flex;gap:16px;align-items:baseline;flex-wrap:wrap}code{font:12px monospace;overflow-wrap:anywhere}dl{border:1px solid GrayText;padding:12px;overflow-wrap:anywhere}dt{font-weight:600}dd{margin:2px 0 12px}footer{display:flex;gap:8px;align-items:center}iframe{display:block;width:100%;height:100%;border:0}[hidden]{display:none!important}:root[data-theme="light"]{color-scheme:light}:root[data-theme="dark"]{color-scheme:dark}
33
+ </style></head><body><p id="ads-scenario-status" role="status" hidden></p><main id="ads-scenario-root" hidden></main><main id="ads-token-root"><header><h1>Token definitions</h1><input id="ads-token-search" type="search" aria-label="Find token" placeholder="Find a token"><select id="ads-token-theme" aria-label="Preview theme"><option value="light">Light</option><option value="dark">Dark</option></select><button id="ads-token-refresh" type="button" disabled>Refresh</button></header><p id="ads-widget-status" role="status">Connecting to the Apps host…</p><ul id="ads-token-list"></ul><footer><button id="ads-token-prev" type="button">Previous</button><span id="ads-token-count"></span><button id="ads-token-next" type="button">Next</button></footer></main><script type="application/json" id="ads-token-seed">${safeJson(data)}</script><script type="module">${bridge()}</script></body></html>`;
34
+ }
35
+
36
+ export function scenarioApp() {
37
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ADS scenario</title><style>html{color-scheme:light dark;background:Canvas;color:CanvasText;font:14px system-ui}html,body,#ads-scenario-root{height:100%}body{margin:0}#ads-widget-status{margin:12px}iframe{display:block;border:0;width:100%;height:100%}</style></head><body><p id="ads-widget-status" role="status">Waiting for a scenario selection…</p><main id="ads-scenario-root"></main><script type="module">${bridge()}</script></body></html>`;
38
+ }
39
+
40
+ export function projectApp(options, app, build) {
41
+ const file = projectPath(options.root, app.file, "mcp.apps.file");
42
+ const real = realpathSync(file);
43
+ if (real !== file || !isInside(options.root, real)) throw new Error("Preview HTML cannot resolve through a symlink or outside the project.");
44
+ if (statSync(file).size > 2 * 1024 * 1024) throw new Error("Preview exceeds 2 MiB. Build a focused component/pattern example.");
45
+ const html = readFileSync(file, "utf8");
46
+ if (!/<\/body\s*>/i.test(html) || /<script\b[^>]*\bsrc\s*=|<link\b[^>]*\brel\s*=\s*["']?stylesheet\b/i.test(html)) throw new Error("Preview must be built as self-contained HTML, with JavaScript and styles inlined.");
47
+ const seed = { id: app.id, session: build?.session ?? null, revision: build?.current ? build.revision : null, state: build?.state ?? "unreported" };
48
+ return html.replace(/<\/body\s*>/i, () => `<script type="application/json" id="ads-preview-seed">${safeJson(seed)}</script><script type="module">${bridge(true)}</script></body>`);
49
+ }
50
+
51
+ export function appMetadata({ nested = false } = {}) {
52
+ return { ui: { csp: { connectDomains: [], resourceDomains: ["data:", "blob:"], ...(nested ? { frameDomains: ["about:"] } : {}) } } };
53
+ }
@@ -0,0 +1,164 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { isInside, projectPath } from "../config.mjs";
6
+ import { readPreviewManifest } from "../preview-resources.mjs";
7
+
8
+ export const BUILD_URI = "ads://project/build";
9
+ export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json");
10
+ const maxBytes = 256 * 1024;
11
+ const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional() }).strict();
12
+ const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
13
+ const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), previews: z.array(preview), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
14
+ export const artifactDigest = (file) => {
15
+ if (realpathSync(file) !== file) throw new Error("Preview output cannot resolve through a symlink.");
16
+ if (statSync(file).size > 2 * 1024 * 1024) throw new Error("Preview exceeds 2 MiB.");
17
+ return `sha256:${createHash("sha256").update(readFileSync(file)).digest("hex")}`;
18
+ };
19
+
20
+ function normalizeDiagnostic(root, error) {
21
+ const location = error?.loc ?? error?.location;
22
+ const source = location?.file ?? error?.id ?? error?.file;
23
+ const file = typeof source === "string" ? source.split("?")[0] : null;
24
+ const line = location?.line ?? error?.line;
25
+ const column = location?.column !== undefined ? location.column + 1 : error?.column;
26
+ return { message: String(error?.message ?? error).slice(0, 4000),
27
+ ...(file ? { file: (path.isAbsolute(file) ? path.relative(root, file) : file).replaceAll("\\", "/").slice(0, 1000) } : {}),
28
+ ...(Number.isInteger(line) && line > 0 ? { line } : {}), ...(Number.isInteger(column) && column > 0 ? { column } : {}),
29
+ ...(typeof error?.plugin === "string" ? { plugin: error.plugin.slice(0, 200) } : {}) };
30
+ }
31
+
32
+ /** One publisher per project compiler; no dependency on a UI framework. */
33
+ export function createBuildReporter(directory, { apps, session = process.env.ADS_MCP_BUILD_SESSION ?? randomUUID(), file = process.env.ADS_MCP_BUILD_STATUS } = {}) {
34
+ const root = path.resolve(directory);
35
+ const target = file ?? buildStatusFile(root);
36
+ if (!isInside(root, target)) throw new Error("Build status must stay inside the project.");
37
+ const entries = new Map(apps.map((app) => [app.id, { id: app.id, state: "idle", revision: 0, lastSuccessfulRevision: null, artifactDigest: null, diagnostics: [] }]));
38
+ let sequence = 0;
39
+ function save() {
40
+ const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()] };
41
+ let text = JSON.stringify(snapshot.parse(value));
42
+ // A shared graph failure applies to every scenario. Store repeated details
43
+ // once only when necessary; ordinary reports retain inline diagnostics.
44
+ if (Buffer.byteLength(text) > maxBytes) {
45
+ const diagnosticSets = [], sets = new Map();
46
+ const previews = value.previews.map(entry => {
47
+ if (!entry.diagnostics.length) return entry;
48
+ const key = JSON.stringify(entry.diagnostics);
49
+ if (!sets.has(key)) { sets.set(key, diagnosticSets.length); diagnosticSets.push(entry.diagnostics); }
50
+ return { ...entry, diagnostics: [], diagnosticSet: sets.get(key) };
51
+ });
52
+ text = JSON.stringify(snapshot.parse({ ...value, previews, diagnosticSets }));
53
+ }
54
+ if (Buffer.byteLength(text) > maxBytes) throw new Error("Build status exceeds 256 KiB. Keep diagnostics bounded.");
55
+ mkdirSync(path.dirname(target), { recursive: true });
56
+ if (realpathSync(path.dirname(target)) !== path.dirname(target)) throw new Error("Build status cannot resolve through a symlink.");
57
+ const temporary = `${target}.${randomUUID()}.tmp`;
58
+ try { writeFileSync(temporary, text); renameSync(temporary, target); }
59
+ finally { rmSync(temporary, { force: true }); }
60
+ }
61
+ function entry(id, revision) {
62
+ const value = entries.get(id);
63
+ if (!value) throw new Error(`Unknown preview ID: ${id}`);
64
+ return revision === undefined || value.revision === revision ? value : null;
65
+ }
66
+ save();
67
+ return {
68
+ session,
69
+ setApps(next) {
70
+ const ids = new Set(next.map(app => app.id));
71
+ for (const id of entries.keys()) if (!ids.has(id)) entries.delete(id);
72
+ for (const app of next) if (!entries.has(app.id)) entries.set(app.id, { id: app.id, state: "idle", revision: 0, lastSuccessfulRevision: null, artifactDigest: null, diagnostics: [] });
73
+ apps = next;
74
+ save();
75
+ },
76
+ begin(id) {
77
+ const value = entry(id);
78
+ value.state = "building";
79
+ value.revision++;
80
+ value.diagnostics = [];
81
+ save();
82
+ return value.revision;
83
+ },
84
+ ready(id, revision) {
85
+ const value = entry(id, revision);
86
+ if (!value) return false; // An older build cannot clear a newer failure/invalidation.
87
+ const app = apps.find((app) => app.id === id);
88
+ value.artifactDigest = artifactDigest(projectPath(root, app.file, "mcp.apps.file"));
89
+ value.state = "ready";
90
+ value.lastSuccessfulRevision = revision;
91
+ value.diagnostics = [];
92
+ save();
93
+ return true;
94
+ },
95
+ error(id, revision, failure) {
96
+ const value = entry(id, revision);
97
+ if (!value) return false;
98
+ value.state = "error";
99
+ value.diagnostics = (Array.isArray(failure) ? failure : [failure]).slice(0, 20).map((error) => normalizeDiagnostic(root, error));
100
+ save();
101
+ return true;
102
+ },
103
+ };
104
+ }
105
+
106
+ /** Read durable build events; a running process alone never means a fresh build. */
107
+ export function inspectBuildStatus(root, config, { session, processState } = {}) {
108
+ let report;
109
+ let reportError;
110
+ const file = buildStatusFile(root);
111
+ try {
112
+ if (existsSync(file)) {
113
+ if (realpathSync(file) !== file || statSync(file).size > maxBytes) throw new Error("Build status must be a real project file under 256 KiB.");
114
+ report = snapshot.parse(JSON.parse(readFileSync(file, "utf8")));
115
+ if (session && report.session !== session) report = undefined;
116
+ }
117
+ } catch (error) { reportError = `Invalid preview build status: ${error.message.slice(0, 500)}`; }
118
+ let graphError;
119
+ if (config.previewManifest && report?.previews.some(entry => entry.state === "ready")) {
120
+ try {
121
+ if (!readPreviewManifest({ root, config: { mcp: config } }, { verify: true })) throw new Error("Preview resource manifest is missing.");
122
+ } catch (error) { graphError = `Invalid preview resource graph: ${error.message}`; }
123
+ }
124
+ const previews = config.apps.map((app) => {
125
+ const value = report?.previews.find((entry) => entry.id === app.id);
126
+ let state = value?.state ?? "unreported";
127
+ let diagnostics = value?.diagnosticSet !== undefined ? report.diagnosticSets[value.diagnosticSet] : value?.diagnostics ?? [];
128
+ const available = existsSync(path.resolve(root, app.file));
129
+ if (reportError) { state = "error"; diagnostics = [{ message: reportError }]; }
130
+ if (graphError && app.id.includes("/")) { state = "error"; diagnostics = [{ message: graphError }]; }
131
+ if (state === "ready") {
132
+ try {
133
+ if (value.lastSuccessfulRevision !== value.revision || artifactDigest(path.resolve(root, app.file)) !== value.artifactDigest) throw new Error("Preview output does not match its successful build. Rebuild before reviewing it.");
134
+ } catch (error) { state = "error"; diagnostics = [{ message: error.message }]; }
135
+ }
136
+ if (processState?.state === "failed") { state = "error"; diagnostics = [{ message: processState.error ?? "Preview watcher exited." }, ...diagnostics].slice(0, 20); }
137
+ if (["stopped", "stopping", "idle"].includes(processState?.state)) state = "unreported";
138
+ const current = state === "ready";
139
+ return { id: app.id, state, session: report?.session ?? null, revision: value?.revision ?? null, lastSuccessfulRevision: value?.lastSuccessfulRevision ?? null,
140
+ current, stale: state === "unreported" ? null : !current, available, diagnostics };
141
+ });
142
+ const state = previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
143
+ return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, resourceUri: BUILD_URI };
144
+ }
145
+
146
+ export class PreviewBuildError extends Error {
147
+ constructor(message, build) { super(message); this.build = build; }
148
+ }
149
+
150
+ /** Keep diagnostics useful in model-visible tool results; resources retain detail. */
151
+ export function summarizeBuild(build) {
152
+ function bounded(value) {
153
+ const diagnostics = value.diagnostics.slice(0, 3).map((entry) => ({ ...entry, message: entry.message.slice(0, 1000) }));
154
+ return { ...value, diagnostics, diagnosticCount: value.diagnostics.length,
155
+ diagnosticsTruncated: value.diagnostics.length > 3 || value.diagnostics.some((entry) => entry.message.length > 1000) };
156
+ }
157
+ return build.previews ? { ...build, previews: build.previews.map(bounded) } : { ...bounded(build), resourceUri: BUILD_URI };
158
+ }
159
+
160
+ export function buildFailure(build) {
161
+ const summary = summarizeBuild(build);
162
+ const details = summary.diagnostics.map((entry) => `${entry.file ?? ""}${entry.line ? `:${entry.line}` : ""}${entry.file ? ": " : ""}${entry.message}`).join("\n");
163
+ return new PreviewBuildError(`Preview build failed. ${details}${summary.diagnosticsTruncated ? `\nMore diagnostic detail: ${BUILD_URI}` : ""}`, summary);
164
+ }
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { isInside, sourceFiles } from "../config.mjs";
5
+ import { projectCompilerOptions } from "../boundaries.mjs";
6
+ import { publicUi } from "../scenarios.mjs";
7
+ import { projectGuideFiles } from "../project-documents.mjs";
8
+
9
+ export const PROJECT_URI = "ads://project";
10
+ export const sourceUri = (file) => `${PROJECT_URI}/files/${file.split("/").map(encodeURIComponent).join("/")}`;
11
+
12
+ export function projectDocuments(options) {
13
+ const { root, dsRoot } = options;
14
+ const files = sourceFiles(dsRoot);
15
+ function docs(directory) {
16
+ // Documentation is intentionally confined to the ADS; no arbitrary filesystem reader.
17
+ for (const name of ["README.md", "readme.md", "SKILL.md", "LICENSE", "LICENSE.md"]) {
18
+ const file = path.join(directory, name);
19
+ if (existsSync(file) && statSync(file).isFile()) files.push(file);
20
+ }
21
+ }
22
+ for (const directory of new Set(files.map((file) => path.dirname(file)))) docs(directory);
23
+ docs(dsRoot);
24
+ for (const name of projectGuideFiles(root)) files.push(path.join(root, name));
25
+ return [...new Set(files)].filter((file) => isInside(root, realpathSync(file))).sort().map((file) => {
26
+ const relative = path.relative(root, file).replaceAll("\\", "/");
27
+ return { uri: sourceUri(relative), name: relative, mimeType: file.endsWith(".md") ? "text/markdown" : "text/plain", file };
28
+ });
29
+ }
30
+
31
+ export function inspectCatalog(options, scenarios = []) {
32
+ const { root, dsRoot, config } = options;
33
+ const entry = path.join(dsRoot, config.publicEntry);
34
+ if (!existsSync(entry)) return [];
35
+ const program = ts.createProgram([entry], projectCompilerOptions(options));
36
+ const checker = program.getTypeChecker();
37
+ const source = program.getSourceFile(entry);
38
+ const module = source && checker.getSymbolAtLocation(source);
39
+ if (!module) return [];
40
+ const documents = projectDocuments(options);
41
+ const items = [];
42
+ const renderable = new Set(publicUi(program, options).filter(item => item.renderable).map(item => item.name));
43
+ for (const exported of checker.getExportsOfModule(module)) {
44
+ let symbol = exported;
45
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
46
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
47
+ if (!(symbol.flags & ts.SymbolFlags.Value) || !declaration || !renderable.has(exported.name)) continue;
48
+ const file = declaration.getSourceFile().fileName;
49
+ const kind = isInside(path.join(dsRoot, "components"), file) ? "components" : isInside(path.join(dsRoot, "patterns"), file) ? "patterns" : null;
50
+ if (!kind) continue;
51
+ const relative = path.relative(root, file).replaceAll("\\", "/");
52
+ const line = declaration.getSourceFile().getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
53
+ const guides = documents.filter((doc) => doc.file.endsWith(".md") && path.dirname(doc.file) === path.dirname(file)).map((doc) => doc.uri);
54
+ items.push({ name: exported.name, kind, importFrom: config.publicAlias, file: relative, line, sourceUri: sourceUri(relative), guides,
55
+ previews: scenarios.filter(item => item.covers.includes(exported.name)).map(item => item.id) });
56
+ }
57
+ return items.sort((a, b) => a.name.localeCompare(b.name));
58
+ }
59
+
60
+ export function readProjectDocument(document) {
61
+ // Recheck after discovery so replacing an allowed file with a symlink cannot escape.
62
+ if (realpathSync(document.file) !== document.file) throw new Error("Project resource cannot resolve through a symlink.");
63
+ if (statSync(document.file).size > 2 * 1024 * 1024) throw new Error("Project resource exceeds the 2 MiB limit.");
64
+ return readFileSync(document.file, "utf8");
65
+ }
@@ -0,0 +1,69 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { sourceUri } from "./catalog.mjs";
4
+ import { projectGuideFiles } from "../project-documents.mjs";
5
+
6
+ export const GUIDE_URI = "ads://project/guide";
7
+ // Put the task triggers and entry path first: some hosts shorten server guidance.
8
+ export const serverInstructions = "Use ADS for UI work: components, patterns, tokens, themes, branding, layout, interaction, accessibility, motion and showcase review. Start with ads_status; if tools are not exposed, read ads://project/guide then ads://project/status. Use the returned projectRoot and commands. When ads_status.mutations.enabled, use ads_units and guarded ads_apply_patch for one owner at a time. Find existing public UI with ads_catalog; discover its previews with ads_scenarios and open ads_show_scenario. Starting ADS also serves its built-in showcase: use ads_status.showcaseUrl for browser review instead of starting another runtime/compiler or project browser app. Read one relevant ADS skill before editing. Inspect the real preview, edit its owned source, check the build and review again. Scope source/guides to the project, its instruction ancestry and user-selected import/reference sources; broad searches of /mnt or home are not ADS discovery. Domain-model/requirements work is a separate project workflow. MCP source checks do not replace browser tests; direct edits still need project checks. Reading HTML is not rendering, and reading a skill is not native activation. A running watcher is not proof of current source: inspect ads_status.build or ads://project/build after edits. Notifications may not reach the agent. Only explicitly enabled mutation tools edit project-owned ADS units after validation. Checks and compiler commands come from validated project configuration, never an ad hoc command argument.";
9
+
10
+ function manager(root, pkg) {
11
+ if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
12
+ const candidates = [["bun", "bun.lock"], ["bun", "bun.lockb"], ["npm", "package-lock.json"], ["pnpm", "pnpm-lock.yaml"], ["yarn", "yarn.lock"]];
13
+ const found = [...new Set(candidates.filter(([, file]) => existsSync(path.join(root, file))).map(([name]) => name))];
14
+ return found.length > 1 ? null : found[0] ?? "npm";
15
+ }
16
+
17
+ export function projectContext(options) {
18
+ const { root, dsRoot, sourceRoot, config } = options;
19
+ let pkg = {};
20
+ try { pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); } catch { /* A partial ADS can still be inspected. */ }
21
+ const packageManager = manager(root, pkg);
22
+ const scripts = pkg.scripts ?? {};
23
+ const commands = ["ads-mcp", "ads-read-mcp", "dev:showcase", "build:showcase", "build:mcp-previews", "lint:ds", "lint", "lint:style", "typecheck", "test", "build"].filter((name) => typeof scripts[name] === "string").map((script) => ({ script, cwd: root, args: packageManager ? [packageManager, "run", script] : null }));
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
+ return { name: pkg.name ?? path.basename(root), projectRoot: root, dsRoot, sourceRoot, framework: config.framework,
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
+ stylesEntry: path.join(dsRoot, config.stylesEntry), packageManager, commands, guides,
28
+ showcase: { client: "tool-owned-mcp", url: config.mcp.showcaseUrl, title: config.mcp.showcaseTitle ?? `${pkg.name ?? path.basename(root)} · Design System` },
29
+ runtime: { node: process.execPath, nodeBinDir: path.dirname(process.execPath), nodeVersion: process.version,
30
+ shellHint: "The agent shell may have a different PATH from MCP. If npm is missing, check this Node bin directory and prepend it to the shell PATH when npm is installed there; do not search the whole machine or assume a reinstall is needed." } };
31
+ }
32
+
33
+ export function workflow(previews) {
34
+ return { guideUri: GUIDE_URI,
35
+ next: "Find the requested public UI with ads_catalog (query + kind + small limit), then read its source/guide and one selected ADS skill. For a broad UI request, inspect the public barrel and targeted application entry files in sourceRoot.",
36
+ skills: [
37
+ { task: "Build, refine or debug UI using the project ADS", name: "ads-designer" },
38
+ { task: "Adapt a component-library reference", name: "ads-assimilate" },
39
+ { task: "Import from another project's design-system", name: "ads-import" },
40
+ { task: "Implement or review motion", name: "ads-motion" },
41
+ { task: "Stress-test layout, states and interactions", name: "ads-break-ui" },
42
+ ],
43
+ preview: previews.length ? "Use ads_scenarios to select an exact path-derived id, then ads_show_scenario. Public UI and related parts share addressable scenarios; composition/ holds larger scenarios. Showcase and MCP mount the same source with colocated fixtures. The showcase is the tool's minimal MCP client and Apps host; it discovers these entries and invokes the same preview tools. Both transports share the managed project compiler. A reusable sandboxed App loads selected modules and shared runtime/CSS/fonts through MCP resources; no project code or styling belongs in the outer host. Check build freshness after changes."
44
+ : "No component scenarios or legacy Apps are registered. Token inspection is available in the built-in showcase. Existing components need colocated independently mountable scenarios/fixtures and a project compiler before MCP can render them. Do not create a replica or separate browser app to get a preview.",
45
+ validation: "Use ads_check for core architecture diagnostics, then run the project's registered lint/typecheck/build and relevant tests from projectRoot. Core checks do not replace those commands. Review real UI in both themes; HTML delivery and compilation do not prove visual/behavioral correctness." };
46
+ }
47
+
48
+ export function projectGuide(context, previews) {
49
+ const route = workflow(previews);
50
+ return `# Start here: project design-system work
51
+
52
+ Use this server for UI components/patterns, tokens/themes/branding, layout, interaction, accessibility, motion and showcase review. Domain-model/requirements workflows are separate. Follow the project's actual instructions; this guide does not override them.
53
+
54
+ 1. Call ads_status (or read ads://project/status) to establish the exact workspace and capabilities. Project root: ${context.projectRoot}. ADS source: ${context.dsRoot}. Application source: ${context.sourceRoot}. The MCP project root may differ from the client's repository root.
55
+ 2. ${route.next} Public barrel resource: ${context.publicEntryUri ?? "missing; run ads_check and use ads-designer for setup"}. Normal targeted filesystem reads remain useful for editing and application integration; do not scan /mnt or home directories to rediscover this ADS.
56
+ 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
+ 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
+ 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. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit. 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
+ 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
+ 7. After edits, inspect ads_status.build or ads://project/build explicitly. Running/unreported/old output is not a current successful preview. Fix diagnostics in owned source, wait for rebuilding and invoke the preview tool again. Notifications may not start an agent turn.
62
+
63
+ Project guide resources: ${context.guides.map((guide) => `${guide.file}: ${guide.uri}`).join("; ") || "none discovered"}.
64
+
65
+ Node executable: ${context.runtime.node}. Node bin directory: ${context.runtime.nodeBinDir}. ${context.runtime.shellHint}
66
+
67
+ If the client exposes resources but no ADS tools, this guide and the project status/public-barrel/guide resources provide the entry path. Use the returned local paths for scoped source inspection and project commands. Do not claim a resource-only connection offers ADS tool calls, native Skills activation or a rendered App. Configure/enable the tools in the client when required; project edits still use local editor/filesystem tools.
68
+ `;
69
+ }