@reqvire-org/ads 0.3.20 → 0.3.21

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 (55) hide show
  1. package/README.md +2 -0
  2. package/dist/behaviors.mjs +14 -4
  3. package/dist/bootstrap-ds/README.md +1 -1
  4. package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
  5. package/dist/bootstrap-ds/tsconfig.build.json +9 -2
  6. package/dist/boundaries.mjs +2 -0
  7. package/dist/check.mjs +25 -5
  8. package/dist/config.mjs +21 -4
  9. package/dist/contract-migration.mjs +110 -0
  10. package/dist/contracts.d.mts +22 -0
  11. package/dist/evidence.mjs +1 -1
  12. package/dist/index.mjs +1 -0
  13. package/dist/init.mjs +1 -1
  14. package/dist/mcp/catalog.mjs +9 -2
  15. package/dist/mcp/context.mjs +1 -1
  16. package/dist/mcp/edit-preparation.mjs +6 -3
  17. package/dist/mcp/preview-widget.bundle.js +41 -39
  18. package/dist/mcp/server.mjs +3 -1
  19. package/dist/paint-contracts.mjs +124 -0
  20. package/dist/preview-resources.mjs +1 -1
  21. package/dist/public-props.mjs +2 -2
  22. package/dist/runtime-contracts.mjs +225 -0
  23. package/dist/scenario-trace.mjs +32 -4
  24. package/dist/scenarios-vite.mjs +1 -1
  25. package/dist/scenarios.mjs +82 -26
  26. package/dist/scoped-tokens.mjs +9 -4
  27. package/dist/skills/ads-break-ui/SKILL.md +4 -0
  28. package/dist/skills/ads-break-ui/references/contracts.md +250 -0
  29. package/dist/skills/ads-designer/SKILL.md +4 -0
  30. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
  31. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
  32. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
  33. package/dist/skills/ads-designer/references/contracts.md +250 -0
  34. package/dist/skills/ads-import/SKILL.md +4 -0
  35. package/dist/skills/ads-import/references/contracts.md +250 -0
  36. package/dist/skills/ads-migrate/SKILL.md +4 -0
  37. package/dist/skills/ads-migrate/references/contracts.md +250 -0
  38. package/dist/skills/ads-motion/SKILL.md +4 -0
  39. package/dist/skills/ads-motion/references/contracts.md +250 -0
  40. package/dist/token-origins.mjs +24 -0
  41. package/dist/tokens.mjs +4 -3
  42. package/dist/unit-contracts.mjs +203 -0
  43. package/dist/units.mjs +9 -3
  44. package/dist/verification/browser.mjs +156 -26
  45. package/dist/verification/playwright-reporter.cjs +13 -3
  46. package/docs/ADS.md +8 -1
  47. package/docs/ARCHITECTURE.md +36 -0
  48. package/docs/BEHAVIORS.md +8 -0
  49. package/docs/CHANGELOG.md +19 -0
  50. package/docs/CONTRACTS.md +250 -0
  51. package/docs/GUIDE.md +26 -1
  52. package/docs/MCP.md +15 -0
  53. package/package.json +4 -1
  54. /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
  55. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
@@ -126,6 +126,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
126
126
  { uri: "ads://guides/atomic-styles", name: "Atomic styles: composition conflicts and custom-property extraction", mimeType: "text/markdown" },
127
127
  { uri: "ads://guides/scenarios", name: "Scenario coverage and public class-helper contracts", mimeType: "text/markdown" },
128
128
  { uri: "ads://guides/layers", name: "Component capabilities, pattern composition and dependency ownership", mimeType: "text/markdown" },
129
+ { uri: "ads://guides/contracts", name: "Static owner contracts, typed paints and guarded migration", mimeType: "text/markdown" },
129
130
  { uri: "ads://guides/review", name: "Development-only review UI, token renderers and migration", mimeType: "text/markdown" },
130
131
  { uri: "ads://guides/captures", name: "On-demand captures and candidate visual review", mimeType: "text/markdown" },
131
132
  { uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
@@ -173,6 +174,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
173
174
  if (uri === "ads://guides/atomic-styles") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/atomic-styles.md", import.meta.url), "utf8") }] };
174
175
  if (uri === "ads://guides/scenarios") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/scenarios.md", import.meta.url), "utf8") }] };
175
176
  if (uri === "ads://guides/layers") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/layers.md", import.meta.url), "utf8") }] };
177
+ if (uri === "ads://guides/contracts") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/contracts.md", import.meta.url), "utf8") }] };
176
178
  if (uri === "ads://guides/review") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/review.md", import.meta.url), "utf8") }] };
177
179
  if (uri === "ads://guides/captures") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/captures.md", import.meta.url), "utf8") }] };
178
180
  if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
@@ -318,7 +320,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
318
320
  return json({ total: matches.length, units: matches.slice(0, limit).map(item => ({ id: item.id, kind: item.kind, fileCount: item.files.length, ...(unit ? { files: item.files.slice(0, 200), filesTruncated: item.files.length > 200, ...(item.companionFiles ? { companionFiles: item.companionFiles } : {}), ...(item.filePolicy ? { filePolicy: item.filePolicy } : {}) } : {}) })) });
319
321
  } },
320
322
  { name: "ads_read_file", description: "Read a bounded range of ADS or registered configuration/extension source. Returns unit, full-file digest, startLine and nextOffset; default/max is 12000 UTF-16 characters. Binary assets return byte count/digest and no encoded content. Paths come from ads_units or discovery. Missing new paths return digest null. Read only relevant context before preparing a Git patch.", schema: z.object({ path: z.string().min(1).max(500), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(mutationLimits.readChars).default(mutationLimits.readChars) }).strict(), call: ({ path: file, ...range }) => json(edits.read(file, range)) },
321
- ...(mutations ? [{ name: "ads_prepare_patch", description: "Prepare a Git patch without a separate project mirror. Create copies only one owner plus its declared companions into a local draft; edit that directory with local file tools. Diff checks baseline/source freshness and owner paths, then writes a patchFile under .ds/patches. Discard removes the draft. Live source is never changed; ads_apply_patch still validates all gates. Remote clients can continue sending inline Git diffs instead.", schema:z.object({unit:z.string().min(1).max(256),action:z.enum(["create","diff","discard"]).default("create"),draft:z.string().max(100).optional()}).strict(), call:args=>json(prepareUnitPatch(options,args)) },
323
+ ...(mutations ? [{ name: "ads_prepare_patch", description: "Prepare a Git patch without a separate project mirror. Create copies only one owner plus its declared companions into a local draft; edit that directory with local file tools. Diff checks baseline/source freshness and owner paths, then writes a patchFile under .ds/patches. Migrate prepares mechanical .ads. role names and legacy contracts in that draft, retaining unresolved sections for review. Discard removes the draft. Live source is never changed; ads_apply_patch still validates all gates. Remote clients can continue sending inline Git diffs instead.", schema:z.object({unit:z.string().min(1).max(256),action:z.enum(["create","migrate","diff","discard"]).default("create"),draft:z.string().max(100).optional()}).strict(), call:args=>json(prepareUnitPatch(options,args)) },
322
324
  {name:"ads_prepare_token_samples",description:"Reconcile a token scenario's consumed literal fixture samples. Removes undeclared tokens, preserves existing project bindings, and accepts explicit real-token/property/probe bindings. Reports unresolved token names; never guesses CSS meaning from token names. Returns an owner patchFile without changing source. Apply it through ads_apply_patch and review the real preview.",schema:z.object({id:z.string().min(1).max(256),bindings:z.array(z.object({token:z.string().max(256),property:z.string().max(100),probe:z.string().max(1000)}).passthrough()).max(1000).optional()}).strict(),call:args=>{
323
325
  const {patch,...result}=reconcileTokenSamples(options,args);
324
326
  return json({...result,removed:result.removed.slice(0,50),unresolved:result.unresolved.slice(0,50),unresolvedCount:result.unresolved.length,...storePreparedPatch(options,patch)});
@@ -0,0 +1,124 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import valueParser from "postcss-value-parser";
4
+ import { isContractFile, isInside } from "./config.mjs";
5
+ import { inspectUnitContracts, implementationIdentity, runtimeBindingOwner } from "./unit-contracts.mjs";
6
+ import { createSourceValues, reference } from "./source-values.mjs";
7
+
8
+ export function paintReference(text) {
9
+ if (typeof text !== "string" || text.length > 1000) return null;
10
+ text=text.trim();
11
+ if (["none", "currentColor"].includes(text)) return {kind:"keyword",value:text};
12
+ const nodes = valueParser(text).nodes.filter(node => !["space","comment"].includes(node.type));
13
+ if (nodes.length !== 1 || nodes[0].type !== "function" || nodes[0].unclosed) return null;
14
+ const node=nodes[0], children=node.nodes.filter(item => !["space","comment"].includes(item.type));
15
+ if (node.value === "var" && children.length === 1 && children[0].type === "word" && /^--[\w-]+$/.test(children[0].value)) return {kind:"variable",name:children[0].value};
16
+ if (node.value === "url" && children.length === 1 && ["word","string"].includes(children[0].type) && /^#[^\s"'()#\\\p{Cc}]+$/u.test(children[0].value)) return {kind:"resource",id:children[0].value.slice(1)};
17
+ return null;
18
+ }
19
+
20
+ function supportedType(type, seen = new Set()) {
21
+ if (!type || seen.has(type)) return false;
22
+ seen=new Set([...seen,type]);
23
+ if (type.flags & ts.TypeFlags.Undefined) return true;
24
+ if (type.flags & ts.TypeFlags.StringLiteral) return !!paintReference(type.value);
25
+ if (type.isUnion()) return type.types.every(item=>supportedType(item,seen));
26
+ if (type.flags & ts.TypeFlags.TemplateLiteral) return type.texts.length===2 && (type.texts[0]==="var(--"||type.texts[0]==="url(#") && type.texts[1]===")" && type.types.every(item=>!!(item.flags & (ts.TypeFlags.String|ts.TypeFlags.StringLiteral)));
27
+ return false;
28
+ }
29
+
30
+ export function inspectPaintContracts(options) {
31
+ if(options.paintContracts)return options.paintContracts;
32
+ const declarations=inspectUnitContracts(options), result={allowedProperties:new Set(),diagnostics:[],contracts:declarations.contracts};
33
+ options.paintContracts=result;
34
+ if(!declarations.program)return result;
35
+ // TypeScript source identities use absolute paths, independently of process cwd.
36
+ for(const contract of declarations.contracts)for(const input of contract.paintInputs??[]) {
37
+ const checker=declarations.program.getTypeChecker();
38
+ const source=declarations.program.getSourceFile(path.resolve(options.root,input.api.source));
39
+ let declaration;
40
+ function find(node){if(node.pos===input.api.position&&(ts.isFunctionDeclaration(node)||ts.isVariableDeclaration(node)||ts.isClassDeclaration(node)))declaration=node;else ts.forEachChild(node,find);}
41
+ if(source)find(source);
42
+ const signature=declaration&&checker.getTypeAtLocation(declaration.name).getCallSignatures()[0];
43
+ let values=signature?.parameters[0]?[{type:checker.getTypeOfSymbolAtLocation(signature.parameters[0],declaration)}]:[];
44
+ for(const segment of input.property.split(".")) {
45
+ const next=[];
46
+ for(const item of values)for(const type of item.type.isUnion()?item.type.types:[item.type]){
47
+ if(type.flags & (ts.TypeFlags.Undefined|ts.TypeFlags.Null))continue;
48
+ if(segment==="*") {
49
+ const indexed=type.getStringIndexType()??type.getNumberIndexType();
50
+ if(indexed)next.push({type:indexed});else for(const prop of type.getProperties())next.push({type:checker.getTypeOfSymbolAtLocation(prop,prop.valueDeclaration??declaration),symbol:prop});
51
+ }else {const prop=type.getProperty(segment);if(prop)next.push({type:checker.getTypeOfSymbolAtLocation(prop,prop.valueDeclaration??declaration),symbol:prop});}
52
+ }
53
+ values=next;
54
+ }
55
+ if(!values.length||values.some(value=>!supportedType(value.type)))result.diagnostics.push({file:contract.file,rule:"paint-contract",line:1,column:1,message:`${input.apiName}.${input.property} must resolve to constrained token/local-resource paint references, not string, any or unknown. A declared role does not waive the actual type contract.`});
56
+ else for(const value of values)for(const node of value.symbol?.declarations??[])result.allowedProperties.add(`${node.getSourceFile().fileName}:${node.pos}`);
57
+ }
58
+ return result;
59
+ }
60
+
61
+ const paintProp=/^(?:background(?:Color)?|color|fill|stroke|stopColor|stop-color|floodColor|flood-color|(?:\w+)?ColorToken)$/i;
62
+
63
+ /** Check resolved values at the consuming source, so exports cannot launder privacy. */
64
+ export function inspectTokenValueFlows(program, options, inventory, scoped, report) {
65
+ if(!scoped.definitions.length && !inspectUnitContracts(options).contracts.some(contract=>contract.paintInputs?.length))return;
66
+ const values=createSourceValues(program,options), checker=program.getTypeChecker();
67
+ const declarations=inspectUnitContracts(options).contracts;
68
+ function strings(input, context=values.state()) {
69
+ const value=values.resolve(input,context);
70
+ if(value.kind==="literal")return typeof value.value==="string"?[value.value]:[];
71
+ if(value.kind==="union")return value.values.slice(0,32).flatMap(item=>strings(item,values.next(context)));
72
+ if(value.kind==="node"&&ts.isTemplateExpression(value.node)) {
73
+ let result=[value.node.head.text];
74
+ for(const span of value.node.templateSpans) {const parts=strings(reference(span.expression,value.env,value.via),values.next(context));result=result.flatMap(prefix=>parts.map(part=>prefix+part+span.literal.text)).slice(0,32);}
75
+ return result;
76
+ }
77
+ if(value.kind==="node"&&ts.isBinaryExpression(value.node)&&value.node.operatorToken.kind===ts.SyntaxKind.PlusToken) {
78
+ const left=strings(reference(value.node.left,value.env,value.via),values.next(context)),right=strings(reference(value.node.right,value.env,value.via),values.next(context));
79
+ return left.flatMap(first=>right.map(second=>first+second)).slice(0,32);
80
+ }
81
+ return [];
82
+ }
83
+ for(const source of program.getSourceFiles()) {
84
+ if(!isInside(options.root,source.fileName)||source.fileName.includes(`${path.sep}node_modules${path.sep}`)||isContractFile(source.fileName))continue;
85
+ const owner=scoped.inventory.owner(scoped.inventory.relative(path.relative(options.root,source.fileName)));
86
+ const emitted=new Set();
87
+ function check(node,text,bare=false,requirePaint=false,allowedBindings=null) {
88
+ const names=bare&&/^--[\w-]+$/.test(text)?[text]:valueParser(text).nodes.flatMap(function walk(value){return value.type==="function"&&value.value==="var"?[value.nodes.find(item=>item.type==="word")?.value]:value.nodes?value.nodes.flatMap(walk):[];}).filter(Boolean);
89
+ for(const name of names) {
90
+ const tokenOwner=scoped.definitions.find(entry=>name.startsWith(entry.namespace));
91
+ const binding=runtimeBindingOwner(options,name);
92
+ const issue=tokenOwner&&tokenOwner.unit!==owner?`${name} is private to ${tokenOwner.unit}; ${owner??"application source"} cannot consume it through props, aliases or helpers. Promote a cross-owner design value to shared tokens.`:binding&&(binding.unit!==owner||allowedBindings&&!allowedBindings.includes(binding.id))?`${name} is an instance binding private to ${binding.unit} and is not permitted by this consuming paint input.`:requirePaint&&!inventory.tokens.some(token=>token.name===name)&&!binding?`Undefined paint reference ${name}. Declare a real accessible token or a scoped runtime binding.`:null;
93
+ if(issue&&!emitted.has(`${node.pos}:${name}`)) {emitted.add(`${node.pos}:${name}`);const p=source.getLineAndCharacterOfPosition(node.getStart());report(source.fileName,"token-reference",issue,p.line+1,p.character+1);}
94
+ }
95
+ if(requirePaint&&!paintReference(text)) {const p=source.getLineAndCharacterOfPosition(node.getStart());report(source.fileName,"paint-reference",`Invalid paint reference ${JSON.stringify(text)}. Use an accessible token, declared scoped binding, local SVG paint resource, none or currentColor.`,p.line+1,p.character+1);}
96
+ }
97
+ function visit(node) {
98
+ if(ts.isCallExpression(node)&&ts.isPropertyAccessExpression(node.expression)&&["getPropertyValue","setProperty","removeProperty"].includes(node.expression.name.text)) {
99
+ for(const text of strings(reference(node.arguments[0])))check(node,text,true);
100
+ if(node.expression.name.text==="setProperty")for(const text of strings(reference(node.arguments[1])))check(node,text);
101
+ }
102
+ if(ts.isBinaryExpression(node)&&node.operatorToken.kind===ts.SyntaxKind.EqualsToken&&ts.isPropertyAccessExpression(node.left)&&ts.isPropertyAccessExpression(node.left.expression)&&node.left.expression.name.text==="style")for(const text of strings(reference(node.right)))check(node,text);
103
+ if(ts.isJsxOpeningElement(node)||ts.isJsxSelfClosingElement(node)) {
104
+ const attributes=values.props(node,new Map(),[]);
105
+ for(const property of values.keys(attributes,values.state())) {
106
+ if(paintProp.test(property))for(const text of strings(values.get(attributes,property,values.state())))check(node,text);
107
+ if(property==="style") {
108
+ const styles=values.get(attributes,"style",values.state());
109
+ for(const property of values.keys(styles,values.state()))for(const text of strings(values.get(styles,property,values.state())))check(node,text);
110
+ }
111
+ }
112
+ let symbol=checker.getSymbolAtLocation(node.tagName);if(symbol?.flags&ts.SymbolFlags.Alias)symbol=checker.getAliasedSymbol(symbol);
113
+ const declaration=symbol?.valueDeclaration;
114
+ if(declaration)for(const contract of declarations)for(const input of contract.paintInputs??[])if(input.api.identity===implementationIdentity(declaration,options.root)) {
115
+ let current=[values.props(node,new Map(),[])];
116
+ for(const segment of input.property.split("."))current=current.flatMap(value=>segment==="*"?values.arrays(value,values.state())??(values.keys(value,values.state())??[]).map(key=>values.get(value,key,values.state())):[values.get(value,segment,values.state())]);
117
+ for(const value of current)for(const text of strings(value))check(node,text,false,true,input.bindings??[]);
118
+ }
119
+ }
120
+ ts.forEachChild(node,visit);
121
+ }
122
+ visit(source);
123
+ }
124
+ }
@@ -125,7 +125,7 @@ export function createPreviewResources(output, options, entries) {
125
125
  const scenarios = entries.map((entry, index) => {
126
126
  const chunk = bundle.find(item => item.type === "chunk" && item.facadeModuleId === `\0ads-preview-entry:${index}`);
127
127
  if (!chunk) throw new Error(`Missing compiled scenario entry: ${entry.id}`);
128
- return { id: entry.id, title: entry.title, kind: entry.kind, ...(entry.samples ? { samples: entry.samples } : {}), module: uris.get(chunk.fileName) };
128
+ return { id: entry.id, title: entry.title, kind: entry.kind, ...(entry.samples ? { samples: entry.samples } : {}), ...(entry.paintBindings?.length ? {paintBindings:entry.paintBindings} : {}), ...(entry.paintAudit?{paintAudit:true}:{}), module: uris.get(chunk.fileName) };
129
129
  });
130
130
  const graph = { schemaVersion: 1, framework: options.config.framework, assets: records, scenarios };
131
131
  const revision = hash(JSON.stringify(graph));
@@ -18,7 +18,7 @@ function stringKeys(node, checker, seen = new Set()) {
18
18
 
19
19
  // Inspect locally declared exported *Props APIs without loading React at runtime.
20
20
  // Follow local aliases and inheritance, and distinguish Omit/Pick's actual keys.
21
- export function inspectPublicProps(source, checker, report) {
21
+ export function inspectPublicProps(source, checker, report, allowedPaintProperties = new Set()) {
22
22
  function inspect(node, excluded = new Set(), seen = new Set()) {
23
23
  if (!node || seen.has(node)) return;
24
24
  seen = new Set([...seen, node]);
@@ -26,7 +26,7 @@ export function inspectPublicProps(source, checker, report) {
26
26
  const name = propertyName(node.name);
27
27
  if (excluded.has(name)) return;
28
28
  if (name === "style") report(node, "public-style-prop", "Public ADS props must omit style. Use semantic props and documented token hooks.");
29
- if (visualName.test(name ?? "") && arbitraryString(node.type, new Set())) report(node, "visual-prop", "Public visual props must use semantic variants or a finite token union, not arbitrary strings.");
29
+ if (visualName.test(name ?? "") && arbitraryString(node.type, new Set()) && !allowedPaintProperties.has(`${node.getSourceFile().fileName}:${node.pos}`)) report(node, "visual-prop", "Public visual props must use semantic variants, a finite token union or an owner-registered constrained paint-reference contract, not arbitrary strings.");
30
30
  return;
31
31
  }
32
32
  if (ts.isTypeReferenceNode(node) || ts.isExpressionWithTypeArguments(node)) {
@@ -0,0 +1,225 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import selectorParser from "postcss-selector-parser";
4
+ import { isDevelopmentFile, isReviewFile } from "./config.mjs";
5
+ import { inspectUnitContracts, implementationIdentity, implementationNode } from "./unit-contracts.mjs";
6
+ import { tokenContract } from "./token-policy.mjs";
7
+ import { uiOperation } from "./ui-framework.mjs";
8
+ import { unwrap, createSourceValues, reference } from "./source-values.mjs";
9
+
10
+ function fn(node) { return node && ts.isVariableDeclaration(node) ? unwrap(node.initializer) : node; }
11
+ function all(node, predicate) { const result=[];function visit(item){if(predicate(item))result.push(item);ts.forEachChild(item,visit);}if(node)visit(node);return result; }
12
+
13
+ export function inspectRuntimeContracts(options, program) {
14
+ if(options.runtimeContracts?.program===program)return options.runtimeContracts;
15
+ const checker=program.getTypeChecker(), declarations=inspectUnitContracts(options);
16
+ const result={program,diagnostics:[],bindings:[],parts:[],allowedStyles:new Set(),allowedStyleTypes:new Set()};
17
+ options.runtimeContracts=result;
18
+ const key=node=>`${node.getSourceFile().fileName}:${node.pos}`;
19
+ const values=createSourceValues(program,options,{strict:true});
20
+ const canonical=node=>{
21
+ if(!node)return null;
22
+ let symbol=checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node)?node.name:node);
23
+ if(symbol?.flags&ts.SymbolFlags.Alias)symbol=checker.getAliasedSymbol(symbol);
24
+ return symbol?.valueDeclaration;
25
+ };
26
+ const same=(node,reference)=>{const declaration=canonical(node);return declaration&&implementationIdentity(declaration,options.root)===reference.identity;};
27
+ function report(contract,message,code="runtime-contract") {result.diagnostics.push({file:contract.file,rule:code,line:1,column:1,message});}
28
+ function localCalls(node,seen=new Set()) {
29
+ if(!node||seen.has(node)||seen.size>32)return [];
30
+ seen=new Set([...seen,node]);const found=[node];
31
+ for(const call of all(node,ts.isCallExpression)) {
32
+ const target=canonical(call.expression);
33
+ if(target&&path.resolve(target.getSourceFile().fileName).startsWith(options.dsRoot+path.sep)&&(!isDevelopmentFile(target.getSourceFile().fileName,options)||isReviewFile(target.getSourceFile().fileName,options)))found.push(...localCalls(fn(target),seen));
34
+ }
35
+ return found;
36
+ }
37
+ for(const contract of declarations.contracts) {
38
+ for(const part of contract.declarativeParts??[]) {
39
+ const consumer=fn(implementationNode(program,part.consumer,options.root));
40
+ const bodies=localCalls(consumer),calls=bodies.flatMap(body=>all(body,ts.isCallExpression));
41
+ function childOrigins(expression,seen=new Set()) {
42
+ expression=unwrap(expression);if(!expression||seen.has(expression)||seen.size>40)return new Set();
43
+ seen=new Set([...seen,expression]);
44
+ const merge=sets=>new Set(sets.flatMap(set=>[...set]));
45
+ if(ts.isPropertyAccessExpression(expression)&&expression.name.text==="children") {
46
+ if(canonical(expression.expression)===consumer?.parameters?.[0])return new Set(["children-input"]);
47
+ if(ts.isPropertyAccessExpression(expression.expression)&&expression.expression.name.text==="props"&&childOrigins(expression.expression.expression,seen).size)return new Set([key(expression)]);
48
+ }
49
+ const declaration=canonical(expression);
50
+ if(declaration&&ts.isBindingElement(declaration)&&ts.isObjectBindingPattern(declaration.parent)&&declaration.parent.parent===consumer?.parameters?.[0]&&(declaration.propertyName??declaration.name).getText()==="children")return new Set(["children-input"]);
51
+ if(declaration&&ts.isVariableDeclaration(declaration)) {
52
+ if(declaration.initializer)return childOrigins(declaration.initializer,seen);
53
+ if(ts.isVariableDeclarationList(declaration.parent)&&ts.isForOfStatement(declaration.parent.parent)&&childOrigins(declaration.parent.parent.expression,seen).size)return new Set([key(declaration)]);
54
+ }
55
+ if(declaration&&ts.isParameter(declaration)) {
56
+ const callback=declaration.parent,index=callback.parameters.indexOf(declaration),origins=[];
57
+ for(const call of calls) {
58
+ if(fn(canonical(call.expression))===callback&&call.arguments[index])origins.push(childOrigins(call.arguments[index],seen));
59
+ const operation=uiOperation(call.expression,checker);
60
+ const childrenCall=["Children.map","Children.forEach"].includes(operation)&&call.arguments[1]&&(call.arguments[1]===callback||fn(canonical(call.arguments[1]))===callback);
61
+ const arrayCall=ts.isPropertyAccessExpression(call.expression)&&["map","forEach","flatMap"].includes(call.expression.name.text)&&call.arguments[0]&&(call.arguments[0]===callback||fn(canonical(call.arguments[0]))===callback);
62
+ if(index===0&&(childrenCall&&childOrigins(call.arguments[0],seen).size||arrayCall&&childOrigins(call.expression.expression,seen).size))origins.push(new Set([key(declaration)]));
63
+ }
64
+ return merge(origins);
65
+ }
66
+ if(ts.isCallExpression(expression)) {
67
+ if(["Children.toArray","toChildArray"].includes(uiOperation(expression.expression,checker)))return childOrigins(expression.arguments[0],seen);
68
+ if(ts.isPropertyAccessExpression(expression.expression)&&["filter","slice"].includes(expression.expression.name.text))return childOrigins(expression.expression.expression,seen);
69
+ }
70
+ return new Set();
71
+ }
72
+ const propOrigins=new Set(bodies.flatMap(body=>all(body,ts.isPropertyAccessExpression).filter(node=>node.name.text==="props").flatMap(node=>[...childOrigins(node.expression)])));
73
+ const readsProps=expression=>[...childOrigins(expression)].some(origin=>propOrigins.has(origin));
74
+ let consumed=false;
75
+ for(const body of bodies) {
76
+ for(const binary of all(body,ts.isBinaryExpression)) {
77
+ if(![ts.SyntaxKind.EqualsEqualsEqualsToken,ts.SyntaxKind.EqualsEqualsToken].includes(binary.operatorToken.kind))continue;
78
+ for(const [value,identity]of[[binary.left,binary.right],[binary.right,binary.left]])if(ts.isPropertyAccessExpression(value)&&value.name.text==="type"&&same(identity,part.api)&&readsProps(value.expression))consumed=true;
79
+ }
80
+ for(const call of all(body,ts.isCallExpression))for(const [index,arg]of call.arguments.entries())if(same(arg,part.api)) {
81
+ const helper=fn(canonical(call.expression)),parameter=helper?.parameters?.[index];
82
+ if(!parameter||!ts.isIdentifier(parameter.name))continue;
83
+ const parameterSymbol=checker.getSymbolAtLocation(parameter.name);
84
+ for(const binary of all(helper,ts.isBinaryExpression))if([ts.SyntaxKind.EqualsEqualsEqualsToken,ts.SyntaxKind.EqualsEqualsToken].includes(binary.operatorToken.kind))for(const [value,identity]of[[binary.left,binary.right],[binary.right,binary.left]]) {
85
+ if(!ts.isPropertyAccessExpression(value)||value.name.text!=="type"||checker.getSymbolAtLocation(identity)!==parameterSymbol)continue;
86
+ const childParameter=canonical(value.expression),childIndex=helper.parameters.indexOf(childParameter);
87
+ if(childIndex>=0&&readsProps(call.arguments[childIndex]))consumed=true;
88
+ }
89
+ }
90
+ }
91
+ if(!consumer||!consumed)report(contract,`${part.apiName}: cannot establish that ${part.consumer.symbol} identifies this declarative child from its children input by resolved symbol and reads that same child's props. A registration, decoy object, import or unused reference is not consumption.`,"declarative-consumer");
92
+ else result.parts.push({...part,unit:contract.unit,file:contract.file});
93
+ }
94
+ for(const [name,binding]of Object.entries(contract.bindings??{})) {
95
+ const producer=fn(implementationNode(program,binding.producer,options.root));
96
+ const [prefix,suffix]=binding.variable.split("${key}");
97
+ if(!(contract.paintInputs??[]).some(input=>input.api.identity===binding.input.api.identity&&input.property===`${binding.input.property}.*.${binding.valueFrom}`)) {report(contract,`${name}: register the actual config-entry value path as a constrained paint input. A producer does not authorize untyped config colours.`,"binding-input");continue;}
98
+ if([...tokenContract.naming.prefixes,"--ds-","--ux-","--showcase-"].some(value=>value.startsWith(prefix)||prefix.startsWith(value))||[...tokenContract.naming.names,...Object.keys(tokenContract.tokens)].some(value=>value.startsWith(prefix)&&value.endsWith(suffix))) {
99
+ report(contract,`${name}: runtime family ${binding.variable} overlaps the shared token vocabulary. Use a nonconflicting private runtime family; registration cannot grant shared-token overrides.`,"binding-collision");continue;
100
+ }
101
+ const other=result.bindings.find(entry=>entry.prefix.startsWith(prefix)||prefix.startsWith(entry.prefix));
102
+ if(other){report(contract,`${name}: runtime family overlaps ${other.unit}/${other.name}. Keep binding ownership unambiguous.`,"binding-collision");continue;}
103
+ let selector;
104
+ try {
105
+ const parsed=selectorParser().astSync(binding.scope.selector);
106
+ if(parsed.nodes.length!==1||parsed.first.nodes.length!==1||!["class","attribute"].includes(parsed.first.first.type))throw new Error();
107
+ selector=parsed.first.first;
108
+ if(selector.type==="attribute"&&(!selector.attribute.startsWith("data-")||selector.operator!=="="))throw new Error();
109
+ }catch{report(contract,`${name}: scope must name one actual owner-root class or exact data-* attribute, not a broad selector.`);continue;}
110
+ if(!producer?.parameters?.[0]||!ts.isIdentifier(producer.parameters[0].name)){report(contract,`${name}: producer requires an explicit config-map parameter.`);continue;}
111
+ const ownerFile=implementationNode(program,binding.producer,options.root).getSourceFile().fileName;
112
+ function builtInEntries(call, input) {
113
+ return ts.isCallExpression(call)&&call.arguments[0]&&ts.isPropertyAccessExpression(call.expression)&&call.expression.name.text==="entries"&&call.expression.expression.getText()==="Object"&&checker.getSymbolAtLocation(call.arguments[0])===input&&!checker.getSymbolAtLocation(call.expression.expression)?.declarations?.some(node=>node.getSourceFile().fileName.startsWith(options.root+path.sep)&&!node.getSourceFile().hasNoDefaultLib&&!node.getSourceFile().fileName.includes(`${path.sep}node_modules${path.sep}`));
114
+ }
115
+ function safeKey(body, keySymbol, before) {
116
+ return ts.isBlock(body)&&body.statements.some(statement=>{
117
+ if(!ts.isIfStatement(statement)||!ts.isPrefixUnaryExpression(statement.expression)||statement.expression.operator!==ts.SyntaxKind.ExclamationToken)return false;
118
+ const test=statement.expression.operand,stop=ts.isBlock(statement.thenStatement)?statement.thenStatement.statements[0]:statement.thenStatement;
119
+ return ts.isCallExpression(test)&&ts.isPropertyAccessExpression(test.expression)&&test.expression.name.text==="test"&&ts.isRegularExpressionLiteral(test.expression.expression)&&test.expression.expression.text==="/^[A-Za-z0-9_-]+$/"&&checker.getSymbolAtLocation(test.arguments[0])===keySymbol&&(ts.isThrowStatement(stop)||ts.isContinueStatement(stop))&&statement.end<=before;
120
+ });
121
+ }
122
+ function pairMatches(template,value,keySymbol,valueSymbol) {
123
+ template=unwrap(template);value=unwrap(value);
124
+ if(!template||!ts.isTemplateExpression(template)||template.templateSpans.length!==1||template.head.text!==prefix||template.templateSpans[0].literal.text!==suffix||checker.getSymbolAtLocation(template.templateSpans[0].expression)!==keySymbol)return false;
125
+ let valueRoot=value;const fields=[];while(ts.isPropertyAccessExpression(valueRoot)){fields.unshift(valueRoot.name.text);valueRoot=valueRoot.expression;}
126
+ return checker.getSymbolAtLocation(valueRoot)===valueSymbol&&fields.join(".")===binding.valueFrom;
127
+ }
128
+ function proveMap(body,seen=new Set()) {
129
+ if(!body?.parameters?.[0]||!ts.isIdentifier(body.parameters[0].name)||seen.has(body)||seen.size>8)return null;
130
+ seen=new Set([...seen,body]);const inputSymbol=checker.getSymbolAtLocation(body.parameters[0].name);
131
+ const returns=ts.isBlock(body.body)?all(body.body,ts.isReturnStatement).filter(node=>{for(let parent=node.parent;parent&&parent!==body;parent=parent.parent)if(ts.isFunctionLike(parent))return false;return true;}):[{expression:body.body}];
132
+ if(returns.length!==1)return null;
133
+ const returned=unwrap(returns[0].expression);
134
+ if(ts.isCallExpression(returned)) {
135
+ if(ts.isPropertyAccessExpression(returned.expression)&&returned.expression.expression.getText()==="Object"&&returned.expression.name.text==="fromEntries"&&!checker.getSymbolAtLocation(returned.expression.expression)?.declarations?.some(node=>node.getSourceFile().fileName.startsWith(options.root+path.sep)&&!node.getSourceFile().hasNoDefaultLib&&!node.getSourceFile().fileName.includes(`${path.sep}node_modules${path.sep}`))) {
136
+ const mapped=unwrap(returned.arguments[0]);
137
+ if(!mapped||!ts.isCallExpression(mapped)||!ts.isPropertyAccessExpression(mapped.expression)||mapped.expression.name.text!=="map"||!builtInEntries(unwrap(mapped.expression.expression),inputSymbol))return null;
138
+ const callback=unwrap(mapped.arguments[0]),tuple=callback?.parameters?.[0]?.name;
139
+ if(!callback||!ts.isArrowFunction(callback)||!tuple||!ts.isArrayBindingPattern(tuple)||tuple.elements.length!==2||!tuple.elements.every(ts.isBindingElement))return null;
140
+ const keySymbol=checker.getSymbolAtLocation(tuple.elements[0].name),valueSymbol=checker.getSymbolAtLocation(tuple.elements[1].name);
141
+ const outputs=ts.isBlock(callback.body)?all(callback.body,ts.isReturnStatement):[{expression:callback.body,pos:callback.body.pos}];
142
+ if(outputs.length!==1||!safeKey(callback.body,keySymbol,outputs[0].pos))return null;
143
+ const pair=unwrap(outputs[0].expression);
144
+ return pair&&ts.isArrayLiteralExpression(pair)&&pair.elements.length===2&&pairMatches(pair.elements[0],pair.elements[1],keySymbol,valueSymbol)?[body]:null;
145
+ }
146
+ const helper=canonical(returned.expression);
147
+ if(helper&&path.dirname(helper.getSourceFile().fileName)===path.dirname(ownerFile)&&checker.getSymbolAtLocation(returned.arguments[0])===inputSymbol){const proof=proveMap(fn(helper),seen);return proof?[body,...proof]:null;}
148
+ return null;
149
+ }
150
+ let mapSymbol;const writes=[];
151
+ for(const loop of all(body,ts.isForOfStatement)) {
152
+ if(!builtInEntries(unwrap(loop.expression),inputSymbol))continue;
153
+ const tuple=ts.isVariableDeclarationList(loop.initializer)&&loop.initializer.declarations[0]?.name;
154
+ if(!tuple||!ts.isArrayBindingPattern(tuple)||tuple.elements.length!==2||!tuple.elements.every(ts.isBindingElement))continue;
155
+ const keySymbol=checker.getSymbolAtLocation(tuple.elements[0].name),valueSymbol=checker.getSymbolAtLocation(tuple.elements[1].name);
156
+ for(const write of all(loop.statement,ts.isBinaryExpression))if(write.operatorToken.kind===ts.SyntaxKind.EqualsToken&&ts.isElementAccessExpression(write.left)&&pairMatches(write.left.argumentExpression,write.right,keySymbol,valueSymbol)&&safeKey(loop.statement,keySymbol,write.pos)) {
157
+ const target=canonical(write.left.expression);
158
+ if(!target||!ts.isVariableDeclaration(target)||!target.initializer||!ts.isObjectLiteralExpression(unwrap(target.initializer))||unwrap(target.initializer).properties.length)continue;
159
+ const actual=checker.getSymbolAtLocation(target.name);if(mapSymbol&&mapSymbol!==actual)continue;mapSymbol=actual;writes.push(write);
160
+ }
161
+ }
162
+ const escapes=mapSymbol&&all(body,ts.isIdentifier).some(node=>checker.getSymbolAtLocation(node)===mapSymbol&&!ts.isVariableDeclaration(node.parent)&&!ts.isReturnStatement(node.parent)&&!(ts.isElementAccessExpression(node.parent)&&writes.some(write=>write.left===node.parent)));
163
+ return mapSymbol&&!escapes&&checker.getSymbolAtLocation(returned)===mapSymbol?[body]:null;
164
+ }
165
+ const proof=proveMap(producer);
166
+ if(!proof){report(contract,`${name}: producer must return only the declared config-entry paint map. Use Object.entries with a loop or Object.fromEntries/map, guard keys with /^[A-Za-z0-9_-]+$/ before writing (throw or continue), and the exact computed family. Owner-local helper returns are supported; escaped maps, extra writes and unknown returns grant no permission.`,"binding-producer");continue;}
167
+ const scope=fn(implementationNode(program,binding.scope.api,options.root));
168
+ let attached=false;
169
+ const returnedElements=new Set();
170
+ function rendered(input,context=values.state()) {
171
+ const value=values.resolve(input,values.next(context));
172
+ context=value.context??context;
173
+ if(value.kind==="union"||value.kind==="array"){for(const item of value.values.slice(0,32))rendered(item,values.next(context));return;}
174
+ if(value.kind!=="node")return;
175
+ const node=value.node;
176
+ if(ts.isJsxExpression(node)){rendered(reference(node.expression,value.env,value.via),values.next(context));return;}
177
+ if(ts.isJsxFragment(node)){for(const child of node.children)rendered(reference(child,value.env,value.via),values.next(context));return;}
178
+ if(!ts.isJsxElement(node)&&!ts.isJsxSelfClosingElement(node))return;
179
+ const opening=ts.isJsxElement(node)?node.openingElement:node,children=ts.isJsxElement(node)?node.children:[];
180
+ const attributes=values.props(opening,value.env,value.via);
181
+ if(children.length)attributes.layers.push(new Map([["children",{kind:"array",values:children.map(child=>reference(child,value.env,value.via))}]]));
182
+ if(ts.isIdentifier(opening.tagName)&&/^[a-z]/.test(opening.tagName.text)) {returnedElements.add(opening);for(const child of children)rendered(reference(child,value.env,value.via),values.next(context));return;}
183
+ const component=values.resolve(reference(opening.tagName,value.env,value.via),values.next(context));
184
+ if(["provider","context"].includes(component.kind))for(const child of children)rendered(reference(child,value.env,value.via),values.next(context));
185
+ else if(component.kind==="node"&&component.node.body)for(const output of values.returns(component,[attributes],values.next(context),"component"))rendered(output,values.next(context));
186
+ }
187
+ const target=scope&&values.resolve(reference(scope),values.state());
188
+ if(target?.kind==="node")for(const output of values.returns(target,[{kind:"unknown"}],values.state(),"component"))rendered(output);
189
+ function inputMatches(expression) {
190
+ expression=unwrap(expression);if(!expression||!scope?.parameters?.[0])return false;
191
+ const fields=[];while(ts.isPropertyAccessExpression(expression)){fields.unshift(expression.name.text);expression=expression.expression;}
192
+ const parameter=scope.parameters[0].name;
193
+ if(ts.isIdentifier(parameter))return checker.getSymbolAtLocation(expression)===checker.getSymbolAtLocation(parameter)&&fields.join(".")===binding.input.property;
194
+ const [first,...rest]=binding.input.property.split(".");
195
+ const item=ts.isObjectBindingPattern(parameter)&&parameter.elements.find(item=>(item.propertyName??item.name).getText()===first);
196
+ return item&&ts.isIdentifier(item.name)&&checker.getSymbolAtLocation(expression)===checker.getSymbolAtLocation(item.name)&&fields.join(".")===rest.join(".");
197
+ }
198
+ function produced(expression,seen=new Set()) {
199
+ expression=unwrap(expression);if(!expression||seen.has(expression)||seen.size>20)return false;seen=new Set([...seen,expression]);
200
+ if(ts.isCallExpression(expression)&&same(expression.expression,binding.producer))return inputMatches(expression.arguments[0]);
201
+ if(ts.isCallExpression(expression)&&uiOperation(expression.expression,checker)==="useMemo") {
202
+ const callback=unwrap(expression.arguments[0]);
203
+ const returns=callback&&ts.isBlock(callback.body)?all(callback.body,ts.isReturnStatement):[];
204
+ return callback&&(ts.isArrowFunction(callback)||ts.isFunctionExpression(callback))&&(ts.isBlock(callback.body)?returns.length>0&&returns.every(node=>produced(node.expression,seen)):produced(callback.body,seen));
205
+ }
206
+ const declaration=canonical(expression);
207
+ return declaration&&ts.isVariableDeclaration(declaration)&&produced(declaration.initializer,seen);
208
+ }
209
+ for(const element of returnedElements) {
210
+ if(!ts.isIdentifier(element.tagName)||!/^[a-z]/.test(element.tagName.text))continue;
211
+ const attributes=element.attributes.properties.filter(ts.isJsxAttribute);
212
+ const target=attributes.find(attribute=>attribute.name.getText()===(selector.type==="class"?"className":selector.attribute));
213
+ const targetValue=target?.initializer&&(ts.isStringLiteralLike(target.initializer)?target.initializer.text:ts.isJsxExpression(target.initializer)&&ts.isStringLiteralLike(target.initializer.expression)?target.initializer.expression.text:null);
214
+ const matched=selector.type==="class"?targetValue?.split(/\s+/).includes(selector.value):targetValue===selector.value;
215
+ const style=attributes.find(attribute=>attribute.name.getText()==="style");
216
+ const overwritten=element.attributes.properties.some((attribute,index)=>ts.isJsxSpreadAttribute(attribute)&&index>Math.min(element.attributes.properties.indexOf(style),element.attributes.properties.indexOf(target)));
217
+ if(matched&&!overwritten&&style?.initializer&&ts.isJsxExpression(style.initializer)&&produced(style.initializer.expression)){attached=true;result.allowedStyles.add(key(style));}
218
+ }
219
+ if(!attached){report(contract,`${name}: cannot trace the producer's returned map onto its declared component root. Unused producers, unrelated elements and arbitrary inline styles do not establish a binding.`,"binding-scope");continue;}
220
+ for(const node of proof.flatMap(body=>all(body,ts.isTypeReferenceNode)))result.allowedStyleTypes.add(key(node));
221
+ result.bindings.push({...binding,name,prefix,suffix,unit:contract.unit,file:contract.file});
222
+ }
223
+ }
224
+ return result;
225
+ }
@@ -6,6 +6,8 @@ import { scenarioApi } from "./public-api.mjs";
6
6
  import { uiOperation } from "./ui-framework.mjs";
7
7
  import { inspectUnits } from "./units.mjs";
8
8
  import { createSourceValues, reference, union, isFunction } from "./source-values.mjs";
9
+ import { inspectRuntimeContracts } from "./runtime-contracts.mjs";
10
+ import { implementationNode } from "./unit-contracts.mjs";
9
11
 
10
12
  /** Source evidence only. No project modules execute, and unknown paths grant no
11
13
  * permission. Coverage uses intersections across unresolved render branches;
@@ -27,6 +29,10 @@ export function createScenarioTrace(program, options) {
27
29
  const isCx = expression => linaria(expression, "cx");
28
30
  const { state, next, at, resolve, get, arrays, returns, props } = createSourceValues(program, options, { preserveCall: expression => isCx(expression) || uiOperation(expression, checker) === "createElement", strict: true });
29
31
  const publicNodes = new Map(), helpers = new Map(), inventory = inspectUnits(options);
32
+ const parts = inspectRuntimeContracts(options, program).parts.map(part => ({ ...part,
33
+ apiNode: resolve(reference(implementationNode(program, part.api, options.root)?.name), state()).node,
34
+ consumerNode: resolve(reference(implementationNode(program, part.consumer, options.root)?.name), state()).node,
35
+ }));
30
36
  const locationKey = value => JSON.stringify([value.file, value.line, value.column]);
31
37
  function runtimeOwner(file) {
32
38
  return (isReviewFile(file, options) || ["components", "patterns"].some(folder => isInside(path.join(options.dsRoot, folder), file)) && !isDevelopmentFile(file, options))
@@ -60,8 +66,20 @@ export function createScenarioTrace(program, options) {
60
66
  return null;
61
67
  }
62
68
 
63
- function traceRenders(site) {
69
+ function traceRenders(site, { entry = false } = {}) {
64
70
  const limits = new Set();
71
+ function childParts(input, context) {
72
+ const value = resolve(input, next(context));
73
+ if (value.kind === "union") return common(value.values.slice(0, maxBranches).map(item => childParts(item, next(context))));
74
+ if (value.kind === "array") return combine(value.values.map(item => childParts(item, next(context))));
75
+ if (value.kind !== "node") return new Set();
76
+ if (ts.isJsxExpression(value.node)) return childParts(reference(value.node.expression, value.env, value.via), next(context));
77
+ if (ts.isJsxFragment(value.node)) return combine(value.node.children.map(item => childParts(reference(item, value.env, value.via), next(context))));
78
+ const view = element(value.node, value.env, value.via);
79
+ if (!view) return new Set();
80
+ const target = resolve(view.tag, next(context));
81
+ return new Set([...parts.filter(part => part.apiNode && part.apiNode === target.node).map(part => part.apiName), ...childParts(get(view.attributes, "children", next(context)), next(context))]);
82
+ }
65
83
  function render(input, context) {
66
84
  const value = resolve(input, next(context));
67
85
  context = value.context ?? input.context ?? context;
@@ -85,15 +103,19 @@ export function createScenarioTrace(program, options) {
85
103
  if (target.kind === "union") return target.values.length <= maxBranches ? common(target.values.map(component)) : new Set();
86
104
  if (target.kind !== "node") return new Set();
87
105
  const names = new Set(publicNodes.get(target.node) ?? []);
88
- if (isFunction(target.node) && (!isDevelopmentFile(target.node.getSourceFile().fileName, options) || isReviewFile(target.node.getSourceFile().fileName, options))) {
106
+ for (const part of parts) if (target.node === part.consumerNode && childParts(get(view.attributes, "children", next(context)), next(context)).has(part.apiName)) names.add(`@consumed:${part.apiName}`);
107
+ if (isFunction(target.node) && (entry || !isDevelopmentFile(target.node.getSourceFile().fileName, options) || isReviewFile(target.node.getSourceFile().fileName, options))) {
89
108
  const outputs = returns(target, [view.attributes], next(context), "component");
90
109
  for (const name of render(union(outputs), next(context))) names.add(name);
91
110
  }
111
+ for (const part of parts) if (names.has(part.parentName) && names.has(`@consumed:${part.apiName}`)) names.add(`@exercised:${part.apiName}`);
92
112
  return names;
93
113
  }
94
114
  return component(view.tag);
95
115
  }
96
- return { rendered: render(reference(site), state()), limits: [...limits].slice(0, 3) };
116
+ const target=entry&&resolve(reference(site),state());
117
+ const evidence = target?.kind==="node"&&isFunction(target.node) ? render(union(returns(target,[{kind:"object",layers:[]}],state(),"component")),state()) : render(reference(site), state());
118
+ return { rendered: new Set([...evidence].filter(name => !name.startsWith("@"))), declarative: new Set([...evidence].filter(name => name.startsWith("@exercised:")).map(name => name.slice(11))), limits: [...limits].slice(0, 3) };
97
119
  }
98
120
 
99
121
  function classValue(expression, property = false) {
@@ -145,5 +167,11 @@ export function createScenarioTrace(program, options) {
145
167
  if (!ok && !reasons.size) reasons.add("Each nonempty return path needs owned Linaria styles; stable hooks alone are not styling evidence.");
146
168
  return { present: !property || value.kind !== "unknown", ok, reasons: [...reasons].slice(0, 3) };
147
169
  }
148
- return { traceRenders, classValue };
170
+ function traceEntry(source) {
171
+ const module=checker.getSymbolAtLocation(source),symbol=module&&checker.getExportsOfModule(module).find(symbol=>symbol.name==="default");
172
+ const resolved=symbol?.flags&ts.SymbolFlags.Alias?checker.getAliasedSymbol(symbol):symbol;
173
+ const declaration=resolved?.valueDeclaration;
174
+ return traceRenders(declaration&&(ts.isExportAssignment(declaration)?declaration.expression:declaration.name??declaration),{entry:true});
175
+ }
176
+ return { traceRenders, traceEntry, classValue };
149
177
  }
@@ -11,7 +11,7 @@ export function scenarioRegistryPlugin(directory = process.cwd()) {
11
11
  const virtual = "virtual:ads-scenarios";
12
12
  const resolved = "\0virtual:ads-scenarios";
13
13
  function registry() {
14
- const value = inspectScenarios(options);
14
+ const value = inspectScenarios(loadConfig(directory));
15
15
  if (value.diagnostics.length) throw new Error(value.diagnostics.map((item) => `${item.file}: ${item.message}`).join("\n"));
16
16
  mkdirSync(path.join(options.root, ".ds/scenarios"), { recursive: true });
17
17
  writeFileSync(path.join(options.root, ".ds/scenarios/manifest.json"), JSON.stringify({ schemaVersion: 1, scenarios: value.scenarios }, null, 2) + "\n");