@reqvire-org/ads 0.3.15 → 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 (41) 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 +18 -18
  13. package/dist/showcase/resource-reader.js +37 -0
  14. package/dist/skills/ads-break-ui/SKILL.md +3 -0
  15. package/dist/skills/ads-break-ui/references/evidence.md +73 -0
  16. package/dist/skills/ads-break-ui/references/guarded-editing.md +12 -0
  17. package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
  18. package/dist/skills/ads-designer/SKILL.md +3 -0
  19. package/dist/skills/ads-designer/references/evidence.md +73 -0
  20. package/dist/skills/ads-designer/references/guarded-editing.md +12 -0
  21. package/dist/skills/ads-designer/references/scenarios.md +32 -0
  22. package/dist/skills/ads-import/SKILL.md +3 -0
  23. package/dist/skills/ads-import/references/evidence.md +73 -0
  24. package/dist/skills/ads-import/references/guarded-editing.md +12 -0
  25. package/dist/skills/ads-import/references/scenarios.md +32 -0
  26. package/dist/skills/ads-migrate/SKILL.md +3 -0
  27. package/dist/skills/ads-migrate/references/evidence.md +73 -0
  28. package/dist/skills/ads-migrate/references/guarded-editing.md +12 -0
  29. package/dist/skills/ads-migrate/references/scenarios.md +32 -0
  30. package/dist/skills/ads-motion/SKILL.md +3 -0
  31. package/dist/skills/ads-motion/references/evidence.md +73 -0
  32. package/dist/skills/ads-motion/references/guarded-editing.md +12 -0
  33. package/dist/skills/ads-motion/references/scenarios.md +32 -0
  34. package/dist/verification/browser.mjs +27 -8
  35. package/dist/verification/playwright-reporter.cjs +39 -1
  36. package/dist/verification.mjs +32 -9
  37. package/docs/ARCHITECTURE.md +29 -3
  38. package/docs/BEHAVIORS.md +35 -2
  39. package/docs/CHANGELOG.md +21 -0
  40. package/docs/MCP.md +13 -0
  41. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ # Investigate the failing boundary
2
+
3
+ Use this reference after a rejected patch, failed check or preview loading error.
4
+ MCP serves it as `ads://guides/evidence`. Inspect the compact result first; fetch
5
+ only the failing check's bundle or relevant artifact, rather than repeating a suite.
6
+
7
+ ## Read the result correctly
8
+
9
+ `failure.category` identifies a boundary, not an inferred root cause:
10
+
11
+ | Category | Established observation | Next inspection |
12
+ |----------|-------------------------|-----------------|
13
+ | `architecture` | A source/ownership/configuration gate rejected the candidate. | Exact rule, source location and `certainty`. An inconclusive ownership proof does not establish that a valid API is wrong. |
14
+ | `build` | A registered project pipeline step or compiled-resource integrity check failed. | Named command/phase, exit code, bounded output and build revision. A custom project script may contain several kinds of checks. |
15
+ | `transport` | An MCP/browser resource delivery boundary failed. | Resource URI, relevant failed startup requests and observed browser errors. Do not infer a network-interface or component cause without evidence. |
16
+ | `runner` | The harness, process, readiness or reporting boundary failed, or a test threw without assertion evidence. | Active/pending checks, timeout, failing step and artifacts. `certainty: "unclassified"` means ADS cannot establish the cause. |
17
+ | `assertion` | A reported assertion step or token binding check failed. | Expected behavior, source location, state and trace. This can still be a test defect; compare the intended contract before editing the component. |
18
+
19
+ A complete failed run can contain independent passing cases. Suite interruption,
20
+ missing/skipped cases, stale inputs and an unexplained process exit cannot certify
21
+ a pass. Keep the reported case outcome separate from whole-run acceptance.
22
+
23
+ ## Get a compact investigation
24
+
25
+ Discover exact owner/check IDs with `ads_behaviors`, then:
26
+
27
+ ```json
28
+ { "unit": "components/Example", "check": "keyboard-navigation" }
29
+ ```
30
+
31
+ Send this to `ads_investigate`. It reads the latest bundle without running code.
32
+ `ads://project/investigations` lists available entries; the returned `resourceUri`
33
+ reads the same bundle. It records:
34
+
35
+ - Project versus isolated candidate, configured framework/profile and scenario.
36
+ - Exact input revision and the preview revision actually observed by the helper.
37
+ - Failing step/source, observed startup viewport and relevant failed requests.
38
+ - Available registered trace, screenshot or context artifact resource URIs.
39
+ - Explicit `acceptance.established` and `acceptance.unverified` scope.
40
+
41
+ Absent observations are `null` or empty; ADS does not invent a viewport, trace or
42
+ source line. Viewport evidence is labeled `preview-startup`; a test that resizes
43
+ later must collect its own failure-state evidence. Failed-request collection
44
+ covers managed startup, not a complete network trace. Bodies and headers are not
45
+ collected. Project-configured Playwright trace/screenshot policy controls which
46
+ artifacts exist; ADS does not turn on expensive recording or rerun a failed case.
47
+ Protocol runners may provide structured failure/step/location/artifact fields;
48
+ older reports remain valid and their unclassified errors stay explicit.
49
+
50
+ Only the latest bundle per registered check is exposed, including dry runs.
51
+ Another verification replaces it and removes old artifacts from resource access;
52
+ old runner output directories follow project cleanup policy. Artifact resources
53
+ are restricted to registered ordinary files under that run's output directory,
54
+ bounded to 20 MiB each and checked against their recorded digest on read.
55
+ Metadata is bounded to 2 MiB. Removed registrations are pruned on verification.
56
+
57
+ Candidate evidence never becomes passing live-source evidence. Compare the
58
+ recorded input revision with current `ads_behaviors` evidence; a bundle describes
59
+ its run, and does not claim current source merely because it remains available.
60
+ For a build/architecture rejection before behavior runs, inspect the mutation
61
+ result's `investigation` (also retained in `ads_status.mutations.lastResult`).
62
+
63
+ ## Preserve the design while repairing the failure
64
+
65
+ Fix the owner of the demonstrated defect. Do not privatize a valid API, introduce
66
+ demo-only props, substitute a styled link, change the reference baseline or weaken
67
+ verification policy to pass a tooling limitation. Preserve the draft and report
68
+ the smallest source/browser reproduction when ownership remains unprovable.
69
+
70
+ `acceptance` states what that result established. Core checks establish source
71
+ guard acceptance; registered tests establish only their actual cases/profiles.
72
+ Neither establishes untested states, visual parity, an accessibility audit or
73
+ another MCP host's compatibility. Captures show an appearance, not a transition.
@@ -51,6 +51,18 @@ blanket sleeps. Fake clocks suit timer-only guarantees; keep real time for actua
51
51
  gesture/motion/layout guarantees. Project settings belong in guarded configuration
52
52
  changes; ADS does not silently raise budgets or override the worker count.
53
53
 
54
+ If a check cannot load its MCP preview, preserve the draft and inspect the exact
55
+ resource URI and failed browser requests before changing component code. The ADS
56
+ showcase retries a fetch transport failure on `resources/read` at most twice
57
+ (200/800 ms backoff); recovered startup reads remain visible in
58
+ `startupDiagnostics`. It never retries tool calls, assertions or complete checks,
59
+ and persistent failures still block application. HTTP/protocol/content errors,
60
+ integrity failures and cancellation do not retry. External hosts own their
61
+ transport behavior. Chrome's `ERR_NETWORK_CHANGED` does not prove an ADS restart
62
+ or identify Docker/VPN churn: correlate failure timestamps with live interface
63
+ events, Chrome network logs and server availability. Do not weaken checks or
64
+ rewrite a valid API to hide infrastructure failures.
65
+
54
66
  Named barrel imports can narrow affected checks when the project accurately
55
67
  declares package `sideEffects`; retain CSS/global effects in that policy. Missing
56
68
  or unsupported policies and dynamic/wildcard dependencies stay conservative.
@@ -101,6 +101,38 @@ do not export the helper solely for a test. Public/private ownership is a produc
101
101
  decision, not a coverage workaround. Browser checks verify transitions but do not
102
102
  replace the current static `covers` requirement; `exercises` does not waive it.
103
103
 
104
+ ## Native semantics through runtime-owned selectors
105
+
106
+ An anchor or other native semantic element can also occupy a position styled by
107
+ public runtime UI, without cloning or an `asChild` API:
108
+
109
+ ```tsx
110
+ // Runtime owner: the class is attached to Line's actual rendered span.
111
+ const line = css`:is(a, button) > & { color: var(--text-body); }`;
112
+ export function Line(props) { return <span className={line} {...props} />; }
113
+
114
+ // Scenario: semantics belong to the consumer, appearance to the runtime owner.
115
+ <a href={fixture.destination}><Line>{fixture.label}</Line></a>
116
+ ```
117
+
118
+ ADS traces the actual returned DOM, the attached Linaria template and matching
119
+ selector relationships. A public parent's `& > a` is evidence too. A tag-agnostic
120
+ parent selector such as `& > :has(> [data-part="line"])` must match a marker
121
+ declared by the actual runtime-owned child; a scenario-authored lookalike does not
122
+ qualify. No component names or marker spellings are reserved.
123
+
124
+ Unused styles, unrelated CSS, broad `& > *` selectors, negative selectors alone,
125
+ fabricated markers, unknown render alternatives and opaque class replacement
126
+ cannot grant permission. A selector cannot establish a scenario-built custom
127
+ widget's keyboard behavior. Inline styles and arbitrary classes remain rejected;
128
+ existing source-verified public class helpers keep their own contract.
129
+
130
+ Do not add a public link part or change the DOM to satisfy a cloning-only model.
131
+ Preserve semantic attributes and the intended public API. Unsupported source
132
+ grammar produces an inconclusive ownership diagnostic, not proof of a bad API.
133
+ Browser verification must establish the real focus treatment, hover/state
134
+ precedence, navigation and other promised behavior in the compiled App iframe.
135
+
104
136
  ## Native semantics through a public runtime slot
105
137
 
106
138
  A native element can be the direct child of a public runtime component that
@@ -16,26 +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(() => ({}));
30
- if (state.id === scenario && state.state === "ready") return frame;
31
- if (state.id === scenario && state.state === "error") throw new Error(state.message || "The MCP App failed before verification.");
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(() => ({}));
42
+ if (state.id === scenario && state.state === "ready") {
43
+ const retries = await page.evaluate(() => Number(document.documentElement.dataset.adsResourceRetries ?? 0)).catch(() => 0);
44
+ if (retries > 0) console.warn(JSON.stringify({ adsPreviewDiagnostic: { kind: "resource-read-recovery", scenario, retries, failedRequests: failures } }));
45
+ diagnostic("ready",{previewRevision:state.revision??null});
46
+ return frame;
47
+ }
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.");}
32
49
  }
33
- const error = await page.evaluate(() => document.querySelector('#status[data-error="true"]')?.textContent).catch(() => null);
34
- 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);}
35
52
  await page.waitForTimeout(50);
36
53
  }
37
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.");
38
55
  } catch (error) {
39
- throw new Error(`Scenario ${scenario}: ${error.message}${failures.length ? `\nFailed browser requests:\n${failures.join("\n")}` : ""}`, { cause: error });
40
- } 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); }
41
60
  }
@@ -8,6 +8,9 @@ module.exports = class AdsBehaviorReporter {
8
8
  this.cases = [];
9
9
  this.errors = [];
10
10
  this.active = new Map();
11
+ this.diagnostics = new Map();
12
+ this.diagnosticBuffers = new Map();
13
+ this.steps = new Map();
11
14
  this.startedAt = Date.now();
12
15
  this.totalTests = 0;
13
16
  }
@@ -20,11 +23,46 @@ module.exports = class AdsBehaviorReporter {
20
23
  this.active.set(test.id, this.entries(test).map(entry => ({ ...entry, startedAt: (result.startTime ?? new Date()).toISOString(), workerIndex: result.workerIndex, retry: result.retry })));
21
24
  this.publish();
22
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
+ }
23
35
  onTestEnd(test, result) {
24
36
  this.active.delete(test.id);
25
- 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(), message: result.errors?.map(error => error.message || error.stack || error.value || "The browser runner reported an error without details.").join("\n").slice(0, 1000) });
37
+ const diagnostics = this.diagnostics.get(test.id);
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) });
42
+ this.diagnostics.delete(test.id);
43
+ this.diagnosticBuffers.delete(test.id);
44
+ this.steps.delete(test.id);
26
45
  this.publish();
27
46
  }
47
+ onStdErr(chunk, test) {
48
+ if (!test) return;
49
+ const lines = ((this.diagnosticBuffers.get(test.id) ?? "") + String(chunk)).split("\n");
50
+ this.diagnosticBuffers.set(test.id, lines.pop().slice(-8192));
51
+ for (const line of lines) {
52
+ if (line.length > 8192) continue;
53
+ try {
54
+ const diagnostic = JSON.parse(line).adsPreviewDiagnostic;
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;
60
+ const collected = this.diagnostics.get(test.id) ?? [];
61
+ if (collected.length < 5) collected.push(entry);
62
+ this.diagnostics.set(test.id, collected);
63
+ } catch { /* Ordinary test output is not an ADS startup diagnostic. */ }
64
+ }
65
+ }
28
66
  onError(error) {
29
67
  if (this.errors.length < 5) this.errors.push(String(error.message || error.stack || error.value || "The browser runner reported a suite error without details.").slice(0, 4000));
30
68
  this.publish();
@@ -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,15 +318,27 @@ 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) } : {}),
334
+ ...(matches.some(item => item.diagnostics?.length) ? { startupDiagnostics: matches.flatMap(item => Array.isArray(item.diagnostics) ? item.diagnostics.slice(0, 5) : []).slice(0, 10) } : {}),
313
335
  ...(runnerFailure ? { runnerExitCode: run.code } : {}),
314
336
  execution: progress.cases.find(item => item.unit === check.unit && item.check === check.id)?.state,
315
337
  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)) });
316
338
  }
317
339
  if (signal.aborted) break;
318
340
  }
341
+ saveInvestigations(reportRoot,results.map(result=>result.investigation),inventory.contracts.flatMap(contract=>Object.keys(contract.checks).map(id=>identity(contract.unit,id))));
319
342
  if (persist) {
320
343
  const evidence = saved(root);
321
344
  for (const result of results) evidence[identity(result.unit, result.check)] = result;
@@ -324,8 +347,8 @@ export function createVerificationController(root, { reportRoot = root, onProgre
324
347
  if (Buffer.byteLength(content) > 2 * 1024 * 1024) throw new Error("Verification evidence exceeds 2 MiB. Remove obsolete generated evidence and retry.");
325
348
  try { writeFileSync(temporary, content); renameSync(temporary, file); } finally { rmSync(temporary, { force: true }); }
326
349
  }
327
- return { ok: diagnostics.length === 0 && results.length > 0 && results.every(result => result.status === "passed" && result.current), diagnostics, results };
328
- } 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); }
329
352
  finally { try { await host?.close(); } finally { if (active === controller) { active = undefined; progress = undefined; } finish(); } }
330
353
  },
331
354
  };
@@ -229,6 +229,15 @@ scenarios unmounts the previous view while keeping unchanged dependencies cached
229
229
  Persistence is a host lifecycle choice; a new external widget loads the same
230
230
  resources independently. Resource sharing is scoped to an App instance.
231
231
 
232
+ The showcase's resource bridge can recover a transient fetch failure with at most
233
+ three read attempts and bounded backoff. This applies only to `resources/read`;
234
+ tool calls, assertions and whole checks are never replayed. Cancellation,
235
+ HTTP/protocol/content errors and integrity failures remain failures. Browser
236
+ diagnostics record retries, and verification retains recovered startup reads in
237
+ its case results. Persistent transport failures still block a mutation. External
238
+ hosts own their transport recovery; the reusable App's integrity checks remain
239
+ the same.
240
+
232
241
  Managed graph builders publish into a private session directory beside the
233
242
  configured manifest. Logical scenario IDs and resource URIs stay unchanged;
234
243
  standalone builds retain the configured output and cannot overwrite a live
@@ -313,7 +322,12 @@ styles. This preserves ownership rather than granting scenario styling freedom.
313
322
  A native element may provide semantics as the single direct child of a public
314
323
  runtime slot when source tracing proves it is cloned with that owner's props.
315
324
  This preserves polymorphic composition without allowing standalone scenario
316
- 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.
317
331
  The skill-local [scenario guide](../skills/ads-designer/references/scenarios.md)
318
332
  explains evidence, limits and diagnostics.
319
333
 
@@ -418,8 +432,20 @@ ADS owns affected-check selection, candidate hosting, progress, evidence freshne
418
432
  and process cleanup. One profile timeout bounds its entire runner process. Parallel
419
433
  workers can share that candidate host with independent App state. Partial outcomes
420
434
  remain diagnostic when execution is interrupted; they cannot approve a mutation.
421
- Passing checks describe their registered assertions, not every natural-language claim. This keeps edits
422
- 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
423
449
  unrelated owners remain outside the selected unit's edit boundary. Shared foundation,
424
450
  branding and guides have their own units. Shared credits and text license notices
425
451
  belong to the documentation unit, including conventional root notices and shared
package/docs/BEHAVIORS.md CHANGED
@@ -169,8 +169,23 @@ pass, even if all reported cases claim success. Results retain `reportedStatus`
169
169
  and `runnerExitCode` and explain the overall failure. Suite-level browser errors and
170
170
  bounded runner output appear in diagnostics. `openBehaviorScenario` includes the
171
171
  scenario ID and failed browser requests in startup failures; App resource errors
172
- identify the resource URI. Failed assertions and startup errors are not retried
173
- automatically. Fix or investigate the failure before explicitly verifying again.
172
+ identify the resource URI. The ADS showcase retries only `resources/read` fetch
173
+ transport failures, with at most three attempts and 200/800 ms backoff. Recovered
174
+ startup reads appear in `startupDiagnostics` with retry counts and failed browser
175
+ requests; each retry is also logged in the browser console. This does not retry a
176
+ tool call, assertion, check or complete startup. HTTP/protocol/content errors,
177
+ integrity failures and cancellations do not retry. Persistent loading failures
178
+ still fail the check and block mutations. External hosts control their own
179
+ transport recovery.
180
+
181
+ `net::ERR_NETWORK_CHANGED` reports Chrome's network-change error, not proof that
182
+ the MCP endpoint restarted or that a particular interface changed. To investigate
183
+ an intermittent failure, correlate the saved case timestamps with Chrome's
184
+ network log, server availability and live interface monitoring (for example,
185
+ `ip -ts monitor link address route` on Linux). Interfaces being present, or stable
186
+ after a failure, does not establish the cause. Preserve the draft and original
187
+ diagnostic; do not change a component API, reference example or verification
188
+ policy to compensate for a resource-loading failure.
174
189
 
175
190
  The reporter atomically publishes partial snapshots at test start/end. While a
176
191
  run is active, `ads_status.behaviors.progress` exposes the profile, runner budget,
@@ -257,6 +272,13 @@ publish `complete:true` at completion. Older final reports without `complete`
257
272
  remain supported after normal process completion. An explicitly incomplete report
258
273
  cannot pass even with exit code zero. Partial reports retain the same schema/run
259
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.
260
282
  `file` is project-relative. Nonzero process exit, missing cases, skipped cases,
261
283
  wrong run IDs and unavailable reports do not pass. Test commands are trusted
262
284
  project code; an adapter cannot prove the quality or honesty of their assertions.
@@ -285,6 +307,17 @@ changes mark results stale; changes during execution prevent a current pass.
285
307
  Guarantees report passing only when all their checks have current passing evidence.
286
308
  Passing applies to those cases and profiles, not every application state.
287
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
+
288
321
  Revisions include owned files, literal import dependencies, local CSS imports and
289
322
  asset URLs, token foundation, project/runner configuration, lockfiles and tooling.
290
323
  Computed module selection conservatively includes all ADS owners. Environment,
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
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
+
16
+ ## 0.3.16
17
+
18
+ - Recover transient showcase MCP resource fetch failures with bounded read-only
19
+ retries. Keep tool calls, assertions, checks and non-transport failures outside
20
+ recovery; expose recovered startup reads in verification diagnostics and keep
21
+ persistent loading failures blocking. Verify interrupted resource delivery and
22
+ failed delivery against a dummy App, without changing project verification policy.
23
+
3
24
  ## 0.3.15
4
25
 
5
26
  - Keep fragment links inside inline preview documents by supplying a local
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.15",
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": {