@reqvire-org/ads 0.3.12 → 0.3.14
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 +20 -1
- package/dist/bootstrap-ds/ADS.md +4 -1
- package/dist/capture/worker.mjs +38 -0
- package/dist/capture-store.mjs +123 -0
- package/dist/capture.mjs +117 -0
- package/dist/check.mjs +8 -2
- package/dist/config.mjs +6 -1
- package/dist/mcp/context.mjs +1 -1
- package/dist/mcp/mutations.mjs +25 -5
- package/dist/mcp/server.mjs +62 -15
- package/dist/scenario-slots.mjs +124 -0
- package/dist/scenarios.mjs +9 -3
- package/dist/showcase/client.bundle.js +18 -18
- package/dist/showcase/style.css +2 -0
- package/dist/skills/ads-break-ui/SKILL.md +6 -1
- package/dist/skills/ads-break-ui/references/captures.md +147 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +72 -3
- package/dist/skills/ads-break-ui/references/scenarios.md +40 -1
- package/dist/skills/ads-designer/SKILL.md +6 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +4 -1
- package/dist/skills/ads-designer/references/captures.md +147 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +72 -3
- package/dist/skills/ads-designer/references/scenarios.md +40 -1
- package/dist/skills/ads-import/SKILL.md +6 -1
- package/dist/skills/ads-import/references/captures.md +147 -0
- package/dist/skills/ads-import/references/guarded-editing.md +72 -3
- package/dist/skills/ads-import/references/scenarios.md +40 -1
- package/dist/skills/ads-migrate/SKILL.md +6 -1
- package/dist/skills/ads-migrate/references/captures.md +147 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +72 -3
- package/dist/skills/ads-migrate/references/scenarios.md +40 -1
- package/dist/skills/ads-motion/SKILL.md +6 -1
- package/dist/skills/ads-motion/references/captures.md +147 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +72 -3
- package/dist/skills/ads-motion/references/scenarios.md +40 -1
- package/dist/source-values.mjs +2 -1
- package/dist/verification/browser.mjs +8 -3
- package/dist/verification/playwright-reporter.cjs +23 -8
- package/dist/verification.mjs +142 -33
- package/docs/ADS.md +10 -2
- package/docs/ARCHITECTURE.md +43 -2
- package/docs/BEHAVIORS.md +66 -7
- package/docs/CHANGELOG.md +27 -0
- package/docs/GUIDE.md +48 -1
- package/docs/MCP.md +54 -1
- package/package.json +1 -1
package/dist/verification.mjs
CHANGED
|
@@ -19,13 +19,55 @@ const slash = file => file.replaceAll("\\", "/");
|
|
|
19
19
|
const resultFile = root => path.join(root, ".ds/behavior-results.json");
|
|
20
20
|
|
|
21
21
|
/** Follow real project imports; package/configuration changes also invalidate evidence. */
|
|
22
|
-
function inputContext(options) {
|
|
22
|
+
function inputContext(options) {
|
|
23
|
+
const sources = new Map(), digests = new Map(), packages = new Map(), resolve = importResolver(options), resolutions = new Map();
|
|
24
|
+
return { inventory: inspectUnits(options), scenarios: inspectScenarios(options).scenarios,
|
|
25
|
+
seeds: [...configurationFiles(options)].map(file => path.join(options.root, file)).concat(sourceFiles(path.join(options.dsRoot, options.config.tokenDir)), path.join(options.dsRoot, options.config.stylesEntry)),
|
|
26
|
+
digest(file) { if (!digests.has(file)) digests.set(file, hash(readFileSync(file))); return digests.get(file); },
|
|
27
|
+
ast(file) { if (!sources.has(file)) sources.set(file, ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true)); return sources.get(file); },
|
|
28
|
+
resolve(specifier, file) { const key = `${file}\0${specifier}`; if (!resolutions.has(key)) resolutions.set(key, resolve(specifier, file)); return resolutions.get(key); },
|
|
29
|
+
hasSideEffects(file, record) {
|
|
30
|
+
let directory = path.dirname(file);
|
|
31
|
+
while (isInside(options.root, directory)) {
|
|
32
|
+
const manifest = path.join(directory, "package.json");
|
|
33
|
+
if (existsSync(manifest)) {
|
|
34
|
+
record(manifest);
|
|
35
|
+
if (!packages.has(manifest)) { try { packages.set(manifest, JSON.parse(readFileSync(manifest, "utf8")).sideEffects); } catch { packages.set(manifest, true); } }
|
|
36
|
+
const policy = packages.get(manifest), relative = slash(path.relative(directory, file));
|
|
37
|
+
if (policy === false) return false;
|
|
38
|
+
// Only the simple package sideEffects glob subset is narrowed. Unknown
|
|
39
|
+
// syntax and undeclared policies preserve the entire dependency graph.
|
|
40
|
+
if (!Array.isArray(policy) || policy.some(pattern => typeof pattern !== "string" || !/^[\w./*?-]+$/.test(pattern))) return true;
|
|
41
|
+
return policy.some(pattern => {
|
|
42
|
+
pattern = pattern.replace(/^\.\//, ""); if (!pattern.includes("/")) pattern = `**/${pattern}`;
|
|
43
|
+
const expression = pattern.match(/\*\*\/|\*\*|\*|\?|[^*?]+/g)?.map(part => part === "**/" ? "(?:.*/)?" : part === "**" ? ".*" : part === "*" ? "[^/]*" : part === "?" ? "[^/]" : part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("");
|
|
44
|
+
return new RegExp(`^${expression}$`).test(relative);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
directory = path.dirname(directory);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
23
53
|
|
|
24
54
|
export function behaviorInputs(options, contract, check, context = inputContext(options)) {
|
|
25
|
-
const files = new Set(),
|
|
26
|
-
|
|
55
|
+
const files = new Set(), demands = new Map(), effectsVisited = new Set();
|
|
56
|
+
const resolve = context.resolve;
|
|
57
|
+
function effects(file) {
|
|
58
|
+
if (effectsVisited.has(file) || !isInside(options.root, file) || file.includes(`${path.sep}node_modules${path.sep}`) || !existsSync(file)) return;
|
|
59
|
+
effectsVisited.add(file);
|
|
60
|
+
if (context.hasSideEffects(file, visit)) { visit(file); return; }
|
|
61
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || !lstatSync(file).isFile()) return;
|
|
62
|
+
if (lstatSync(file).isSymbolicLink() || realpathSync(file) !== file) throw new Error(`Behavior inputs cannot resolve through symlinks: ${file}`);
|
|
63
|
+
for (const statement of context.ast(file).statements) if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) for (const target of resolve(statement.moduleSpecifier.text, file)) effects(target);
|
|
64
|
+
}
|
|
65
|
+
function visit(file, names = null) {
|
|
27
66
|
file = path.resolve(file);
|
|
28
|
-
if (
|
|
67
|
+
if (!isInside(options.root, file) || file.includes(`${path.sep}node_modules${path.sep}`) || !existsSync(file)) return;
|
|
68
|
+
const prior = demands.get(file);
|
|
69
|
+
if (demands.has(file) && (prior === null || names && names.every(name => prior.has(name)))) return;
|
|
70
|
+
demands.set(file, names === null ? null : new Set([...(prior ?? []), ...names]));
|
|
29
71
|
if (lstatSync(file).isSymbolicLink() || realpathSync(file) !== file) throw new Error(`Behavior inputs cannot resolve through symlinks: ${file}`);
|
|
30
72
|
if (!lstatSync(file).isFile()) return;
|
|
31
73
|
files.add(file);
|
|
@@ -39,12 +81,31 @@ export function behaviorInputs(options, contract, check, context = inputContext(
|
|
|
39
81
|
css.walk(node => { if (node.type === "decl" || node.type === "atrule") valueParser(node.type === "decl" ? node.value : node.params).walk(value => { if (value.type === "function" && value.value.toLowerCase() === "url") localAsset(value.nodes.filter(part => part.type !== "space").map(part => part.value).join("")); }); });
|
|
40
82
|
}
|
|
41
83
|
if (!/\.[cm]?[jt]sx?$/.test(file)) return;
|
|
42
|
-
const ast =
|
|
84
|
+
const ast = context.ast(file);
|
|
85
|
+
const barrel = names && ast.statements.every(statement => ts.isEmptyStatement(statement) || ts.isImportDeclaration(statement) && !statement.importClause || ts.isExportDeclaration(statement) && (!statement.exportClause || ts.isNamedExports(statement.exportClause)) && (!statement.exportClause || !statement.exportClause.elements.length || statement.moduleSpecifier));
|
|
86
|
+
if (barrel) {
|
|
87
|
+
for (const statement of ast.statements) {
|
|
88
|
+
if (!statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
|
|
89
|
+
for (const target of resolve(statement.moduleSpecifier.text, file)) {
|
|
90
|
+
if (ts.isImportDeclaration(statement) || !statement.exportClause) visit(target);
|
|
91
|
+
else {
|
|
92
|
+
const imported = statement.exportClause.elements.filter(entry => names.includes(entry.name.text)).map(entry => (entry.propertyName ?? entry.name).text);
|
|
93
|
+
if (imported.length) visit(target, imported);
|
|
94
|
+
effects(target);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
43
100
|
function walk(node) {
|
|
44
101
|
let specifier;
|
|
45
102
|
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) specifier = node.moduleSpecifier.text;
|
|
46
103
|
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;
|
|
47
|
-
if (specifier)
|
|
104
|
+
if (specifier) {
|
|
105
|
+
const clause = ts.isImportDeclaration(node) && node.importClause;
|
|
106
|
+
const names = clause && !clause.namedBindings ? clause.name ? ["default"] : null : clause?.namedBindings && ts.isNamedImports(clause.namedBindings) ? [...(clause.name ? ["default"] : []), ...clause.namedBindings.elements.map(entry => (entry.propertyName ?? entry.name).text)] : null;
|
|
107
|
+
for (const target of resolve(specifier, file)) visit(target, names);
|
|
108
|
+
}
|
|
48
109
|
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "URL" && node.arguments?.[0] && ts.isStringLiteralLike(node.arguments[0])) localAsset(node.arguments[0].text);
|
|
49
110
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && (!node.arguments[0] || !ts.isStringLiteralLike(node.arguments[0]))) {
|
|
50
111
|
// Computed module selection cannot provide a precise dependency closure.
|
|
@@ -56,13 +117,11 @@ export function behaviorInputs(options, contract, check, context = inputContext(
|
|
|
56
117
|
walk(ast);
|
|
57
118
|
}
|
|
58
119
|
for (const file of context.inventory.units.find(unit => unit.id === contract.unit)?.files ?? []) visit(path.join(options.root, file));
|
|
59
|
-
for (const file of
|
|
60
|
-
for (const file of sourceFiles(path.join(options.dsRoot, options.config.tokenDir))) visit(file);
|
|
61
|
-
visit(path.join(options.dsRoot, options.config.stylesEntry));
|
|
120
|
+
for (const file of context.seeds) visit(file);
|
|
62
121
|
const scenario = check.scenario && context.scenarios.find(item => item.id === check.scenario);
|
|
63
122
|
if (scenario) visit(path.join(options.root, scenario.file));
|
|
64
|
-
const rows = [...files].sort().map(file => [slash(path.relative(options.root, file)),
|
|
65
|
-
for (const file of ["package-lock.json", "bun.lock", "bun.lockb", "pnpm-lock.yaml", "yarn.lock", "node_modules/.package-lock.json"]) rows.push([file, existsSync(path.join(options.root, file)) ?
|
|
123
|
+
const rows = [...files].sort().map(file => [slash(path.relative(options.root, file)), context.digest(file)]);
|
|
124
|
+
for (const file of ["package-lock.json", "bun.lock", "bun.lockb", "pnpm-lock.yaml", "yarn.lock", "node_modules/.package-lock.json"]) rows.push([file, existsSync(path.join(options.root, file)) ? context.digest(path.join(options.root, file)) : null]);
|
|
66
125
|
rows.push(["profile", JSON.stringify(options.config.mcp.behaviorProfiles[check.profile])], ["node", process.version]);
|
|
67
126
|
for (const file of ["./verification.mjs", "./verification/browser.mjs", "./verification/playwright-reporter.cjs"]) rows.push([`tool:${file}`, hash(readFileSync(new URL(file, import.meta.url)))]);
|
|
68
127
|
return { revision: hash(JSON.stringify(rows)), files: rows.map(([file]) => file) };
|
|
@@ -104,9 +163,11 @@ export function affectedBehaviorUnits(options, changedFiles) {
|
|
|
104
163
|
return contracts.filter(contract => Object.values(contract.checks).some(check => behaviorInputs(options, contract, check, context).files.some(file => changed.has(file)))).map(contract => contract.unit);
|
|
105
164
|
}
|
|
106
165
|
|
|
107
|
-
function
|
|
166
|
+
export function executeDevelopmentCommand(profile, args, root, env, signal) {
|
|
108
167
|
return new Promise((resolve, reject) => {
|
|
109
|
-
|
|
168
|
+
const label = profile.label ?? "Behavior verification";
|
|
169
|
+
if (signal.aborted) { reject(new Error(`${label} cancelled.`)); return; }
|
|
170
|
+
const startedAt = Date.now(), timeoutMs = profile.timeoutMs ?? 60000;
|
|
110
171
|
let output = "", failure, child, guard, done = false;
|
|
111
172
|
const kill = () => {
|
|
112
173
|
if (!child?.pid) return;
|
|
@@ -121,28 +182,47 @@ function execute(profile, args, root, env, signal) {
|
|
|
121
182
|
guard.once("error", error => { failure = error; kill(); });
|
|
122
183
|
guard.once("exit", () => { if (!done) { failure ??= new Error("Verification ownership guard exited unexpectedly."); kill(); } });
|
|
123
184
|
}
|
|
124
|
-
const abort = () => { failure = new Error(
|
|
185
|
+
const abort = () => { failure = Object.assign(new Error(`${label} cancelled.`), { reason: "cancelled" }); kill(); };
|
|
125
186
|
signal.addEventListener("abort", abort, { once: true });
|
|
126
|
-
const timeout = setTimeout(() => { failure = new Error(
|
|
187
|
+
const timeout = setTimeout(() => { failure = Object.assign(new Error(`${label} timed out after ${timeoutMs} ms for the complete profile run, not each check.`), { reason: "timeout", timeoutMs }); kill(); }, timeoutMs);
|
|
127
188
|
for (const stream of [child.stdout, child.stderr]) stream.on("data", chunk => { output = (output + chunk.toString()).slice(-12000); });
|
|
128
189
|
child.once("error", error => { failure = error; });
|
|
129
190
|
child.once("exit", kill);
|
|
130
191
|
child.once("close", code => {
|
|
131
192
|
done = true; kill(); guard?.stdin.end("disarm\n"); clearTimeout(timeout); signal.removeEventListener("abort", abort);
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
const elapsedMs = Date.now() - startedAt;
|
|
194
|
+
if (failure) reject(Object.assign(failure, { output, elapsedMs }));
|
|
195
|
+
else resolve({ code, output, elapsedMs });
|
|
134
196
|
});
|
|
135
197
|
});
|
|
136
198
|
}
|
|
137
199
|
|
|
200
|
+
function readReport(file, runId) {
|
|
201
|
+
if (!existsSync(file) || realpathSync(file) !== file || !lstatSync(file).isFile() || lstatSync(file).size > 1024 * 1024) throw new Error("Runner produced no valid bounded verification report. A successful exit alone is not passing evidence.");
|
|
202
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
203
|
+
if (value.schemaVersion !== 1 || value.runId !== runId || !Array.isArray(value.cases) || value.cases.length > 500 || value.active !== undefined && (!Array.isArray(value.active) || value.active.length > 100)) throw new Error("Runner report has the wrong run ID/schema or too many cases.");
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function runProgress(group, report, startedAt, profile, timeoutMs) {
|
|
208
|
+
const selected = group.map(check => ({ unit: check.unit, check: check.id, id: check.case, file: slash(path.posix.join(path.posix.dirname(slash(check.contract.file)), check.file)) }));
|
|
209
|
+
const cases = selected.map(check => {
|
|
210
|
+
const matches = report?.cases.filter(item => item?.id === check.id && item.file === check.file) ?? [];
|
|
211
|
+
const running = report?.active?.find(item => item?.id === check.id && item.file === check.file);
|
|
212
|
+
return { unit: check.unit, check: check.check, state: running ? "running" : matches.length ? "completed" : Array.isArray(report?.active) ? "not_started" : "not_reported", ...(running ? { elapsedMs: Math.max(0, Date.now() - Date.parse(running.startedAt)) || 0, workerIndex: running.workerIndex } : {}), ...(matches.length ? { reportedStatus: matches.at(-1).status, durationMs: matches.reduce((sum, item) => sum + (Number.isFinite(item.durationMs) ? Math.max(0, item.durationMs) : 0), 0) } : {}) };
|
|
213
|
+
});
|
|
214
|
+
return { profile, elapsedMs: Date.now() - startedAt, timeoutMs, total: cases.length, completed: cases.filter(item => item.state === "completed").length, active: cases.filter(item => item.state === "running"), pending: cases.filter(item => ["not_started", "not_reported"].includes(item.state)).length, cases, ...(Number.isSafeInteger(report?.workers) ? { workers: report.workers } : {}) };
|
|
215
|
+
}
|
|
216
|
+
|
|
138
217
|
/** Registered development commands are trusted project code, never model-supplied commands. */
|
|
139
|
-
export function createVerificationController(root) {
|
|
218
|
+
export function createVerificationController(root, { reportRoot = root, onProgress } = {}) {
|
|
140
219
|
root = path.resolve(root);
|
|
141
|
-
|
|
220
|
+
reportRoot = path.resolve(reportRoot);
|
|
221
|
+
let active, progress;
|
|
142
222
|
return {
|
|
143
|
-
status: () => ({ running: !!active }),
|
|
223
|
+
status: () => ({ running: !!active, ...(active && progress ? { progress: { ...progress, elapsedMs: Date.now() - progress.startedAt } } : {}) }),
|
|
144
224
|
async close() { active?.abort(); await active?.completion; },
|
|
145
|
-
async run({ unit, units, checks, persist = true } = {}, signal) {
|
|
225
|
+
async run({ unit, units, checks, persist = true } = {}, signal, notifyProgress) {
|
|
146
226
|
if (active) return { ok: false, diagnostics: [{ rule: "behavior-busy", message: "A behavior verification is already running." }], results: [] };
|
|
147
227
|
active = new AbortController();
|
|
148
228
|
const controller = active;
|
|
@@ -162,7 +242,7 @@ export function createVerificationController(root) {
|
|
|
162
242
|
for (const profileName of new Set(selection.map(check => check.profile))) {
|
|
163
243
|
const profile = options.config.mcp.behaviorProfiles[profileName];
|
|
164
244
|
const group = selection.filter(check => check.profile === profileName);
|
|
165
|
-
const directory = path.join(
|
|
245
|
+
const directory = path.join(reportRoot, ".ds/verification", randomUUID());
|
|
166
246
|
mkdirSync(directory, { recursive: true });
|
|
167
247
|
if (realpathSync(directory) !== directory) throw new Error("Verification output cannot resolve through a symlink.");
|
|
168
248
|
const runId = randomUUID(), report = path.join(directory, "report.json"), request = path.join(directory, "request.json");
|
|
@@ -179,31 +259,60 @@ export function createVerificationController(root) {
|
|
|
179
259
|
const requestContext = { schemaVersion: 1, runId, projectRoot: root, framework: options.config.framework, profile: profileName, mode: profile.mode ?? "unit", checks: group.map(({ contract: _contract, ...check }) => check), scenarios };
|
|
180
260
|
writeFileSync(request, JSON.stringify(requestContext));
|
|
181
261
|
const args = [...profile.args];
|
|
182
|
-
if (profile.runner === "playwright")
|
|
262
|
+
if (profile.runner === "playwright") {
|
|
263
|
+
const tags = [...new Set(group.map(check => check.case))].map(id => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
264
|
+
args.push(...new Set(group.map(check => slash(path.relative(root, path.resolve(root, path.dirname(check.contract.file), check.file))))), `--grep=(?:^|\\s)@ads:(?:${tags.join("|")})(?=\\s|$)`, `--reporter=${fileURLToPath(new URL("./verification/playwright-reporter.cjs", import.meta.url))}`, `--output=${path.join(directory, "artifacts")}`);
|
|
265
|
+
}
|
|
183
266
|
let run, reportValue, runnerError;
|
|
267
|
+
const startedAt = Date.now();
|
|
268
|
+
const update = value => {
|
|
269
|
+
progress = { ...runProgress(group, value, startedAt, profileName, profile.timeoutMs ?? 60000), startedAt };
|
|
270
|
+
onProgress?.(progress); notifyProgress?.(progress);
|
|
271
|
+
};
|
|
272
|
+
update();
|
|
273
|
+
let lastSnapshot = "", lastUpdate = Date.now();
|
|
274
|
+
const monitor = setInterval(() => {
|
|
275
|
+
let value;
|
|
276
|
+
try { value = readReport(report, runId); } catch { /* An absent/invalid intermediate snapshot cannot provide evidence. */ }
|
|
277
|
+
const snapshot = JSON.stringify(value);
|
|
278
|
+
if (snapshot !== lastSnapshot || Date.now() - lastUpdate >= 5000) { lastSnapshot = snapshot; lastUpdate = Date.now(); update(value); }
|
|
279
|
+
}, 250);
|
|
184
280
|
try {
|
|
185
|
-
run = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
281
|
+
run = await executeDevelopmentCommand(profile, args, root, { ADS_VERIFY_REQUEST: request, ADS_VERIFY_REPORT: report, ADS_VERIFY_MODE: requestContext.mode }, signal);
|
|
282
|
+
} catch (error) { runnerError = error; }
|
|
283
|
+
finally { clearInterval(monitor); }
|
|
284
|
+
try { reportValue = readReport(report, runId); }
|
|
285
|
+
catch (error) { runnerError ??= error; }
|
|
286
|
+
update(reportValue);
|
|
287
|
+
if (runnerError) {
|
|
288
|
+
const detail = `${progress.completed}/${progress.total} checks completed; ${progress.pending} without a completed/active result. Active: ${progress.active.map(item => `${item.unit}#${item.check} (${Math.round(item.elapsedMs)} ms)`).join(", ") || "none reported"}.`;
|
|
289
|
+
diagnostics.push({ rule: "behavior-runner", message: `${runnerError.message} ${detail}`, reason: runnerError.reason, elapsedMs: runnerError.elapsedMs ?? Date.now() - startedAt, progress, output: runnerError.output?.slice(-4000), profile: profileName });
|
|
290
|
+
}
|
|
190
291
|
const suiteErrors = Array.isArray(reportValue?.errors) ? reportValue.errors.slice(0, 5).map(error => String(error).slice(0, 1000)) : [];
|
|
191
|
-
const
|
|
292
|
+
const incompleteReport = reportValue?.complete === false || progress.pending > 0 || progress.active.length > 0;
|
|
293
|
+
// Exit 1 is the normal test-failure exit. A complete report with actual
|
|
294
|
+
// assertion failures still contains independent, valid passing outcomes.
|
|
295
|
+
// Unexplained exits, suite errors and interrupted reports grant no pass.
|
|
296
|
+
const caseFailureExit = run?.code === 1 && !runnerError && !suiteErrors.length && !incompleteReport && reportValue?.cases.some(item => item?.status === "failed");
|
|
297
|
+
const runnerFailure = run && (run.code !== 0 || suiteErrors.length || incompleteReport) ? `Registered runner ${profileName} ${run.code !== 0 ? `exited with code ${run.code ?? "unknown"}` : incompleteReport ? "left an incomplete report" : "reported a suite error"}. ${suiteErrors.join("; ") || reportValue?.cases.filter(item => item?.status === "failed").slice(0, 3).map(item => `${item.id}: ${item.message || "failed without a case diagnostic"}`).join("; ") || "Inspect the runner output and verification report."}`.slice(0, 3000) : undefined;
|
|
192
298
|
if (runnerFailure) diagnostics.push({ rule: "behavior-runner", message: runnerFailure, output: run.output?.slice(-4000), profile: profileName });
|
|
193
299
|
const afterOptions = loadConfig(root), afterContext = inputContext(afterOptions);
|
|
194
300
|
for (const check of group) {
|
|
195
301
|
const file = slash(path.relative(root, path.resolve(root, path.dirname(check.contract.file), check.file)));
|
|
196
302
|
const matches = reportValue?.cases.filter(item => item && item.id === check.case && item.file === file) ?? [];
|
|
197
303
|
const reportedStatus = matches.length && matches.every(item => item.status === "passed") ? "passed" : matches.length && matches.every(item => item.status === "skipped") ? "skipped" : "failed";
|
|
198
|
-
|
|
304
|
+
const blockedRun = !!runnerError || !!runnerFailure && !caseFailureExit;
|
|
305
|
+
let status = matches.some(item => item.status === "failed") ? "failed" : !matches.length || blockedRun ? reportValue ? "incomplete" : "failed" : reportedStatus;
|
|
199
306
|
if (runnerError?.code === "ENOENT") status = "unavailable";
|
|
200
307
|
const current = behaviorInputs(afterOptions, check.contract, check, afterContext).revision === check.inputRevision;
|
|
201
308
|
if (!current) status = "stale";
|
|
202
309
|
if (signal.aborted) status = "cancelled";
|
|
203
310
|
results.push({ unit: check.unit, check: check.id, file: check.file, case: check.case, profile: profileName, status, current, revision: check.inputRevision, runId, completedAt: new Date().toISOString(),
|
|
204
311
|
previewRevision: profile.mode === "browser" ? host?.previewRevision() ?? readPreviewManifest(options)?.revision ?? null : null,
|
|
205
|
-
...(
|
|
206
|
-
|
|
312
|
+
...(matches.length ? { reportedStatus, durationMs: matches.reduce((sum, item) => sum + (Number.isFinite(item.durationMs) ? Math.max(0, item.durationMs) : 0), 0) } : {}),
|
|
313
|
+
...(runnerFailure ? { runnerExitCode: run.code } : {}),
|
|
314
|
+
execution: progress.cases.find(item => item.unit === check.unit && item.check === check.id)?.state,
|
|
315
|
+
message: [matches.filter(item => item.status !== "passed").map(item => String(item.message || `Case reported ${item.status} without a diagnostic.`).slice(0, 2000)).join("\n"), runnerError ? `${runnerError.message} This check ${matches.length ? `reported ${reportedStatus}` : `was ${progress.cases.find(item => item.unit === check.unit && item.check === check.id)?.state ?? "not reported"}`}; the complete run did not pass.` : !matches.length ? "The selected case did not execute/report. Missing and skipped cases do not pass." : runnerFailure ? `This case reported ${reportedStatus}, but the complete run did not pass. ${runnerFailure}` : ""].filter(Boolean).join("\n"), report: slash(path.relative(reportRoot, report)) });
|
|
207
316
|
}
|
|
208
317
|
if (signal.aborted) break;
|
|
209
318
|
}
|
|
@@ -217,7 +326,7 @@ export function createVerificationController(root) {
|
|
|
217
326
|
}
|
|
218
327
|
return { ok: diagnostics.length === 0 && results.length > 0 && results.every(result => result.status === "passed" && result.current), diagnostics, results };
|
|
219
328
|
} catch (error) { return { ok: false, diagnostics: [{ rule: "behavior-verification", message: error.message }], results }; }
|
|
220
|
-
finally { try { await host?.close(); } finally { if (active === controller) active = undefined; finish(); } }
|
|
329
|
+
finally { try { await host?.close(); } finally { if (active === controller) { active = undefined; progress = undefined; } finish(); } }
|
|
221
330
|
},
|
|
222
331
|
};
|
|
223
332
|
}
|
package/docs/ADS.md
CHANGED
|
@@ -107,6 +107,11 @@ framework/dependency gates and MCP execution contract.
|
|
|
107
107
|
Flat owners may use `Button.scenario.tsx` and `Button.fixture.ts` instead.
|
|
108
108
|
`Scenario.tsx` exports a default independently mountable view and literal metadata
|
|
109
109
|
`export const scenario = { title: "Button states", covers: ["Button"] };`.
|
|
110
|
+
Optional static `captureTargets` maps up to 20 names to CSS selectors scoped to
|
|
111
|
+
that scenario's App. These support explicitly requested element screenshots; they
|
|
112
|
+
do not grant coverage or styling exceptions. PNGs and current/candidate metadata
|
|
113
|
+
are generated development evidence, outside source ownership and product bundles.
|
|
114
|
+
See the portable [capture workflow](../skills/ads-designer/references/captures.md).
|
|
110
115
|
Token entries under the configured token directory declare
|
|
111
116
|
`export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
|
|
112
117
|
`TokenSamples` is any project-owned public review pattern, not a required name.
|
|
@@ -159,7 +164,10 @@ or applies the right CSS property. Browser review still verifies the samples.
|
|
|
159
164
|
|
|
160
165
|
Scenarios and fixture helpers are development-only wherever colocated. Runtime
|
|
161
166
|
source and the public barrel cannot import or re-export them. Showcase/scenario
|
|
162
|
-
code defines no CSS, custom classes or native controls; use public ADS
|
|
167
|
+
code defines no CSS, custom classes or standalone native controls; use public ADS
|
|
168
|
+
APIs. A single direct native child can supply semantics to a public runtime slot
|
|
169
|
+
when source tracing proves that owner clones the exact child with its own props.
|
|
170
|
+
An `asChild` flag alone is not evidence; scenario styling remains forbidden.
|
|
163
171
|
Scenarios may consume public class helpers whose returned styles trace to owned
|
|
164
172
|
Linaria. Stable hooks in that owner may accompany real styles; strings forwarded
|
|
165
173
|
from scenarios, hook-only returns and untraceable outputs remain invalid.
|
|
@@ -378,7 +386,7 @@ The checks enforce the architecture's ownership boundaries:
|
|
|
378
386
|
| Public API and dependencies | Consumers use public ADS imports; ADS implementations and showcase do not import application source; runtime code does not import showcase fixtures |
|
|
379
387
|
| Interaction policy | Primitive state variables stay inside components; non-state customization hooks require explicit registration |
|
|
380
388
|
| Renderer exceptions | Inline styles and imperative writes require used, narrow configured boundaries; stale entries fail; public props omit inherited style and reject open-ended visual strings |
|
|
381
|
-
| Preview ownership | Scenarios define no CSS, custom classes, or native controls; all declared tokens and public renderable UI have reachable registered scenarios; token references must exist; legacy catalogue pages are checked when present |
|
|
389
|
+
| Preview ownership | Scenarios define no CSS, custom classes, or standalone native controls; native slot children require public runtime cloning evidence; all declared tokens and public renderable UI have reachable registered scenarios; token references must exist; legacy catalogue pages are checked when present |
|
|
382
390
|
| Assets | Source/public collisions and configured generated artifacts in source are rejected |
|
|
383
391
|
|
|
384
392
|
Pointer capture without locally visible capture-loss cleanup produces a
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -303,6 +303,10 @@ Opaque conditional branches do not grant speculative coverage. Scenarios may
|
|
|
303
303
|
consume deliberate public class helpers when their returned styles retain
|
|
304
304
|
runtime-owned Linaria provenance; stable owner-authored hooks may accompany real
|
|
305
305
|
styles. This preserves ownership rather than granting scenario styling freedom.
|
|
306
|
+
A native element may provide semantics as the single direct child of a public
|
|
307
|
+
runtime slot when source tracing proves it is cloned with that owner's props.
|
|
308
|
+
This preserves polymorphic composition without allowing standalone scenario
|
|
309
|
+
controls, scenario styling or name-based exceptions.
|
|
306
310
|
The skill-local [scenario guide](../skills/ads-designer/references/scenarios.md)
|
|
307
311
|
explains evidence, limits and diagnostics.
|
|
308
312
|
|
|
@@ -336,6 +340,39 @@ The MCP server serves compiled bytes without evaluating project TSX. Compiler
|
|
|
336
340
|
adapters normalize each entry to mount/unmount, retaining native React or Preact
|
|
337
341
|
semantics; choosing a framework does not install one into the tooling host.
|
|
338
342
|
|
|
343
|
+
## On-demand visual evidence
|
|
344
|
+
|
|
345
|
+
The capture unit is a registered, independently mountable scenario or one of its
|
|
346
|
+
project-declared `captureTargets`. Components, patterns, compositions and token
|
|
347
|
+
previews use the same real MCP App, fixtures and adapter as interactive review.
|
|
348
|
+
Shared controllers/hooks need a visible demonstration. Capture introduces no
|
|
349
|
+
tool-owned component templates, styling or fixture inference.
|
|
350
|
+
|
|
351
|
+
ADS owns isolated preview hosting, App readiness and an owned Playwright process;
|
|
352
|
+
the receiving project supplies Playwright and its installed browser. The capture
|
|
353
|
+
host is the existing minimal MCP client/bridge with navigation hidden, giving the
|
|
354
|
+
sandboxed App the requested viewport. Project runtime, fonts and styles remain
|
|
355
|
+
inside the App resource graph. The image excludes showcase chrome.
|
|
356
|
+
|
|
357
|
+
Captures exist only when requested. `ads_capture` replaces current and removes any
|
|
358
|
+
candidate for that scenario/target. Guarded patch/transaction dry runs can opt into
|
|
359
|
+
`screenshot:true` with explicit selected captures: after the normal gates, ADS
|
|
360
|
+
renders current and candidate under matching settings without changing live source.
|
|
361
|
+
A new scenario has no fabricated baseline. Final freshness/conflict checks precede
|
|
362
|
+
image publication. Matching accepted source promotes candidate to current.
|
|
363
|
+
|
|
364
|
+
There are at most two slots per target: current and candidate. Revisions/settings
|
|
365
|
+
are metadata, not new resource identities. There is no previous slot, accumulating
|
|
366
|
+
history or TTL. Stale source is identified explicitly; deleted targets are collected.
|
|
367
|
+
PNG/pixel/store limits and explicit clearing bound generated storage. These images
|
|
368
|
+
are visual evidence for one state, not passing behavioral guarantees.
|
|
369
|
+
|
|
370
|
+
One MCP capture index and a PNG resource template expose stable image slots.
|
|
371
|
+
Subscriptions receive resource updates when images change or disappear. The pair
|
|
372
|
+
is published as one atomic generated record under `.ds/captures/` and stays outside
|
|
373
|
+
source ownership and product bundles. Detailed usage lives in each portable skill's
|
|
374
|
+
[capture reference](../skills/ads-designer/references/captures.md).
|
|
375
|
+
|
|
339
376
|
## Build checks keep the architecture in place
|
|
340
377
|
|
|
341
378
|
The project build runs ADS checks before producing application or showcase
|
|
@@ -369,8 +406,12 @@ context, or the shared token foundation. ADS resolves file ownership, reconstruc
|
|
|
369
406
|
candidate files locally and validates a temporary project copy with architecture
|
|
370
407
|
checks, registered lint rules and project validation scripts. Affected registered
|
|
371
408
|
behavior checks, including consumers of a changed shared controller, also run in
|
|
372
|
-
the isolated candidate project.
|
|
373
|
-
|
|
409
|
+
the isolated candidate project. The project owns its test code and runner policy;
|
|
410
|
+
ADS owns affected-check selection, candidate hosting, progress, evidence freshness
|
|
411
|
+
and process cleanup. One profile timeout bounds its entire runner process. Parallel
|
|
412
|
+
workers can share that candidate host with independent App state. Partial outcomes
|
|
413
|
+
remain diagnostic when execution is interrupted; they cannot approve a mutation.
|
|
414
|
+
Passing checks describe their registered assertions, not every natural-language claim. This keeps edits
|
|
374
415
|
focused without requiring entire file bodies in model context. Peer units and
|
|
375
416
|
unrelated owners remain outside the selected unit's edit boundary. Shared foundation,
|
|
376
417
|
branding and guides have their own units. Shared credits and text license notices
|
package/docs/BEHAVIORS.md
CHANGED
|
@@ -139,17 +139,57 @@ configuration must discover colocated spec files. Configure browser installation
|
|
|
139
139
|
and any required test setup in that project. The ADS tooling host gains no
|
|
140
140
|
framework or browser dependency from a profile.
|
|
141
141
|
|
|
142
|
-
The Playwright adapter appends selected file paths
|
|
143
|
-
artifact directory.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
The Playwright adapter appends selected file paths and case-tag filtering, its
|
|
143
|
+
reporter and a generated artifact directory. Unselected cases in the same spec
|
|
144
|
+
do not run. It preserves the project's worker configuration.
|
|
145
|
+
|
|
146
|
+
`timeoutMs` bounds the complete runner process for one profile, including runner
|
|
147
|
+
startup, hooks, retries and all selected cases. It is not a per-test timeout.
|
|
148
|
+
Playwright's own test/hook timeouts remain separate project settings. Set the
|
|
149
|
+
profile budget from measured suite durations with headroom; increasing it does
|
|
150
|
+
not repair a stalled assertion or cleanup.
|
|
151
|
+
|
|
152
|
+
Independent specs can use multiple Playwright workers against the same managed
|
|
153
|
+
candidate host. Each check needs its own page/context and state; avoid fixed ports,
|
|
154
|
+
shared mutable fixtures and test-order dependencies. Start with a small worker
|
|
155
|
+
count and measure on the target machine. Files normally run in parallel; tests
|
|
156
|
+
within one file normally remain sequential. ADS does not force parallelism or
|
|
157
|
+
change verification policy. See [Playwright parallelism](https://playwright.dev/docs/test-parallel).
|
|
158
|
+
Prefer readiness/assertion polling over arbitrary sleeps. Timer-only guarantees
|
|
159
|
+
may use [Playwright's clock](https://playwright.dev/docs/clock); retain real time
|
|
160
|
+
when browser motion, pointer gestures or layout timing are what the check verifies.
|
|
161
|
+
|
|
162
|
+
An ordinary test-failure exit (code 1) with a complete report and actual failing
|
|
163
|
+
cases preserves independent case outcomes: passing checks stay `passed`, failed
|
|
164
|
+
checks stay `failed`, and overall verification still blocks the mutation. The
|
|
165
|
+
report must account for every selected check, with no active cases or suite errors.
|
|
166
|
+
The same rule supports older custom reports without an explicit `complete` field
|
|
167
|
+
after normal process completion. An unexplained nonzero exit cannot certify a
|
|
168
|
+
pass, even if all reported cases claim success. Results retain `reportedStatus`
|
|
169
|
+
and `runnerExitCode` and explain the overall failure. Suite-level browser errors and
|
|
148
170
|
bounded runner output appear in diagnostics. `openBehaviorScenario` includes the
|
|
149
171
|
scenario ID and failed browser requests in startup failures; App resource errors
|
|
150
172
|
identify the resource URI. Failed assertions and startup errors are not retried
|
|
151
173
|
automatically. Fix or investigate the failure before explicitly verifying again.
|
|
152
174
|
|
|
175
|
+
The reporter atomically publishes partial snapshots at test start/end. While a
|
|
176
|
+
run is active, `ads_status.behaviors.progress` exposes the profile, runner budget,
|
|
177
|
+
elapsed time, completed/active/pending counts, worker count when available and
|
|
178
|
+
case identities/timings. Candidate mutation verification exposes the same data at
|
|
179
|
+
`ads_status.mutations.progress`. Clients requesting MCP progress receive updates
|
|
180
|
+
and a heartbeat during the runner phase; client-side request timeouts still apply.
|
|
181
|
+
`durationMs` records completed case execution, including attempts, separately from
|
|
182
|
+
the suite's wall time. Parallel case durations can sum to more than wall time.
|
|
183
|
+
|
|
184
|
+
After a deadline, valid completed outcomes remain visible through `reportedStatus`
|
|
185
|
+
and durations, alongside active and unstarted case identities. A passed case from
|
|
186
|
+
an interrupted run has blocking status `incomplete`; real assertion failures retain
|
|
187
|
+
their own messages. No incomplete run can accept a mutation or certify a guarantee.
|
|
188
|
+
Reports and artifacts stay under the live project's `.ds/verification/` even after
|
|
189
|
+
the candidate copy is removed. This is diagnostic output, not passing live-source
|
|
190
|
+
evidence. Old/custom runners without incremental progress use `not_reported` for
|
|
191
|
+
cases whose execution state cannot be established.
|
|
192
|
+
|
|
153
193
|
Use native test tags for stable case identity:
|
|
154
194
|
|
|
155
195
|
```ts
|
|
@@ -210,7 +250,13 @@ selected `checks` and optional `scenarios` URLs. Write a bounded JSON report:
|
|
|
210
250
|
}
|
|
211
251
|
```
|
|
212
252
|
|
|
213
|
-
Case statuses are `passed`, `failed` or `skipped`, with optional `message
|
|
253
|
+
Case statuses are `passed`, `failed` or `skipped`, with optional `message` and
|
|
254
|
+
`durationMs`. Protocol runners may atomically publish intermediate reports with
|
|
255
|
+
`complete:false` and an `active` list of `{id,file,startedAt,workerIndex?}`, then
|
|
256
|
+
publish `complete:true` at completion. Older final reports without `complete`
|
|
257
|
+
remain supported after normal process completion. An explicitly incomplete report
|
|
258
|
+
cannot pass even with exit code zero. Partial reports retain the same schema/run
|
|
259
|
+
identity and size limits as final reports.
|
|
214
260
|
`file` is project-relative. Nonzero process exit, missing cases, skipped cases,
|
|
215
261
|
wrong run IDs and unavailable reports do not pass. Test commands are trusted
|
|
216
262
|
project code; an adapter cannot prove the quality or honesty of their assertions.
|
|
@@ -245,10 +291,23 @@ Computed module selection conservatively includes all ADS owners. Environment,
|
|
|
245
291
|
network and other unregistered external inputs remain the runner's responsibility;
|
|
246
292
|
an evidence record is not a proof about arbitrary external state.
|
|
247
293
|
|
|
294
|
+
For named imports through simple re-export barrels, ADS follows selected bindings
|
|
295
|
+
and retains dependencies with declared side effects. This narrowing requires a
|
|
296
|
+
package `sideEffects:false` or a supported simple `sideEffects` glob array; preserve
|
|
297
|
+
CSS/global effects in that policy. Unknown policies/patterns, wildcard exports and
|
|
298
|
+
computed imports remain conservative. Barrel source changes still invalidate
|
|
299
|
+
dependents. ADS caches source parsing, resolution and digests within an inspection
|
|
300
|
+
only; later runs read current source again.
|
|
301
|
+
|
|
248
302
|
Guarded mutations execute affected registered behavior contracts after core,
|
|
249
303
|
Oxlint and configured project checks, before writing source. Imported shared
|
|
250
304
|
controller changes also select dependent contracts conservatively. Rejected and
|
|
251
305
|
dry-run patches publish no source changes. Verification output stays under `.ds/`.
|
|
306
|
+
To check a prepared draft, request its diff and pass the returned `patchFile` to
|
|
307
|
+
`ads_apply_patch` with `dryRun:true`. This checks the isolated candidate, including
|
|
308
|
+
affected behavior contracts and its configured browser preview. Candidate evidence
|
|
309
|
+
is not persisted as passing evidence for live source. `ads_verify` selects checks
|
|
310
|
+
for current source and has no draft argument.
|
|
252
311
|
|
|
253
312
|
## Packaging and performance
|
|
254
313
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.14
|
|
4
|
+
|
|
5
|
+
- Preserve independent passing behavior outcomes in complete runs with failed
|
|
6
|
+
assertions while keeping overall verification and mutations blocked. Retain
|
|
7
|
+
incomplete status for missing results, suite errors and unexplained exits.
|
|
8
|
+
- Recognize native scenario children through source-verified public runtime slot
|
|
9
|
+
composition, including React/Preact cloning and local prop mergers. Keep
|
|
10
|
+
standalone controls, custom scenario styling and unproven slot paths rejected;
|
|
11
|
+
update diagnostics, architecture and portable skill guidance generically.
|
|
12
|
+
|
|
13
|
+
## 0.3.13
|
|
14
|
+
|
|
15
|
+
- Add on-demand scenario/named-element PNG captures and screenshot-enabled guarded
|
|
16
|
+
dry runs. Expose stable current/candidate resources, freshness and notifications;
|
|
17
|
+
standalone captures clear earlier candidates, matching applies promote them, and
|
|
18
|
+
bounded storage keeps no previous/history/TTL. Document architecture and portable
|
|
19
|
+
skill usage without introducing project-specific renderers or fixtures.
|
|
20
|
+
- Report behavior progress, worker activity and per-case durations, preserving
|
|
21
|
+
partial outcomes and candidate artifacts on timeout while blocking incomplete
|
|
22
|
+
runs. Send requested MCP progress updates and expose candidate progress in status.
|
|
23
|
+
- Filter selected Playwright case tags, preserve project worker configuration and
|
|
24
|
+
verify concurrent Apps against one candidate host. Narrow named barrel dependencies
|
|
25
|
+
under declared side-effect policies and reuse parsing/digests within an inspection.
|
|
26
|
+
- Explain draft behavior verification through diff preparation and candidate
|
|
27
|
+
dry runs in all five portable skills. Distinguish candidate checks from
|
|
28
|
+
selected live-source verification through `ads_verify`.
|
|
29
|
+
|
|
3
30
|
## 0.3.12
|
|
4
31
|
|
|
5
32
|
- Let the documentation unit create shared credits, conventional root license
|
package/docs/GUIDE.md
CHANGED
|
@@ -5,7 +5,54 @@ contains a Node CLI, shared token contract, static checks, an Oxlint plugin, and
|
|
|
5
5
|
empty foundation scaffolding. It includes no component or pattern catalogue,
|
|
6
6
|
fonts, mock screens, or UI runtime dependency.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Use ADS through MCP
|
|
9
|
+
|
|
10
|
+
Connect your coding agent using the [MCP client setup](MCP.md#connect-codex-or-claude).
|
|
11
|
+
The client launches `ads mcp` in your project; add `--mutations` to enable guarded
|
|
12
|
+
source editing. Initialization also supplies `ads-mcp` and `ads-read-mcp` launch
|
|
13
|
+
scripts. The server starts its showcase and configured preview compiler, so use
|
|
14
|
+
the returned showcase URL rather than starting a separate preview application.
|
|
15
|
+
|
|
16
|
+
An agent uses the MCP tools to inspect, edit and review the project:
|
|
17
|
+
|
|
18
|
+
1. Call `ads_status` to identify the connected project, available commands,
|
|
19
|
+
mutation mode, showcase URL and build freshness.
|
|
20
|
+
2. Find public UI/behavior APIs with `ads_catalog`, inspect relevant definitions
|
|
21
|
+
with `ads_tokens`, and discover preview IDs with `ads_scenarios`. Read one
|
|
22
|
+
relevant workflow through `ads_skills` and `ads_read_skill` before editing.
|
|
23
|
+
3. Open a discovered preview with `ads_show_scenario` or `ads_show_tokens`. These
|
|
24
|
+
render the project's actual scenario, fixtures and adapter through MCP Apps.
|
|
25
|
+
Use the returned showcase link when the client cannot render Apps.
|
|
26
|
+
4. When editing is enabled, discover ownership with `ads_units`, read relevant
|
|
27
|
+
source through `ads_read_file`, and submit an owner Git patch through
|
|
28
|
+
`ads_apply_patch`. Use `ads_apply_transaction` for genuinely coupled owners.
|
|
29
|
+
Prepare/diff a local draft with `ads_prepare_patch` when useful. Dry-run first;
|
|
30
|
+
ADS checks an isolated candidate before any live source is written.
|
|
31
|
+
5. Review diagnostics, registered behavior evidence and the rendered result.
|
|
32
|
+
`ads_behaviors` discovers guarantees; `ads_verify` executes selected registered
|
|
33
|
+
checks. Recheck `ads_status.build` after changes; notifications alone do not
|
|
34
|
+
establish current output.
|
|
35
|
+
|
|
36
|
+
For an on-demand screenshot, call `ads_capture` with a discovered scenario ID and
|
|
37
|
+
optionally a named `captureTargets` element, theme and viewport. To compare a
|
|
38
|
+
proposed change before applying it, add `screenshot:true` and an explicit
|
|
39
|
+
`captures:[{scenario:"<discovered ID>"}]` selection to the patch/transaction dry run.
|
|
40
|
+
Normal patches and dry runs create no screenshots. Capture uses project-installed
|
|
41
|
+
Playwright and its browser; ADS installs neither.
|
|
42
|
+
|
|
43
|
+
Read `ads://project/captures` and the returned PNG image URIs through MCP
|
|
44
|
+
`resources/read`. Each target keeps only current and candidate: a successful
|
|
45
|
+
standalone screenshot replaces current and clears candidate; a screenshot-enabled
|
|
46
|
+
dry run replaces the pair; applying matching source promotes candidate. Source
|
|
47
|
+
changes mark images stale. There is no previous slot, history or TTL.
|
|
48
|
+
`ads_capture_clear` discards candidate or clears both images. Subscribe to resource
|
|
49
|
+
updates when supported, and explicitly reread otherwise. Screenshots provide visual
|
|
50
|
+
evidence for one state; they do not replace behavior checks.
|
|
51
|
+
|
|
52
|
+
The [MCP guide](MCP.md) details tool/resource schemas, prompts, skills and
|
|
53
|
+
notifications. The portable [capture guide](../skills/ads-designer/references/captures.md)
|
|
54
|
+
explains screenshot setup, named targets, limits and comparison usage; MCP serves
|
|
55
|
+
the same guide at `ads://guides/captures`.
|
|
9
56
|
|
|
10
57
|
## Implementation support
|
|
11
58
|
|