@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
|
@@ -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/behaviors.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { isBuiltin } from "node:module";
|
|
4
4
|
import ts from "typescript";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles } from "./config.mjs";
|
|
6
|
+
import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles, isReviewFile } from "./config.mjs";
|
|
7
7
|
import { importResolver, projectCompilerOptions } from "./boundaries.mjs";
|
|
8
8
|
import { inspectUnits } from "./units.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { scenarioApi } from "./public-api.mjs";
|
|
10
10
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
11
11
|
import { uiOperation } from "./ui-framework.mjs";
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
31
31
|
if (!files.length) return { contracts: [], diagnostics: [] };
|
|
32
32
|
const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
|
|
33
33
|
const checker = program.getTypeChecker();
|
|
34
|
-
const apis = new Map(
|
|
34
|
+
const apis = new Map(scenarioApi(program, options).map(item => [item.name, item]));
|
|
35
35
|
const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
|
|
36
36
|
const diagnostics = [], contracts = [], owners = new Set();
|
|
37
37
|
const relative = file => path.relative(root, file).replaceAll("\\", "/");
|
|
@@ -45,7 +45,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
45
45
|
value = parsed.data;
|
|
46
46
|
} catch (error) { report(error.message); continue; }
|
|
47
47
|
const unit = inventory.owner(inventory.relative(relative(file)));
|
|
48
|
-
if (!["components", "patterns", "controllers"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern or
|
|
48
|
+
if (!["components", "patterns", "controllers", "review"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern, controller or development review unit."); continue; }
|
|
49
49
|
if (owners.has(unit)) { report(`Only one behavior contract may describe ${unit}.`); continue; }
|
|
50
50
|
owners.add(unit);
|
|
51
51
|
function owned(target, field) {
|
|
@@ -61,7 +61,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
|
|
|
61
61
|
if (!api || inventory.owner(inventory.relative(relative(api.file))) !== unit) report(`Subject ${name} must resolve to a public API owned by ${unit}: ${entry.api}`);
|
|
62
62
|
} else {
|
|
63
63
|
const source = owned(entry.source, `Subject ${name}`);
|
|
64
|
-
if (source && isDevelopmentFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
|
|
64
|
+
if (source && isDevelopmentFile(source, options) && !isReviewFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
|
|
65
65
|
if (source && entry.symbol) {
|
|
66
66
|
const ast = program.getSourceFile(source);
|
|
67
67
|
if (!ast || !checker.getSymbolsInScope(ast, ts.SymbolFlags.Value).some(symbol => symbol.name === entry.symbol && symbol.declarations?.some(declaration => declaration.getSourceFile() === ast))) report(`Subject ${name}: symbol ${entry.symbol} is not declared in ${entry.source}.`);
|
package/dist/bootstrap-ds/ADS.md
CHANGED
|
@@ -72,7 +72,7 @@ layout, component-specific focus rules and hover/disabled skins are rejected.
|
|
|
72
72
|
Shared token definitions belong in value sheets, not `base.css`. No project token
|
|
73
73
|
names are embedded in this policy. A zero-specificity `:where()` wrapper does not
|
|
74
74
|
change ownership. Put component design and animation in Linaria owners, and
|
|
75
|
-
scenario layout variations in typed
|
|
75
|
+
scenario layout variations in typed review-helper props. Browser review
|
|
76
76
|
still checks the effects and accessibility of permitted defaults.
|
|
77
77
|
|
|
78
78
|
Own primitive markup, focus, hover, selected, disabled, loading, and motion in
|
|
@@ -151,7 +151,7 @@ Flat owners may use `Button.scenario.tsx` and `Button.fixture.ts` instead.
|
|
|
151
151
|
`export const scenario = { title: "Button states", covers: ["Button"] };`.
|
|
152
152
|
Token entries under the configured token directory declare
|
|
153
153
|
`export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
|
|
154
|
-
`TokenSamples` is any project-owned
|
|
154
|
+
`TokenSamples` is any project-owned development-only review renderer, not a required name.
|
|
155
155
|
Its colocated fixture declares literal `samples: [{ token, property, probe }]`.
|
|
156
156
|
Each token must exist and have sample coverage; duplicate bindings, missing probes,
|
|
157
157
|
empty covers and value-only inspectors fail. The default entry passes the declared
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Empty native design system
|
|
2
2
|
|
|
3
3
|
This private React/TypeScript workspace contains semantic CSS tokens, a project-owned
|
|
4
|
-
token review
|
|
4
|
+
token review renderer, a public barrel, Linaria extraction and a preview compiler
|
|
5
5
|
adapter. It has no product components, icons, fonts or example screens. The minimal showcase
|
|
6
6
|
shell and MCP client come from ADS tooling.
|
|
7
7
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TokenPreview } from "./token-preview/TokenPreview";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TokenPreview } from "@ds";
|
|
1
|
+
import { TokenPreview } from "@ds/review";
|
|
2
2
|
import { fixture } from "./Foundation.fixture";
|
|
3
3
|
export const scenario = { title: fixture.title, covers: ["TokenPreview"], samples: fixture.samples };
|
|
4
4
|
export default function Scenario() { return <TokenPreview {...fixture} />; }
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"lib": [
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022",
|
|
6
|
+
"DOM",
|
|
7
|
+
"DOM.Iterable"
|
|
8
|
+
],
|
|
5
9
|
"module": "ESNext",
|
|
6
10
|
"moduleResolution": "bundler",
|
|
7
11
|
"skipLibCheck": true,
|
|
@@ -14,7 +18,18 @@
|
|
|
14
18
|
"noUnusedParameters": true,
|
|
15
19
|
"noFallthroughCasesInSwitch": true,
|
|
16
20
|
"baseUrl": ".",
|
|
17
|
-
"paths": {
|
|
21
|
+
"paths": {
|
|
22
|
+
"@ds/review": [
|
|
23
|
+
"design-system/review/index.ts"
|
|
24
|
+
],
|
|
25
|
+
"@ds": [
|
|
26
|
+
"design-system/index.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
18
29
|
},
|
|
19
|
-
"include": [
|
|
30
|
+
"include": [
|
|
31
|
+
"src",
|
|
32
|
+
"design-system",
|
|
33
|
+
"vite.config.ts"
|
|
34
|
+
]
|
|
20
35
|
}
|
|
@@ -7,7 +7,7 @@ import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
|
7
7
|
export default defineConfig({
|
|
8
8
|
plugins: [wyw({ tagResolver: linariaTagResolver }), react()],
|
|
9
9
|
resolve: {
|
|
10
|
-
alias: { "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
10
|
+
alias: { "@ds/review": path.resolve(import.meta.dirname, "design-system/review/index.ts"), "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
11
11
|
},
|
|
12
12
|
build: {
|
|
13
13
|
outDir: "dist",
|
package/dist/boundaries.mjs
CHANGED
|
@@ -5,14 +5,16 @@ import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./conf
|
|
|
5
5
|
import { isSharedToken } from "./token-policy.mjs";
|
|
6
6
|
import { scopedTokenOwner } from "./scoped-tokens.mjs";
|
|
7
7
|
|
|
8
|
-
export function projectCompilerOptions({ root }) {
|
|
8
|
+
export function projectCompilerOptions({ root, config, dsRoot }) {
|
|
9
9
|
const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
10
10
|
let compiler = {};
|
|
11
11
|
if (file && isInside(root, file)) {
|
|
12
12
|
const parsed = ts.readConfigFile(file, ts.sys.readFile);
|
|
13
13
|
if (!parsed.error) compiler = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file)).options;
|
|
14
14
|
}
|
|
15
|
-
return compiler
|
|
15
|
+
return config && dsRoot ? { ...compiler, paths: { ...compiler.paths,
|
|
16
|
+
[config.publicAlias]: [path.join(dsRoot, config.publicEntry)],
|
|
17
|
+
[`${config.publicAlias}/review`]: [path.join(dsRoot, config.reviewEntry)] } } : compiler;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function importResolver(options) {
|
package/dist/check.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import postcss from "postcss";
|
|
5
5
|
import valueParser from "postcss-value-parser";
|
|
6
|
-
import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile } from "./config.mjs";
|
|
6
|
+
import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile, isReviewFile } from "./config.mjs";
|
|
7
7
|
import { inspectTokens, references } from "./tokens.mjs";
|
|
8
8
|
import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-policy.mjs";
|
|
9
9
|
import { inspectAssets } from "./assets.mjs";
|
|
@@ -24,8 +24,10 @@ import { createScenarioTrace } from "./scenario-trace.mjs";
|
|
|
24
24
|
import { createScenarioSlots } from "./scenario-slots.mjs";
|
|
25
25
|
import { createScenarioSelectors } from "./scenario-selectors.mjs";
|
|
26
26
|
import { architectureEvidence, failure } from "./evidence.mjs";
|
|
27
|
+
import { propertyRegistration } from "./property-registrations.mjs";
|
|
28
|
+
import { createLayerBoundaries, layerSourceFiles } from "./layers.mjs";
|
|
27
29
|
|
|
28
|
-
const scenarioLayoutGuidance = "Put demo-only sizing and framing in
|
|
30
|
+
const scenarioLayoutGuidance = "Put demo-only sizing and framing in the development-only review/ layer, imported through the separate review entry. Patterns are production UI. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
|
|
29
31
|
const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
|
|
30
32
|
|
|
31
33
|
const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
|
|
@@ -51,16 +53,19 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
51
53
|
const rootHtml = path.join(root, "index.html");
|
|
52
54
|
const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
|
|
53
55
|
const boundaryFiles = [...config.inlineStyleFiles, ...config.boundaries.styleWrites.map(entry => entry.file)].map(file => path.resolve(root, file)).filter(file => existsSync(file) && statSync(file).isFile());
|
|
54
|
-
const scriptFiles = [...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file));
|
|
56
|
+
const scriptFiles = layerSourceFiles([...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file)), options);
|
|
55
57
|
const compiler = projectCompilerOptions(options);
|
|
56
58
|
const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
|
|
57
59
|
const checker = program.getTypeChecker();
|
|
58
60
|
const atomicCompositionReview = createAtomicCompositionReview(program, options);
|
|
59
61
|
const scenarioTrace = createScenarioTrace(program, options);
|
|
60
62
|
let scenarioSlot, scenarioSelector;
|
|
61
|
-
function report(file, rule, message, line = 1, column = 1) {
|
|
62
|
-
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
|
|
63
|
+
function report(file, rule, message, line = 1, column = 1, detail = {}) {
|
|
64
|
+
diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column, ...detail });
|
|
63
65
|
}
|
|
66
|
+
const layers = createLayerBoundaries(program, options, report), layerStyles = [];
|
|
67
|
+
layers.inspectDependencies();
|
|
68
|
+
for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
|
|
64
69
|
inspectStyleBoundaries(options, program, report);
|
|
65
70
|
inspectShowcasePages(options, program, resolveImport, report);
|
|
66
71
|
inspectShowcaseCoverage(options, program, report);
|
|
@@ -70,6 +75,30 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
70
75
|
inspectControllers(options, program, report, behaviors.contracts);
|
|
71
76
|
// The foundation remains a fixed import-only entry, not an extra styling layer.
|
|
72
77
|
const foundationEntry = path.join(dsRoot, config.stylesEntry);
|
|
78
|
+
const registeredProperties=new Map();
|
|
79
|
+
function checkRegistrations(file,css,offset={line:1}){
|
|
80
|
+
css.walkAtRules(/^property$/i,rule=>{
|
|
81
|
+
const line=offset.line+(rule.source?.start.line??1)-1;
|
|
82
|
+
try{
|
|
83
|
+
const registration=propertyRegistration(rule),relative=scoped.inventory.relative(path.relative(root,file));
|
|
84
|
+
const owner=scoped.inventory.owner(relative),foundation=isInside(path.join(dsRoot,config.tokenDir),file);
|
|
85
|
+
if(foundation){
|
|
86
|
+
let defined=false;css.walkDecls(registration.name,()=>{defined=true;});
|
|
87
|
+
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.");
|
|
88
|
+
}else{
|
|
89
|
+
if(!owner||!/^(?:components|patterns|review)\//.test(owner))throw new Error("@property belongs to its component/pattern/review implementation or real token value sheet, never application, scenario, controller or shared reset styling.");
|
|
90
|
+
const violation=checkComponentVariable(registration.name,file,options);if(violation)throw new Error(violation.message);
|
|
91
|
+
}
|
|
92
|
+
for(const descriptor of rule.nodes??[])if(descriptor.type==="decl")for(const ref of references(descriptor.value)){
|
|
93
|
+
const tokenOwner=scopedTokenOwner(options,ref.name);
|
|
94
|
+
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.`);
|
|
95
|
+
}
|
|
96
|
+
const previous=registeredProperties.get(registration.name);
|
|
97
|
+
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.`);
|
|
98
|
+
registeredProperties.set(registration.name,{...registration,owner,file});
|
|
99
|
+
}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";}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
73
102
|
try {
|
|
74
103
|
const css = postcss.parse(readFileSync(foundationEntry, "utf8"), { from: foundationEntry });
|
|
75
104
|
const imports = css.nodes.filter((node) => node.type !== "comment");
|
|
@@ -81,8 +110,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
81
110
|
let css;
|
|
82
111
|
try { css = postcss.parse(text, { from: file }); }
|
|
83
112
|
catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
layers.collectComponentStyles(file, css);
|
|
114
|
+
layerStyles.push({ file, css, offset });
|
|
115
|
+
checkRegistrations(file,css,offset);
|
|
116
|
+
if (!foundation && !isReviewFile(file, options) && (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/patterns or development review implementations.");
|
|
117
|
+
if (isDevelopmentFile(file, options) && !isReviewFile(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);
|
|
86
118
|
css.walkAtRules("import", (rule) => {
|
|
87
119
|
if (foundation) report(file, "foundation-import", "Import foundation sheets through the configured styles entry. Token sheets cannot import an unchecked styling layer.", rule.source.start.line);
|
|
88
120
|
const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
@@ -100,7 +132,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
100
132
|
}
|
|
101
133
|
if (isInside(path.join(dsRoot, "showcase"), file) && /:(?:hover|active|focus(?:-visible|-within)?|checked|disabled|enabled)\b|\[(?:aria-(?:selected|pressed|checked|disabled|expanded)|data-state)\b/.test(rule.selector)) report(file, "showcase-state", "Interactive state styling belongs to the real ADS component or pattern, not showcase or mock styles.", offset.line + rule.source.start.line - 1);
|
|
102
134
|
if (/\.ads-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "components"), file)) report(file, "style-ownership", "ADS component internals are owned by components, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
|
|
103
|
-
if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or
|
|
135
|
+
if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or scenario CSS.", offset.line + rule.source.start.line - 1);
|
|
104
136
|
if (/\.showcase-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "showcase"), file)) report(file, "style-ownership", "Showcase internals are owned by showcase scaffolding.", offset.line + rule.source.start.line - 1);
|
|
105
137
|
});
|
|
106
138
|
if (foundation) css.walkAtRules(rule => {
|
|
@@ -108,12 +140,13 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
108
140
|
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);
|
|
109
141
|
});
|
|
110
142
|
css.walkDecls((decl) => {
|
|
143
|
+
const propertyDescriptor=decl.parent.type==="atrule"&&decl.parent.name.toLowerCase()==="property";
|
|
111
144
|
if (foundation && decl.prop.startsWith("--")) {
|
|
112
145
|
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);
|
|
113
146
|
return; // Definition names, values and aliases are validated by inspectTokens.
|
|
114
147
|
}
|
|
115
148
|
const fontDescriptor = foundation && path.basename(file) === "fonts.css" && decl.parent.type === "atrule" && decl.parent.name === "font-face";
|
|
116
|
-
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);
|
|
149
|
+
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);
|
|
117
150
|
if (foundation && path.basename(file) === "base.css" && decl.parent.type === "rule") {
|
|
118
151
|
const violation = foundationDeclaration(decl.parent, decl);
|
|
119
152
|
if (violation) report(file, "foundation-role", violation, decl.source.start.line, decl.source.start.column);
|
|
@@ -131,7 +164,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
131
164
|
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);
|
|
132
165
|
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);
|
|
133
166
|
}
|
|
134
|
-
if (fontDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
167
|
+
if (fontDescriptor || propertyDescriptor) return; // Font metadata defines source assets, not token-backed UI styles.
|
|
135
168
|
const violations = new Set();
|
|
136
169
|
valueParser(decl.value).walk((node) => {
|
|
137
170
|
if (node.type === "function" && node.value === "url") return false;
|
|
@@ -146,7 +179,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
146
179
|
});
|
|
147
180
|
}
|
|
148
181
|
for (const file of files) {
|
|
149
|
-
if (scopedFiles.has(file)) continue; // Closed definition-only contract checked by inspectTokens.
|
|
182
|
+
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.
|
|
150
183
|
if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
|
|
151
184
|
if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
|
|
152
185
|
if (file.endsWith(".css")) {
|
|
@@ -163,7 +196,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
163
196
|
report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
|
|
164
197
|
}
|
|
165
198
|
const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
|
|
166
|
-
const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
|
|
199
|
+
const showcase = isDevelopmentFile(file, options) && !isTestFile(file) && !isReviewFile(file, options);
|
|
167
200
|
function nodeReport(node, rule, message, detail) {
|
|
168
201
|
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
169
202
|
report(file, rule, message, position.line + 1, position.character + 1);
|
|
@@ -207,8 +240,8 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
207
240
|
function inspectClass(value) {
|
|
208
241
|
if (ts.isStringLiteralLike(value)) {
|
|
209
242
|
if (/(?:^|\s)ads-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "components"), file)) nodeReport(value, "style-ownership", "Primitive ads-* classes must be emitted by components, not consumers.");
|
|
210
|
-
if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
|
|
211
|
-
if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by
|
|
243
|
+
if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
|
|
244
|
+
if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by development review code, not runtime implementations.");
|
|
212
245
|
}
|
|
213
246
|
ts.forEachChild(value, inspectClass);
|
|
214
247
|
}
|
|
@@ -268,7 +301,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
268
301
|
}
|
|
269
302
|
const mutation = styleWriteViolation(node, file, options);
|
|
270
303
|
if (mutation) nodeReport(node, "style-mutation", mutation);
|
|
271
|
-
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a
|
|
304
|
+
if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
|
|
272
305
|
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
|
|
273
306
|
inspectClass(node.initializer);
|
|
274
307
|
}
|
|
@@ -283,6 +316,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
|
|
|
283
316
|
visit(source);
|
|
284
317
|
}
|
|
285
318
|
// Bounded clients should see actionable warnings before source-analysis limits.
|
|
319
|
+
for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
|
|
286
320
|
advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
|
|
287
321
|
return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
|
|
288
322
|
}
|
package/dist/config.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export const defaultConfig = {
|
|
|
11
11
|
dsDir: "design-system",
|
|
12
12
|
publicAlias: "@ds",
|
|
13
13
|
publicEntry: "index.ts",
|
|
14
|
+
reviewEntry: "review/index.ts",
|
|
14
15
|
stylesEntry: "styles.css",
|
|
15
16
|
tokenDir: "tokens",
|
|
16
17
|
inlineStyleFiles: [],
|
|
@@ -54,7 +55,8 @@ export function loadConfig(directory = process.cwd()) {
|
|
|
54
55
|
if (!Array.isArray(config.inlineStyleFiles)) throw new Error("inlineStyleFiles must be an array of relative paths.");
|
|
55
56
|
const sourceRoot = projectPath(root, config.sourceDir, "sourceDir");
|
|
56
57
|
const dsRoot = projectPath(root, config.dsDir, "dsDir");
|
|
57
|
-
for (const field of ["publicEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
|
|
58
|
+
for (const field of ["publicEntry", "reviewEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
|
|
59
|
+
if (!isInside(path.join(dsRoot, "review"), path.resolve(dsRoot, config.reviewEntry)) || path.resolve(dsRoot, config.reviewEntry) === path.join(dsRoot, "review") || !/\.[cm]?[jt]sx?$/.test(config.reviewEntry)) throw new Error("reviewEntry must name a script entry inside the ADS review/ development layer.");
|
|
58
60
|
for (const file of config.inlineStyleFiles) projectPath(root, file, "inlineStyleFiles");
|
|
59
61
|
if (!Array.isArray(config.boundaries.componentVariables) || config.boundaries.componentVariables.some((name) => typeof name !== "string" || !/^--ds-[\w-]+$/.test(name) || isStateVariable(name))) throw new Error("boundaries.componentVariables must name non-state --ds-* customization variables.");
|
|
60
62
|
if (!Array.isArray(config.boundaries.styleWrites)) throw new Error("boundaries.styleWrites must be an array.");
|
|
@@ -165,11 +167,16 @@ export function isDevelopmentFile(file, { dsRoot }) {
|
|
|
165
167
|
if (!isInside(dsRoot, file)) return isTestFile(file);
|
|
166
168
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
167
169
|
return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
|
|
168
|
-
|| /^(?:showcase|composition)\//.test(relative)
|
|
170
|
+
|| /^(?:showcase|composition|review)\//.test(relative)
|
|
169
171
|
|| /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
|
|
170
172
|
|| /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
/** Review implementations may own demo presentation; scenarios/fixtures may not. */
|
|
176
|
+
export function isReviewFile(file, { dsRoot }) {
|
|
177
|
+
return isInside(path.join(dsRoot, "review"), file) && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
|
|
174
181
|
|
|
175
182
|
export function sourceFiles(root) {
|
|
@@ -188,6 +195,7 @@ export function importViolation(specifier, filename, options, resolve) {
|
|
|
188
195
|
const { root, dsRoot, config } = options;
|
|
189
196
|
// ADS implementation code needs its private imports; the showcase is a consumer.
|
|
190
197
|
if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
|
|
198
|
+
if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
|
|
191
199
|
if (specifier.startsWith(`${config.publicAlias}/`)) return true;
|
|
192
200
|
if (specifier === config.publicAlias) return false;
|
|
193
201
|
const imported = resolve ? resolve(specifier, filename)
|
|
@@ -195,6 +203,9 @@ export function importViolation(specifier, filename, options, resolve) {
|
|
|
195
203
|
: specifier.startsWith(`${config.dsDir}/`) ? [path.resolve(root, specifier)] : [];
|
|
196
204
|
return imported.some((file) => {
|
|
197
205
|
if (!isInside(dsRoot, file)) return false;
|
|
206
|
+
// Review implementations can reuse local foundation helpers. Resolved public
|
|
207
|
+
// UI/controller contracts are enforced by the layer inspector, as for patterns.
|
|
208
|
+
if (isReviewFile(filename, options)) return false;
|
|
198
209
|
// Owner-local white-box tests may exercise private helpers without making
|
|
199
210
|
// those helpers public API. Scenarios remain public-API consumers.
|
|
200
211
|
if (isTestFile(filename) && isInside(dsRoot, filename) && isInside(path.dirname(filename), file)) return false;
|
package/dist/init.mjs
CHANGED
|
@@ -153,15 +153,15 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
153
153
|
});
|
|
154
154
|
if (!existingTokens.length) {
|
|
155
155
|
for (const name of ["Foundation.scenario.tsx", "Foundation.fixture.ts"]) {
|
|
156
|
-
const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds"', JSON.stringify(config.publicAlias));
|
|
156
|
+
const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds/review"', JSON.stringify(`${config.publicAlias}/review`)).replaceAll('"@ds"', JSON.stringify(config.publicAlias));
|
|
157
157
|
addMissing(path.join(tokenRoot, name), content);
|
|
158
158
|
}
|
|
159
|
-
for (const name of ["TokenPreview.tsx"
|
|
160
|
-
let content = readFileSync(path.join(starter, "design-system/
|
|
159
|
+
for (const name of ["TokenPreview.tsx"]) {
|
|
160
|
+
let content = readFileSync(path.join(starter, "design-system/review/token-preview", name), "utf8");
|
|
161
161
|
if (selectedFramework === "preact") content = content.replace('from "react"', 'from "preact/hooks"');
|
|
162
|
-
addMissing(path.join(dsRoot, "
|
|
162
|
+
addMissing(path.join(dsRoot, "review/token-preview", name), content.replaceAll('"@ds"', JSON.stringify(config.publicAlias)));
|
|
163
163
|
}
|
|
164
|
-
const reviewFile = path.relative(root, path.join(dsRoot, "
|
|
164
|
+
const reviewFile = path.relative(root, path.join(dsRoot, "review/token-preview/TokenPreview.tsx")).replaceAll("\\", "/");
|
|
165
165
|
if (!config.boundaries.styleWrites.some(item => item.file === reviewFile)) config.boundaries.styleWrites.push({ file: reviewFile, properties: ["--ux-token-preview-value"] });
|
|
166
166
|
}
|
|
167
167
|
tokenFiles.sort((a, b) => foundationFiles.indexOf(path.basename(a)) - foundationFiles.indexOf(path.basename(b)) || a.localeCompare(b));
|
|
@@ -172,16 +172,19 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
|
|
|
172
172
|
};
|
|
173
173
|
addMissing(stylesFile, tokenFiles.map((file) => `@import ${JSON.stringify(relativeImport(stylesFile, file))};`).join("\n") + "\n");
|
|
174
174
|
const entryFile = path.join(dsRoot, config.publicEntry);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
current =
|
|
175
|
+
addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`);
|
|
176
|
+
if (!existingTokens.length) {
|
|
177
|
+
const reviewEntry = path.join(dsRoot, config.reviewEntry);
|
|
178
|
+
const reviewExport = `export { TokenPreview } from ${JSON.stringify(relativeImport(reviewEntry, path.join(dsRoot, "review/token-preview/TokenPreview")))};\n`;
|
|
179
|
+
addMissing(reviewEntry, reviewExport);
|
|
180
|
+
if (existsSync(reviewEntry) && !readFileSync(reviewEntry, "utf8").includes("token-preview/TokenPreview")) throw new Error("The project owns an existing review entry. Add the new token renderer through that development API before initializing the foundation.");
|
|
181
|
+
if (existsSync(entryFile)) {
|
|
182
|
+
let current = readFileSync(entryFile, "utf8");
|
|
183
|
+
const ast = ts.createSourceFile(entryFile, current, ts.ScriptTarget.Latest, true);
|
|
184
|
+
for (const statement of [...ast.statements].reverse()) if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length) current = current.slice(0, statement.getStart(ast)) + current.slice(statement.end);
|
|
185
|
+
if (!ast.statements.some(statement => ts.isImportDeclaration(statement) || statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword))) current += `\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`;
|
|
186
|
+
if (current !== readFileSync(entryFile, "utf8")) updates.set(entryFile, current);
|
|
183
187
|
}
|
|
184
|
-
updates.set(entryFile, current + "\n" + reviewExport);
|
|
185
188
|
}
|
|
186
189
|
for (const section of ["components", "patterns", "controllers", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
|
|
187
190
|
addMissing(path.join(dsRoot, "assets/brand/README.md"), readFileSync(path.join(starter, "design-system/assets/brand/README.md")));
|
package/dist/investigations.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { compactAcceptance, compactRow, jsonBytes, resultBudgets, resultPage, resultRecord } from "./mcp/result-pages.mjs";
|
|
4
5
|
|
|
5
6
|
export const INVESTIGATIONS_URI = "ads://project/investigations";
|
|
6
7
|
const indexFile = root => path.join(root,".ds/investigations.json");
|
|
@@ -32,7 +33,7 @@ export function saveInvestigations(root, entries, registered) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function investigationIndex(root) {
|
|
35
|
-
return {retention:"latest-per-check",entries:Object.values(saved(root)).map(({unit,check,status,completedAt,inputRevision,resourceUri,failure})=>({unit,check,status,completedAt,inputRevision,resourceUri,...(failure?{failure}:{})}))};
|
|
36
|
+
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}:{})}))};
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function readInvestigation(root,{unit,check}) {
|
|
@@ -41,6 +42,35 @@ export function readInvestigation(root,{unit,check}) {
|
|
|
41
42
|
return {...bundle,artifacts:(bundle.artifacts??[]).map(artifact=>({...artifact,available:!!artifactPath(root,artifact.path)}))};
|
|
42
43
|
}
|
|
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
|
+
|
|
44
74
|
export function investigationArtifacts(root,report,attachments=[]) {
|
|
45
75
|
return attachments.slice(0,5).flatMap(attachment=>{
|
|
46
76
|
if(typeof attachment?.path!=="string")return [];
|
|
@@ -57,9 +87,10 @@ export function investigationArtifacts(root,report,attachments=[]) {
|
|
|
57
87
|
/** Only files explicitly registered by the latest bundles are resource-readable. */
|
|
58
88
|
export function readInvestigationResource(root,uri) {
|
|
59
89
|
if(uri.startsWith("ads://investigations/")) {
|
|
60
|
-
const parts=
|
|
90
|
+
const url=new URL(uri),parts=url.pathname.slice(1).split("/").map(decodeURIComponent);
|
|
61
91
|
if(parts.length!==2)throw new Error("Invalid investigation URI.");
|
|
62
|
-
|
|
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))]))}))};
|
|
63
94
|
}
|
|
64
95
|
const artifact=Object.values(saved(root)).flatMap(bundle=>bundle.artifacts??[]).find(artifact=>artifact.resourceUri===uri);
|
|
65
96
|
const file=artifact&&artifactPath(root,artifact.path);
|