@reqvire-org/ads 0.3.16 → 0.3.18
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/atomic-composition.mjs +1 -1
- package/dist/check.mjs +43 -11
- package/dist/evidence.mjs +18 -0
- package/dist/investigations.mjs +101 -0
- package/dist/linaria/preserve-values.cjs +21 -2
- package/dist/mcp/behavior-pages.mjs +36 -0
- package/dist/mcp/context.mjs +3 -1
- package/dist/mcp/execution-results.mjs +146 -0
- 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/result-pages.mjs +87 -0
- package/dist/mcp/server.mjs +67 -30
- package/dist/property-registrations.mjs +39 -0
- package/dist/scenario-selectors.mjs +140 -0
- package/dist/scenario-trace.mjs +1 -1
- package/dist/scoped-tokens.mjs +6 -0
- package/dist/selector-match.mjs +94 -0
- package/dist/showcase/client.bundle.js +2 -2
- package/dist/skills/ads-break-ui/SKILL.md +8 -3
- package/dist/skills/ads-break-ui/references/atomic-styles.md +48 -0
- package/dist/skills/ads-break-ui/references/captures.md +6 -1
- package/dist/skills/ads-break-ui/references/evidence.md +120 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +8 -5
- package/dist/skills/ads-break-ui/references/scenarios.md +32 -0
- package/dist/skills/ads-designer/SKILL.md +8 -3
- package/dist/skills/ads-designer/references/atomic-styles.md +48 -0
- package/dist/skills/ads-designer/references/captures.md +6 -1
- package/dist/skills/ads-designer/references/evidence.md +120 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +8 -5
- package/dist/skills/ads-designer/references/scenarios.md +32 -0
- package/dist/skills/ads-import/SKILL.md +8 -3
- package/dist/skills/ads-import/references/atomic-styles.md +48 -0
- package/dist/skills/ads-import/references/captures.md +6 -1
- package/dist/skills/ads-import/references/evidence.md +120 -0
- package/dist/skills/ads-import/references/guarded-editing.md +8 -5
- package/dist/skills/ads-import/references/scenarios.md +32 -0
- package/dist/skills/ads-migrate/SKILL.md +8 -3
- package/dist/skills/ads-migrate/references/atomic-styles.md +48 -0
- package/dist/skills/ads-migrate/references/captures.md +6 -1
- package/dist/skills/ads-migrate/references/evidence.md +120 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +8 -5
- package/dist/skills/ads-migrate/references/scenarios.md +32 -0
- package/dist/skills/ads-motion/SKILL.md +8 -3
- package/dist/skills/ads-motion/references/atomic-styles.md +48 -0
- package/dist/skills/ads-motion/references/captures.md +6 -1
- package/dist/skills/ads-motion/references/evidence.md +120 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +8 -5
- 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/ADS.md +10 -0
- package/docs/ARCHITECTURE.md +47 -3
- package/docs/BEHAVIORS.md +30 -0
- package/docs/CHANGELOG.md +30 -0
- package/docs/MCP.md +66 -7
- package/package.json +2 -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
|
|
@@ -168,7 +168,7 @@ export function createAtomicCompositionReview(program, options) {
|
|
|
168
168
|
if (parent.type === "atrule") conditions.unshift(`${parent.name} ${parent.params}`.trim().replace(/\s+/g, " "));
|
|
169
169
|
}
|
|
170
170
|
// Animation frames describe different instants, not combined classes.
|
|
171
|
-
if (conditions.some(condition => /^(?:-\w+-)?keyframes\b/.test(condition))) return;
|
|
171
|
+
if (conditions.some(condition => /^(?:(?:-\w+-)?keyframes|property)\b/.test(condition))) return;
|
|
172
172
|
result.push({ property: decl.prop.toLowerCase(), value: decl.value, important: !!decl.important, selectors: JSON.stringify(selectors), conditions: JSON.stringify(conditions),
|
|
173
173
|
file: path.relative(options.root, source.fileName).replaceAll("\\", "/"), line: position.line + decl.source.start.line, column: decl.source.start.column + (decl.source.start.line === 1 ? position.character : 0) });
|
|
174
174
|
});
|
package/dist/check.mjs
CHANGED
|
@@ -22,6 +22,9 @@ 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";
|
|
27
|
+
import { propertyRegistration } from "./property-registrations.mjs";
|
|
25
28
|
|
|
26
29
|
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
30
|
const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
|
|
@@ -38,7 +41,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
38
41
|
for (const [field, file] of [["dsDir", dsRoot], ["publicEntry", path.join(dsRoot, config.publicEntry)], ["stylesEntry", path.join(dsRoot, config.stylesEntry)]]) {
|
|
39
42
|
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
43
|
}
|
|
41
|
-
if (diagnostics.length) return { ok: false, diagnostics };
|
|
44
|
+
if (diagnostics.length) return architectureEvidence({ ok: false, diagnostics });
|
|
42
45
|
const tokenInventory = inspectTokens(root);
|
|
43
46
|
const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
|
|
44
47
|
diagnostics.push(...tokenInventory.diagnostics);
|
|
@@ -55,7 +58,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
55
58
|
const checker = program.getTypeChecker();
|
|
56
59
|
const atomicCompositionReview = createAtomicCompositionReview(program, options);
|
|
57
60
|
const scenarioTrace = createScenarioTrace(program, options);
|
|
58
|
-
let scenarioSlot;
|
|
61
|
+
let scenarioSlot, scenarioSelector;
|
|
59
62
|
function report(file, rule, message, line = 1, column = 1) {
|
|
60
63
|
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
|
|
61
64
|
}
|
|
@@ -68,6 +71,30 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
68
71
|
inspectControllers(options, program, report, behaviors.contracts);
|
|
69
72
|
// The foundation remains a fixed import-only entry, not an extra styling layer.
|
|
70
73
|
const foundationEntry = path.join(dsRoot, config.stylesEntry);
|
|
74
|
+
const registeredProperties=new Map();
|
|
75
|
+
function checkRegistrations(file,css,offset={line:1}){
|
|
76
|
+
css.walkAtRules(/^property$/i,rule=>{
|
|
77
|
+
const line=offset.line+(rule.source?.start.line??1)-1;
|
|
78
|
+
try{
|
|
79
|
+
const registration=propertyRegistration(rule),relative=scoped.inventory.relative(path.relative(root,file));
|
|
80
|
+
const owner=scoped.inventory.owner(relative),foundation=isInside(path.join(dsRoot,config.tokenDir),file);
|
|
81
|
+
if(foundation){
|
|
82
|
+
let defined=false;css.walkDecls(registration.name,()=>{defined=true;});
|
|
83
|
+
if(!isSharedToken(registration.name)||!declaredTokens.has(registration.name)||!defined||["base.css","fonts.css"].includes(path.basename(file)))throw new Error("Shared @property registrations belong beside their real token definitions in foundation value sheets, not base.css or fonts.css.");
|
|
84
|
+
}else{
|
|
85
|
+
if(!owner||!/^(?:components|patterns)\//.test(owner))throw new Error("@property belongs to its runtime component/pattern or real token value sheet, never application, scenario, controller or shared reset styling.");
|
|
86
|
+
const violation=checkComponentVariable(registration.name,file,options);if(violation)throw new Error(violation.message);
|
|
87
|
+
}
|
|
88
|
+
for(const descriptor of rule.nodes??[])if(descriptor.type==="decl")for(const ref of references(descriptor.value)){
|
|
89
|
+
const tokenOwner=scopedTokenOwner(options,ref.name);
|
|
90
|
+
if(tokenOwner&&(tokenOwner.unit!==owner||!declaredTokens.has(ref.name))||!tokenOwner&&!/^(?:--ds-|--ux-|--showcase-)/.test(ref.name)&&(!isSharedToken(ref.name)||!declaredTokens.has(ref.name)&&!ref.fallback))throw new Error(`Invalid or cross-owner token reference ${ref.name} in registration metadata.`);
|
|
91
|
+
}
|
|
92
|
+
const previous=registeredProperties.get(registration.name);
|
|
93
|
+
if(previous&&(previous.owner!==owner||previous.signature!==registration.signature))throw new Error(`Conflicting @property registration for ${registration.name}; ${path.relative(root,previous.file)} already registers it. Keep one owner and identical descriptors across that owner's modules.`);
|
|
94
|
+
registeredProperties.set(registration.name,{...registration,owner,file});
|
|
95
|
+
}catch(error){report(file,"property-registration",`${error.message} See ads://guides/atomic-styles.`,line,rule.source?.start.column??1);diagnostics.at(-1).guidanceUri="ads://guides/atomic-styles";}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
71
98
|
try {
|
|
72
99
|
const css = postcss.parse(readFileSync(foundationEntry, "utf8"), { from: foundationEntry });
|
|
73
100
|
const imports = css.nodes.filter((node) => node.type !== "comment");
|
|
@@ -79,6 +106,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
79
106
|
let css;
|
|
80
107
|
try { css = postcss.parse(text, { from: file }); }
|
|
81
108
|
catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
|
|
109
|
+
checkRegistrations(file,css,offset);
|
|
82
110
|
if (!foundation && (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry)) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
|
|
83
111
|
if (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", `Scenarios and fixtures define no styles. ${scenarioLayoutGuidance}`, offset.line);
|
|
84
112
|
css.walkAtRules("import", (rule) => {
|
|
@@ -106,12 +134,13 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
106
134
|
if (rule.name === "font-face" && path.basename(file) !== "fonts.css") report(file, "foundation-role", "Font faces belong in the canonical fonts.css foundation sheet.", rule.source.start.line);
|
|
107
135
|
});
|
|
108
136
|
css.walkDecls((decl) => {
|
|
137
|
+
const propertyDescriptor=decl.parent.type==="atrule"&&decl.parent.name.toLowerCase()==="property";
|
|
109
138
|
if (foundation && decl.prop.startsWith("--")) {
|
|
110
139
|
if (path.basename(file) === "base.css") report(file, "foundation-role", "Shared token definitions belong in value sheets, not base.css.", decl.source.start.line, decl.source.start.column);
|
|
111
140
|
return; // Definition names, values and aliases are validated by inspectTokens.
|
|
112
141
|
}
|
|
113
142
|
const fontDescriptor = foundation && path.basename(file) === "fonts.css" && decl.parent.type === "atrule" && decl.parent.name === "font-face";
|
|
114
|
-
if (foundation && !fontDescriptor && (path.basename(file) !== "base.css" || decl.parent.type !== "rule")) report(file, "foundation-role", "Token sheets contain shared definitions; native defaults belong in base.css, font descriptors in fonts.css, and UI styling in Linaria owners.", decl.source.start.line, decl.source.start.column);
|
|
143
|
+
if (foundation && !fontDescriptor && !propertyDescriptor && (path.basename(file) !== "base.css" || decl.parent.type !== "rule")) report(file, "foundation-role", "Token sheets contain shared definitions; native defaults belong in base.css, font descriptors in fonts.css, and UI styling in Linaria owners.", decl.source.start.line, decl.source.start.column);
|
|
115
144
|
if (foundation && path.basename(file) === "base.css" && decl.parent.type === "rule") {
|
|
116
145
|
const violation = foundationDeclaration(decl.parent, decl);
|
|
117
146
|
if (violation) report(file, "foundation-role", violation, decl.source.start.line, decl.source.start.column);
|
|
@@ -129,7 +158,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
129
158
|
if (!local && !isSharedToken(ref.name)) report(file, "token-name", `${ref.name} does not follow ADS's fixed token naming contract.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
130
159
|
else if (!local && !declaredTokens.has(ref.name) && !ref.fallback) report(file, "token-reference", `Undefined shared token ${ref.name}.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
|
|
131
160
|
}
|
|
132
|
-
if (fontDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
161
|
+
if (fontDescriptor || propertyDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
133
162
|
const violations = new Set();
|
|
134
163
|
valueParser(decl.value).walk((node) => {
|
|
135
164
|
if (node.type === "function" && node.value === "url") return false;
|
|
@@ -144,7 +173,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
144
173
|
});
|
|
145
174
|
}
|
|
146
175
|
for (const file of files) {
|
|
147
|
-
if (scopedFiles.has(file)) continue; // Closed definition-only contract checked by inspectTokens.
|
|
176
|
+
if (scopedFiles.has(file)) { try{checkRegistrations(file,postcss.parse(readFileSync(file,"utf8"),{from:file}));}catch{/* Token inspection reports malformed sheets. */} continue; } // Closed definition-only contract checked by inspectTokens.
|
|
148
177
|
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
|
|
149
178
|
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
150
179
|
if (file.endsWith(".css")) {
|
|
@@ -162,17 +191,20 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
162
191
|
}
|
|
163
192
|
const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
|
|
164
193
|
const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
|
|
165
|
-
function nodeReport(node, rule, message) {
|
|
194
|
+
function nodeReport(node, rule, message, detail) {
|
|
166
195
|
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
167
196
|
report(file, rule, message, position.line + 1, position.character + 1);
|
|
197
|
+
if (detail) Object.assign(diagnostics.at(-1), detail);
|
|
168
198
|
}
|
|
169
199
|
function scenarioClass(node, expression, property = false) {
|
|
170
200
|
const proof = scenarioTrace.classValue(expression, property);
|
|
171
201
|
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
202
|
}
|
|
173
|
-
function scenarioControl(node) {
|
|
203
|
+
function scenarioControl(node, native) {
|
|
174
204
|
const proof = (scenarioSlot ??= createScenarioSlots(program, options))(node);
|
|
175
|
-
if (
|
|
205
|
+
if (proof.ok) return;
|
|
206
|
+
const selector = native ? (scenarioSelector ??= createScenarioSelectors(program, options))(node) : { ok: false, reasons: ["A selector cannot establish custom widget behavior or keyboard semantics."] };
|
|
207
|
+
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
208
|
}
|
|
177
209
|
if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport);
|
|
178
210
|
function isLinariaTag(tag) {
|
|
@@ -231,7 +263,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
231
263
|
if (showcase) for (const property of properties) if (ts.isSpreadAssignment(property)) scenarioClass(property, property.expression, true);
|
|
232
264
|
if (showcase && tag && ts.isStringLiteralLike(tag)) {
|
|
233
265
|
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);
|
|
266
|
+
if (showcaseControls.has(tag.text) || role && ts.isStringLiteralLike(role) && widgetRoles.has(role.text)) scenarioControl(node, showcaseControls.has(tag.text));
|
|
235
267
|
}
|
|
236
268
|
}
|
|
237
269
|
if (isInside(dsRoot, file) && (ts.isIdentifier(node) && ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"].includes(node.text) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node))) {
|
|
@@ -242,7 +274,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
242
274
|
if (showcase && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)) {
|
|
243
275
|
const role = node.attributes.properties.find((attr) => ts.isJsxAttribute(attr) && attr.name.text === "role")?.initializer;
|
|
244
276
|
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);
|
|
277
|
+
if (showcaseControls.has(node.tagName.text) || widgetRoles.has(roleValue)) scenarioControl(node, showcaseControls.has(node.tagName.text));
|
|
246
278
|
}
|
|
247
279
|
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
280
|
if (showcase && ts.isJsxSpreadAttribute(node)) scenarioClass(node, node.expression, true);
|
|
@@ -279,5 +311,5 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
279
311
|
}
|
|
280
312
|
// Bounded clients should see actionable warnings before source-analysis limits.
|
|
281
313
|
advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
|
|
282
|
-
return { ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) };
|
|
314
|
+
return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
|
|
283
315
|
}
|
|
@@ -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,101 @@
|
|
|
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
|
+
import { compactAcceptance, compactRow, jsonBytes, resultBudgets, resultPage, resultRecord } from "./mcp/result-pages.mjs";
|
|
5
|
+
|
|
6
|
+
export const INVESTIGATIONS_URI = "ads://project/investigations";
|
|
7
|
+
const indexFile = root => path.join(root,".ds/investigations.json");
|
|
8
|
+
const identity = entry => `${entry.unit}#${entry.check}`;
|
|
9
|
+
export const investigationUri = entry => `ads://investigations/${encodeURIComponent(entry.unit)}/${encodeURIComponent(entry.check)}`;
|
|
10
|
+
const ordinary = file => { try { return realpathSync(file)===path.resolve(file)&&lstatSync(file).isFile()&&lstatSync(file).nlink===1; } catch { return false; } };
|
|
11
|
+
const digest = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
12
|
+
function saved(root) {
|
|
13
|
+
const file=indexFile(root);
|
|
14
|
+
if(!ordinary(file)||lstatSync(file).size>2*1024*1024)return {};
|
|
15
|
+
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 {};}
|
|
16
|
+
}
|
|
17
|
+
function artifactPath(root, relative) {
|
|
18
|
+
if(typeof relative!=="string"||!/^\.ds\/verification\/[a-f0-9-]{36}\/artifacts\//.test(relative)||relative.includes("\\")||relative.split("/").some(part=>[".","..",""].includes(part)))return null;
|
|
19
|
+
const file=path.resolve(root,relative);
|
|
20
|
+
return ordinary(file)&&lstatSync(file).size<=20*1024*1024?file:null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One latest bundle per registered check, including isolated candidate runs. */
|
|
24
|
+
export function saveInvestigations(root, entries, registered) {
|
|
25
|
+
const valid=new Set(registered),bundles=Object.fromEntries(Object.entries(saved(root)).filter(([key])=>valid.has(key)));
|
|
26
|
+
for(const entry of entries)bundles[identity(entry)]={...entry,resourceUri:investigationUri(entry)};
|
|
27
|
+
const content=JSON.stringify({schemaVersion:1,bundles});
|
|
28
|
+
if(Buffer.byteLength(content)>2*1024*1024)throw new Error("Latest investigation metadata exceeds 2 MiB.");
|
|
29
|
+
const file=indexFile(root),temporary=`${file}.${randomUUID()}.tmp`;
|
|
30
|
+
mkdirSync(path.dirname(file),{recursive:true});
|
|
31
|
+
if(realpathSync(path.dirname(file))!==path.dirname(file))throw new Error("Investigation output cannot resolve through a symlink.");
|
|
32
|
+
try {writeFileSync(temporary,content);renameSync(temporary,file);}finally{rmSync(temporary,{force:true});}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function investigationIndex(root) {
|
|
36
|
+
return {retention:"latest-per-check",entries:Object.values(saved(root)).map(({unit,check,status,runId,target,completedAt,inputRevision,resourceUri,failure})=>({unit,check,status,runId,target,completedAt,inputRevision,resourceUri,...(failure?{failure}:{})}))};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readInvestigation(root,{unit,check}) {
|
|
40
|
+
const bundle=saved(root)[identity({unit,check})];
|
|
41
|
+
if(!bundle)throw new Error("No investigation exists for this check. Run ads_verify or a guarded candidate first.");
|
|
42
|
+
return {...bundle,artifacts:(bundle.artifacts??[]).map(artifact=>({...artifact,available:!!artifactPath(root,artifact.path)}))};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const investigationSections=["summary","details","requests","browserErrors","artifacts"];
|
|
46
|
+
export function investigationPage(root,{unit,check,runId,section="summary",cursor,limit,recordIndex,offset,chars}={}) {
|
|
47
|
+
if(check===undefined){
|
|
48
|
+
if(runId!==undefined||section!=="summary"||recordIndex!==undefined||offset!==undefined||chars!==undefined)throw new Error("Select an exact unit/check before reading investigation details.");
|
|
49
|
+
const entries=investigationIndex(root).entries.filter(entry=>!unit||entry.unit===unit);
|
|
50
|
+
return resultPage(entries,{cursor,limit,identity:["investigations",unit,entries],key:"entries",header:{retention:"latest-per-check"}});
|
|
51
|
+
}
|
|
52
|
+
if(!unit||!investigationSections.includes(section))throw new Error("Select an exact unit/check and a known investigation section.");
|
|
53
|
+
const bundle=readInvestigation(root,{unit,check});
|
|
54
|
+
if(runId!==undefined&&bundle.runId!==runId)throw new Error("Investigation was superseded by a different run. Do not mix it with the earlier result.");
|
|
55
|
+
const details={source:bundle.source,failingStep:bundle.failingStep,viewport:bundle.viewport,viewportSource:bundle.viewportSource,failure:bundle.failure};
|
|
56
|
+
if(section!=="summary"){
|
|
57
|
+
const rows=(section==="details"?[details]:bundle[section]??[]).map((row,recordIndex)=>({recordIndex,...(row&&typeof row==="object"?row:{value:row})})),header={unit,check,runId:bundle.runId,section};
|
|
58
|
+
if(recordIndex!==undefined){if(cursor!==undefined||!runId)throw new Error("Read investigation record ranges with the exact runId and without a cursor.");return resultRecord(rows,{recordIndex,offset,chars,header});}
|
|
59
|
+
if(offset!==undefined||chars!==undefined)throw new Error("Select a recordIndex before reading text ranges.");
|
|
60
|
+
return resultPage(rows,{cursor,limit,identity:["investigation",unit,check,bundle.runId,bundle.inputRevision,section,rows],header});
|
|
61
|
+
}
|
|
62
|
+
if(recordIndex!==undefined||offset!==undefined||chars!==undefined)throw new Error("Select an investigation detail section before reading record ranges.");
|
|
63
|
+
if(cursor!==undefined)throw new Error("The investigation summary has no continuation; select a detail section.");
|
|
64
|
+
const result={unit,check,runId:bundle.runId,status:bundle.status,target:bundle.target,inputRevision:bundle.inputRevision,inputsCurrentAtCompletion:bundle.inputsCurrentAtCompletion,
|
|
65
|
+
framework:bundle.framework,profile:bundle.profile,mode:bundle.mode,scenario:bundle.scenario,observedPreviewRevision:bundle.observedPreviewRevision,
|
|
66
|
+
details:compactRow(details,1536),
|
|
67
|
+
acceptance:compactAcceptance(bundle.acceptance),counts:Object.fromEntries(investigationSections.slice(2).map(key=>[key,bundle[key]?.length??0])),resourceUri:bundle.resourceUri,byteLimit:resultBudgets.summary};
|
|
68
|
+
if(jsonBytes(result)>resultBudgets.summary){delete result.resourceUri;result.resourceUriOmitted=true;result.details=compactRow(result.details,512);}
|
|
69
|
+
if(jsonBytes(result)>resultBudgets.summary){result.acceptance={status:bundle.acceptance?.status,detailTruncated:true};delete result.scenario;delete result.observedPreviewRevision;result.detailTruncated=true;}
|
|
70
|
+
if(jsonBytes(result)>resultBudgets.summary)throw new Error("Investigation identity exceeds the summary budget.");
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function investigationArtifacts(root,report,attachments=[]) {
|
|
75
|
+
return attachments.slice(0,5).flatMap(attachment=>{
|
|
76
|
+
if(typeof attachment?.path!=="string")return [];
|
|
77
|
+
const target=path.resolve(path.dirname(report),attachment.path);
|
|
78
|
+
if(!target.startsWith(path.join(path.dirname(report),"artifacts")+path.sep))return [];
|
|
79
|
+
const relative=path.relative(root,target).replaceAll("\\","/");
|
|
80
|
+
const file=artifactPath(root,relative);
|
|
81
|
+
if(!file)return [];
|
|
82
|
+
const bytes=readFileSync(file);
|
|
83
|
+
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("/")}`}];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Only files explicitly registered by the latest bundles are resource-readable. */
|
|
88
|
+
export function readInvestigationResource(root,uri) {
|
|
89
|
+
if(uri.startsWith("ads://investigations/")) {
|
|
90
|
+
const url=new URL(uri),parts=url.pathname.slice(1).split("/").map(decodeURIComponent);
|
|
91
|
+
if(parts.length!==2)throw new Error("Invalid investigation URI.");
|
|
92
|
+
if([...url.searchParams.keys()].some(key=>!["section","runId","cursor","limit","recordIndex","offset","chars"].includes(key)))throw new Error("Unknown investigation resource query.");
|
|
93
|
+
return {uri,mimeType:"application/json",text:JSON.stringify(investigationPage(root,{unit:parts[0],check:parts[1],section:url.searchParams.get("section")??"summary",runId:url.searchParams.get("runId")??undefined,cursor:url.searchParams.get("cursor")??undefined,...Object.fromEntries(["limit","recordIndex","offset","chars"].filter(key=>url.searchParams.has(key)).map(key=>[key,Number(url.searchParams.get(key))]))}))};
|
|
94
|
+
}
|
|
95
|
+
const artifact=Object.values(saved(root)).flatMap(bundle=>bundle.artifacts??[]).find(artifact=>artifact.resourceUri===uri);
|
|
96
|
+
const file=artifact&&artifactPath(root,artifact.path);
|
|
97
|
+
if(!file||!existsSync(file))throw new Error("Investigation artifact is unavailable or has been superseded.");
|
|
98
|
+
const bytes=readFileSync(file);
|
|
99
|
+
if(bytes.length!==artifact.bytes||digest(bytes)!==artifact.digest)throw new Error("Investigation artifact changed after verification.");
|
|
100
|
+
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")};
|
|
101
|
+
}
|
|
@@ -8,6 +8,23 @@ module.exports = function preserveCustomProperties(Processor) {
|
|
|
8
8
|
return class PreservedCustomProperties extends Processor {
|
|
9
9
|
extractRules(valueCache, cssText, loc) {
|
|
10
10
|
const source = postcss.parse(cssText);
|
|
11
|
+
const registrations = [];
|
|
12
|
+
const layerOrder=[];
|
|
13
|
+
// Moving a registration must not change which named layer was first seen.
|
|
14
|
+
source.walkAtRules("layer",rule=>{
|
|
15
|
+
if(!rule.nodes||!rule.params.trim())return;
|
|
16
|
+
let statement=postcss.atRule({name:"layer",params:rule.params});
|
|
17
|
+
for(let parent=rule.parent;parent&&parent.type!=="root";parent=parent.parent){if(parent.type!=="atrule")return;const wrapper=parent.clone({nodes:[]});wrapper.raws.semicolon=true;wrapper.append(statement);statement=wrapper;}
|
|
18
|
+
layerOrder.push(statement.toString()+(statement.nodes?"":";"));
|
|
19
|
+
});
|
|
20
|
+
source.walkAtRules(/^property$/i, rule => {
|
|
21
|
+
let extracted=rule.clone();
|
|
22
|
+
for(let parent=rule.parent;parent&&parent.type!=="root";parent=parent.parent){
|
|
23
|
+
if(parent.type!=="atrule")throw new Error("@property is document-global. Put it at the Linaria template's top level or inside conditional at-rules, not inside an element selector.");
|
|
24
|
+
const wrapper=parent.clone({nodes:[]});wrapper.append(extracted);extracted=wrapper;
|
|
25
|
+
}
|
|
26
|
+
registrations.push(extracted.toString());rule.remove();
|
|
27
|
+
});
|
|
11
28
|
const values = new Map();
|
|
12
29
|
source.walkDecls(declaration => {
|
|
13
30
|
if (!declaration.prop.startsWith("--")) return;
|
|
@@ -17,9 +34,9 @@ module.exports = function preserveCustomProperties(Processor) {
|
|
|
17
34
|
declaration.value = marker;
|
|
18
35
|
delete declaration.raws.value;
|
|
19
36
|
});
|
|
20
|
-
if (!values.size) return super.extractRules(valueCache, cssText, loc);
|
|
37
|
+
if (!values.size && !registrations.length) return super.extractRules(valueCache, cssText, loc);
|
|
21
38
|
const rules = super.extractRules(valueCache, source.toString(), loc);
|
|
22
|
-
|
|
39
|
+
const restoredRules=Object.fromEntries(Object.entries(rules).map(([key, rule]) => {
|
|
23
40
|
const output = postcss.parse(rule.cssText);
|
|
24
41
|
output.walkDecls(declaration => {
|
|
25
42
|
if (declaration.prop.startsWith("--") && values.has(declaration.value)) declaration.value = values.get(declaration.value);
|
|
@@ -27,6 +44,8 @@ module.exports = function preserveCustomProperties(Processor) {
|
|
|
27
44
|
const restored = output.toString();
|
|
28
45
|
return [rule.atom ? restored : key, { ...rule, cssText: restored }];
|
|
29
46
|
}));
|
|
47
|
+
const globalRule=cssText=>({cssText,start:loc?.start??null,className:this.className,displayName:this.displayName,atom:true});
|
|
48
|
+
return {...Object.fromEntries(registrations.length?layerOrder.map(css=>[css,globalRule(css)]):[]),...restoredRules,...Object.fromEntries(registrations.map(css=>[css,globalRule(css)]))};
|
|
30
49
|
}
|
|
31
50
|
};
|
|
32
51
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { behaviorStatus } from "../verification.mjs";
|
|
2
|
+
import { compactRow, resultPage } from "./result-pages.mjs";
|
|
3
|
+
|
|
4
|
+
export const behaviorSections = ["owners", "checks", "guarantees", "subjects", "demonstrations", "profiles"];
|
|
5
|
+
const checkResult = (unit, id, result) => {
|
|
6
|
+
const {status,current,revision,runId,profile,case:caseId,durationMs,completedAt,failure}=result;
|
|
7
|
+
return {status,current,revision,runId,profile,case:caseId,durationMs,completedAt,...(failure?{failure}:{}),
|
|
8
|
+
...(runId?{investigation:{unit,check:id,runId}}:{})};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Discovery never embeds stored runner reports or investigation bodies. */
|
|
12
|
+
export function behaviorPage(options, {unit,api,query="",section=unit?"checks":"owners",cursor,limit} = {}) {
|
|
13
|
+
if(!behaviorSections.includes(section)||section!=="owners"&&!unit)throw new Error("Select an exact unit before paging behavioral details.");
|
|
14
|
+
const inventory=behaviorStatus(options);
|
|
15
|
+
const matches=inventory.contracts.filter(contract=>(!unit||contract.unit===unit)&&(!api||Object.values(contract.subjects).some(subject=>subject.api===api))&&JSON.stringify([contract.unit,contract.subjects,contract.guarantees.map(({id,description})=>({id,description}))]).toLowerCase().includes(query.toLowerCase()));
|
|
16
|
+
let rows;
|
|
17
|
+
if(section==="owners") rows=matches.map(contract=>({unit:contract.unit,file:contract.file,
|
|
18
|
+
apis:Object.values(contract.subjects).filter(subject=>subject.api).map(subject=>subject.api).slice(0,10),
|
|
19
|
+
apiCount:Object.values(contract.subjects).filter(subject=>subject.api).length,
|
|
20
|
+
checkCount:Object.keys(contract.checks).length,guaranteeCount:contract.guarantees.length,
|
|
21
|
+
guarantees:contract.guarantees.slice(0,3).map(({id,result})=>({id,result})),guaranteesTruncated:contract.guarantees.length>3,
|
|
22
|
+
sections:{checks:Object.keys(contract.checks).length,guarantees:contract.guarantees.length,subjects:Object.keys(contract.subjects).length,demonstrations:Object.keys(contract.demonstrations).length},
|
|
23
|
+
next:{tool:"ads_behaviors",arguments:{unit:contract.unit,section:"checks"}}}));
|
|
24
|
+
else {
|
|
25
|
+
const contract=matches[0];
|
|
26
|
+
if(!contract)throw new Error("No behavior contract matches this unit/filter.");
|
|
27
|
+
if(section==="checks")rows=Object.entries(contract.checks).map(([id,{result,...check}])=>({id,...check,result:checkResult(contract.unit,id,result)}));
|
|
28
|
+
if(section==="guarantees")rows=contract.guarantees;
|
|
29
|
+
if(section==="subjects")rows=Object.entries(contract.subjects).map(([id,subject])=>({id,...subject}));
|
|
30
|
+
if(section==="demonstrations")rows=Object.entries(contract.demonstrations).map(([subject,scenarios])=>({subject,scenarios}));
|
|
31
|
+
if(section==="profiles")rows=[...new Set(Object.values(contract.checks).map(check=>check.profile))].map(id=>({id,...options.config.mcp.behaviorProfiles[id]}));
|
|
32
|
+
}
|
|
33
|
+
const revision=matches.map(contract=>[contract.unit,contract.file,Object.entries(contract.checks).map(([id,check])=>[id,check.result.revision,check.result.status,check.result.runId])]);
|
|
34
|
+
return resultPage(rows,{cursor,limit,identity:["behaviors",section,unit,api,query,revision,rows],key:section==="owners"?"contracts":"items",
|
|
35
|
+
header:{section,...(unit?{unit,sourceFile:matches[0]?.file}:{}),diagnosticCount:inventory.diagnostics.length,diagnostics:inventory.diagnostics.slice(0,2).map(row=>compactRow(row,384)),resourceUri:"ads://project/behaviors"}});
|
|
36
|
+
}
|
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. Execution replies are bounded summaries. Read only needed sections with ads_results(resultId, section, status, cursor); follow nextCursor without rerunning. For a failure, inspect its boundary and acceptance, then ads_investigate with the outcome's exact unit/check/runId; detail sections and artifacts load separately (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];
|
|
@@ -58,6 +58,8 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
|
|
|
58
58
|
Inspection connects a public export to its owning source, available guides, token values and discovered preview IDs. Bounded tools keep discovery focused; explicit resource reads supply detail. Source metadata does not prove layout, interaction or motion. A scenario renders the real public implementation and its fixture context in either review surface.
|
|
59
59
|
5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
|
|
60
60
|
6. ${route.validation} Registered commands: ${context.commands.map((command) => command.args?.join(" ") ?? `choose the project package manager for ${command.script}`).join("; ") || "none discovered; inspect the project's build setup"}. Run them from ${context.projectRoot}.
|
|
61
|
+
Execution replies from ads_verify and guarded edits contain counts, acceptance and resultId, not full reports. Read only needed outcomes/diagnostics/captures with ads_results, following nextCursor with identical filters. A truncated row has recordIndex for bounded text ranges. For one failure, use ads_investigate with its unit/check/runId; read request/error/artifact sections only when needed. Only the latest verification and mutation results, and latest investigation per check, are available. Notifications do not establish freshness.
|
|
62
|
+
|
|
61
63
|
7. After edits, inspect ads_status.build or ads://project/build explicitly. Running/unreported/old output is not a current successful preview. Fix diagnostics in owned source, wait for rebuilding and invoke the preview tool again. Notifications may not start an agent turn.
|
|
62
64
|
|
|
63
65
|
Project guide resources: ${context.guides.map((guide) => `${guide.file}: ${guide.uri}`).join("; ") || "none discovered"}.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { compactAcceptance, compactRow, jsonBytes, resultBudgets, resultPage, resultRecord } from "./result-pages.mjs";
|
|
5
|
+
|
|
6
|
+
export const RESULTS_URI = "ads://project/results";
|
|
7
|
+
export const resultSections = ["outcomes", "checks", "diagnostics", "advisories", "warnings", "changes", "captures", "acceptance", "investigation", "selection"];
|
|
8
|
+
const kinds = ["verification", "mutation"];
|
|
9
|
+
const maxStoredBytes = 8 * 1024 * 1024;
|
|
10
|
+
const uuid = value => typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value);
|
|
11
|
+
const resultUri = id => `ads://results/${id}`;
|
|
12
|
+
const count = items => {
|
|
13
|
+
const statuses = {};
|
|
14
|
+
for (const item of items) { const status = item.status ?? (item.skipped ? "skipped" : item.passed === true ? "passed" : item.passed === false ? "failed" : "unknown"); statuses[status] = (statuses[status] ?? 0) + 1; }
|
|
15
|
+
return { total: items.length, ...statuses };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function outcome(item) {
|
|
19
|
+
const { current, investigation, ...detail } = item;
|
|
20
|
+
return { ...detail, inputsCurrentAtCompletion:current,
|
|
21
|
+
investigation: { unit:item.unit, check:item.check, runId:item.runId, resourceUri:investigation?.resourceUri } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function snapshot(kind, raw) {
|
|
25
|
+
const outcomes = raw.results ?? raw.behavior?.results ?? (raw.checks ?? []).flatMap(check => check.results ?? []);
|
|
26
|
+
const sections = {
|
|
27
|
+
outcomes: outcomes.map(outcome),
|
|
28
|
+
checks: (raw.checks ?? []).map(({ results, ...check }) => ({ ...check, ...(results ? { outcomeCount: results.length } : {}) })),
|
|
29
|
+
diagnostics: raw.diagnostics ?? [], advisories: raw.advisories ?? [], warnings: raw.warnings ?? [], changes: raw.changes ?? [],
|
|
30
|
+
captures: raw.screenshots?.captures ?? [], selection: [{unit:raw.unit,units:raw.units,transaction:raw.transaction}],
|
|
31
|
+
acceptance: raw.acceptance ? [raw.acceptance] : [], investigation: raw.investigation ? [raw.investigation] : [],
|
|
32
|
+
};
|
|
33
|
+
const resultId = randomUUID();
|
|
34
|
+
return { schemaVersion: 1, resultId, kind, createdAt: new Date().toISOString(), ok: !!raw.ok,
|
|
35
|
+
...(raw.applied !== undefined ? { applied: raw.applied } : {}), ...(raw.unit ? { unit: raw.unit } : {}),
|
|
36
|
+
...(raw.units ? { units: raw.units } : {}), ...(raw.transaction ? { transaction: true } : {}),
|
|
37
|
+
acceptance: raw.acceptance, investigation: raw.investigation,
|
|
38
|
+
sections: Object.fromEntries(Object.entries(sections).map(([key, rows]) => [key, rows.map((row, recordIndex) => ({ recordIndex, ...(row && typeof row === "object" ? row : {message:row}) }))])),
|
|
39
|
+
counts: { outcomes: count(outcomes), checks: count(sections.checks), diagnostics: sections.diagnostics.length,
|
|
40
|
+
advisories: sections.advisories.length, warnings: sections.warnings.length, changes: sections.changes.length, captures: sections.captures.length },
|
|
41
|
+
resourceUri: resultUri(resultId) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function summary(value, storageWarning) {
|
|
45
|
+
const { ok, resultId, kind, applied, unit, units, transaction, createdAt, counts, resourceUri } = value;
|
|
46
|
+
const failedOutcomes = value.sections.outcomes.filter(item => item.status !== "passed");
|
|
47
|
+
const failureRows = failedOutcomes.length ? failedOutcomes : value.sections.diagnostics;
|
|
48
|
+
const section = failedOutcomes.length ? "outcomes" : value.sections.diagnostics.length ? "diagnostics" : value.sections.captures.length ? "captures" : value.sections.outcomes.length ? "outcomes" : "checks";
|
|
49
|
+
const result = { ok, resultId, kind, ...(applied !== undefined ? { applied } : {}), ...(unit ? { unit } : {}),
|
|
50
|
+
...(units ? { units } : {}), ...(transaction ? { transaction } : {}), createdAt, counts, freshness:"recorded-at-completion",
|
|
51
|
+
acceptance: compactAcceptance(value.acceptance), failures: value.failurePreview ?? failureRows.slice(0, 3).map(item => compactRow({recordIndex:item.recordIndex,unit:item.unit,check:item.check,status:item.status,rule:item.rule,file:item.file,failure:item.failure,message:item.message},640)),
|
|
52
|
+
failureCount: value.failureCount ?? failureRows.length, resourceUri,
|
|
53
|
+
next: value.detailsUnavailable ? null : { tool: "ads_results", arguments: { resultId, section, ...(section === "outcomes" && failedOutcomes.length ? { status: "not_passed" } : {}) } },
|
|
54
|
+
...((storageWarning ?? value.storageWarning) ? { storageWarning: storageWarning ?? value.storageWarning } : {}), byteLimit: resultBudgets.summary };
|
|
55
|
+
if (jsonBytes(result) > resultBudgets.summary) {
|
|
56
|
+
result.failures = failureRows.slice(0, 3).map(item => compactRow(item, 384));
|
|
57
|
+
result.detailTruncated = true;
|
|
58
|
+
}
|
|
59
|
+
if (jsonBytes(result) > resultBudgets.summary) {
|
|
60
|
+
result.failures = []; result.detailTruncated = true;
|
|
61
|
+
result.acceptance = { scope: value.acceptance?.scope, status: value.acceptance?.status, detailTruncated: true };
|
|
62
|
+
}
|
|
63
|
+
if (jsonBytes(result) > resultBudgets.summary) {
|
|
64
|
+
delete result.unit; delete result.units;
|
|
65
|
+
result.acceptance = { status: value.acceptance?.status, detailTruncated: true };
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One immutable, replaceable snapshot per execution kind. No result history. */
|
|
71
|
+
export function createExecutionResults(root) {
|
|
72
|
+
root = path.resolve(root);
|
|
73
|
+
const folder = path.join(root, ".ds/tool-results"), memory = new Map();
|
|
74
|
+
function file(kind) { return path.join(folder, `${kind}.json`); }
|
|
75
|
+
function read(kind) {
|
|
76
|
+
const temporary = memory.get(kind);
|
|
77
|
+
if (temporary?.memoryOnly) return temporary.value;
|
|
78
|
+
const target = file(kind);
|
|
79
|
+
try {
|
|
80
|
+
if (realpathSync(target) !== target || !lstatSync(target).isFile() || lstatSync(target).nlink !== 1 || lstatSync(target).size > maxStoredBytes) return undefined;
|
|
81
|
+
const value = JSON.parse(readFileSync(target, "utf8"));
|
|
82
|
+
return value.schemaVersion === 1 && value.kind === kind && uuid(value.resultId) && resultSections.every(section => Array.isArray(value.sections?.[section])) ? value : undefined;
|
|
83
|
+
} catch { return undefined; }
|
|
84
|
+
}
|
|
85
|
+
function find(resultId) {
|
|
86
|
+
if (!uuid(resultId)) throw new Error("Use an exact resultId returned by an execution summary.");
|
|
87
|
+
const value = kinds.map(read).find(value => value?.resultId === resultId);
|
|
88
|
+
if (!value) throw new Error("Result is unavailable or superseded. Only the latest completed result of each kind is retained; discover its ID with ads_status.");
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
save(kind, raw) {
|
|
93
|
+
if (!kinds.includes(kind)) throw new Error("Unknown execution kind.");
|
|
94
|
+
const value = snapshot(kind, raw);
|
|
95
|
+
let content = JSON.stringify(value);
|
|
96
|
+
if (Buffer.byteLength(content) > maxStoredBytes) {
|
|
97
|
+
const preview=summary(value);
|
|
98
|
+
value.failurePreview=preview.failures;value.failureCount=preview.failureCount;
|
|
99
|
+
value.sections=Object.fromEntries(resultSections.map(section=>[section,[]]));
|
|
100
|
+
value.acceptance=preview.acceptance;delete value.investigation;
|
|
101
|
+
value.detailsUnavailable=true;
|
|
102
|
+
value.storageWarning="Result details exceeded the 8 MiB retention limit. Only this summary is retained; execution and applied state above are unchanged.";
|
|
103
|
+
content=JSON.stringify(value);
|
|
104
|
+
}
|
|
105
|
+
let storageWarning;
|
|
106
|
+
try {
|
|
107
|
+
let ancestor=folder;
|
|
108
|
+
while(true){try{lstatSync(ancestor);break;}catch(error){if(error.code!=="ENOENT")throw error;ancestor=path.dirname(ancestor);}}
|
|
109
|
+
if(realpathSync(ancestor)!==ancestor)throw new Error("Result output cannot resolve through a symlink.");
|
|
110
|
+
mkdirSync(folder, { recursive: true });
|
|
111
|
+
if (realpathSync(folder) !== folder) throw new Error("Result output cannot resolve through a symlink.");
|
|
112
|
+
const staged = `${file(kind)}.${randomUUID()}.tmp`;
|
|
113
|
+
try { writeFileSync(staged, content); renameSync(staged, file(kind)); } finally { rmSync(staged, { force: true }); }
|
|
114
|
+
memory.set(kind, { value, memoryOnly: false });
|
|
115
|
+
} catch {
|
|
116
|
+
memory.set(kind, { value: JSON.parse(content), memoryOnly: true });
|
|
117
|
+
storageWarning = "Result details are available in this server's memory only; storage failed. Execution and applied state above are unchanged.";
|
|
118
|
+
}
|
|
119
|
+
return summary(value, storageWarning);
|
|
120
|
+
},
|
|
121
|
+
latest(kind) { const value = read(kind); return value ? summary(value, memory.get(kind)?.memoryOnly ? "Result details are in server memory only." : undefined) : null; },
|
|
122
|
+
index() { return { retention: "latest-per-kind", results: kinds.map(kind => this.latest(kind)).filter(Boolean).map(({resultId,kind,ok,applied,createdAt,counts,resourceUri})=>({resultId,kind,ok,applied,createdAt,counts,resourceUri})) }; },
|
|
123
|
+
fingerprint() { return kinds.map(kind=>{if(memory.get(kind)?.memoryOnly)return memory.get(kind).value.resultId;try{const stat=lstatSync(file(kind));return `${kind}:${stat.mtimeMs}:${stat.size}`;}catch{return `${kind}:missing`;}}).join("\n"); },
|
|
124
|
+
page({ resultId, section = "outcomes", status = "all", cursor, limit, recordIndex, offset, chars } = {}) {
|
|
125
|
+
const value = find(resultId);
|
|
126
|
+
if(value.detailsUnavailable)throw new Error("This result exceeded the 8 MiB retention limit; only its summary is available.");
|
|
127
|
+
if (!resultSections.includes(section) || !["all", "passed", "not_passed"].includes(status) || status !== "all" && section !== "outcomes") throw new Error("Select a registered result section; status filters apply only to outcomes.");
|
|
128
|
+
if (recordIndex !== undefined) {
|
|
129
|
+
if (cursor !== undefined || status !== "all") throw new Error("Read record ranges without a cursor or status filter.");
|
|
130
|
+
return resultRecord(value.sections[section],{recordIndex,offset,chars,header:{resultId,section}});
|
|
131
|
+
}
|
|
132
|
+
if(offset!==undefined||chars!==undefined)throw new Error("Select a recordIndex before reading text ranges.");
|
|
133
|
+
const rows = value.sections[section].filter(row => status === "all" || (status === "passed" ? row.status === "passed" : row.status !== "passed"));
|
|
134
|
+
return resultPage(rows, { cursor, limit, identity: [resultId, section, status], header: { resultId, kind: value.kind, section, status, freshness:"recorded-at-completion" } });
|
|
135
|
+
},
|
|
136
|
+
readResource(uri) {
|
|
137
|
+
if (uri === RESULTS_URI) return { uri, mimeType: "application/json", text: JSON.stringify(this.index()) };
|
|
138
|
+
const url = new URL(uri);
|
|
139
|
+
if (url.protocol !== "ads:" || url.hostname !== "results" || [...url.searchParams.keys()].some(key => !["section", "status", "cursor", "limit", "recordIndex", "offset", "chars"].includes(key))) throw new Error("Invalid execution result resource URI.");
|
|
140
|
+
const id = url.pathname.slice(1);
|
|
141
|
+
const value = url.search ? this.page({ resultId: id, section: url.searchParams.get("section") ?? "outcomes", status: url.searchParams.get("status") ?? "all", cursor: url.searchParams.get("cursor") ?? undefined,
|
|
142
|
+
...Object.fromEntries(["limit","recordIndex","offset","chars"].filter(key=>url.searchParams.has(key)).map(key=>[key,Number(url.searchParams.get(key))])) }) : summary(find(id));
|
|
143
|
+
return { uri, mimeType: "application/json", text: JSON.stringify(value) };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|