project-graph-mcp 1.5.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
@@ -0,0 +1,36 @@
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}}}
@@ -0,0 +1,9 @@
1
+ // @ctx .context/src/analysis/db-analysis.ctx
2
+ import{parseProject as e}from"../core/parser.js";import{buildGraph as t}from"../core/graph-builder.js";
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}
@@ -0,0 +1,19 @@
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)}}
@@ -0,0 +1,18 @@
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."}}
@@ -0,0 +1,24 @@
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}}}
@@ -0,0 +1,10 @@
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}
@@ -0,0 +1,11 @@
1
+ // @ctx .context/src/analysis/large-files.ctx
2
+ import{readFileSync as s,readdirSync as e,statSync as t}from"fs";import{join as n,relative as i,resolve as r}from"path";import{parse as o}from"../../vendor/acorn.mjs";import*as l from"../../vendor/walk.mjs";import{shouldExcludeDir as a,shouldExcludeFile as c,parseGitignore as u}from"../core/filters.js";function findJSFiles(s,r=s){s===r&&u(r);
3
+ const o=[];try{for(const l of e(s)){const e=n(s,l),u=i(r,e);t(e).isDirectory()?a(l,u)||o.push(...findJSFiles(e,r)):!l.endsWith(".js")||l.endsWith(".css.js")||l.endsWith(".tpl.js")||c(l,u)||o.push(e)}}catch(s){}return o}
4
+ function analyzeFile(e,t){const n=s(e,"utf-8"),r=i(t,e),a=n.split("\n").length;
5
+ let c,u=0,p=0,f=0;try{c=o(n,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(s){return{file:r,lines:a,functions:0,classes:0,exports:0,rating:"ok",reasons:[]}}l.simple(c,{FunctionDeclaration(){u++},ArrowFunctionExpression(s){"BlockStatement"===s.body.type&&u++},ClassDeclaration(){p++},ExportNamedDeclaration(){f++},ExportDefaultDeclaration(){f++}});
6
+ const h=[];
7
+ let d=0;a>500?(d+=2,h.push(`${a} lines (>500)`)):a>300&&(d+=1,h.push(`${a} lines (>300)`)),u>15?(d+=2,h.push(`${u} functions (>15)`)):u>10&&(d+=1,h.push(`${u} functions (>10)`)),p>3?(d+=2,h.push(`${p} classes (>3)`)):p>1&&(d+=1,h.push(`${p} classes (>1)`)),f>10?(d+=2,h.push(`${f} exports (>10)`)):f>5&&(d+=1,h.push(`${f} exports (>5)`));
8
+ let g="ok";return d>=4?g="critical":d>=2&&(g="warning"),{file:r,lines:a,functions:u,classes:p,exports:f,rating:g,reasons:h}}
9
+ export async function getLargeFiles(s,e={}){const t=e.onlyProblematic||!1,n=r(s),i=findJSFiles(s);
10
+ let o=i.map(s=>analyzeFile(s,n));t&&(o=o.filter(s=>"ok"!==s.rating)),o.sort((s,e)=>e.lines-s.lines);
11
+ const l={totalFiles:i.length,ok:o.filter(s=>"ok"===s.rating).length,warning:o.filter(s=>"warning"===s.rating).length,critical:o.filter(s=>"critical"===s.rating).length,totalLines:o.reduce((s,e)=>s+e.lines,0),avgLines:o.length>0?Math.round(o.reduce((s,e)=>s+e.lines,0)/o.length):0};return{total:o.length,stats:l,items:o.slice(0,30)}}
@@ -0,0 +1,12 @@
1
+ // @ctx .context/src/analysis/outdated-patterns.ctx
2
+ import{readFileSync as e,readdirSync as r,statSync as t,existsSync as n}from"fs";import{join as s,relative as o,resolve as i}from"path";import{parse as c}from"../../vendor/acorn.mjs";import*as a from"../../vendor/walk.mjs";import{shouldExcludeDir as l,shouldExcludeFile as p,parseGitignore as d}from"../core/filters.js";
3
+ const f={"node-fetch":{replacement:"fetch()",since:"Node 18"},"cross-fetch":{replacement:"fetch()",since:"Node 18"},"isomorphic-fetch":{replacement:"fetch()",since:"Node 18"},uuid:{replacement:"crypto.randomUUID()",since:"Node 19"},"deep-clone":{replacement:"structuredClone()",since:"Node 17"},"lodash.clonedeep":{replacement:"structuredClone()",since:"Node 17"},"abort-controller":{replacement:"AbortController (global)",since:"Node 15"},"form-data":{replacement:"FormData (global)",since:"Node 18"},"web-streams-polyfill":{replacement:"ReadableStream (global)",since:"Node 18"},"url-parse":{replacement:"URL (global)",since:"Node 10"},querystring:{replacement:"URLSearchParams",since:"Node 10"},rimraf:{replacement:"fs.rm({ recursive: true })",since:"Node 14"},mkdirp:{replacement:"fs.mkdir({ recursive: true })",since:"Node 10"},"recursive-readdir":{replacement:"fs.readdir({ recursive: true })",since:"Node 20"},glob:{replacement:"fs.glob()",since:"Node 22"}},m=[{name:"var-usage",description:"Use const/let instead of var",check:e=>"VariableDeclaration"===e.type&&"var"===e.kind,severity:"warning",replacement:"const/let"},{name:"require-usage",description:"Use ESM import instead of require()",check:e=>"CallExpression"===e.type&&"Identifier"===e.callee.type&&"require"===e.callee.name,severity:"info",replacement:"import ... from"},{name:"module-exports",description:"Use ESM export instead of module.exports",check:e=>"AssignmentExpression"===e.type&&"MemberExpression"===e.left.type&&"Identifier"===e.left.object.type&&"module"===e.left.object.name&&"Identifier"===e.left.property.type&&"exports"===e.left.property.name,severity:"info",replacement:"export default/export"},{name:"buffer-constructor",description:"new Buffer() is deprecated",check:e=>"NewExpression"===e.type&&"Identifier"===e.callee.type&&"Buffer"===e.callee.name,severity:"error",replacement:"Buffer.from() / Buffer.alloc()"},{name:"arguments-usage",description:"Use rest parameters instead of arguments",check:e=>"Identifier"===e.type&&"arguments"===e.name,severity:"warning",replacement:"...args"},{name:"promisify-usage",description:"Use fs/promises instead of util.promisify",check:e=>"CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&"util"===e.callee.object.name&&"Identifier"===e.callee.property.type&&"promisify"===e.callee.property.name,severity:"info",replacement:"fs/promises module"},{name:"sync-in-async",description:"Avoid sync methods in async context (readFileSync, etc.)",check:(e,r)=>{if("CallExpression"!==e.type)return!1;
4
+ const t=e.callee;return"MemberExpression"===t.type&&"Identifier"===t.property.type&&t.property.name.endsWith("Sync")&&r.inAsync},severity:"warning",replacement:"async fs/promises methods"}];function findJSFiles(e,n=e){e===n&&d(n);
5
+ const i=[];try{for(const c of r(e)){const r=s(e,c),a=o(n,r);t(r).isDirectory()?l(c,a)||i.push(...findJSFiles(r,n)):!c.endsWith(".js")||c.endsWith(".css.js")||c.endsWith(".tpl.js")||p(c,a)||i.push(r)}}catch(e){}return i}
6
+ function analyzeFilePatterns(r,t){const n=e(r,"utf-8"),s=o(t,r),i=[];
7
+ let l;try{l=c(n,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return i}const p={inAsync:!1};return a.simple(l,{FunctionDeclaration(e){p.inAsync=e.async},ArrowFunctionExpression(e){p.inAsync=e.async}}),p.inAsync=!1,a.ancestor(l,{"*"(e,r){for(const e of r)if(("FunctionDeclaration"===e.type||"ArrowFunctionExpression"===e.type||"FunctionExpression"===e.type)&&e.async){p.inAsync=!0;break}for(const r of m)r.check(e,p)&&i.push({pattern:r.name,description:r.description,file:s,line:e.loc?.start?.line||0,severity:r.severity,replacement:r.replacement})}}),i}
8
+ function analyzePackageJson(r){const t=s(r,"package.json"),o=[];if(!n(t))return o;try{const r=JSON.parse(e(t,"utf-8")),n={...r.dependencies,...r.devDependencies};for(const e of Object.keys(n))f[e]&&o.push({name:e,...f[e]})}catch(e){}return o}
9
+ export async function getOutdatedPatterns(e,r={}){const t=r.codeOnly||!1,n=r.depsOnly||!1,s=i(e);
10
+ let o=[],c=[];if(!n){const r=findJSFiles(e);for(const e of r)o.push(...analyzeFilePatterns(e,s));
11
+ const t={error:0,warning:1,info:2};o.sort((e,r)=>t[e.severity]-t[r.severity])}t||(c=analyzePackageJson(e));
12
+ const a={totalPatterns:o.length,byPattern:{},bySeverity:{error:o.filter(e=>"error"===e.severity).length,warning:o.filter(e=>"warning"===e.severity).length,info:o.filter(e=>"info"===e.severity).length},redundantDeps:c.length};for(const e of o)a.byPattern[e.pattern]=(a.byPattern[e.pattern]||0)+1;return{codePatterns:o.slice(0,50),redundantDeps:c,stats:a}}
@@ -0,0 +1,16 @@
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)}}
@@ -0,0 +1,21 @@
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}}
@@ -0,0 +1,8 @@
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}}
@@ -0,0 +1,14 @@
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)}}
@@ -0,0 +1,4 @@
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");let npx;try{npx=x("which npx",{encoding:"utf-8"}).trim()}catch{npx="npx"}const cfg={mcpServers:{"project-graph":{command:npx,args:["-y","project-graph-mcp"]}}};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})}}};
package/src/cli/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ // @ctx .context/src/cli/cli.ctx
2
+ import{CLI_HANDLERS as e}from"./cli-handlers.js";
3
+ export function printHelp(){console.log("\nproject-graph-mcp - MCP server for AI agents\n\nUsage:\n npx project-graph-mcp Start MCP stdio server\n npx project-graph-mcp <command> [args] Run CLI command\n\nCommands:\n config Generate MCP config with correct paths\n skeleton <path> Get compact project overview\n expand <symbol> Expand minified symbol (e.g., SN, SN.togglePin)\n deps <symbol> Get dependency tree\n usages <symbol> Find all usages\n pending <path> List pending .ctx.md test checklists\n summary <path> Get test progress summary\n undocumented <path> Find missing JSDoc (--level=tests|params|all)\n deadcode <path> Find unused functions/classes\n jsdoc <file> Generate JSDoc for file\n similar <path> Find similar functions (--threshold=60)\n complexity <path> Analyze cyclomatic complexity (--min=1)\n largefiles <path> Find files needing split (--problematic)\n outdated <path> Find legacy patterns & redundant deps\n analyze <path> Run ALL checks with Health Score\n jsdoc-check <path> Validate JSDoc vs function signatures\n types <path> Run tsc type checking (--max=50)\n compress <file> Compress JS file for AI (--no-beautify, --no-legend)\n compact <path> Compact all JS files (--dry-run)\n beautify <path> Beautify/expand all JS files (--dry-run)\n inject-jsdoc <path> Generate JSDoc from .ctx files and inject into source\n strip-jsdoc <path> Strip all JSDoc blocks from source files\n docs <path> Get project docs in doc-dialect format (--file=<name>)\n generate-ctx <path> Generate .context/ docs (--overwrite --scope=focus)\n validate-ctx <path> Validate .ctx contracts against source AST (--strict)\n mode <path> Show current compact code mode and workflow\n compact-migrate <path> Migrate formatted source to compact mode (git must be clean)\n set-mode <path> <1|2> Set mode (1=compact*, 2=full)\n serve <path> Start web dashboard (--port=N)\n filters Show current filter configuration\n instructions Show agent guidelines (JSDoc, Arch)\n help Show this help\n\nQuick Start:\n npx project-graph-mcp config # Get MCP config for your IDE\n\nWeb Dashboard:\n npx project-graph-mcp serve . # Start at auto-assigned port\n npx project-graph-mcp serve . --port 3000\n # Then open http://localhost:{port}/ or http://project-graph.local/{name}/\n\nExamples:\n npx project-graph-mcp skeleton src/components\n npx project-graph-mcp expand SN\n npx project-graph-mcp compact src/ --dry-run\n")}
4
+ export async function runCLI(n,t){if(!n||"help"===n||"--help"===n||"-h"===n)return void printHelp();
5
+ const o=e[n];o||(console.error(`Unknown command: ${n}`),console.error('Run with "help" for usage information'),process.exit(1)),o.requiresArg&&!t[0]&&(console.error(o.argError||`Argument required for: ${n}`),process.exit(1));try{const e=await o.handler(t);o.rawOutput?console.log(e):console.log(JSON.stringify(e,null,2))}catch(e){console.error("Error:",e.message),process.exit(1)}}
@@ -0,0 +1 @@
1
+ {"version":1,"path":"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact","mtimes":{"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/ai-context.js":1775932068307.1665,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/compact-migrate.js":1776001462202.9746,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/compact.js":1775932562256.5852,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/compress.js":1775958722177.8923,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/ctx-to-jsdoc.js":1775932068348.9448,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/doc-dialect.js":1775932068402.0254,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/expand.js":1775932068432.0977,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/framework-references.js":1775932068436.373,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/instructions.js":1775932068437.6816,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/mode-config.js":1776001388446.0337,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/compact/validate-pipeline.js":1775932068445.2222},"graph":{"v":1,"legend":{"estimateTokens":"eT","getAiContext":"AC","walkJS":"JS","checkGitClean":"GC","extractNames":"eN","buildNamesDirective":"ND","updateCtxNames":"CN","S":"sS","compactMigrate":"cM","walkJSFiles":"JSF","addTopLevelNewlines":"TLN","resolveCtxPath":"CP","compactFile":"cF","beautifyFile":"bF","compactProject":"cP","expandProject":"eP","extractLegend":"eL","compressFile":"cF1","editCompressed":"eC","findSymbolRange":"SR","parseCtxFile":"CF","buildJSDocBlock":"JSD","findCtxFile":"CF1","findExportStart":"ES","injectJSDoc":"JSD1","stripJSDoc":"JSD2","splitTopLevelParams":"TLP","validateCtxContracts":"CC","generateDocDialect":"DD","walkCtxFiles":"CF2","resolveCtxMdPath":"CMP","readContextDocs":"CD","getProjectDocs":"PD","computeSignature":"cS","parseCtxDescriptions":"CD1","checkStaleness":"cS1","buildFileTemplate":"FT","generateContextFiles":"CF3","processFileCtx":"FC","parseCtxSignatures":"CS","parseCtxParams":"CP1","extractReturnType":"RT","sanitizeJSDocText":"JSD3","generateJSDoc":"JSD4","parseCtxVars":"CV","parseCtxNames":"CN1","collectLocals":"cL","collectLocalDecls":"LD","restoreNames":"rN","expandFile":"eF","resolveCtx":"rC","fetchReference":"fR","listAvailable":"lA","getFrameworkReference":"FR","getInstructions":"gI","getConfig":"gC","setConfig":"sC","getModeDescription":"MD","getModeWorkflow":"MW","validatePipeline":"vP"},"reverseLegend":{"eT":"estimateTokens","AC":"getAiContext","JS":"walkJS","GC":"checkGitClean","eN":"extractNames","ND":"buildNamesDirective","CN":"updateCtxNames","sS":"S","cM":"compactMigrate","JSF":"walkJSFiles","TLN":"addTopLevelNewlines","CP":"resolveCtxPath","cF":"compactFile","bF":"beautifyFile","cP":"compactProject","eP":"expandProject","eL":"extractLegend","cF1":"compressFile","eC":"editCompressed","SR":"findSymbolRange","CF":"parseCtxFile","JSD":"buildJSDocBlock","CF1":"findCtxFile","ES":"findExportStart","JSD1":"injectJSDoc","JSD2":"stripJSDoc","TLP":"splitTopLevelParams","CC":"validateCtxContracts","DD":"generateDocDialect","CF2":"walkCtxFiles","CMP":"resolveCtxMdPath","CD":"readContextDocs","PD":"getProjectDocs","cS":"computeSignature","CD1":"parseCtxDescriptions","cS1":"checkStaleness","FT":"buildFileTemplate","CF3":"generateContextFiles","FC":"processFileCtx","CS":"parseCtxSignatures","CP1":"parseCtxParams","RT":"extractReturnType","JSD3":"sanitizeJSDocText","JSD4":"generateJSDoc","CV":"parseCtxVars","CN1":"parseCtxNames","cL":"collectLocals","LD":"collectLocalDecls","rN":"restoreNames","eF":"expandFile","rC":"resolveCtx","fR":"fetchReference","lA":"listAvailable","FR":"getFrameworkReference","gI":"getInstructions","gC":"getConfig","sC":"setConfig","MD":"getModeDescription","MW":"getModeWorkflow","vP":"validatePipeline"},"stats":{"files":11,"classes":0,"functions":67,"tables":0},"nodes":{"eT":{"t":"F","e":false,"f":"validate-pipeline.js"},"AC":{"t":"F","e":true,"f":"ai-context.js"},"JS":{"t":"F","e":false,"f":"compact-migrate.js"},"GC":{"t":"F","e":false,"f":"compact-migrate.js"},"eN":{"t":"F","e":false,"f":"compact-migrate.js"},"ND":{"t":"F","e":false,"f":"compact-migrate.js"},"CN":{"t":"F","e":false,"f":"compact-migrate.js"},"sS":{"t":"F","e":false,"f":"compact-migrate.js"},"cM":{"t":"F","e":true,"f":"compact-migrate.js"},"JSF":{"t":"F","e":false,"f":"validate-pipeline.js"},"TLN":{"t":"F","e":false,"f":"compact.js"},"CP":{"t":"F","e":false,"f":"doc-dialect.js"},"cF":{"t":"F","e":false,"f":"compact.js"},"bF":{"t":"F","e":false,"f":"compact.js"},"cP":{"t":"F","e":true,"f":"compact.js"},"eP":{"t":"F","e":true,"f":"expand.js"},"eL":{"t":"F","e":false,"f":"compress.js"},"cF1":{"t":"F","e":true,"f":"compress.js"},"eC":{"t":"F","e":true,"f":"compress.js"},"SR":{"t":"F","e":false,"f":"compress.js"},"CF":{"t":"F","e":true,"f":"ctx-to-jsdoc.js"},"JSD":{"t":"F","e":false,"f":"ctx-to-jsdoc.js"},"CF1":{"t":"F","e":false,"f":"ctx-to-jsdoc.js"},"ES":{"t":"F","e":false,"f":"ctx-to-jsdoc.js"},"JSD1":{"t":"F","e":true,"f":"ctx-to-jsdoc.js"},"JSD2":{"t":"F","e":true,"f":"ctx-to-jsdoc.js"},"TLP":{"t":"F","e":false,"f":"ctx-to-jsdoc.js"},"CC":{"t":"F","e":true,"f":"ctx-to-jsdoc.js"},"DD":{"t":"F","e":true,"f":"doc-dialect.js"},"CF2":{"t":"F","e":false,"f":"doc-dialect.js"},"CMP":{"t":"F","e":false,"f":"doc-dialect.js"},"CD":{"t":"F","e":true,"f":"doc-dialect.js"},"PD":{"t":"F","e":true,"f":"doc-dialect.js"},"cS":{"t":"F","e":false,"f":"doc-dialect.js"},"CD1":{"t":"F","e":false,"f":"doc-dialect.js"},"cS1":{"t":"F","e":true,"f":"doc-dialect.js"},"FT":{"t":"F","e":false,"f":"doc-dialect.js"},"CF3":{"t":"F","e":true,"f":"doc-dialect.js"},"FC":{"t":"F","e":false,"f":"doc-dialect.js"},"CS":{"t":"F","e":false,"f":"expand.js"},"CP1":{"t":"F","e":false,"f":"expand.js"},"RT":{"t":"F","e":false,"f":"expand.js"},"JSD3":{"t":"F","e":false,"f":"expand.js"},"JSD4":{"t":"F","e":false,"f":"expand.js"},"CV":{"t":"F","e":false,"f":"expand.js"},"CN1":{"t":"F","e":false,"f":"expand.js"},"cL":{"t":"F","e":false,"f":"expand.js"},"LD":{"t":"F","e":false,"f":"expand.js"},"rN":{"t":"F","e":false,"f":"expand.js"},"eF":{"t":"F","e":true,"f":"expand.js"},"rC":{"t":"F","e":false,"f":"expand.js"},"fR":{"t":"F","e":false,"f":"framework-references.js"},"lA":{"t":"F","e":false,"f":"framework-references.js"},"FR":{"t":"F","e":true,"f":"framework-references.js"},"gI":{"t":"F","e":true,"f":"instructions.js"},"gC":{"t":"F","e":true,"f":"mode-config.js"},"sC":{"t":"F","e":true,"f":"mode-config.js"},"MD":{"t":"F","e":true,"f":"mode-config.js"},"MW":{"t":"F","e":true,"f":"mode-config.js"},"vP":{"t":"F","e":true,"f":"validate-pipeline.js"}},"edges":[],"orphans":["estimateTokens","walkJS","checkGitClean","extractNames","buildNamesDirective","updateCtxNames","S","walkJSFiles","addTopLevelNewlines","resolveCtxPath","compactFile","beautifyFile","extractLegend","findSymbolRange","buildJSDocBlock","findCtxFile","findExportStart","splitTopLevelParams","walkCtxFiles","resolveCtxMdPath","computeSignature","parseCtxDescriptions","buildFileTemplate","processFileCtx","parseCtxSignatures","parseCtxParams","extractReturnType","sanitizeJSDocText","generateJSDoc","parseCtxVars","parseCtxNames","collectLocals","collectLocalDecls","restoreNames","resolveCtx","fetchReference","listAvailable"],"duplicates":{},"files":["ai-context.js","compact-migrate.js","compact.js","compress.js","ctx-to-jsdoc.js","doc-dialect.js","expand.js","framework-references.js","instructions.js","mode-config.js","validate-pipeline.js"]}}
@@ -0,0 +1,7 @@
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)}
4
+ export async function getAiContext(a,l={}){const{includeFiles:f=[],includeDocs:m=!0,includeSkeleton:d=!0}=l,p=e(a),u={};
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
+ 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);
7
+ let k=0;for(const e of h)try{const{readFileSync:t}=await import("fs");k+=estimateTokens(t(e,"utf-8"))}catch{}const y=k>0?Math.round(100*(1-g/k)):0;return u.totalTokens=g,u.vsOriginal=k,u.savings=`${y}%`,u}
@@ -0,0 +1,17 @@
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"}}
@@ -0,0 +1,18 @@
1
+ // @ctx .context/src/compact/compact.ctx
2
+ import{readFileSync as e,writeFileSync as t,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{minify as u}from"../../vendor/terser.mjs";
3
+ const d=new Set([".js",".mjs"]),f=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents"]);function walkJSFiles(e,t=e){const s=[];try{for(const a of n(e)){if(a.startsWith(".")&&"."!==a)continue;
4
+ const n=r(e,a);o(n).isDirectory()?f.has(a)||s.push(...walkJSFiles(n,t)):d.has(c(a).toLowerCase())&&s.push(n)}}catch{}return s}
5
+ function addTopLevelNewlines(e){return e.replace(/;(import )/g,";\n$1").replace(/;(export )/g,";\n$1").replace(/\}(export )/g,"}\n$1").replace(/\}(function )/g,"}\n$1").replace(/\}(async function )/g,"}\n$1").replace(/\}(class )/g,"}\n$1").replace(/;(const |let |var )/g,";\n$1")}
6
+ function resolveCtxPath(e,t){const n=a(t,e),o=i(n,c(n))+".ctx",u=l(n),d=r(t,".context",u,o);if(s(d))return".context/"+u+"/"+o;
7
+ const f=r(t,u,o);return s(f)?u+"/"+o:null}
8
+ async function compactFile(n,o){const s=e(n,"utf-8"),r=s.length;if(!s.trim())return{original:0,compacted:0};
9
+ const c=await u(s,{compress:{dead_code:!0,drop_console:!1,passes:1,reduce_funcs:!1,inline:!1},mangle:{keep_fnames:!0,module:!0},module:!0,output:{beautify:!1,comments:!1,semicolons:!0}});if(c.error)throw c.error;
10
+ let a=addTopLevelNewlines(c.code);if(o){const e=resolveCtxPath(n,o);e&&(a.startsWith("#!")?a=a.replace(/^(#![^\n]*\n)/,"$1// @ctx "+e+"\n"):a="// @ctx "+e+"\n"+a)}return t(n,a,"utf-8"),{original:r,compacted:a.length}}
11
+ async function beautifyFile(n){const o=e(n,"utf-8"),s=o.length;if(!o.trim())return{original:0,beautified:0};
12
+ const r=await u(o,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:2,semicolons:!0}});if(r.error)throw r.error;return t(n,r.code+"\n","utf-8"),{original:s,beautified:r.code.length}}
13
+ export async function compactProject(t,n={}){const{dryRun:o=!1}=n,s=walkJSFiles(t);
14
+ let r=0,c=0;
15
+ const i=[],l=[];for(const n of s){const s=a(t,n);try{const a=e(n,"utf-8");if(r+=a.length,o)c+=addTopLevelNewlines((await u(a,{compress:{dead_code:!0,drop_console:!1,passes:1,reduce_funcs:!1,inline:!1},mangle:{keep_fnames:!0,module:!0},module:!0,output:{beautify:!1,comments:!1}})).code||"").length||a.length;else{const{compacted:e}=await compactFile(n,t);c+=e}i.push(s)}catch(e){l.push({file:s,error:e.message})}}const d=r>0?Math.round(100*(1-c/r)):0;return{files:i.length,fileList:i,originalBytes:r,compactedBytes:c,savings:`${d}%`,errors:l.length>0?l:void 0,dryRun:o}}
16
+ export async function expandProject(t,n={}){const{dryRun:o=!1}=n,s=walkJSFiles(t);
17
+ let r=0,c=0;
18
+ const i=[],l=[];for(const n of s){const s=a(t,n);try{const t=e(n,"utf-8");if(r+=t.length,o){const e=await u(t,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:2}});c+=e.code?.length||t.length}else{const{beautified:e}=await beautifyFile(n);c+=e}i.push(s)}catch(e){l.push({file:s,error:e.message})}}return{files:i.length,fileList:i,originalBytes:r,beautifiedBytes:c,errors:l.length>0?l:void 0,dryRun:o}}
@@ -0,0 +1,14 @@
1
+ // @ctx .context/src/compact/compress.ctx
2
+ import{readFileSync as e}from"fs";import{basename as t,extname as n}from"path";import{minify as a}from"../../vendor/terser.mjs";import{parse as s}from"../../vendor/acorn.mjs";import{simple as r}from"../../vendor/walk.mjs";
3
+ const o=new Set([".js",".mjs",".ts",".tsx"]);function estimateTokens(e){return Math.ceil(e.length/4)}
4
+ function extractLegend(e,n){const a=[];a.push(`--- ${t(n)} ---`);try{const t=s(e,{ecmaVersion:2022,sourceType:"module",locations:!0});r(t,{ExportNamedDeclaration(t){const n=t.declaration;if(!n)return;
5
+ let s="";if(t.start>0){const n=Math.max(0,t.start-500),a=e.slice(n,t.start).trimEnd().match(/\/\*\*[\s\S]*?\*\/\s*$/);if(a&&e.slice(n+a.index+a[0].length,t.start).split("\n").length<=3){const e=a[0].replace(/\/\*\*\s*\n?/,"").replace(/\s*\*\//,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"").trim()).filter(e=>e&&!e.startsWith("@")).join(" ").trim();e&&(s=e.length>80?e.slice(0,77)+"...":e)}}if("FunctionDeclaration"===n.type){const e=n.id?.name||"anonymous",t=n.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(","),r=`${n.async?"async ":""}${e}(${t})`;a.push(s?`${r}|${s}`:r)}if("ClassDeclaration"===n.type){const e=n.id?.name||"AnonymousClass",t=n.superClass?` extends ${n.superClass.name||"?"}`:"";a.push(`class ${e}${t}${s?"|"+s:""}`);for(const e of n.body.body)if("MethodDefinition"===e.type&&e.key?.name){const t=e.value.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(",");a.push(` .${e.key.name}(${t})`)}}if("VariableDeclaration"===n.type)for(const e of n.declarations)e.id?.name&&a.push(`${n.kind} ${e.id.name}${s?"|"+s:""}`)}})}catch(e){a.push(`PARSE_ERROR: ${e.message}`)}return a.join("\n")}
6
+ export async function compressFile(t,s={}){const{beautify:r=!0,legend:i=!0}=s,c=n(t).toLowerCase();if(!o.has(c))throw new Error(`Unsupported file type: ${c}. Supported: ${[...o].join(", ")}`);
7
+ const l=e(t,"utf-8"),m=estimateTokens(l);if(!l.trim())return{code:"",legend:"",original:0,compressed:0,savings:"0%"};
8
+ const d={compress:{dead_code:!0,drop_console:!1,passes:2},mangle:!1,module:!0,output:{beautify:r,comments:!1,semicolons:!r}};
9
+ let p;try{const e=await a(l,d);if(e.error)throw e.error;p=e.code}catch(e){p=l.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}// Join consecutive import lines into single line
10
+ p=p.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");const f=i?extractLegend(l,t):"",u=f?`/*\n${f}\n*/\n${p}`:p,y=estimateTokens(u);return{code:u,legend:f,original:m,compressed:y,savings:`${m>0?Math.round(100*(1-y/m)):0}%`}}
11
+ export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,l=e(t,"utf-8");
12
+ let m;try{m=s(l,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){throw new Error(`Failed to parse ${t}: ${e.message}`)}const d=findSymbolRange(m,l,n);if(!d)throw new Error(`Symbol "${n}" not found in ${t}`);
13
+ let p=l.slice(0,d.start)+r+l.slice(d.end);if(i)try{const e=await a(p,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!0,semicolons:!1}});e.code&&(p=e.code)}catch{}try{s(p,{ecmaVersion:"latest",sourceType:"module"})}catch(e){throw new Error(`Edit would create invalid syntax: ${e.message}`)}if(!c){const{writeFileSync:e}=await import("fs");e(t,p,"utf-8")}return{success:!0,file:t,symbol:n,oldRange:{start:d.start,end:d.end},newLength:r.length,...c?{dryRun:!0}:{}}}
14
+ function findSymbolRange(e,t,n){let a=null;return r(e,{FunctionDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"FunctionDeclaration"})},ClassDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"ClassDeclaration"})},VariableDeclaration(e){for(const t of e.declarations)t.id?.name===n&&(a={start:e.start,end:e.end,type:"VariableDeclaration"})},ExportNamedDeclaration(e){if(e.declaration){const t=e.declaration;(t.id?.name||t.declarations?.[0]?.id?.name)===n&&(a={start:e.start,end:e.end,type:"ExportNamedDeclaration"})}},ExportDefaultDeclaration(e){e.declaration?.id?.name===n&&(a={start:e.start,end:e.end,type:"ExportDefaultDeclaration"})}}),a}
@@ -0,0 +1,29 @@
1
+ // @ctx .context/src/compact/ctx-to-jsdoc.ctx
2
+ import{readFileSync as t,writeFileSync as e,readdirSync as n,statSync as s,existsSync as o}from"fs";import{join as r,extname as i,relative as a}from"path";import{parse as c}from"../../vendor/acorn.mjs";import{simple as l}from"../../vendor/walk.mjs";
3
+ const p=new Set([".js",".mjs"]),f=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents"]);
4
+ export function parseCtxFile(t){const e=t.split("\n"),n={file:null,functions:[]};for(const t of e){const e=t.match(/^--- (.+) ---$/);if(e){n.file=e[1];continue}const s=t.match(/^(export\s+)?(\w+)\(([^)]*)\)((?:→[^→|]+)*)(?:\|(.*))?$/);if(s){const[,t,e,o,r,i]=s;
5
+ let a="";if(r){const t=r.split("→").filter(Boolean);t.length>0&&/^[A-Z]|^Promise|^Array|^Object|^string|^number|^boolean|^void|^null/.test(t[0])&&(a=t[0])}const c=i&&"{DESCRIBE}"!==i?i.trim():"";n.functions.push({name:e,params:o||"",exported:!!t,description:c,returns:a});continue}t.match(/^class\s+(\w+)/)}return n}
6
+ function buildJSDocBlock(t){const e=["/**"];if(t.description?e.push(` * ${t.description}`):e.push(` * ${t.name}`),t.params){const n=t.params.split(",").map(t=>t.trim()).filter(Boolean);for(const t of n){const n=t.match(/^(\.\.\.)?(\w+)(?::(\w+(?:<[^>]+>)?))?(=)?$/);if(n){const[,t,s,o,r]=n,i=o||"*",a=t||"";r?e.push(` * @param {${i}} [${a}${s}]`):e.push(` * @param {${i}} ${a}${s}`)}}}return t.returns&&e.push(` * @returns {${t.returns}}`),e.push(" */"),e.join("\n")}
7
+ function findCtxFile(t,e){const n=t.replace(/\.[^.]+$/,".ctx"),s=r(e,".context",n);if(o(s))return s;
8
+ const i=r(e,n);return o(i)?i:null}
9
+ export function injectJSDoc(n,s={}){const{dryRun:o=!1}=s,r=n,i=walkJSFiles(n);
10
+ let p=0,f=0;
11
+ const u=[];for(const m of i){const d=a(r,m),h=findCtxFile(d,r);if(!h){f++;continue}const y=parseCtxFile(t(h,"utf-8"));if(0===y.functions.length){f++;continue}let g,x=t(m,"utf-8"),$=!1,S=0;try{g=c(x,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{f++;continue}const v=[];function findExportStart(t){for(const e of g.body)if("ExportNamedDeclaration"===e.type&&e.declaration===t)return e.start;return t.start}l(g,{FunctionDeclaration(t){if(!t.id)return;
12
+ const e=t.id.name,n=y.functions.find(t=>t.name===e);if(!n)return;
13
+ const s=findExportStart(t);if(x.slice(0,s).trimEnd().endsWith("*/"))return;
14
+ const o=buildJSDocBlock(n);v.push({position:s,jsdoc:o}),S++}}),v.sort((t,e)=>e.position-t.position);for(const{position:w,jsdoc:F}of v){const j=x.slice(0,w).lastIndexOf("\n")+1,C=x.slice(j,w).match(/^(\s*)/)?.[1]||"",k=F.split("\n").map(t=>C+t).join("\n")+"\n";x=x.slice(0,w)+k+x.slice(w),$=!0}$&&!o&&e(m,x,"utf-8"),S>0&&(p+=S,u.push({file:d,injected:S}))}return{files:i.length,injected:p,skipped:f,dryRun:o,details:u}}
15
+ export function stripJSDoc(n,s={}){const{dryRun:o=!1}=s,r=walkJSFiles(n);
16
+ let i=0,l=0;
17
+ const p=[];for(const s of r){const r=t(s,"utf-8"),f=[];
18
+ let u,m=!1;try{c(r,{ecmaVersion:"latest",sourceType:"module",onComment:f}),m=!0}catch{}if(m){const t=f.filter(t=>"Block"===t.type&&t.value.startsWith("*")).sort((t,e)=>e.start-t.start);u=r;for(const{start:e,end:n}of t){let t=n;for(;t<u.length&&("\n"===u[t]||"\r"===u[t]);)t++;u=u.slice(0,e)+u.slice(t)}}else u=r.replace(/\/\*\*[\s\S]*?\*\/\s*\n?/g,"");
19
+ const d=u.replace(/\n{3,}/g,"\n\n"),h=r.length-d.length;h>0&&(i++,l+=h,p.push({file:a(n,s),saved:h}),o||e(s,d,"utf-8"))}return{files:r.length,stripped:i,savedBytes:l,dryRun:o,details:p}}
20
+ function walkJSFiles(t){const e=[];try{for(const o of n(t)){if(o.startsWith(".")&&"."!==o)continue;
21
+ const n=r(t,o);s(n).isDirectory()?f.has(o)||e.push(...walkJSFiles(n)):p.has(i(o).toLowerCase())&&e.push(n)}}catch{}return e}
22
+ function splitTopLevelParams(t){const e=[];
23
+ let n=0,s="";for(const o of t)if("{"===o||"<"===o||"("===o?n++:"}"!==o&&">"!==o&&")"!==o||n--,","===o&&0===n){const t=s.trim();t&&e.push(t),s=""}else s+=o;
24
+ const o=s.trim();return o&&e.push(o),e}
25
+ export function validateCtxContracts(e,n={}){const s=n.strict||!1,o=walkJSFiles(e),r=[];
26
+ let i=0;for(const n of o){const o=a(e,n),p=findCtxFile(o,e);if(!p)continue;i++;
27
+ const f=parseCtxFile(t(p,"utf-8"));
28
+ let u,m;try{u=t(n,"utf-8")}catch{continue}try{m=c(u,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{continue}const d=new Map;l(m,{FunctionDeclaration(t){t.id&&d.set(t.id.name,{paramCount:t.params.length,params:t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&t.left?.name?t.left.name:"RestElement"===t.type&&t.argument?.name?t.argument.name:"ObjectPattern"===t.type?"options":"?"),async:t.async||!1,line:t.loc.start.line})}});
29
+ const h=new Set;l(m,{ExportNamedDeclaration(t){if(t.declaration?.id&&h.add(t.declaration.id.name),t.specifiers)for(const e of t.specifiers)h.add(e.exported.name)}});for(const t of f.functions){const e=d.get(t.name);if(!e){r.push({file:o,severity:t.exported?"error":"warning",message:`Function "${t.name}" in .ctx not found in source`});continue}const n=t.params?splitTopLevelParams(t.params):[];n.length!==e.paramCount&&r.push({file:o,severity:"error",message:`"${t.name}": .ctx has ${n.length} params, AST has ${e.paramCount}`});for(let s=0;s<Math.min(n.length,e.params.length);s++){const i=n[s].replace(/^\.\.\./,"").replace(/:.*/,"").replace(/=$/,""),a=e.params[s];i!==a&&"?"!==i&&"?"!==a&&r.push({file:o,severity:"warning",message:`"${t.name}" param ${s}: .ctx="${i}", AST="${a}"`})}const s=h.has(t.name);t.exported!==s&&r.push({file:o,severity:"warning",message:`"${t.name}": .ctx says ${t.exported?"exported":"private"}, AST says ${s?"exported":"private"}`}),d.delete(t.name)}if(s&&d.size>0)for(const[t]of d)r.push({file:o,severity:"info",message:`Function "${t}" in source (line ${d.get(t)?.line}) not documented in .ctx`})}const p=r.filter(t=>"error"===t.severity).length,f=r.filter(t=>"warning"===t.severity).length;return{files:i,violations:r,summary:{errors:p,warnings:f}}}
@@ -0,0 +1,30 @@
1
+ // @ctx .context/src/compact/doc-dialect.ctx
2
+ import{readFileSync as t,readdirSync as e,existsSync as s,mkdirSync as n,writeFileSync as o,statSync as c}from"fs";import{join as i,basename as r,extname as a,dirname as l,relative as h}from"path";import{execSync as u}from"child_process";import{createHash as f}from"crypto";import{writeCache as p,computeContentHash as m}from"../analysis/analysis-cache.js";import{analyzeComplexityFile as d}from"../analysis/complexity.js";import{checkUndocumentedFile as $}from"../analysis/undocumented.js";import{checkJSDocFile as x}from"../analysis/jsdoc-checker.js";
3
+ export function generateDocDialect(t,e){const s=[],n=e?r(e):"unknown";s.push(`=== PROJECT: ${n} ===`);const{stats:o}=t,c=[];o.files>0&&c.push(`${o.files} files`),o.classes>0&&c.push(`${o.classes} classes`),o.functions>0&&c.push(`${o.functions} functions`),o.tables>0&&c.push(`${o.tables} tables`),s.push(`STATS: ${c.join("|")}`);
4
+ const i=t.edges.filter(t=>"→"===t[1]).length,a=t.edges.filter(t=>"R→"===t[1]).length,l=t.edges.filter(t=>"W→"===t[1]).length,h=[];i>0&&h.push(`${i} calls`),a>0&&h.push(`${a} db_reads`),l>0&&h.push(`${l} db_writes`),h.length>0&&s.push(`EDGES: ${h.join("|")}`),t.orphans.length>0&&s.push(`ORPHANS: ${t.orphans.join(",")}`),Object.keys(t.duplicates).length>0&&s.push(`DUPLICATES: ${Object.keys(t.duplicates).join(",")}`);
5
+ const u={};for(const[e,s]of Object.entries(t.nodes)){const t=s.f||"?";u[t]||(u[t]=[]),u[t].push({shortName:e,...s})}for(const[e,n]of Object.entries(u)){if("?"===e)continue;s.push(""),s.push(`--- ${e} ---`);for(const e of n){const n=t.reverseLegend[e.shortName]||e.shortName;if("C"===e.t){const o=e.x?` extends ${e.x}`:"",c=e.m?.length||0,i=e.$?.length||0,r=[];if(c>0&&r.push(`${c}m`),i>0&&r.push(`${i}$`),s.push(`class ${n}${o}|${r.join(",")}`),e.m)for(const n of e.m){const e=t.reverseLegend[n]||n;s.push(` .${e}`)}}else if("F"===e.t){const t=e.e?"export ":"";s.push(`${t}${n}()`)}else if("T"===e.t){const t=e.cols?.join(",")||"";s.push(`TABLE ${n}|${t}`)}}const o=t.edges.filter(s=>{const n=t.nodes[s[0]];return n?.f===e});if(o.length>0){const e=o.map(e=>{const s=t.reverseLegend[e[0]]||e[0],n=t.reverseLegend[e[2]?.split(".")[0]]||e[2];return`${s}${e[1]}${n}`}),n=[...new Set(e)];n.length<=5?s.push(`CALLS: ${n.join("|")}`):s.push(`CALLS: ${n.slice(0,5).join("|")}|+${n.length-5} more`)}}return s.join("\n")}
6
+ function walkCtxFiles(t,n){const o=[];if(!s(t))return o;try{for(const s of e(t)){const e=i(t,s);try{c(e).isDirectory()?o.push(...walkCtxFiles(e,n)):(s.endsWith(".ctx")||s.endsWith(".ctx.md"))&&o.push({relPath:h(n,e),absPath:e})}catch{}}}catch{}return o}
7
+ function resolveCtxPath(t,e){const n=r(e,a(e))+".ctx",o=l(e),c=i(t,o,n);if(s(c))return c;
8
+ const h=i(t,".context",o,n);return s(h)?h:null}
9
+ function resolveCtxMdPath(t,e){const n=r(e,a(e))+".ctx.md",o=l(e),c=i(t,o,n);if(s(c))return c;
10
+ const h=i(t,".context",o,n);return s(h)?h:null}
11
+ export function readContextDocs(e){const s=i(e,".context"),n=new Map;for(const{relPath:e,absPath:o}of walkCtxFiles(s,s))try{const s=t(o,"utf-8").trim();s&&n.set(e,s)}catch{}const o=walkCtxFiles(e,e).filter(t=>!t.relPath.startsWith(".context"));for(const{relPath:e,absPath:s}of o)try{const o=t(s,"utf-8").trim();o&&n.set(e,o)}catch{}const c=[...n.keys()].sort((t,e)=>"project.ctx"===t?-1:"project.ctx"===e?1:r(t).startsWith("_")?-1:r(e).startsWith("_")?1:t.localeCompare(e)),a=c,l=c.map(t=>n.get(t)),h=n.has("project.ctx");return{combined:l.join("\n\n"),files:a,hasProjectCtx:h}}
12
+ export function getProjectDocs(e,s,n={}){const{file:o}=n,c=readContextDocs(s);if(o){const n=resolveCtxPath(s,o);
13
+ let c="";if(n)c=t(n,"utf-8").trim();else{const t=generateDocDialect(e,s),n=`--- ${o} ---`,i=t.indexOf(n);if(-1===i)return`No documentation found for: ${o}`;
14
+ const r=t.indexOf("\n---",i+n.length);c=-1===r?t.slice(i).trim():t.slice(i,r).trim()}const i=resolveCtxMdPath(s,o);if(i){const e=t(i,"utf-8").trim();e&&!e.match(/^#[^\n]*\n+## Notes\n+## TODO\n+## Decisions\s*$/)&&(c+="\n\n"+e)}return c}if(c.combined){const t=generateDocDialect(e,s);return`${c.combined}\n\n${t}`}return generateDocDialect(e,s)}
15
+ function computeSignature(t,e){const s=[];for(const n of(e.functions||[]).filter(e=>e.file===t))s.push(`F:${n.exported?"e":""}:${n.name}(${n.params?.join(",")||""})`);for(const n of(e.classes||[]).filter(e=>e.file===t)){const t=n.methods?.sort().join(",")||"";s.push(`C:${n.name}:${n.extends||""}:${t}`)}return s.sort(),f("md5").update(s.join("|")).digest("hex").slice(0,8)}
16
+ function parseCtxDescriptions(t){const e=new Map;for(const s of t.split("\n")){const t=s.trim();if(!t||t.startsWith("---")||t.startsWith("@sig")||t.startsWith("@enrich")||t.startsWith("Rules:")||t.startsWith("Save this")||t.startsWith("CALLS→")||t.startsWith("R→")||t.startsWith("W→"))continue;
17
+ const n=t.match(/^(PATTERNS|EDGE_CASES):\s*(.+)/);if(n&&"{DESCRIBE}"!==n[2]){e.set(n[1],n[2]);continue}const o=t.match(/^(?:export\s+)?(?:class\s+)?\.?([\w]+)\([^)]*\)(?:[^|]*?)\|(.+)/);if(o&&"{DESCRIBE}"!==o[2]){e.set(o[1],o[2]);continue}const c=t.match(/^class\s+([\w]+)[^|]*\|[^|]*\|(.+)/);c&&"{DESCRIBE}"!==c[2]&&e.set(c[1],c[2])}return e}
18
+ export function checkStaleness(e,s){const n=i(e,".context"),o=[];
19
+ let c=0,r=0;for(const{relPath:e,absPath:i}of walkCtxFiles(n,n))try{const e=t(i,"utf-8"),n=e.match(/@sig\s+(\w+)/),a=e.match(/^--- (.+) ---/m);if(!n||!a){r++;continue}computeSignature(a[1],s)!==n[1]?o.push(a[1]):c++}catch{}for(const{absPath:n}of walkCtxFiles(e,e).filter(t=>!t.relPath.startsWith(".context")))try{const e=t(n,"utf-8"),i=e.match(/@sig\s+(\w+)/),a=e.match(/^--- (.+) ---/m);if(!i||!a){r++;continue}computeSignature(a[1],s)!==i[1]?o.push(a[1]):c++}catch{}return{stale:o,fresh:c,unknown:r}}
20
+ function buildFileTemplate(t,e,s,n,o){const c=[`--- ${t} ---`,`@sig ${computeSignature(t,n)}`],i=o||new Map,r={};for(const e of n.functions||[])e.file===t&&(r[e.name]=e);
21
+ const a={};for(const e of n.classes||[])e.file===t&&(a[e.name]=e);for(const t of e){const e=s.reverseLegend[t.shortName]||t.shortName;if("C"===t.t){const n=a[e]||{},o=t.x?` extends ${t.x}`:"",r=t.m?.length||0,l=t.$?.length||0,h=[];r>0&&h.push(`${r}m`),l>0&&h.push(`${l}$`);
22
+ const u=i.get(e)||"{DESCRIBE}";if(c.push(`class ${e}${o}|${h.join(",")}|${u}`),t.m)for(const e of t.m){const t=s.reverseLegend[e]||e,n=i.get(t)||"{DESCRIBE}";c.push(` .${t}()|${n}`)}n.calls?.length>0&&c.push(` CALLS→${n.calls.slice(0,8).join(",")}`),n.dbReads?.length>0&&c.push(` R→${n.dbReads.join(",")}`),n.dbWrites?.length>0&&c.push(` W→${n.dbWrites.join(",")}`)}else if("F"===t.t){const s=r[e]||{},n=t.e?"export ":"",o=s.params?.length>0?s.params.join(","):"",a=s.returns?`→${s.returns}`:"",l=s.calls?.slice(0,6)||[],h=l.length>0?`→${l.join(",")}`:"",u=i.get(e)||"{DESCRIBE}";c.push(`${n}${e}(${o})${a}${h}|${u}`),s.dbReads?.length>0&&c.push(` R→${s.dbReads.join(",")}`),s.dbWrites?.length>0&&c.push(` W→${s.dbWrites.join(",")}`)}}const l=i.get("PATTERNS")||"{DESCRIBE}",h=i.get("EDGE_CASES")||"{DESCRIBE}";return c.push(`PATTERNS: ${l}`),c.push(`EDGE_CASES: ${h}`),c.join("\n").includes("{DESCRIBE}")&&c.splice(2,0,`@enrich: Replace each {DESCRIBE} below. Read ${t} for context.`," Rules: max 80ch, pipe|separated, abbrev (fn/ret/cfg/init/auth/db/msg)."," Save this file after filling all markers. Remove @enrich lines when done."),c.join("\n")}
23
+ export async function generateContextFiles(t,e,c,a={}){const{overwrite:l=!1,scope:h="all"}=a,f=i(e,".context"),p=[],m=[],d={};s(f)||n(f,{recursive:!0});
24
+ const $=i(f,"project.ctx");if(!s($)||l){const s=r(e),{stats:n}=t,c=[];n.files>0&&c.push(`${n.files} files`),n.classes>0&&c.push(`${n.classes} classes`),n.functions>0&&c.push(`${n.functions} functions`);
25
+ let i=[`=== PROJECT: ${s} ===`,"ARCH: {DESCRIBE}","FLOW: {DESCRIBE}",`STATS: ${c.join("|")}`].join("\n")+"\n";o($,i,"utf-8"),p.push("project.ctx"),d["project.ctx"]=i}else m.push("project.ctx");
26
+ const x={};for(const[e,s]of Object.entries(t.nodes)){const t=s.f;t&&(x[t]||(x[t]=[]),x[t].push({shortName:e,...s}))}let g=null;if("focus"===h)try{const t=u("git diff --name-only HEAD~5",{cwd:e,encoding:"utf-8"});g=new Set(t.split("\n").filter(t=>t.endsWith(".js")||t.endsWith(".mjs")||t.endsWith(".ts")).map(t=>t.trim()).filter(Boolean))}catch{g=null}else Array.isArray(h)&&(g=new Set(h));
27
+ const S=Object.entries(x).filter(([t])=>!g||g.has(t));for(let s=0;s<S.length;s+=5){const n=S.slice(s,s+5),o=await Promise.allSettled(n.map(([s,n])=>processFileCtx(s,n,t,c,f,e,l)));for(const t of o)if("fulfilled"===t.status&&t.value){const{action:e,path:s,template:n}=t.value;"created"===e?(p.push(s),d[s]=n):m.push(s)}}const j={created:p,skipped:m};return p.length>0&&(j.templates=d),j}
28
+ async function processFileCtx(e,c,h,u,f,g,S){const j=r(e,a(e))+".ctx",C=l(e),E=i(f,C),D=i(E,j),y=i(C,j),b=i(g,C,j);if((s(D)||s(b))&&!S)return{action:"skipped",path:y};
29
+ let R;s(E)||n(E,{recursive:!0});
30
+ const W=s(b)?b:s(D)?D:null;if(W&&S)try{R=parseCtxDescriptions(t(W,"utf-8"))}catch{}let w=buildFileTemplate(e,c,h,u,R);o(D,w+"\n","utf-8");try{const n=i(g,e);if(s(n)&&e.endsWith(".js")){const s=t(n,"utf-8"),o=m(s),c=d(s,e),i=$(s,e,"tests"),r=x(s,e);p(f,e,{sig:o,contentHash:o,complexity:c,undocumented:i,jsdocIssues:r})}}catch{}const P=r(e,a(e))+".ctx.md",v=i(E,P);if(!s(v)){const t=`# ${r(e)}\n\n## Notes\n\n## TODO\n\n## Decisions\n`;o(v,t,"utf-8")}return{action:"created",path:y,template:w}}