@reqvire-org/ads 0.3.3 → 0.3.5
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.
- package/README.md +12 -4
- package/dist/behaviors.mjs +157 -0
- package/dist/bootstrap-ds/ADS.md +7 -0
- package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/check.mjs +7 -2
- package/dist/cli.mjs +17 -0
- package/dist/config.mjs +19 -3
- package/dist/css-values.mjs +26 -0
- package/dist/index.mjs +2 -0
- package/dist/init.mjs +1 -1
- package/dist/mcp/build-status.mjs +10 -3
- package/dist/mcp/catalog.mjs +11 -8
- package/dist/mcp/context.mjs +2 -2
- package/dist/mcp/mutations.mjs +10 -0
- package/dist/mcp/preview-owner-guard.mjs +29 -0
- package/dist/mcp/preview-watch.mjs +28 -4
- package/dist/mcp/preview-widget.bundle.js +2 -2
- package/dist/mcp/prompts.mjs +2 -2
- package/dist/mcp/server.mjs +56 -19
- package/dist/mcp/showcase-host.mjs +9 -5
- package/dist/public-api.mjs +39 -0
- package/dist/scenarios-compiler.mjs +17 -2
- package/dist/scenarios.mjs +27 -39
- package/dist/skills/README.md +3 -3
- package/dist/skills/ads-break-ui/SKILL.md +22 -0
- package/dist/skills/ads-designer/SKILL.md +23 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +7 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/skills/ads-designer/references/cli.md +1 -1
- package/dist/skills/ads-designer/references/design-craft.md +1 -1
- package/dist/skills/ads-import/SKILL.md +22 -0
- package/dist/skills/ads-import/references/cli.md +1 -1
- package/dist/skills/{ads-assimilate → ads-migrate}/SKILL.md +28 -6
- package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
- package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
- package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +11 -0
- package/dist/skills/ads-motion/SKILL.md +22 -0
- package/dist/tokens.mjs +3 -2
- package/dist/units.mjs +20 -9
- package/dist/verification/browser.mjs +27 -0
- package/dist/verification/playwright-reporter.cjs +20 -0
- package/dist/verification.mjs +214 -0
- package/docs/ADS.md +27 -2
- package/docs/ARCHITECTURE.md +39 -1
- package/docs/BEHAVIORS.md +230 -0
- package/docs/CHANGELOG.md +36 -0
- package/docs/GUIDE.md +16 -1
- package/docs/MCP.md +46 -7
- package/package.json +4 -2
- package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/testing.md +0 -0
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ Linaria; integrate its extraction with your build as described in the
|
|
|
58
58
|
| `ads init --framework react` | Set up ADS in the current project; also accepts `preact`; saved choices can omit the flag |
|
|
59
59
|
| `ads check` | Check tokens, styles, imports, and ADS ownership |
|
|
60
60
|
| `ads tokens` | Inspect token values and themes |
|
|
61
|
+
| `ads behaviors` | Inspect registered behavior guarantees and current evidence |
|
|
62
|
+
| `ads verify --unit <id>` | Run an owner's registered behavior checks |
|
|
61
63
|
| `ads assets` | Generate standard web icons from the project's SVG |
|
|
62
64
|
| `ads lint-config` | Generate lint config; normally called by the build |
|
|
63
65
|
| `ads mcp` | Start the stdio MCP server and its local showcase |
|
|
@@ -92,9 +94,15 @@ fixtures, and runs the guards before applying it. Foundation styles, original
|
|
|
92
94
|
assets, guides and project configuration/guard extensions have editable units too. See [guarded editing](docs/MCP.md#guarded-editing).
|
|
93
95
|
|
|
94
96
|
The server exposes public components/patterns, token values,
|
|
95
|
-
source guides, architecture checks,
|
|
97
|
+
source guides, controllers/hooks, architecture checks, behavioral verification,
|
|
98
|
+
workflow prompts, and packaged ADS skills.
|
|
96
99
|
It supports older MCP clients and the newer Skills protocol.
|
|
97
100
|
|
|
101
|
+
Private controllers stay beside their component; shared controllers have their own
|
|
102
|
+
edit units. Optional behavior contracts connect guarantees to maintained checks.
|
|
103
|
+
Demonstrations and passing execution evidence remain distinct. See
|
|
104
|
+
[controllers and verification](docs/BEHAVIORS.md).
|
|
105
|
+
|
|
98
106
|
Experimental MCP Apps show an interactive token inspector and project-built
|
|
99
107
|
token, component, pattern and composition scenarios. They use the same public implementations and fixtures
|
|
100
108
|
as the showcase. Text and resource access remain available without Apps support.
|
|
@@ -202,13 +210,13 @@ app's bundler. With no icon source, generation is skipped. See the
|
|
|
202
210
|
Install skills with `npx skills`:
|
|
203
211
|
|
|
204
212
|
```sh
|
|
205
|
-
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-
|
|
213
|
+
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
206
214
|
```
|
|
207
215
|
|
|
208
216
|
For local development, install from this checkout:
|
|
209
217
|
|
|
210
218
|
```sh
|
|
211
|
-
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-
|
|
219
|
+
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
212
220
|
```
|
|
213
221
|
|
|
214
222
|
Acknowledgments: `ads-designer`'s craft/mobile guidance, `ads-break-ui`, and
|
|
@@ -220,7 +228,7 @@ notice beside its files.
|
|
|
220
228
|
|
|
221
229
|
- [ads-designer](skills/ads-designer/SKILL.md): start an ADS or build UI with your local ADS.
|
|
222
230
|
- [ads-import](skills/ads-import/SKILL.md): import external components, patterns, styles, token values, or brand artwork.
|
|
223
|
-
- [ads-
|
|
231
|
+
- [ads-migrate](skills/ads-migrate/SKILL.md): implement a native component inspired by an external library.
|
|
224
232
|
- [ads-break-ui](skills/ads-break-ui/SKILL.md): stress-test real components and patterns with difficult content.
|
|
225
233
|
- [ads-motion](skills/ads-motion/SKILL.md): implement, review, audit, or profile component animation.
|
|
226
234
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isBuiltin } from "node:module";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles } from "./config.mjs";
|
|
7
|
+
import { importResolver, projectCompilerOptions } from "./boundaries.mjs";
|
|
8
|
+
import { inspectUnits } from "./units.mjs";
|
|
9
|
+
import { publicApi } from "./public-api.mjs";
|
|
10
|
+
import { inspectScenarios } from "./scenarios.mjs";
|
|
11
|
+
import { uiOperation } from "./ui-framework.mjs";
|
|
12
|
+
|
|
13
|
+
const id = z.string().regex(/^[a-zA-Z0-9][\w.-]{0,99}$/);
|
|
14
|
+
const reference = z.string().min(1).max(500);
|
|
15
|
+
const subject = z.union([
|
|
16
|
+
z.object({ api: reference }).strict(),
|
|
17
|
+
z.object({ source: reference, symbol: reference.optional() }).strict(),
|
|
18
|
+
]);
|
|
19
|
+
export const behaviorSchema = z.object({
|
|
20
|
+
schemaVersion: z.literal(1),
|
|
21
|
+
subjects: z.record(id, subject).refine(value => Object.keys(value).length > 0 && Object.keys(value).length <= 100),
|
|
22
|
+
guarantees: z.array(z.object({ id, description: z.string().min(1).max(2000), subjects: z.array(id).min(1).max(100), checks: z.array(id).min(1).max(100) }).strict()).min(1).max(100),
|
|
23
|
+
checks: z.record(id, z.object({ file: reference, case: id, scenario: reference.optional(), profile: id }).strict()).refine(value => Object.keys(value).length > 0 && Object.keys(value).length <= 100),
|
|
24
|
+
}).strict();
|
|
25
|
+
|
|
26
|
+
/** Static contracts never execute project code or certify natural-language claims. */
|
|
27
|
+
export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
28
|
+
const { root, dsRoot, config } = options;
|
|
29
|
+
const inventory = inspectUnits(options);
|
|
30
|
+
const files = sourceFiles(dsRoot).filter(isBehaviorFile);
|
|
31
|
+
if (!files.length) return { contracts: [], diagnostics: [] };
|
|
32
|
+
const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
|
|
33
|
+
const checker = program.getTypeChecker();
|
|
34
|
+
const apis = new Map(publicApi(program, options).map(item => [item.name, item]));
|
|
35
|
+
const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
|
|
36
|
+
const diagnostics = [], contracts = [], owners = new Set();
|
|
37
|
+
const relative = file => path.relative(root, file).replaceAll("\\", "/");
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const report = message => diagnostics.push({ file: relative(file), rule: "behavior-contract", line: 1, column: 1, message });
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
if (statSync(file).size > 128 * 1024 || realpathSync(file) !== file) throw new Error("Behavior contracts must be regular files of at most 128 KiB.");
|
|
43
|
+
const parsed = behaviorSchema.safeParse(JSON.parse(readFileSync(file, "utf8")));
|
|
44
|
+
if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map(issue => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
45
|
+
value = parsed.data;
|
|
46
|
+
} catch (error) { report(error.message); continue; }
|
|
47
|
+
const unit = inventory.owner(inventory.relative(relative(file)));
|
|
48
|
+
if (!["components", "patterns", "controllers"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern or controller unit."); continue; }
|
|
49
|
+
if (owners.has(unit)) { report(`Only one behavior contract may describe ${unit}.`); continue; }
|
|
50
|
+
owners.add(unit);
|
|
51
|
+
function owned(target, field) {
|
|
52
|
+
if (path.isAbsolute(target) || target.includes("\\") || target.includes("\0")) { report(`${field} must be an owner-local relative path.`); return null; }
|
|
53
|
+
const absolute = path.resolve(path.dirname(file), target);
|
|
54
|
+
if (!isInside(dsRoot, absolute) || inventory.owner(inventory.relative(relative(absolute))) !== unit
|
|
55
|
+
|| !existsSync(absolute) || !statSync(absolute).isFile() || realpathSync(absolute) !== absolute) { report(`${field} must resolve to a real file in ${unit}: ${target}`); return null; }
|
|
56
|
+
return absolute;
|
|
57
|
+
}
|
|
58
|
+
for (const [name, entry] of Object.entries(value.subjects)) {
|
|
59
|
+
if (entry.api) {
|
|
60
|
+
const api = apis.get(entry.api);
|
|
61
|
+
if (!api || inventory.owner(inventory.relative(relative(api.file))) !== unit) report(`Subject ${name} must resolve to a public API owned by ${unit}: ${entry.api}`);
|
|
62
|
+
} else {
|
|
63
|
+
const source = owned(entry.source, `Subject ${name}`);
|
|
64
|
+
if (source && isDevelopmentFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
|
|
65
|
+
if (source && entry.symbol) {
|
|
66
|
+
const ast = program.getSourceFile(source);
|
|
67
|
+
if (!ast || !checker.getSymbolsInScope(ast, ts.SymbolFlags.Value).some(symbol => symbol.name === entry.symbol && symbol.declarations?.some(declaration => declaration.getSourceFile() === ast))) report(`Subject ${name}: symbol ${entry.symbol} is not declared in ${entry.source}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const guaranteeIds = new Set();
|
|
72
|
+
for (const guarantee of value.guarantees) {
|
|
73
|
+
if (guaranteeIds.has(guarantee.id)) report(`Duplicate guarantee ID ${guarantee.id}.`);
|
|
74
|
+
guaranteeIds.add(guarantee.id);
|
|
75
|
+
for (const name of guarantee.subjects) if (!Object.hasOwn(value.subjects, name)) report(`Guarantee ${guarantee.id} references unknown subject ${name}.`);
|
|
76
|
+
for (const check of guarantee.checks) if (!Object.hasOwn(value.checks, check)) report(`Guarantee ${guarantee.id} references unknown check ${check}.`);
|
|
77
|
+
}
|
|
78
|
+
const cases = new Set();
|
|
79
|
+
for (const [name, check] of Object.entries(value.checks)) {
|
|
80
|
+
const target = owned(check.file, `Check ${name}`);
|
|
81
|
+
if (target && !isDevelopmentFile(target, options)) report(`Check ${name} must use a development-only test/spec file.`);
|
|
82
|
+
if (!Object.hasOwn(config.mcp.behaviorProfiles, check.profile)) report(`Check ${name} references unregistered profile ${check.profile}. Register its literal runner command in ds.config.json; checks cannot install packages or assume host globals.`);
|
|
83
|
+
if (check.scenario && !scenarios.some(scenario => scenario.id === check.scenario)) report(`Check ${name} references unknown scenario ${check.scenario}.`);
|
|
84
|
+
const identity = JSON.stringify([check.file, check.case, check.profile]);
|
|
85
|
+
if (cases.has(identity)) report(`Duplicate execution identity for check ${name}. Use one check ID and reference it from several guarantees.`);
|
|
86
|
+
cases.add(identity);
|
|
87
|
+
}
|
|
88
|
+
contracts.push({ ...value, unit, file: relative(file), demonstrations: Object.fromEntries(Object.entries(value.subjects).filter(([, item]) => item.api).map(([name, item]) => [name, scenarios.filter(scenario => scenario.exercises?.includes(item.api)).map(scenario => scenario.id)])) });
|
|
89
|
+
}
|
|
90
|
+
return { contracts, diagnostics };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Headless ownership applies to shared controllers and explicitly named private subjects. */
|
|
94
|
+
export function inspectControllers(options, program, report, contracts = []) {
|
|
95
|
+
const { root, dsRoot, config } = options;
|
|
96
|
+
const resolve = importResolver(options);
|
|
97
|
+
const pkg = existsSync(path.join(root, "package.json")) ? JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) : {};
|
|
98
|
+
const declared = new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies, ...pkg.optionalDependencies }));
|
|
99
|
+
const privateFiles = new Map();
|
|
100
|
+
for (const contract of contracts) for (const subject of Object.values(contract.subjects).filter(item => item.source)) {
|
|
101
|
+
const file = path.resolve(root, path.dirname(contract.file), subject.source);
|
|
102
|
+
if (!privateFiles.has(file)) privateFiles.set(file, new Set());
|
|
103
|
+
privateFiles.get(file).add(subject.symbol ?? null);
|
|
104
|
+
}
|
|
105
|
+
const checker = program.getTypeChecker();
|
|
106
|
+
for (const source of program.getSourceFiles()) {
|
|
107
|
+
const file = source.fileName;
|
|
108
|
+
if (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "controllers"), file) && !privateFiles.has(file)) continue;
|
|
109
|
+
const wholeFile = isInside(path.join(dsRoot, "controllers"), file) || privateFiles.get(file)?.has(null);
|
|
110
|
+
const subjects = wholeFile ? [] : checker.getSymbolsInScope(source, ts.SymbolFlags.Value).filter(symbol => privateFiles.get(file)?.has(symbol.name)).flatMap(symbol => symbol.declarations ?? []);
|
|
111
|
+
const headless = node => wholeFile || subjects.some(subject => subject.getSourceFile() === source && node.pos >= subject.pos && node.end <= subject.end);
|
|
112
|
+
function emit(node, rule, message) { const p = source.getLineAndCharacterOfPosition(node.getStart()); report(file, rule, message, p.line + 1, p.character + 1); }
|
|
113
|
+
function visit(node) {
|
|
114
|
+
let specifier;
|
|
115
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) specifier = node.moduleSpecifier.text;
|
|
116
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) specifier = node.arguments[0].text;
|
|
117
|
+
if (specifier) {
|
|
118
|
+
const typeOnly = ts.isImportDeclaration(node) && (node.importClause?.isTypeOnly || node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && !node.importClause.name && node.importClause.namedBindings.elements.every(item => item.isTypeOnly)) || ts.isExportDeclaration(node) && node.isTypeOnly;
|
|
119
|
+
if (!typeOnly) {
|
|
120
|
+
if (wholeFile && /^(?:@linaria\/|.*\.css(?:[?#].*)?$)/.test(specifier)) emit(node, "controller-skin", "Controllers own behavior and measurements. Put CSS/Linaria skins in the consuming component or pattern.");
|
|
121
|
+
const framework = /^(?:react|react-dom)(?:\/|$)/.test(specifier) ? "react" : /^preact(?:\/|$)/.test(specifier) ? "preact" : null;
|
|
122
|
+
if (framework && config.framework !== framework) emit(node, "controller-framework", `Controller imports ${framework}, but ds.config.json selects ${config.framework ?? "no framework"}. Use the configured framework or plain TypeScript/browser APIs; MCP hosts do not supply another runtime.`);
|
|
123
|
+
const targets = resolve(specifier, file);
|
|
124
|
+
const local = specifier.startsWith(".") || specifier === config.publicAlias || targets.some(target => isInside(root, target) && !target.includes(`${path.sep}node_modules${path.sep}`) && existsSync(target));
|
|
125
|
+
if (local) {
|
|
126
|
+
if (targets.some(target => !existsSync(target))) emit(node, "controller-dependency", `Unresolved controller dependency ${specifier}. Declare real project dependencies and local modules; do not assume MCP host globals.`);
|
|
127
|
+
if (isInside(path.join(dsRoot, "controllers"), file)) {
|
|
128
|
+
const targetsInUi = targets.some(target => ["components", "patterns"].some(kind => isInside(path.join(dsRoot, kind), target)));
|
|
129
|
+
if (targetsInUi) emit(node, "controller-dependency", "Shared controllers cannot import component/pattern runtime implementations. Pass behavior adapters or callbacks from the consumer; type-only imports are allowed.");
|
|
130
|
+
if (specifier === config.publicAlias) {
|
|
131
|
+
for (const item of node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) ? node.importClause.namedBindings.elements : []) {
|
|
132
|
+
let symbol = checker.getSymbolAtLocation(item.name);
|
|
133
|
+
if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
134
|
+
if (symbol?.declarations?.some(declaration => ["components", "patterns"].some(kind => isInside(path.join(dsRoot, kind), declaration.getSourceFile().fileName)))) emit(item, "controller-dependency", "Shared controllers consume behavior helpers, not styled component/pattern implementations.");
|
|
135
|
+
}
|
|
136
|
+
if (!node.importClause?.namedBindings || ts.isNamespaceImport(node.importClause.namedBindings)) emit(node, "controller-dependency", "Use explicit behavior imports in shared controllers so dependency ownership can be checked.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else if (!isBuiltin(specifier)) {
|
|
140
|
+
const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
141
|
+
if (!declared.has(name)) emit(node, "controller-dependency", `Undeclared controller dependency ${name}. Register it in the project package; ADS does not install dependencies or receive them from an MCP host.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (headless(node) && ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)
|
|
146
|
+
|| ts.isCallExpression(node) && uiOperation(node.expression, checker) === "createElement" && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]))) emit(node, "controller-skin", "Visual DOM markup belongs in components/patterns. Controllers may use framework hooks and context providers.");
|
|
147
|
+
if (!wholeFile && headless(node) && (ts.isTaggedTemplateExpression(node) || ts.isCallExpression(node))) {
|
|
148
|
+
let base = ts.isTaggedTemplateExpression(node) ? node.tag : node.expression;
|
|
149
|
+
while (ts.isPropertyAccessExpression(base) || ts.isCallExpression(base)) base = base.expression;
|
|
150
|
+
if (checker.getSymbolAtLocation(base)?.declarations?.some(declaration => ts.isImportSpecifier(declaration) && declaration.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/")
|
|
151
|
+
|| ts.isNamespaceImport(declaration) && declaration.parent.parent.moduleSpecifier.text.startsWith("@linaria/"))) emit(node, "controller-skin", "Keep Linaria styling outside the declared private controller subject, in its visual consumer.");
|
|
152
|
+
}
|
|
153
|
+
ts.forEachChild(node, visit);
|
|
154
|
+
}
|
|
155
|
+
visit(source);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/bootstrap-ds/ADS.md
CHANGED
|
@@ -241,3 +241,10 @@ Run `npm run build` after adding or changing components. Add meaningful tests
|
|
|
241
241
|
for stateful behavior, accessibility, gestures, or async lifecycle where needed.
|
|
242
242
|
Run `npm run dev` to inspect the real showcase. A screenshot alone does not
|
|
243
243
|
verify interaction or animation. Generated output and dependencies are not source.
|
|
244
|
+
|
|
245
|
+
Shared behavior may live in `controllers/<Name>/`; private controllers stay with
|
|
246
|
+
their component/pattern. Use the configured framework and declared dependencies.
|
|
247
|
+
Behavior contracts/checks are development-only, and scenario `exercises` links
|
|
248
|
+
public hooks to demonstrations separately from rendered `covers`. Read the
|
|
249
|
+
installed ADS package's `docs/BEHAVIORS.md` or MCP `ads://guides/behaviors` before
|
|
250
|
+
registering guarantees, profiles and verification.
|
|
File without changes
|
package/dist/check.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { inspectShowcaseCoverage } from "./showcase-coverage.mjs";
|
|
|
15
15
|
import { inspectScenarioCoverage } from "./scenarios.mjs";
|
|
16
16
|
import { uiOperation, importedUiApi } from "./ui-framework.mjs";
|
|
17
17
|
import { inspectStyleBoundaries, pointerCaptureAdvisories } from "./style-boundaries.mjs";
|
|
18
|
+
import { inspectBehaviors, inspectControllers } from "./behaviors.mjs";
|
|
19
|
+
import { numericCssUnit, tokenLengthLiteral } from "./css-values.mjs";
|
|
18
20
|
import { foundationDeclaration, foundationSelector } from "./foundation.mjs";
|
|
19
21
|
|
|
20
22
|
const scenarioLayoutGuidance = "Put demo-only sizing and framing in a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
|
|
@@ -51,7 +53,10 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
51
53
|
inspectStyleBoundaries(options, program, report);
|
|
52
54
|
inspectShowcasePages(options, program, resolveImport, report);
|
|
53
55
|
inspectShowcaseCoverage(options, program, report);
|
|
54
|
-
inspectScenarioCoverage(options, program, report);
|
|
56
|
+
inspectScenarioCoverage(options, program, report, (file, rule, message) => advisories.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line: 1, column: 1 }));
|
|
57
|
+
const behaviors = inspectBehaviors(options, program);
|
|
58
|
+
diagnostics.push(...behaviors.diagnostics);
|
|
59
|
+
inspectControllers(options, program, report, behaviors.contracts);
|
|
55
60
|
// The foundation remains a fixed import-only entry, not an extra styling layer.
|
|
56
61
|
const foundationEntry = path.join(dsRoot, config.stylesEntry);
|
|
57
62
|
try {
|
|
@@ -114,7 +119,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
114
119
|
const violations = new Set();
|
|
115
120
|
valueParser(decl.value).walk((node) => {
|
|
116
121
|
if (node.type === "function" && node.value === "url") return false;
|
|
117
|
-
if (node.type === "word" &&
|
|
122
|
+
if (node.type === "word" && (/^#[\da-f]{3,8}$/i.test(node.value) || tokenLengthLiteral(node.value) || ["s", "ms"].includes(numericCssUnit(node.value)))) violations.add("Use a token for colors, absolute/font-relative lengths (including ch), and durations. Unitless geometry and structural angle/viewport/container relationships remain valid.");
|
|
118
123
|
if (node.type === "function" && /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix|cubic-bezier)$/.test(node.value)) violations.add("Define color formulas and easing in tokens.");
|
|
119
124
|
if (node.type === "function" && /^(?:blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|saturate|sepia)$/.test(node.value)) violations.add("Define visual filter effects in tokens.");
|
|
120
125
|
if (node.type === "word" && /^ease(?:-in|-out|-in-out)?$/.test(node.value)) violations.add("Use a motion token for easing.");
|
package/dist/cli.mjs
CHANGED
|
@@ -11,6 +11,8 @@ Usage: ads init [options] Run inside your project
|
|
|
11
11
|
|
|
12
12
|
init Set up ADS in the current project (requires package.json)
|
|
13
13
|
check [directory] Check tokens, ownership, imports, and showcase coverage
|
|
14
|
+
behaviors [dir] Inspect behavior contracts and current verification evidence
|
|
15
|
+
verify [directory] Run registered behavior checks (--unit and --check select)
|
|
14
16
|
tokens [directory] Inspect local token values and theme definitions
|
|
15
17
|
assets [directory] Generate standard web icons in .ds/assets from assets.iconSource
|
|
16
18
|
lint-config [dir] Generate .ds/oxlintrc.json with core and project plugins
|
|
@@ -25,6 +27,8 @@ Usage: ads init [options] Run inside your project
|
|
|
25
27
|
--port number MCP/showcase only: loopback website port (default: automatic)
|
|
26
28
|
--no-showcase MCP only: disable the local website
|
|
27
29
|
--mutations MCP only: enable guarded ADS source editing
|
|
30
|
+
--unit id Verify only: select one discovered behavior contract owner
|
|
31
|
+
--check id Verify only: select a registered check (repeatable)
|
|
28
32
|
|
|
29
33
|
External components and examples are imported using the ADS skills.
|
|
30
34
|
No component library or example catalogue is bundled.
|
|
@@ -38,6 +42,8 @@ async function main(args) {
|
|
|
38
42
|
let packageManager;
|
|
39
43
|
let framework;
|
|
40
44
|
let port;
|
|
45
|
+
let unit;
|
|
46
|
+
const selectedChecks = [];
|
|
41
47
|
let showcase = true;
|
|
42
48
|
let mutations = false;
|
|
43
49
|
const positional = [];
|
|
@@ -46,6 +52,12 @@ async function main(args) {
|
|
|
46
52
|
if (argument === "--json") json = true;
|
|
47
53
|
else if (argument === "--no-showcase") showcase = false;
|
|
48
54
|
else if (argument === "--mutations") mutations = true;
|
|
55
|
+
else if (argument === "--unit" || argument === "--check") {
|
|
56
|
+
const value = args[++index];
|
|
57
|
+
if (!value || value.startsWith("-")) throw new Error(`${argument} needs a discovered unit/check ID.`);
|
|
58
|
+
if (argument === "--unit") { if (unit) throw new Error("Specify one --unit."); unit = value; }
|
|
59
|
+
else selectedChecks.push(value);
|
|
60
|
+
}
|
|
49
61
|
else if (argument === "--port") {
|
|
50
62
|
const value = args[++index];
|
|
51
63
|
if (port !== undefined || !/^\d+$/.test(value ?? "") || Number(value) > 65535) throw new Error("--port requires one port between 0 and 65535.");
|
|
@@ -72,6 +84,7 @@ async function main(args) {
|
|
|
72
84
|
if (packageManager !== undefined && command !== "init") throw new Error("--package-manager is supported only by init.");
|
|
73
85
|
if (framework !== undefined && command !== "init") throw new Error("--framework is supported only by init.");
|
|
74
86
|
if (command === "init" && directory) throw new Error("Run ads init inside the project, without a directory argument.");
|
|
87
|
+
if ((unit || selectedChecks.length) && command !== "verify") throw new Error("--unit and --check are supported only by verify.");
|
|
75
88
|
const target = path.resolve(directory ?? process.cwd());
|
|
76
89
|
if (command === "mcp" || command === "showcase") {
|
|
77
90
|
if (json) throw new Error("--json is not supported by mcp/showcase; mcp reserves stdout for JSON-RPC.");
|
|
@@ -82,6 +95,8 @@ async function main(args) {
|
|
|
82
95
|
let result;
|
|
83
96
|
if (command === "init") result = initProject(target, { toolPackage, packageManager, framework });
|
|
84
97
|
else if (command === "check") result = checkProject(target);
|
|
98
|
+
else if (command === "behaviors") { const {behaviorStatus}=await import("./verification.mjs"); const {loadConfig}=await import("./config.mjs"); result=behaviorStatus(loadConfig(target)); }
|
|
99
|
+
else if (command === "verify") { const {createVerificationController}=await import("./verification.mjs"); const verification=createVerificationController(target); try {result=await verification.run({...(unit?{unit}:{}),...(selectedChecks.length?{checks:selectedChecks}:{})});} finally {await verification.close();} }
|
|
85
100
|
else if (command === "tokens") result = inspectTokens(target);
|
|
86
101
|
else if (command === "assets") result = await generateBrandAssets(target);
|
|
87
102
|
else if (command === "lint-config") result = generateLintConfig(target);
|
|
@@ -90,6 +105,8 @@ async function main(args) {
|
|
|
90
105
|
else if (command === "init") console.log(`Initialized ${target}\nFramework: ${result.framework}\nAdded ${result.toolDependency.name} (${result.toolDependency.spec}) as a development dependency.\nRun ${result.packageManager} install and ${result.packageManager} run build; the build refreshes lint configuration, runs ADS checks, and generates configured brand assets automatically.`);
|
|
91
106
|
else if (command === "assets") console.log(result.skipped ? "No assets.iconSource configured; brand generation skipped." : `Generated ${result.files.length} brand assets in ${result.directory}.`);
|
|
92
107
|
else if (command === "lint-config") console.log(`Generated ${result.file}\nProjects initialized with ads init refresh this automatically during lint:ds/build.`);
|
|
108
|
+
else if (command === "behaviors") console.log(JSON.stringify(result, null, 2));
|
|
109
|
+
else if (command === "verify") { for (const item of result.results) console.log(`${item.unit}#${item.check}: ${item.status} (${item.profile})`); }
|
|
93
110
|
else if (command === "tokens") {
|
|
94
111
|
for (const token of result.tokens) console.log(`${token.name}${token.type ? ` (${token.type})` : ""}: ${token.definitions.map((entry) => `${entry.scope}: ${entry.value}`).join("; ")}`);
|
|
95
112
|
} else if (result.ok) console.log(`ADS check passed: ${target}`);
|
package/dist/config.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const defaultConfig = {
|
|
|
17
17
|
boundaries: { styleWrites: [], componentVariables: [] },
|
|
18
18
|
assets: { publicDirs: ["public/assets"], generatedPaths: ["dist", "dist-showcase", ".vite", ".ds"], generatedFiles: [], iconSource: null },
|
|
19
19
|
oxlint: { plugins: [], rules: {}, overrides: [] },
|
|
20
|
-
mcp: { showcaseUrl: null, showcaseTitle: null, apps: [], previewWatch: null, previewManifest: null, mutationChecks: null },
|
|
20
|
+
mcp: { showcaseUrl: null, showcaseTitle: null, apps: [], previewWatch: null, previewManifest: null, mutationChecks: null, behaviorProfiles: {} },
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export function isInside(parent, file) {
|
|
@@ -100,6 +100,18 @@ export function loadConfig(directory = process.cwd()) {
|
|
|
100
100
|
}
|
|
101
101
|
if (config.mcp.showcaseTitle !== null && (typeof config.mcp.showcaseTitle !== "string" || !config.mcp.showcaseTitle.trim() || config.mcp.showcaseTitle.length > 120)) throw new Error("mcp.showcaseTitle must be a non-empty title of at most 120 characters or null.");
|
|
102
102
|
if (!Array.isArray(config.mcp.apps)) throw new Error("mcp.apps must be an array of built project previews.");
|
|
103
|
+
if (!config.mcp.behaviorProfiles || typeof config.mcp.behaviorProfiles !== "object" || Array.isArray(config.mcp.behaviorProfiles) || Object.keys(config.mcp.behaviorProfiles).length > 20) throw new Error("mcp.behaviorProfiles must be an object of up to 20 registered execution profiles.");
|
|
104
|
+
for (const [name, profile] of Object.entries(config.mcp.behaviorProfiles)) {
|
|
105
|
+
if (!/^[a-zA-Z0-9][\w.-]{0,79}$/.test(name) || !profile || typeof profile !== "object" || Array.isArray(profile)
|
|
106
|
+
|| Object.keys(profile).some(key => !["command", "args", "timeoutMs", "runner", "framework", "mode"].includes(key))
|
|
107
|
+
|| typeof profile.command !== "string" || !profile.command.trim() || profile.command.includes("\0")
|
|
108
|
+
|| !Array.isArray(profile.args) || profile.args.some(arg => typeof arg !== "string" || arg.includes("\0"))
|
|
109
|
+
|| !["protocol", "playwright"].includes(profile.runner ?? "protocol")
|
|
110
|
+
|| profile.framework !== undefined && profile.framework !== config.framework
|
|
111
|
+
|| profile.mode !== undefined && !["browser", "strict", "unit"].includes(profile.mode)
|
|
112
|
+
|| profile.timeoutMs !== undefined && (!Number.isInteger(profile.timeoutMs) || profile.timeoutMs < 100 || profile.timeoutMs > 300000)) throw new Error(`Invalid behavior profile ${name}. Use literal executable/args, a protocol or Playwright runner, a matching framework and a bounded timeout. Profiles do not install dependencies or receive APIs from an MCP host.`);
|
|
113
|
+
if (profile.mode === "strict" && config.framework !== "react") throw new Error(`Behavior profile ${name}: React StrictMode requires framework react; use the configured framework's lifecycle tests instead.`);
|
|
114
|
+
}
|
|
103
115
|
if (config.mcp.mutationChecks !== null && (!Array.isArray(config.mcp.mutationChecks) || config.mcp.mutationChecks.length > 12 || new Set(config.mcp.mutationChecks).size !== config.mcp.mutationChecks.length || config.mcp.mutationChecks.some(name => typeof name !== "string" || !/^[a-zA-Z0-9][\w:.-]*$/.test(name)))) throw new Error("mcp.mutationChecks must be null or up to 12 unique package script names.");
|
|
104
116
|
if (config.mcp.previewManifest !== null) {
|
|
105
117
|
const manifest = projectPath(root, config.mcp.previewManifest, "mcp.previewManifest");
|
|
@@ -136,10 +148,14 @@ export function isFixtureFile(file) {
|
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
/** File role, rather than folder location, keeps colocated preview code private. */
|
|
151
|
+
export function isBehaviorFile(file) {
|
|
152
|
+
return /(?:^|[\\/])behavior\.json$|\.behavior\.json$/.test(file);
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
export function isDevelopmentFile(file, { dsRoot }) {
|
|
140
156
|
if (!isInside(dsRoot, file)) return isTestFile(file);
|
|
141
157
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
142
|
-
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file)
|
|
158
|
+
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file)
|
|
143
159
|
|| /^(?:showcase|composition)\//.test(relative)
|
|
144
160
|
|| /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
|
|
145
161
|
|| /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
|
|
@@ -154,7 +170,7 @@ export function sourceFiles(root) {
|
|
|
154
170
|
if (ignored.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
155
171
|
const file = path.join(root, entry.name);
|
|
156
172
|
if (entry.isDirectory()) files.push(...sourceFiles(file));
|
|
157
|
-
else if (entry.isFile() && /\.(css|html|[cm]?[jt]sx?)$/.test(entry.name)) files.push(file);
|
|
173
|
+
else if (entry.isFile() && (/\.(css|html|[cm]?[jt]sx?)$/.test(entry.name) || isBehaviorFile(entry.name))) files.push(file);
|
|
158
174
|
}
|
|
159
175
|
return files.sort();
|
|
160
176
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import valueParser from "postcss-value-parser";
|
|
2
|
+
|
|
3
|
+
// CSS-language categories, independent of any project's token names or values.
|
|
4
|
+
// https://www.w3.org/TR/css-values-4/#lengths
|
|
5
|
+
const absoluteLengths = new Set(["px", "cm", "mm", "q", "in", "pt", "pc"]);
|
|
6
|
+
const fontLengths = new Set(["em", "rem", "ex", "rex", "cap", "rcap", "ch", "rch", "ic", "ric", "lh", "rlh"]);
|
|
7
|
+
const responsiveLength = /^(?:[sld]?v(?:w|h|i|b|min|max)|cq(?:w|h|i|b|min|max))$/;
|
|
8
|
+
|
|
9
|
+
export function numericCssUnit(value) {
|
|
10
|
+
const parsed = valueParser.unit(value);
|
|
11
|
+
return parsed ? parsed.unit.toLowerCase() : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Skin lengths use tokens regardless of font metrics, physical units or notation.
|
|
15
|
+
* Responsive viewport/container relationships retain their structural treatment. */
|
|
16
|
+
export function tokenLengthLiteral(value) {
|
|
17
|
+
const unit = numericCssUnit(value);
|
|
18
|
+
return absoluteLengths.has(unit) || fontLengths.has(unit);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The same CSS categories apply to length-valued foundation tokens. */
|
|
22
|
+
export function dimensionValue(value) {
|
|
23
|
+
const unit = numericCssUnit(value);
|
|
24
|
+
return unit !== null && (absoluteLengths.has(unit) || fontLengths.has(unit)
|
|
25
|
+
|| responsiveLength.test(unit) || unit === "%" || unit === "" && Number(value) === 0);
|
|
26
|
+
}
|
package/dist/index.mjs
CHANGED
|
@@ -7,3 +7,5 @@ export { generateBrandAssets } from "./brand-assets.mjs";
|
|
|
7
7
|
// same validated configuration and property boundary as the core checker.
|
|
8
8
|
export { loadConfig } from "./config.mjs";
|
|
9
9
|
export { importResolver, styleWriteViolation } from "./boundaries.mjs";
|
|
10
|
+
export { inspectBehaviors } from "./behaviors.mjs";
|
|
11
|
+
export { behaviorStatus, createVerificationController } from "./verification.mjs";
|
package/dist/init.mjs
CHANGED
|
@@ -183,7 +183,7 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
183
183
|
}
|
|
184
184
|
updates.set(entryFile, current + "\n" + reviewExport);
|
|
185
185
|
}
|
|
186
|
-
for (const section of ["components", "patterns", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
|
|
186
|
+
for (const section of ["components", "patterns", "controllers", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
|
|
187
187
|
addMissing(path.join(dsRoot, "assets/brand/README.md"), readFileSync(path.join(starter, "design-system/assets/brand/README.md")));
|
|
188
188
|
addMissing(path.join(dsRoot, "assets/fonts/.gitkeep"), "");
|
|
189
189
|
const configFile = path.join(root, "ds.config.json");
|
|
@@ -10,7 +10,8 @@ export const buildStatusFile = (root) => path.join(root, ".ds/build-status.json"
|
|
|
10
10
|
const maxBytes = 256 * 1024;
|
|
11
11
|
const diagnostic = z.object({ message: z.string().max(4000), file: z.string().max(1000).optional(), line: z.number().int().positive().optional(), column: z.number().int().positive().optional(), plugin: z.string().max(200).optional(), code: z.string().max(100).optional() }).strict();
|
|
12
12
|
const preview = z.object({ id: z.string().max(256), state: z.enum(["idle", "building", "ready", "error"]), revision: z.number().int().nonnegative(), lastSuccessfulRevision: z.number().int().positive().nullable(), artifactDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(), diagnostics: z.array(diagnostic).max(20), diagnosticSet: z.number().int().nonnegative().optional() }).strict();
|
|
13
|
-
const
|
|
13
|
+
const progressSchema = z.object({ phase: z.enum(["queued", "validation", "compilation", "validation-and-compilation", "publication", "idle"]), startedAt: z.string().datetime(), updatedAt: z.string().datetime() }).strict();
|
|
14
|
+
const snapshot = z.object({ schemaVersion: z.literal(1), session: z.string().regex(/^[\w-]{1,100}$/), sequence: z.number().int().nonnegative(), updatedAt: z.string().datetime(), progress: progressSchema.optional(), previews: z.array(preview), diagnostics: z.array(diagnostic).max(20).optional(), diagnosticSets: z.array(z.array(diagnostic).max(20)).optional() }).strict().refine(value => value.previews.every(entry => entry.diagnosticSet === undefined || value.diagnosticSets?.[entry.diagnosticSet]), "Invalid shared diagnostic reference.");
|
|
14
15
|
export const artifactDigest = (file) => {
|
|
15
16
|
if (realpathSync(file) !== file) throw new Error("Preview output cannot resolve through a symlink.");
|
|
16
17
|
if (statSync(file).size > 2 * 1024 * 1024) throw new Error("Preview exceeds 2 MiB.");
|
|
@@ -37,8 +38,9 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
37
38
|
const entries = new Map(apps.map((app) => [app.id, { id: app.id, state: "idle", revision: 0, lastSuccessfulRevision: null, artifactDigest: null, diagnostics: [] }]));
|
|
38
39
|
let sequence = 0;
|
|
39
40
|
let diagnostics = [];
|
|
41
|
+
let progress;
|
|
40
42
|
function save() {
|
|
41
|
-
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()], ...(diagnostics.length ? { diagnostics } : {}) };
|
|
43
|
+
const value = { schemaVersion: 1, session, sequence: ++sequence, updatedAt: new Date().toISOString(), previews: [...entries.values()], ...(progress ? { progress } : {}), ...(diagnostics.length ? { diagnostics } : {}) };
|
|
42
44
|
let text = JSON.stringify(snapshot.parse(value));
|
|
43
45
|
// A shared graph failure applies to every scenario. Store repeated details
|
|
44
46
|
// once only when necessary; ordinary reports retain inline diagnostics.
|
|
@@ -67,6 +69,11 @@ export function createBuildReporter(directory, { apps, session = process.env.ADS
|
|
|
67
69
|
save();
|
|
68
70
|
return {
|
|
69
71
|
session,
|
|
72
|
+
progress(phase) {
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
progress = { phase, startedAt: !progress || progress.phase === "idle" ? now : progress.startedAt, updatedAt: now };
|
|
75
|
+
save();
|
|
76
|
+
},
|
|
70
77
|
failure(error) {
|
|
71
78
|
diagnostics = (Array.isArray(error) ? error : [error]).slice(0, 20).map(error => normalizeDiagnostic(root, error));
|
|
72
79
|
for (const value of entries.values()) { value.state = "error"; ++value.revision; value.diagnostics = []; }
|
|
@@ -153,7 +160,7 @@ export function inspectBuildStatus(root, config, { session, processState, file =
|
|
|
153
160
|
current, stale: state === "unreported" ? null : !current, available, diagnostics };
|
|
154
161
|
});
|
|
155
162
|
const state = reportError || report?.diagnostics?.length || previews.some((entry) => entry.state === "error") ? "error" : previews.some((entry) => entry.state === "building") ? "building" : previews.some((entry) => entry.state === "unreported") ? "unreported" : previews.some(entry => entry.current) ? "ready" : previews.length ? "idle" : "disabled";
|
|
156
|
-
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
|
|
163
|
+
return { state, reported: Boolean(report), session: report?.session ?? null, sequence: report?.sequence ?? null, updatedAt: report?.updatedAt ?? null, ...(report?.progress ? { progress: report.progress } : {}), previews, ...(report?.diagnostics?.length ? { diagnostics: report.diagnostics } : {}), resourceUri: BUILD_URI };
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
export class PreviewBuildError extends Error {
|
package/dist/mcp/catalog.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { isInside, sourceFiles } from "../config.mjs";
|
|
5
5
|
import { projectCompilerOptions } from "../boundaries.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { publicApi } from "../public-api.mjs";
|
|
7
7
|
import { projectGuideFiles } from "../project-documents.mjs";
|
|
8
8
|
|
|
9
9
|
export const PROJECT_URI = "ads://project";
|
|
@@ -24,7 +24,7 @@ export function projectDocuments(options) {
|
|
|
24
24
|
for (const name of projectGuideFiles(root)) files.push(path.join(root, name));
|
|
25
25
|
return [...new Set(files)].filter((file) => isInside(root, realpathSync(file))).sort().map((file) => {
|
|
26
26
|
const relative = path.relative(root, file).replaceAll("\\", "/");
|
|
27
|
-
return { uri: sourceUri(relative), name: relative, mimeType: file.endsWith(".md") ? "text/markdown" : "text/plain", file };
|
|
27
|
+
return { uri: sourceUri(relative), name: relative, mimeType: file.endsWith(".md") ? "text/markdown" : file.endsWith(".json") ? "application/json" : "text/plain", file };
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,27 +32,30 @@ export function inspectCatalog(options, scenarios = []) {
|
|
|
32
32
|
const { root, dsRoot, config } = options;
|
|
33
33
|
const entry = path.join(dsRoot, config.publicEntry);
|
|
34
34
|
if (!existsSync(entry)) return [];
|
|
35
|
-
const program = ts.createProgram([entry], projectCompilerOptions(options));
|
|
35
|
+
const program = ts.createProgram([entry], { ...projectCompilerOptions(options), allowJs: true });
|
|
36
36
|
const checker = program.getTypeChecker();
|
|
37
37
|
const source = program.getSourceFile(entry);
|
|
38
38
|
const module = source && checker.getSymbolAtLocation(source);
|
|
39
39
|
if (!module) return [];
|
|
40
40
|
const documents = projectDocuments(options);
|
|
41
41
|
const items = [];
|
|
42
|
-
const
|
|
42
|
+
const api = new Map(publicApi(program, options).map(item => [item.name, item]));
|
|
43
43
|
for (const exported of checker.getExportsOfModule(module)) {
|
|
44
44
|
let symbol = exported;
|
|
45
45
|
if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
46
46
|
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
47
|
-
if (!(symbol.flags & ts.SymbolFlags.Value) || !declaration || !
|
|
47
|
+
if (!(symbol.flags & ts.SymbolFlags.Value) || !declaration || !api.has(exported.name)) continue;
|
|
48
48
|
const file = declaration.getSourceFile().fileName;
|
|
49
|
-
const kind =
|
|
49
|
+
const kind = api.get(exported.name).kind;
|
|
50
|
+
const ui = api.get(exported.name).renderable;
|
|
51
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
52
|
+
if (!ui && !type.getCallSignatures().length && !type.getConstructSignatures().length) continue;
|
|
50
53
|
if (!kind) continue;
|
|
51
54
|
const relative = path.relative(root, file).replaceAll("\\", "/");
|
|
52
55
|
const line = declaration.getSourceFile().getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
|
|
53
56
|
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) });
|
|
57
|
+
items.push({ name: exported.name, kind, apiKind: ui ? "ui" : "behavior", importFrom: config.publicAlias, file: relative, line, sourceUri: sourceUri(relative), guides,
|
|
58
|
+
previews: scenarios.filter(item => item.covers.includes(exported.name) || item.exercises?.includes(exported.name)).map(item => item.id) });
|
|
56
59
|
}
|
|
57
60
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
61
|
}
|
package/dist/mcp/context.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { projectGuideFiles } from "../project-documents.mjs";
|
|
|
5
5
|
|
|
6
6
|
export const GUIDE_URI = "ads://project/guide";
|
|
7
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.";
|
|
8
|
+
export const serverInstructions = "Use ADS for UI work: components, patterns, controllers, hooks, 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. For behavior work, read ads://guides/behaviors and use ads_behaviors/ads_verify: scenario exercises are demonstrations, and registered guarantees need current passing execution evidence. Use the configured project framework and declared dependencies; MCP hosts supply no ad hoc APIs. 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
9
|
|
|
10
10
|
function manager(root, pkg) {
|
|
11
11
|
if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
|
|
@@ -35,7 +35,7 @@ export function workflow(previews) {
|
|
|
35
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
36
|
skills: [
|
|
37
37
|
{ task: "Build, refine or debug UI using the project ADS", name: "ads-designer" },
|
|
38
|
-
{ task: "Adapt a component-library reference", name: "ads-
|
|
38
|
+
{ task: "Adapt a component-library reference", name: "ads-migrate" },
|
|
39
39
|
{ task: "Import from another project's design-system", name: "ads-import" },
|
|
40
40
|
{ task: "Implement or review motion", name: "ads-motion" },
|
|
41
41
|
{ task: "Stress-test layout, states and interactions", name: "ads-break-ui" },
|
package/dist/mcp/mutations.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, re
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { isInside, loadConfig } from "../config.mjs";
|
|
8
|
+
import { affectedBehaviorUnits, createVerificationController } from "../verification.mjs";
|
|
8
9
|
import { checkProject } from "../check.mjs";
|
|
9
10
|
import { generateLintConfig } from "../lint-config.mjs";
|
|
10
11
|
import { generateBrandAssets } from "../brand-assets.mjs";
|
|
@@ -260,6 +261,15 @@ export function createMutationController(directory) {
|
|
|
260
261
|
verified.push({ name: "brand assets", passed: true, skipped: !!brand.skipped });
|
|
261
262
|
}
|
|
262
263
|
for (const check of extraChecks) verified.push(await checkCommand(check.command, check.args, candidate, signal));
|
|
264
|
+
const behaviorUnits = affectedBehaviorUnits(candidateOptions, changes.map(change => change.path));
|
|
265
|
+
if (behaviorUnits.length) {
|
|
266
|
+
const verification = createVerificationController(candidate);
|
|
267
|
+
let evidence;
|
|
268
|
+
try { evidence = await verification.run({ units: behaviorUnits, persist: false }, signal); }
|
|
269
|
+
finally { await verification.close(); }
|
|
270
|
+
if (!evidence.ok) return lastResult = { ok: false, applied: false, diagnostics: [...evidence.diagnostics, ...evidence.results.filter(item => item.status !== "passed").map(item => ({ rule: "behavior-check", message: `${item.unit}#${item.check}: ${item.status}. ${item.message}` }))].slice(0, 50), checks: verified, behavior: evidence };
|
|
271
|
+
verified.push({ name: "registered behavior checks", passed: true, results: evidence.results });
|
|
272
|
+
}
|
|
263
273
|
if (signal?.aborted) throw fail("mutation-cancelled", "Mutation cancelled before application.");
|
|
264
274
|
if (snapshot(candidateOptions) !== expected) throw fail("mutation-check", "A validation command changed source files. Use checks without source-writing side effects.");
|
|
265
275
|
if (snapshot(loadConfig(root)) !== before) throw fail("mutation-conflict", "Project inputs changed during validation; no changes were applied. Read current source and retry.");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// The only stdin writer is the MCP owner. EOF also arrives after SIGKILL or a
|
|
4
|
+
// client launcher crash, when the owner's JavaScript cleanup cannot execute.
|
|
5
|
+
const pid = Number(process.argv[2]);
|
|
6
|
+
if (!Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) throw new Error("Invalid owned preview process group");
|
|
7
|
+
let released = false, stopping = false, timer, input = "";
|
|
8
|
+
function stop() {
|
|
9
|
+
if (released || stopping) return;
|
|
10
|
+
stopping = true;
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
const terminator = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
13
|
+
terminator.on("error", () => { process.exitCode = 1; });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try { process.kill(-pid, "SIGTERM"); }
|
|
17
|
+
catch (error) { if (error.code !== "ESRCH") process.exitCode = 1; return; }
|
|
18
|
+
timer = setTimeout(() => {
|
|
19
|
+
try { process.kill(-pid, "SIGKILL"); }
|
|
20
|
+
catch (error) { if (error.code !== "ESRCH") process.exitCode = 1; }
|
|
21
|
+
}, 1500);
|
|
22
|
+
}
|
|
23
|
+
process.stdin.on("data", bytes => {
|
|
24
|
+
input = (input + bytes).slice(-32);
|
|
25
|
+
if (input === "disarm\n") { released = true; clearTimeout(timer); }
|
|
26
|
+
});
|
|
27
|
+
for (const event of ["end", "close", "error"]) process.stdin.once(event, stop);
|
|
28
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, stop);
|
|
29
|
+
process.stdin.resume();
|