@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.
Files changed (101) hide show
  1. package/dist/atomic-composition.mjs +1 -1
  2. package/dist/behaviors.mjs +5 -5
  3. package/dist/bootstrap-ds/ADS.md +2 -2
  4. package/dist/bootstrap-ds/README.md +1 -1
  5. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  6. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  7. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  8. package/dist/bootstrap-ds/ds.config.json +1 -1
  9. package/dist/bootstrap-ds/tsconfig.json +18 -3
  10. package/dist/bootstrap-ds/vite.config.ts +1 -1
  11. package/dist/boundaries.mjs +4 -2
  12. package/dist/check.mjs +49 -15
  13. package/dist/config.mjs +13 -2
  14. package/dist/init.mjs +17 -14
  15. package/dist/investigations.mjs +34 -3
  16. package/dist/layers.mjs +288 -0
  17. package/dist/linaria/preserve-values.cjs +21 -2
  18. package/dist/lint-config.mjs +1 -1
  19. package/dist/mcp/behavior-pages.mjs +36 -0
  20. package/dist/mcp/context.mjs +5 -2
  21. package/dist/mcp/edit-preparation.mjs +3 -3
  22. package/dist/mcp/execution-results.mjs +146 -0
  23. package/dist/mcp/mutations.mjs +4 -4
  24. package/dist/mcp/prompts.mjs +1 -1
  25. package/dist/mcp/result-pages.mjs +87 -0
  26. package/dist/mcp/server.mjs +49 -30
  27. package/dist/mcp/widget.bundle.js +1 -1
  28. package/dist/oxlint.mjs +3 -1
  29. package/dist/preview-resources.mjs +11 -1
  30. package/dist/property-registrations.mjs +39 -0
  31. package/dist/public-api.mjs +13 -6
  32. package/dist/scenario-trace.mjs +7 -7
  33. package/dist/scenarios-vite.mjs +2 -1
  34. package/dist/scenarios.mjs +10 -7
  35. package/dist/scoped-tokens.mjs +6 -0
  36. package/dist/showcase/client.bundle.js +1 -1
  37. package/dist/showcase/sandbox.bundle.js +1 -1
  38. package/dist/skills/ads-break-ui/SKILL.md +27 -10
  39. package/dist/skills/ads-break-ui/references/atomic-styles.md +48 -0
  40. package/dist/skills/ads-break-ui/references/captures.md +6 -1
  41. package/dist/skills/ads-break-ui/references/evidence.md +52 -5
  42. package/dist/skills/ads-break-ui/references/guarded-editing.md +9 -6
  43. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  44. package/dist/skills/ads-break-ui/references/review.md +96 -0
  45. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  46. package/dist/skills/ads-designer/SKILL.md +28 -11
  47. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  48. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
  49. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  50. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  51. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  52. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  53. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  54. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  55. package/dist/skills/ads-designer/references/atomic-styles.md +48 -0
  56. package/dist/skills/ads-designer/references/captures.md +6 -1
  57. package/dist/skills/ads-designer/references/evidence.md +52 -5
  58. package/dist/skills/ads-designer/references/guarded-editing.md +9 -6
  59. package/dist/skills/ads-designer/references/layers.md +123 -0
  60. package/dist/skills/ads-designer/references/review.md +96 -0
  61. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  62. package/dist/skills/ads-import/SKILL.md +27 -10
  63. package/dist/skills/ads-import/references/atomic-styles.md +48 -0
  64. package/dist/skills/ads-import/references/captures.md +6 -1
  65. package/dist/skills/ads-import/references/evidence.md +52 -5
  66. package/dist/skills/ads-import/references/guarded-editing.md +9 -6
  67. package/dist/skills/ads-import/references/layers.md +123 -0
  68. package/dist/skills/ads-import/references/review.md +96 -0
  69. package/dist/skills/ads-import/references/scenarios.md +24 -3
  70. package/dist/skills/ads-migrate/SKILL.md +27 -10
  71. package/dist/skills/ads-migrate/references/atomic-styles.md +48 -0
  72. package/dist/skills/ads-migrate/references/captures.md +6 -1
  73. package/dist/skills/ads-migrate/references/evidence.md +52 -5
  74. package/dist/skills/ads-migrate/references/guarded-editing.md +9 -6
  75. package/dist/skills/ads-migrate/references/layers.md +123 -0
  76. package/dist/skills/ads-migrate/references/review.md +96 -0
  77. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  78. package/dist/skills/ads-motion/SKILL.md +28 -11
  79. package/dist/skills/ads-motion/references/atomic-styles.md +48 -0
  80. package/dist/skills/ads-motion/references/audit.md +1 -1
  81. package/dist/skills/ads-motion/references/captures.md +6 -1
  82. package/dist/skills/ads-motion/references/evidence.md +52 -5
  83. package/dist/skills/ads-motion/references/guarded-editing.md +9 -6
  84. package/dist/skills/ads-motion/references/layers.md +123 -0
  85. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  86. package/dist/skills/ads-motion/references/review.md +96 -35
  87. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  88. package/dist/units.mjs +17 -14
  89. package/docs/ADS.md +42 -5
  90. package/docs/ARCHITECTURE.md +64 -4
  91. package/docs/BEHAVIORS.md +21 -2
  92. package/docs/CHANGELOG.md +40 -0
  93. package/docs/GUIDE.md +3 -2
  94. package/docs/MCP.md +87 -20
  95. package/package.json +2 -1
  96. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  97. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  98. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  100. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  101. /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
  });
@@ -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 { publicApi } from "./public-api.mjs";
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(publicApi(program, options).map(item => [item.name, item]));
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 controller unit."); continue; }
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}.`);
@@ -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 public review-pattern props. Browser review
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 public review pattern, not a required name.
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 pattern, a public barrel, Linaria extraction and a preview compiler
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
 
@@ -2,4 +2,3 @@
2
2
  import "./styles.css";
3
3
 
4
4
  // Add public component and pattern exports here as they are implemented.
5
- export { TokenPreview } from "./patterns/token-preview/TokenPreview";
@@ -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} />; }
@@ -13,7 +13,7 @@
13
13
  "boundaries": {
14
14
  "styleWrites": [
15
15
  {
16
- "file": "design-system/patterns/token-preview/TokenPreview.tsx",
16
+ "file": "design-system/review/token-preview/TokenPreview.tsx",
17
17
  "properties": [
18
18
  "--ux-token-preview-value"
19
19
  ]
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
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": { "@ds": ["design-system/index.ts"] }
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": ["src", "design-system", "vite.config.ts"]
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",
@@ -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 a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
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
- if (!foundation && (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry)) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
85
- if (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", `Scenarios and fixtures define no styles. ${scenarioLayoutGuidance}`, offset.line);
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 showcase CSS.", offset.line + rule.source.start.line - 1);
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 the showcase, not runtime implementations.");
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 public review pattern.");
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", "Scenario.tsx", "fixture.ts"]) {
160
- let content = readFileSync(path.join(starter, "design-system/patterns/token-preview", name), "utf8");
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, "patterns/token-preview", name), content.replaceAll('"@ds"', JSON.stringify(config.publicAlias)));
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, "patterns/token-preview/TokenPreview.tsx")).replaceAll("\\", "/");
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
- const reviewExport = !existingTokens.length ? `export { TokenPreview } from ${JSON.stringify(relativeImport(entryFile, path.join(dsRoot, "patterns/token-preview/TokenPreview")))};\n` : "";
176
- addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n${reviewExport}`);
177
- if (reviewExport && existsSync(entryFile) && !readFileSync(entryFile, "utf8").includes("patterns/token-preview/TokenPreview")) {
178
- let current = readFileSync(entryFile, "utf8");
179
- if (/\bTokenPreview\b/.test(current)) throw new Error("TokenPreview is already owned by this project. Add a token review scenario using its public API before initializing the foundation.");
180
- const ast = ts.createSourceFile(entryFile, current, ts.ScriptTarget.Latest, true);
181
- for (const statement of [...ast.statements].reverse()) if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length) {
182
- current = current.slice(0, statement.getStart(ast)) + current.slice(statement.end);
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")));
@@ -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=uri.slice("ads://investigations/".length).split("/").map(decodeURIComponent);
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
- return {uri,mimeType:"application/json",text:JSON.stringify(readInvestigation(root,{unit:parts[0],check:parts[1]}))};
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);