@wtdlee/repomap 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,6 +50,7 @@ Powered by [SWC](https://swc.rs/) for blazing-fast AST parsing. Analyze large-sc
50
50
  - **Field details** - View all fields with types and arguments
51
51
  - **Usage tracking** - See where operations are used in components
52
52
  - **Component integration** - Track GraphQL usage through component dependencies
53
+ - **Custom wrapper hooks** - Configurable hook presets/patterns (Apollo/urql/Relay/custom)
53
54
 
54
55
  ### 📊 Data Flow
55
56
  - **Visual diagrams** - Mermaid-generated flowcharts
@@ -425,6 +426,7 @@ export default config;
425
426
  - Auto-generated markdown documentation
426
427
  - Navigation sidebar
427
428
  - Syntax-highlighted code blocks
429
+ - Coverage metrics (parse failures, codegen detection) to prevent silent omissions
428
430
 
429
431
  ## Supported Frameworks
430
432
 
@@ -466,6 +468,7 @@ interface AnalysisResult {
466
468
  timestamp: string;
467
469
  version: string;
468
470
  commitHash: string;
471
+ coverage?: CoverageMetrics;
469
472
  pages: PageInfo[];
470
473
  graphqlOperations: GraphQLOperation[];
471
474
  apiCalls: APICall[];
@@ -476,6 +479,15 @@ interface AnalysisResult {
476
479
  crossRepoLinks: CrossRepoLink[];
477
480
  }
478
481
 
482
+ interface CoverageMetrics {
483
+ tsFilesScanned: number;
484
+ tsParseFailures: number;
485
+ graphqlParseFailures: number;
486
+ codegenFilesDetected: number;
487
+ codegenFilesParsed: number;
488
+ codegenExportsFound: number;
489
+ }
490
+
479
491
  // Report
480
492
  interface DocumentationReport {
481
493
  generatedAt: string;
@@ -1,5 +1,5 @@
1
- import { B as BaseAnalyzer } from '../dataflow-analyzer-CJ2T0cGS.js';
2
- export { D as DataFlowAnalyzer, G as GraphQLAnalyzer, P as PagesAnalyzer } from '../dataflow-analyzer-CJ2T0cGS.js';
1
+ import { B as BaseAnalyzer } from '../dataflow-analyzer-DIUsRpvv.js';
2
+ export { D as DataFlowAnalyzer, G as GraphQLAnalyzer, P as PagesAnalyzer } from '../dataflow-analyzer-DIUsRpvv.js';
3
3
  import { Module, CallExpression } from '@swc/core';
4
4
  import { DataFetchingInfo, RepositoryConfig, AnalysisResult } from '../types.js';
5
5
 
@@ -33,7 +33,7 @@ declare const HOOK_TYPE_MAP: Record<string, DataFetchingInfo['type']>;
33
33
  /**
34
34
  * Keywords that indicate GraphQL usage in a file
35
35
  */
36
- declare const GRAPHQL_INDICATORS: readonly ["Document", "useQuery", "useMutation", "useLazyQuery", "useSuspenseQuery", "useBackgroundQuery", "useSubscription", "Query", "Mutation", "gql", "graphql", "GET_", "FETCH_", "SEARCH_", "CREATE_", "UPDATE_", "DELETE_", "SUBSCRIBE_", "@apollo", "ApolloClient"];
36
+ declare const GRAPHQL_INDICATORS: readonly ["Document", "useQuery", "useMutation", "useLazyQuery", "useSuspenseQuery", "useBackgroundQuery", "useSubscription", "Query", "Mutation", "gql", "graphql", "GET_", "FETCH_", "SEARCH_", "CREATE_", "UPDATE_", "DELETE_", "SUBSCRIBE_", "@apollo", "ApolloClient", "@urql", "urql", "react-relay", "Relay"];
37
37
  /**
38
38
  * Context extracted from a file for GraphQL operation resolution
39
39
  */
@@ -62,23 +62,23 @@ interface ExtractedGraphQLOperation {
62
62
  /**
63
63
  * Check if a hook name is a GraphQL query hook
64
64
  */
65
- declare function isQueryHook(hookName: string): boolean;
65
+ declare function isQueryHook(hookName: string, extraHookPatterns?: string[]): boolean;
66
66
  /**
67
67
  * Check if a hook name is a GraphQL mutation hook
68
68
  */
69
- declare function isMutationHook(hookName: string): boolean;
69
+ declare function isMutationHook(hookName: string, extraHookPatterns?: string[]): boolean;
70
70
  /**
71
71
  * Check if a hook name is a GraphQL subscription hook
72
72
  */
73
- declare function isSubscriptionHook(hookName: string): boolean;
73
+ declare function isSubscriptionHook(hookName: string, extraHookPatterns?: string[]): boolean;
74
74
  /**
75
75
  * Check if a hook name is any GraphQL hook
76
76
  */
77
- declare function isGraphQLHook(hookName: string): boolean;
77
+ declare function isGraphQLHook(hookName: string, extraHookPatterns?: string[]): boolean;
78
78
  /**
79
79
  * Get the data fetching type for a hook
80
80
  */
81
- declare function getHookType(hookName: string): DataFetchingInfo['type'];
81
+ declare function getHookType(hookName: string, extraHookPatterns?: string[]): DataFetchingInfo['type'];
82
82
  /**
83
83
  * Check if content has any GraphQL indicators
84
84
  */
@@ -1 +1 @@
1
- export{e as ALL_GRAPHQL_HOOKS,a as BaseAnalyzer,A as DataFlowAnalyzer,g as GRAPHQL_INDICATORS,c as GRAPHQL_MUTATION_HOOKS,d as GRAPHQL_OTHER_HOOKS,b as GRAPHQL_QUERY_HOOKS,z as GraphQLAnalyzer,f as HOOK_TYPE_MAP,y as PagesAnalyzer,B as RestApiAnalyzer,n as cleanOperationName,r as extractGraphQLContext,w as extractGraphQLOperationsFromFile,s as extractOperationNameFromGqlCall,t as extractOperationNameFromTemplate,p as getCalleeName,x as getHookInfoString,l as getHookType,u as hasGraphQLArgument,m as hasGraphQLIndicators,k as isGraphQLHook,i as isMutationHook,h as isQueryHook,j as isSubscriptionHook,o as parseToAst,v as resolveOperationName,q as traverseAst}from'../chunk-QDVE7MT3.js';
1
+ export{f as ALL_GRAPHQL_HOOKS,a as BaseAnalyzer,B as DataFlowAnalyzer,h as GRAPHQL_INDICATORS,d as GRAPHQL_MUTATION_HOOKS,e as GRAPHQL_OTHER_HOOKS,c as GRAPHQL_QUERY_HOOKS,A as GraphQLAnalyzer,g as HOOK_TYPE_MAP,z as PagesAnalyzer,C as RestApiAnalyzer,o as cleanOperationName,s as extractGraphQLContext,x as extractGraphQLOperationsFromFile,t as extractOperationNameFromGqlCall,u as extractOperationNameFromTemplate,q as getCalleeName,y as getHookInfoString,m as getHookType,v as hasGraphQLArgument,n as hasGraphQLIndicators,l as isGraphQLHook,j as isMutationHook,i as isQueryHook,k as isSubscriptionHook,p as parseToAst,w as resolveOperationName,r as traverseAst}from'../chunk-IGRCLYVZ.js';import'../chunk-ZWRDP37E.js';
@@ -1,4 +1,4 @@
1
- var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let r=[],f=n?.envResult,t=n?.railsAnalysis,l=n?.activeTab||"pages",e=s.repositories[0]?.displayName||s.repositories[0]?.name||"Repository";for(let p of s.repositories){this.graphqlOps.push(...p.analysis?.graphqlOperations||[]),this.apiCalls.push(...p.analysis?.apiCalls||[]);let u=p.analysis?.components||[];for(let a of u)this.components.push({name:a.name,filePath:a.filePath,type:a.type,dependencies:a.dependencies||[],hooks:a.hooks||[]});}for(let p of s.repositories){let u=p.analysis?.pages||[];for(let a of u)r.push({...a,repo:p.name,children:[],parent:null,depth:0});}let{rootPages:i,relations:o}=this.buildHierarchy(r);return this.renderPageMapHtml(r,i,o,e,{envResult:f,railsAnalysis:t,activeTab:l})}buildHierarchy(s){let n=new Map,r=[];for(let t of s)n.set(t.path,t);for(let t of s){let l=t.path.split("/").filter(Boolean);for(let e=l.length-1;e>=1;e--){let i="/"+l.slice(0,e).join("/"),o=n.get(i);if(o){t.parent=i,t.depth=o.depth+1,o.children.includes(t.path)||o.children.push(t.path),r.push({from:i,to:t.path,type:"parent-child",description:`Sub-page of ${i}`});break}}if(t.parent||(t.depth=Math.max(0,l.length-1)),t.layout)for(let e of s)e.path!==t.path&&e.layout===t.layout&&(r.find(o=>o.type==="same-layout"&&(o.from===t.path&&o.to===e.path||o.from===e.path&&o.to===t.path))||r.push({from:t.path,to:e.path,type:"same-layout",description:`Both use ${t.layout}`}));}return {rootPages:s.filter(t=>!t.parent).sort((t,l)=>t.path.localeCompare(l.path)),relations:r}}renderPageMapHtml(s,n,r,f,t){let l=t?.envResult,e=t?.railsAnalysis,i=t?.activeTab||"pages",o=JSON.stringify(this.graphqlOps.map(c=>({name:c.name,type:c.type,variables:c.variables,fields:c.fields,returnType:c.returnType,usedIn:c.usedIn}))),p=JSON.stringify(this.components),u=e?JSON.stringify(e.routes.routes):"[]",a=e?JSON.stringify(e.controllers.controllers):"[]",m=e?JSON.stringify(e.models.models):"[]",v=e?JSON.stringify(e.views):'{ "views": [], "pages": [], "summary": {} }',y=e?JSON.stringify(e.react):'{ "components": [], "entryPoints": [], "summary": {} }',d=e?JSON.stringify(e.grpc):'{ "services": [] }',C=e?JSON.stringify(e.summary):"null",h=l?.hasRails||false,b=l?.hasNextjs||false,w=l?.hasReact||false,x=new Map;for(let c of s){let g=c.path.split("/").filter(Boolean)[0]||"root";x.has(g)||x.set(g,[]),x.get(g)?.push(c);}return `<!DOCTYPE html>
1
+ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let r=[],u=n?.envResult,t=n?.railsAnalysis,o=n?.activeTab||"pages",e=i.repositories[0]?.displayName||i.repositories[0]?.name||"Repository";for(let p of i.repositories){this.graphqlOps.push(...p.analysis?.graphqlOperations||[]),this.apiCalls.push(...p.analysis?.apiCalls||[]);let g=p.analysis?.components||[];for(let a of g)this.components.push({name:a.name,filePath:a.filePath,type:a.type,dependencies:a.dependencies||[],hooks:a.hooks||[]});}for(let p of i.repositories){let g=p.analysis?.pages||[];for(let a of g)r.push({...a,repo:p.name,children:[],parent:null,depth:0});}let{rootPages:l,relations:s}=this.buildHierarchy(r);return this.renderPageMapHtml(r,l,s,e,{envResult:u,railsAnalysis:t,activeTab:o})}buildHierarchy(i){let n=new Map,r=[];for(let t of i)n.set(t.path,t);for(let t of i){let o=t.path.split("/").filter(Boolean);for(let e=o.length-1;e>=1;e--){let l="/"+o.slice(0,e).join("/"),s=n.get(l);if(s){t.parent=l,t.depth=s.depth+1,s.children.includes(t.path)||s.children.push(t.path),r.push({from:l,to:t.path,type:"parent-child",description:`Sub-page of ${l}`});break}}if(t.parent||(t.depth=Math.max(0,o.length-1)),t.layout)for(let e of i)e.path!==t.path&&e.layout===t.layout&&(r.find(s=>s.type==="same-layout"&&(s.from===t.path&&s.to===e.path||s.from===e.path&&s.to===t.path))||r.push({from:t.path,to:e.path,type:"same-layout",description:`Both use ${t.layout}`}));}return {rootPages:i.filter(t=>!t.parent).sort((t,o)=>t.path.localeCompare(o.path)),relations:r}}renderPageMapHtml(i,n,r,u,t){let o=t?.envResult,e=t?.railsAnalysis,l=t?.activeTab||"pages",s=c=>JSON.stringify(c).replace(/</g,"\\u003c"),p=s(this.graphqlOps.map(c=>({name:c.name,type:c.type,variables:c.variables,fields:c.fields,returnType:c.returnType,usedIn:c.usedIn}))),g=s(this.components),a=s(i),h=s(r),v=s(this.apiCalls),y=e?s(e.routes.routes):"[]",d=e?s(e.controllers.controllers):"[]",w=e?s(e.models.models):"[]",C=e?s(e.views):'{ "views": [], "pages": [], "summary": {} }',P=e?s(e.react):'{ "components": [], "entryPoints": [], "summary": {} }',x=e?s(e.grpc):'{ "services": [] }',S=e?s(e.summary):"null",m=o?.hasRails||false,f=o?.hasNextjs||false,R=o?.hasReact||false,b=new Map;for(let c of i){let k=c.path.split("/").filter(Boolean)[0]||"root";b.has(k)||b.set(k,[]),b.get(k)?.push(c);}return `<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -14,17 +14,17 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
14
14
  <body>
15
15
  <header class="header">
16
16
  <div style="display:flex;align-items:center;gap:24px">
17
- <h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${f}</h1>
17
+ <h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${u}</h1>
18
18
  <nav style="display:flex;gap:4px">
19
- <a href="/page-map" class="nav-link ${i==="pages"?"active":""}">Page Map</a>
20
- ${h?`<a href="/rails-map" class="nav-link ${i==="rails"?"active":""}">Rails Map</a>`:""}
19
+ <a href="/page-map" class="nav-link ${l==="pages"?"active":""}">Page Map</a>
20
+ ${m?`<a href="/rails-map" class="nav-link ${l==="rails"?"active":""}">Rails Map</a>`:""}
21
21
  <a href="/docs" class="nav-link">Docs</a>
22
22
  <a href="/api/report" class="nav-link" target="_blank">API</a>
23
23
  </nav>
24
24
  </div>
25
25
  <div style="display:flex;gap:12px;align-items:center">
26
26
  <!-- Environment filter badges -->
27
- ${h&&b?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
27
+ ${m&&f?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
28
28
  <button class="env-badge env-badge-active" data-env="all" onclick="filterByEnv('all')">All</button>
29
29
  <button class="env-badge" data-env="nextjs" onclick="filterByEnv('nextjs')">\u269B\uFE0F Next.js</button>
30
30
  <button class="env-badge" data-env="rails" onclick="filterByEnv('rails')">\u{1F6E4}\uFE0F Rails</button>
@@ -63,13 +63,13 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
63
63
  <!-- Frontend Stats -->
64
64
  <h3 style="margin-top:16px;font-size:10px;text-transform:uppercase;color:var(--text2);letter-spacing:1px">Frontend</h3>
65
65
  <div class="stats" id="stats-container">
66
- <div class="stat" data-filter="pages"><div class="stat-val">${s.length}</div><div class="stat-label">Pages</div></div>
66
+ <div class="stat" data-filter="pages"><div class="stat-val">${i.length}</div><div class="stat-label">Pages</div></div>
67
67
  <div class="stat" data-filter="hierarchies"><div class="stat-val">${r.filter(c=>c.type==="parent-child").length}</div><div class="stat-label">Hierarchies</div></div>
68
68
  <div class="stat" data-filter="graphql"><div class="stat-val">${this.graphqlOps.length}</div><div class="stat-label">GraphQL</div></div>
69
69
  <div class="stat" data-filter="restapi"><div class="stat-val">${this.apiCalls.length}</div><div class="stat-label">REST API</div></div>
70
70
  </div>
71
71
 
72
- ${h&&e?`
72
+ ${m&&e?`
73
73
  <!-- Rails Stats -->
74
74
  <h3 style="margin-top:16px;font-size:10px;text-transform:uppercase;color:var(--text2);letter-spacing:1px;cursor:pointer" onclick="switchToRailsTab()">Rails Backend</h3>
75
75
  <div class="stats" id="rails-stats">
@@ -84,19 +84,19 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
84
84
 
85
85
  <div class="content">
86
86
  <!-- Pages Tree View (for all screens - Next.js/React/Rails) -->
87
- <div class="tree-view ${i==="pages"?"active":""}" id="tree-view" data-tab="pages">
88
- ${s.length>0?this.buildTreeHtml(x,s):""}
89
- <div id="page-map-react-components-section" style="${h?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
87
+ <div class="tree-view ${l==="pages"?"active":""}" id="tree-view" data-tab="pages">
88
+ ${i.length>0?this.buildTreeHtml(b,i):""}
89
+ <div id="page-map-react-components-section" style="${m?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
90
90
  </div>
91
- <div id="page-map-rails-section" style="${s.length>0&&h?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
92
- ${h&&s.length===0?'<div class="empty-state-sm">Loading screens...</div>':""}
91
+ <div id="page-map-rails-section" style="${i.length>0&&m?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
92
+ ${m&&i.length===0?'<div class="empty-state-sm">Loading screens...</div>':""}
93
93
  </div>
94
94
  </div>
95
95
 
96
96
  <!-- Rails Routes View (dedicated) -->
97
- <div class="tree-view ${i==="rails"?"active":""}" id="rails-tree-view" data-tab="rails">
97
+ <div class="tree-view ${l==="rails"?"active":""}" id="rails-tree-view" data-tab="rails">
98
98
  <div id="rails-routes-container">
99
- ${h?'<div class="empty-state-sm">Loading Rails routes...</div>':'<div class="empty-state">No Rails environment detected</div>'}
99
+ ${m?'<div class="empty-state-sm">Loading Rails routes...</div>':'<div class="empty-state">No Rails environment detected</div>'}
100
100
  </div>
101
101
  </div>
102
102
 
@@ -138,33 +138,35 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
138
138
  <script>
139
139
  // Environment detection results
140
140
  const envInfo = {
141
- hasRails: ${h},
142
- hasNextjs: ${b},
143
- hasReact: ${w}
141
+ hasRails: ${m},
142
+ hasNextjs: ${f},
143
+ hasReact: ${R}
144
144
  };
145
145
 
146
146
  // Frontend data
147
- const pages = ${JSON.stringify(s)};
148
- const relations = ${JSON.stringify(r)};
149
- const graphqlOps = ${o};
150
- const components = ${p};
151
- const apiCallsData = ${JSON.stringify(this.apiCalls)};
147
+ const pages = ${a};
148
+ const relations = ${h};
149
+ const graphqlOps = ${p};
150
+ const components = ${g};
151
+ const apiCallsData = ${v};
152
152
  window.apiCalls = apiCallsData;
153
153
  const pageMap = new Map(pages.map(p => [p.path, p]));
154
154
  const gqlMap = new Map(graphqlOps.map(op => [op.name, op]));
155
155
  const compMap = new Map(components.map(c => [c.name, c]));
156
+ // Mapping metadata for UI debugging (key -> {confidence, evidence})
157
+ const opMetaMap = new Map();
156
158
 
157
159
  // Rails data (if available)
158
- const railsRoutes = ${u};
159
- const railsControllers = ${a};
160
- const railsModels = ${m};
161
- const railsViews = ${v};
162
- const railsReact = ${y};
163
- const railsGrpc = ${d};
164
- const railsSummary = ${C};
160
+ const railsRoutes = ${y};
161
+ const railsControllers = ${d};
162
+ const railsModels = ${w};
163
+ const railsViews = ${C};
164
+ const railsReact = ${P};
165
+ const railsGrpc = ${x};
166
+ const railsSummary = ${S};
165
167
 
166
168
  // Current active tab state
167
- let currentMainTab = '${i}';
169
+ let currentMainTab = '${l}';
168
170
 
169
171
  // Modal history stack for back navigation
170
172
  const modalHistory = [];
@@ -629,12 +631,18 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
629
631
  html += '<div class="route-stat" data-filter="grpc"><div class="route-stat-val cyan">' + totalWithGrpc + '</div><div class="route-stat-label">gRPC</div></div>';
630
632
  html += '</div>';
631
633
  // Analysis coverage indicator
632
- if (totalWithActionInfo > 0) {
633
- const coverage = Math.round((totalWithActionInfo / combinedData.length) * 100);
634
- const coverageTooltip = 'Percentage of routes successfully matched with controller actions to extract details (JSON/HTML rendering, redirects, etc). This is a tool analysis metric, not a code quality indicator.';
635
- const coverageClass = coverage > 70 ? 'coverage-high' : coverage > 40 ? 'coverage-mid' : 'coverage-low';
634
+ {
635
+ const totalRoutesAnalyzed = combinedData.length;
636
+ const totalWithControllerMatch = combinedData.filter(r => r.controllerInfo).length;
637
+ const overallCoverage = totalRoutesAnalyzed > 0 ? Math.round((totalWithActionInfo / totalRoutesAnalyzed) * 100) : 0;
638
+ const matchedCoverage = totalWithControllerMatch > 0 ? Math.round((totalWithActionInfo / totalWithControllerMatch) * 100) : 0;
639
+ const overallTooltip = 'Percentage of routes that were successfully matched with controller actions to extract details. This includes routes pointing to external controllers (gems/engines) which are not analyzable from app/controllers.';
640
+ const matchedTooltip = 'Percentage of routes with a matched app controller (app/controllers) where the target action method was found and parsed.';
641
+ const overallClass = overallCoverage > 70 ? 'coverage-high' : overallCoverage > 40 ? 'coverage-mid' : 'coverage-low';
642
+ const matchedClass = matchedCoverage > 70 ? 'coverage-high' : matchedCoverage > 40 ? 'coverage-mid' : 'coverage-low';
636
643
  html += '<div class="coverage-info">';
637
- html += '<div class="coverage-text" title="' + coverageTooltip + '">Action Details Coverage: <span class="' + coverageClass + '">' + coverage + '%</span> (' + totalWithActionInfo + '/' + combinedData.length + ' routes analyzed) \u2139\uFE0F</div>';
644
+ html += '<div class="coverage-text" title="' + overallTooltip + '">Action Details Coverage (overall): <span class="' + overallClass + '">' + overallCoverage + '%</span> (' + totalWithActionInfo + '/' + totalRoutesAnalyzed + ' routes analyzed) \u2139\uFE0F</div>';
645
+ html += '<div class="coverage-text" title="' + matchedTooltip + '">Action Details Coverage (matched controllers): <span class="' + matchedClass + '">' + matchedCoverage + '%</span> (' + totalWithActionInfo + '/' + totalWithControllerMatch + ' matched) \u2139\uFE0F</div>';
638
646
  html += '</div>';
639
647
  }
640
648
  html += '</div>';
@@ -2140,10 +2148,17 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2140
2148
  const isQ = !op.type?.includes('Mutation');
2141
2149
  const sourceForModal = op.sourceDetail || op.sourcePath;
2142
2150
  const srcArg = op.sourcePath !== 'Direct' && sourceForModal
2143
- ? ",\\'"+sourceForModal.replace(/'/g, "\\\\'")+"\\'"
2144
- : '';
2151
+ ? "\\'"+sourceForModal.replace(/'/g, "\\\\'")+"\\'"
2152
+ : 'null';
2153
+
2154
+ // Store mapping metadata in a global map and pass only a key to onclick.
2155
+ const metaKey = op.queryName + '|' + (sourceForModal || '') + '|' + (op.type || '');
2156
+ if (op.confidence || (op.evidence && op.evidence.length)) {
2157
+ opMetaMap.set(metaKey, { confidence: op.confidence, evidence: op.evidence });
2158
+ }
2159
+ const metaArg = ",\\'"+metaKey.replace(/'/g, "\\\\'")+"\\'";
2145
2160
  // detail-item keeps base padding, adds indent
2146
- dataHtml += '<div class="detail-item data-op" style="padding:8px 10px 8px '+totalPadding+'px" onclick="showDataDetail(\\''+op.queryName.replace(/'/g, "\\\\'")+"\\'"+srcArg+')">' +
2161
+ dataHtml += '<div class="detail-item data-op" style="padding:8px 10px 8px '+totalPadding+'px" onclick="showDataDetail(\\''+op.queryName.replace(/'/g, "\\\\'")+"\\',"+srcArg+metaArg+')">' +
2147
2162
  '<span class="tag '+(isQ?'tag-query':'tag-mutation')+'" style="font-size:10px">'+(isQ?'Q':'M')+'</span> '+op.queryName+'</div>';
2148
2163
  });
2149
2164
 
@@ -2649,12 +2664,28 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2649
2664
  btn.remove();
2650
2665
  };
2651
2666
 
2652
- function showDataDetail(rawName, sourcePath) {
2667
+ function showDataDetail(rawName, sourcePath, metaKeyOrJson) {
2653
2668
  // Clean up name: remove "\u2192 " prefix and " (ComponentName)" suffix
2654
2669
  const name = rawName
2655
2670
  .replace(/^[\u2192\\->\\s]+/, '')
2656
2671
  .replace(/\\s*\\([^)]+\\)\\s*$/, '');
2657
2672
 
2673
+ let meta = null;
2674
+ try {
2675
+ // New: lookup by meta key
2676
+ if (metaKeyOrJson && typeof metaKeyOrJson === 'string' && opMetaMap.has(metaKeyOrJson)) {
2677
+ meta = opMetaMap.get(metaKeyOrJson);
2678
+ }
2679
+ // Backward compatibility: if someone passes raw JSON string
2680
+ else if (metaKeyOrJson && typeof metaKeyOrJson === 'string' && metaKeyOrJson.trim().startsWith('{')) {
2681
+ meta = JSON.parse(metaKeyOrJson);
2682
+ } else if (metaKeyOrJson && typeof metaKeyOrJson === 'object') {
2683
+ meta = metaKeyOrJson;
2684
+ }
2685
+ } catch {
2686
+ meta = null;
2687
+ }
2688
+
2658
2689
  // Convert SCREAMING_CASE to PascalCase (e.g., COMPANY_QUERY \u2192 CompanyQuery)
2659
2690
  const toPascalCase = (str) => {
2660
2691
  if (!/^[A-Z][A-Z0-9_]*$/.test(str)) return str;
@@ -2726,7 +2757,90 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2726
2757
 
2727
2758
  if (op) {
2728
2759
  // Found GraphQL operation
2729
- html = '<div class="detail-section"><h4>Type</h4><span class="tag '+(op.type==='mutation'?'tag-mutation':'tag-query')+'">'+op.type.toUpperCase()+'</span></div>';
2760
+ const confidence = meta?.confidence || '';
2761
+ const confidenceTheme = (level) => {
2762
+ if (level === 'certain') {
2763
+ return {
2764
+ label: 'CERTAIN',
2765
+ bg: '#22c55e',
2766
+ title: 'Certain: reached via a very close import path from the page (0\u20132 steps)',
2767
+ };
2768
+ }
2769
+ if (level === 'likely') {
2770
+ return {
2771
+ label: 'LIKELY',
2772
+ bg: '#f59e0b',
2773
+ title: 'Likely: reachable via the import graph, but indirect (3+ steps)',
2774
+ };
2775
+ }
2776
+ if (level === 'unknown') {
2777
+ return {
2778
+ label: 'COMMON',
2779
+ bg: '#64748b',
2780
+ title: 'Common: reached via widely shared modules across many pages',
2781
+ };
2782
+ }
2783
+ return null;
2784
+ };
2785
+
2786
+ const conf = confidenceTheme(confidence);
2787
+ const confidenceBadge =
2788
+ confidence === 'likely' && conf
2789
+ ? '<span class="tag" title="' +
2790
+ conf.title +
2791
+ '" style="cursor:help;font-size:10px;display:inline-flex;align-items:center;justify-content:center;height:18px;line-height:18px;padding:0 8px;border-radius:999px;background:' +
2792
+ conf.bg +
2793
+ ';color:white;opacity:0.95">' +
2794
+ conf.label +
2795
+ '</span>'
2796
+ : '';
2797
+
2798
+ html =
2799
+ '<div class="detail-section"><h4>Type</h4><div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
2800
+ '<span class="tag ' +
2801
+ (op.type === 'mutation' ? 'tag-mutation' : 'tag-query') +
2802
+ '" style="display:inline-flex;align-items:center;justify-content:center;height:18px;line-height:18px;padding:0 8px;border-radius:999px">' +
2803
+ op.type.toUpperCase() +
2804
+ '</span>' +
2805
+ confidenceBadge +
2806
+ '</div></div>';
2807
+
2808
+ const escapeHtml = (s) => String(s ?? '')
2809
+ .replace(/&/g, '&amp;')
2810
+ .replace(/</g, '&lt;')
2811
+ .replace(/>/g, '&gt;')
2812
+ .replace(/"/g, '&quot;')
2813
+ .replace(/'/g, '&#39;');
2814
+
2815
+ // Prepare Evidence section HTML (render later, after GraphQL section)
2816
+ let evidenceSectionHtml = '';
2817
+ if (meta && meta.evidence && Array.isArray(meta.evidence) && meta.evidence.length > 0) {
2818
+ evidenceSectionHtml += '<div class="detail-section"><h4>Evidence</h4>';
2819
+ meta.evidence.slice(0, 12).forEach(ev => {
2820
+ const file = ev.file || '';
2821
+ const line = ev.line ? ':' + ev.line : '';
2822
+ const detailRaw = ev.detail ? String(ev.detail) : '';
2823
+
2824
+ // Escape then enhance arrows for readability
2825
+ const detailEsc = escapeHtml(detailRaw);
2826
+ const detailPretty = detailEsc.replace(/-&gt;|->/g, '<span style="color:#60a5fa;font-weight:700;padding:0 4px">\u2192</span>');
2827
+
2828
+ evidenceSectionHtml += '<div class="detail-item" style="font-size:11px;display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%;overflow:hidden">' +
2829
+ '<code style="background:#0f172a;color:#93c5fd;padding:2px 6px;border-radius:3px;font-size:10px;display:block;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' +
2830
+ escapeHtml(file + line) +
2831
+ '</code>' +
2832
+ (detailRaw
2833
+ ? '<div style="opacity:0.92;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' + detailPretty + '</div>'
2834
+ : '') +
2835
+ '</div>';
2836
+ });
2837
+ if (meta.evidence.length > 12) {
2838
+ evidenceSectionHtml += '<div class="detail-item" style="font-size:11px;opacity:0.8">... '+(meta.evidence.length - 12)+' more</div>';
2839
+ }
2840
+ evidenceSectionHtml += '</div>';
2841
+ }
2842
+
2843
+ // Confidence is shown as a small badge next to Type to avoid taking extra vertical space.
2730
2844
 
2731
2845
  // Source info
2732
2846
  if (sourcePath) {
@@ -2738,7 +2852,15 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2738
2852
  sourcePath.endsWith('.jsx');
2739
2853
  const isHook = !looksLikeFile && sourcePath.startsWith('use');
2740
2854
  const label = looksLikeFile ? 'File' : isHook ? 'Hook' : 'Component';
2741
- html += '<div class="detail-section"><h4>Source</h4><div class="detail-item" style="font-size:12px">via '+label+': <span class="text-accent">'+sourcePath+'</span></div></div>';
2855
+ html += '<div class="detail-section">' +
2856
+ '<h4 style="display:flex;justify-content:space-between;align-items:center">' +
2857
+ 'Source' +
2858
+ '<span class="tag tag-default" style="font-size:10px">via ' + label + '</span>' +
2859
+ '</h4>' +
2860
+ '<code style="background:#0f172a;color:#93c5fd;padding:8px 10px;border-radius:6px;font-family:monospace;font-size:11px;display:block;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' +
2861
+ escapeHtml(sourcePath) +
2862
+ '</code>' +
2863
+ '</div>';
2742
2864
  }
2743
2865
 
2744
2866
  // Operation Name with copy button
@@ -2763,10 +2885,14 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2763
2885
 
2764
2886
  html += '<div class="detail-section"><h4 style="display:flex;justify-content:space-between;align-items:center">GraphQL<button class="copy-btn" onclick="copyGqlCode(this)" data-code="'+gqlCodeEscaped+'" title="Copy GraphQL">\u{1F4CB}</button></h4>';
2765
2887
  html += '<pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:11px;overflow-x:auto;white-space:pre;max-height:300px;overflow-y:auto">' + gqlCode + '</pre></div>';
2888
+ // Evidence should appear right after GraphQL section
2889
+ if (evidenceSectionHtml) html += evidenceSectionHtml;
2766
2890
  } else if (op.variables?.length) {
2767
2891
  html += '<div class="detail-section"><h4>Variables</h4>';
2768
2892
  op.variables.forEach(v => { html += '<div class="detail-item">'+v.name+': <code style="background:#0f172a;color:#93c5fd;padding:2px 6px;border-radius:3px;font-family:monospace">'+v.type+'</code>'+(v.required?' (required)':'')+'</div>'; });
2769
2893
  html += '</div>';
2894
+ // Evidence should appear right after Variables section (when GraphQL block is absent)
2895
+ if (evidenceSectionHtml) html += evidenceSectionHtml;
2770
2896
  }
2771
2897
  if (op.usedIn?.length) {
2772
2898
  html += '<div class="detail-section"><h4>Used In ('+op.usedIn.length+' files)</h4>';
@@ -3569,12 +3695,12 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3569
3695
  setTimeout(updatePageGqlCounts, 100);
3570
3696
  </script>
3571
3697
  </body>
3572
- </html>`}buildTreeHtml(s,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],f=0;return Array.from(s.entries()).sort((t,l)=>t[0].localeCompare(l[0])).map(([t,l])=>{let e=r[f++%r.length],i=l.sort((a,m)=>a.path.localeCompare(m.path)),o=new Set(i.map(a=>a.path)),p=new Map;for(let a of i){let m=a.path.split("/").filter(Boolean),v=0;for(let y=m.length-1;y>=1;y--){let d="/"+m.slice(0,y).join("/");if(o.has(d)){v=(p.get(d)??0)+1;break}}p.set(a.path,v);}let u=i.map(a=>{let m=this.getPageType(a.path),v=p.get(a.path)??0,d=a.repo||"",C=n.some(g=>g.repo&&g.repo!==d),h=d.split("/").pop()?.split("-").map(g=>g.substring(0,4)).join("-")||d.substring(0,8),b=C&&d?`<span class="tag tag-repo" title="${d}">${h}</span>`:"",w=/^\/[A-Z]/.test(a.path)||a.filePath&&a.filePath.includes("components/pages"),x=w&&a.filePath?a.filePath.replace(/\.tsx?$/,"").replace(/^(frontend\/src\/|src\/)/,""):a.path,c=w?'<span class="tag tag-info" title="SPA Component Page">SPA</span>':"";return `<div class="page-item" data-path="${a.path}" data-repo="${d}" onclick="selectPage('${a.path}')" style="--depth:${v}">
3573
- <span class="page-type" style="--type-color:${m.color}">${m.label}</span>
3574
- <span class="page-path">${x}</span>
3698
+ </html>`}buildTreeHtml(i,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],u=0;return Array.from(i.entries()).sort((t,o)=>t[0].localeCompare(o[0])).map(([t,o])=>{let e=r[u++%r.length],l=o.sort((a,h)=>a.path.localeCompare(h.path)),s=new Set(l.map(a=>a.path)),p=new Map;for(let a of l){let h=a.path.split("/").filter(Boolean),v=0;for(let y=h.length-1;y>=1;y--){let d="/"+h.slice(0,y).join("/");if(s.has(d)){v=(p.get(d)??0)+1;break}}p.set(a.path,v);}let g=l.map(a=>{let h=this.getPageType(a.path),v=p.get(a.path)??0,d=a.repo||"",w=n.some(f=>f.repo&&f.repo!==d),C=d.split("/").pop()?.split("-").map(f=>f.substring(0,4)).join("-")||d.substring(0,8),P=w&&d?`<span class="tag tag-repo" title="${d}">${C}</span>`:"",x=/^\/[A-Z]/.test(a.path)||a.filePath&&a.filePath.includes("components/pages"),S=x&&a.filePath?a.filePath.replace(/\.tsx?$/,"").replace(/^(frontend\/src\/|src\/)/,""):a.path,m=x?'<span class="tag tag-info" title="SPA Component Page">SPA</span>':"";return `<div class="page-item" data-path="${a.path}" data-repo="${d}" onclick="selectPage('${a.path}')" style="--depth:${v}">
3699
+ <span class="page-type" style="--type-color:${h.color}">${h.label}</span>
3700
+ <span class="page-path">${S}</span>
3575
3701
  <div class="page-tags">
3576
- ${b}
3577
- ${c}
3702
+ ${P}
3703
+ ${m}
3578
3704
  ${a.authentication?.required?'<span class="tag tag-auth">AUTH</span>':""}
3579
3705
  <span class="tag tag-query gql-count" data-page-path="${a.path}" style="display:none">Q:0</span>
3580
3706
  <span class="tag tag-mutation gql-count-m" data-page-path="${a.path}" style="display:none">M:0</span>
@@ -3583,7 +3709,7 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3583
3709
  <div class="group-header" onclick="toggleGroup(this)" style="--group-color:${e}">
3584
3710
  <span class="group-arrow">\u25BC</span>
3585
3711
  <span class="group-name">/${t}</span>
3586
- <span class="group-count">${l.length}</span>
3712
+ <span class="group-count">${o.length}</span>
3587
3713
  </div>
3588
- <div class="group-content">${u}</div>
3589
- </div>`}).join("")}getPageType(s){let n=s.split("/").filter(Boolean).pop()||"";return n==="new"||s.endsWith("/new")?{label:"CREATE",color:"#22c55e"}:n==="edit"||s.includes("/edit")?{label:"EDIT",color:"#f59e0b"}:n.startsWith("[")||n.startsWith(":")?{label:"DETAIL",color:"#3b82f6"}:s.includes("setting")?{label:"SETTINGS",color:"#6b7280"}:{label:"LIST",color:"#06b6d4"}}};export{P as a};
3714
+ <div class="group-content">${g}</div>
3715
+ </div>`}).join("")}getPageType(i){let n=i.split("/").filter(Boolean).pop()||"";return n==="new"||i.endsWith("/new")?{label:"CREATE",color:"#22c55e"}:n==="edit"||i.includes("/edit")?{label:"EDIT",color:"#f59e0b"}:n.startsWith("[")||n.startsWith(":")?{label:"DETAIL",color:"#3b82f6"}:i.includes("setting")?{label:"SETTINGS",color:"#6b7280"}:{label:"LIST",color:"#06b6d4"}}};export{E as a};