@reqvire-org/ads 0.3.16 → 0.3.17
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 +3 -0
- package/dist/check.mjs +13 -8
- package/dist/evidence.mjs +18 -0
- package/dist/investigations.mjs +70 -0
- package/dist/mcp/context.mjs +1 -1
- package/dist/mcp/mutations.mjs +34 -17
- package/dist/mcp/preview-widget.bundle.js +12 -12
- package/dist/mcp/resource-loader.js +3 -3
- package/dist/mcp/server.mjs +30 -8
- package/dist/scenario-selectors.mjs +140 -0
- package/dist/selector-match.mjs +94 -0
- package/dist/showcase/client.bundle.js +2 -2
- package/dist/skills/ads-break-ui/SKILL.md +3 -0
- package/dist/skills/ads-break-ui/references/evidence.md +73 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
- package/dist/skills/ads-designer/SKILL.md +3 -0
- package/dist/skills/ads-designer/references/evidence.md +73 -0
- package/dist/skills/ads-designer/references/scenarios.md +32 -0
- package/dist/skills/ads-import/SKILL.md +3 -0
- package/dist/skills/ads-import/references/evidence.md +73 -0
- package/dist/skills/ads-import/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/SKILL.md +3 -0
- package/dist/skills/ads-migrate/references/evidence.md +73 -0
- package/dist/skills/ads-migrate/references/scenarios.md +32 -0
- package/dist/skills/ads-motion/SKILL.md +3 -0
- package/dist/skills/ads-motion/references/evidence.md +73 -0
- package/dist/skills/ads-motion/references/scenarios.md +32 -0
- package/dist/verification/browser.mjs +22 -7
- package/dist/verification/playwright-reporter.cjs +23 -5
- package/dist/verification.mjs +31 -9
- package/docs/ARCHITECTURE.md +20 -3
- package/docs/BEHAVIORS.md +18 -0
- package/docs/CHANGELOG.md +13 -0
- package/docs/MCP.md +13 -0
- package/package.json +1 -1
|
@@ -16,30 +16,45 @@ export async function openBehaviorScenario(page, checkId, { timeout = 30000 } =
|
|
|
16
16
|
|
|
17
17
|
/** Shared readiness for registered verification and on-demand captures. */
|
|
18
18
|
export async function openPreviewScenario(page, url, scenario, { timeout = 30000 } = {}) {
|
|
19
|
-
const failures = [];
|
|
19
|
+
const failures = [], requests = [], errors = [];
|
|
20
|
+
let boundary;
|
|
21
|
+
const record = (request, detail) => {
|
|
22
|
+
if (requests.length >= 8) return;
|
|
23
|
+
let resourceUri;
|
|
24
|
+
try { const payload=request.postDataJSON(); if(payload?.method==="resources/read"&&typeof payload.params?.uri==="string"&&payload.params.uri.startsWith("ads://"))resourceUri=payload.params.uri.slice(0,500); } catch { /* Bodies are never retained. */ }
|
|
25
|
+
requests.push({method:request.method(),url:request.url().slice(0,300),...detail,...(resourceUri?{resourceUri}:{})});
|
|
26
|
+
};
|
|
20
27
|
const failed = request => {
|
|
21
28
|
if (failures.length < 5) failures.push(`${request.method()} ${request.url().slice(0, 300)}: ${request.failure()?.errorText ?? "request failed"}`);
|
|
29
|
+
record(request,{error:request.failure()?.errorText??"request failed"});
|
|
22
30
|
};
|
|
31
|
+
const response = response => { if(response.status()>=400)record(response.request(),{status:response.status()}); };
|
|
32
|
+
const pageError = error => { if(errors.length<3)errors.push(error.message.slice(0,500)); };
|
|
33
|
+
const diagnostic = (outcome, detail={}) => console.warn(JSON.stringify({adsPreviewDiagnostic:{kind:"preview-startup",scenario,outcome,viewport:page.viewportSize(),requests,errors,...detail}}));
|
|
23
34
|
page.on("requestfailed", failed);
|
|
35
|
+
page.on("response", response);page.on("pageerror", pageError);
|
|
24
36
|
try {
|
|
25
37
|
await page.goto(url, { waitUntil: "load", timeout });
|
|
26
38
|
const end = Date.now() + timeout;
|
|
27
39
|
while (Date.now() < end) {
|
|
28
40
|
for (const frame of page.frames().filter(frame => frame !== page.mainFrame())) {
|
|
29
|
-
const state = await frame.evaluate(() =>
|
|
41
|
+
const state = await frame.evaluate(() => {const data=document.documentElement.dataset;return {state:data.adsPreviewState,id:data.adsScenario,revision:data.adsPreviewRevision,category:data.adsPreviewErrorCategory,code:data.adsPreviewErrorCode,resourceUri:data.adsPreviewResourceUri,message:document.getElementById("ads-preview-status")?.textContent};}).catch(() => ({}));
|
|
30
42
|
if (state.id === scenario && state.state === "ready") {
|
|
31
43
|
const retries = await page.evaluate(() => Number(document.documentElement.dataset.adsResourceRetries ?? 0)).catch(() => 0);
|
|
32
44
|
if (retries > 0) console.warn(JSON.stringify({ adsPreviewDiagnostic: { kind: "resource-read-recovery", scenario, retries, failedRequests: failures } }));
|
|
45
|
+
diagnostic("ready",{previewRevision:state.revision??null});
|
|
33
46
|
return frame;
|
|
34
47
|
}
|
|
35
|
-
if (state.id === scenario && state.state === "error") throw new Error(state.message || "The MCP App failed before verification.");
|
|
48
|
+
if (state.id === scenario && state.state === "error") {boundary={category:state.category??"runner",phase:"preview-startup",code:state.code??"ADS_PREVIEW_STARTUP",...(state.resourceUri?{resourceUri:state.resourceUri}:{})};throw new Error(state.message || "The MCP App failed before verification.");}
|
|
36
49
|
}
|
|
37
|
-
const error = await page.evaluate(() => document.querySelector('#status[data-error="true"]')
|
|
38
|
-
if (error) throw new Error(error);
|
|
50
|
+
const error = await page.evaluate(() => {const node=document.querySelector('#status[data-error="true"]');return node?{message:node.textContent,category:node.dataset.errorCategory,code:node.dataset.errorCode}:null;}).catch(() => null);
|
|
51
|
+
if (error) {boundary={category:error.category??(requests.length?"transport":"runner"),phase:"host-startup",code:error.code??(requests.length?"ADS_BROWSER_REQUEST":"ADS_HOST_STARTUP")};throw new Error(error.message);}
|
|
39
52
|
await page.waitForTimeout(50);
|
|
40
53
|
}
|
|
41
54
|
throw new Error("Timed out waiting for the selected MCP App scenario to become current and ready. Inspect ads_status and ads://project/build.");
|
|
42
55
|
} catch (error) {
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
boundary??={category:requests.length?"transport":"runner",phase:"preview-startup",code:requests.length?"ADS_BROWSER_REQUEST":"ADS_PREVIEW_NOT_READY",certainty:requests.length?"observed-boundary":"unclassified"};
|
|
57
|
+
diagnostic("failed",{failure:boundary});
|
|
58
|
+
throw Object.assign(new Error(`Scenario ${scenario}: ${error.message}${failures.length ? `\nFailed browser requests:\n${failures.join("\n")}` : ""}`, { cause: error }),{failure:boundary});
|
|
59
|
+
} finally { page.off("requestfailed", failed);page.off("response",response);page.off("pageerror",pageError); }
|
|
45
60
|
}
|
|
@@ -10,6 +10,7 @@ module.exports = class AdsBehaviorReporter {
|
|
|
10
10
|
this.active = new Map();
|
|
11
11
|
this.diagnostics = new Map();
|
|
12
12
|
this.diagnosticBuffers = new Map();
|
|
13
|
+
this.steps = new Map();
|
|
13
14
|
this.startedAt = Date.now();
|
|
14
15
|
this.totalTests = 0;
|
|
15
16
|
}
|
|
@@ -22,25 +23,42 @@ module.exports = class AdsBehaviorReporter {
|
|
|
22
23
|
this.active.set(test.id, this.entries(test).map(entry => ({ ...entry, startedAt: (result.startTime ?? new Date()).toISOString(), workerIndex: result.workerIndex, retry: result.retry })));
|
|
23
24
|
this.publish();
|
|
24
25
|
}
|
|
26
|
+
location(value) {
|
|
27
|
+
if (!value?.file) return undefined;
|
|
28
|
+
const file=path.relative(this.request.projectRoot,value.file).replaceAll("\\","/");
|
|
29
|
+
return {file,line:value.line,column:value.column};
|
|
30
|
+
}
|
|
31
|
+
onStepEnd(test, _result, step) {
|
|
32
|
+
if (!step.error || this.steps.has(test.id)) return;
|
|
33
|
+
this.steps.set(test.id,{title:String(step.title).slice(0,300),category:step.category,location:this.location(step.error.location??step.location),durationMs:step.duration});
|
|
34
|
+
}
|
|
25
35
|
onTestEnd(test, result) {
|
|
26
36
|
this.active.delete(test.id);
|
|
27
37
|
const diagnostics = this.diagnostics.get(test.id);
|
|
28
|
-
|
|
38
|
+
const step=this.steps.get(test.id),startup=diagnostics?.findLast(item=>item.kind==="preview-startup"),failed=result.status!=="passed"||test.expectedStatus!=="passed";
|
|
39
|
+
const failure=failed?(startup?.failure??{category:step?.category==="expect"?"assertion":"runner",phase:step?.category==="expect"?"test-assertion":"test-execution",code:result.status==="timedOut"?"ADS_CASE_TIMEOUT":"ADS_CASE_FAILURE",certainty:step?.category==="expect"?"observed-boundary":"unclassified"}):undefined;
|
|
40
|
+
const artifacts=(result.attachments??[]).filter(item=>item.path).slice(0,5).map(item=>({name:String(item.name).slice(0,100),mimeType:String(item.contentType).slice(0,100),path:path.relative(path.dirname(process.env.ADS_VERIFY_REPORT),item.path).replaceAll("\\","/")}));
|
|
41
|
+
for (const entry of this.entries(test)) this.cases.push({ ...entry, status: !failed ? "passed" : ["skipped", "interrupted"].includes(result.status) ? "skipped" : "failed", durationMs: result.duration, workerIndex: result.workerIndex, retry: result.retry, completedAt: new Date().toISOString(), source:step?.location??this.location(result.errors?.[0]?.location??test.location), ...(step?{failingStep:step}:{}),...(failure?{failure}:{}),...(startup?.viewport?{viewport:startup.viewport,viewportSource:"preview-startup"}:{}),...(artifacts.length?{artifacts}:{}), ...(diagnostics?.length ? { diagnostics } : {}), message: result.errors?.map(error => error.message || error.stack || error.value || "The browser runner reported an error without details.").join("\n").slice(0, 1000) });
|
|
29
42
|
this.diagnostics.delete(test.id);
|
|
30
43
|
this.diagnosticBuffers.delete(test.id);
|
|
44
|
+
this.steps.delete(test.id);
|
|
31
45
|
this.publish();
|
|
32
46
|
}
|
|
33
47
|
onStdErr(chunk, test) {
|
|
34
48
|
if (!test) return;
|
|
35
49
|
const lines = ((this.diagnosticBuffers.get(test.id) ?? "") + String(chunk)).split("\n");
|
|
36
|
-
this.diagnosticBuffers.set(test.id, lines.pop().slice(-
|
|
50
|
+
this.diagnosticBuffers.set(test.id, lines.pop().slice(-8192));
|
|
37
51
|
for (const line of lines) {
|
|
38
|
-
if (line.length >
|
|
52
|
+
if (line.length > 8192) continue;
|
|
39
53
|
try {
|
|
40
54
|
const diagnostic = JSON.parse(line).adsPreviewDiagnostic;
|
|
41
|
-
if (
|
|
55
|
+
if (typeof diagnostic?.scenario !== "string") continue;
|
|
56
|
+
let entry;
|
|
57
|
+
if(diagnostic.kind==="resource-read-recovery"&&Number.isSafeInteger(diagnostic.retries)&&diagnostic.retries>0)entry={ kind: diagnostic.kind, scenario: diagnostic.scenario.slice(0, 300), retries: diagnostic.retries, failedRequests: (Array.isArray(diagnostic.failedRequests) ? diagnostic.failedRequests : []).slice(0, 5).map(value => String(value).slice(0, 500)) };
|
|
58
|
+
else if(diagnostic.kind==="preview-startup"&&["ready","failed"].includes(diagnostic.outcome))entry={kind:diagnostic.kind,scenario:diagnostic.scenario.slice(0,300),outcome:diagnostic.outcome,viewport:diagnostic.viewport,previewRevision:diagnostic.previewRevision,requests:diagnostic.requests,errors:diagnostic.errors,...(diagnostic.failure&&["architecture","build","transport","runner","assertion"].includes(diagnostic.failure.category)?{failure:diagnostic.failure}:{})};
|
|
59
|
+
if(!entry)continue;
|
|
42
60
|
const collected = this.diagnostics.get(test.id) ?? [];
|
|
43
|
-
if (collected.length < 5) collected.push(
|
|
61
|
+
if (collected.length < 5) collected.push(entry);
|
|
44
62
|
this.diagnostics.set(test.id, collected);
|
|
45
63
|
} catch { /* Ordinary test output is not an ADS startup diagnostic. */ }
|
|
46
64
|
}
|
package/dist/verification.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import { inspectUnits, configurationFiles } from "./units.mjs";
|
|
|
12
12
|
import { inspectBehaviors } from "./behaviors.mjs";
|
|
13
13
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
14
14
|
import { readPreviewManifest } from "./preview-resources.mjs";
|
|
15
|
+
import { acceptance, failure, failureCategories } from "./evidence.mjs";
|
|
16
|
+
import { investigationArtifacts, investigationUri, saveInvestigations } from "./investigations.mjs";
|
|
15
17
|
|
|
16
18
|
export const BEHAVIOR_URI = "ads://project/behaviors";
|
|
17
19
|
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
@@ -123,7 +125,7 @@ export function behaviorInputs(options, contract, check, context = inputContext(
|
|
|
123
125
|
const rows = [...files].sort().map(file => [slash(path.relative(options.root, file)), context.digest(file)]);
|
|
124
126
|
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]);
|
|
125
127
|
rows.push(["profile", JSON.stringify(options.config.mcp.behaviorProfiles[check.profile])], ["node", process.version]);
|
|
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)))]);
|
|
128
|
+
for (const file of ["./verification.mjs", "./verification/browser.mjs", "./verification/playwright-reporter.cjs", "./evidence.mjs", "./investigations.mjs"]) rows.push([`tool:${file}`, hash(readFileSync(new URL(file, import.meta.url)))]);
|
|
127
129
|
return { revision: hash(JSON.stringify(rows)), files: rows.map(([file]) => file) };
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -136,6 +138,11 @@ function saved(root) {
|
|
|
136
138
|
} catch { return {}; }
|
|
137
139
|
}
|
|
138
140
|
const identity = (unit, check) => `${unit}#${check}`;
|
|
141
|
+
function verificationResult(diagnostics, results) {
|
|
142
|
+
const ok=diagnostics.length===0&&results.length>0&&results.every(result=>result.status==="passed"&&result.current);
|
|
143
|
+
return {ok,diagnostics:diagnostics.map(diagnostic=>({...diagnostic,failure:diagnostic.failure??failure("runner","verification",diagnostic.rule)})),results,
|
|
144
|
+
acceptance:acceptance("selected-behavior-checks",ok,results.filter(result=>result.status==="passed"&&result.current).map(result=>`${result.unit}#${result.check}`),["unselected-checks","unasserted-behavior","visual-parity","accessibility-audit"])};
|
|
145
|
+
}
|
|
139
146
|
|
|
140
147
|
export function behaviorStatus(options) {
|
|
141
148
|
const inventory = inspectBehaviors(options), evidence = saved(options.root);
|
|
@@ -223,7 +230,7 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
223
230
|
status: () => ({ running: !!active, ...(active && progress ? { progress: { ...progress, elapsedMs: Date.now() - progress.startedAt } } : {}) }),
|
|
224
231
|
async close() { active?.abort(); await active?.completion; },
|
|
225
232
|
async run({ unit, units, checks, persist = true } = {}, signal, notifyProgress) {
|
|
226
|
-
if (active) return
|
|
233
|
+
if (active) return verificationResult([{ rule: "behavior-busy", message: "A behavior verification is already running." }],[]);
|
|
227
234
|
active = new AbortController();
|
|
228
235
|
const controller = active;
|
|
229
236
|
signal = signal ? AbortSignal.any([signal, active.signal]) : active.signal;
|
|
@@ -232,13 +239,13 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
232
239
|
const results = [], diagnostics = [];
|
|
233
240
|
try {
|
|
234
241
|
const options = loadConfig(root), inventory = inspectBehaviors(options);
|
|
235
|
-
if (inventory.diagnostics.length) return {
|
|
242
|
+
if (inventory.diagnostics.length) return verificationResult(inventory.diagnostics.map(diagnostic=>({...diagnostic,failure:failure("architecture","behavior-contract",diagnostic.rule)})),results);
|
|
236
243
|
const selectedUnits = new Set(units ?? (unit ? [unit] : inventory.contracts.map(item => item.unit)));
|
|
237
244
|
const contracts = inventory.contracts.filter(item => selectedUnits.has(item.unit));
|
|
238
|
-
if (!contracts.length || [...selectedUnits].some(id => !contracts.some(contract => contract.unit === id))) return
|
|
245
|
+
if (!contracts.length || [...selectedUnits].some(id => !contracts.some(contract => contract.unit === id))) return verificationResult([{ rule: "behavior-selection", message: "No behavior contract matches the complete selection. Discover exact owner/check IDs with ads_behaviors." }],results);
|
|
239
246
|
const context = inputContext(options);
|
|
240
247
|
const selection = contracts.flatMap(contract => Object.entries(contract.checks).filter(([id]) => !checks || checks.includes(id)).map(([id, check]) => ({ unit: contract.unit, id, ...check, inputRevision: behaviorInputs(options, contract, check, context).revision, contract })));
|
|
241
|
-
if (!selection.length || checks?.some(id => !selection.some(check => check.id === id))) return
|
|
248
|
+
if (!selection.length || checks?.some(id => !selection.some(check => check.id === id))) return verificationResult([{ rule: "behavior-selection", message: "Unknown check selection. Use check IDs returned by ads_behaviors." }],results);
|
|
242
249
|
for (const profileName of new Set(selection.map(check => check.profile))) {
|
|
243
250
|
const profile = options.config.mcp.behaviorProfiles[profileName];
|
|
244
251
|
const group = selection.filter(check => check.profile === profileName);
|
|
@@ -295,7 +302,11 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
295
302
|
// Unexplained exits, suite errors and interrupted reports grant no pass.
|
|
296
303
|
const caseFailureExit = run?.code === 1 && !runnerError && !suiteErrors.length && !incompleteReport && reportValue?.cases.some(item => item?.status === "failed");
|
|
297
304
|
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;
|
|
298
|
-
if (runnerFailure)
|
|
305
|
+
if (runnerFailure) {
|
|
306
|
+
const caseFailures=reportValue?.cases.filter(item=>item.status==="failed"&&failureCategories.has(item.failure?.category)).map(item=>item.failure)??[];
|
|
307
|
+
diagnostics.push({ rule: "behavior-runner", message: runnerFailure, output: run.output?.slice(-4000), profile: profileName,
|
|
308
|
+
failure:caseFailureExit&&caseFailures.length?{...caseFailures[0],phase:"case-results",relatedCategories:[...new Set(caseFailures.map(item=>item.category))]}:failure("runner","profile-execution","ADS_RUNNER_EXIT",{certainty:caseFailureExit?"unclassified":"observed-boundary"}) });
|
|
309
|
+
}
|
|
299
310
|
const afterOptions = loadConfig(root), afterContext = inputContext(afterOptions);
|
|
300
311
|
for (const check of group) {
|
|
301
312
|
const file = slash(path.relative(root, path.resolve(root, path.dirname(check.contract.file), check.file)));
|
|
@@ -307,7 +318,17 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
307
318
|
const current = behaviorInputs(afterOptions, check.contract, check, afterContext).revision === check.inputRevision;
|
|
308
319
|
if (!current) status = "stale";
|
|
309
320
|
if (signal.aborted) status = "cancelled";
|
|
310
|
-
|
|
321
|
+
const caseDetail=matches.find(item=>item.status!=="passed")??matches.at(-1);
|
|
322
|
+
const reportedFailure=caseDetail?.failure;
|
|
323
|
+
const checkFailure=status==="passed"?undefined:status==="stale"||status==="cancelled"?failure("runner","verification",`ADS_${status.toUpperCase()}`):caseDetail?.status==="failed"&&failureCategories.has(reportedFailure?.category)?reportedFailure:blockedRun?failure("runner","profile-execution",runnerError?.reason??"ADS_RUNNER_INCOMPLETE",{certainty:"observed-boundary"}):failure("runner","test-execution","ADS_CASE_UNCLASSIFIED",{certainty:"unclassified"});
|
|
324
|
+
const accepted=acceptance("registered-case",status==="passed"&¤t,status==="passed"&¤t?["registered-case-completed"]:[],["unasserted-behavior","visual-parity","accessibility-audit"]);
|
|
325
|
+
const investigation={unit:check.unit,check:check.id,status,runId,resourceUri:investigationUri({unit:check.unit,check:check.id}),target:root===reportRoot?"project":"candidate",framework:options.config.framework,profile:profileName,mode:profile.mode??"unit",scenario:check.scenario??null,inputRevision:check.inputRevision,inputsCurrentAtCompletion:current,
|
|
326
|
+
source:caseDetail?.source??{file},failingStep:caseDetail?.failingStep??null,viewport:caseDetail?.viewport??null,viewportSource:caseDetail?.viewportSource??null,
|
|
327
|
+
observedPreviewRevision:matches.flatMap(item=>item.diagnostics??[]).findLast(item=>item.kind==="preview-startup")?.previewRevision??null,
|
|
328
|
+
requests:matches.flatMap(item=>item.diagnostics??[]).flatMap(item=>item.requests??[]).slice(0,8),browserErrors:matches.flatMap(item=>item.diagnostics??[]).flatMap(item=>item.errors??[]).slice(0,3),
|
|
329
|
+
artifacts:investigationArtifacts(reportRoot,report,matches.flatMap(item=>item.artifacts??[])),acceptance:accepted,...(checkFailure?{failure:checkFailure}:{}),completedAt:new Date().toISOString()};
|
|
330
|
+
results.push({ unit: check.unit, check: check.id, file: check.file, case: check.case, profile: profileName, status, current, revision: check.inputRevision, runId, completedAt: investigation.completedAt,
|
|
331
|
+
...(checkFailure?{failure:checkFailure}:{}),acceptance:accepted,investigation,
|
|
311
332
|
previewRevision: profile.mode === "browser" ? host?.previewRevision() ?? readPreviewManifest(options)?.revision ?? null : null,
|
|
312
333
|
...(matches.length ? { reportedStatus, durationMs: matches.reduce((sum, item) => sum + (Number.isFinite(item.durationMs) ? Math.max(0, item.durationMs) : 0), 0) } : {}),
|
|
313
334
|
...(matches.some(item => item.diagnostics?.length) ? { startupDiagnostics: matches.flatMap(item => Array.isArray(item.diagnostics) ? item.diagnostics.slice(0, 5) : []).slice(0, 10) } : {}),
|
|
@@ -317,6 +338,7 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
317
338
|
}
|
|
318
339
|
if (signal.aborted) break;
|
|
319
340
|
}
|
|
341
|
+
saveInvestigations(reportRoot,results.map(result=>result.investigation),inventory.contracts.flatMap(contract=>Object.keys(contract.checks).map(id=>identity(contract.unit,id))));
|
|
320
342
|
if (persist) {
|
|
321
343
|
const evidence = saved(root);
|
|
322
344
|
for (const result of results) evidence[identity(result.unit, result.check)] = result;
|
|
@@ -325,8 +347,8 @@ export function createVerificationController(root, { reportRoot = root, onProgre
|
|
|
325
347
|
if (Buffer.byteLength(content) > 2 * 1024 * 1024) throw new Error("Verification evidence exceeds 2 MiB. Remove obsolete generated evidence and retry.");
|
|
326
348
|
try { writeFileSync(temporary, content); renameSync(temporary, file); } finally { rmSync(temporary, { force: true }); }
|
|
327
349
|
}
|
|
328
|
-
return
|
|
329
|
-
} catch (error) { return
|
|
350
|
+
return verificationResult(diagnostics,results);
|
|
351
|
+
} catch (error) { return verificationResult([{ rule: "behavior-verification", message: error.message }],results); }
|
|
330
352
|
finally { try { await host?.close(); } finally { if (active === controller) { active = undefined; progress = undefined; } finish(); } }
|
|
331
353
|
},
|
|
332
354
|
};
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -322,7 +322,12 @@ styles. This preserves ownership rather than granting scenario styling freedom.
|
|
|
322
322
|
A native element may provide semantics as the single direct child of a public
|
|
323
323
|
runtime slot when source tracing proves it is cloned with that owner's props.
|
|
324
324
|
This preserves polymorphic composition without allowing standalone scenario
|
|
325
|
-
controls, scenario styling or name-based exceptions.
|
|
325
|
+
controls, scenario styling or name-based exceptions. Selector-based composition
|
|
326
|
+
can establish the same ownership through actual returned DOM and attached runtime
|
|
327
|
+
Linaria: a positive native-tag relationship, or a `:has` relationship to a marker
|
|
328
|
+
declared by a real runtime-owned child. Broad selectors and scenario-authored
|
|
329
|
+
lookalikes grant no permission. This source proof does not establish computed
|
|
330
|
+
appearance, keyboard behavior or visual parity; browser checks remain necessary.
|
|
326
331
|
The skill-local [scenario guide](../skills/ads-designer/references/scenarios.md)
|
|
327
332
|
explains evidence, limits and diagnostics.
|
|
328
333
|
|
|
@@ -427,8 +432,20 @@ ADS owns affected-check selection, candidate hosting, progress, evidence freshne
|
|
|
427
432
|
and process cleanup. One profile timeout bounds its entire runner process. Parallel
|
|
428
433
|
workers can share that candidate host with independent App state. Partial outcomes
|
|
429
434
|
remain diagnostic when execution is interrupted; they cannot approve a mutation.
|
|
430
|
-
Passing checks describe their registered assertions, not every natural-language claim.
|
|
431
|
-
|
|
435
|
+
Passing checks describe their registered assertions, not every natural-language claim.
|
|
436
|
+
Results distinguish architecture, project-build, transport, runner and assertion
|
|
437
|
+
boundaries with structured phase/code evidence; an unclassified test exception
|
|
438
|
+
does not become an assertion or a component defect. Acceptance explicitly lists
|
|
439
|
+
established and unverified scope. Verification retains one latest investigation
|
|
440
|
+
per registered check, including isolated candidates: exact input revision,
|
|
441
|
+
observed preview revision, failing step/source, observed startup viewport,
|
|
442
|
+
failed requests and available project-recorded artifacts. MCP exposes bounded
|
|
443
|
+
bundle/index resources and only registered, digest-checked artifact files.
|
|
444
|
+
Another run replaces that check's resource access; candidate evidence never
|
|
445
|
+
certifies live source. Detailed usage lives in the portable skill-local
|
|
446
|
+
[evidence guide](../skills/ads-designer/references/evidence.md).
|
|
447
|
+
|
|
448
|
+
Edits stay focused without requiring entire file bodies in model context. Peer units and
|
|
432
449
|
unrelated owners remain outside the selected unit's edit boundary. Shared foundation,
|
|
433
450
|
branding and guides have their own units. Shared credits and text license notices
|
|
434
451
|
belong to the documentation unit, including conventional root notices and shared
|
package/docs/BEHAVIORS.md
CHANGED
|
@@ -272,6 +272,13 @@ publish `complete:true` at completion. Older final reports without `complete`
|
|
|
272
272
|
remain supported after normal process completion. An explicitly incomplete report
|
|
273
273
|
cannot pass even with exit code zero. Partial reports retain the same schema/run
|
|
274
274
|
identity and size limits as final reports.
|
|
275
|
+
Cases may optionally include `failure: {category,phase,code,certainty?}`,
|
|
276
|
+
`source: {file,line,column}`, `failingStep`, `viewport`/`viewportSource`, and
|
|
277
|
+
`artifacts: [{name,mimeType,path}]`. Artifact paths are relative to the report
|
|
278
|
+
directory and must remain under that run's `artifacts/` directory. The built-in
|
|
279
|
+
Playwright reporter supplies these from actual step/startup/attachment evidence.
|
|
280
|
+
Older protocol runners can omit them; ADS preserves that lack of classification
|
|
281
|
+
instead of treating an arbitrary thrown message as an assertion.
|
|
275
282
|
`file` is project-relative. Nonzero process exit, missing cases, skipped cases,
|
|
276
283
|
wrong run IDs and unavailable reports do not pass. Test commands are trusted
|
|
277
284
|
project code; an adapter cannot prove the quality or honesty of their assertions.
|
|
@@ -300,6 +307,17 @@ changes mark results stale; changes during execution prevent a current pass.
|
|
|
300
307
|
Guarantees report passing only when all their checks have current passing evidence.
|
|
301
308
|
Passing applies to those cases and profiles, not every application state.
|
|
302
309
|
|
|
310
|
+
Results expose structured `failure` boundaries and explicit `acceptance` scope.
|
|
311
|
+
Use `ads_investigate({unit,check})` for the latest compact investigation, including
|
|
312
|
+
isolated dry runs; `ads://project/investigations` lists available entries. Exact
|
|
313
|
+
input and observed preview revisions, failing step/source, startup viewport,
|
|
314
|
+
failed startup requests and available project-recorded artifacts keep diagnosis
|
|
315
|
+
separate from an inferred component defect. No artifact is invented or test
|
|
316
|
+
retried. Each check retains one latest bundle; old artifact resource access is
|
|
317
|
+
superseded. Candidate bundles remain distinct from passing live-source evidence.
|
|
318
|
+
Read the portable [evidence reference](../skills/ads-designer/references/evidence.md)
|
|
319
|
+
for category meanings, collection limits and artifact policy.
|
|
320
|
+
|
|
303
321
|
Revisions include owned files, literal import dependencies, local CSS imports and
|
|
304
322
|
asset URLs, token foundation, project/runner configuration, lockfiles and tooling.
|
|
305
323
|
Computed module selection conservatively includes all ADS owners. Environment,
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.17
|
|
4
|
+
|
|
5
|
+
- Recognize native semantic composition through source-verified runtime Linaria
|
|
6
|
+
selectors, including runtime-owned part bindings. Keep unrelated controls,
|
|
7
|
+
broad selectors, fabricated markers and custom scenario styling rejected.
|
|
8
|
+
- Distinguish architecture, build, transport, runner and assertion boundaries;
|
|
9
|
+
expose explicit acceptance scope and latest-per-check investigation bundles
|
|
10
|
+
with revisions, source/step, startup viewport, requests and available artifacts.
|
|
11
|
+
Preserve candidate/live-source evidence and independent case outcomes.
|
|
12
|
+
- Add generic React/Preact conformance checks for selector composition, extracted
|
|
13
|
+
CSS, MCP resource recovery and native iframe interaction. Document diagnosis
|
|
14
|
+
through portable skill references without project-specific implementation rules.
|
|
15
|
+
|
|
3
16
|
## 0.3.16
|
|
4
17
|
|
|
5
18
|
- Recover transient showcase MCP resource fetch failures with bounded read-only
|
package/docs/MCP.md
CHANGED
|
@@ -177,6 +177,7 @@ The server supplies agent guidance through its instructions, tool descriptions,
|
|
|
177
177
|
| `ads_show_scenario` | Render an exact discovered scenario with its own fixtures |
|
|
178
178
|
| `ads_behaviors` | Bounded guarantee/evidence discovery; pass an exact unit for contract details |
|
|
179
179
|
| `ads_verify` | Execute an owner's registered checks and record revision-bound evidence |
|
|
180
|
+
| `ads_investigate` | Read one check's latest compact failure/evidence bundle without rerunning it |
|
|
180
181
|
| `ads_capture` | On-demand PNG of a discovered scenario or named element; replace current and clear candidate |
|
|
181
182
|
| `ads_capture_clear` | Remove generated captures or discard only the candidate |
|
|
182
183
|
| `ads_show_<id>` | Optional explicitly configured grouped App |
|
|
@@ -275,6 +276,18 @@ single failed assertion does not turn the other checks into failures. Overall
|
|
|
275
276
|
verification and guarded application still fail. Suite errors, unexplained exits,
|
|
276
277
|
missing results and interrupted reports cannot certify passing checks.
|
|
277
278
|
|
|
279
|
+
For diagnosis, call `ads_investigate({ "unit": "components/Example", "check": "keyboard-navigation" })`
|
|
280
|
+
with discovered IDs. It reads the latest check bundle without rerunning anything,
|
|
281
|
+
including rejected candidates. `ads://project/investigations` lists available
|
|
282
|
+
bundles; returned resource URIs expose the bundle and available registered
|
|
283
|
+
artifacts. Exact input/observed preview revisions, source/failing step, startup
|
|
284
|
+
viewport and failed requests separate evidence from a suspected cause.
|
|
285
|
+
`failure.category` distinguishes architecture, build, transport, runner and
|
|
286
|
+
assertion boundaries; `acceptance` names established and unverified scope.
|
|
287
|
+
Only the latest bundle per check remains resource-accessible. Read the portable
|
|
288
|
+
[evidence guide](../skills/ads-designer/references/evidence.md) for collection
|
|
289
|
+
limits and project-controlled trace/screenshot recording.
|
|
290
|
+
|
|
278
291
|
Browser profiles start an isolated showcase and verify its sandboxed App using
|
|
279
292
|
the selected candidate's sources. StrictMode profiles need a project React
|
|
280
293
|
development harness. Profiles describe the execution environment, not a proof
|