@reqvire-org/ads 0.3.16 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/check.mjs +13 -8
- package/dist/evidence.mjs +18 -0
- package/dist/investigations.mjs +70 -0
- package/dist/mcp/context.mjs +1 -1
- package/dist/mcp/mutations.mjs +34 -17
- package/dist/mcp/preview-widget.bundle.js +12 -12
- package/dist/mcp/resource-loader.js +3 -3
- package/dist/mcp/server.mjs +30 -8
- package/dist/scenario-selectors.mjs +140 -0
- package/dist/selector-match.mjs +94 -0
- package/dist/showcase/client.bundle.js +2 -2
- package/dist/skills/ads-break-ui/SKILL.md +3 -0
- package/dist/skills/ads-break-ui/references/evidence.md +73 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
- package/dist/skills/ads-designer/SKILL.md +3 -0
- package/dist/skills/ads-designer/references/evidence.md +73 -0
- package/dist/skills/ads-designer/references/scenarios.md +32 -0
- package/dist/skills/ads-import/SKILL.md +3 -0
- package/dist/skills/ads-import/references/evidence.md +73 -0
- package/dist/skills/ads-import/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/SKILL.md +3 -0
- package/dist/skills/ads-migrate/references/evidence.md +73 -0
- package/dist/skills/ads-migrate/references/scenarios.md +32 -0
- package/dist/skills/ads-motion/SKILL.md +3 -0
- package/dist/skills/ads-motion/references/evidence.md +73 -0
- package/dist/skills/ads-motion/references/scenarios.md +32 -0
- package/dist/verification/browser.mjs +22 -7
- package/dist/verification/playwright-reporter.cjs +23 -5
- package/dist/verification.mjs +31 -9
- package/docs/ARCHITECTURE.md +20 -3
- package/docs/BEHAVIORS.md +18 -0
- package/docs/CHANGELOG.md +13 -0
- package/docs/MCP.md +13 -0
- package/package.json +1 -1
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 (
|
|
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
|
+
}
|
package/dist/mcp/context.mjs
CHANGED
|
@@ -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];
|
package/dist/mcp/mutations.mjs
CHANGED
|
@@ -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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
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
|
};
|