@reqvire-org/ads 0.3.17 → 0.3.20
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/dist/atomic-composition.mjs +1 -1
- package/dist/behaviors.mjs +5 -5
- package/dist/bootstrap-ds/ADS.md +2 -2
- package/dist/bootstrap-ds/README.md +1 -1
- package/dist/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
- package/dist/bootstrap-ds/ds.config.json +1 -1
- package/dist/bootstrap-ds/tsconfig.json +18 -3
- package/dist/bootstrap-ds/vite.config.ts +1 -1
- package/dist/boundaries.mjs +4 -2
- package/dist/check.mjs +49 -15
- package/dist/config.mjs +13 -2
- package/dist/init.mjs +17 -14
- package/dist/investigations.mjs +34 -3
- package/dist/layers.mjs +288 -0
- package/dist/linaria/preserve-values.cjs +21 -2
- package/dist/lint-config.mjs +1 -1
- package/dist/mcp/behavior-pages.mjs +36 -0
- package/dist/mcp/context.mjs +5 -2
- package/dist/mcp/edit-preparation.mjs +3 -3
- package/dist/mcp/execution-results.mjs +146 -0
- package/dist/mcp/mutations.mjs +4 -4
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/result-pages.mjs +87 -0
- package/dist/mcp/server.mjs +49 -30
- package/dist/mcp/widget.bundle.js +1 -1
- package/dist/oxlint.mjs +3 -1
- package/dist/preview-resources.mjs +11 -1
- package/dist/property-registrations.mjs +39 -0
- package/dist/public-api.mjs +13 -6
- package/dist/scenario-trace.mjs +7 -7
- package/dist/scenarios-vite.mjs +2 -1
- package/dist/scenarios.mjs +10 -7
- package/dist/scoped-tokens.mjs +6 -0
- package/dist/showcase/client.bundle.js +1 -1
- package/dist/showcase/sandbox.bundle.js +1 -1
- package/dist/skills/ads-break-ui/SKILL.md +27 -10
- 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 +52 -5
- package/dist/skills/ads-break-ui/references/guarded-editing.md +9 -6
- package/dist/skills/ads-break-ui/references/layers.md +123 -0
- package/dist/skills/ads-break-ui/references/review.md +96 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
- package/dist/skills/ads-designer/SKILL.md +28 -11
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
- 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 +52 -5
- package/dist/skills/ads-designer/references/guarded-editing.md +9 -6
- package/dist/skills/ads-designer/references/layers.md +123 -0
- package/dist/skills/ads-designer/references/review.md +96 -0
- package/dist/skills/ads-designer/references/scenarios.md +24 -3
- package/dist/skills/ads-import/SKILL.md +27 -10
- 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 +52 -5
- package/dist/skills/ads-import/references/guarded-editing.md +9 -6
- package/dist/skills/ads-import/references/layers.md +123 -0
- package/dist/skills/ads-import/references/review.md +96 -0
- package/dist/skills/ads-import/references/scenarios.md +24 -3
- package/dist/skills/ads-migrate/SKILL.md +27 -10
- 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 +52 -5
- package/dist/skills/ads-migrate/references/guarded-editing.md +9 -6
- package/dist/skills/ads-migrate/references/layers.md +123 -0
- package/dist/skills/ads-migrate/references/review.md +96 -0
- package/dist/skills/ads-migrate/references/scenarios.md +24 -3
- package/dist/skills/ads-motion/SKILL.md +28 -11
- package/dist/skills/ads-motion/references/atomic-styles.md +48 -0
- package/dist/skills/ads-motion/references/audit.md +1 -1
- package/dist/skills/ads-motion/references/captures.md +6 -1
- package/dist/skills/ads-motion/references/evidence.md +52 -5
- package/dist/skills/ads-motion/references/guarded-editing.md +9 -6
- package/dist/skills/ads-motion/references/layers.md +123 -0
- package/dist/skills/ads-motion/references/motion-review.md +35 -0
- package/dist/skills/ads-motion/references/review.md +96 -35
- package/dist/skills/ads-motion/references/scenarios.md +24 -3
- package/dist/units.mjs +17 -14
- package/docs/ADS.md +42 -5
- package/docs/ARCHITECTURE.md +64 -4
- package/docs/BEHAVIORS.md +21 -2
- package/docs/CHANGELOG.md +40 -0
- package/docs/GUIDE.md +3 -2
- package/docs/MCP.md +87 -20
- package/package.json +2 -1
- package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
package/dist/layers.mjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import selectorParser from "postcss-selector-parser";
|
|
5
|
+
import valueParser from "postcss-value-parser";
|
|
6
|
+
import { isDevelopmentFile, isInside, isStateVariable, isTestFile, isReviewFile } from "./config.mjs";
|
|
7
|
+
import { importResolver, styleAccessProperty } from "./boundaries.mjs";
|
|
8
|
+
|
|
9
|
+
const guidance = "See ads://guides/layers. A component provides one cohesive UI capability; a pattern arranges capabilities into a reusable structure or workflow. Component count does not decide the layer.";
|
|
10
|
+
|
|
11
|
+
/** Include statically addressed local helpers outside the configured source roots.
|
|
12
|
+
* Dependency packages remain external; no project module is imported/executed. */
|
|
13
|
+
export function layerSourceFiles(initial, options) {
|
|
14
|
+
const files = new Set(initial), resolve = importResolver(options);
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const source = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
|
|
17
|
+
function include(literal) {
|
|
18
|
+
if (!literal || !ts.isStringLiteralLike(literal)) return;
|
|
19
|
+
for (const dependency of resolve(literal.text, file)) {
|
|
20
|
+
if (isInside(options.root, dependency) && !dependency.split(path.sep).includes("node_modules") && /\.[cm]?[jt]sx?$/.test(dependency) && existsSync(dependency)) files.add(dependency);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function visit(node) {
|
|
24
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) include(node.moduleSpecifier);
|
|
25
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) include(node.moduleReference.expression);
|
|
26
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) include(node.argument.literal);
|
|
27
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require")) include(node.arguments[0]);
|
|
28
|
+
ts.forEachChild(node, visit);
|
|
29
|
+
}
|
|
30
|
+
visit(source);
|
|
31
|
+
}
|
|
32
|
+
return [...files];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Source-only dependency and private-style evidence; never evaluates project code. */
|
|
36
|
+
export function createLayerBoundaries(program, options, report) {
|
|
37
|
+
const { root, dsRoot, sourceRoot, config } = options;
|
|
38
|
+
const checker = program.getTypeChecker(), resolve = importResolver(options);
|
|
39
|
+
const sources = program.getSourceFiles().filter(source => isInside(root, source.fileName) && !source.isDeclarationFile && !source.fileName.split(path.sep).includes("node_modules"));
|
|
40
|
+
const runtime = file => !isTestFile(file) && !isDevelopmentFile(file, options);
|
|
41
|
+
const layer = file => ["components", "patterns", "controllers", "composition", "review"].find(name => isInside(path.join(dsRoot, name), file)) ?? (isInside(sourceRoot, file) && !isInside(dsRoot, file) ? "application" : "shared");
|
|
42
|
+
const unalias = symbol => symbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
43
|
+
const entry = program.getSourceFile(path.join(dsRoot, config.publicEntry));
|
|
44
|
+
const module = entry && checker.getSymbolAtLocation(entry);
|
|
45
|
+
const publicSymbols = new Set((module ? checker.getExportsOfModule(module) : []).map(unalias));
|
|
46
|
+
const publicFiles = new Set([...publicSymbols].filter(symbol => symbol?.flags & ts.SymbolFlags.Value).flatMap(symbol => symbol.declarations?.map(node => node.getSourceFile().fileName) ?? []));
|
|
47
|
+
const edges = new Map(), emitted = new Set(), classOwners = new Map(), attributeOwners = new Map(), componentVariables = new Set(), patternVariables = new Set(), publicHooks = new Set();
|
|
48
|
+
|
|
49
|
+
function emit(node, rule, message) {
|
|
50
|
+
const source = node.getSourceFile(), position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
51
|
+
const key = `${source.fileName}:${node.pos}:${rule}:${message}`;
|
|
52
|
+
if (emitted.has(key)) return;
|
|
53
|
+
emitted.add(key);
|
|
54
|
+
report(source.fileName, rule, `${message} ${guidance}`, position.line + 1, position.character + 1, { guidanceUri: "ads://guides/layers" });
|
|
55
|
+
}
|
|
56
|
+
function target(specifier, source) {
|
|
57
|
+
return resolve(specifier, source.fileName).find(file => program.getSourceFile(file));
|
|
58
|
+
}
|
|
59
|
+
function exportsOf(file) {
|
|
60
|
+
const source = file && program.getSourceFile(file), symbol = source && checker.getSymbolAtLocation(source);
|
|
61
|
+
return symbol ? checker.getExportsOfModule(symbol) : [];
|
|
62
|
+
}
|
|
63
|
+
function origins(symbol) {
|
|
64
|
+
symbol = unalias(symbol);
|
|
65
|
+
return [...new Set(symbol?.declarations?.map(node => node.getSourceFile().fileName) ?? [])].filter(file => isInside(root, file) && !file.split(path.sep).includes("node_modules")).map(file => ({ file, symbol }));
|
|
66
|
+
}
|
|
67
|
+
function bindings(pattern) {
|
|
68
|
+
return ts.isObjectBindingPattern(pattern) ? pattern.elements.filter(node => !node.dotDotDotToken).map(node => (node.propertyName ?? node.name).text).filter(Boolean) : null;
|
|
69
|
+
}
|
|
70
|
+
function members(identifier, source) {
|
|
71
|
+
const symbol = checker.getSymbolAtLocation(identifier), names = new Set();
|
|
72
|
+
let opaque = false;
|
|
73
|
+
function visit(node) {
|
|
74
|
+
if (ts.isIdentifier(node) && node !== identifier && checker.getSymbolAtLocation(node) === symbol) {
|
|
75
|
+
const parent = node.parent;
|
|
76
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) names.add(parent.name.text);
|
|
77
|
+
else if (ts.isElementAccessExpression(parent) && parent.expression === node && ts.isStringLiteralLike(parent.argumentExpression)) names.add(parent.argumentExpression.text);
|
|
78
|
+
else if (ts.isVariableDeclaration(parent) && parent.initializer === node && bindings(parent.name)) for (const name of bindings(parent.name)) names.add(name);
|
|
79
|
+
else opaque = true;
|
|
80
|
+
}
|
|
81
|
+
ts.forEachChild(node, visit);
|
|
82
|
+
}
|
|
83
|
+
visit(source);
|
|
84
|
+
return opaque ? null : [...names];
|
|
85
|
+
}
|
|
86
|
+
function selectedImport(call, source) {
|
|
87
|
+
let expression = call;
|
|
88
|
+
while (ts.isAwaitExpression(expression.parent) || ts.isParenthesizedExpression(expression.parent)) expression = expression.parent;
|
|
89
|
+
const parent = expression.parent;
|
|
90
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === expression && parent.name.text !== "then") return [parent.name.text];
|
|
91
|
+
if (ts.isElementAccessExpression(parent) && parent.expression === expression && ts.isStringLiteralLike(parent.argumentExpression)) return [parent.argumentExpression.text];
|
|
92
|
+
if (ts.isVariableDeclaration(parent)) return bindings(parent.name) ?? (ts.isIdentifier(parent.name) ? members(parent.name, source) : null);
|
|
93
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name.text === "then" && ts.isCallExpression(parent.parent)) {
|
|
94
|
+
const callback = parent.parent.arguments[0];
|
|
95
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && callback.parameters[0]) {
|
|
96
|
+
const parameter = callback.parameters[0].name;
|
|
97
|
+
return bindings(parameter) ?? (ts.isIdentifier(parameter) ? members(parameter, source) : null);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function add(source, node, dependency, typeOnly = false) {
|
|
103
|
+
if (!dependency?.file || !isInside(root, dependency.file) || dependency.file.split(path.sep).includes("node_modules")) return;
|
|
104
|
+
const list = edges.get(source.fileName) ?? [];
|
|
105
|
+
list.push({ ...dependency, node, typeOnly });
|
|
106
|
+
edges.set(source.fileName, list);
|
|
107
|
+
}
|
|
108
|
+
function addSymbol(source, node, symbol, typeOnly = false) {
|
|
109
|
+
for (const origin of origins(symbol)) add(source, node, origin, typeOnly);
|
|
110
|
+
}
|
|
111
|
+
function addModule(source, node, file, names, typeOnly = false) {
|
|
112
|
+
if (!file) return;
|
|
113
|
+
const values = exportsOf(file).filter(symbol => names === null || names.includes(symbol.name));
|
|
114
|
+
for (const symbol of values) if (typeOnly || unalias(symbol)?.flags & ts.SymbolFlags.Value) addSymbol(source, node, symbol, typeOnly);
|
|
115
|
+
if (!values.length || layer(file) === "composition" || isDevelopmentFile(file, options)) add(source, node, { file }, typeOnly);
|
|
116
|
+
// Explicit side effects in a forwarding barrel still run for a selected export.
|
|
117
|
+
if (!typeOnly) for (const statement of program.getSourceFile(file)?.statements ?? []) {
|
|
118
|
+
if (ts.isImportDeclaration(statement) && !statement.importClause && ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
119
|
+
const sideEffect = target(statement.moduleSpecifier.text, statement.getSourceFile());
|
|
120
|
+
if (sideEffect) add(source, node, { file: sideEffect });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const source of sources) {
|
|
125
|
+
function visit(node) {
|
|
126
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
127
|
+
const file = target(node.moduleSpecifier.text, source), clause = node.importClause;
|
|
128
|
+
if (!clause) add(source, node, { file: file ?? resolve(node.moduleSpecifier.text, source.fileName)[0] });
|
|
129
|
+
else {
|
|
130
|
+
if (clause.name) addSymbol(source, node, checker.getSymbolAtLocation(clause.name), !!clause.isTypeOnly);
|
|
131
|
+
const named = clause.namedBindings;
|
|
132
|
+
if (named && ts.isNamedImports(named)) for (const item of named.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(clause.isTypeOnly || item.isTypeOnly));
|
|
133
|
+
if (named && ts.isNamespaceImport(named)) addModule(source, node, file, members(named.name, source), !!clause.isTypeOnly);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (ts.isExportDeclaration(node)) {
|
|
137
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) for (const item of node.exportClause.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(node.isTypeOnly || item.isTypeOnly));
|
|
138
|
+
else if (node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) addModule(source, node, target(node.moduleSpecifier.text, source), null, !!node.isTypeOnly);
|
|
139
|
+
}
|
|
140
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
|
|
141
|
+
addModule(source, node, target(node.moduleReference.expression.text, source), members(node.name, source), !!node.isTypeOnly);
|
|
142
|
+
}
|
|
143
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal)) addModule(source, node, target(node.argument.literal.text, source), null, true);
|
|
144
|
+
const requireSymbol = ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" ? checker.getSymbolAtLocation(node.expression) : null;
|
|
145
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require" && (!requireSymbol || requireSymbol.declarations?.every(declaration => declaration.getSourceFile().isDeclarationFile))) && ts.isStringLiteralLike(node.arguments[0])) {
|
|
146
|
+
addModule(source, node, target(node.arguments[0].text, source), selectedImport(node, source));
|
|
147
|
+
}
|
|
148
|
+
ts.forEachChild(node, visit);
|
|
149
|
+
}
|
|
150
|
+
visit(source);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runtimeHelpers() {
|
|
154
|
+
const found = new Set(sources.filter(source => isReviewFile(source.fileName, options) || runtime(source.fileName) && ["components", "patterns", "controllers"].includes(layer(source.fileName))).map(source => source.fileName));
|
|
155
|
+
for (const file of found) for (const edge of edges.get(file) ?? []) if (!edge.typeOnly && runtime(edge.file) && program.getSourceFile(edge.file)) found.add(edge.file);
|
|
156
|
+
return [...found];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inspectDependencies() {
|
|
160
|
+
for (const source of sources.filter(source => runtime(source.fileName) || isReviewFile(source.fileName, options))) {
|
|
161
|
+
const ownLayer = layer(source.fileName);
|
|
162
|
+
// Check resolved declaration ownership too: a barrel cannot launder fixtures/app code.
|
|
163
|
+
if (isInside(dsRoot, source.fileName) || ownLayer === "application") for (const edge of edges.get(source.fileName) ?? []) {
|
|
164
|
+
if (runtime(source.fileName) && layer(edge.file) === "review") emit(edge.node, "review-import", "Production code and its public entry cannot depend on review implementations or their types, including through aliases and re-exports. Use the development review entry only from scenarios, fixtures and tests.");
|
|
165
|
+
if (edge.typeOnly) continue;
|
|
166
|
+
if (runtime(source.fileName) && !runtime(edge.file)) emit(edge.node, "showcase-import", "Runtime code cannot depend on review helpers, scenarios, fixtures or checks, including through re-exports. Keep demonstrations beside their owner and integration harnesses in tests.");
|
|
167
|
+
if (isInside(dsRoot, source.fileName) && layer(edge.file) === "application") emit(edge.node, "no-app-import", "ADS runtime cannot import application stores, routing or services, including through forwarding modules. Receive data and navigation callbacks through public APIs.");
|
|
168
|
+
}
|
|
169
|
+
if (!["components", "patterns", "review"].includes(ownLayer)) continue;
|
|
170
|
+
for (const initial of edges.get(source.fileName) ?? []) {
|
|
171
|
+
if (initial.typeOnly && ownLayer === "components") continue;
|
|
172
|
+
const queue = [{ edge: initial, trail: [source.fileName] }], seen = new Set();
|
|
173
|
+
while (queue.length) {
|
|
174
|
+
const { edge, trail } = queue.shift();
|
|
175
|
+
const key = `${edge.file}:${edge.symbol?.name ?? ""}`;
|
|
176
|
+
if (seen.has(key)) continue;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
const dependencyLayer = layer(edge.file), chain = [...trail, edge.file];
|
|
179
|
+
const via = chain.map(file => path.relative(root, file).replaceAll("\\", "/")).join(" → ");
|
|
180
|
+
if (ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer)) {
|
|
181
|
+
emit(initial.node, "component-dependency", `Component runtime depends on ${dependencyLayer}: ${via}. Move the arrangement to a pattern or extract a lower-level component/controller; do not change a valid API just to satisfy classification.`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (["patterns", "review"].includes(ownLayer) && dependencyLayer === "components") {
|
|
185
|
+
if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "component-contract", `Pattern accesses private component ${edge.symbol?.name ?? "module"}: ${via}. Export an intentional public contract or keep the helper/state/style inside its component; importing a module directly is allowed when the selected symbol is public.`);
|
|
186
|
+
continue; // Public component implementations may use their own private files.
|
|
187
|
+
}
|
|
188
|
+
if (ownLayer === "review" && ["patterns", "controllers"].includes(dependencyLayer)) {
|
|
189
|
+
if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "review-contract", `Review accesses private ${dependencyLayer} ${edge.symbol?.name ?? "module"}: ${via}. Demonstrate the production API instead of depending on private implementation details.`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (dependencyLayer === "application" || !runtime(edge.file)) continue;
|
|
193
|
+
if (["patterns", "review"].includes(ownLayer) && dependencyLayer !== "shared") continue;
|
|
194
|
+
for (const next of edges.get(edge.file) ?? []) if (!next.typeOnly) queue.push({ edge: next, trail: chain });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function strings(node, seen = new Set()) {
|
|
201
|
+
if (!node || seen.has(node)) return [];
|
|
202
|
+
seen.add(node);
|
|
203
|
+
if (ts.isStringLiteralLike(node)) return [node.text];
|
|
204
|
+
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
|
|
205
|
+
const symbol = unalias(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node));
|
|
206
|
+
return (symbol?.declarations ?? []).flatMap(declaration => ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration) ? strings(declaration.initializer, seen) : []);
|
|
207
|
+
}
|
|
208
|
+
if (ts.isTaggedTemplateExpression(node)) return [];
|
|
209
|
+
const found = [];
|
|
210
|
+
ts.forEachChild(node, child => { found.push(...strings(child, seen)); });
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
function own(map, key, file) {
|
|
214
|
+
if (!map.has(key)) map.set(key, new Set());
|
|
215
|
+
map.get(key).add(file);
|
|
216
|
+
}
|
|
217
|
+
for (const symbol of publicSymbols) for (const node of symbol?.declarations ?? []) if (ts.isVariableDeclaration(node)) for (const value of strings(node.initializer)) publicHooks.add(value);
|
|
218
|
+
for (const source of sources.filter(source => runtime(source.fileName))) {
|
|
219
|
+
function visit(node) {
|
|
220
|
+
let name, value;
|
|
221
|
+
if (["components", "patterns"].includes(layer(source.fileName))) {
|
|
222
|
+
const write = styleAccessProperty(node);
|
|
223
|
+
if (typeof write === "string" && write.startsWith("--")) (layer(source.fileName) === "components" ? componentVariables : patternVariables).add(write);
|
|
224
|
+
}
|
|
225
|
+
if (ts.isJsxAttribute(node)) { name = node.name.getText(); value = node.initializer; }
|
|
226
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name))) { name = node.name.text; value = node.initializer; }
|
|
227
|
+
if (name === "className" || name === "class") for (const text of strings(value)) for (const token of text.split(/\s+/).filter(Boolean)) own(classOwners, token, source.fileName);
|
|
228
|
+
if (name?.startsWith("data-")) for (const text of strings(value)) own(attributeOwners, `${name}=${text}`, source.fileName);
|
|
229
|
+
ts.forEachChild(node, visit);
|
|
230
|
+
}
|
|
231
|
+
visit(source);
|
|
232
|
+
}
|
|
233
|
+
function privateHook(map, key, kinds = ["components"]) {
|
|
234
|
+
const owners = map.get(key);
|
|
235
|
+
return !publicHooks.has(key) && owners?.size && (map !== attributeOwners || owners.size === 1) && [...owners].every(file => kinds.includes(layer(file)));
|
|
236
|
+
}
|
|
237
|
+
// Called first for all component styles, then for patterns by checkProject.
|
|
238
|
+
function collectComponentStyles(file, css) {
|
|
239
|
+
if (runtime(file) && ["components", "patterns"].includes(layer(file))) css.walkDecls(declaration => {
|
|
240
|
+
if (declaration.prop.startsWith("--")) (layer(file) === "components" ? componentVariables : patternVariables).add(declaration.prop);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function inspectStyles(file, css, offset = { line: 1 }) {
|
|
244
|
+
if (!runtime(file) && !isReviewFile(file, options)) return;
|
|
245
|
+
const ownLayer = layer(file);
|
|
246
|
+
css.walkAtRules("import", rule => {
|
|
247
|
+
const first = valueParser(rule.params).nodes.find(node => node.type !== "space" && node.type !== "comment");
|
|
248
|
+
const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
|
|
249
|
+
if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
|
|
250
|
+
for (const dependency of resolve(specifier, file)) {
|
|
251
|
+
const dependencyLayer = layer(dependency);
|
|
252
|
+
const boundary = ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer) ? "component-dependency"
|
|
253
|
+
: ["patterns", "review"].includes(ownLayer) && dependencyLayer === "components" ? "component-contract" : null;
|
|
254
|
+
if (boundary) report(file, boundary, `Stylesheet import crosses the ${ownLayer} → ${dependencyLayer} boundary: ${specifier}. Consume public component contracts instead of importing private styles. ${guidance}`, offset.line + (rule.source?.start.line ?? 1) - 1, rule.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
if (!["patterns", "review"].includes(ownLayer)) return;
|
|
258
|
+
const failures = new Set();
|
|
259
|
+
const styleReport = (node, message) => {
|
|
260
|
+
const line = offset.line + (node.source?.start.line ?? 1) - 1;
|
|
261
|
+
const key = `${line}:${message}`;
|
|
262
|
+
if (failures.has(key)) return;
|
|
263
|
+
failures.add(key);
|
|
264
|
+
report(file, "component-style", `${message} Use component props, public class helpers or registered non-state customization variables; patterns own arrangement and spacing. ${guidance}`, line, node.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
|
|
265
|
+
};
|
|
266
|
+
css.walkRules(rule => {
|
|
267
|
+
try {
|
|
268
|
+
selectorParser(selectors => {
|
|
269
|
+
function subject(selector) {
|
|
270
|
+
const nodes = selector.nodes ?? [], lastCombinator = nodes.findLastIndex(node => node.type === "combinator");
|
|
271
|
+
for (const node of nodes.slice(lastCombinator + 1)) {
|
|
272
|
+
if (node.type === "class" && privateHook(classOwners, node.value, ownLayer === "review" ? ["components", "patterns"] : undefined)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} class .${node.value}.`);
|
|
273
|
+
if (node.type === "attribute" && node.operator === "=" && privateHook(attributeOwners, `${node.attribute}=${node.value}`, ownLayer === "review" ? ["components", "patterns"] : undefined) && !publicHooks.has(node.value)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} marker [${node.attribute}="${node.value}"].`);
|
|
274
|
+
if (node.type === "pseudo" && [":is", ":where"].includes(node.value)) for (const branch of node.nodes ?? []) subject(branch);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const selector of selectors.nodes) subject(selector);
|
|
278
|
+
}).processSync(rule.selector);
|
|
279
|
+
} catch { /* CSS syntax diagnostics are owned by the existing parser/build. */ }
|
|
280
|
+
});
|
|
281
|
+
css.walkDecls(declaration => valueParser(declaration.value).walk(node => {
|
|
282
|
+
if (node.type !== "function" || node.value !== "var") return;
|
|
283
|
+
const name = node.nodes.find(child => child.type === "word")?.value;
|
|
284
|
+
if ((componentVariables.has(name) || ownLayer === "review" && patternVariables.has(name) && !publicHooks.has(name)) && (isStateVariable(name) || !config.boundaries.componentVariables.includes(name))) styleReport(declaration, `${ownLayer === "review" ? "Review reads private product" : "Pattern reads private component"} variable ${name}.`);
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
return { inspectDependencies, runtimeHelpers, collectComponentStyles, inspectStyles };
|
|
288
|
+
}
|
|
@@ -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
|
};
|
package/dist/lint-config.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export function createLintConfig({ root, config }) {
|
|
|
13
13
|
if (!existsSync(specifier)) throw new Error(`Project plugin not found: ${plugin.path}`);
|
|
14
14
|
return { name: plugin.name, specifier };
|
|
15
15
|
});
|
|
16
|
-
const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
|
|
16
|
+
const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, publicEntry: config.publicEntry, reviewEntry: config.reviewEntry, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
|
|
17
17
|
configurationFiles: [...configurationFiles({ root, config, sourceRoot: path.resolve(root,config.sourceDir), dsRoot: path.resolve(root,config.dsDir) })].sort() };
|
|
18
18
|
return {
|
|
19
19
|
jsPlugins: [{ name: "ads", specifier: fileURLToPath(new URL("./oxlint.mjs", import.meta.url)) }, ...projectPlugins],
|
|
@@ -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.
|
|
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];
|
|
@@ -24,6 +24,7 @@ export function projectContext(options) {
|
|
|
24
24
|
const guides = [...projectGuideFiles(root), path.join(config.dsDir, "README.md"), path.join(config.dsDir, "readme.md"), path.join(config.dsDir, "SKILL.md")].filter((file) => existsSync(path.join(root, file))).map((file) => ({ file: file.replaceAll("\\", "/"), uri: sourceUri(file.replaceAll("\\", "/")) }));
|
|
25
25
|
return { name: pkg.name ?? path.basename(root), projectRoot: root, dsRoot, sourceRoot, framework: config.framework,
|
|
26
26
|
publicEntry: path.join(dsRoot, config.publicEntry), publicEntryUri: existsSync(path.join(dsRoot, config.publicEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.publicEntry)).replaceAll("\\", "/")) : null,
|
|
27
|
+
review: { importFrom: `${config.publicAlias}/review`, entry: path.join(dsRoot, config.reviewEntry), sourceUri: existsSync(path.join(dsRoot, config.reviewEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.reviewEntry)).replaceAll("\\", "/")) : null, developmentOnly: true },
|
|
27
28
|
stylesEntry: path.join(dsRoot, config.stylesEntry), packageManager, commands, guides,
|
|
28
29
|
showcase: { client: "tool-owned-mcp", url: config.mcp.showcaseUrl, title: config.mcp.showcaseTitle ?? `${pkg.name ?? path.basename(root)} · Design System` },
|
|
29
30
|
runtime: { node: process.execPath, nodeBinDir: path.dirname(process.execPath), nodeVersion: process.version,
|
|
@@ -56,8 +57,10 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
|
|
|
56
57
|
3. Select one workflow with ads_skills, then ads_read_skill. ${route.skills.map((skill) => `${skill.task}: ${skill.name}`).join("; ")}. Native skill activation is host-controlled. Relative skill references resolve from its SKILL.md URI. Read support files only when relevant.
|
|
57
58
|
4. ${route.preview} Token samples: ads_scenarios(kind: tokens), then ads_show_scenario or ads_show_tokens(category: id). Token source inspection: ads_tokens with a focused query. Registered previews: ${previews.map((preview) => `${preview.id}: ${preview.tool ?? "ads_show_scenario"}`).join(", ") || "none"}. Showcase URL: ${context.showcase.url ?? "not configured; use the project's showcase command"}.
|
|
58
59
|
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
|
-
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
|
+
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/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for guarded owners, aliases and migration. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
|
|
60
61
|
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}.
|
|
62
|
+
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.
|
|
63
|
+
|
|
61
64
|
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
65
|
|
|
63
66
|
Project guide resources: ${context.guides.map((guide) => `${guide.file}: ${guide.uri}`).join("; ") || "none discovered"}.
|
|
@@ -30,9 +30,9 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
|
|
|
30
30
|
const owner = inventory.units.find(entry => entry.id === unit);
|
|
31
31
|
const files = [...new Set([...(owner?.files ?? []), ...(owner?.companionFiles ?? []).map(entry => entry.path),
|
|
32
32
|
...(!owner ? ["ds.config.json"] : []),
|
|
33
|
-
...(/^(?:components|patterns|controllers)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,options.config.publicEntry)).replaceAll("\\","/")] : [])])];
|
|
34
|
-
if (!owner && !/^(?:components|patterns|controllers|composition)\/.+/.test(unit)) throw new Error("Discover an existing unit or choose a new unoccupied component/pattern/controller/composition owner.");
|
|
35
|
-
if (/^(?:components|patterns|controllers|composition)\//.test(unit)) for (let directory=unit;directory!==".";directory=path.posix.dirname(directory)) {
|
|
33
|
+
...(/^(?:components|patterns|controllers|review)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,unit.startsWith("review/")?options.config.reviewEntry:options.config.publicEntry)).replaceAll("\\","/")] : [])])];
|
|
34
|
+
if (!owner && !/^(?:components|patterns|controllers|composition|review)\/.+/.test(unit)) throw new Error("Discover an existing unit or choose a new unoccupied component/pattern/controller/composition/review owner.");
|
|
35
|
+
if (/^(?:components|patterns|controllers|composition|review)\//.test(unit)) for (let directory=unit;directory!==".";directory=path.posix.dirname(directory)) {
|
|
36
36
|
const marker=path.join(options.dsRoot,directory,".gitkeep");
|
|
37
37
|
if (existsSync(marker) && realpathSync(marker)===marker && lstatSync(marker).isFile() && lstatSync(marker).size===0) {
|
|
38
38
|
const relative=path.relative(options.root,marker).replaceAll("\\","/");
|
|
@@ -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
|
+
}
|