@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.
Files changed (35) hide show
  1. package/README.md +3 -0
  2. package/dist/check.mjs +13 -8
  3. package/dist/evidence.mjs +18 -0
  4. package/dist/investigations.mjs +70 -0
  5. package/dist/mcp/context.mjs +1 -1
  6. package/dist/mcp/mutations.mjs +34 -17
  7. package/dist/mcp/preview-widget.bundle.js +12 -12
  8. package/dist/mcp/resource-loader.js +3 -3
  9. package/dist/mcp/server.mjs +30 -8
  10. package/dist/scenario-selectors.mjs +140 -0
  11. package/dist/selector-match.mjs +94 -0
  12. package/dist/showcase/client.bundle.js +2 -2
  13. package/dist/skills/ads-break-ui/SKILL.md +3 -0
  14. package/dist/skills/ads-break-ui/references/evidence.md +73 -0
  15. package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
  16. package/dist/skills/ads-designer/SKILL.md +3 -0
  17. package/dist/skills/ads-designer/references/evidence.md +73 -0
  18. package/dist/skills/ads-designer/references/scenarios.md +32 -0
  19. package/dist/skills/ads-import/SKILL.md +3 -0
  20. package/dist/skills/ads-import/references/evidence.md +73 -0
  21. package/dist/skills/ads-import/references/scenarios.md +32 -0
  22. package/dist/skills/ads-migrate/SKILL.md +3 -0
  23. package/dist/skills/ads-migrate/references/evidence.md +73 -0
  24. package/dist/skills/ads-migrate/references/scenarios.md +32 -0
  25. package/dist/skills/ads-motion/SKILL.md +3 -0
  26. package/dist/skills/ads-motion/references/evidence.md +73 -0
  27. package/dist/skills/ads-motion/references/scenarios.md +32 -0
  28. package/dist/verification/browser.mjs +22 -7
  29. package/dist/verification/playwright-reporter.cjs +23 -5
  30. package/dist/verification.mjs +31 -9
  31. package/docs/ARCHITECTURE.md +20 -3
  32. package/docs/BEHAVIORS.md +18 -0
  33. package/docs/CHANGELOG.md +13 -0
  34. package/docs/MCP.md +13 -0
  35. 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(() => ({ state: document.documentElement.dataset.adsPreviewState, id: document.documentElement.dataset.adsScenario, message: document.getElementById("ads-preview-status")?.textContent })).catch(() => ({}));
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"]')?.textContent).catch(() => null);
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
- throw new Error(`Scenario ${scenario}: ${error.message}${failures.length ? `\nFailed browser requests:\n${failures.join("\n")}` : ""}`, { cause: error });
44
- } finally { page.off("requestfailed", failed); }
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
- for (const entry of this.entries(test)) this.cases.push({ ...entry, status: result.status === "passed" && test.expectedStatus === "passed" ? "passed" : ["skipped", "interrupted"].includes(result.status) ? "skipped" : "failed", durationMs: result.duration, workerIndex: result.workerIndex, retry: result.retry, completedAt: new Date().toISOString(), ...(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) });
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(-4000));
50
+ this.diagnosticBuffers.set(test.id, lines.pop().slice(-8192));
37
51
  for (const line of lines) {
38
- if (line.length > 4000) continue;
52
+ if (line.length > 8192) continue;
39
53
  try {
40
54
  const diagnostic = JSON.parse(line).adsPreviewDiagnostic;
41
- if (diagnostic?.kind !== "resource-read-recovery" || typeof diagnostic.scenario !== "string" || !Number.isSafeInteger(diagnostic.retries) || diagnostic.retries < 1) continue;
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({ 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)) });
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
  }
@@ -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 { ok: false, diagnostics: [{ rule: "behavior-busy", message: "A behavior verification is already running." }], results: [] };
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 { ok: false, diagnostics: inventory.diagnostics, results };
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 { ok: false, diagnostics: [{ rule: "behavior-selection", message: "No behavior contract matches the complete selection. Discover exact owner/check IDs with ads_behaviors." }], results };
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 { ok: false, diagnostics: [{ rule: "behavior-selection", message: "Unknown check selection. Use check IDs returned by ads_behaviors." }], results };
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) diagnostics.push({ rule: "behavior-runner", message: runnerFailure, output: run.output?.slice(-4000), profile: profileName });
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
- 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(),
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"&&current,status==="passed"&&current?["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 { ok: diagnostics.length === 0 && results.length > 0 && results.every(result => result.status === "passed" && result.current), diagnostics, results };
329
- } catch (error) { return { ok: false, diagnostics: [{ rule: "behavior-verification", message: error.message }], results }; }
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
  };
@@ -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. This keeps edits
431
- focused without requiring entire file bodies in model context. Peer units and
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvire-org/ads",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "A design system your agent can see and shape. Your components. Your look. Your code.",
6
6
  "repository": {