@dcoder-x/plugin-shared 0.1.3 → 0.1.5

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 (36) hide show
  1. package/dist/buildId.d.ts +5 -0
  2. package/dist/buildId.js +17 -0
  3. package/dist/extractors/ComponentContextResolver.d.ts +37 -0
  4. package/dist/extractors/ComponentContextResolver.js +167 -0
  5. package/dist/extractors/ComponentExtractor.d.ts +6 -1
  6. package/dist/extractors/ComponentExtractor.js +124 -11
  7. package/dist/extractors/FlowInferrer.d.ts +5 -2
  8. package/dist/extractors/FlowInferrer.js +117 -14
  9. package/dist/extractors/InteractionGraphExtractor.d.ts +28 -0
  10. package/dist/extractors/InteractionGraphExtractor.js +333 -0
  11. package/dist/extractors/SelectorGenerator.d.ts +6 -1
  12. package/dist/extractors/SelectorGenerator.js +28 -3
  13. package/dist/index.d.ts +7 -1
  14. package/dist/index.js +21 -0
  15. package/dist/injection/ClippyIdInjector.d.ts +17 -0
  16. package/dist/injection/ClippyIdInjector.js +164 -0
  17. package/dist/injection/HtmlTagGuards.d.ts +3 -0
  18. package/dist/injection/HtmlTagGuards.js +41 -0
  19. package/dist/injection/IdStrategy.d.ts +36 -0
  20. package/dist/injection/IdStrategy.js +148 -0
  21. package/dist/types.d.ts +108 -1
  22. package/dist/upload/Adapter.d.ts +3 -0
  23. package/dist/upload/Adapter.js +13 -0
  24. package/dist/upload/BackendAdapter.d.ts +30 -0
  25. package/dist/upload/BackendAdapter.js +42 -0
  26. package/dist/upload/BackendUploadAdapter.d.ts +13 -0
  27. package/dist/upload/BackendUploadAdapter.js +51 -0
  28. package/dist/upload/PackageBuilder.d.ts +34 -1
  29. package/dist/upload/PackageBuilder.js +220 -0
  30. package/dist/upload/PackageWriter.d.ts +9 -1
  31. package/dist/upload/PackageWriter.js +26 -0
  32. package/dist/upload/UploadStrategy.d.ts +11 -0
  33. package/dist/upload/UploadStrategy.js +40 -0
  34. package/dist/upload/Uploader.d.ts +6 -1
  35. package/dist/upload/Uploader.js +48 -3
  36. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ export interface ModuleGraphEntry {
2
+ id: string;
3
+ importedIds: readonly string[];
4
+ }
5
+ export declare function deriveBuildId(moduleGraph: Map<string, ModuleGraphEntry>): string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveBuildId = deriveBuildId;
4
+ function deriveBuildId(moduleGraph) {
5
+ if (!moduleGraph.size) {
6
+ return `vite_${Date.now()}`;
7
+ }
8
+ const rows = Array.from(moduleGraph.values())
9
+ .map((mod) => `${mod.id}|${[...mod.importedIds].sort().join(',')}`)
10
+ .sort()
11
+ .join('\n');
12
+ let hash = 0;
13
+ for (let i = 0; i < rows.length; i++) {
14
+ hash = (hash * 31 + rows.charCodeAt(i)) >>> 0;
15
+ }
16
+ return `vite_${hash.toString(36)}`;
17
+ }
@@ -0,0 +1,37 @@
1
+ import type { NodePath } from '@babel/traverse';
2
+ export interface StateVariable {
3
+ name: string;
4
+ setter?: string;
5
+ initialValue?: string;
6
+ line: number;
7
+ }
8
+ export interface EventHandler {
9
+ name: string;
10
+ events: string[];
11
+ line: number;
12
+ callsStateSetters: string[];
13
+ }
14
+ export interface ComponentContext {
15
+ componentName: string;
16
+ stateVariables: StateVariable[];
17
+ eventHandlers: EventHandler[];
18
+ }
19
+ /**
20
+ * Resolves state declarations, setters, and event handlers within a component scope.
21
+ * This provides the symbol table needed for interaction graph extraction.
22
+ */
23
+ export declare class ComponentContextResolver {
24
+ /**
25
+ * Extract state, handlers, and their relationships from a component AST node.
26
+ */
27
+ extractContext(componentPath: NodePath<any>, componentName: string): ComponentContext;
28
+ /**
29
+ * Extract handler name, event references, and called setters from a function.
30
+ */
31
+ private extractHandlerInfo;
32
+ /**
33
+ * Map event handlers referenced in JSX attributes to their handler definitions.
34
+ * Returns which state setters a given event attribute calls.
35
+ */
36
+ getStateSettersCalledByEvent(event: string, handlers: EventHandler[]): string[];
37
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComponentContextResolver = void 0;
4
+ /**
5
+ * Resolves state declarations, setters, and event handlers within a component scope.
6
+ * This provides the symbol table needed for interaction graph extraction.
7
+ */
8
+ class ComponentContextResolver {
9
+ /**
10
+ * Extract state, handlers, and their relationships from a component AST node.
11
+ */
12
+ extractContext(componentPath, componentName) {
13
+ const stateVariables = [];
14
+ const eventHandlers = [];
15
+ const stateSetterMap = new Map(); // setter -> state var
16
+ // First pass: collect useState declarations and build setter map
17
+ componentPath.traverse({
18
+ VariableDeclarator: (varPath) => {
19
+ const id = varPath.node.id;
20
+ const init = varPath.node.init;
21
+ // Detect useState pattern: const [state, setState] = useState(...)
22
+ if (id.type === 'ArrayPattern' &&
23
+ id.elements &&
24
+ id.elements.length === 2 &&
25
+ init?.type === 'CallExpression') {
26
+ const callee = init.callee;
27
+ if ((callee.type === 'Identifier' && callee.name === 'useState') ||
28
+ (callee.type === 'MemberExpression' && callee.property?.name === 'useState')) {
29
+ const stateId = id.elements[0];
30
+ const setterIdNode = id.elements[1];
31
+ if (stateId?.type === 'Identifier' && setterIdNode?.type === 'Identifier') {
32
+ const stateName = stateId.name;
33
+ const setterName = setterIdNode.name;
34
+ stateSetterMap.set(setterName, stateName);
35
+ stateVariables.push({
36
+ name: stateName,
37
+ setter: setterName,
38
+ initialValue: init.arguments?.[0]?.type === 'StringLiteral'
39
+ ? init.arguments[0].value
40
+ : undefined,
41
+ line: varPath.node.loc?.start.line ?? 0,
42
+ });
43
+ }
44
+ }
45
+ }
46
+ // Detect useReducer pattern: const [state, dispatch] = useReducer(...)
47
+ if (id.type === 'ArrayPattern' &&
48
+ id.elements &&
49
+ id.elements.length === 2 &&
50
+ init?.type === 'CallExpression') {
51
+ const callee = init.callee;
52
+ if ((callee.type === 'Identifier' && callee.name === 'useReducer') ||
53
+ (callee.type === 'MemberExpression' && callee.property?.name === 'useReducer')) {
54
+ const stateId = id.elements[0];
55
+ const dispatchIdNode = id.elements[1];
56
+ if (stateId?.type === 'Identifier' && dispatchIdNode?.type === 'Identifier') {
57
+ const stateName = stateId.name;
58
+ const dispatchName = dispatchIdNode.name;
59
+ stateSetterMap.set(dispatchName, stateName);
60
+ stateVariables.push({
61
+ name: stateName,
62
+ setter: dispatchName,
63
+ initialValue: undefined,
64
+ line: varPath.node.loc?.start.line ?? 0,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ },
70
+ });
71
+ // Second pass: find event handler functions and track which setters they call
72
+ componentPath.traverse({
73
+ FunctionDeclaration: (funcPath) => {
74
+ const handlerInfo = this.extractHandlerInfo(funcPath, stateSetterMap);
75
+ if (handlerInfo) {
76
+ handlerInfo.name = funcPath.node.id?.name || handlerInfo.name;
77
+ eventHandlers.push(handlerInfo);
78
+ }
79
+ },
80
+ VariableDeclarator: (varPath) => {
81
+ if ((varPath.node.init?.type === 'ArrowFunctionExpression' ||
82
+ varPath.node.init?.type === 'FunctionExpression') &&
83
+ varPath.node.id?.type === 'Identifier') {
84
+ const handlerName = varPath.node.id.name;
85
+ const funcPath = varPath.get('init');
86
+ const handlerInfo = this.extractHandlerInfo(funcPath, stateSetterMap);
87
+ if (handlerInfo) {
88
+ handlerInfo.name = handlerName;
89
+ eventHandlers.push(handlerInfo);
90
+ }
91
+ }
92
+ },
93
+ });
94
+ // Third pass: annotate which events correspond to each handler reference
95
+ componentPath.traverse({
96
+ JSXAttribute: (attrPath) => {
97
+ const name = attrPath.node.name?.name;
98
+ if (!name || !name.startsWith('on'))
99
+ return;
100
+ const value = attrPath.node.value;
101
+ if (!value || value.type !== 'JSXExpressionContainer')
102
+ return;
103
+ const expression = value.expression;
104
+ if (!expression)
105
+ return;
106
+ const handlerName = expression.type === 'Identifier' ? expression.name : null;
107
+ if (!handlerName)
108
+ return;
109
+ const handler = eventHandlers.find((h) => h.name === handlerName);
110
+ if (!handler)
111
+ return;
112
+ if (!handler.events.includes(name)) {
113
+ handler.events.push(name);
114
+ }
115
+ },
116
+ });
117
+ return {
118
+ componentName,
119
+ stateVariables,
120
+ eventHandlers,
121
+ };
122
+ }
123
+ /**
124
+ * Extract handler name, event references, and called setters from a function.
125
+ */
126
+ extractHandlerInfo(funcPath, stateSetterMap) {
127
+ const calledSetters = new Set();
128
+ funcPath.traverse({
129
+ CallExpression(callPath) {
130
+ const callee = callPath.node.callee;
131
+ let setterName = null;
132
+ if (callee.type === 'Identifier') {
133
+ setterName = callee.name;
134
+ }
135
+ else if (callee.type === 'MemberExpression' && callee.property?.type === 'Identifier') {
136
+ setterName = callee.property.name;
137
+ }
138
+ if (setterName && stateSetterMap.has(setterName)) {
139
+ calledSetters.add(stateSetterMap.get(setterName));
140
+ }
141
+ },
142
+ });
143
+ return {
144
+ name: funcPath.node.id?.name || 'anonymous',
145
+ events: [],
146
+ line: funcPath.node.loc?.start.line ?? 0,
147
+ callsStateSetters: Array.from(calledSetters),
148
+ };
149
+ }
150
+ /**
151
+ * Map event handlers referenced in JSX attributes to their handler definitions.
152
+ * Returns which state setters a given event attribute calls.
153
+ */
154
+ getStateSettersCalledByEvent(event, handlers) {
155
+ // Simple heuristic: look for handlers named after the event
156
+ const candidates = handlers.filter((h) => h.name.toLowerCase().includes(event.toLowerCase().replace('on', '')) ||
157
+ h.events.includes(event));
158
+ const allSetters = new Set();
159
+ for (const handler of candidates) {
160
+ for (const setter of handler.callsStateSetters) {
161
+ allSetters.add(setter);
162
+ }
163
+ }
164
+ return Array.from(allSetters);
165
+ }
166
+ }
167
+ exports.ComponentContextResolver = ComponentContextResolver;
@@ -1,9 +1,14 @@
1
- import type { ModuleGraphSource, DiscoveredRoute, DiscoveredElement } from '../types';
1
+ import type { ModuleGraphSource, DiscoveredRoute, DiscoveredElement, PolicyComponent } from '../types';
2
2
  export declare class ComponentExtractor {
3
3
  private source;
4
4
  private routes;
5
5
  constructor(source: ModuleGraphSource, routes: DiscoveredRoute[]);
6
6
  extract(): Promise<DiscoveredElement[]>;
7
+ /**
8
+ * Extract component-level analysis including state, handlers, and interaction graphs.
9
+ * Returns PolicyComponent data suitable for policy document generation.
10
+ */
11
+ extractComponents(): Promise<PolicyComponent[]>;
7
12
  private getModuleFilesForRoute;
8
13
  private walkWebpackGraph;
9
14
  private findWebpackModule;
@@ -8,6 +8,10 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const parser_1 = require("@babel/parser");
10
10
  const traverse_1 = __importDefault(require("@babel/traverse"));
11
+ const InteractionGraphExtractor_1 = require("./InteractionGraphExtractor");
12
+ const ComponentContextResolver_1 = require("./ComponentContextResolver");
13
+ const HtmlTagGuards_1 = require("../injection/HtmlTagGuards");
14
+ const IdStrategy_1 = require("../injection/IdStrategy");
11
15
  class ComponentExtractor {
12
16
  constructor(source, routes) {
13
17
  this.source = source;
@@ -26,6 +30,66 @@ class ComponentExtractor {
26
30
  }
27
31
  return elements;
28
32
  }
33
+ /**
34
+ * Extract component-level analysis including state, handlers, and interaction graphs.
35
+ * Returns PolicyComponent data suitable for policy document generation.
36
+ */
37
+ async extractComponents() {
38
+ const components = [];
39
+ const contextResolver = new ComponentContextResolver_1.ComponentContextResolver();
40
+ const graphExtractor = new InteractionGraphExtractor_1.InteractionGraphExtractor();
41
+ for (const route of this.routes) {
42
+ const moduleFiles = this.getModuleFilesForRoute(route.filePath);
43
+ for (const filePath of moduleFiles) {
44
+ const source = this.readSource(filePath);
45
+ if (!source)
46
+ continue;
47
+ let ast;
48
+ try {
49
+ ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ // Find all component declarations and extract their analysis
55
+ (0, traverse_1.default)(ast, {
56
+ FunctionDeclaration: (path) => {
57
+ const componentName = path.node.id?.name;
58
+ if (componentName && /^[A-Z]/.test(componentName)) {
59
+ const context = contextResolver.extractContext(path, componentName);
60
+ const interactions = graphExtractor.extractInteractions(path, componentName);
61
+ components.push({
62
+ name: componentName,
63
+ filePath,
64
+ route: route.path,
65
+ stateVariables: context.stateVariables,
66
+ interactions,
67
+ });
68
+ }
69
+ },
70
+ VariableDeclarator: (path) => {
71
+ const componentName = path.node.id?.name;
72
+ if (componentName &&
73
+ /^[A-Z]/.test(componentName) &&
74
+ (path.node.init?.type === 'ArrowFunctionExpression' ||
75
+ path.node.init?.type === 'FunctionExpression')) {
76
+ const funcPath = path.get('init');
77
+ const context = contextResolver.extractContext(funcPath, componentName);
78
+ const interactions = graphExtractor.extractInteractions(funcPath, componentName);
79
+ components.push({
80
+ name: componentName,
81
+ filePath,
82
+ route: route.path,
83
+ stateVariables: context.stateVariables,
84
+ interactions,
85
+ });
86
+ }
87
+ },
88
+ });
89
+ }
90
+ }
91
+ return components;
92
+ }
29
93
  getModuleFilesForRoute(entryFilePath) {
30
94
  if (this.source.type === 'webpack') {
31
95
  return this.walkWebpackGraph(entryFilePath, this.source.compilation);
@@ -195,20 +259,32 @@ class ComponentExtractor {
195
259
  const tagName = nodePath.node.name?.name;
196
260
  if (!tagName)
197
261
  return;
198
- const isInteractive = ['button', 'input', 'select', 'textarea', 'a', 'form'].includes(tagName) ||
199
- nodePath.node.attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'onClick');
200
- if (!isInteractive)
262
+ // Navigation components (Link, NavLink) are included for flow graph building
263
+ // but flagged so the selector manifest skips them
264
+ const isNavigationComponent = (tagName === 'Link' || tagName === 'NavLink' || tagName === 'RouterLink') &&
265
+ nodePath.node.attributes.some((a) => a.name?.name === 'href' || a.name?.name === 'to');
266
+ // Only intrinsic HTML tags get data-clippy-id injected; skip React components
267
+ if (!(0, HtmlTagGuards_1.isIntrinsicTag)(tagName) && !isNavigationComponent)
268
+ return;
269
+ const INTERACTIVE_TAGS = new Set(['button', 'input', 'select', 'textarea', 'a', 'form', 'label']);
270
+ if (!isNavigationComponent && !INTERACTIVE_TAGS.has(tagName))
201
271
  return;
202
272
  const staticProps = extractStaticProps(nodePath.node.attributes);
203
273
  const nearbyText = extractNearbyText(nodePath);
274
+ const label = deriveLabel(staticProps, nearbyText) ?? undefined;
275
+ const componentName = (0, IdStrategy_1.findEnclosingComponentName)(nodePath) ?? undefined;
204
276
  elements.push({
205
277
  tag: tagName,
206
278
  filePath,
207
279
  route,
208
280
  staticProps,
209
281
  nearbyText,
282
+ label,
283
+ component: componentName,
210
284
  semanticRole: inferSemanticRole(tagName, staticProps, nearbyText),
211
285
  interactions: inferInteractions(tagName, staticProps),
286
+ loc: { line: nodePath.node.loc?.start?.line ?? 0 },
287
+ isNavigationLink: isNavigationComponent,
212
288
  });
213
289
  },
214
290
  });
@@ -268,18 +344,55 @@ function extractStaticProps(attributes) {
268
344
  return props;
269
345
  }
270
346
  function extractNearbyText(nodePath) {
271
- const texts = [];
272
347
  const parent = nodePath.parentPath?.node;
273
- if (parent?.children) {
274
- for (const child of parent.children) {
275
- if (child.type === 'JSXText') {
276
- const text = child.value.trim();
277
- if (text)
278
- texts.push(text);
348
+ if (!parent?.children)
349
+ return [];
350
+ return extractTextFromChildren(parent.children, 0);
351
+ }
352
+ function extractTextFromChildren(children, depth) {
353
+ if (depth > 3)
354
+ return [];
355
+ const texts = [];
356
+ for (const child of children) {
357
+ if (child.type === 'JSXText') {
358
+ const t = child.value.trim();
359
+ if (t)
360
+ texts.push(t);
361
+ }
362
+ else if (child.type === 'JSXExpressionContainer') {
363
+ if (child.expression?.type === 'StringLiteral') {
364
+ texts.push(child.expression.value);
365
+ }
366
+ else if (child.expression?.type === 'TemplateLiteral' &&
367
+ child.expression.quasis?.length === 1) {
368
+ const raw = child.expression.quasis[0].value.raw.trim();
369
+ if (raw)
370
+ texts.push(raw);
371
+ }
372
+ }
373
+ else if (child.type === 'JSXElement') {
374
+ const childTag = child.openingElement?.name?.name || '';
375
+ // Skip icon-like leaf components — they contribute no readable text
376
+ if (!/^(Icon|Svg|Loader|Spinner|Arrow|Check|Plus|Minus|Close|X)[A-Z]?/i.test(childTag)) {
377
+ texts.push(...extractTextFromChildren(child.children || [], depth + 1));
279
378
  }
280
379
  }
281
380
  }
282
- return texts;
381
+ return texts.filter(Boolean);
382
+ }
383
+ function deriveLabel(props, nearbyText) {
384
+ if (props['aria-label'])
385
+ return props['aria-label'];
386
+ if (props['title'])
387
+ return props['title'];
388
+ const joined = nearbyText.join(' ').trim();
389
+ if (joined && joined.length <= 60)
390
+ return joined;
391
+ if (props['placeholder'])
392
+ return props['placeholder'];
393
+ if (props['name'])
394
+ return props['name'];
395
+ return null;
283
396
  }
284
397
  function inferSemanticRole(tag, props, nearbyText) {
285
398
  const label = props['aria-label'] || props['placeholder'] || nearbyText[0] || '';
@@ -1,10 +1,13 @@
1
- import type { DiscoveredRoute, DiscoveredElement, InferredFlow } from '../types';
1
+ import type { DiscoveredRoute, DiscoveredElement, InferredFlow, PolicyComponent } from '../types';
2
2
  export declare class FlowInferrer {
3
3
  private routes;
4
4
  private elements;
5
- constructor(routes: DiscoveredRoute[], elements: DiscoveredElement[]);
5
+ private components?;
6
+ constructor(routes: DiscoveredRoute[], elements: DiscoveredElement[], components?: PolicyComponent[] | undefined);
6
7
  infer(): InferredFlow[];
7
8
  private buildEdgeGraph;
8
9
  private detectLinearFlows;
10
+ private detectInteractionFlows;
9
11
  private buildChain;
12
+ private generateIntentPatterns;
10
13
  }
@@ -1,28 +1,69 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlowInferrer = void 0;
4
+ const SEMANTIC_INTENT_PATTERNS = {
5
+ 'login': ['log in', 'sign in', 'login'],
6
+ 'signup': ['sign up', 'create account', 'register', 'get started'],
7
+ 'register': ['register', 'sign up', 'create account'],
8
+ 'forgot-password': ['forgot password', 'reset password', 'recover account'],
9
+ 'reset-password': ['reset my password', 'set new password', 'change password'],
10
+ 'verify': ['verify email', 'verify account', 'confirm email'],
11
+ 'otp': ['enter code', 'verify code', 'enter otp'],
12
+ 'billing': ['upgrade plan', 'manage billing', 'change subscription', 'view billing'],
13
+ 'payment-method': ['add payment method', 'update card', 'change payment'],
14
+ 'settings': ['settings', 'account settings', 'preferences'],
15
+ 'team': ['invite team member', 'manage team', 'add team member'],
16
+ 'forms': ['view forms', 'manage forms', 'create form', 'my forms'],
17
+ 'builder': ['build form', 'form builder', 'edit form'],
18
+ 'submissions': ['view submissions', 'form submissions', 'see responses'],
19
+ 'analytics': ['view analytics', 'form analytics', 'see stats'],
20
+ 'templates': ['view templates', 'browse templates', 'use template'],
21
+ 'studio': ['template studio', 'create template'],
22
+ 'transactions': ['view transactions', 'transaction history', 'payment history'],
23
+ 'documents': ['view documents', 'open document', 'my documents'],
24
+ 'integrations': ['integrations', 'connect integration', 'manage integrations'],
25
+ 'developer': ['developer settings', 'api keys', 'developer dashboard'],
26
+ 'automations': ['automations', 'manage automations', 'view automations'],
27
+ 'campaigns': ['campaigns', 'view campaigns', 'manage campaigns'],
28
+ 'website': ['website', 'manage website'],
29
+ 'workflows': ['workflows', 'manage workflows'],
30
+ 'invite': ['accept invite', 'join team'],
31
+ };
4
32
  class FlowInferrer {
5
- constructor(routes, elements) {
33
+ constructor(routes, elements, components) {
6
34
  this.routes = routes;
7
35
  this.elements = elements;
36
+ this.components = components;
8
37
  }
9
38
  infer() {
10
39
  const edges = this.buildEdgeGraph();
11
- return this.detectLinearFlows(edges);
40
+ const routeFlows = this.detectLinearFlows(edges);
41
+ const interactionFlows = this.detectInteractionFlows();
42
+ return [...routeFlows, ...interactionFlows];
12
43
  }
13
44
  buildEdgeGraph() {
14
45
  const edges = [];
15
46
  const routePaths = new Set(this.routes.map((r) => r.path));
16
47
  for (const el of this.elements) {
48
+ // Include both native <a> tags and navigation components (Link, NavLink)
49
+ const isNav = el.tag === 'a' ||
50
+ el.isNavigationLink === true;
17
51
  const href = el.staticProps['href'] || el.staticProps['to'];
18
- if (el.tag === 'a' && href && routePaths.has(href)) {
19
- edges.push({
20
- from: el.route,
21
- to: href,
22
- trigger: el.staticProps['aria-label'] || el.nearbyText[0] || 'link',
23
- confidence: 0.85,
24
- });
25
- }
52
+ if (!isNav || !href)
53
+ continue;
54
+ // Only build edges to known internal routes
55
+ if (!routePaths.has(href))
56
+ continue;
57
+ const triggerText = el.staticProps['aria-label'] ||
58
+ el.label ||
59
+ el.nearbyText[0] ||
60
+ 'link';
61
+ edges.push({
62
+ from: el.route,
63
+ to: href,
64
+ trigger: triggerText,
65
+ confidence: 0.85,
66
+ });
26
67
  }
27
68
  return edges;
28
69
  }
@@ -40,22 +81,63 @@ class FlowInferrer {
40
81
  continue;
41
82
  const chain = this.buildChain(route.path, adjacency, new Set());
42
83
  if (chain.length >= 2) {
84
+ const chainEdges = edges.filter((e) => {
85
+ const s = new Set(chain);
86
+ return s.has(e.from) && s.has(e.to);
87
+ });
88
+ const intentPatterns = this.generateIntentPatterns(chain, chainEdges);
43
89
  flows.push({
44
90
  id: `flow_${flows.length + 1}`,
45
91
  name: chain
46
92
  .map((p) => p.split('/').filter(Boolean).pop() || 'home')
47
93
  .join(' -> '),
94
+ page: chain[0],
48
95
  steps: chain,
49
- edges: edges.filter((e) => {
50
- const s = new Set(chain);
51
- return s.has(e.from) && s.has(e.to);
52
- }),
96
+ edges: chainEdges,
97
+ intentPatterns,
53
98
  });
54
99
  chain.forEach((p) => visited.add(p));
55
100
  }
56
101
  }
57
102
  return flows;
58
103
  }
104
+ detectInteractionFlows() {
105
+ if (!this.components || this.components.length === 0)
106
+ return [];
107
+ const flows = [];
108
+ for (const component of this.components) {
109
+ for (const interaction of component.interactions || []) {
110
+ // Only emit flows for interactions with a meaningful wait strategy
111
+ if (!interaction.trigger?.event)
112
+ continue;
113
+ if (!interaction.effect || interaction.effect.waitStrategy === 'none')
114
+ continue;
115
+ const routePath = component.route || '/';
116
+ const effectTarget = interaction.effect.selector ||
117
+ (interaction.effect.rendersWhenTrue
118
+ ? `[data-clippy-component='${interaction.effect.rendersWhenTrue}']`
119
+ : null);
120
+ if (!effectTarget)
121
+ continue;
122
+ flows.push({
123
+ id: `flow_${flows.length + 1}`,
124
+ name: interaction.effect.rendersWhenTrue || interaction.trigger.event,
125
+ page: routePath,
126
+ steps: [routePath],
127
+ edges: [
128
+ {
129
+ from: routePath,
130
+ to: routePath,
131
+ trigger: interaction.trigger.event,
132
+ confidence: 0.65,
133
+ },
134
+ ],
135
+ intentPatterns: [],
136
+ });
137
+ }
138
+ }
139
+ return flows;
140
+ }
59
141
  buildChain(start, adjacency, seen) {
60
142
  if (seen.has(start))
61
143
  return [];
@@ -66,5 +148,26 @@ class FlowInferrer {
66
148
  const best = [...next].sort((a, b) => b.confidence - a.confidence)[0];
67
149
  return [start, ...this.buildChain(best.to, adjacency, seen)];
68
150
  }
151
+ generateIntentPatterns(chain, edges) {
152
+ const patterns = new Set();
153
+ // 1. Route-segment semantic lookup
154
+ for (const routePath of chain) {
155
+ const segments = routePath.split('/').filter(Boolean);
156
+ for (const segment of segments) {
157
+ const clean = segment.replace(/[\[\]$:]/g, '').toLowerCase();
158
+ const known = SEMANTIC_INTENT_PATTERNS[clean];
159
+ if (known)
160
+ known.forEach((p) => patterns.add(p));
161
+ }
162
+ }
163
+ // 2. Link/button trigger text from the edges themselves
164
+ for (const edge of edges) {
165
+ const trigger = edge.trigger?.trim();
166
+ if (trigger && trigger !== 'link' && trigger.length > 2) {
167
+ patterns.add(trigger.toLowerCase());
168
+ }
169
+ }
170
+ return Array.from(patterns).filter(Boolean);
171
+ }
69
172
  }
70
173
  exports.FlowInferrer = FlowInferrer;
@@ -0,0 +1,28 @@
1
+ import type { NodePath } from '@babel/traverse';
2
+ import type { ComponentInteraction } from '../types';
3
+ /**
4
+ * Extracts interaction graphs from React component ASTs.
5
+ * Maps event handlers -> state mutations -> conditional renders, navigation, and async effects.
6
+ */
7
+ export declare class InteractionGraphExtractor {
8
+ private contextResolver;
9
+ /**
10
+ * Extract all interactions (trigger -> effect mappings) from a component.
11
+ */
12
+ extractInteractions(componentPath: NodePath<any>, componentName: string): ComponentInteraction[];
13
+ private buildInteractionFromSetters;
14
+ /**
15
+ * Detect router.push() / router.replace() / navigate() calls inside a handler.
16
+ * Returns the destination route string if found, otherwise null.
17
+ */
18
+ private resolveRouterNavigation;
19
+ private findHandlerBody;
20
+ private extractRouterCallFromNode;
21
+ private resolveStateSettersForAttribute;
22
+ private collectSettersFromNode;
23
+ private findConditionalEffect;
24
+ private findConditionalEffectInAncestors;
25
+ private findConditionalEffectInComponent;
26
+ private nodeReferencesState;
27
+ private extractComponentNameFromNode;
28
+ }