@dcoder-x/plugin-shared 0.1.5 → 0.1.6

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.
@@ -271,7 +271,7 @@ class ComponentExtractor {
271
271
  return;
272
272
  const staticProps = extractStaticProps(nodePath.node.attributes);
273
273
  const nearbyText = extractNearbyText(nodePath);
274
- const label = deriveLabel(staticProps, nearbyText) ?? undefined;
274
+ const label = deriveLabel(tagName, staticProps, nearbyText) ?? undefined;
275
275
  const componentName = (0, IdStrategy_1.findEnclosingComponentName)(nodePath) ?? undefined;
276
276
  elements.push({
277
277
  tag: tagName,
@@ -380,7 +380,16 @@ function extractTextFromChildren(children, depth) {
380
380
  }
381
381
  return texts.filter(Boolean);
382
382
  }
383
- function deriveLabel(props, nearbyText) {
383
+ function deriveLabel(tag, props, nearbyText) {
384
+ // Forms: child text is a concatenation of every field label + button inside the form.
385
+ // That is not a useful description. Use aria-label if present, otherwise take only
386
+ // the last short text node (typically the submit button label).
387
+ if (tag === 'form') {
388
+ if (props['aria-label'])
389
+ return props['aria-label'];
390
+ const lastShort = [...nearbyText].reverse().find((t) => t.length >= 2 && t.length <= 30);
391
+ return lastShort || null;
392
+ }
384
393
  if (props['aria-label'])
385
394
  return props['aria-label'];
386
395
  if (props['title'])
@@ -7,7 +7,25 @@ export declare class FlowInferrer {
7
7
  infer(): InferredFlow[];
8
8
  private buildEdgeGraph;
9
9
  private detectLinearFlows;
10
+ /**
11
+ * Fix 3b: A coherent chain stays within the same top-level route domain.
12
+ * A chain that jumps from /auth/* to / or from /dashboard/* to /auth/signup
13
+ * is an artifact of footer/layout navigation links, not a real user flow.
14
+ */
15
+ private isCoherentChain;
10
16
  private detectInteractionFlows;
11
17
  private buildChain;
12
18
  private generateIntentPatterns;
19
+ /**
20
+ * Returns true if a string is a usable user intent phrase.
21
+ * Rejects blocklisted words, PascalCase component names, very short strings,
22
+ * and URL-like strings.
23
+ */
24
+ private isValidIntentPattern;
25
+ /**
26
+ * Fix 4: Remove intent patterns that appear across more than 30% of all flows.
27
+ * These are structural navigation phrases (sidebar, footer links) that ended up
28
+ * attached to many unrelated flows rather than genuine user intents.
29
+ */
30
+ private deduplicateCrossFlowNoise;
13
31
  }
@@ -29,6 +29,13 @@ const SEMANTIC_INTENT_PATTERNS = {
29
29
  'workflows': ['workflows', 'manage workflows'],
30
30
  'invite': ['accept invite', 'join team'],
31
31
  };
32
+ // Fix 4: words that are never meaningful user intent phrases
33
+ const INTENT_BLOCKLIST = new Set([
34
+ 'back', 'next', 'continue', 'click', 'button', 'here', 'more',
35
+ 'loader2', 'loader', 'spinner', 'close', 'x', 'menu', 'open',
36
+ 'cancel', 'ok', 'yes', 'no', 'loading', 'view all', 'learn more',
37
+ 'read more', 'see more', 'link', 'skip', 'done', 'go',
38
+ ]);
32
39
  class FlowInferrer {
33
40
  constructor(routes, elements, components) {
34
41
  this.routes = routes;
@@ -39,34 +46,39 @@ class FlowInferrer {
39
46
  const edges = this.buildEdgeGraph();
40
47
  const routeFlows = this.detectLinearFlows(edges);
41
48
  const interactionFlows = this.detectInteractionFlows();
42
- return [...routeFlows, ...interactionFlows];
49
+ const combined = [...routeFlows, ...interactionFlows];
50
+ // Fix 4: remove patterns that appear in too many flows — they are structural
51
+ // navigation chrome (sidebars, footers) rather than user intent signals
52
+ return this.deduplicateCrossFlowNoise(combined);
43
53
  }
54
+ // ---------------------------------------------------------------------------
55
+ // Edge graph
56
+ // ---------------------------------------------------------------------------
44
57
  buildEdgeGraph() {
45
58
  const edges = [];
46
59
  const routePaths = new Set(this.routes.map((r) => r.path));
47
60
  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;
61
+ const isNav = el.tag === 'a' || el.isNavigationLink === true;
51
62
  const href = el.staticProps['href'] || el.staticProps['to'];
52
63
  if (!isNav || !href)
53
64
  continue;
54
- // Only build edges to known internal routes
55
65
  if (!routePaths.has(href))
56
66
  continue;
57
67
  const triggerText = el.staticProps['aria-label'] ||
58
68
  el.label ||
59
69
  el.nearbyText[0] ||
60
70
  'link';
61
- edges.push({
62
- from: el.route,
63
- to: href,
64
- trigger: triggerText,
65
- confidence: 0.85,
66
- });
71
+ // Fix 3a: demote confidence for elements from layout/footer/header files.
72
+ // These links appear on every page and corrupt flow chains when followed.
73
+ const isLayoutSource = /footer|layout|header|navbar|sidebar|navigation/i.test(el.filePath || '');
74
+ const confidence = isLayoutSource ? 0.3 : 0.85;
75
+ edges.push({ from: el.route, to: href, trigger: triggerText, confidence });
67
76
  }
68
77
  return edges;
69
78
  }
79
+ // ---------------------------------------------------------------------------
80
+ // Linear flow detection
81
+ // ---------------------------------------------------------------------------
70
82
  detectLinearFlows(edges) {
71
83
  const adjacency = new Map();
72
84
  for (const edge of edges) {
@@ -80,12 +92,11 @@ class FlowInferrer {
80
92
  if (visited.has(route.path))
81
93
  continue;
82
94
  const chain = this.buildChain(route.path, adjacency, new Set());
83
- if (chain.length >= 2) {
95
+ if (chain.length >= 2 && this.isCoherentChain(chain)) {
84
96
  const chainEdges = edges.filter((e) => {
85
97
  const s = new Set(chain);
86
98
  return s.has(e.from) && s.has(e.to);
87
99
  });
88
- const intentPatterns = this.generateIntentPatterns(chain, chainEdges);
89
100
  flows.push({
90
101
  id: `flow_${flows.length + 1}`,
91
102
  name: chain
@@ -94,20 +105,32 @@ class FlowInferrer {
94
105
  page: chain[0],
95
106
  steps: chain,
96
107
  edges: chainEdges,
97
- intentPatterns,
108
+ intentPatterns: this.generateIntentPatterns(chain, chainEdges),
98
109
  });
99
110
  chain.forEach((p) => visited.add(p));
100
111
  }
101
112
  }
102
113
  return flows;
103
114
  }
115
+ /**
116
+ * Fix 3b: A coherent chain stays within the same top-level route domain.
117
+ * A chain that jumps from /auth/* to / or from /dashboard/* to /auth/signup
118
+ * is an artifact of footer/layout navigation links, not a real user flow.
119
+ */
120
+ isCoherentChain(chain) {
121
+ const firstSegment = (p) => p.split('/').filter(Boolean)[0] ?? 'root';
122
+ const domain = firstSegment(chain[0]);
123
+ return chain.every((r) => firstSegment(r) === domain);
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Interaction flow detection
127
+ // ---------------------------------------------------------------------------
104
128
  detectInteractionFlows() {
105
129
  if (!this.components || this.components.length === 0)
106
130
  return [];
107
131
  const flows = [];
108
132
  for (const component of this.components) {
109
133
  for (const interaction of component.interactions || []) {
110
- // Only emit flows for interactions with a meaningful wait strategy
111
134
  if (!interaction.trigger?.event)
112
135
  continue;
113
136
  if (!interaction.effect || interaction.effect.waitStrategy === 'none')
@@ -138,6 +161,9 @@ class FlowInferrer {
138
161
  }
139
162
  return flows;
140
163
  }
164
+ // ---------------------------------------------------------------------------
165
+ // Chain builder
166
+ // ---------------------------------------------------------------------------
141
167
  buildChain(start, adjacency, seen) {
142
168
  if (seen.has(start))
143
169
  return [];
@@ -148,6 +174,9 @@ class FlowInferrer {
148
174
  const best = [...next].sort((a, b) => b.confidence - a.confidence)[0];
149
175
  return [start, ...this.buildChain(best.to, adjacency, seen)];
150
176
  }
177
+ // ---------------------------------------------------------------------------
178
+ // Intent pattern generation (Fix 4)
179
+ // ---------------------------------------------------------------------------
151
180
  generateIntentPatterns(chain, edges) {
152
181
  const patterns = new Set();
153
182
  // 1. Route-segment semantic lookup
@@ -160,14 +189,52 @@ class FlowInferrer {
160
189
  known.forEach((p) => patterns.add(p));
161
190
  }
162
191
  }
163
- // 2. Link/button trigger text from the edges themselves
192
+ // 2. Link/button trigger text from the edges — apply noise filter
164
193
  for (const edge of edges) {
165
194
  const trigger = edge.trigger?.trim();
166
- if (trigger && trigger !== 'link' && trigger.length > 2) {
195
+ if (trigger && this.isValidIntentPattern(trigger)) {
167
196
  patterns.add(trigger.toLowerCase());
168
197
  }
169
198
  }
170
199
  return Array.from(patterns).filter(Boolean);
171
200
  }
201
+ /**
202
+ * Returns true if a string is a usable user intent phrase.
203
+ * Rejects blocklisted words, PascalCase component names, very short strings,
204
+ * and URL-like strings.
205
+ */
206
+ isValidIntentPattern(pattern) {
207
+ if (!pattern || pattern.length < 4)
208
+ return false;
209
+ const lower = pattern.toLowerCase();
210
+ if (INTENT_BLOCKLIST.has(lower))
211
+ return false;
212
+ // Reject PascalCase strings — these are component names leaking through
213
+ if (/^[A-Z][a-zA-Z0-9]+$/.test(pattern))
214
+ return false;
215
+ // Reject paths and URLs
216
+ if (pattern.startsWith('/') || pattern.startsWith('http'))
217
+ return false;
218
+ return true;
219
+ }
220
+ /**
221
+ * Fix 4: Remove intent patterns that appear across more than 30% of all flows.
222
+ * These are structural navigation phrases (sidebar, footer links) that ended up
223
+ * attached to many unrelated flows rather than genuine user intents.
224
+ */
225
+ deduplicateCrossFlowNoise(flows) {
226
+ if (flows.length === 0)
227
+ return flows;
228
+ const threshold = Math.max(3, Math.ceil(flows.length * 0.3));
229
+ const counts = new Map();
230
+ flows.forEach((f) => (f.intentPatterns ?? []).forEach((p) => counts.set(p, (counts.get(p) ?? 0) + 1)));
231
+ const globalNoise = new Set([...counts.entries()].filter(([, n]) => n > threshold).map(([p]) => p));
232
+ if (globalNoise.size === 0)
233
+ return flows;
234
+ return flows.map((f) => ({
235
+ ...f,
236
+ intentPatterns: (f.intentPatterns ?? []).filter((p) => !globalNoise.has(p)),
237
+ }));
238
+ }
172
239
  }
173
240
  exports.FlowInferrer = FlowInferrer;
@@ -1,10 +1,6 @@
1
1
  import type { DiscoveredElement, ElementWithSelectors } from '../types';
2
+ import type { InjectedEntry } from '../injection/ClippyIdInjector';
2
3
  export declare class SelectorGenerator {
3
- generate(elements: DiscoveredElement[], injectedMap?: Record<string, Array<{
4
- clippyId: string;
5
- component: string;
6
- tag: string;
7
- line: number;
8
- }>>): ElementWithSelectors[];
4
+ generate(elements: DiscoveredElement[], injectedMap?: Record<string, InjectedEntry[]>): ElementWithSelectors[];
9
5
  private generateForElement;
10
6
  }
@@ -16,14 +16,19 @@ class SelectorGenerator {
16
16
  if (injectedMap && el.filePath) {
17
17
  const entries = injectedMap[el.filePath];
18
18
  if (entries && entries.length) {
19
- // Prefer exact line match when available
20
- let match = null;
21
- if (el.loc && el.loc.line) {
22
- match = entries.find((e) => e.line === el.loc.line);
19
+ const elLine = el.loc?.line;
20
+ const elLabel = el.label;
21
+ // 1. Exact line match — most reliable
22
+ let match = elLine ? entries.find((e) => e.line === elLine) : null;
23
+ // 2. Label + tag match — handles multiple same-tag elements in one file
24
+ // (e.g. two <button> tags where line lookup fails due to AST offset)
25
+ if (!match && elLabel) {
26
+ match = entries.find((e) => e.tag === el.tag && e.label === elLabel) ?? null;
23
27
  }
24
- // Fallback to tag/component match
28
+ // 3. Tag-only fallback — last resort, may pick the wrong element when
29
+ // multiple same-tag elements exist but is better than no clippy_id at all
25
30
  if (!match) {
26
- match = entries.find((e) => e.tag === el.tag);
31
+ match = entries.find((e) => e.tag === el.tag) ?? null;
27
32
  }
28
33
  if (match) {
29
34
  candidates.push({
@@ -49,7 +49,7 @@ function injectClippyIds(source, filePath, routePath) {
49
49
  }
50
50
  const line = opening.loc?.start.line ?? 0;
51
51
  // Extract visible label text from element children for ID enrichment
52
- const labelText = extractLabelFromOpeningElement(path, attrs);
52
+ const labelText = extractLabelFromOpeningElement(path, attrs, tagName);
53
53
  const clippyId = (0, IdStrategy_1.deriveClippyId)(componentName, tagName, line, routePath, labelText ?? undefined);
54
54
  const componentAttr = (0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-component')
55
55
  ? ''
@@ -77,8 +77,10 @@ function injectClippyIds(source, filePath, routePath) {
77
77
  /**
78
78
  * Extract a usable label from the element's attributes and children.
79
79
  * Priority: aria-label > title > visible text content > placeholder > name.
80
+ * For form elements: only aria-label or the last short text (submit button),
81
+ * never the full concatenation of all child field labels.
80
82
  */
81
- function extractLabelFromOpeningElement(path, attrs) {
83
+ function extractLabelFromOpeningElement(path, attrs, tagName) {
82
84
  // Check attributes first — fastest path
83
85
  for (const attr of attrs) {
84
86
  if (attr.type !== 'JSXAttribute')
@@ -96,11 +98,17 @@ function extractLabelFromOpeningElement(path, attrs) {
96
98
  const parent = path.parentPath?.node;
97
99
  if (parent?.children) {
98
100
  const text = extractTextFromChildren(parent.children, 0);
101
+ if (tagName === 'form') {
102
+ // Forms: only use the last short text node (submit button label), not the full
103
+ // concatenation of every field label inside the form.
104
+ const lastShort = [...text].reverse().find((t) => t.length >= 2 && t.length <= 30);
105
+ return lastShort || null;
106
+ }
99
107
  const joined = text.join(' ').trim();
100
108
  if (joined && joined.length <= 60) {
101
109
  const sanitized = (0, IdStrategy_1.sanitizeLabelForId)(joined);
102
110
  if (sanitized)
103
- return joined; // return raw joined text; sanitization happens in deriveClippyId
111
+ return joined;
104
112
  }
105
113
  }
106
114
  // Fallback to placeholder / name for inputs
@@ -3,6 +3,7 @@ export declare class PackageBuilder {
3
3
  private version;
4
4
  buildPackage(data: Omit<KnowledgePackage, 'generatedAt'>): KnowledgePackage;
5
5
  buildArtifacts(data: {
6
+ projectRoot?: string;
6
7
  buildId: string;
7
8
  bundler: 'webpack' | 'vite';
8
9
  routes: DiscoveredRoute[];
@@ -11,6 +12,7 @@ export declare class PackageBuilder {
11
12
  components?: PolicyComponent[];
12
13
  }): PolicyArtifacts;
13
14
  buildPolicyDocument(data: {
15
+ projectRoot?: string;
14
16
  buildId: string;
15
17
  generatedAt: string;
16
18
  bundler: 'webpack' | 'vite';
@@ -26,13 +28,21 @@ export declare class PackageBuilder {
26
28
  }): SelectorManifest;
27
29
  build(data: Omit<KnowledgePackage, 'generatedAt'>): Buffer;
28
30
  buildArtifactsBuffer(artifacts: PolicyArtifacts): Buffer;
31
+ /**
32
+ * Compute a fallback project root from the common ancestor of route file paths.
33
+ * Used only when the adapter does not provide an explicit projectRoot.
34
+ */
29
35
  private inferProjectRoot;
30
36
  private normalizePath;
37
+ /**
38
+ * Remove design-system primitives and other shared components that carry no
39
+ * state or interaction data. Normalises filePaths against the project root.
40
+ */
41
+ private filterMeaningfulComponents;
31
42
  private toPolicySelectors;
32
43
  private toPolicyComponents;
33
44
  private toPolicyFlows;
34
45
  private deriveComponentName;
35
46
  private deriveStableId;
36
47
  private extractClippyIdFromSelector;
37
- private simpleHash;
38
48
  }
@@ -40,13 +40,20 @@ class PackageBuilder {
40
40
  };
41
41
  }
42
42
  buildPolicyDocument(data) {
43
- const projectRoot = this.inferProjectRoot(data.routes);
43
+ // Use the adapter-provided project root (most reliable).
44
+ // Fall back to computing the common ancestor of route paths only when unavailable.
45
+ const projectRoot = data.projectRoot || this.inferProjectRoot(data.routes);
44
46
  const normalizedRoutes = data.routes.map((r) => ({
45
47
  ...r,
46
48
  filePath: this.normalizePath(r.filePath, projectRoot),
47
49
  layout: r.layout ? this.normalizePath(r.layout, projectRoot) : null,
48
50
  }));
49
51
  const policySelectors = this.toPolicySelectors(data.selectors, projectRoot);
52
+ // Fix 2: only keep components that carry meaningful data (state or interactions).
53
+ // Pure design-system primitives (Button, Card, Input, etc.) with empty arrays
54
+ // are excluded — they bloat the policy without adding actionable information.
55
+ const rawComponents = data.components ?? this.toPolicyComponents(data.selectors, projectRoot);
56
+ const meaningfulComponents = this.filterMeaningfulComponents(rawComponents, projectRoot);
50
57
  return {
51
58
  version: this.version,
52
59
  buildId: data.buildId,
@@ -54,7 +61,7 @@ class PackageBuilder {
54
61
  bundler: data.bundler,
55
62
  routes: normalizedRoutes,
56
63
  selectors: policySelectors,
57
- components: data.components ?? this.toPolicyComponents(data.selectors),
64
+ components: meaningfulComponents,
58
65
  flows: this.toPolicyFlows(data.flows, normalizedRoutes, policySelectors),
59
66
  };
60
67
  }
@@ -94,11 +101,17 @@ class PackageBuilder {
94
101
  buildArtifactsBuffer(artifacts) {
95
102
  return zlib_1.default.gzipSync(JSON.stringify(artifacts));
96
103
  }
104
+ // ---------------------------------------------------------------------------
105
+ // Path helpers
106
+ // ---------------------------------------------------------------------------
107
+ /**
108
+ * Compute a fallback project root from the common ancestor of route file paths.
109
+ * Used only when the adapter does not provide an explicit projectRoot.
110
+ */
97
111
  inferProjectRoot(routes) {
98
112
  const filePaths = routes.map((r) => r.filePath).filter(Boolean);
99
113
  if (filePaths.length === 0)
100
114
  return process.cwd();
101
- // Find the longest common directory prefix across all route file paths
102
115
  const parts = filePaths[0].replace(/\\/g, '/').split('/');
103
116
  let commonParts = parts;
104
117
  for (const fp of filePaths.slice(1)) {
@@ -109,17 +122,45 @@ class PackageBuilder {
109
122
  i++;
110
123
  commonParts = commonParts.slice(0, i);
111
124
  }
112
- return commonParts.join('/') || process.cwd();
125
+ // Common ancestor of route files is typically the `app/` dir.
126
+ // Go one level up to reach the actual project root.
127
+ if (commonParts.length > 0) {
128
+ const candidate = commonParts.join('/');
129
+ const parent = candidate.includes('/') ? candidate.replace(/\/[^/]+$/, '') : candidate;
130
+ if (parent)
131
+ return parent;
132
+ }
133
+ return process.cwd();
113
134
  }
114
135
  normalizePath(filePath, projectRoot) {
115
136
  try {
116
137
  const rel = path_1.default.relative(projectRoot, filePath).replace(/\\/g, '/');
138
+ // Only use the relative path if it doesn't escape above the project root
117
139
  return rel.startsWith('..') ? filePath.replace(/\\/g, '/') : rel;
118
140
  }
119
141
  catch {
120
142
  return filePath.replace(/\\/g, '/');
121
143
  }
122
144
  }
145
+ // ---------------------------------------------------------------------------
146
+ // Component filtering (Fix 2)
147
+ // ---------------------------------------------------------------------------
148
+ /**
149
+ * Remove design-system primitives and other shared components that carry no
150
+ * state or interaction data. Normalises filePaths against the project root.
151
+ */
152
+ filterMeaningfulComponents(components, projectRoot) {
153
+ return components
154
+ .filter((c) => (c.stateVariables && c.stateVariables.length > 0) ||
155
+ (c.interactions && c.interactions.length > 0))
156
+ .map((c) => ({
157
+ ...c,
158
+ filePath: this.normalizePath(c.filePath, projectRoot),
159
+ }));
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Selector conversion
163
+ // ---------------------------------------------------------------------------
123
164
  toPolicySelectors(selectors, projectRoot) {
124
165
  return selectors
125
166
  .filter((element) => !element.isNavigationLink)
@@ -132,8 +173,7 @@ class PackageBuilder {
132
173
  ? this.extractClippyIdFromSelector(preferred.value) ||
133
174
  this.deriveStableId(component, element.tag, stableLine)
134
175
  : this.deriveStableId(component, element.tag, stableLine);
135
- const selector = preferred?.value ||
136
- `[data-clippy-id='${clippyId}']`;
176
+ const selector = preferred?.value || `[data-clippy-id='${clippyId}']`;
137
177
  return {
138
178
  clippyId,
139
179
  selector,
@@ -141,13 +181,15 @@ class PackageBuilder {
141
181
  component,
142
182
  label: element.label,
143
183
  route: element.route,
144
- filePath: projectRoot ? this.normalizePath(element.filePath, projectRoot) : element.filePath.replace(/\\/g, '/'),
184
+ filePath: projectRoot
185
+ ? this.normalizePath(element.filePath, projectRoot)
186
+ : element.filePath.replace(/\\/g, '/'),
145
187
  attributes: Object.entries(element.staticProps).map(([name, value]) => ({ name, value })),
146
188
  candidates: element.selectors,
147
189
  };
148
190
  });
149
191
  }
150
- toPolicyComponents(selectors) {
192
+ toPolicyComponents(selectors, projectRoot) {
151
193
  const byFile = new Map();
152
194
  for (const element of selectors) {
153
195
  if (element.isNavigationLink)
@@ -157,32 +199,28 @@ class PackageBuilder {
157
199
  byFile.get(element.filePath).push(element);
158
200
  }
159
201
  return Array.from(byFile.entries()).map(([filePath, items]) => {
160
- // Prefer the component name captured during extraction over the filename
161
- const componentName = items.find((i) => i.component)?.component ||
162
- this.deriveComponentName(filePath);
202
+ const componentName = items.find((i) => i.component)?.component || this.deriveComponentName(filePath);
163
203
  const route = items[0]?.route || '/';
164
204
  return {
165
205
  name: componentName,
166
- filePath,
206
+ filePath: projectRoot ? this.normalizePath(filePath, projectRoot) : filePath.replace(/\\/g, '/'),
167
207
  route,
168
208
  stateVariables: [],
169
209
  interactions: [],
170
210
  };
171
211
  });
172
212
  }
213
+ // ---------------------------------------------------------------------------
214
+ // Flow conversion
215
+ // ---------------------------------------------------------------------------
173
216
  toPolicyFlows(flows, routes, selectors) {
174
217
  return flows.map((flow, flowIndex) => {
175
218
  const firstRoute = flow.steps[0] || '/';
176
219
  const page = flow.page || routes.find((r) => r.path === firstRoute)?.path || firstRoute;
177
220
  const steps = flow.steps.map((routePath, stepIndex) => {
178
- // For transition steps (not the first), find the edge that leads to this route
179
- // and try to match its trigger text to a specific selector
180
- const incomingEdge = stepIndex > 0
181
- ? flow.edges.find((e) => e.to === routePath)
182
- : null;
221
+ const incomingEdge = stepIndex > 0 ? flow.edges.find((e) => e.to === routePath) : null;
183
222
  let target;
184
223
  if (incomingEdge?.trigger && incomingEdge.trigger !== 'link') {
185
- // Try to find the specific triggering element by label match
186
224
  const fromRoute = incomingEdge.from;
187
225
  const byLabel = selectors.find((s) => s.route === fromRoute &&
188
226
  s.label?.toLowerCase() === incomingEdge.trigger.toLowerCase());
@@ -192,7 +230,6 @@ class PackageBuilder {
192
230
  : null;
193
231
  target = (byLabel || byAttr)?.selector;
194
232
  }
195
- // Fallback: first selector on the destination route
196
233
  if (!target) {
197
234
  target =
198
235
  selectors.find((s) => s.route === routePath)?.selector ||
@@ -202,9 +239,7 @@ class PackageBuilder {
202
239
  const isInteractionStep = flow.edges.some((e) => e.from === e.to && e.trigger?.startsWith('on'));
203
240
  return {
204
241
  step: stepIndex + 1,
205
- action: isFirst
206
- ? isInteractionStep ? 'interact' : 'navigate'
207
- : 'transition',
242
+ action: isFirst ? (isInteractionStep ? 'interact' : 'navigate') : 'transition',
208
243
  target,
209
244
  };
210
245
  });
@@ -219,6 +254,9 @@ class PackageBuilder {
219
254
  };
220
255
  });
221
256
  }
257
+ // ---------------------------------------------------------------------------
258
+ // Helpers
259
+ // ---------------------------------------------------------------------------
222
260
  deriveComponentName(filePath) {
223
261
  return (0, IdStrategy_1.deriveComponentName)(filePath);
224
262
  }
@@ -229,12 +267,5 @@ class PackageBuilder {
229
267
  const match = selector.match(/\[data-clippy-id=['\"]([^'\"]+)['\"]\]/);
230
268
  return match?.[1] || null;
231
269
  }
232
- simpleHash(input) {
233
- let hash = 0;
234
- for (let i = 0; i < input.length; i++) {
235
- hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
236
- }
237
- return hash.toString(36);
238
- }
239
270
  }
240
271
  exports.PackageBuilder = PackageBuilder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcoder-x/plugin-shared",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",