@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
package/README.md CHANGED
@@ -125,6 +125,9 @@ Private controllers stay beside their component; shared controllers have their o
125
125
  edit units. Optional behavior contracts connect guarantees to maintained checks.
126
126
  Demonstrations and passing execution evidence remain distinct. See
127
127
  [controllers and verification](docs/BEHAVIORS.md).
128
+ Structured failure boundaries and compact investigation bundles help agents
129
+ inspect the failing source, requests and available artifacts while preserving
130
+ the intended design. Each result states what it established and left unverified.
128
131
 
129
132
  Experimental MCP Apps show an interactive token inspector and project-built
130
133
  token, component, pattern and composition scenarios. They use the same public implementations and fixtures
package/dist/check.mjs CHANGED
@@ -22,6 +22,8 @@ import { scopedTokens, scopedTokenOwner } from "./scoped-tokens.mjs";
22
22
  import { createAtomicCompositionReview } from "./atomic-composition.mjs";
23
23
  import { createScenarioTrace } from "./scenario-trace.mjs";
24
24
  import { createScenarioSlots } from "./scenario-slots.mjs";
25
+ import { createScenarioSelectors } from "./scenario-selectors.mjs";
26
+ import { architectureEvidence, failure } from "./evidence.mjs";
25
27
 
26
28
  const scenarioLayoutGuidance = "Put demo-only sizing and framing in a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
27
29
  const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
@@ -38,7 +40,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
38
40
  for (const [field, file] of [["dsDir", dsRoot], ["publicEntry", path.join(dsRoot, config.publicEntry)], ["stylesEntry", path.join(dsRoot, config.stylesEntry)]]) {
39
41
  if (!existsSync(file) || (field === "dsDir" ? !statSync(file).isDirectory() : !statSync(file).isFile())) diagnostics.push({ rule: "project-structure", file: path.relative(root, file), line: 1, column: 1, message: `Missing or invalid ${field}. Bootstrap an empty ADS or configure its actual path.` });
40
42
  }
41
- if (diagnostics.length) return { ok: false, diagnostics };
43
+ if (diagnostics.length) return architectureEvidence({ ok: false, diagnostics });
42
44
  const tokenInventory = inspectTokens(root);
43
45
  const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
44
46
  diagnostics.push(...tokenInventory.diagnostics);
@@ -55,7 +57,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
55
57
  const checker = program.getTypeChecker();
56
58
  const atomicCompositionReview = createAtomicCompositionReview(program, options);
57
59
  const scenarioTrace = createScenarioTrace(program, options);
58
- let scenarioSlot;
60
+ let scenarioSlot, scenarioSelector;
59
61
  function report(file, rule, message, line = 1, column = 1) {
60
62
  diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
61
63
  }
@@ -162,17 +164,20 @@ export function checkProject(directory, { trackingRoot } = {}) {
162
164
  }
163
165
  const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
164
166
  const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
165
- function nodeReport(node, rule, message) {
167
+ function nodeReport(node, rule, message, detail) {
166
168
  const position = source.getLineAndCharacterOfPosition(node.getStart(source));
167
169
  report(file, rule, message, position.line + 1, position.character + 1);
170
+ if (detail) Object.assign(diagnostics.at(-1), detail);
168
171
  }
169
172
  function scenarioClass(node, expression, property = false) {
170
173
  const proof = scenarioTrace.classValue(expression, property);
171
174
  if (proof.present && !proof.ok) nodeReport(node, "showcase-class", `Scenarios define no custom class names. A public class helper must return runtime-owned Linaria styles; stable hooks authored in that owner may accompany real styles. Arbitrary strings, private styles and forwarded consumer classes do not qualify.${proof.reasons.length ? ` ${proof.reasons.join(" ")}` : ""} ${scenarioLayoutGuidance} See ads://guides/scenarios.`);
172
175
  }
173
- function scenarioControl(node) {
176
+ function scenarioControl(node, native) {
174
177
  const proof = (scenarioSlot ??= createScenarioSlots(program, options))(node);
175
- if (!proof.ok) nodeReport(node, "showcase-control", `Scenario controls must render public ADS components for actual interaction. A native direct child may provide semantics only when source tracing proves its public runtime parent clones that child with owned props; an asChild prop alone grants no permission.${proof.reasons.length ? ` ${proof.reasons.join(" ")}` : ""} ${scenarioLayoutGuidance} See ads://guides/scenarios.`);
178
+ if (proof.ok) return;
179
+ const selector = native ? (scenarioSelector ??= createScenarioSelectors(program, options))(node) : { ok: false, reasons: ["A selector cannot establish custom widget behavior or keyboard semantics."] };
180
+ if (!selector.ok) nodeReport(node, "showcase-control", `Scenario controls must compose public ADS runtime UI. Native semantics require source evidence of owned cloning props or an attached runtime Linaria selector addressing that native tag or a verified runtime-owned part in that position. A prop name, broad selector or scenario-authored marker grants no permission. ${selector.reasons.join(" ")} Preserve valid APIs when tracing is inconclusive; report the limitation instead of changing the API to satisfy coverage. ${scenarioLayoutGuidance} See ads://guides/scenarios.`, { failure: failure("architecture", "source-ownership", "ownership-not-proven", { certainty: "inconclusive" }) });
176
181
  }
177
182
  if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport);
178
183
  function isLinariaTag(tag) {
@@ -231,7 +236,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
231
236
  if (showcase) for (const property of properties) if (ts.isSpreadAssignment(property)) scenarioClass(property, property.expression, true);
232
237
  if (showcase && tag && ts.isStringLiteralLike(tag)) {
233
238
  const role = properties.find(property => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) && property.name.text === "role")?.initializer;
234
- if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) scenarioControl(node);
239
+ if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) scenarioControl(node, showcaseControls.has(tag.text));
235
240
  }
236
241
  }
237
242
  if (isInside(dsRoot, file) && (ts.isIdentifier(node) && ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"].includes(node.text) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node))) {
@@ -242,7 +247,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
242
247
  if (showcase && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)) {
243
248
  const role = node.attributes.properties.find((attr) => ts.isJsxAttribute(attr) && attr.name.text === "role")?.initializer;
244
249
  const roleValue = role && ts.isStringLiteralLike(role) ? role.text : role && ts.isJsxExpression(role) && role.expression && ts.isStringLiteralLike(role.expression) ? role.expression.text : null;
245
- if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) scenarioControl(node);
250
+ if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) scenarioControl(node, showcaseControls.has(node.tagName.text));
246
251
  }
247
252
  if (showcase && ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className") scenarioClass(node, node.initializer && ts.isJsxExpression(node.initializer) ? node.initializer.expression : node.initializer);
248
253
  if (showcase && ts.isJsxSpreadAttribute(node)) scenarioClass(node, node.expression, true);
@@ -279,5 +284,5 @@ export function checkProject(directory, { trackingRoot } = {}) {
279
284
  }
280
285
  // Bounded clients should see actionable warnings before source-analysis limits.
281
286
  advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
282
- return { ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) };
287
+ return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
283
288
  }
@@ -0,0 +1,18 @@
1
+ /** Categories describe the failing boundary, never an inferred root cause. */
2
+ export const failureCategories = new Set(["architecture", "build", "transport", "runner", "assertion"]);
3
+
4
+ export function failure(category, phase, code, detail = {}) {
5
+ if (!failureCategories.has(category)) throw new Error("Unknown ADS evidence category.");
6
+ return { category, phase, code, ...detail };
7
+ }
8
+
9
+ export function acceptance(scope, passed, established, unverified) {
10
+ return { scope, status: passed ? "passed" : "not_established", established, unverified };
11
+ }
12
+
13
+ export function architectureEvidence(result) {
14
+ return { ...result,
15
+ diagnostics: result.diagnostics.map(diagnostic => ({ ...diagnostic, failure: diagnostic.failure ?? failure("architecture", "source-analysis", diagnostic.rule) })),
16
+ acceptance: acceptance("core-architecture", result.ok, result.ok ? ["core-source-guards"] : [], ["project-checks", "compiled-preview", "browser-behavior", "visual-parity", "accessibility-audit"]),
17
+ };
18
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+
5
+ export const INVESTIGATIONS_URI = "ads://project/investigations";
6
+ const indexFile = root => path.join(root,".ds/investigations.json");
7
+ const identity = entry => `${entry.unit}#${entry.check}`;
8
+ export const investigationUri = entry => `ads://investigations/${encodeURIComponent(entry.unit)}/${encodeURIComponent(entry.check)}`;
9
+ const ordinary = file => { try { return realpathSync(file)===path.resolve(file)&&lstatSync(file).isFile()&&lstatSync(file).nlink===1; } catch { return false; } };
10
+ const digest = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
11
+ function saved(root) {
12
+ const file=indexFile(root);
13
+ if(!ordinary(file)||lstatSync(file).size>2*1024*1024)return {};
14
+ try {const value=JSON.parse(readFileSync(file,"utf8"));return value.schemaVersion===1&&value.bundles&&typeof value.bundles==="object"&&!Array.isArray(value.bundles)?value.bundles:{};}catch{return {};}
15
+ }
16
+ function artifactPath(root, relative) {
17
+ if(typeof relative!=="string"||!/^\.ds\/verification\/[a-f0-9-]{36}\/artifacts\//.test(relative)||relative.includes("\\")||relative.split("/").some(part=>[".","..",""].includes(part)))return null;
18
+ const file=path.resolve(root,relative);
19
+ return ordinary(file)&&lstatSync(file).size<=20*1024*1024?file:null;
20
+ }
21
+
22
+ /** One latest bundle per registered check, including isolated candidate runs. */
23
+ export function saveInvestigations(root, entries, registered) {
24
+ const valid=new Set(registered),bundles=Object.fromEntries(Object.entries(saved(root)).filter(([key])=>valid.has(key)));
25
+ for(const entry of entries)bundles[identity(entry)]={...entry,resourceUri:investigationUri(entry)};
26
+ const content=JSON.stringify({schemaVersion:1,bundles});
27
+ if(Buffer.byteLength(content)>2*1024*1024)throw new Error("Latest investigation metadata exceeds 2 MiB.");
28
+ const file=indexFile(root),temporary=`${file}.${randomUUID()}.tmp`;
29
+ mkdirSync(path.dirname(file),{recursive:true});
30
+ if(realpathSync(path.dirname(file))!==path.dirname(file))throw new Error("Investigation output cannot resolve through a symlink.");
31
+ try {writeFileSync(temporary,content);renameSync(temporary,file);}finally{rmSync(temporary,{force:true});}
32
+ }
33
+
34
+ export function investigationIndex(root) {
35
+ return {retention:"latest-per-check",entries:Object.values(saved(root)).map(({unit,check,status,completedAt,inputRevision,resourceUri,failure})=>({unit,check,status,completedAt,inputRevision,resourceUri,...(failure?{failure}:{})}))};
36
+ }
37
+
38
+ export function readInvestigation(root,{unit,check}) {
39
+ const bundle=saved(root)[identity({unit,check})];
40
+ if(!bundle)throw new Error("No investigation exists for this check. Run ads_verify or a guarded candidate first.");
41
+ return {...bundle,artifacts:(bundle.artifacts??[]).map(artifact=>({...artifact,available:!!artifactPath(root,artifact.path)}))};
42
+ }
43
+
44
+ export function investigationArtifacts(root,report,attachments=[]) {
45
+ return attachments.slice(0,5).flatMap(attachment=>{
46
+ if(typeof attachment?.path!=="string")return [];
47
+ const target=path.resolve(path.dirname(report),attachment.path);
48
+ if(!target.startsWith(path.join(path.dirname(report),"artifacts")+path.sep))return [];
49
+ const relative=path.relative(root,target).replaceAll("\\","/");
50
+ const file=artifactPath(root,relative);
51
+ if(!file)return [];
52
+ const bytes=readFileSync(file);
53
+ return [{name:String(attachment.name).slice(0,100),mimeType:String(attachment.mimeType).slice(0,100),path:relative,bytes:bytes.length,digest:digest(bytes),resourceUri:`ads://verification-artifacts/${relative.split("/").map(encodeURIComponent).join("/")}`}];
54
+ });
55
+ }
56
+
57
+ /** Only files explicitly registered by the latest bundles are resource-readable. */
58
+ export function readInvestigationResource(root,uri) {
59
+ if(uri.startsWith("ads://investigations/")) {
60
+ const parts=uri.slice("ads://investigations/".length).split("/").map(decodeURIComponent);
61
+ if(parts.length!==2)throw new Error("Invalid investigation URI.");
62
+ return {uri,mimeType:"application/json",text:JSON.stringify(readInvestigation(root,{unit:parts[0],check:parts[1]}))};
63
+ }
64
+ const artifact=Object.values(saved(root)).flatMap(bundle=>bundle.artifacts??[]).find(artifact=>artifact.resourceUri===uri);
65
+ const file=artifact&&artifactPath(root,artifact.path);
66
+ if(!file||!existsSync(file))throw new Error("Investigation artifact is unavailable or has been superseded.");
67
+ const bytes=readFileSync(file);
68
+ if(bytes.length!==artifact.bytes||digest(bytes)!==artifact.digest)throw new Error("Investigation artifact changed after verification.");
69
+ return ["text/plain","text/markdown","application/json"].includes(artifact.mimeType)?{uri,mimeType:artifact.mimeType,text:bytes.toString("utf8")}:{uri,mimeType:artifact.mimeType,blob:bytes.toString("base64")};
70
+ }
@@ -5,7 +5,7 @@ import { projectGuideFiles } from "../project-documents.mjs";
5
5
 
6
6
  export const GUIDE_URI = "ads://project/guide";
7
7
  // Put the task triggers and entry path first: some hosts shorten server guidance.
8
- export const serverInstructions = "Use ADS for UI work: components, patterns, controllers, hooks, tokens, themes, branding, layout, interaction, accessibility, motion and showcase review. Start with ads_status; if tools are not exposed, read ads://project/guide then ads://project/status. Use the returned projectRoot and commands. When ads_status.mutations.enabled, use ads_units and guarded ads_apply_patch for one owner; use ads_apply_transaction only for coupled owner patches. Find existing public UI with ads_catalog; discover its previews with ads_scenarios and open ads_show_scenario. Starting ADS also serves its built-in showcase: use ads_status.showcaseUrl for browser review instead of starting another runtime/compiler or project browser app. Read one relevant ADS skill before editing. Inspect the real preview, edit its owned source, check the build and review again. Scope source/guides to the project, its instruction ancestry and user-selected import/reference sources; broad searches of /mnt or home are not ADS discovery. Domain-model/requirements work is a separate project workflow. For requested visual evidence, use ads_capture or screenshot-enabled patch dry runs; read ads://guides/captures for named targets, image resources and the current/candidate lifecycle. For behavior work, read ads://guides/behaviors and use ads_behaviors/ads_verify: scenario exercises are demonstrations, and registered guarantees need current passing execution evidence. Use the configured project framework and declared dependencies; MCP hosts supply no ad hoc APIs. MCP source checks do not replace browser tests; direct edits still need project checks. Reading HTML is not rendering, and reading a skill is not native activation. A running watcher is not proof of current source: inspect ads_status.build or ads://project/build after edits. Notifications may not reach the agent. Only explicitly enabled mutation tools edit project-owned ADS units after validation. Checks and compiler commands come from validated project configuration, never an ad hoc command argument.";
8
+ export const serverInstructions = "Use ADS for UI work: components, patterns, controllers, hooks, tokens, themes, branding, layout, interaction, accessibility, motion and showcase review. Start with ads_status; if tools are not exposed, read ads://project/guide then ads://project/status. Use the returned projectRoot and commands. When ads_status.mutations.enabled, use ads_units and guarded ads_apply_patch for one owner; use ads_apply_transaction only for coupled owner patches. Find existing public UI with ads_catalog; discover its previews with ads_scenarios and open ads_show_scenario. Starting ADS also serves its built-in showcase: use ads_status.showcaseUrl for browser review instead of starting another runtime/compiler or project browser app. Read one relevant ADS skill before editing. Inspect the real preview, edit its owned source, check the build and review again. Scope source/guides to the project, its instruction ancestry and user-selected import/reference sources; broad searches of /mnt or home are not ADS discovery. Domain-model/requirements work is a separate project workflow. For requested visual evidence, use ads_capture or screenshot-enabled patch dry runs; read ads://guides/captures for named targets, image resources and the current/candidate lifecycle. For behavior work, read ads://guides/behaviors and use ads_behaviors/ads_verify: scenario exercises are demonstrations, and registered guarantees need current passing execution evidence. For a failed check or rejected patch, inspect its structured failure boundary and acceptance scope; ads_investigate reads retained per-check evidence without rerunning tests (ads://guides/evidence). Preserve valid APIs when source tracing is inconclusive. Use the configured project framework and declared dependencies; MCP hosts supply no ad hoc APIs. MCP source checks do not replace browser tests; direct edits still need project checks. Reading HTML is not rendering, and reading a skill is not native activation. A running watcher is not proof of current source: inspect ads_status.build or ads://project/build after edits. Notifications may not reach the agent. Only explicitly enabled mutation tools edit project-owned ADS units after validation. Checks and compiler commands come from validated project configuration, never an ad hoc command argument.";
9
9
 
10
10
  function manager(root, pkg) {
11
11
  if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
@@ -15,6 +15,7 @@ import { createCaptureController, captureSelections } from "../capture.mjs";
15
15
  import { previewSourceRevision } from "../preview-inputs.mjs";
16
16
  import { inspectScenarios } from "../scenarios.mjs";
17
17
  import { checkUnitCandidate, checkUnitFiles, inspectUnits, validateUnitId } from "../units.mjs";
18
+ import { acceptance, failure as evidenceFailure } from "../evidence.mjs";
18
19
 
19
20
  export const mutationLimits = { units: 8, files: 32, patchBytes: 256 * 1024, patchFileBytes: 16 * 1024 * 1024, fileBytes: 16 * 1024 * 1024, readChars: 12000, snapshotFiles: 20000, snapshotBytes: 128 * 1024 * 1024 };
20
21
  const digest = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
@@ -22,8 +23,8 @@ const ignored = new Set([".git", ".ds", "node_modules", ".vite", "coverage", ".p
22
23
  const textFile = file => /\.(?:[cm]?[jt]sx?|css|html|jsonc?|md|txt|svg)$/.test(file);
23
24
  const setupDotfile = file => /^\.(?:oxlintrc|linariarc|babelrc)(?:\.json)?$/.test(file);
24
25
 
25
- function fail(rule, message, file) {
26
- return Object.assign(new Error(message), { diagnostic: { rule, message, ...(file ? { file } : {}) } });
26
+ function fail(rule, message, file, boundary = evidenceFailure("architecture","mutation-policy",rule)) {
27
+ return Object.assign(new Error(message), { diagnostic: { rule, message, failure:boundary, ...(file ? { file } : {}) } });
27
28
  }
28
29
 
29
30
  function address(options, file) {
@@ -108,12 +109,13 @@ function snapshot(options, destination) {
108
109
  return JSON.stringify(entries);
109
110
  }
110
111
 
111
- function checkCommand(command, args, cwd, signal, input) {
112
+ function checkCommand(command, args, cwd, signal, input, gate = {category:"architecture",phase:"patch-preparation"}) {
112
113
  return new Promise((resolve, reject) => {
113
- if (signal?.aborted) { reject(fail("mutation-cancelled", "Mutation cancelled before validation.")); return; }
114
+ if (signal?.aborted) { reject(fail("mutation-cancelled", "Mutation cancelled before validation.",undefined,evidenceFailure("runner","mutation-execution","ADS_CANCELLED"))); return; }
114
115
  const child = spawn(command, args, { cwd, shell: false, detached: process.platform !== "win32", windowsHide: true, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
115
116
  env: { ...process.env, PATH: [path.join(cwd, "node_modules/.bin"), path.dirname(process.execPath), process.env.PATH ?? ""].join(path.delimiter) } });
116
117
  let output = "", failure;
118
+ const startedAt=Date.now();
117
119
  if (input !== undefined) { child.stdin.on("error", () => {}); child.stdin.end(input); }
118
120
  function kill() {
119
121
  if (!child.pid) return;
@@ -130,13 +132,23 @@ function checkCommand(command, args, cwd, signal, input) {
130
132
  child.once("exit", kill); // Also clean up descendants of a finished launcher.
131
133
  child.once("close", code => {
132
134
  clearTimeout(timer); signal?.removeEventListener("abort", abort);
133
- if (failure) reject(failure);
134
- else if (code !== 0) reject(fail("mutation-check", `${path.basename(command)} ${args.join(" ")} failed (${code}).\n${output}`));
135
- else resolve({ command: path.basename(command), args, passed: true });
135
+ const detail={command:path.basename(command),args,elapsedMs:Date.now()-startedAt,exitCode:code};
136
+ if (failure || code !== 0) {
137
+ const error=failure??fail("mutation-check", `${path.basename(command)} ${args.join(" ")} failed (${code}).\n${output}`);
138
+ error.diagnostic={...(error.diagnostic??{rule:"mutation-check",message:error.message}),...detail,output:output.slice(-4000),failure:evidenceFailure(failure?"runner":gate.category,gate.phase,failure?.code??(failure?"ADS_COMMAND_INTERRUPTED":"ADS_COMMAND_EXIT"),{certainty:"observed-boundary"})};
139
+ reject(error);
140
+ } else resolve({ ...detail,phase:gate.phase, passed: true });
136
141
  });
137
142
  });
138
143
  }
139
144
 
145
+ function mutationEvidence(result, candidateRevision) {
146
+ const diagnostics=(result.diagnostics??[]).map(diagnostic=>({...diagnostic,failure:diagnostic.failure??evidenceFailure("architecture","mutation-policy",diagnostic.rule)}));
147
+ const failed=diagnostics[0];
148
+ const accepted=acceptance("candidate-validation",result.ok,(result.checks??[]).filter(check=>check.passed&&!check.skipped).map(check=>check.name??[check.command,...(check.args??[])].join(" ")),["unregistered-checks","unselected-behavior-checks","post-application-preview-freshness","visual-parity","accessibility-audit"]);
149
+ return {...result,...(result.diagnostics?{diagnostics}:{}),acceptance:accepted,investigation:{target:"candidate",candidateRevision:candidateRevision??null,...(failed?{failure:failed.failure,failingStep:failed.failure.phase,source:failed.file?{file:failed.file,line:failed.line,column:failed.column}:null,...(failed.command?{command:failed.command,args:failed.args,exitCode:failed.exitCode,output:failed.output}:{}),message:failed.message}:{}),acceptance:accepted}};
150
+ }
151
+
140
152
  function checks(options) {
141
153
  const context = projectContext(options);
142
154
  const pkg = JSON.parse(readFileSync(path.join(options.root, "package.json"), "utf8"));
@@ -175,11 +187,12 @@ export function createMutationController(directory, { captures = createCaptureCo
175
187
  startLine: source.content.slice(0, offset).split("\n").length };
176
188
  },
177
189
  async apply({ unit, patch, patchFile, patches, dryRun = false, screenshot = false, captures: selections }, signal, notifyProgress) {
178
- if (busy) return { ok: false, applied: false, diagnostics: [{ rule: "mutation-busy", message: "Another mutation is validating; retry after it finishes." }] };
190
+ if (busy) return mutationEvidence({ ok: false, applied: false, diagnostics: [{ rule: "mutation-busy", message: "Another mutation is validating; retry after it finishes.",failure:evidenceFailure("runner","mutation-execution","ADS_MUTATION_BUSY") }] });
179
191
  busy = true;
180
192
  active = new AbortController();
181
193
  signal = signal ? AbortSignal.any([signal, active.signal]) : active.signal;
182
- let temporary;
194
+ let temporary, candidateRevision;
195
+ let boundary=evidenceFailure("runner","mutation-execution","ADS_MUTATION_EXCEPTION",{certainty:"unclassified"});
183
196
  const verified = [];
184
197
  try {
185
198
  const options = loadConfig(root);
@@ -264,8 +277,9 @@ export function createMutationController(directory, { captures = createCaptureCo
264
277
  const candidateOptions = loadConfig(candidate);
265
278
  const extraChecks = checks(candidateOptions);
266
279
  const expected = snapshot(candidateOptions);
280
+ candidateRevision=digest(Buffer.from(expected));
267
281
  const result = checkProject(candidate, { trackingRoot: root });
268
- if (!result.ok) return lastResult = { ok: false, applied: false, diagnostics: result.diagnostics.slice(0, 50), total: result.diagnostics.length, checks: verified };
282
+ if (!result.ok) return lastResult = mutationEvidence({ ok: false, applied: false, diagnostics: result.diagnostics.slice(0, 50), total: result.diagnostics.length, checks: verified },candidateRevision);
269
283
  verified.push({ name: "ads check", passed: true });
270
284
  const lintConfig = generateLintConfig(candidate).file;
271
285
  const require = createRequire(path.join(candidate, "package.json"));
@@ -276,30 +290,33 @@ export function createMutationController(directory, { captures = createCaptureCo
276
290
  if (configuration) for (const file of inspectUnits(candidateOptions).units.find(item => item.id === "configuration").files) {
277
291
  if (/\.[cm]?[jt]sx?$/.test(file) && !lintPaths.some(directory => isInside(path.resolve(candidate, directory), path.resolve(candidate, file)))) lintPaths.push(file);
278
292
  }
279
- await checkCommand(process.execPath, [executable, "--deny-warnings", "--config", lintConfig, ...lintPaths], candidate, signal);
293
+ await checkCommand(process.execPath, [executable, "--deny-warnings", "--config", lintConfig, ...lintPaths], candidate, signal, undefined, {category:"architecture",phase:"registered-oxlint"});
280
294
  verified.push({ name: "registered Oxlint rules", passed: true });
281
295
  if (assets || configuration) {
296
+ boundary=evidenceFailure("build","brand-assets","ADS_ASSET_BUILD");
282
297
  const brand = await generateBrandAssets(candidate);
283
298
  verified.push({ name: "brand assets", passed: true, skipped: !!brand.skipped });
284
299
  }
285
- for (const check of extraChecks) verified.push(await checkCommand(check.command, check.args, candidate, signal));
300
+ for (const check of extraChecks) verified.push(await checkCommand(check.command, check.args, candidate, signal, undefined, {category:"build",phase:`project-check:${check.args[1]}`}));
301
+ boundary=evidenceFailure("runner","behavior-verification","ADS_BEHAVIOR_SETUP");
286
302
  const behaviorUnits = affectedBehaviorUnits(candidateOptions, changes.map(change => change.path));
287
303
  if (behaviorUnits.length) {
288
304
  const verification = createVerificationController(candidate, { reportRoot: root, onProgress: value => { progress = { stage: "behavior", ...value }; notifyProgress?.(progress); } });
289
305
  let evidence;
290
306
  try { evidence = await verification.run({ units: behaviorUnits, persist: false }, signal); }
291
307
  finally { await verification.close(); }
292
- if (!evidence.ok) return lastResult = { ok: false, applied: false, diagnostics: [...evidence.diagnostics, ...evidence.results.filter(item => item.status !== "passed").map(item => ({ rule: "behavior-check", message: `${item.unit}#${item.check}: ${item.status}. ${item.message}` }))].slice(0, 50), checks: verified, behavior: evidence };
308
+ if (!evidence.ok) return lastResult = mutationEvidence({ ok: false, applied: false, diagnostics: [...evidence.results.filter(item => item.status !== "passed").map(item => ({ rule: "behavior-check", failure:item.failure, message: `${item.unit}#${item.check}: ${item.status}. ${item.message}` })),...evidence.diagnostics].slice(0, 50), checks: verified, behavior: evidence },candidateRevision);
293
309
  verified.push({ name: "registered behavior checks", passed: true, results: evidence.results });
294
310
  }
295
311
  let screenshots;
296
312
  if (screenshot) {
313
+ boundary=evidenceFailure("runner","capture","ADS_CAPTURE_FAILED");
297
314
  progress = { stage: "capture", total: selections.length, completed: 0, active: [], profile: "screenshots", elapsedMs: 0 };
298
315
  notifyProgress?.(progress);
299
316
  const proposal = digest(Buffer.from(JSON.stringify(changes.map(change => [change.path, change.content === null ? null : digest(Buffer.from(change.content))]))));
300
317
  screenshots = await captures.run({ captures: selections, candidateRoot: candidate, proposal, publish: false }, signal, value => { progress = value; notifyProgress?.(value); });
301
318
  }
302
- if (signal?.aborted) throw fail("mutation-cancelled", "Mutation cancelled before application.");
319
+ if (signal?.aborted) throw fail("mutation-cancelled", "Mutation cancelled before application.",undefined,evidenceFailure("runner","mutation-execution","ADS_CANCELLED"));
303
320
  if (snapshot(candidateOptions) !== expected) throw fail("mutation-check", "A validation command changed source files. Use checks without source-writing side effects.");
304
321
  if (snapshot(loadConfig(root)) !== before) throw fail("mutation-conflict", "Project inputs changed during validation; no changes were applied. Read current source and retry.");
305
322
  for (const change of changes) address(options, change.path);
@@ -336,11 +353,11 @@ export function createMutationController(directory, { captures = createCaptureCo
336
353
  const warnings = [];
337
354
  if (!dryRun && (assets || configuration)) { try { await generateBrandAssets(root); } catch (error) { warnings.push({ rule: "mutation-assets", message: `Source was applied; generated brand output needs recovery: ${error.message}` }); } }
338
355
  if (!dryRun && captures.store.fingerprint()) { try { captures.store.promote(previewSourceRevision(loadConfig(root)), inspectScenarios(loadConfig(root)).scenarios); } catch (error) { warnings.push({ rule: "mutation-capture", message: `Source was applied; capture promotion needs recovery: ${error.message}` }); } }
339
- return lastResult = { ok: true, ...(patches ? {units,transaction:true} : {unit}), applied: !dryRun, changes: changes.map(change => ({ path: change.path, operation: change.content === null ? "delete" : change.expectedDigest === null ? "create" : "update", digest: change.content === null ? null : digest(Buffer.from(change.content)) })), checks: verified, ...(result.advisories?.length ? { advisories: result.advisories.slice(0, 50), advisoryCount: result.advisories.length } : {}), ...(warnings.length ? { warnings } : {}),
356
+ return lastResult = mutationEvidence({ ok: true, ...(patches ? {units,transaction:true} : {unit}), applied: !dryRun, changes: changes.map(change => ({ path: change.path, operation: change.content === null ? "delete" : change.expectedDigest === null ? "create" : "update", digest: change.content === null ? null : digest(Buffer.from(change.content)) })), checks: verified, ...(result.advisories?.length ? { advisories: result.advisories.slice(0, 50), advisoryCount: result.advisories.length } : {}), ...(warnings.length ? { warnings } : {}),
340
357
  ...(screenshots ? { screenshots } : {}),
341
- preview: "Source validation passed. Inspect the managed preview build and rendered scenario after applying; validation is not visual verification." };
358
+ preview: "Source validation passed. Inspect the managed preview build and rendered scenario after applying; validation is not visual verification." },candidateRevision);
342
359
  } catch (error) {
343
- return lastResult = { ok: false, applied: false, diagnostics: [error.diagnostic ?? { rule: "mutation-error", message: error.message }], checks: verified };
360
+ return lastResult = mutationEvidence({ ok: false, applied: false, diagnostics: [error.diagnostic ?? { rule: "mutation-error", message: error.message,failure:error.failure??boundary }], checks: verified },candidateRevision);
344
361
  } finally { try { if (temporary) rmSync(temporary, { recursive: true, force: true }); } finally { busy = false; active = undefined; progress = undefined; } }
345
362
  },
346
363
  };