project-graph-mcp 2.1.2 → 2.1.4

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 (70) hide show
  1. package/GUIDE.md +237 -0
  2. package/package.json +2 -1
  3. package/rules/test-rules.json +15 -0
  4. package/src/.project-graph-cache.json +1 -1
  5. package/src/analysis/analysis-cache.js +3 -1
  6. package/src/analysis/complexity.js +9 -13
  7. package/src/analysis/custom-rules.js +16 -35
  8. package/src/analysis/db-analysis.js +2 -6
  9. package/src/analysis/dead-code.js +8 -18
  10. package/src/analysis/full-analysis.js +9 -17
  11. package/src/analysis/jsdoc-checker.js +11 -23
  12. package/src/analysis/jsdoc-generator.js +8 -9
  13. package/src/analysis/similar-functions.js +8 -15
  14. package/src/analysis/test-annotations.js +12 -20
  15. package/src/analysis/type-checker.js +5 -7
  16. package/src/analysis/undocumented.js +10 -13
  17. package/src/cli/cli-handlers.js +4 -3
  18. package/src/compact/ai-context.js +2 -2
  19. package/src/compact/compact-migrate.js +8 -16
  20. package/src/compact/compact.js +3 -5
  21. package/src/compact/compress.js +7 -13
  22. package/src/compact/ctx-resolver.js +5 -0
  23. package/src/compact/ctx-to-jsdoc.js +13 -28
  24. package/src/compact/doc-dialect.js +18 -29
  25. package/src/compact/expand.js +10 -36
  26. package/src/compact/jsdoc-builder.js +5 -0
  27. package/src/compact/mode-config.js +6 -6
  28. package/src/compact/split-declarations.js +2 -0
  29. package/src/compact/validate-pipeline.js +7 -8
  30. package/src/core/event-bus.js +2 -1
  31. package/src/core/file-walker.js +4 -0
  32. package/src/core/filters.js +6 -5
  33. package/src/core/graph-builder.js +4 -11
  34. package/src/core/parser.js +19 -29
  35. package/src/core/utils.js +2 -0
  36. package/src/lang/lang-sql.js +7 -20
  37. package/src/mcp/mcp-server.js +2 -3
  38. package/src/mcp/tool-defs.js +1 -1
  39. package/src/mcp/tools.js +13 -21
  40. package/src/network/backend-lifecycle.js +15 -18
  41. package/src/network/local-gateway.js +10 -22
  42. package/src/network/mdns.js +5 -11
  43. package/src/network/server.js +1 -2
  44. package/src/network/web-server.js +7 -33
  45. package/web/app.js +19 -14
  46. package/web/components/code-block.js +1 -0
  47. package/web/components/quick-open.js +1 -0
  48. package/web/dashboard-state.js +1 -0
  49. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  50. package/web/panels/ActionBoard/ActionBoard.js +5 -4
  51. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  52. package/web/panels/EventItem/EventItem.css.js +1 -0
  53. package/web/panels/EventItem/EventItem.js +4 -4
  54. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  55. package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
  56. package/web/panels/ProjectItem/ProjectItem.js +3 -4
  57. package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
  58. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  59. package/web/panels/ProjectList/ProjectList.js +5 -4
  60. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  61. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  62. package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
  63. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  64. package/web/panels/code-viewer.js +1 -0
  65. package/web/panels/ctx-panel.js +1 -0
  66. package/web/panels/dep-graph.js +1 -0
  67. package/web/panels/file-tree.js +4 -188
  68. package/web/panels/health-panel.js +1 -0
  69. package/web/panels/live-monitor.js +1 -0
  70. package/web/state.js +7 -10
@@ -1,36 +1,17 @@
1
1
  // @ctx .context/src/analysis/custom-rules.ctx
2
- import{readFileSync as e,writeFileSync as t,readdirSync as s,existsSync as n,statSync as r}from"fs";import{join as o,relative as i,dirname as c,resolve as l}from"path";import{fileURLToPath as u}from"url";import{shouldExcludeDir as f,shouldExcludeFile as a,parseGitignore as d}from"../core/filters.js";
3
- const p=c(u(import.meta.url)),h=o(p,"..","..","rules");
4
- let m=[];function parseGraphignore(t){m=[];
5
- let s=t;for(;s!==c(s);){const t=o(s,".graphignore");if(n(t))try{const s=e(t,"utf-8");return void(m=s.split("\n").map(e=>e.trim()).filter(e=>e&&!e.startsWith("#")))}catch(e){}s=c(s)}}
6
- function isGraphignored(e){const t=e.split("/").pop();for(const s of m)if(s.endsWith("*")){const n=s.slice(0,-1);if(e.startsWith(n)||t.startsWith(n))return!0}else if(s.startsWith("*")){const n=s.slice(1);if(e.endsWith(n)||t.endsWith(n))return!0}else if(e.includes(s)||t===s)return!0;return!1}
7
- function loadRuleSets(){const t={};if(!n(h))return t;for(const n of s(h))if(n.endsWith(".json"))try{const s=e(o(h,n),"utf-8"),r=JSON.parse(s);t[r.name]=r}catch(e){}return t}
8
- function saveRuleSet(e){const s=o(h,`${e.name}.json`);t(s,JSON.stringify(e,null,2))}
9
- function findFiles(e,t,n=e){e===n&&(d(n),parseGraphignore(n));
10
- const c=[],l=t.replace("*","");try{for(const u of s(e)){const s=o(e,u),d=i(n,s);r(s).isDirectory()?f(u,d)||c.push(...findFiles(s,t,n)):u.endsWith(l)&&(a(u,d)||isGraphignored(d)||c.push(s))}}catch(e){}return c}
11
- function isExcluded(e,t=[]){for(const s of t){const t=s.replace("*","");if(e.endsWith(t))return!0}return!1}
12
- function isInStringOrComment(e,t){const s=e.indexOf("//");if(-1!==s&&t>s)return!0;
13
- let n=!1,r=null;for(let s=0;s<t;s++){const t=e[s],o=s>0?e[s-1]:"";n||'"'!==t&&"'"!==t&&"`"!==t?n&&t===r&&"\\"!==o&&(n=!1,r=null):(n=!0,r=t)}return n}
14
- function isWithinContext(e,t,s){const n=s,r=`</${n.replace(/[<>]/g,"")}>`;
15
- let o=0;for(let s=0;s<=t;s++){const t=e[s];
16
- let i=0;for(;i<t.length;){const e=t.indexOf(n,i),s=t.indexOf(r,i);if(-1===e&&-1===s)break;-1!==e&&(-1===s||e<s)?(o++,i=e+n.length):(o--,i=s+r.length)}}return o>0}
17
- function checkFileAgainstRule(t,s,n){if(isExcluded(t,s.exclude))return[];
18
- const r=[],o=e(t,"utf-8").split("\n"),c=i(n,t);for(let e=0;e<o.length;e++){const t=o[e];
19
- let n=!1,i="";if("regex"===s.patternType)try{const e=new RegExp(s.pattern,"g");
20
- let r;for(;null!==(r=e.exec(t));)if(!isInStringOrComment(t,r.index)){n=!0,i=r[0];break}}catch(e){}else{const e=t.indexOf(s.pattern);-1===e||isInStringOrComment(t,e)||(n=!0,i=s.pattern)}n&&s.contextRequired&&!isWithinContext(o,e,s.contextRequired)||n&&r.push({ruleId:s.id,ruleName:s.name,severity:s.severity,file:c,line:e+1,match:i,replacement:s.replacement})}return r}
21
- export async function getCustomRules(){const e=loadRuleSets();
22
- let t=0;
23
- const s={};for(const[n,r]of Object.entries(e))s[n]={description:r.description,ruleCount:r.rules.length,rules:r.rules.map(e=>({id:e.id,name:e.name,severity:e.severity}))},t+=r.rules.length;return{ruleSets:s,totalRules:t}}
24
- export async function setCustomRule(e,t){const s=loadRuleSets();s[e]||(s[e]={name:e,description:`Custom rules for ${e}`,rules:[]});
25
- const n=s[e],r=n.rules.findIndex(e=>e.id===t.id);return r>=0?n.rules[r]=t:n.rules.push(t),saveRuleSet(n),{success:!0,message:r>=0?`Updated rule "${t.id}" in ${e}`:`Added rule "${t.id}" to ${e}`}}
26
- export async function deleteCustomRule(e,t){const s=loadRuleSets();if(!s[e])return{success:!1,message:`Ruleset "${e}" not found`};
27
- const n=s[e],r=n.rules.findIndex(e=>e.id===t);return r<0?{success:!1,message:`Rule "${t}" not found`}:(n.rules.splice(r,1),saveRuleSet(n),{success:!0,message:`Deleted rule "${t}" from ${e}`})}
28
- export function detectProjectRuleSets(t){const s=loadRuleSets(),r=[],c={};
29
- let l=[];try{const s=o(t,"package.json");if(n(s)){const t=JSON.parse(e(s,"utf-8"));l=[...Object.keys(t.dependencies||{}),...Object.keys(t.devDependencies||{})]}}catch(e){}for(const[n,o]of Object.entries(s)){if(!o.detect)continue;
30
- const s=o.detect;if(s.packageJson)for(const e of s.packageJson)if(l.includes(e)){r.push(n),c[n]=`Found "${e}" in package.json`;break}if(!r.includes(n)&&(s.imports||s.patterns)){const o=findFiles(t,"*.js");e:for(const l of o.slice(0,50))try{const o=e(l,"utf-8");if(s.imports)for(const e of s.imports)if(o.includes(e)){r.push(n),c[n]=`Found "${e}" in ${i(t,l)}`;break e}if(s.patterns)for(const e of s.patterns)if(o.includes(e)){r.push(n),c[n]=`Found "${e}" in ${i(t,l)}`;break e}}catch(e){}}}return{detected:r,reasons:c}}
31
- export async function checkCustomRules(e,t={}){const s=l(e),n=loadRuleSets();
32
- let r=[],o=null;if(t.ruleSet)n[t.ruleSet]&&(r=n[t.ruleSet].rules);else if(!1!==t.autoDetect){if(o=detectProjectRuleSets(e),o.detected.length>0)for(const e of o.detected)n[e]&&r.push(...n[e].rules);for(const[e,t]of Object.entries(n))t.alwaysApply&&!o.detected.includes(e)&&r.push(...t.rules)}else for(const e of Object.values(n))r.push(...e.rules);
33
- const i={};for(const e of r){const t=e.filePattern||"*.js";i[t]||(i[t]=[]),i[t].push(e)}const c=[];for(const[t,n]of Object.entries(i)){const r=findFiles(e,t);for(const e of r)for(const t of n){const n=checkFileAgainstRule(e,t,s);c.push(...n)}}const u=new Set,f=c.filter(e=>{const t=`${e.file}:${e.line}:${e.match}`;return!u.has(t)&&(u.add(t),!0)});
34
- let a=f;t.severity&&(a=f.filter(e=>e.severity===t.severity));
35
- const d={error:0,warning:1,info:2};a.sort((e,t)=>{const s=d[e.severity]-d[t.severity];return 0!==s?s:e.file.localeCompare(t.file)});
36
- const p={error:a.filter(e=>"error"===e.severity).length,warning:a.filter(e=>"warning"===e.severity).length,info:a.filter(e=>"info"===e.severity).length},h={};for(const e of a)h[e.ruleId]=(h[e.ruleId]||0)+1;return{basePath:e,total:a.length,bySeverity:p,byRule:h,violations:a.slice(0,50),...o&&{detected:o}}}
2
+ import{readFileSync as e,writeFileSync as t,readdirSync as s,existsSync as n,statSync as r}from"fs";import{join as o,relative as c,dirname as i,resolve as l}from"path";import{fileURLToPath as u}from"url";import{shouldExcludeDir as f,shouldExcludeFile as a,parseGitignore as d}from"../core/filters.js";
3
+ const p=i(u(import.meta.url)),h=o(p,"..","..","rules");
4
+ let m=[];
5
+ function y(t){m=[];let s=t;for(;s!==i(s);){const t=o(s,".graphignore");if(n(t))try{const s=e(t,"utf-8");return void(m=s.split("\n").map(e=>e.trim()).filter(e=>e&&!e.startsWith("#")))}catch(e){}s=i(s)}}
6
+ function g(e){const t=e.split("/").pop();for(const s of m)if(s.endsWith("*")){const n=s.slice(0,-1);if(e.startsWith(n)||t.startsWith(n))return!0}else if(s.startsWith("*")){const n=s.slice(1);if(e.endsWith(n)||t.endsWith(n))return!0}else if(e.includes(s)||t===s)return!0;return!1}
7
+ function x(){const t={};if(!n(h))return t;for(const n of s(h))if(n.endsWith(".json"))try{const s=e(o(h,n),"utf-8"),r=JSON.parse(s);t[r.name]=r}catch(e){}return t}
8
+ function v(e){const s=o(h,`${e.name}.json`);t(s,JSON.stringify(e,null,2))}
9
+ function $(e,t,n=e){e===n&&(d(n),y(n));const i=[],l=t.replace("*","");try{for(const u of s(e)){const s=o(e,u),d=c(n,s);r(s).isDirectory()?f(u,d)||i.push(...$(s,t,n)):u.endsWith(l)&&(a(u,d)||g(d)||i.push(s))}}catch(e){}return i}
10
+ function j(e,t){const s=e.indexOf("//");if(-1!==s&&t>s)return!0;let n=!1,r=null;for(let s=0;s<t;s++){const t=e[s],o=s>0?e[s-1]:"";n||'"'!==t&&"'"!==t&&"`"!==t?n&&t===r&&"\\"!==o&&(n=!1,r=null):(n=!0,r=t)}return n}
11
+ function S(e,t,s){const n=s,r=`</${n.replace(/[<>]/g,"")}>`;let o=0;for(let s=0;s<=t;s++){const t=e[s];let c=0;for(;c<t.length;){const e=t.indexOf(n,c),s=t.indexOf(r,c);if(-1===e&&-1===s)break;-1!==e&&(-1===s||e<s)?(o++,c=e+n.length):(o--,c=s+r.length)}}return o>0}
12
+ function b(t,s,n){if(function(e,t=[]){for(const s of t){const t=s.replace("*","");if(e.endsWith(t))return!0}return!1}(t,s.exclude))return[];const r=[],o=e(t,"utf-8").split("\n"),i=c(n,t);for(let e=0;e<o.length;e++){const t=o[e];let n=!1,c="";if("regex"===s.patternType)try{const e=new RegExp(s.pattern,"g");let r;for(;null!==(r=e.exec(t));)if(!j(t,r.index)){n=!0,c=r[0];break}}catch(e){}else{const e=t.indexOf(s.pattern);-1===e||j(t,e)||(n=!0,c=s.pattern)}n&&s.contextRequired&&!S(o,e,s.contextRequired)||n&&r.push({ruleId:s.id,ruleName:s.name,severity:s.severity,file:i,line:e+1,match:c,replacement:s.replacement})}return r}
13
+ export async function getCustomRules(){const e=x();let t=0;const s={};for(const[n,r]of Object.entries(e))s[n]={description:r.description,ruleCount:r.rules.length,rules:r.rules.map(e=>({id:e.id,name:e.name,severity:e.severity}))},t+=r.rules.length;return{ruleSets:s,totalRules:t}}
14
+ export async function setCustomRule(e,t){const s=x();s[e]||(s[e]={name:e,description:`Custom rules for ${e}`,rules:[]});const n=s[e],r=n.rules.findIndex(e=>e.id===t.id);return r>=0?n.rules[r]=t:n.rules.push(t),v(n),{success:!0,message:r>=0?`Updated rule "${t.id}" in ${e}`:`Added rule "${t.id}" to ${e}`}}
15
+ export async function deleteCustomRule(e,t){const s=x();if(!s[e])return{success:!1,message:`Ruleset "${e}" not found`};const n=s[e],r=n.rules.findIndex(e=>e.id===t);return r<0?{success:!1,message:`Rule "${t}" not found`}:(n.rules.splice(r,1),v(n),{success:!0,message:`Deleted rule "${t}" from ${e}`})}
16
+ export function detectProjectRuleSets(t){const s=x(),r=[],i={};let l=[];try{const s=o(t,"package.json");if(n(s)){const t=JSON.parse(e(s,"utf-8"));l=[...Object.keys(t.dependencies||{}),...Object.keys(t.devDependencies||{})]}}catch(e){}for(const[n,o]of Object.entries(s)){if(!o.detect)continue;const s=o.detect;if(s.packageJson)for(const e of s.packageJson)if(l.includes(e)){r.push(n),i[n]=`Found "${e}" in package.json`;break}if(!r.includes(n)&&(s.imports||s.patterns)){const o=$(t,"*.js");e:for(const l of o.slice(0,50))try{const o=e(l,"utf-8");if(s.imports)for(const e of s.imports)if(o.includes(e)){r.push(n),i[n]=`Found "${e}" in ${c(t,l)}`;break e}if(s.patterns)for(const e of s.patterns)if(o.includes(e)){r.push(n),i[n]=`Found "${e}" in ${c(t,l)}`;break e}}catch(e){}}}return{detected:r,reasons:i}}
17
+ export async function checkCustomRules(e,t={}){const s=l(e),n=x();let r=[],o=null;if(t.ruleSet)n[t.ruleSet]&&(r=n[t.ruleSet].rules);else if(!1!==t.autoDetect){if(o=detectProjectRuleSets(e),o.detected.length>0)for(const e of o.detected)n[e]&&r.push(...n[e].rules);for(const[e,t]of Object.entries(n))t.alwaysApply&&!o.detected.includes(e)&&r.push(...t.rules)}else for(const e of Object.values(n))r.push(...e.rules);const c={};for(const e of r){const t=e.filePattern||"*.js";c[t]||(c[t]=[]),c[t].push(e)}const i=[];for(const[t,n]of Object.entries(c)){const r=$(e,t);for(const e of r)for(const t of n){const n=b(e,t,s);i.push(...n)}}const u=new Set,f=i.filter(e=>{const t=`${e.file}:${e.line}:${e.match}`;return!u.has(t)&&(u.add(t),!0)});let a=f;t.severity&&(a=f.filter(e=>e.severity===t.severity));const d={error:0,warning:1,info:2};a.sort((e,t)=>{const s=d[e.severity]-d[t.severity];return 0!==s?s:e.file.localeCompare(t.file)});const p={error:a.filter(e=>"error"===e.severity).length,warning:a.filter(e=>"warning"===e.severity).length,info:a.filter(e=>"info"===e.severity).length},h={};for(const e of a)h[e.ruleId]=(h[e.ruleId]||0)+1;return{basePath:e,total:a.length,bySeverity:p,byRule:h,violations:a.slice(0,50),...o&&{detected:o}}}
@@ -1,9 +1,5 @@
1
1
  // @ctx .context/src/analysis/db-analysis.ctx
2
2
  import{parseProject as e}from"../core/parser.js";import{buildGraph as t}from"../core/graph-builder.js";
3
3
  export async function getDBSchema(t){const a=(await e(t)).tables||[];return{tables:a.map(e=>({name:e.name,columns:e.columns,file:e.file,line:e.line})),totalTables:a.length,totalColumns:a.reduce((e,t)=>e+t.columns.length,0)}}
4
- export async function getTableUsage(a,s){const n=await e(a),o=t(n),r={};for(const[e,t,a]of o.edges){if("R→"!==t&&"W→"!==t)continue;
5
- const n=a;if(s&&n!==s)continue;r[n]||(r[n]={readers:[],writers:[]});
6
- const l=o.reverseLegend[e]||e,d=o.nodes[e],c={name:l,file:d?.f||"?"};"R→"===t?r[n].readers.some(e=>e.name===l)||r[n].readers.push(c):r[n].writers.some(e=>e.name===l)||r[n].writers.push(c)}const l=Object.entries(r).map(([e,t])=>({table:e,readers:t.readers,writers:t.writers,totalReaders:t.readers.length,totalWriters:t.writers.length})).sort((e,t)=>t.totalReaders+t.totalWriters-(e.totalReaders+e.totalWriters));return{tables:l,totalTables:l.length,totalQueries:l.reduce((e,t)=>e+t.totalReaders+t.totalWriters,0)}}
7
- export async function getDBDeadTables(a){const s=await e(a),n=t(s),o=s.tables||[],r=new Set;for(const[,e,t]of n.edges)"R→"!==e&&"W→"!==e||r.add(t);
8
- const l=o.filter(e=>!r.has(e.name)).map(e=>({name:e.name,file:e.file,line:e.line,columnCount:e.columns.length})),d=collectReferencedColumns(s),c=[];for(const e of o)if(r.has(e.name))for(const t of e.columns)d.has(t.name)||c.push({table:e.name,column:t.name,type:t.type});return{deadTables:l,deadColumns:c,stats:{totalSchemaTables:o.length,totalSchemaColumns:o.reduce((e,t)=>e+t.columns.length,0),deadTableCount:l.length,deadColumnCount:c.length}}}
9
- function collectReferencedColumns(e){const t=new Set;for(const a of e.functions||[])if(a.dbReads?.length||a.dbWrites?.length)for(const e of[...a.dbReads||[],...a.dbWrites||[]])t.add(e);for(const a of e.classes||[])if(a.dbReads?.length||a.dbWrites?.length)for(const e of[...a.dbReads||[],...a.dbWrites||[]])t.add(e);return t.add("id"),t.add("uuid"),t.add("created_at"),t.add("updated_at"),t}
4
+ export async function getTableUsage(a,s){const n=await e(a),o=t(n),r={};for(const[e,t,a]of o.edges){if("R→"!==t&&"W→"!==t)continue;const n=a;if(s&&n!==s)continue;r[n]||(r[n]={readers:[],writers:[]});const l=o.reverseLegend[e]||e,d=o.nodes[e],i={name:l,file:d?.f||"?"};"R→"===t?r[n].readers.some(e=>e.name===l)||r[n].readers.push(i):r[n].writers.some(e=>e.name===l)||r[n].writers.push(i)}const l=Object.entries(r).map(([e,t])=>({table:e,readers:t.readers,writers:t.writers,totalReaders:t.readers.length,totalWriters:t.writers.length})).sort((e,t)=>t.totalReaders+t.totalWriters-(e.totalReaders+e.totalWriters));return{tables:l,totalTables:l.length,totalQueries:l.reduce((e,t)=>e+t.totalReaders+t.totalWriters,0)}}
5
+ export async function getDBDeadTables(a){const s=await e(a),n=t(s),o=s.tables||[],r=new Set;for(const[,e,t]of n.edges)"R→"!==e&&"W→"!==e||r.add(t);const l=o.filter(e=>!r.has(e.name)).map(e=>({name:e.name,file:e.file,line:e.line,columnCount:e.columns.length})),d=function(e){const t=new Set;for(const a of e.functions||[])if(a.dbReads?.length||a.dbWrites?.length)for(const e of[...a.dbReads||[],...a.dbWrites||[]])t.add(e);for(const a of e.classes||[])if(a.dbReads?.length||a.dbWrites?.length)for(const e of[...a.dbReads||[],...a.dbWrites||[]])t.add(e);return t.add("id"),t.add("uuid"),t.add("created_at"),t.add("updated_at"),t}(s),i=[];for(const e of o)if(r.has(e.name))for(const t of e.columns)d.has(t.name)||i.push({table:e.name,column:t.name,type:t.type});return{deadTables:l,deadColumns:i,stats:{totalSchemaTables:o.length,totalSchemaColumns:o.reduce((e,t)=>e+t.columns.length,0),deadTableCount:l.length,deadColumnCount:i.length}}}
@@ -1,19 +1,9 @@
1
1
  // @ctx .context/src/analysis/dead-code.ctx
2
- import{readFileSync as e,readdirSync as t,statSync as n,existsSync as s}from"fs";import{join as o,relative as i,resolve as a,dirname as r}from"path";import{parse as c}from"../../vendor/acorn.mjs";import*as l from"../../vendor/walk.mjs";import{shouldExcludeDir as d,shouldExcludeFile as f,parseGitignore as p}from"../core/filters.js";function findJSFiles(e,s=e){e===s&&p(s);
3
- const a=[];try{for(const r of t(e)){const t=o(e,r),c=i(s,t);n(t).isDirectory()?d(r,c)||a.push(...findJSFiles(t,s)):!r.endsWith(".js")||r.endsWith(".css.js")||r.endsWith(".tpl.js")?(r.endsWith(".css.js")||r.endsWith(".tpl.js"))&&(f(r,c)||a.push(t)):f(r,c)||a.push(t)}}catch(e){}return a}
4
- function findProjectRoot(e){let t=a(e);for(;t!==r(t);){if(s(o(t,"package.json")))return t;t=r(t)}return a(e)}
5
- function analyzeFile(e){const t=new Set,n=new Set,s=new Set,o=[],i=[];
6
- let a;try{a=c(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return{definitions:t,calls:n,exports:s,imports:o,namedExports:i}}return l.simple(a,{FunctionDeclaration(e){e.id&&t.add(e.id.name)},ClassDeclaration(e){e.id&&t.add(e.id.name)},CallExpression(e){"Identifier"===e.callee.type?n.add(e.callee.name):"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&n.add(e.callee.object.name);for(const t of e.arguments)"Identifier"===t.type&&n.add(t.name)},NewExpression(e){"Identifier"===e.callee.type&&n.add(e.callee.name)},ImportDeclaration(e){const t=e.source.value;for(const n of e.specifiers)"ImportSpecifier"===n.type?o.push({name:n.imported.name,source:t}):"ImportDefaultSpecifier"===n.type&&o.push({name:"default",source:t})},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id){const t=e.declaration.id.name;s.add(t),i.push({name:t,line:e.loc.start.line})}else if(e.declaration.declarations)for(const t of e.declaration.declarations)if("Identifier"===t.id.type){const n=t.id.name;s.add(n),i.push({name:n,line:e.loc.start.line})}if(e.specifiers)for(const t of e.specifiers){const n=t.exported.name;s.add(n),i.push({name:n,line:e.loc.start.line})}},ExportDefaultDeclaration(e){e.declaration?.id&&s.add(e.declaration.id.name)}}),{definitions:t,calls:n,exports:s,imports:o,namedExports:i}}
7
- function analyzeFileLocals(e){const t=[],n=[];
8
- let s;try{s=c(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return{unusedVars:t,unusedImports:n}}const o=new Set,i=[],a=[],r=new Set;l.simple(s,{VariableDeclaration(e){const t="ExportNamedDeclaration"===e.parent?.type;for(const n of e.declarations)"Identifier"===n.id.type&&(i.push({name:n.id.name,line:n.loc.start.line,isExported:t}),r.add(n.id))},ImportDeclaration(e){for(const t of e.specifiers){const n=t.local.name,s="ImportSpecifier"===t.type?t.imported.name:"ImportDefaultSpecifier"===t.type?"default":"*";a.push({name:s,local:n,source:e.source.value,line:e.loc.start.line}),r.add(t.local)}}});
9
- const d=new Set;for(const e of s.body){if("ExportNamedDeclaration"===e.type&&e.declaration){if(e.declaration.declarations)for(const t of e.declaration.declarations)"Identifier"===t.id.type&&d.add(t.id.name);e.declaration.id&&d.add(e.declaration.id.name)}if("ExportNamedDeclaration"===e.type&&e.specifiers)for(const t of e.specifiers)d.add(t.local.name)}l.simple(s,{Identifier(e){o.add(e.name)}});for(const n of i){if(d.has(n.name))continue;if(n.name.startsWith("_"))continue;
10
- const s=new RegExp(`\\b${n.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g"),o=e.match(s);o&&o.length<=1&&t.push({name:n.name,line:n.line})}for(const t of a){if("*"===t.name)continue;
11
- const s=new RegExp(`\\b${t.local.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g"),o=e.match(s);o&&o.length<=1&&n.push(t)}return{unusedVars:t,unusedImports:n}}
12
- export async function getDeadCode(t){const n=a(t),s=findJSFiles(t),o=[],d=new Set,f=new Set,p=[],m=new Map,u=findJSFiles(findProjectRoot(t));for(const t of u){let s;try{s=e(t,"utf-8")}catch{continue}const o=i(n,t),{imports:c}=analyzeFile(s);for(const e of c){if(!e.source.startsWith("."))continue;
13
- const s=r(t);
14
- let c=a(s,e.source);c.endsWith(".js")||(c+=".js");
15
- const l=i(n,c),d=`${e.name}@${l}`;m.has(d)||m.set(d,new Set),m.get(d).add(o)}}for(const t of s){const s=e(t,"utf-8"),o=i(n,t),{definitions:a,calls:r,exports:c,namedExports:l}=analyzeFile(s);for(const e of r)d.add(e);for(const e of c)f.add(e);p.push({file:o,code:s,definitions:a,calls:r,exports:c,namedExports:l})}for(const{file:e,code:t,definitions:n,exports:s}of p){if(e.includes(".test.")||e.includes("/tests/"))continue;if(e.endsWith(".css.js")||e.endsWith(".tpl.js"))continue;
16
- let n;try{n=c(t,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){continue}l.simple(n,{FunctionDeclaration(t){if(!t.id)return;
17
- const n=t.id.name;s.has(n)||f.has(n)||d.has(n)||n.startsWith("_")||o.push({name:n,type:"function",file:e,line:t.loc.start.line,reason:"Never called"})},ClassDeclaration(t){if(!t.id)return;
18
- const n=t.id.name;s.has(n)||f.has(n)||d.has(n)||o.push({name:n,type:"class",file:e,line:t.loc.start.line,reason:"Never instantiated"})}})}for(const{file:e,calls:t,namedExports:n}of p)if(!e.includes(".test.")&&!e.includes("/tests/"))for(const s of n){if(t.has(s.name))continue;
19
- const n=`${s.name}@${e}`,i=m.get(n);i&&0!==i.size||o.push({name:s.name,type:"export",file:e,line:s.line,reason:"Exported but never imported"})}for(const{file:e,code:t}of p){if(e.includes(".test.")||e.includes("/tests/"))continue;if(e.endsWith(".css.js")||e.endsWith(".tpl.js"))continue;const{unusedVars:n,unusedImports:s}=analyzeFileLocals(t);for(const t of n)o.push({name:t.name,type:"variable",file:e,line:t.line,reason:"Declared but never used"});for(const t of s)o.push({name:t.local,type:"import",file:e,line:t.line,reason:`Imported from '${t.source}' but never used`})}const h={function:o.filter(e=>"function"===e.type).length,class:o.filter(e=>"class"===e.type).length,export:o.filter(e=>"export"===e.type).length,variable:o.filter(e=>"variable"===e.type).length,import:o.filter(e=>"import"===e.type).length};return{total:o.length,byType:h,items:o.slice(0,50)}}
2
+ import{readFileSync as e,readdirSync as t,statSync as n,existsSync as s}from"fs";import{join as o,relative as a,resolve as i,dirname as r}from"path";import{parse as c}from"../../vendor/acorn.mjs";
3
+ import*as l from"../../vendor/walk.mjs";
4
+ import{shouldExcludeDir as d,shouldExcludeFile as f,parseGitignore as p}from"../core/filters.js";
5
+ function m(e,s=e){e===s&&p(s);const i=[];try{for(const r of t(e)){const t=o(e,r),c=a(s,t);n(t).isDirectory()?d(r,c)||i.push(...m(t,s)):!r.endsWith(".js")||r.endsWith(".css.js")||r.endsWith(".tpl.js")?(r.endsWith(".css.js")||r.endsWith(".tpl.js"))&&(f(r,c)||i.push(t)):f(r,c)||i.push(t)}}catch(e){}return i}
6
+ function u(e){let t=i(e);for(;t!==r(t);){if(s(o(t,"package.json")))return t;t=r(t)}return i(e)}
7
+ function h(e){const t=new Set,n=new Set,s=new Set,o=[],a=[];let i;try{i=c(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return{definitions:t,calls:n,exports:s,imports:o,namedExports:a}}return l.simple(i,{FunctionDeclaration(e){e.id&&t.add(e.id.name)},ClassDeclaration(e){e.id&&t.add(e.id.name)},CallExpression(e){"Identifier"===e.callee.type?n.add(e.callee.name):"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&n.add(e.callee.object.name);for(const t of e.arguments)"Identifier"===t.type&&n.add(t.name)},NewExpression(e){"Identifier"===e.callee.type&&n.add(e.callee.name)},ImportDeclaration(e){const t=e.source.value;for(const n of e.specifiers)"ImportSpecifier"===n.type?o.push({name:n.imported.name,source:t}):"ImportDefaultSpecifier"===n.type&&o.push({name:"default",source:t})},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id){const t=e.declaration.id.name;s.add(t),a.push({name:t,line:e.loc.start.line})}else if(e.declaration.declarations)for(const t of e.declaration.declarations)if("Identifier"===t.id.type){const n=t.id.name;s.add(n),a.push({name:n,line:e.loc.start.line})}if(e.specifiers)for(const t of e.specifiers){const n=t.exported.name;s.add(n),a.push({name:n,line:e.loc.start.line})}},ExportDefaultDeclaration(e){e.declaration?.id&&s.add(e.declaration.id.name)}}),{definitions:t,calls:n,exports:s,imports:o,namedExports:a}}
8
+ function y(e){const t=[],n=[];let s;try{s=c(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return{unusedVars:t,unusedImports:n}}const o=new Set,a=[],i=[],r=new Set;l.simple(s,{VariableDeclaration(e){const t="ExportNamedDeclaration"===e.parent?.type;for(const n of e.declarations)"Identifier"===n.id.type&&(a.push({name:n.id.name,line:n.loc.start.line,isExported:t}),r.add(n.id))},ImportDeclaration(e){for(const t of e.specifiers){const n=t.local.name,s="ImportSpecifier"===t.type?t.imported.name:"ImportDefaultSpecifier"===t.type?"default":"*";i.push({name:s,local:n,source:e.source.value,line:e.loc.start.line}),r.add(t.local)}}});const d=new Set;for(const e of s.body){if("ExportNamedDeclaration"===e.type&&e.declaration){if(e.declaration.declarations)for(const t of e.declaration.declarations)"Identifier"===t.id.type&&d.add(t.id.name);e.declaration.id&&d.add(e.declaration.id.name)}if("ExportNamedDeclaration"===e.type&&e.specifiers)for(const t of e.specifiers)d.add(t.local.name)}l.simple(s,{Identifier(e){o.add(e.name)}});for(const n of a){if(d.has(n.name))continue;if(n.name.startsWith("_"))continue;const s=new RegExp(`\\b${n.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g"),o=e.match(s);o&&o.length<=1&&t.push({name:n.name,line:n.line})}for(const t of i){if("*"===t.name)continue;const s=new RegExp(`\\b${t.local.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g"),o=e.match(s);o&&o.length<=1&&n.push(t)}return{unusedVars:t,unusedImports:n}}
9
+ export async function getDeadCode(t){const n=i(t),s=m(t),o=[],d=new Set,f=new Set,p=[],x=new Map,g=m(u(t));for(const t of g){let s;try{s=e(t,"utf-8")}catch{continue}const o=a(n,t),{imports:c}=h(s);for(const e of c){if(!e.source.startsWith("."))continue;const s=r(t);let c=i(s,e.source);c.endsWith(".js")||(c+=".js");const l=a(n,c),d=`${e.name}@${l}`;x.has(d)||x.set(d,new Set),x.get(d).add(o)}}for(const t of s){const s=e(t,"utf-8"),o=a(n,t),{definitions:i,calls:r,exports:c,namedExports:l}=h(s);for(const e of r)d.add(e);for(const e of c)f.add(e);p.push({file:o,code:s,definitions:i,calls:r,exports:c,namedExports:l})}for(const{file:e,code:t,definitions:n,exports:s}of p){if(e.includes(".test.")||e.includes("/tests/"))continue;if(e.endsWith(".css.js")||e.endsWith(".tpl.js"))continue;let n;try{n=c(t,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){continue}l.simple(n,{FunctionDeclaration(t){if(!t.id)return;const n=t.id.name;s.has(n)||f.has(n)||d.has(n)||n.startsWith("_")||o.push({name:n,type:"function",file:e,line:t.loc.start.line,reason:"Never called"})},ClassDeclaration(t){if(!t.id)return;const n=t.id.name;s.has(n)||f.has(n)||d.has(n)||o.push({name:n,type:"class",file:e,line:t.loc.start.line,reason:"Never instantiated"})}})}for(const{file:e,calls:t,namedExports:n}of p)if(!e.includes(".test.")&&!e.includes("/tests/"))for(const s of n){if(t.has(s.name))continue;const n=`${s.name}@${e}`,a=x.get(n);a&&0!==a.size||o.push({name:s.name,type:"export",file:e,line:s.line,reason:"Exported but never imported"})}for(const{file:e,code:t}of p){if(e.includes(".test.")||e.includes("/tests/"))continue;if(e.endsWith(".css.js")||e.endsWith(".tpl.js"))continue;const{unusedVars:n,unusedImports:s}=y(t);for(const t of n)o.push({name:t.name,type:"variable",file:e,line:t.line,reason:"Declared but never used"});for(const t of s)o.push({name:t.local,type:"import",file:e,line:t.line,reason:`Imported from '${t.source}' but never used`})}const D={function:o.filter(e=>"function"===e.type).length,class:o.filter(e=>"class"===e.type).length,export:o.filter(e=>"export"===e.type).length,variable:o.filter(e=>"variable"===e.type).length,import:o.filter(e=>"import"===e.type).length};return{total:o.length,byType:D,items:o.slice(0,50)}}
@@ -1,18 +1,10 @@
1
1
  // @ctx .context/src/analysis/full-analysis.ctx
2
- import{readFileSync as t,readdirSync as e,statSync as s}from"fs";import{join as a,relative as o,resolve as n}from"path";import{getDeadCode as i}from"./dead-code.js";import{checkUndocumentedFile as l}from"./undocumented.js";import{getSimilarFunctions as r}from"./similar-functions.js";import{analyzeComplexityFile as c}from"./complexity.js";import{getLargeFiles as d}from"./large-files.js";import{getOutdatedPatterns as u}from"./outdated-patterns.js";import{getTableUsage as m}from"./db-analysis.js";import{checkJSDocFile as h}from"./jsdoc-checker.js";import{readCache as p,writeCache as g,computeContentHash as y,isCacheValid as f}from"./analysis-cache.js";import{shouldExcludeDir as x,shouldExcludeFile as j,parseGitignore as b}from"../core/filters.js";import{getWorkspaceRoot as C}from"../core/workspace.js";function calculateHealthScore(t){let e=100;
3
- const s=[];e-=Math.min(2*t.deadCode.total,20),t.deadCode.total>0&&s.push(`${t.deadCode.total} unused functions/classes`),e-=Math.min(.5*t.undocumented.total,15),t.undocumented.total>10&&s.push(`${t.undocumented.total} undocumented items`),e-=Math.min(3*t.similar.total,15),t.similar.total>0&&s.push(`${t.similar.total} similar function pairs`);
4
- const a=t.complexity.stats?.critical||0,o=t.complexity.stats?.high||0;e-=Math.min(5*a+2*o,20),a>0&&s.push(`${a} critical complexity functions`);
5
- const n=t.largeFiles.stats?.critical||0,i=t.largeFiles.stats?.warning||0;e-=Math.min(4*n+1*i,10),n>0&&s.push(`${n} files need splitting`);
6
- const l=t.outdated.stats?.bySeverity?.error||0,r=t.outdated.stats?.bySeverity?.warning||0;if(e-=Math.min(3*l+1*r,10),t.outdated.redundantDeps?.length>0&&s.push(`${t.outdated.redundantDeps.length} redundant npm dependencies`),t.jsdocConsistency){const a=t.jsdocConsistency.errors||0,o=t.jsdocConsistency.warnings||0;e-=Math.min(2*a+1*o,15),a>0&&s.push(`${a} JSDoc consistency errors`)}let c;return e=Math.max(0,Math.min(100,Math.round(e))),c=e>=90?"excellent":e>=70?"good":e>=50?"warning":"critical",{score:e,rating:c,topIssues:s.slice(0,5)}}
7
- function findJSFiles(t,n=t){t===n&&b(n);
8
- const i=[];try{for(const l of e(t)){const e=a(t,l),r=o(n,e);s(e).isDirectory()?x(l,r)||i.push(...findJSFiles(e,n)):!l.endsWith(".js")||l.endsWith(".css.js")||l.endsWith(".tpl.js")||j(l,r)||i.push(e)}}catch(t){}return i}
9
- function runCacheableAnalyses(e,s){const a=n(e),i=C(),r=findJSFiles(e),d=[],u=[],m=[];
10
- let x=0,j=0;for(const e of r){const n=o(a,e),r=o(i,e);
11
- let b;try{b=t(e,"utf-8")}catch(t){continue}const C=y(b),S=p(s,r);if(S&&f(S,S.sig,C,"content"))x++,S.complexity&&d.push(...S.complexity),S.undocumented&&u.push(...S.undocumented),S.jsdocIssues&&m.push(...S.jsdocIssues);else{j++;
12
- const t=c(b,n),e=l(b,n,"tests"),a=h(b,n);d.push(...t),u.push(...e),m.push(...a),g(s,r,{sig:S?.sig||C,contentHash:C,complexity:t,undocumented:e,jsdocIssues:a})}}return{complexity:d,undocumented:u,jsdocIssues:m,cacheStats:{hits:x,misses:j}}}
13
- function aggregateComplexity(t,e=5){let s=t.filter(t=>t.complexity>=e);s.sort((t,e)=>e.complexity-t.complexity);
14
- const a={low:s.filter(t=>"low"===t.rating).length,moderate:s.filter(t=>"moderate"===t.rating).length,high:s.filter(t=>"high"===t.rating).length,critical:s.filter(t=>"critical"===t.rating).length,average:s.length>0?Math.round(s.reduce((t,e)=>t+e.complexity,0)/s.length*10)/10:0};return{total:s.length,stats:a,items:s.slice(0,30)}}
15
- function aggregateUndocumented(t){const e={class:t.filter(t=>"class"===t.type).length,function:t.filter(t=>"function"===t.type).length,method:t.filter(t=>"method"===t.type).length};return{total:t.length,byType:e,items:t.slice(0,20)}}
16
- function aggregateJSDoc(t){const e=t.filter(t=>"error"===t.severity).length,s=t.filter(t=>"warning"===t.severity).length,a={};for(const e of t)a[e.file]=(a[e.file]||0)+1;return{issues:t,summary:{total:t.length,errors:e,warnings:s,byFile:a}}}
17
- export async function getFullAnalysis(t,e={}){const s=e.includeItems||!1,o=(n(t),runCacheableAnalyses(t,a(C(),".context"))),l=aggregateComplexity(o.complexity),c=aggregateUndocumented(o.undocumented),h=aggregateJSDoc(o.jsdocIssues),[p,g,y,f,x]=await Promise.all([i(t).catch(()=>({total:0,byType:{},items:[]})),r(t,{threshold:70}).catch(()=>({total:0,pairs:[]})),d(t).catch(()=>({total:0,stats:{},items:[]})),u(t).catch(()=>({codePatterns:[],redundantDeps:[],stats:{totalPatterns:0,bySeverity:{},byPattern:{},redundantDeps:0}})),m(t).catch(()=>({tables:[],totalTables:0,totalQueries:0}))]),j=calculateHealthScore({deadCode:p,undocumented:c,similar:g,complexity:l,largeFiles:y,outdated:f,jsdocConsistency:h.summary}),b={deadCode:{total:p.total,byType:p.byType,...s&&{items:p.items.slice(0,10)}},undocumented:{total:c.total,byType:c.byType,...s&&{items:c.items.slice(0,10)}},similar:{total:g.total,...s&&{pairs:g.pairs.slice(0,5)}},complexity:{total:l.total,stats:l.stats,...s&&{items:l.items.slice(0,10)}},largeFiles:{total:y.total,stats:y.stats,...s&&{items:y.items.slice(0,10)}},outdated:{totalPatterns:f.stats.totalPatterns,redundantDeps:f.redundantDeps,...s&&{codePatterns:f.codePatterns.slice(0,10)}},jsdocConsistency:{total:h.summary.total,errors:h.summary.errors,warnings:h.summary.warnings,...s&&{issues:h.issues.slice(0,10)}},cache:o.cacheStats,overall:j};return x.totalTables>0&&(b.database={tablesUsed:x.totalTables,totalQueries:x.totalQueries,tables:x.tables.map(t=>({name:t.table,readers:t.totalReaders,writers:t.totalWriters}))}),b}
18
- export function getAnalysisSummaryOnly(t){const e=runCacheableAnalyses(t,a(C(),".context")),s=aggregateComplexity(e.complexity),o=aggregateUndocumented(e.undocumented),n=aggregateJSDoc(e.jsdocIssues),i=calculateHealthScore({deadCode:{total:0},undocumented:o,similar:{total:0},complexity:s,largeFiles:{total:0},outdated:{stats:{totalPatterns:0}},jsdocConsistency:n.summary});return{healthScore:i.score,grade:i.rating,complexity:s.total,undocumented:o.total,jsdocIssues:n.summary.total,cache:e.cacheStats,note:"Partial score — cross-file analyses skipped for speed. Run get_full_analysis for complete health check."}}
2
+ import{readFileSync as t,readdirSync as e,statSync as s}from"fs";import{join as a,relative as o,resolve as n}from"path";import{getDeadCode as i}from"./dead-code.js";import{checkUndocumentedFile as r}from"./undocumented.js";import{getSimilarFunctions as l}from"./similar-functions.js";import{analyzeComplexityFile as c}from"./complexity.js";import{getLargeFiles as d}from"./large-files.js";import{getOutdatedPatterns as u}from"./outdated-patterns.js";import{getTableUsage as m}from"./db-analysis.js";import{checkJSDocFile as h}from"./jsdoc-checker.js";import{readCache as p,writeCache as y,computeContentHash as g,isCacheValid as f}from"./analysis-cache.js";import{shouldExcludeDir as j,shouldExcludeFile as x,parseGitignore as b}from"../core/filters.js";import{getWorkspaceRoot as C}from"../core/workspace.js";
3
+ function w(t){let e=100;const s=[];e-=Math.min(2*t.deadCode.total,20),t.deadCode.total>0&&s.push(`${t.deadCode.total} unused functions/classes`),e-=Math.min(.5*t.undocumented.total,15),t.undocumented.total>10&&s.push(`${t.undocumented.total} undocumented items`),e-=Math.min(3*t.similar.total,15),t.similar.total>0&&s.push(`${t.similar.total} similar function pairs`);const a=t.complexity.stats?.critical||0,o=t.complexity.stats?.high||0;e-=Math.min(5*a+2*o,20),a>0&&s.push(`${a} critical complexity functions`);const n=t.largeFiles.stats?.critical||0,i=t.largeFiles.stats?.warning||0;e-=Math.min(4*n+1*i,10),n>0&&s.push(`${n} files need splitting`);const r=t.outdated.stats?.bySeverity?.error||0,l=t.outdated.stats?.bySeverity?.warning||0;if(e-=Math.min(3*r+1*l,10),t.outdated.redundantDeps?.length>0&&s.push(`${t.outdated.redundantDeps.length} redundant npm dependencies`),t.jsdocConsistency){const a=t.jsdocConsistency.errors||0,o=t.jsdocConsistency.warnings||0;e-=Math.min(2*a+1*o,15),a>0&&s.push(`${a} JSDoc consistency errors`)}let c;return e=Math.max(0,Math.min(100,Math.round(e))),c=e>=90?"excellent":e>=70?"good":e>=50?"warning":"critical",{score:e,rating:c,topIssues:s.slice(0,5)}}
4
+ function F(t,n=t){t===n&&b(n);const i=[];try{for(const r of e(t)){const e=a(t,r),l=o(n,e);s(e).isDirectory()?j(r,l)||i.push(...F(e,n)):!r.endsWith(".js")||r.endsWith(".css.js")||r.endsWith(".tpl.js")||x(r,l)||i.push(e)}}catch(t){}return i}
5
+ function S(e,s){const a=n(e),i=C(),l=F(e),d=[],u=[],m=[];let j=0,x=0;for(const e of l){const n=o(a,e),l=o(i,e);let b;try{b=t(e,"utf-8")}catch(t){continue}const C=g(b),w=p(s,l);if(w&&f(w,w.sig,C,"content"))j++,w.complexity&&d.push(...w.complexity),w.undocumented&&u.push(...w.undocumented),w.jsdocIssues&&m.push(...w.jsdocIssues);else{x++;const t=c(b,n),e=r(b,n,"tests"),a=h(b,n);d.push(...t),u.push(...e),m.push(...a),y(s,l,{sig:w?.sig||C,contentHash:C,complexity:t,undocumented:e,jsdocIssues:a})}}return{complexity:d,undocumented:u,jsdocIssues:m,cacheStats:{hits:j,misses:x}}}
6
+ function D(t,e=5){let s=t.filter(t=>t.complexity>=e);s.sort((t,e)=>e.complexity-t.complexity);const a={low:s.filter(t=>"low"===t.rating).length,moderate:s.filter(t=>"moderate"===t.rating).length,high:s.filter(t=>"high"===t.rating).length,critical:s.filter(t=>"critical"===t.rating).length,average:s.length>0?Math.round(s.reduce((t,e)=>t+e.complexity,0)/s.length*10)/10:0};return{total:s.length,stats:a,items:s.slice(0,30)}}
7
+ function M(t){const e={class:t.filter(t=>"class"===t.type).length,function:t.filter(t=>"function"===t.type).length,method:t.filter(t=>"method"===t.type).length};return{total:t.length,byType:e,items:t.slice(0,20)}}
8
+ function P(t){const e=t.filter(t=>"error"===t.severity).length,s=t.filter(t=>"warning"===t.severity).length,a={};for(const e of t)a[e.file]=(a[e.file]||0)+1;return{issues:t,summary:{total:t.length,errors:e,warnings:s,byFile:a}}}
9
+ export async function getFullAnalysis(t,e={}){const s=e.includeItems||!1,o=(n(t),S(t,a(C(),".context"))),r=D(o.complexity),c=M(o.undocumented),h=P(o.jsdocIssues),[p,y,g,f,j]=await Promise.all([i(t).catch(()=>({total:0,byType:{},items:[]})),l(t,{threshold:70}).catch(()=>({total:0,pairs:[]})),d(t).catch(()=>({total:0,stats:{},items:[]})),u(t).catch(()=>({codePatterns:[],redundantDeps:[],stats:{totalPatterns:0,bySeverity:{},byPattern:{},redundantDeps:0}})),m(t).catch(()=>({tables:[],totalTables:0,totalQueries:0}))]),x=w({deadCode:p,undocumented:c,similar:y,complexity:r,largeFiles:g,outdated:f,jsdocConsistency:h.summary}),b={deadCode:{total:p.total,byType:p.byType,...s&&{items:p.items.slice(0,10)}},undocumented:{total:c.total,byType:c.byType,...s&&{items:c.items.slice(0,10)}},similar:{total:y.total,...s&&{pairs:y.pairs.slice(0,5)}},complexity:{total:r.total,stats:r.stats,...s&&{items:r.items.slice(0,10)}},largeFiles:{total:g.total,stats:g.stats,...s&&{items:g.items.slice(0,10)}},outdated:{totalPatterns:f.stats.totalPatterns,redundantDeps:f.redundantDeps,...s&&{codePatterns:f.codePatterns.slice(0,10)}},jsdocConsistency:{total:h.summary.total,errors:h.summary.errors,warnings:h.summary.warnings,...s&&{issues:h.issues.slice(0,10)}},cache:o.cacheStats,overall:x};return j.totalTables>0&&(b.database={tablesUsed:j.totalTables,totalQueries:j.totalQueries,tables:j.tables.map(t=>({name:t.table,readers:t.totalReaders,writers:t.totalWriters}))}),b}
10
+ export function getAnalysisSummaryOnly(t){const e=S(t,a(C(),".context")),s=D(e.complexity),o=M(e.undocumented),n=P(e.jsdocIssues),i=w({deadCode:{total:0},undocumented:o,similar:{total:0},complexity:s,largeFiles:{total:0},outdated:{stats:{totalPatterns:0}},jsdocConsistency:n.summary});return{healthScore:i.score,grade:i.rating,complexity:s.total,undocumented:o.total,jsdocIssues:n.summary.total,cache:e.cacheStats,note:"Partial score — cross-file analyses skipped for speed. Run get_full_analysis for complete health check."}}
@@ -1,24 +1,12 @@
1
1
  // @ctx .context/src/analysis/jsdoc-checker.ctx
2
- import{readFileSync as e,readdirSync as t,statSync as n}from"fs";import{join as r,relative as s,resolve as i}from"path";import{parse as o}from"../../vendor/acorn.mjs";import*as a from"../../vendor/walk.mjs";import{shouldExcludeDir as c,shouldExcludeFile as l,parseGitignore as u}from"../core/filters.js";function findJSFiles(e,i=e){e===i&&u(i);
3
- const o=[];try{for(const a of t(e)){const t=r(e,a),u=s(i,t);n(t).isDirectory()?c(a,u)||o.push(...findJSFiles(t,i)):!a.endsWith(".js")||a.endsWith(".css.js")||a.endsWith(".tpl.js")||l(a,u)||o.push(t)}}catch(e){}return o}
4
- function extractJSDocComments(e){const t=[],n=/\/\*\*[\s\S]*?\*\//g;
5
- let r;for(;null!==(r=n.exec(e));){const n=r[0],s=e.slice(0,r.index+n.length).split("\n").length,i=[],o=/@param\s+\{/g;
6
- let a;for(;null!==(a=o.exec(n));){let e=1,t=a.index+a[0].length;for(;t<n.length&&e>0;)"{"===n[t]?e++:"}"===n[t]&&e--,t++;if(0!==e)continue;
7
- const r=n.slice(a.index+a[0].length,t-1),s=n.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!s)continue;
8
- let o=s[1];o.startsWith("[")&&(o=o.slice(1)),o.endsWith("]")&&(o=o.slice(0,-1)),o.includes(".")||i.push({name:o,type:r})}const c=/@returns?\s/.test(n);t.push({text:n,endLine:s,params:i,hasReturns:c})}return t}
9
- function findJSDocBefore(e,t){for(const n of e){const e=t-n.endLine;if(e>=0&&e<=2)return n}return null}
10
- function extractParamName(e){return"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name:"RestElement"===e.type&&"Identifier"===e.argument.type?e.argument.name:"ObjectPattern"===e.type?"options":"ArrayPattern"===e.type?"args":"param"}
11
- function inferTypeFromDefault(e){if("AssignmentPattern"!==e.type)return null;
12
- const t=e.right;if("Literal"===t.type){if("string"==typeof t.value)return"string";if("number"==typeof t.value)return"number";if("boolean"==typeof t.value)return"boolean"}return"ArrayExpression"===t.type?"Array":"ObjectExpression"===t.type?"Object":null}
13
- function hasReturnValue(e){let t=!1;try{a.simple(e.body,{ReturnStatement(e){e.argument&&(t=!0)},FunctionDeclaration(){},FunctionExpression(){},ArrowFunctionExpression(){}})}catch(e){}return t}
14
- function validateFunction(e,t,n,r,s,i){const o=[];if(!e)return o;
15
- const a=e.params;a.length>0&&a.length!==t.length&&o.push({file:s,line:i,name:r,severity:"error",message:`Param count mismatch: JSDoc has ${a.length}, function has ${t.length}`});
16
- const c=Math.min(a.length,t.length);for(let e=0;e<c;e++){const n=a[e].name,c=extractParamName(t[e]);n!==c&&"options"!==c&&"args"!==c&&"param"!==c&&o.push({file:s,line:i,name:r,severity:"error",message:`Param name mismatch at position ${e}: JSDoc says "${n}", code has "${c}"`})}!e.hasReturns&&hasReturnValue(n)&&o.push({file:s,line:i,name:r,severity:"warning",message:"Function returns a value but JSDoc has no @returns"});for(let e=0;e<c;e++){const n=a[e].type,c=inferTypeFromDefault(t[e]);if(c&&n&&"*"!==n){let t=n.toLowerCase().includes(c.toLowerCase());!t&&"string"===c&&n.includes("'")&&n.includes("|")&&(t=!0),!t&&"Array"===c&&n.includes("[]")&&(t=!0),t||o.push({file:s,line:i,name:r,severity:"warning",message:`Type mismatch for "${a[e].name}": JSDoc says {${n}}, default value suggests {${c}}`})}}return o}
17
- export function checkJSDocFile(e,t){const n=[];
18
- let r;try{r=o(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return n}const s=extractJSDocComments(e);return a.simple(r,{FunctionDeclaration(e){if(!e.id)return;
19
- const r=findJSDocBefore(s,e.loc.start.line);r&&n.push(...validateFunction(r,e.params,e,e.id.name,t,e.loc.start.line))},VariableDeclaration(e){for(const r of e.declarations){if(!r.init)continue;
20
- const i="ArrowFunctionExpression"===r.init.type||"FunctionExpression"===r.init.type?r.init:null;if(!i||!r.id?.name)continue;
21
- const o=findJSDocBefore(s,e.loc.start.line);o&&n.push(...validateFunction(o,i.params,i,r.id.name,t,e.loc.start.line))}},ClassDeclaration(e){const r=e.id?.name||"Anonymous";for(const i of e.body.body){if("MethodDefinition"!==i.type)continue;
22
- const e=i.key.name||i.key.value;if(!e||"constructor"===e)continue;if("method"!==i.kind)continue;
23
- const o=i.value,a=findJSDocBefore(s,i.loc.start.line);a&&n.push(...validateFunction(a,o.params,o,`${r}.${e}`,t,i.loc.start.line))}}}),n}
24
- export function checkJSDocConsistency(t){const n=i(t),r=findJSFiles(t),o=[];for(const t of r){let r;try{r=e(t,"utf-8")}catch(e){continue}const i=checkJSDocFile(r,s(n,t));o.push(...i)}const a=o.filter(e=>"error"===e.severity).length,c=o.filter(e=>"warning"===e.severity).length,l={};for(const e of o)l[e.file]=(l[e.file]||0)+1;return{issues:o,summary:{total:o.length,errors:a,warnings:c,byFile:l}}}
2
+ import{readFileSync as e,readdirSync as t,statSync as n}from"fs";import{join as s,relative as r,resolve as i}from"path";import{parse as o}from"../../vendor/acorn.mjs";
3
+ import*as a from"../../vendor/walk.mjs";
4
+ import{shouldExcludeDir as c,shouldExcludeFile as l,parseGitignore as u}from"../core/filters.js";
5
+ function f(e,i=e){e===i&&u(i);const o=[];try{for(const a of t(e)){const t=s(e,a),u=r(i,t);n(t).isDirectory()?c(a,u)||o.push(...f(t,i)):!a.endsWith(".js")||a.endsWith(".css.js")||a.endsWith(".tpl.js")||l(a,u)||o.push(t)}}catch(e){}return o}
6
+ function m(e,t){for(const n of e){const e=t-n.endLine;if(e>=0&&e<=2)return n}return null}
7
+ function p(e){return"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name:"RestElement"===e.type&&"Identifier"===e.argument.type?e.argument.name:"ObjectPattern"===e.type?"options":"ArrayPattern"===e.type?"args":"param"}
8
+ function h(e){if("AssignmentPattern"!==e.type)return null;const t=e.right;if("Literal"===t.type){if("string"==typeof t.value)return"string";if("number"==typeof t.value)return"number";if("boolean"==typeof t.value)return"boolean"}return"ArrayExpression"===t.type?"Array":"ObjectExpression"===t.type?"Object":null}
9
+ function y(e){let t=!1;try{a.simple(e.body,{ReturnStatement(e){e.argument&&(t=!0)},FunctionDeclaration(){},FunctionExpression(){},ArrowFunctionExpression(){}})}catch(e){}return t}
10
+ function d(e,t,n,s,r,i){const o=[];if(!e)return o;const a=e.params;a.length>0&&a.length!==t.length&&o.push({file:r,line:i,name:s,severity:"error",message:`Param count mismatch: JSDoc has ${a.length}, function has ${t.length}`});const c=Math.min(a.length,t.length);for(let e=0;e<c;e++){const n=a[e].name,c=p(t[e]);n!==c&&"options"!==c&&"args"!==c&&"param"!==c&&o.push({file:r,line:i,name:s,severity:"error",message:`Param name mismatch at position ${e}: JSDoc says "${n}", code has "${c}"`})}!e.hasReturns&&y(n)&&o.push({file:r,line:i,name:s,severity:"warning",message:"Function returns a value but JSDoc has no @returns"});for(let e=0;e<c;e++){const n=a[e].type,c=h(t[e]);if(c&&n&&"*"!==n){let t=n.toLowerCase().includes(c.toLowerCase());!t&&"string"===c&&n.includes("'")&&n.includes("|")&&(t=!0),!t&&"Array"===c&&n.includes("[]")&&(t=!0),t||o.push({file:r,line:i,name:s,severity:"warning",message:`Type mismatch for "${a[e].name}": JSDoc says {${n}}, default value suggests {${c}}`})}}return o}
11
+ export function checkJSDocFile(e,t){const n=[];let s;try{s=o(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return n}const r=function(e){const t=[],n=/\/\*\*[\s\S]*?\*\//g;let s;for(;null!==(s=n.exec(e));){const n=s[0],r=e.slice(0,s.index+n.length).split("\n").length,i=[],o=/@param\s+\{/g;let a;for(;null!==(a=o.exec(n));){let e=1,t=a.index+a[0].length;for(;t<n.length&&e>0;)"{"===n[t]?e++:"}"===n[t]&&e--,t++;if(0!==e)continue;const s=n.slice(a.index+a[0].length,t-1),r=n.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let o=r[1];o.startsWith("[")&&(o=o.slice(1)),o.endsWith("]")&&(o=o.slice(0,-1)),o.includes(".")||i.push({name:o,type:s})}const c=/@returns?\s/.test(n);t.push({text:n,endLine:r,params:i,hasReturns:c})}return t}(e);return a.simple(s,{FunctionDeclaration(e){if(!e.id)return;const s=m(r,e.loc.start.line);s&&n.push(...d(s,e.params,e,e.id.name,t,e.loc.start.line))},VariableDeclaration(e){for(const s of e.declarations){if(!s.init)continue;const i="ArrowFunctionExpression"===s.init.type||"FunctionExpression"===s.init.type?s.init:null;if(!i||!s.id?.name)continue;const o=m(r,e.loc.start.line);o&&n.push(...d(o,i.params,i,s.id.name,t,e.loc.start.line))}},ClassDeclaration(e){const s=e.id?.name||"Anonymous";for(const i of e.body.body){if("MethodDefinition"!==i.type)continue;const e=i.key.name||i.key.value;if(!e||"constructor"===e)continue;if("method"!==i.kind)continue;const o=i.value,a=m(r,i.loc.start.line);a&&n.push(...d(a,o.params,o,`${s}.${e}`,t,i.loc.start.line))}}}),n}
12
+ export function checkJSDocConsistency(t){const n=i(t),s=f(t),o=[];for(const t of s){let s;try{s=e(t,"utf-8")}catch(e){continue}const i=checkJSDocFile(s,r(n,t));o.push(...i)}const a=o.filter(e=>"error"===e.severity).length,c=o.filter(e=>"warning"===e.severity).length,l={};for(const e of o)l[e.file]=(l[e.file]||0)+1;return{issues:o,summary:{total:o.length,errors:a,warnings:c,byFile:l}}}
@@ -1,10 +1,9 @@
1
1
  // @ctx .context/src/analysis/jsdoc-generator.ctx
2
- import{readFileSync as t}from"fs";import{relative as e}from"path";import{parse as r}from"../../vendor/acorn.mjs";import*as n from"../../vendor/walk.mjs";import{getWorkspaceRoot as a}from"../core/workspace.js";
3
- export function generateJSDoc(i,o={}){const s=[],c=t(i,"utf-8"),m=e(a(),i);
4
- let f;try{f=r(c,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return s}const hasJSDocAt=t=>{const e=c.split("\n");for(let r=t-2;r>=Math.max(0,t-15);r--){const t=e[r]?.trim();if(t){if("*/"===t||t.endsWith("*/")){for(let t=r-1;t>=Math.max(0,r-20);t--){const r=e[t]?.trim();if(r?.startsWith("/**"))return!0;if(r&&!r.startsWith("*"))break}return!1}if(!t.startsWith("*")&&!t.startsWith("//"))break}}return!1};return n.simple(f,{FunctionDeclaration(t){if(!t.id)return;if(hasJSDocAt(t.loc.start.line))return;
5
- const e=buildJSDoc({name:t.id.name,params:t.params,async:t.async});s.push({name:t.id.name,type:"function",file:m,line:t.loc.start.line,jsdoc:e})},ClassDeclaration(t){if(t.id)for(const e of t.body.body)if("MethodDefinition"===e.type){const r=e.key.name||e.key.value;if("method"!==e.kind)continue;if(r.startsWith("_"))continue;if(hasJSDocAt(e.loc.start.line))continue;
6
- const n=e.value,a=buildJSDoc({name:r,params:n.params,async:n.async});s.push({name:`${t.id.name}.${r}`,type:"method",file:m,line:e.loc.start.line,jsdoc:a})}}}),s}
7
- function buildJSDoc(t){const e=["/**"];e.push(` * TODO: Add description for ${t.name}`);for(const r of t.params){const t=extractParamName(r),n=inferParamType(r);e.push(` * @param {${n}} ${t}`)}return e.push(` * @returns {${t.async?"Promise<*>":"*"}}`),e.push(" */"),e.join("\n")}
8
- function extractParamName(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?`[${t.left.name}]`:"RestElement"===t.type&&"Identifier"===t.argument.type?`...${t.argument.name}`:"ObjectPattern"===t.type?"options":"ArrayPattern"===t.type?"args":"param"}
9
- function inferParamType(t){if("AssignmentPattern"===t.type){const e=t.right;if("Literal"===e.type){if("string"==typeof e.value)return"string";if("number"==typeof e.value)return"number";if("boolean"==typeof e.value)return"boolean"}if("ArrayExpression"===e.type)return"Array";if("ObjectExpression"===e.type)return"Object"}return"RestElement"===t.type?"Array":"ObjectPattern"===t.type?"Object":"ArrayPattern"===t.type?"Array":"*"}
10
- export function generateJSDocFor(t,e,r={}){return generateJSDoc(t,r).find(t=>t.name===e||t.name.endsWith(`.${e}`))||null}
2
+ import{readFileSync as t}from"fs";import{relative as e}from"path";import{parse as n}from"../../vendor/acorn.mjs";
3
+ import*as r from"../../vendor/walk.mjs";
4
+ import{getWorkspaceRoot as a}from"../core/workspace.js";
5
+ export function generateJSDoc(o,s={}){const c=[],f=t(o,"utf-8"),m=e(a(),o);let p;try{p=n(f,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return c}const u=t=>{const e=f.split("\n");for(let n=t-2;n>=Math.max(0,t-15);n--){const t=e[n]?.trim();if(t){if("*/"===t||t.endsWith("*/")){for(let t=n-1;t>=Math.max(0,n-20);t--){const n=e[t]?.trim();if(n?.startsWith("/**"))return!0;if(n&&!n.startsWith("*"))break}return!1}if(!t.startsWith("*")&&!t.startsWith("//"))break}}return!1};return r.simple(p,{FunctionDeclaration(t){if(!t.id)return;if(u(t.loc.start.line))return;const e=i({name:t.id.name,params:t.params,async:t.async});c.push({name:t.id.name,type:"function",file:m,line:t.loc.start.line,jsdoc:e})},ClassDeclaration(t){if(t.id)for(const e of t.body.body)if("MethodDefinition"===e.type){const n=e.key.name||e.key.value;if("method"!==e.kind)continue;if(n.startsWith("_"))continue;if(u(e.loc.start.line))continue;const r=e.value,a=i({name:n,params:r.params,async:r.async});c.push({name:`${t.id.name}.${n}`,type:"method",file:m,line:e.loc.start.line,jsdoc:a})}}}),c}
6
+ function i(t){const e=["/**"];e.push(` * TODO: Add description for ${t.name}`);for(const n of t.params){const t=o(n),r=s(n);e.push(` * @param {${r}} ${t}`)}return e.push(` * @returns {${t.async?"Promise<*>":"*"}}`),e.push(" */"),e.join("\n")}
7
+ function o(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?`[${t.left.name}]`:"RestElement"===t.type&&"Identifier"===t.argument.type?`...${t.argument.name}`:"ObjectPattern"===t.type?"options":"ArrayPattern"===t.type?"args":"param"}
8
+ function s(t){if("AssignmentPattern"===t.type){const e=t.right;if("Literal"===e.type){if("string"==typeof e.value)return"string";if("number"==typeof e.value)return"number";if("boolean"==typeof e.value)return"boolean"}if("ArrayExpression"===e.type)return"Array";if("ObjectExpression"===e.type)return"Object"}return"RestElement"===t.type?"Array":"ObjectPattern"===t.type?"Object":"ArrayPattern"===t.type?"Array":"*"}
9
+ export function generateJSDocFor(t,e,n={}){return generateJSDoc(t,n).find(t=>t.name===e||t.name.endsWith(`.${e}`))||null}
@@ -1,16 +1,9 @@
1
1
  // @ctx .context/src/analysis/similar-functions.ctx
2
- import{readFileSync as t,readdirSync as e,statSync as a}from"fs";import{join as n,relative as s,resolve as r}from"path";import{parse as i}from"../../vendor/acorn.mjs";import*as l from"../../vendor/walk.mjs";import{shouldExcludeDir as o,shouldExcludeFile as c,parseGitignore as h}from"../core/filters.js";function findJSFiles(t,r=t){t===r&&h(r);
3
- const i=[];try{for(const l of e(t)){const e=n(t,l),h=s(r,e);a(e).isDirectory()?o(l,h)||i.push(...findJSFiles(e,r)):!l.endsWith(".js")||l.endsWith(".css.js")||l.endsWith(".tpl.js")||c(l,h)||i.push(e)}}catch(t){}return i}
4
- function extractSignatures(e,a){const n=t(e,"utf-8"),r=s(a,e),o=[];
5
- let c;try{c=i(n,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return o}return l.simple(c,{FunctionDeclaration(t){t.id&&o.push(buildSignature(t,t.id.name,r))},MethodDefinition(t){if("method"!==t.kind)return;
6
- const e=t.key.name||t.key.value;e.startsWith("_")||o.push(buildSignature(t.value,e,r))}}),o}
7
- function buildSignature(t,e,a){const n=t.params.map(t=>extractParamName(t)),s=[];l.simple(t.body,{CallExpression(t){"Identifier"===t.callee.type?s.push(t.callee.name):"MemberExpression"===t.callee.type&&"Identifier"===t.callee.property.type&&s.push(t.callee.property.name)}});
8
- const r=hashBodyStructure(t.body);return{name:e,file:a,line:t.loc?.start?.line||0,paramCount:t.params.length,paramNames:n,async:t.async||!1,bodyHash:r,calls:[...new Set(s)]}}
9
- function extractParamName(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument.type?t.argument.name:"param"}
10
- function hashBodyStructure(t){const e=[];return l.simple(t,{IfStatement(){e.push("IF")},ForStatement(){e.push("FOR")},ForOfStatement(){e.push("FOROF")},ForInStatement(){e.push("FORIN")},WhileStatement(){e.push("WHILE")},SwitchStatement(){e.push("SWITCH")},TryStatement(){e.push("TRY")},ReturnStatement(){e.push("RET")},ThrowStatement(){e.push("THROW")},AwaitExpression(){e.push("AWAIT")}}),e.join("|")}
11
- function calculateSimilarity(t,e){const a=[];
12
- let n=0,s=0;s+=30,t.paramCount===e.paramCount&&(n+=30,a.push("Same param count")),s+=20;
13
- const r=t.paramNames.filter(t=>e.paramNames.includes(t));if(r.length>0&&t.paramNames.length>0){const s=r.length/Math.max(t.paramNames.length,e.paramNames.length);n+=Math.round(20*s),s>=.5&&a.push(`Similar params: ${r.join(", ")}`)}if(s+=10,t.async===e.async&&(n+=10),s+=25,t.bodyHash===e.bodyHash&&t.bodyHash.length>0)n+=25,a.push("Identical structure");else if(t.bodyHash.length>0&&e.bodyHash.length>0){const s=t.bodyHash.split("|"),r=e.bodyHash.split("|"),i=s.filter(t=>r.includes(t));if(i.length>0){const t=i.length/Math.max(s.length,r.length);n+=Math.round(25*t),t>=.5&&a.push("Similar control flow")}}s+=15;
14
- const i=t.calls.filter(t=>e.calls.includes(t));if(i.length>0&&t.calls.length>0&&e.calls.length>0){const s=i.length/Math.max(t.calls.length,e.calls.length);n+=Math.round(15*s),i.length>=2&&a.push(`Common calls: ${i.slice(0,3).join(", ")}`)}return{similarity:Math.round(n/100*100),reasons:a}}
15
- export async function getSimilarFunctions(t,e={}){const a=e.threshold||60,n=r(t),s=findJSFiles(t),i=[];for(const t of s)i.push(...extractSignatures(t,n));
16
- const l=[];for(let t=0;t<i.length;t++)for(let e=t+1;e<i.length;e++){const n=i[t],s=i[e];if(n.file===s.file&&n.name===s.name)continue;if(n.bodyHash.length<3&&s.bodyHash.length<3)continue;const{similarity:r,reasons:o}=calculateSimilarity(n,s);r>=a&&o.length>0&&l.push({a:n,b:s,similarity:r,reasons:o})}return l.sort((t,e)=>e.similarity-t.similarity),{total:l.length,pairs:l.slice(0,20)}}
2
+ import{readFileSync as t,readdirSync as e,statSync as n}from"fs";import{join as s,relative as a,resolve as r}from"path";import{parse as l}from"../../vendor/acorn.mjs";
3
+ import*as o from"../../vendor/walk.mjs";
4
+ import{shouldExcludeDir as i,shouldExcludeFile as h,parseGitignore as c}from"../core/filters.js";
5
+ function m(t,r=t){t===r&&c(r);const l=[];try{for(const o of e(t)){const e=s(t,o),c=a(r,e);n(e).isDirectory()?i(o,c)||l.push(...m(e,r)):!o.endsWith(".js")||o.endsWith(".css.js")||o.endsWith(".tpl.js")||h(o,c)||l.push(e)}}catch(t){}return l}
6
+ function p(e,n){const s=t(e,"utf-8"),r=a(n,e),i=[];let h;try{h=l(s,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return i}return o.simple(h,{FunctionDeclaration(t){t.id&&i.push(u(t,t.id.name,r))},MethodDefinition(t){if("method"!==t.kind)return;const e=t.key.name||t.key.value;e.startsWith("_")||i.push(u(t.value,e,r))}}),i}
7
+ function u(t,e,n){const s=t.params.map(t=>function(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument.type?t.argument.name:"param"}(t)),a=[];o.simple(t.body,{CallExpression(t){"Identifier"===t.callee.type?a.push(t.callee.name):"MemberExpression"===t.callee.type&&"Identifier"===t.callee.property.type&&a.push(t.callee.property.name)}});const r=function(t){const e=[];return o.simple(t,{IfStatement(){e.push("IF")},ForStatement(){e.push("FOR")},ForOfStatement(){e.push("FOROF")},ForInStatement(){e.push("FORIN")},WhileStatement(){e.push("WHILE")},SwitchStatement(){e.push("SWITCH")},TryStatement(){e.push("TRY")},ReturnStatement(){e.push("RET")},ThrowStatement(){e.push("THROW")},AwaitExpression(){e.push("AWAIT")}}),e.join("|")}(t.body);return{name:e,file:n,line:t.loc?.start?.line||0,paramCount:t.params.length,paramNames:s,async:t.async||!1,bodyHash:r,calls:[...new Set(a)]}}
8
+ function f(t,e){const n=[];let s=0,a=0;a+=30,t.paramCount===e.paramCount&&(s+=30,n.push("Same param count")),a+=20;const r=t.paramNames.filter(t=>e.paramNames.includes(t));if(r.length>0&&t.paramNames.length>0){const a=r.length/Math.max(t.paramNames.length,e.paramNames.length);s+=Math.round(20*a),a>=.5&&n.push(`Similar params: ${r.join(", ")}`)}if(a+=10,t.async===e.async&&(s+=10),a+=25,t.bodyHash===e.bodyHash&&t.bodyHash.length>0)s+=25,n.push("Identical structure");else if(t.bodyHash.length>0&&e.bodyHash.length>0){const a=t.bodyHash.split("|"),r=e.bodyHash.split("|"),l=a.filter(t=>r.includes(t));if(l.length>0){const t=l.length/Math.max(a.length,r.length);s+=Math.round(25*t),t>=.5&&n.push("Similar control flow")}}a+=15;const l=t.calls.filter(t=>e.calls.includes(t));if(l.length>0&&t.calls.length>0&&e.calls.length>0){const a=l.length/Math.max(t.calls.length,e.calls.length);s+=Math.round(15*a),l.length>=2&&n.push(`Common calls: ${l.slice(0,3).join(", ")}`)}return{similarity:Math.round(s/100*100),reasons:n}}
9
+ export async function getSimilarFunctions(t,e={}){const n=e.threshold||60,s=r(t),a=m(t),l=[];for(const t of a)l.push(...p(t,s));const o=[];for(let t=0;t<l.length;t++)for(let e=t+1;e<l.length;e++){const s=l[t],a=l[e];if(s.file===a.file&&s.name===a.name)continue;if(s.bodyHash.length<3&&a.bodyHash.length<3)continue;const{similarity:r,reasons:i}=f(s,a);r>=n&&i.length>0&&o.push({a:s,b:a,similarity:r,reasons:i})}return o.sort((t,e)=>e.similarity-t.similarity),{total:o.length,pairs:o.slice(0,20)}}
@@ -1,21 +1,13 @@
1
1
  // @ctx .context/src/analysis/test-annotations.ctx
2
- import{readFileSync as t,readdirSync as e,statSync as s,writeFileSync as n}from"fs";import{join as o,relative as r,resolve as a}from"path";function findCtxMdFiles(t){const n=[];try{for(const r of e(t)){const e=o(t,r);s(e).isDirectory()&&!r.startsWith(".")?n.push(...findCtxMdFiles(e)):r.endsWith(".ctx.md")&&n.push(e)}}catch(t){}return n}
3
- export function parseAnnotations(t,e){const s=t.split("\n"),n=[];
4
- let o=!1,r=[];for(const t of s){if(t.startsWith("## ")){o&&r.length&&(n.push(...groupByName(r,e)),r=[]),o=t.startsWith("## Tests");continue}if(!o)continue;
5
- const s=t.match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(!s)continue;const[,a,i,c]=s,f=c.split("→").map(t=>t.trim()),u=f[0],l=f[1]||null;
6
- let p=null,d="pending";if("x"===a&&(d="passed"),"!"===a){d="failed";
7
- const t=u.match(/\(FAILED:\s*(.+)\)$/);t&&(p=t[1].trim())}r.push({name:i,action:u,expected:l,status:d,failReason:p})}return o&&r.length&&n.push(...groupByName(r,e)),n}
8
- function groupByName(t,e){const s={};
9
- let n={};for(const e of t)s[e.name]||(s[e.name]=[],n[e.name]=0),s[e.name].push({id:`${e.name}.${n[e.name]++}`,action:e.action,expected:e.expected,status:e.status,failReason:e.failReason});return Object.entries(s).map(([t,s])=>({name:t,tests:s,file:e}))}
10
- export function getAllFeatures(e){const s=findCtxMdFiles(o(a(e),".context")),n=[];for(const e of s)try{const s=parseAnnotations(t(e,"utf-8"),e);n.push(...s)}catch(t){}return n}
11
- export function getPendingTests(t){const e=a(t),s=getAllFeatures(t),n=[];for(const t of s)for(const s of t.tests)"pending"===s.status&&n.push({...s,feature:t.name,file:r(e,t.file)});return n}
12
- export function markTestPassed(t){return updateTestState(t.split(".")[0],t,"x")}
13
- export function markTestFailed(t,e){return updateTestState(t.split(".")[0],t,"!",e)}
14
- function updateTestState(e,s,r,a){const i=process.cwd(),c=findCtxMdFiles(o(i,".context")),f=parseInt(s.split(".")[1],10);for(const o of c)try{const i=t(o,"utf-8").split("\n");
15
- let c=!1,u=0;for(let t=0;t<i.length;t++){if(i[t].startsWith("## ")){c=i[t].startsWith("## Tests");continue}if(!c)continue;
16
- const l=i[t].match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(l&&l[2]===e){if(u===f){const c=l[3].replace(/\s*\(FAILED:.*\)$/,""),f=a?` (FAILED: ${a})`:"";return i[t]=`- [${r}] ${e}: ${c}${f}`,n(o,i.join("\n"),"utf-8"),{success:!0,testId:s,...a?{reason:a}:{}}}u++}}}catch(t){}return{success:!1,testId:s,error:"Test not found"}}
17
- export function getTestSummary(t){const e=getAllFeatures(t);
18
- let s=0,n=0,o=0,r=0;
19
- const a=[];for(const t of e)for(const e of t.tests)s++,"passed"===e.status?n++:"failed"===e.status?(o++,a.push({id:e.id,reason:e.failReason})):r++;return{total:s,passed:n,failed:o,pending:r,progress:s>0?Math.round((n+o)/s*100):0,failures:a}}
20
- export function resetTestState(){const e=process.cwd(),s=findCtxMdFiles(o(e,".context"));for(const e of s)try{let s=t(e,"utf-8");
21
- const o=s.replace(/^(- )\[([x!])\] (\w+:\s*.+?)(?:\s*\(FAILED:.*\))?$/gm,"$1[ ] $3");o!==s&&n(e,o,"utf-8")}catch(t){}return{success:!0}}
2
+ import{readFileSync as t,readdirSync as s,statSync as e,writeFileSync as n}from"fs";
3
+ import{join as o,relative as r,resolve as c}from"path";
4
+ function a(t){const n=[];try{for(const r of s(t)){const s=o(t,r);e(s).isDirectory()&&!r.startsWith(".")?n.push(...a(s)):r.endsWith(".ctx.md")&&n.push(s)}}catch(t){}return n}
5
+ export function parseAnnotations(t,s){const e=t.split("\n"),n=[];let o=!1,r=[];for(const t of e){if(t.startsWith("## ")){o&&r.length&&(n.push(...i(r,s)),r=[]),o=t.startsWith("## Tests");continue}if(!o)continue;const e=t.match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(!e)continue;const[,c,a,f]=e,u=f.split("→").map(t=>t.trim()),l=u[0],p=u[1]||null;let d=null,h="pending";if("x"===c&&(h="passed"),"!"===c){h="failed";const t=l.match(/\(FAILED:\s*(.+)\)$/);t&&(d=t[1].trim())}r.push({name:a,action:l,expected:p,status:h,failReason:d})}return o&&r.length&&n.push(...i(r,s)),n}
6
+ function i(t,s){const e={};let n={};for(const s of t)e[s.name]||(e[s.name]=[],n[s.name]=0),e[s.name].push({id:`${s.name}.${n[s.name]++}`,action:s.action,expected:s.expected,status:s.status,failReason:s.failReason});return Object.entries(e).map(([t,e])=>({name:t,tests:e,file:s}))}
7
+ export function getAllFeatures(s){const e=a(o(c(s),".context")),n=[];for(const s of e)try{const e=parseAnnotations(t(s,"utf-8"),s);n.push(...e)}catch(t){}return n}
8
+ export function getPendingTests(t){const s=c(t),e=getAllFeatures(t),n=[];for(const t of e)for(const e of t.tests)"pending"===e.status&&n.push({...e,feature:t.name,file:r(s,t.file)});return n}
9
+ export function markTestPassed(t){return f(t.split(".")[0],t,"x")}
10
+ export function markTestFailed(t,s){return f(t.split(".")[0],t,"!",s)}
11
+ function f(s,e,r,c){const i=process.cwd(),f=a(o(i,".context")),u=parseInt(e.split(".")[1],10);for(const o of f)try{const a=t(o,"utf-8").split("\n");let i=!1,f=0;for(let t=0;t<a.length;t++){if(a[t].startsWith("## ")){i=a[t].startsWith("## Tests");continue}if(!i)continue;const l=a[t].match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(l&&l[2]===s){if(f===u){const i=l[3].replace(/\s*\(FAILED:.*\)$/,""),f=c?` (FAILED: ${c})`:"";return a[t]=`- [${r}] ${s}: ${i}${f}`,n(o,a.join("\n"),"utf-8"),{success:!0,testId:e,...c?{reason:c}:{}}}f++}}}catch(t){}return{success:!1,testId:e,error:"Test not found"}}
12
+ export function getTestSummary(t){const s=getAllFeatures(t);let e=0,n=0,o=0,r=0;const c=[];for(const t of s)for(const s of t.tests)e++,"passed"===s.status?n++:"failed"===s.status?(o++,c.push({id:s.id,reason:s.failReason})):r++;return{total:e,passed:n,failed:o,pending:r,progress:e>0?Math.round((n+o)/e*100):0,failures:c}}
13
+ export function resetTestState(){const s=process.cwd(),e=a(o(s,".context"));for(const s of e)try{let e=t(s,"utf-8");const o=e.replace(/^(- )\[([x!])\] (\w+:\s*.+?)(?:\s*\(FAILED:.*\))?$/gm,"$1[ ] $3");o!==e&&n(s,o,"utf-8")}catch(t){}return{success:!0}}
@@ -1,8 +1,6 @@
1
1
  // @ctx .context/src/analysis/type-checker.ctx
2
- import{execSync as t,spawn as e}from"child_process";import{existsSync as s}from"fs";import{resolve as n,join as i}from"path";function detectTsc(){try{return{available:!0,version:t("tsc --version",{encoding:"utf-8",timeout:5e3}).trim(),path:t("which tsc",{encoding:"utf-8",timeout:5e3}).trim()}}catch(e){try{return{available:!0,version:t("npx tsc --version",{encoding:"utf-8",timeout:15e3}).trim(),path:"npx tsc"}}catch(t){return{available:!1,version:null,path:null}}}}
3
- function parseDiagnosticLine(t,e){const s=t.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);return s?{file:s[1],line:parseInt(s[2]),column:parseInt(s[3]),severity:s[4],message:s[6],code:s[5]}:null}
4
- function buildArgs(t,e={}){const n=["--noEmit"],r=i(t,"tsconfig.json"),o=i(t,"jsconfig.json");return s(r)?n.push("--project",r):s(o)?n.push("--project",o):(n.push("--allowJs","--checkJs"),n.push("--target","ESNext"),n.push("--module","NodeNext"),n.push("--moduleResolution","NodeNext"),n.push("--skipLibCheck"),e.files?.length?n.push(...e.files):n.push("--rootDir",t)),n}
5
- export async function checkTypes(t,s={}){const i=s.maxDiagnostics||50,r=n(t),o=detectTsc();if(!o.available)return{available:!1,version:null,diagnostics:[],summary:{total:0,errors:0,warnings:0},hint:"TypeScript not found. Install: npm i -g typescript"};
6
- const l=buildArgs(r,s),a=o.path.includes("npx")?"npx":"tsc",c=o.path.includes("npx")?["tsc",...l]:l,u=await new Promise(t=>{const s=e(a,c,{cwd:r,stdio:["ignore","pipe","pipe"]});
7
- let n="",i="";s.stdout.on("data",t=>{n+=t}),s.stderr.on("data",t=>{i+=t});
8
- const o=setTimeout(()=>{s.kill("SIGTERM"),t({stdout:n,stderr:i,killed:!0})},6e4);s.on("close",()=>{clearTimeout(o),t({stdout:n,stderr:i,killed:!1})}),s.on("error",e=>{clearTimeout(o),t({stdout:"",stderr:e.message,killed:!1})})}),p=((u.stdout||"")+(u.stderr||"")).split("\n").filter(t=>t.trim()),d=[];for(const t of p){const e=parseDiagnosticLine(t);e&&d.length<i&&d.push(e)}const h=d.filter(t=>"error"===t.severity).length,m=d.filter(t=>"warning"===t.severity).length,f={};for(const t of d)f[t.file]=(f[t.file]||0)+1;return{available:!0,version:o.version,diagnostics:d,summary:{total:d.length,errors:h,warnings:m,byFile:f},hint:null}}
2
+ import{execSync as t,spawn as e}from"child_process";import{existsSync as s}from"fs";import{resolve as n,join as r}from"path";
3
+ function o(){try{return{available:!0,version:t("tsc --version",{encoding:"utf-8",timeout:5e3}).trim(),path:t("which tsc",{encoding:"utf-8",timeout:5e3}).trim()}}catch(e){try{return{available:!0,version:t("npx tsc --version",{encoding:"utf-8",timeout:15e3}).trim(),path:"npx tsc"}}catch(t){return{available:!1,version:null,path:null}}}}
4
+ function i(t,e){const s=t.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);return s?{file:s[1],line:parseInt(s[2]),column:parseInt(s[3]),severity:s[4],message:s[6],code:s[5]}:null}
5
+ function l(t,e={}){const n=["--noEmit"],o=r(t,"tsconfig.json"),i=r(t,"jsconfig.json");return s(o)?n.push("--project",o):s(i)?n.push("--project",i):(n.push("--allowJs","--checkJs"),n.push("--target","ESNext"),n.push("--module","NodeNext"),n.push("--moduleResolution","NodeNext"),n.push("--skipLibCheck"),e.files?.length?n.push(...e.files):n.push("--rootDir",t)),n}
6
+ export async function checkTypes(t,s={}){const r=s.maxDiagnostics||50,a=n(t),c=o();if(!c.available)return{available:!1,version:null,diagnostics:[],summary:{total:0,errors:0,warnings:0},hint:"TypeScript not found. Install: npm i -g typescript"};const u=l(a,s),p=c.path.includes("npx")?"npx":"tsc",d=c.path.includes("npx")?["tsc",...u]:u,h=await new Promise(t=>{const s=e(p,d,{cwd:a,stdio:["ignore","pipe","pipe"]});let n="",r="";s.stdout.on("data",t=>{n+=t}),s.stderr.on("data",t=>{r+=t});const o=setTimeout(()=>{s.kill("SIGTERM"),t({stdout:n,stderr:r,killed:!0})},6e4);s.on("close",()=>{clearTimeout(o),t({stdout:n,stderr:r,killed:!1})}),s.on("error",e=>{clearTimeout(o),t({stdout:"",stderr:e.message,killed:!1})})}),m=((h.stdout||"")+(h.stderr||"")).split("\n").filter(t=>t.trim()),f=[];for(const t of m){const e=i(t);e&&f.length<r&&f.push(e)}const g=f.filter(t=>"error"===t.severity).length,v=f.filter(t=>"warning"===t.severity).length,y={};for(const t of f)y[t.file]=(y[t.file]||0)+1;return{available:!0,version:c.version,diagnostics:f,summary:{total:f.length,errors:g,warnings:v,byFile:y},hint:null}}
@@ -1,14 +1,11 @@
1
1
  // @ctx .context/src/analysis/undocumented.ctx
2
- import{readFileSync as e,readdirSync as t,statSync as n}from"fs";import{join as s,relative as o,resolve as c}from"path";import{parse as i}from"../../vendor/acorn.mjs";import*as r from"../../vendor/walk.mjs";import{shouldExcludeDir as l,shouldExcludeFile as a,parseGitignore as u}from"../core/filters.js";function findJSFiles(e,c=e){e===c&&u(c);
3
- const i=[];try{for(const r of t(e)){const t=s(e,r),u=o(c,t);n(t).isDirectory()?l(r,u)||i.push(...findJSFiles(t,c)):!r.endsWith(".js")||r.endsWith(".css.js")||r.endsWith(".tpl.js")||a(r,u)||i.push(t)}}catch(e){}return i}
4
- function extractComments(e){const t=[],n=/\/\*\*[\s\S]*?\*\//g;
5
- let s;for(;null!==(s=n.exec(e));){const n=e.slice(0,s.index+s[0].length).split("\n").length;t.push({text:s[0],endLine:n})}return t}
6
- function findJSDocBefore(e,t){for(const n of e){const e=t-n.endLine;if(e>=0&&e<=2)return n.text}return null}
7
- function checkMissing(e,t){const n=[];return e?("params"!==t&&"all"!==t||(e.includes("@param")||n.push("@param"),e.includes("@returns")||e.includes("@return")||n.push("@returns")),n):("all"===t&&n.push("description"),"params"!==t&&"all"!==t||n.push("@param","@returns"),n)}const d=["constructor","connectedCallback","disconnectedCallback","attributeChangedCallback","renderCallback"];
8
- export function checkUndocumentedFile(e,t,n){const s=[];
9
- let o;try{o=i(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return s}const c=extractComments(e);return r.simple(o,{ClassDeclaration(e){const o=e.id?.name||"Anonymous";"all"===n&&(findJSDocBefore(c,e.loc.start.line)||s.push({name:o,type:"class",file:t,line:e.loc.start.line,reason:"No JSDoc"}));for(const i of e.body.body)if("MethodDefinition"===i.type){const e=i.key.name||i.key.value;if("get"===i.kind||"set"===i.kind)continue;if(e?.startsWith("_"))continue;if(d.includes(e))continue;
10
- const r=checkMissing(findJSDocBefore(c,i.loc.start.line),n);r.length>0&&s.push({name:`${o}.${e}`,type:"method",file:t,line:i.loc.start.line,reason:r.join(", ")})}},FunctionDeclaration(e){if(!e.id)return;
11
- const o=e.id.name;if(o.startsWith("_"))return;
12
- const i=checkMissing(findJSDocBefore(c,e.loc.start.line),n);i.length>0&&s.push({name:o,type:"function",file:t,line:e.loc.start.line,reason:i.join(", ")})}}),s}
13
- export function getUndocumented(t,n="tests"){const s=c(t),i=findJSFiles(t),r=[];for(const t of i){let c;try{c=e(t,"utf-8")}catch(e){continue}const i=checkUndocumentedFile(c,o(s,t),n);r.push(...i)}return r}
14
- export function getUndocumentedSummary(e,t="tests"){const n=getUndocumented(e,t),s={class:n.filter(e=>"class"===e.type).length,function:n.filter(e=>"function"===e.type).length,method:n.filter(e=>"method"===e.type).length},o={};for(const e of n)o[e.reason]=(o[e.reason]||0)+1;return{total:n.length,byType:s,byReason:o,items:n.slice(0,20)}}
2
+ import{readFileSync as t,readdirSync as e,statSync as n}from"fs";import{join as s,relative as o,resolve as r}from"path";import{parse as c}from"../../vendor/acorn.mjs";
3
+ import*as i from"../../vendor/walk.mjs";
4
+ import{shouldExcludeDir as l,shouldExcludeFile as a,parseGitignore as u}from"../core/filters.js";
5
+ function d(t,r=t){t===r&&u(r);const c=[];try{for(const i of e(t)){const e=s(t,i),u=o(r,e);n(e).isDirectory()?l(i,u)||c.push(...d(e,r)):!i.endsWith(".js")||i.endsWith(".css.js")||i.endsWith(".tpl.js")||a(i,u)||c.push(e)}}catch(t){}return c}
6
+ function f(t,e){for(const n of t){const t=e-n.endLine;if(t>=0&&t<=2)return n.text}return null}
7
+ function p(t,e){const n=[];return t?("params"!==e&&"all"!==e||(t.includes("@param")||n.push("@param"),t.includes("@returns")||t.includes("@return")||n.push("@returns")),n):("all"===e&&n.push("description"),"params"!==e&&"all"!==e||n.push("@param","@returns"),n)}
8
+ const m=["constructor","connectedCallback","disconnectedCallback","attributeChangedCallback","renderCallback"];
9
+ export function checkUndocumentedFile(t,e,n){const s=[];let o;try{o=c(t,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return s}const r=function(t){const e=[],n=/\/\*\*[\s\S]*?\*\//g;let s;for(;null!==(s=n.exec(t));){const n=t.slice(0,s.index+s[0].length).split("\n").length;e.push({text:s[0],endLine:n})}return e}(t);return i.simple(o,{ClassDeclaration(t){const o=t.id?.name||"Anonymous";"all"===n&&(f(r,t.loc.start.line)||s.push({name:o,type:"class",file:e,line:t.loc.start.line,reason:"No JSDoc"}));for(const c of t.body.body)if("MethodDefinition"===c.type){const t=c.key.name||c.key.value;if("get"===c.kind||"set"===c.kind)continue;if(t?.startsWith("_"))continue;if(m.includes(t))continue;const i=p(f(r,c.loc.start.line),n);i.length>0&&s.push({name:`${o}.${t}`,type:"method",file:e,line:c.loc.start.line,reason:i.join(", ")})}},FunctionDeclaration(t){if(!t.id)return;const o=t.id.name;if(o.startsWith("_"))return;const c=p(f(r,t.loc.start.line),n);c.length>0&&s.push({name:o,type:"function",file:e,line:t.loc.start.line,reason:c.join(", ")})}}),s}
10
+ export function getUndocumented(e,n="tests"){const s=r(e),c=d(e),i=[];for(const e of c){let r;try{r=t(e,"utf-8")}catch(t){continue}const c=checkUndocumentedFile(r,o(s,e),n);i.push(...c)}return i}
11
+ export function getUndocumentedSummary(t,e="tests"){const n=getUndocumented(t,e),s={class:n.filter(t=>"class"===t.type).length,function:n.filter(t=>"function"===t.type).length,method:n.filter(t=>"method"===t.type).length},o={};for(const t of n)o[t.reason]=(o[t.reason]||0)+1;return{total:n.length,byType:s,byReason:o,items:n.slice(0,20)}}
@@ -1,4 +1,5 @@
1
1
  // @ctx .context/src/cli/cli-handlers.ctx
2
- import{getSkeleton as r,expand as e,deps as s,usages as t}from"../mcp/tools.js";import{getPendingTests as a,getTestSummary as n}from"../analysis/test-annotations.js";import{getFilters as o}from"../core/filters.js";import{getInstructions as c}from"../compact/instructions.js";import{getUndocumentedSummary as i}from"../analysis/undocumented.js";import{getDeadCode as d}from"../analysis/dead-code.js";import{generateJSDoc as l}from"../analysis/jsdoc-generator.js";import{getSimilarFunctions as m}from"../analysis/similar-functions.js";import{getComplexity as g}from"../analysis/complexity.js";import{getLargeFiles as u}from"../analysis/large-files.js";import{getOutdatedPatterns as p}from"../analysis/outdated-patterns.js";import{getFullAnalysis as y}from"../analysis/full-analysis.js";import{compressFile as h}from"../compact/compress.js";import{getProjectDocs as f,generateContextFiles as j}from"../compact/doc-dialect.js";import{getGraph as A}from"../mcp/tools.js";import{parseProject as P}from"../core/parser.js";import{resolvePath as q}from"../core/workspace.js";import{checkJSDocConsistency as x}from"../analysis/jsdoc-checker.js";import{checkTypes as E}from"../analysis/type-checker.js";import{compactProject as b,expandProject as w}from"../compact/compact.js";import{injectJSDoc as S,stripJSDoc as U,validateCtxContracts as k}from"../compact/ctx-to-jsdoc.js";import{getConfig as C,setConfig as v,getModeDescription as D,getModeWorkflow as I}from"../compact/mode-config.js";import{compactMigrate as M}from"../compact/compact-migrate.js";function getArg(r,e){const s=r.find(r=>r.startsWith(`--${e}=`));return s?s.split("=")[1]:void 0}
3
- function getPath(r){const e=r.find(r=>!r.startsWith("--"))||".";return q(e)}
4
- export const CLI_HANDLERS={config:{rawOutput:!0,handler:async()=>{const{execSync:x}=await import("child_process");const{dirname:dn}=await import("path");let npx,nodePath;try{npx=x("which npx",{encoding:"utf-8"}).trim()}catch{npx="npx"}try{nodePath=x("which node",{encoding:"utf-8"}).trim()}catch{nodePath=""}const env=nodePath?{PATH:`${dn(nodePath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`}:{};const cfg={mcpServers:{"project-graph":{command:npx,args:["-y","project-graph-mcp"],env}}};console.log("Add this to your MCP config:\n");return JSON.stringify(cfg,null,2)}},skeleton:{requiresArg:!0,argError:"Path required: skeleton <path>",handler:async e=>r(q(e[0]))},expand:{requiresArg:!0,argError:"Symbol required: expand <symbol>",handler:async r=>e(r[0])},deps:{requiresArg:!0,argError:"Symbol required: deps <symbol>",handler:async r=>s(r[0])},usages:{requiresArg:!0,argError:"Symbol required: usages <symbol>",handler:async r=>t(r[0])},pending:{handler:async r=>a(getPath(r))},summary:{handler:async r=>n(getPath(r))},filters:{handler:async()=>o()},instructions:{rawOutput:!0,handler:async()=>c()},undocumented:{handler:async r=>{const e=getArg(r,"level")||"tests";return i(getPath(r),e)}},deadcode:{handler:async r=>d(getPath(r))},jsdoc:{requiresArg:!0,argError:"Usage: jsdoc <file>",handler:async r=>l(q(r[0]))},similar:{handler:async r=>{const e=parseInt(getArg(r,"threshold"))||60;return m(getPath(r),{threshold:e})}},complexity:{handler:async r=>{const e=parseInt(getArg(r,"min"))||1,s=r.includes("--problematic");return g(getPath(r),{minComplexity:e,onlyProblematic:s})}},largefiles:{handler:async r=>{const e=r.includes("--problematic");return u(getPath(r),{onlyProblematic:e})}},outdated:{handler:async r=>{const e=r.includes("--code"),s=r.includes("--deps");return p(getPath(r),{codeOnly:e,depsOnly:s})}},analyze:{handler:async r=>{const e=r.includes("--items");return y(getPath(r),{includeItems:e})}},"jsdoc-check":{handler:async r=>x(getPath(r))},types:{handler:async r=>{const e=parseInt(getArg(r,"max"))||50;return E(getPath(r),{maxDiagnostics:e})}},compress:{requiresArg:!0,argError:"Usage: compress <file> [--no-beautify] [--no-legend]",handler:async r=>{const e=!r.includes("--no-beautify"),s=!r.includes("--no-legend");return h(q(r[0]),{beautify:e,legend:s})}},docs:{requiresArg:!0,argError:"Usage: docs <path> [--file=<filename>]",handler:async r=>{const e=q(r[0]),s=await A(e),t=r.find(r=>r.startsWith("--file="))?.split("=")[1];return f(s,e,{file:t})}},"generate-ctx":{requiresArg:!0,argError:"Usage: generate-ctx <path> [--overwrite] [--scope=focus|all]",handler:async r=>{const e=q(r[0]),s=await A(e),t=await P(e),a=r.includes("--overwrite"),n=r.find(r=>r.startsWith("--scope="))?.split("=")[1]||"all";return j(s,e,t,{overwrite:a,scope:n})}},compact:{requiresArg:!0,argError:"Usage: compact <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return b(e,{dryRun:s})}},beautify:{requiresArg:!0,argError:"Usage: beautify <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return w(e,{dryRun:s})}},"inject-jsdoc":{requiresArg:!0,argError:"Usage: inject-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return S(e,{dryRun:s})}},"strip-jsdoc":{requiresArg:!0,argError:"Usage: strip-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return U(e,{dryRun:s})}},"validate-ctx":{requiresArg:!0,argError:"Usage: validate-ctx <path> [--strict]",handler:async r=>{const e=q(r[0]),s=r.includes("--strict");return k(e,{strict:s})}},mode:{requiresArg:!0,argError:"Usage: mode <path>",handler:async r=>{const e=q(r[0]),s=C(e);return{...s,description:D(s.mode),workflow:I(s.mode)}}},"compact-migrate":{requiresArg:!0,argError:"Usage: compact-migrate <path>",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return M(e,{dryRun:s})}},"set-mode":{requiresArg:!0,argError:"Usage: set-mode <path> <1|2>",handler:async r=>{const e=q(r[0]),s=parseInt(r[1],10);if(!s||![1,2].includes(s))throw new Error("Mode must be 1 (compact) or 2 (full)");return v(e,{mode:s})}}};
2
+ import{getSkeleton as r,expand as e,deps as s,usages as a}from"../mcp/tools.js";import{getPendingTests as t,getTestSummary as n}from"../analysis/test-annotations.js";import{getFilters as o}from"../core/filters.js";import{getInstructions as c}from"../compact/instructions.js";import{getUndocumentedSummary as i}from"../analysis/undocumented.js";import{getDeadCode as d}from"../analysis/dead-code.js";import{generateJSDoc as l}from"../analysis/jsdoc-generator.js";import{getSimilarFunctions as m}from"../analysis/similar-functions.js";import{getComplexity as u}from"../analysis/complexity.js";import{getLargeFiles as p}from"../analysis/large-files.js";import{getOutdatedPatterns as g}from"../analysis/outdated-patterns.js";import{getFullAnalysis as y}from"../analysis/full-analysis.js";import{compressFile as h}from"../compact/compress.js";import{getProjectDocs as f,generateContextFiles as j}from"../compact/doc-dialect.js";import{getGraph as b}from"../mcp/tools.js";import{parseProject as x}from"../core/parser.js";import{resolvePath as q}from"../core/workspace.js";import{checkJSDocConsistency as A}from"../analysis/jsdoc-checker.js";import{checkTypes as w}from"../analysis/type-checker.js";import{compactProject as E,expandProject as S}from"../compact/compact.js";import{injectJSDoc as U,stripJSDoc as P,validateCtxContracts as k}from"../compact/ctx-to-jsdoc.js";import{getConfig as v,setConfig as C,getModeDescription as D,getModeWorkflow as I}from"../compact/mode-config.js";import{compactMigrate as F}from"../compact/compact-migrate.js";
3
+ function O(r,e){const s=r.find(r=>r.startsWith(`--${e}=`));return s?s.split("=")[1]:void 0}
4
+ function R(r){const e=r.find(r=>!r.startsWith("--"))||".";return q(e)}
5
+ export const CLI_HANDLERS={config:{rawOutput:!0,handler:async()=>{const{execSync:r}=await import("child_process"),{dirname:e}=await import("path");let s,a;try{s=r("which npx",{encoding:"utf-8"}).trim()}catch{s="npx"}try{a=r("which node",{encoding:"utf-8"}).trim()}catch{a=""}const t={mcpServers:{"project-graph":{command:s,args:["-y","project-graph-mcp"],env:a?{PATH:`${e(a)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`}:{}}}};return console.log("Add this to your MCP config:\n"),JSON.stringify(t,null,2)}},skeleton:{requiresArg:!0,argError:"Path required: skeleton <path>",handler:async e=>r(q(e[0]))},expand:{requiresArg:!0,argError:"Symbol required: expand <symbol>",handler:async r=>e(r[0])},deps:{requiresArg:!0,argError:"Symbol required: deps <symbol>",handler:async r=>s(r[0])},usages:{requiresArg:!0,argError:"Symbol required: usages <symbol>",handler:async r=>a(r[0])},pending:{handler:async r=>t(R(r))},summary:{handler:async r=>n(R(r))},filters:{handler:async()=>o()},instructions:{rawOutput:!0,handler:async()=>c()},undocumented:{handler:async r=>{const e=O(r,"level")||"tests";return i(R(r),e)}},deadcode:{handler:async r=>d(R(r))},jsdoc:{requiresArg:!0,argError:"Usage: jsdoc <file>",handler:async r=>l(q(r[0]))},similar:{handler:async r=>{const e=parseInt(O(r,"threshold"))||60;return m(R(r),{threshold:e})}},complexity:{handler:async r=>{const e=parseInt(O(r,"min"))||1,s=r.includes("--problematic");return u(R(r),{minComplexity:e,onlyProblematic:s})}},largefiles:{handler:async r=>{const e=r.includes("--problematic");return p(R(r),{onlyProblematic:e})}},outdated:{handler:async r=>{const e=r.includes("--code"),s=r.includes("--deps");return g(R(r),{codeOnly:e,depsOnly:s})}},analyze:{handler:async r=>{const e=r.includes("--items");return y(R(r),{includeItems:e})}},"jsdoc-check":{handler:async r=>A(R(r))},types:{handler:async r=>{const e=parseInt(O(r,"max"))||50;return w(R(r),{maxDiagnostics:e})}},compress:{requiresArg:!0,argError:"Usage: compress <file> [--no-beautify] [--no-legend]",handler:async r=>{const e=!r.includes("--no-beautify"),s=!r.includes("--no-legend");return h(q(r[0]),{beautify:e,legend:s})}},docs:{requiresArg:!0,argError:"Usage: docs <path> [--file=<filename>]",handler:async r=>{const e=q(r[0]),s=await b(e),a=r.find(r=>r.startsWith("--file="))?.split("=")[1];return f(s,e,{file:a})}},"generate-ctx":{requiresArg:!0,argError:"Usage: generate-ctx <path> [--overwrite] [--scope=focus|all]",handler:async r=>{const e=q(r[0]),s=await b(e),a=await x(e),t=r.includes("--overwrite"),n=r.find(r=>r.startsWith("--scope="))?.split("=")[1]||"all";return j(s,e,a,{overwrite:t,scope:n})}},compact:{requiresArg:!0,argError:"Usage: compact <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return E(e,{dryRun:s})}},beautify:{requiresArg:!0,argError:"Usage: beautify <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return S(e,{dryRun:s})}},"inject-jsdoc":{requiresArg:!0,argError:"Usage: inject-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return U(e,{dryRun:s})}},"strip-jsdoc":{requiresArg:!0,argError:"Usage: strip-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return P(e,{dryRun:s})}},"validate-ctx":{requiresArg:!0,argError:"Usage: validate-ctx <path> [--strict]",handler:async r=>{const e=q(r[0]),s=r.includes("--strict");return k(e,{strict:s})}},mode:{requiresArg:!0,argError:"Usage: mode <path>",handler:async r=>{const e=q(r[0]),s=v(e);return{...s,description:D(s.mode),workflow:I(s.mode)}}},"compact-migrate":{requiresArg:!0,argError:"Usage: compact-migrate <path>",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return F(e,{dryRun:s})}},"set-mode":{requiresArg:!0,argError:"Usage: set-mode <path> <1|2>",handler:async r=>{const e=q(r[0]),s=parseInt(r[1],10);if(!s||![1,2].includes(s))throw new Error("Mode must be 1 (compact) or 2 (full)");return C(e,{mode:s})}}};
@@ -1,6 +1,6 @@
1
1
  // @ctx .context/src/compact/ai-context.ctx
2
- import{resolve as e,extname as t}from"path";import{getSkeleton as s,getGraph as o}from"../mcp/tools.js";import{getProjectDocs as n}from"./doc-dialect.js";import{compressFile as i}from"./compress.js";import{findJSFiles as r}from"../core/parser.js";
3
- const c=new Set([".js",".mjs",".ts",".tsx"]);function estimateTokens(e){const t="string"==typeof e?e:JSON.stringify(e);return Math.ceil(t.length/4)}
2
+ import{estimateTokens}from"../core/utils.js";import{resolve as e,extname as t}from"path";import{getSkeleton as s,getGraph as o}from"../mcp/tools.js";import{getProjectDocs as n}from"./doc-dialect.js";import{compressFile as i}from"./compress.js";import{findJSFiles as r}from"../core/parser.js";
3
+ const c=new Set([".js",".mjs",".ts",".tsx"]);
4
4
  export async function getAiContext(a,l={}){const{includeFiles:f=[],includeDocs:m=!0,includeSkeleton:d=!0}=l,p=e(a),u={};
5
5
  let g=0;if(d&&(u.skeleton=await s(p),g+=estimateTokens(u.skeleton)),m){const e=await o(p);u.docs=n(e,p),g+=estimateTokens(u.docs)}if(f.length>0){u.files={};
6
6
  const e=r(p);for(const s of f){const o=e.find(e=>e.endsWith(s)||e.endsWith("/"+s));if(!o){u.files[s]={error:`File not found: ${s}`};continue}const n=t(o).toLowerCase();if(c.has(n))try{const e=await i(o,{beautify:!0,legend:!0});u.files[s]=e.code,g+=e.compressed}catch(e){u.files[s]={error:e.message}}else u.files[s]={error:`Unsupported file type: ${n}`}}}const h=r(p);
@@ -1,17 +1,9 @@
1
1
  // @ctx .context/src/compact/compact-migrate.ctx
2
- import{readFileSync as R,writeFileSync as W,readdirSync as n,statSync as o,existsSync as s}from"fs";import{join as r,extname as c,relative as a,basename as i,dirname as l}from"path";import{execSync as u}from"child_process";import{compactProject as d}from"./compact.js";import{validatePipeline as m}from"./validate-pipeline.js";import{setConfig as f}from"./mode-config.js";
3
- const g=new Set([".js",".mjs"]),h=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded"]);
4
- function walkJS(e,t=e){const s=[];try{for(const a of n(e)){if(a.startsWith(".")&&"."!==a)continue;
5
- const n=r(e,a);o(n).isDirectory()?h.has(a)||s.push(...walkJS(n,t)):g.has(c(a).toLowerCase())&&s.push(n)}}catch{}return s}
6
- function checkGitClean(e){try{const t=u("git status --porcelain",{cwd:e,encoding:"utf-8"}).trim();if(t)throw new Error("Working directory is not clean. Commit or stash changes first.\n\nDirty files:\n"+t)}catch(e){if(e.message.includes("not clean"))throw e;throw new Error("Not a git repository or git not available: "+e.message)}}
7
- async function extractNames(e){const t=R(e,"utf-8"),n=new Set;try{const{parse:o}=await import("../../vendor/acorn.mjs"),{simple:s}=await import("../../vendor/walk.mjs"),r=o(t,{ecmaVersion:"latest",sourceType:"module"});s(r,{FunctionDeclaration(e){e.id?.name&&n.add(e.id.name)},ClassDeclaration(e){e.id?.name&&n.add(e.id.name)},VariableDeclarator(e){e.id?.name&&n.add(e.id.name)},ImportSpecifier(e){e.local?.name&&n.add(e.local.name)},ImportDefaultSpecifier(e){e.local?.name&&n.add(e.local.name)},AssignmentExpression(e){"Identifier"===e.left?.type&&n.add(e.left.name)}})}catch{const o=/(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))|(?:const|let|var)\s+(\w+))/g;let s;while(s=o.exec(t)){const e=s[1]||s[2]||s[3];e&&e.length>1&&n.add(e)}}return[...n].filter(e=>e.length>1)}
8
- function buildNamesDirective(e,t){const n=t.split("\n"),o=new Set;const s=/(?:^|[^a-zA-Z_$])([a-z])(?:\s*[=,)}\];:]|$)/g;for(const e of n){let t;while(t=s.exec(e)){o.add(t[1])}}const r=new Map;for(const t of e){const e=t.charAt(0).toLowerCase();if(o.has(e)&&!r.has(e)){r.set(e,t)}}if(0===r.size)return null;return"@names "+[...r.entries()].map(([e,t])=>`${e}=${t}`).join(",")}
9
- function updateCtxNames(e,t,n){const o=a(n,e),s=i(o,c(o))+".ctx",u=l(o),d=r(n,".context",u,s);if(!S(d))return;try{let e=R(d,"utf-8");if(e.includes("@names")){e=e.replace(/@names .*/,t)}else{const n=e.indexOf("\n");e=-1===n?t+"\n"+e:e.slice(0,n+1)+t+"\n"+e.slice(n+1)}W(d,e,"utf-8")}catch{}}
10
- function S(e){return s(e)}
11
- export async function compactMigrate(e,t={}){const{dryRun:n=!1}=t;checkGitClean(e);
12
- const o=walkJS(e),p=[];for(const t of o){const n=a(e,t),o=await extractNames(t),s=R(t,"utf-8");p.push({file:n,namesCount:o.length,names:o,originalSize:s.length})}
13
- if(n)return{dryRun:!0,files:p.length,fileSummary:p.map(e=>({file:e.file,identifiers:e.namesCount,originalSize:e.originalSize}))};
14
- const v=await d(e,{dryRun:!1});for(const t of o){const n=R(t,"utf-8"),o=p.find(e=>t.endsWith(e.file));if(o&&o.names.length>0){const s=buildNamesDirective(o.names,n);s&&updateCtxNames(t,s,e)}}
15
- let b;try{b=await m(e,{strict:!1})}catch(e){b={status:"SKIP",reason:e.message}}
16
- f(e,{mode:1});
17
- return{migrated:!0,files:v.files,savings:v.savings,originalBytes:v.originalBytes,compactedBytes:v.compactedBytes,validation:b?.status||"SKIP",mode:"compact (1)",hint:"Run 'expand_project' to generate .expanded/ cache for human review"}}
2
+ import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as e,writeFileSync as t,existsSync as s}from"fs";import{join as o,extname as i,relative as r,basename as c,dirname as l}from"path";import{execSync as m}from"child_process";import{compactProject as f}from"./compact.js";import{validatePipeline as d}from"./validate-pipeline.js";import{setConfig as u}from"./mode-config.js";
3
+ const p=new Set([".js",".mjs"]),g=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded"]);
4
+
5
+ async function y(t){const n=e(t,"utf-8"),a=new Set;try{const{parse:e}=await import("../../vendor/acorn.mjs"),{simple:t}=await import("../../vendor/walk.mjs");t(e(n,{ecmaVersion:"latest",sourceType:"module"}),{FunctionDeclaration(e){e.id?.name&&a.add(e.id.name)},ClassDeclaration(e){e.id?.name&&a.add(e.id.name)},VariableDeclarator(e){e.id?.name&&a.add(e.id.name)},ImportSpecifier(e){e.local?.name&&a.add(e.local.name)},ImportDefaultSpecifier(e){e.local?.name&&a.add(e.local.name)},AssignmentExpression(e){"Identifier"===e.left?.type&&a.add(e.left.name)}})}catch{const e=/(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))|(?:const|let|var)\s+(\w+))/g;let t;for(;t=e.exec(n);){const e=t[1]||t[2]||t[3];e&&e.length>1&&a.add(e)}}return[...a].filter(e=>e.length>1)}
6
+ function w(e,t){const n=t.split("\n"),a=new Set,s=/(?:^|[^a-zA-Z_$])([a-z])(?:\s*[=,)}\];:]|$)/g;for(const e of n){let t;for(;t=s.exec(e);)a.add(t[1])}const o=new Map;for(const t of e){const e=t.charAt(0).toLowerCase();a.has(e)&&!o.has(e)&&o.set(e,t)}return 0===o.size?null:"@names "+[...o.entries()].map(([e,t])=>`${e}=${t}`).join(",")}
7
+ function S(n,a,s){const m=r(s,n),f=c(m,i(m))+".ctx",d=l(m),u=o(s,".context",d,f);if(x(u))try{let n=e(u,"utf-8");if(n.includes("@names"))n=n.replace(/@names .*/,a);else{const e=n.indexOf("\n");n=-1===e?a+"\n"+n:n.slice(0,e+1)+a+"\n"+n.slice(e+1)}t(u,n,"utf-8")}catch{}}
8
+ function x(e){return s(e)}
9
+ export async function compactMigrate(t,n={}){const{dryRun:a=!1}=n;!function(e){try{const t=m("git status --porcelain",{cwd:e,encoding:"utf-8"}).trim();if(t)throw new Error("Working directory is not clean. Commit or stash changes first.\n\nDirty files:\n"+t)}catch(e){if(e.message.includes("not clean"))throw e;throw new Error("Not a git repository or git not available: "+e.message)}}(t);const s=h(t),o=[];for(const n of s){const a=r(t,n),s=await y(n),i=e(n,"utf-8");o.push({file:a,namesCount:s.length,names:s,originalSize:i.length})}if(a)return{dryRun:!0,files:o.length,fileSummary:o.map(e=>({file:e.file,identifiers:e.namesCount,originalSize:e.originalSize}))};const i=await f(t,{dryRun:!1});for(const n of s){const a=e(n,"utf-8"),s=o.find(e=>n.endsWith(e.file));if(s&&s.names.length>0){const e=w(s.names,a);e&&S(n,e,t)}}let c;try{c=await d(t,{strict:!1})}catch(t){c={status:"SKIP",reason:t.message}}return u(t,{mode:1}),{migrated:!0,files:i.files,savings:i.savings,originalBytes:i.originalBytes,compactedBytes:i.compactedBytes,validation:c?.status||"SKIP",mode:"compact (1)",hint:"Run 'expand_project' to generate .expanded/ cache for human review"}}