project-graph-mcp 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/GUIDE.md +237 -0
  2. package/package.json +2 -1
  3. package/rules/test-rules.json +15 -0
  4. package/src/.project-graph-cache.json +1 -1
  5. package/src/analysis/analysis-cache.js +3 -1
  6. package/src/analysis/complexity.js +9 -13
  7. package/src/analysis/custom-rules.js +16 -35
  8. package/src/analysis/db-analysis.js +2 -6
  9. package/src/analysis/dead-code.js +8 -18
  10. package/src/analysis/full-analysis.js +9 -17
  11. package/src/analysis/jsdoc-checker.js +11 -23
  12. package/src/analysis/jsdoc-generator.js +8 -9
  13. package/src/analysis/similar-functions.js +8 -15
  14. package/src/analysis/test-annotations.js +12 -20
  15. package/src/analysis/type-checker.js +5 -7
  16. package/src/analysis/undocumented.js +10 -13
  17. package/src/cli/cli-handlers.js +4 -3
  18. package/src/compact/ai-context.js +2 -2
  19. package/src/compact/compact-migrate.js +8 -16
  20. package/src/compact/compact.js +3 -5
  21. package/src/compact/compress.js +7 -13
  22. package/src/compact/ctx-resolver.js +5 -0
  23. package/src/compact/ctx-to-jsdoc.js +13 -28
  24. package/src/compact/doc-dialect.js +18 -29
  25. package/src/compact/expand.js +10 -36
  26. package/src/compact/jsdoc-builder.js +5 -0
  27. package/src/compact/mode-config.js +6 -6
  28. package/src/compact/split-declarations.js +2 -0
  29. package/src/compact/validate-pipeline.js +7 -8
  30. package/src/core/event-bus.js +2 -1
  31. package/src/core/file-walker.js +4 -0
  32. package/src/core/filters.js +6 -5
  33. package/src/core/graph-builder.js +4 -11
  34. package/src/core/parser.js +19 -29
  35. package/src/core/utils.js +2 -0
  36. package/src/lang/lang-sql.js +7 -20
  37. package/src/mcp/mcp-server.js +2 -3
  38. package/src/mcp/tool-defs.js +1 -1
  39. package/src/mcp/tools.js +13 -21
  40. package/src/network/backend-lifecycle.js +15 -18
  41. package/src/network/local-gateway.js +10 -22
  42. package/src/network/mdns.js +5 -11
  43. package/src/network/server.js +1 -2
  44. package/src/network/web-server.js +7 -33
  45. package/web/app.js +19 -14
  46. package/web/components/code-block.js +1 -0
  47. package/web/components/quick-open.js +1 -0
  48. package/web/dashboard-state.js +1 -0
  49. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  50. package/web/panels/ActionBoard/ActionBoard.js +5 -4
  51. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  52. package/web/panels/EventItem/EventItem.css.js +1 -0
  53. package/web/panels/EventItem/EventItem.js +4 -4
  54. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  55. package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
  56. package/web/panels/ProjectItem/ProjectItem.js +3 -4
  57. package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
  58. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  59. package/web/panels/ProjectList/ProjectList.js +5 -4
  60. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  61. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  62. package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
  63. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  64. package/web/panels/code-viewer.js +1 -0
  65. package/web/panels/ctx-panel.js +1 -0
  66. package/web/panels/dep-graph.js +1 -0
  67. package/web/panels/file-tree.js +4 -188
  68. package/web/panels/health-panel.js +1 -0
  69. package/web/panels/live-monitor.js +1 -0
  70. package/web/state.js +7 -10
@@ -1,10 +1,8 @@
1
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}
2
+ import{resolveCtxRelPath as resolveCtxPath}from"./ctx-resolver.js";import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as e,writeFileSync as t,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
+
5
4
  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}
5
+
8
6
  async function compactFile(n,o){const s=e(n,"utf-8"),r=s.length;if(!s.trim())return{original:0,compacted:0};
9
7
  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
8
  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}}
@@ -1,14 +1,8 @@
1
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}
2
+ import{estimateTokens as i}from"../core/utils.js";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"]);
4
+
5
+ function c(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;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:l=!0}=s,m=n(t).toLowerCase();if(!o.has(m))throw new Error(`Unsupported file type: ${m}. Supported: ${[...o].join(", ")}`);const d=e(t,"utf-8"),p=i(d);if(!d.trim())return{code:"",legend:"",original:0,compressed:0,savings:"0%"};const u={compress:{dead_code:!0,drop_console:!1,passes:2},mangle:!1,module:!0,output:{beautify:r,comments:!1,semicolons:!r}};let f;try{const e=await a(d,u);if(e.error)throw e.error;f=e.code}catch(e){f=d.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}f=f.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");const y=l?c(d,t):"",h=y?`/*\n${y}\n*/\n${f}`:f,$=i(h);return{code:h,legend:y,original:p,compressed:$,savings:`${p>0?Math.round(100*(1-$/p)):0}%`}}
7
+ export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,m=e(t,"utf-8");let d;try{d=s(m,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){throw new Error(`Failed to parse ${t}: ${e.message}`)}const p=l(d,m,n);if(!p)throw new Error(`Symbol "${n}" not found in ${t}`);let u=m.slice(0,p.start)+r+m.slice(p.end);if(i)try{const e=await a(u,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!0,semicolons:!1}});e.code&&(u=e.code)}catch{}try{s(u,{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,u,"utf-8")}return{success:!0,file:t,symbol:n,oldRange:{start:p.start,end:p.end},newLength:r.length,...c?{dryRun:!0}:{}}}
8
+ function l(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,5 @@
1
+ // @ctx .context/src/compact/ctx-resolver.ctx
2
+ import{readFileSync as t,existsSync as s}from"fs";import{join as i,basename as a,extname as c,dirname as p,relative as l}from"path";
3
+ export function resolveCtxPath(e,n){const o=a(n,c(n))+".ctx",r=p(n),d=i(e,".context",r,o);if(s(d))return d;const f=i(e,r,o);return s(f)?f:null}
4
+ export function resolveCtxRelPath(e,n){const o=l(n,e),r=a(o,c(o))+".ctx",u=p(o),d=i(n,".context",u,r);if(s(d))return".context/"+u+"/"+r;const f=i(n,u,r);return s(f)?u+"/"+r:null}
5
+ export function readCtxFile(e,n){const o=resolveCtxPath(e,n);return o?t(o,"utf-8"):null}
@@ -1,29 +1,14 @@
1
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}}}
2
+ import{walkJSFiles}from"../core/file-walker.js";import{resolveCtxPath}from"./ctx-resolver.js";import{buildJSDocFromRaw as u}from"./jsdoc-builder.js";import{readFileSync as t,writeFileSync as e,readdirSync as n,statSync as s,existsSync as o}from"fs";
3
+ import{join as r,extname as i,relative as a}from"path";
4
+ import{parse as c}from"../../vendor/acorn.mjs";
5
+ import{simple as l}from"../../vendor/walk.mjs";
6
+
7
+ 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;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}
8
+
9
+ const m=(t,e)=>resolveCtxPath(e,t);
10
+ export function injectJSDoc(n,s={}){const{dryRun:o=!1}=s,r=n,i=d(n);let p=0,f=0;const h=[];for(const y of i){const g=a(r,y),x=m(g,r);if(!x){f++;continue}const $=parseCtxFile(t(x,"utf-8"));if(0===$.functions.length){f++;continue}let v,w=t(y,"utf-8"),S=!1,j=0;try{v=c(w,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{f++;continue}const C=[];function F(t){for(const e of v.body)if("ExportNamedDeclaration"===e.type&&e.declaration===t)return e.start;return t.start}l(v,{FunctionDeclaration(t){if(!t.id)return;const e=t.id.name,n=$.functions.find(t=>t.name===e);if(!n)return;const s=F(t);if(w.slice(0,s).trimEnd().endsWith("*/"))return;const o=u(n);C.push({position:s,jsdoc:o}),j++}}),C.sort((t,e)=>e.position-t.position);for(const{position:D,jsdoc:A}of C){const E=w.slice(0,D).lastIndexOf("\n")+1,R=w.slice(E,D).match(/^(\s*)/)?.[1]||"",T=A.split("\n").map(t=>R+t).join("\n")+"\n";w=w.slice(0,D)+T+w.slice(D),S=!0}S&&!o&&e(y,w,"utf-8"),j>0&&(p+=j,h.push({file:g,injected:j}))}return{files:i.length,injected:p,skipped:f,dryRun:o,details:h}}
11
+ export function stripJSDoc(n,s={}){const{dryRun:o=!1}=s,r=d(n);let i=0,l=0;const p=[];for(const s of r){const r=t(s,"utf-8"),f=[];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,"");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}}
12
+ const d=walkJSFiles;
13
+ function h(t){const e=[];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;const o=s.trim();return o&&e.push(o),e}
14
+ export function validateCtxContracts(e,n={}){const s=n.strict||!1,o=d(e),r=[];let i=0;for(const n of o){const o=a(e,n),p=m(o,e);if(!p)continue;i++;const f=parseCtxFile(t(p,"utf-8"));let u,d;try{u=t(n,"utf-8")}catch{continue}try{d=c(u,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{continue}const y=new Map;l(d,{FunctionDeclaration(t){t.id&&y.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})}});const g=new Set;l(d,{ExportNamedDeclaration(t){if(t.declaration?.id&&g.add(t.declaration.id.name),t.specifiers)for(const e of t.specifiers)g.add(e.exported.name)}});for(const t of f.functions){const e=y.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?h(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=g.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"}`}),y.delete(t.name)}if(s&&y.size>0)for(const[t]of y)r.push({file:o,severity:"info",message:`Function "${t}" in source (line ${y.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}}}
@@ -1,30 +1,19 @@
1
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}}
2
+ import{readFileSync as t,readdirSync as e,existsSync as s,mkdirSync as n,writeFileSync as o,statSync as c}from"fs";
3
+ import{join as i,basename as r,extname as a,dirname as l,relative as h}from"path";
4
+ import{execSync as f}from"child_process";
5
+ import{createHash as u}from"crypto";
6
+ import{writeCache as p,computeContentHash as m}from"../analysis/analysis-cache.js";
7
+ import{analyzeComplexityFile as d}from"../analysis/complexity.js";
8
+ import{checkUndocumentedFile as $}from"../analysis/undocumented.js";
9
+ import{checkJSDocFile as g}from"../analysis/jsdoc-checker.js";
10
+ 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("|")}`);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(",")}`);const f={};for(const[e,s]of Object.entries(t.nodes)){const t=s.f||"?";f[t]||(f[t]=[]),f[t].push({shortName:e,...s})}for(const[e,n]of Object.entries(f)){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")}
11
+ function x(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(...x(e,n)):(s.endsWith(".ctx")||s.endsWith(".ctx.md"))&&o.push({relPath:h(n,e),absPath:e})}catch{}}}catch{}return o}
12
+ function j(t,e){const n=r(e,a(e))+".ctx",o=l(e),c=i(t,o,n);if(s(c))return c;const h=i(t,".context",o,n);return s(h)?h:null}
13
+ function S(t,e){const n=r(e,a(e))+".ctx.md",o=l(e),c=i(t,o,n);if(s(c))return c;const h=i(t,".context",o,n);return s(h)?h:null}
14
+ export function readContextDocs(e){const s=i(e,".context"),n=new Map;for(const{relPath:e,absPath:o}of x(s,s))try{const s=t(o,"utf-8").trim();s&&n.set(e,s)}catch{}const o=x(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}}
15
+ export function getProjectDocs(e,s,n={}){const{file:o}=n,c=readContextDocs(s);if(o){const n=j(s,o);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}`;const r=t.indexOf("\n---",i+n.length);c=-1===r?t.slice(i).trim():t.slice(i,r).trim()}const i=S(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)}
16
+ function E(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(),u("md5").update(s.join("|")).digest("hex").slice(0,8)}
17
+ export function checkStaleness(e,s){const n=i(e,".context"),o=[];let c=0,r=0;for(const{relPath:e,absPath:i}of x(n,n))try{const e=t(i,"utf-8"),n=e.match(/@sig\s+(\w+)/),a=e.match(/^--- (.+) ---/m);if(!n||!a){r++;continue}E(a[1],s)!==n[1]?o.push(a[1]):c++}catch{}for(const{absPath:n}of x(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}E(a[1],s)!==i[1]?o.push(a[1]):c++}catch{}return{stale:o,fresh:c,unknown:r}}
18
+ export async function generateContextFiles(t,e,c,a={}){const{overwrite:l=!1,scope:h="all"}=a,u=i(e,".context"),p=[],m=[],d={};s(u)||n(u,{recursive:!0});const $=i(u,"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`);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");const g={};for(const[e,s]of Object.entries(t.nodes)){const t=s.f;t&&(g[t]||(g[t]=[]),g[t].push({shortName:e,...s}))}let x=null;if("focus"===h)try{const t=f("git diff --name-only HEAD~5",{cwd:e,encoding:"utf-8"});x=new Set(t.split("\n").filter(t=>t.endsWith(".js")||t.endsWith(".mjs")||t.endsWith(".ts")).map(t=>t.trim()).filter(Boolean))}catch{x=null}else Array.isArray(h)&&(x=new Set(h));const j=Object.entries(g).filter(([t])=>!x||x.has(t));for(let s=0;s<j.length;s+=5){const n=j.slice(s,s+5),o=await Promise.allSettled(n.map(([s,n])=>y(s,n,t,c,u,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 S={created:p,skipped:m};return p.length>0&&(S.templates=d),S}
19
+ async function y(e,c,h,f,u,x,j){const S=r(e,a(e))+".ctx",y=l(e),D=i(u,y),C=i(D,S),b=i(y,S),R=i(x,y,S);if((s(C)||s(R))&&!j)return{action:"skipped",path:b};let W;s(D)||n(D,{recursive:!0});const P=s(R)?R:s(C)?C:null;if(P&&j)try{W=function(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;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}(t(P,"utf-8"))}catch{}let w=function(t,e,s,n,o){const c=[`--- ${t} ---`,`@sig ${E(t,n)}`],i=o||new Map,r={};for(const e of n.functions||[])e.file===t&&(r[e.name]=e);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}$`);const f=i.get(e)||"{DESCRIBE}";if(c.push(`class ${e}${o}|${h.join(",")}|${f}`),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(",")}`:"",f=i.get(e)||"{DESCRIBE}";c.push(`${n}${e}(${o})${a}${h}|${f}`),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")}(e,c,h,f,W);o(C,w+"\n","utf-8");try{const n=i(x,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=g(s,e);p(u,e,{sig:o,contentHash:o,complexity:c,undocumented:i,jsdocIssues:r})}}catch{}const A=r(e,a(e))+".ctx.md",L=i(D,A);if(!s(L)){const t=`# ${r(e)}\n\n## Notes\n\n## TODO\n\n## Decisions\n`;o(L,t,"utf-8")}return{action:"created",path:b,template:w}}
@@ -1,37 +1,11 @@
1
1
  // @ctx .context/src/compact/expand.ctx
2
- import{readFileSync as t,writeFileSync as e,mkdirSync as n,existsSync as s,readdirSync as r,statSync as o}from"fs";import{join as a,basename as i,extname as c,dirname as p,relative as l}from"path";import{minify as m}from"../../vendor/terser.mjs";import{parse as d}from"../../vendor/acorn.mjs";import{simple as f,ancestor as u}from"../../vendor/walk.mjs";function parseCtxSignatures(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;
3
- const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const r=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(r){e.set(r[1],{type:"method",params:parseCtxParams(r[2]),description:r[3]?.trim()||""});continue}const o=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(o){const t=o[2],n=o[3],s=o[4]||"",r=(o[5]||"").split("|"),a=r[0]?.trim()||"";e.set(t,{type:"function",params:parseCtxParams(n),returnType:extractReturnType(s),description:a,exported:!!o[1]});continue}}return e}
4
- function parseCtxParams(t){return t&&t.trim()?t.split(",").map(t=>{const e=t.trim();if(!e)return null;
5
- const n=e.match(/^(\w+)(\?)?(?::(\w[\w<>\[\]|.]*))?(\=)?$/);if(n)return{name:n[1],type:n[3]||null,optional:!(!n[2]&&!n[4])};
6
- const s=e.match(/^(\w+)(=)?$/);return s?{name:s[1],type:null,optional:!!s[2]}:"..."===e?{name:"args",type:null,rest:!0}:{name:e.replace(/[=?:].*/g,""),type:null}}).filter(Boolean):[]}
7
- function extractReturnType(t){if(!t)return null;
8
- const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}
9
- function sanitizeJSDocText(t){return t.replace(/\*\//g,"*\\/")}
10
- function generateJSDoc(t){const e=["/**"];if(t.description&&"{DESCRIBE}"!==t.description&&e.push(` * ${sanitizeJSDocText(t.description)}`),t.params&&t.params.length>0)for(const n of t.params){const t=n.type||"*",s=n.optional?`[${n.name}]`:n.name;e.push(` * @param {${t}} ${s}`)}return t.returnType&&e.push(` * @returns {${t.returnType}}`),e.push(" */"),e.join("\n")}
11
- function parseCtxVars(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}
12
- function parseCtxNames(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;
13
- const s=t.slice(0,n),r=t.slice(n+1),o=new Map;for(const t of r.split(",")){const e=t.trim().split("=");2===e.length&&o.set(e[0].trim(),e[1].trim())}o.size>0&&e.set(s,o)}}}return e}
14
- function restoreNames(t,e,n,s,r){const o=new Map,a=[];for(const t of e.body)if("ImportDeclaration"===t.type)for(const e of t.specifiers)if("ImportSpecifier"===e.type&&e.imported.name!==e.local.name)o.set(e.local.name,e.imported.name),a.push({s:e.start,e:e.end,n:e.imported.name});else if("ImportDefaultSpecifier"===e.type){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(o.set(e.local.name,n),a.push({s:e.start,e:e.end,n:n}))}else if("ImportNamespaceSpecifier"===e.type&&e.local.name.length<=2){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(o.set(e.local.name,n),a.push({s:e.start,e:e.end,n:"* as "+n}))}for(const[t,e]of s)o.set(t,e);if(r.has("__top__"))for(const[t,e]of r.get("__top__"))o.set(t,e);
15
- const i=new Set(o.values());for(const[t]of[...o])i.has(t)&&o.delete(t);
16
- const c=[],p=[],l=[];function collectLocals(t){const e=new Set;return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.add(t.id.name)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.add(t.param.name)}}),e}
17
- function collectLocalDecls(t){const e=[];return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.push(t.id)}}),e}const pf=t=>{const e=t.params.map(t=>"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:null).filter(Boolean),s=new Map;if(t.id?.name){const o=n.get(t.id.name);if(o?.params)for(let t=0;t<Math.min(e.length,o.params.length);t++)e[t]!==o.params[t].name&&s.set(e[t],o.params[t].name);
18
- const a=r.get(t.id.name);if(a)for(const[t,e]of a)s.has(t)||s.set(t,e);if(s.size>0)for(const e of t.params){const t="Identifier"===e.type?e:"AssignmentPattern"===e.type&&"Identifier"===e.left?.type?e.left:null;t&&s.has(t.name)&&p.push({s:t.start,e:t.end,n:s.get(t.name)})}}const o=collectLocals(t.body);for(const t of e)o.add(t);
19
- const a=t.id?.name&&r.get(t.id.name);for(const e of collectLocalDecls(t.body)){const t=a?.get(e.name);t&&l.push({s:e.start,e:e.end,n:t})}c.push({s:t.params.length>0?t.params[0].start:t.body.start,e:t.body.end,p:o,r:s})};f(e,{FunctionDeclaration:pf,FunctionExpression:pf,ArrowFunctionExpression:pf});for(const t of e.body)if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type&&o.has(e.id.name)&&l.push({s:e.id.start,e:e.id.end,n:o.get(e.id.name)});
20
- const m=[...a,...p,...l];u(e,{Identifier(t,e,n){const s=n[n.length-2];if("MemberExpression"===s?.type&&s.property===t&&!s.computed)return;if("Property"===s?.type&&s.key===t&&!s.computed&&s.value!==t)return;if("ExportSpecifier"===s?.type)return;
21
- const r=t.name;
22
- let a=null;for(const e of c)t.start>=e.s&&t.end<=e.e&&(!a||e.e-e.s<a.e-a.s)&&(a=e);a&&a.p.has(r)?a.r.has(r)&&m.push({s:t.start,e:t.end,n:a.r.get(r)}):o.has(r)&&m.push({s:t.start,e:t.end,n:o.get(r)})}}),m.sort((t,e)=>e.s-t.s);
23
- let d=t;for(const t of m)d=d.slice(0,t.s)+t.n+d.slice(t.e);return d}
24
- export async function expandFile(e,n,s={}){const{indentLevel:r=2}=s,o=t(e,"utf-8");if(!o.trim())return{code:"",injected:0,original:0,decompiled:0};
25
- let a;try{a=(await m(o,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:r,semicolons:!0}})).code||o}catch{a=o}{const t=a.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}a=e.join("\n")}a=a.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/r);return"\t".repeat(e)+" ".repeat(t.length%r)});
26
- const i=parseCtxSignatures(n),c=parseCtxVars(n),p=parseCtxNames(n);
27
- let l;try{l=d(a,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:a,injected:0,original:o.length,decompiled:a.length}}try{a=restoreNames(a,l,i,c,p),l=d(a,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{}if(0===i.size)return{code:a,injected:0,original:o.length,decompiled:a.length};
28
- const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=i.get(e.id.name);n&&u.push({pos:t.start,jsdoc:generateJSDoc(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=i.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;
29
- const e=i.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:generateJSDoc(e)})},ClassDeclaration(t){if(!t.id?.name)return;
30
- const e=i.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);
31
- let h=a,y=0;for(const{pos:t,jsdoc:e}of u){const n=h.lastIndexOf("\n",t-1),s=-1===n?0:n+1,r=h.slice(s,t).match(/^(\s*)/)?.[1]||"",o=e.split("\n").map(t=>r+t).join("\n");h=h.slice(0,t)+o+"\n"+h.slice(t),y++}return{code:h,injected:y,original:o.length,decompiled:h.length}}const h=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded","web"]),y=new Set([".js",".mjs"]);function walkJSFiles(t,e=t){const n=[];try{for(const s of r(t)){if(s.startsWith(".")&&"."!==s)continue;
32
- const r=a(t,s);o(r).isDirectory()?h.has(s)||n.push(...walkJSFiles(r,e)):y.has(c(s).toLowerCase())&&n.push(r)}}catch{}return n}
33
- function resolveCtx(e,n){const r=i(n,c(n))+".ctx",o=p(n),l=a(e,o,r);if(s(l))return t(l,"utf-8");
34
- const m=a(e,".context",o,r);return s(m)?t(m,"utf-8"):null}
35
- export async function expandProject(t,r={}){const{dryRun:o=!1,outputDir:i}=r,c=i||a(t,".expanded"),m=a(t,"src");if(!s(m))return{error:"No src/ directory found",files:0};
36
- const d=walkJSFiles(m,t),f=[],u=[];
37
- let h=0;for(const r of d){const i=l(t,r);try{const l=resolveCtx(t,i),m=await expandFile(r,l);if(!o){const t=a(c,i),r=p(t);s(r)||n(r,{recursive:!0}),e(t,m.code,"utf-8")}f.push({file:i,injected:m.injected,original:m.original,decompiled:m.decompiled}),h+=m.injected}catch(t){u.push({file:i,error:t.message})}}return{outputDir:c,files:f.length,totalJSDocInjected:h,fileDetails:f,errors:u.length>0?u:void 0,dryRun:o}}
2
+ import{parseCtxParams as h,buildJSDocBlock as g}from"./jsdoc-builder.js";import{readCtxFile as $}from"./ctx-resolver.js";import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as t,writeFileSync as e,mkdirSync as n,existsSync as s}from"fs";import{join as i,basename as a,extname as c,dirname as p,relative as l}from"path";import{minify as m}from"../../vendor/terser.mjs";import{parse as d}from"../../vendor/acorn.mjs";import{simple as f,ancestor as u}from"../../vendor/walk.mjs";
3
+
4
+ function y(t){if(!t)return null;const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}
5
+
6
+ function w(t,e,n,s,o){const r=new Map,i=[];for(const t of e.body)if("ImportDeclaration"===t.type)for(const e of t.specifiers)if("ImportSpecifier"===e.type&&e.imported.name!==e.local.name)r.set(e.local.name,e.imported.name),i.push({s:e.start,e:e.end,n:e.imported.name});else if("ImportDefaultSpecifier"===e.type){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:n}))}else if("ImportNamespaceSpecifier"===e.type&&e.local.name.length<=2){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:"* as "+n}))}for(const[t,e]of s)r.set(t,e);if(o.has("__top__"))for(const[t,e]of o.get("__top__"))r.set(t,e);const a=new Set(r.values());for(const[t]of[...r])a.has(t)&&r.delete(t);const c=[],p=[],l=[];const m=t=>{const e=t.params.map(t=>"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:null).filter(Boolean),s=new Map;if(t.id?.name){const r=n.get(t.id.name);if(r?.params)for(let t=0;t<Math.min(e.length,r.params.length);t++)e[t]!==r.params[t].name&&s.set(e[t],r.params[t].name);const i=o.get(t.id.name);if(i)for(const[t,e]of i)s.has(t)||s.set(t,e);if(s.size>0)for(const e of t.params){const t="Identifier"===e.type?e:"AssignmentPattern"===e.type&&"Identifier"===e.left?.type?e.left:null;t&&s.has(t.name)&&p.push({s:t.start,e:t.end,n:s.get(t.name)})}}const r=function(t){const e=new Set;return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.add(t.id.name)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.add(t.param.name)}}),e}(t.body);for(const t of e)r.add(t);const i=t.id?.name&&o.get(t.id.name);for(const e of function(t){const e=[];return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.push(t.id)}}),e}(t.body)){const t=i?.get(e.name);t&&l.push({s:e.start,e:e.end,n:t})}c.push({s:t.params.length>0?t.params[0].start:t.body.start,e:t.body.end,p:r,r:s})};f(e,{FunctionDeclaration:m,FunctionExpression:m,ArrowFunctionExpression:m});for(const t of e.body)if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type&&r.has(e.id.name)&&l.push({s:e.id.start,e:e.id.end,n:r.get(e.id.name)});const d=[...i,...p,...l];u(e,{Identifier(t,e,n){const s=n[n.length-2];if("MemberExpression"===s?.type&&s.property===t&&!s.computed)return;if("Property"===s?.type&&s.key===t&&!s.computed&&s.value!==t)return;if("ExportSpecifier"===s?.type)return;const o=t.name;let i=null;for(const e of c)t.start>=e.s&&t.end<=e.e&&(!i||e.e-e.s<i.e-i.s)&&(i=e);i&&i.p.has(o)?i.r.has(o)&&d.push({s:t.start,e:t.end,n:i.r.get(o)}):r.has(o)&&d.push({s:t.start,e:t.end,n:r.get(o)})}}),d.sort((t,e)=>e.s-t.s);let h=t;for(const t of d)h=h.slice(0,t.s)+t.n+h.slice(t.e);return h}
7
+ export async function expandFile(e,n,s={}){const{indentLevel:o=2}=s,r=t(e,"utf-8");if(!r.trim())return{code:"",injected:0,original:0,decompiled:0};let i;try{i=(await m(r,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:o,semicolons:!0}})).code||r}catch{i=r}{const t=i.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}i=e.join("\n")}i=i.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/o);return"\t".repeat(e)+" ".repeat(t.length%o)});const a=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const o=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(o){e.set(o[1],{type:"method",params:h(o[2]),description:o[3]?.trim()||""});continue}const r=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(r){const t=r[2],n=r[3],s=r[4]||"",o=(r[5]||"").split("|"),i=o[0]?.trim()||"";e.set(t,{type:"function",params:h(n),returnType:y(s),description:i,exported:!!r[1]});continue}}return e}(n),c=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}(n),p=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;const s=t.slice(0,n),o=t.slice(n+1),r=new Map;for(const t of o.split(",")){const e=t.trim().split("=");2===e.length&&r.set(e[0].trim(),e[1].trim())}r.size>0&&e.set(s,r)}}}return e}(n);let l;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}try{i=w(i,l,a,c,p),l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{}if(0===a.size)return{code:i,injected:0,original:r.length,decompiled:i.length};const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&u.push({pos:t.start,jsdoc:g(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:g(e)})},ClassDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);let x=i,j=0;for(const{pos:t,jsdoc:e}of u){const n=x.lastIndexOf("\n",t-1),s=-1===n?0:n+1,o=x.slice(s,t).match(/^(\s*)/)?.[1]||"",r=e.split("\n").map(t=>o+t).join("\n");x=x.slice(0,t)+r+"\n"+x.slice(t),j++}return{code:x,injected:j,original:r.length,decompiled:x.length}}
8
+
9
+
10
+
11
+ export async function expandProject(t,o={}){const{dryRun:r=!1,outputDir:a}=o,c=a||i(t,".expanded"),m=i(t,"src");if(!s(m))return{error:"No src/ directory found",files:0};const d=walkJSFiles(m,t),f=[],u=[];let h=0;for(const o of d){const a=l(t,o);try{const l=$(t,a),m=await expandFile(o,l);if(!r){const t=i(c,a),o=p(t);s(o)||n(o,{recursive:!0}),e(t,m.code,"utf-8")}f.push({file:a,injected:m.injected,original:m.original,decompiled:m.decompiled}),h+=m.injected}catch(t){u.push({file:a,error:t.message})}}return{outputDir:c,files:f.length,totalJSDocInjected:h,fileDetails:f,errors:u.length>0?u:void 0,dryRun:r}}
@@ -0,0 +1,5 @@
1
+ // @ctx .context/src/compact/jsdoc-builder.ctx
2
+ export function parseCtxParams(t){return t&&t.trim()?t.split(",").map(t=>{const e=t.trim();if(!e)return null;const n=e.match(/^(\w+)(\?)?(?::(\w[\w<>\[\]|.]*))?(\=)?$/);if(n)return{name:n[1],type:n[3]||null,optional:!(!n[2]&&!n[4])};const s=e.match(/^(\w+)(=)?$/);return s?{name:s[1],type:null,optional:!!s[2]}:"..."===e?{name:"args",type:null,rest:!0}:{name:e.replace(/[=?:].*/g,""),type:null}}).filter(Boolean):[]}
3
+ function sanitize(t){return t.replace(/\*\//g,"*\\/")}
4
+ export function buildJSDocBlock(t){const e=["/**"];if(t.description&&"{DESCRIBE}"!==t.description&&e.push(` * ${sanitize(t.description)}`),t.params&&t.params.length>0&&!t.params.every(p=>!p.type&&p.name.length<=1))for(const n of t.params){const t=n.type||"*",s=n.optional?`[${n.name}]`:n.name;e.push(` * @param {${t}} ${s}`)}return t.returnType&&e.push(` * @returns {${t.returnType}}`),e.push(" */"),e.join("\n")}
5
+ export function buildJSDocFromRaw(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);if(!n.every(t=>{const m=t.match(/^(\.\.\.)?(\w+)(?::(\w))?/);return m&&!m[3]&&m[2].length<=1}))for(const t of n){const n=t.match(/^(\.\.\.)?\s*(\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")}
@@ -1,8 +1,8 @@
1
1
  // @ctx .context/src/compact/mode-config.ctx
2
- import{readFileSync as e,writeFileSync as t,existsSync as d,mkdirSync as o}from"fs";import{join as a,dirname as n}from"path";
3
- const r=".context/config.json",c={mode:2,beautify:!0,autoValidate:!1,stripJSDoc:!1};
4
- export function getConfig(t){const o=a(t,r);if(!d(o))return{...c};try{const t=e(o,"utf-8"),d=JSON.parse(t),s={...c,...d};if(![1,2].includes(s.mode)){s.mode=2}return s}catch{return{...c}}}
5
- export function setConfig(e,c){const i=a(e,r),s=n(i);d(s)||o(s,{recursive:!0});
6
- const f={...getConfig(e),...c};if(![1,2].includes(f.mode))throw new Error(`Invalid mode: ${f.mode}. Valid: 1 (compact), 2 (full)`);return t(i,JSON.stringify(f,null,2)+"\n","utf-8"),{saved:!0,path:i,config:f}}
2
+ import{readFileSync as e,writeFileSync as t,existsSync as r,mkdirSync as o}from"fs";
3
+ import{join as i,dirname as c}from"path";
4
+ const n=".context/config.json",a={mode:2,beautify:!0,autoValidate:!1,stripJSDoc:!1};
5
+ export function getConfig(t){const o=i(t,n);if(!r(o))return{...a};try{const t=e(o,"utf-8"),r=JSON.parse(t),i={...a,...r};return[1,2].includes(i.mode)||(i.mode=2),i}catch{return{...a}}}
6
+ export function setConfig(e,a){const s=i(e,n),d=c(s);r(d)||o(d,{recursive:!0});const f={...getConfig(e),...a};if(![1,2].includes(f.mode))throw new Error(`Invalid mode: ${f.mode}. Valid: 1 (compact), 2 (full)`);return t(s,JSON.stringify(f,null,2)+"\n","utf-8"),{saved:!0,path:s,config:f}}
7
7
  export function getModeDescription(e){switch(e){case 1:return"Compact — minified source + .expanded/ cache for human review (recommended)";case 2:return"Full — formatted source, agents use compressed view for reading";default:return`Unknown mode: ${e}`}}
8
- export function getModeWorkflow(e){switch(e){case 1:return{read:"Read .js files directly (compact source = fewer tokens)",write:"Write .js files directly (compact output = cheaper output tokens)",review:".expanded/ cache with restored names + JSDoc for human review",validate:"Run validate_pipeline → contracts + expand + AST verify",expand:"Run expand_project to regenerate .expanded/ from compact + .ctx",migrate:"Run compact-migrate to convert formatted source → compact"};case 2:return{read:"Use get_compressed_file for token-efficient reading",write:"Use edit_compressed(path, symbol, code) for AST-safe editing",review:"Read source files directly (already formatted)",validate:"Run validate-ctx to check .ctx ↔ AST consistency"};default:return{read:"N/A",write:"N/A",review:"N/A",validate:"N/A"}}}
8
+ export function getModeWorkflow(e){switch(e){case 1:return{read:"Read .js files directly (compact source = fewer tokens)",write:"Write .js files directly (compact output = cheaper output tokens)",edit:"Write .js files directly — terser-style compact code",review:".expanded/ cache with restored names + JSDoc for human review",validate:"Run validate_pipeline → contracts + expand + AST verify",expand:"Run expand_project to regenerate .expanded/ from compact + .ctx",migrate:"Run compact-migrate to convert formatted source → compact"};case 2:return{read:"Use get_compressed_file for token-efficient reading",write:"Use edit_compressed(path, symbol, code) for AST-safe editing",edit:"Use edit_compressed(path, symbol, code) for AST-safe editing",review:"Read source files directly (already formatted)",validate:"Run validate-ctx to check .ctx ↔ AST consistency"};default:return{read:"N/A",write:"N/A",edit:"N/A",review:"N/A",validate:"N/A"}}}
@@ -0,0 +1,2 @@
1
+ // @ctx .context/src/compact/split-declarations.ctx
2
+ import{parse as t}from"../../vendor/acorn.mjs";export function splitDeclarations(r){try{const e=t(r,{ecmaVersion:"latest",sourceType:"module"});if(e.body.length<=3)return r;const n=[];for(const t of e.body)n.push(t.start);n.sort((t,r)=>t-r);const o=[];let s=0;for(const t of n)t>0&&(o.push(r.slice(s,t).trimEnd()),s=t);return o.push(r.slice(s).trimEnd()),o.filter(t=>t).join("\n")}catch{return r}}export function isSingleLineBlob(r){try{if(r.split("\n").filter(t=>!t.startsWith("// @ctx ")&&!t.startsWith("#!")).length>2)return!1;return t(r,{ecmaVersion:"latest",sourceType:"module"}).body.length>3}catch{return!1}}
@@ -1,9 +1,8 @@
1
1
  // @ctx .context/src/compact/validate-pipeline.ctx
2
- import{readFileSync as s,readdirSync as e,statSync as t,existsSync as o}from"fs";import{join as r,extname as a,relative as n}from"path";import{parse as i}from"../../vendor/acorn.mjs";import{validateCtxContracts as c}from"./ctx-to-jsdoc.js";import{expandProject as l}from"./expand.js";
3
- const f=new Set([".js",".mjs"]),d=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded","web"]);function walkJSFiles(s){const o=[];try{for(const n of e(s)){if(n.startsWith(".")&&"."!==n)continue;
4
- const e=r(s,n);t(e).isDirectory()?d.has(n)||o.push(...walkJSFiles(e)):f.has(a(n).toLowerCase())&&o.push(e)}}catch{}return o}
5
- function estimateTokens(s){return Math.ceil(s.length/4)}
6
- export async function validatePipeline(e,t={}){const{strict:a=!1,skipDecompile:f=!1}=t,d=Date.now(),m=c(e,{strict:a});
7
- let u=null;f||(u=await l(e));
8
- const p=r(e,".expanded"),S={passed:0,failed:0,errors:[]};if(o(p)){const e=walkJSFiles(p);for(const t of e){const e=n(p,t);try{const e=s(t,"utf-8");i(e,{ecmaVersion:"latest",sourceType:"module"}),S.passed++}catch(s){S.failed++,S.errors.push({file:e,error:s.message,line:s.loc?.line})}}}const j=r(e,"src"),y={compact:0,full:0,savings:"0%"};if(o(j)&&o(p)){const e=walkJSFiles(j);for(const t of e){const e=n(j,t),a=s(t,"utf-8");y.compact+=estimateTokens(a);
9
- const i=r(p,"src",e);if(o(i)){const e=s(i,"utf-8");y.full+=estimateTokens(e)}else y.full+=estimateTokens(a)}y.full>0&&(y.savings=Math.round(100*(1-y.compact/y.full))+"%")}const w=Date.now()-d,h=m.summary?.errors||0,v=S.failed,g=h+v;return{status:0===g?"PASS":"FAIL",duration:`${w}ms`,contracts:{files:m.files,errors:m.summary?.errors||0,warnings:m.summary?.warnings||0,violations:m.violations?.slice(0,20)},expand:u?{files:u.files,jsdocInjected:u.totalJSDocInjected,errors:u.errors}:null,astVerify:{passed:S.passed,failed:S.failed,errors:S.errors.slice(0,10)},tokens:y,summary:{totalErrors:g,contractErrors:h,astErrors:v,filesProcessed:u?.files||0,jsdocInjected:u?.totalJSDocInjected||0,tokenSavings:y.savings}}}
2
+ import{walkJSFiles as S}from"../core/file-walker.js";import{estimateTokens as W}from"../core/utils.js";import{readFileSync as e,readdirSync as t,statSync as s,existsSync as n,writeFileSync as i,mkdirSync as o}from"fs";import{join as r,extname as a,relative as c,dirname as l,basename as d}from"path";import{execSync as f}from"node:child_process";import{parse as m}from"../../vendor/acorn.mjs";import{validateCtxContracts as p}from"./ctx-to-jsdoc.js";import{expandProject as u}from"./expand.js";import{parseProject as h}from"../core/parser.js";import{getGraph as g}from"../mcp/tools.js";import{generateContextFiles as x}from"./doc-dialect.js";import{simple as y}from"../../vendor/walk.mjs";import{splitDeclarations as j,isSingleLineBlob as w}from"./split-declarations.js";
3
+
4
+
5
+ function D(t,s){const i=[];for(const o of s){const s=c(t,o),f=e(o,"utf-8"),p=f.split("\n");let u=p[0]?.trim();u?.startsWith("#!")&&(u=p[1]?.trim()),u?.startsWith("// @ctx ")||i.push({file:s,rule:"missing-ctx-header",message:"Missing // @ctx header comment"});const h=r(t,".context",c(t,l(o))),g=d(o,a(o))+".ctx",x=r(h,g);if(n(x)){const t=e(x,"utf-8"),n=(t.match(/\{DESCRIBE\}/g)||[]).length,o=(t.match(/\(auto-documented\)/g)||[]).length;o>0&&i.push({file:s,rule:"incomplete-ctx",message:o+" placeholder (auto-documented) description(s) — needs real descriptions"}),n>0&&i.push({file:s,rule:"incomplete-ctx",message:`${n} unfilled {DESCRIBE} marker(s) in .ctx — descriptions required for full restore`});try{const e=m(f,{ecmaVersion:"latest",sourceType:"module"}),n=new Set;for(const t of e.body){if("ExportNamedDeclaration"===t.type){if(t.declaration?.id&&n.add(t.declaration.id.name),t.declaration?.declarations)for(const e of t.declaration.declarations)e.id?.name&&n.add(e.id.name);if(t.specifiers)for(const e of t.specifiers)n.add(e.exported.name)}"ExportDefaultDeclaration"===t.type&&t.declaration?.id&&n.add(t.declaration.id.name)}if(n.size>0){const e=[];for(const s of n)new RegExp("(?:^|[^a-zA-Z_$])"+s+"(?:[^a-zA-Z0-9_$]|$)").test(t)||e.push(s);e.length>0&&i.push({file:s,rule:"incomplete-ctx",message:`${e.length} export(s) not in .ctx: ${e.slice(0,5).join(", ")}${e.length>5?"...":""}`})}}catch{}w(f)&&i.push({file:s,rule:"single-line-blob",message:"Multiple declarations on 1 line — each should be on separate line"});const r=t.match(/^export\s+\w+\([^)]*\)/gm)||[];for(const e of r){const t=e.match(/\(([^)]*)\)/);if(t&&t[1]){const n=t[1].split(",").map(e=>e.trim().replace(/[?=:].*/,"")),o=n.filter(e=>1===e.length);o.length>0&&i.push({file:s,rule:"ctx-minified-params",message:"signature has minified params: "+e.slice(0,60)})}}const a=t.match(/^export\s+\w+\([^)]+\)/gm)||[];for(const e of a){const t=e.match(/\(([^)]*)\)/);if(t&&t[1]){const n=t[1].split(",").map(e=>e.trim()),o=n.filter(e=>e&&!e.includes(":"));o.length>0&&i.push({file:s,rule:"ctx-untyped-params",message:"params without types: "+o.join(", ")+" in "+e.slice(0,50)})}}const c=[];try{if(y(m(f,{ecmaVersion:"latest",sourceType:"module"}),{FunctionDeclaration(e){e.id&&c.push(e.id.name)}}),c.length>0){const e=c.filter(e=>!new RegExp("(?:^|[^a-zA-Z_$])"+e.replace(/\$/g,"\\$")+"(?:[^a-zA-Z0-9_$]|$)").test(t));e.length>0&&i.push({file:s,rule:"ctx-coverage",message:e.length+"/"+c.length+" function(s) not in .ctx: "+e.slice(0,5).join(", ")+(e.length>5?"...":"")})}}catch{}}else i.push({file:s,rule:"missing-ctx-file",message:`No .ctx file at .context/${c(t,l(o))}/${g}`});const j=p.filter(e=>e.trim().startsWith("import "));if(j.length>1){const e=p.findIndex(e=>e.trim().startsWith("import ")),t=p.length-1-[...p].reverse().findIndex(e=>e.trim().startsWith("import "));t>e&&i.push({file:s,rule:"multi-line-imports",message:`${j.length} import statements on separate lines (should be single line in compact mode)`})}const $=[];for(let e=0;e<p.length;e++){const t=p[e];t.length>0&&(" "===t[0]||"\t"===t[0])&&$.push(e+1)}$.length>0&&i.push({file:s,rule:"indented-lines",message:`${$.length} indented line(s) — compact mode requires no indentation (lines: ${$.slice(0,5).join(", ")}${$.length>5?"...":""})`});const v=f.replace(/\/\/ @ctx [^\n]*/,"").replace(/import[^;]+;/g,"").replace(/"(?:[^"\\]|\\.)*"/g,'""').replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`(?:[^`\\]|\\.)*`/g,"``"),S=/(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]{2,})/g,W=new Set;let D;for(;null!==(D=S.exec(v));)W.add(D[1]);const E=new Set;try{const e=m(f,{ecmaVersion:"latest",sourceType:"module"});for(const t of e.body){if("ExportNamedDeclaration"===t.type){if(t.declaration?.id&&E.add(t.declaration.id.name),t.declaration?.declarations)for(const e of t.declaration.declarations)e.id?.name&&E.add(e.id.name);if(t.specifiers)for(const e of t.specifiers)E.add(e.exported.name)}"ExportDefaultDeclaration"===t.type&&t.declaration?.id&&E.add(t.declaration.id.name)}}catch{}for(const e of E)W.delete(e);if(W.size>3){const e=[...W];i.push({file:s,rule:"long-names",message:`${e.length} unminified identifiers (non-export): ${e.slice(0,5).join(", ")}${e.length>5?"...":""}`})}}return i}
6
+ async function E(t,s){const{minify:f}=await import("../vendor/terser.mjs".replace("../","../../"));try{const e=await g(t),s=await h(t);await x(e,t,s,{overwrite:!0})}catch{}let m=0;for(const p of s){const s=c(t,p),u=e(p,"utf-8"),h=u.split("\n");let g=!1,x=u;const y=a(p),$=d(p,y),v=l(p),S=".context/"+c(t,v)+"/"+$+".ctx",W=r(t,S);n(W)||(o(l(W),{recursive:!0}),i(W,"# "+$+"\nContext for "+s+"\n"));let D=h[0]?.trim();const E=D?.startsWith("#!");if(E&&(D=h[1]?.trim()),!D?.startsWith("// @ctx ")){const e="// @ctx "+S;x=E?h[0]+"\n"+e+"\n"+h.slice(1).join("\n"):e+"\n"+x,g=!0}const z=x.split("\n"),A=[],_=[];for(const e of z)e.trim().startsWith("import ")||e.trim().startsWith("import{")?A.push(e.trim()):(A.length>0&&(_.push(A.join("")),A.length=0),_.push(e));A.length>0&&_.push(A.join(""));const b=_.join("\n");b!==x&&(x=b,g=!0);const I=x.split("\n").some(e=>e.length>0&&(" "===e[0]||"\t"===e[0])),C=x.replace(/\/\/ @ctx [^\n]*/,"").replace(/import[^;]+;/g,"").replace(/"(?:[^"\\]|\\.)*"/g,'""').replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`(?:[^`\\]|\\.)*`/g,"``"),Z=/(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]{2,})/g,k=new Set;let F;for(;null!==(F=Z.exec(C));)k.add(F[1]);const P=k.size>3;if(I||P)try{const e=x.split("\n").filter(e=>e.startsWith("// @ctx ")||e.startsWith("#!"))[0]||"",t=await f(x,{compress:!0,mangle:!0,module:!0});t.code&&(x=(e?e+"\n":"")+j(t.code),g=!0)}catch{}if(!g&&w(x)){const e=x.split("\n").filter(e=>e.startsWith("// @ctx ")||e.startsWith("#!")),t=x.split("\n").filter(e=>!e.startsWith("// @ctx ")&&!e.startsWith("#!")).join("\n"),s=j(t);s!==t&&(x=e.join("\n")+"\n"+s,g=!0)}g&&(i(p,x),m++)}return{fixed:m,total:s.length}}
7
+ export async function validatePipeline(t,s={}){const{strict:o=!1,skipDecompile:a=!1,fix:l=!1}=s,d=Date.now(),h=p(t,{strict:o});let g=null;a||(g=await u(t));const x=r(t,".expanded"),y={passed:0,failed:0,errors:[]};if(n(x)){const t=S(x);for(const s of t){const t=c(x,s);try{const t=e(s,"utf-8");m(t,{ecmaVersion:"latest",sourceType:"module"}),y.passed++}catch(e){y.failed++,y.errors.push({file:t,error:e.message,line:e.loc?.line})}}}const j=r(t,"src"),w={compact:0,full:0,savings:"0%"};if(n(j)&&n(x)){const t=S(j);for(const s of t){const t=c(j,s),i=e(s,"utf-8");w.compact+=W(i);const o=r(x,"src",t);if(n(o)){const t=e(o,"utf-8");w.full+=W(t)}else w.full+=W(i)}w.full>0&&(w.savings=Math.round(100*(1-w.compact/w.full))+"%")}const $=function(t){try{const s=f("git ls-files --cached --others --exclude-standard",{cwd:t,encoding:"utf-8"}),o=r(t,".pgignore");let a=[];if(n(o))a=e(o,"utf-8").split("\n").map(e=>e.trim()).filter(e=>e&&!e.startsWith("#"));else{try{i(o,"# Third-party vendored code\nvendor/\n\n# Generated files\n.context/\n.expanded/\n")}catch{}a=["vendor/",".context/",".expanded/"]}return s.split("\n").filter(e=>e&&(e.endsWith(".js")||e.endsWith(".mjs"))&&!a.some(t=>t.endsWith("/")?e.startsWith(t):e===t||e.includes("/"+t))).map(e=>r(t,e))}catch{return[]}}(t);let v=null;l&&(v=await E(t,$));const z=D(t,$),A=Date.now()-d,_=h.summary?.errors||0,b=y.failed,I=z.length,C=_+b+I;return{status:0===C?"PASS":"FAIL",duration:`${A}ms`,contracts:{files:h.files,errors:h.summary?.errors||0,warnings:h.summary?.warnings||0,violations:h.violations?.slice(0,20)},expand:g?{files:g.files,jsdocInjected:g.totalJSDocInjected,errors:g.errors}:null,astVerify:{passed:y.passed,failed:y.failed,errors:y.errors.slice(0,10)},style:{files:$.length,issues:z.length,details:z.slice(0,30)},fix:v,tokens:w,summary:{totalErrors:C,contractErrors:_,astErrors:b,styleErrors:I,filesProcessed:g?.files||0,jsdocInjected:g?.totalJSDocInjected||0,tokenSavings:w.savings}}}
8
+ export{E as fixCompactStyle};
@@ -1,6 +1,7 @@
1
1
  // @ctx .context/src/core/event-bus.ctx
2
2
  import{EventEmitter as o}from"node:events";
3
- const t=new o;t.setMaxListeners(50);
3
+ const t=new o;
4
+ t.setMaxListeners(50);
4
5
  export function emitToolCall(o,e){t.emit("tool:call",{type:"tool_call",tool:o,args:e,ts:Date.now()})}
5
6
  export function emitToolResult(o,e,l,n,s){t.emit("tool:result",{type:"tool_result",tool:o,args:e,duration_ms:n,success:s,result_keys:l?Object.keys(l):[],ts:Date.now()})}
6
7
  export function onToolCall(o){t.on("tool:call",o)}
@@ -0,0 +1,4 @@
1
+ // @ctx .context/src/core/file-walker.ctx
2
+ import{readdirSync as n,statSync as o}from"fs";import{join as r,extname as c}from"path";
3
+ const f=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded","web"]),d=new Set([".js",".mjs"]);
4
+ export function walkJSFiles(e,t=e){const s=[];try{for(const a of n(e)){if(a.startsWith(".")&&"."!==a)continue;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}
@@ -1,5 +1,6 @@
1
1
  // @ctx .context/src/core/filters.ctx
2
- import{readFileSync as e,existsSync as t}from"fs";import{join as r}from"path";
2
+ import{readFileSync as e,existsSync as t}from"fs";
3
+ import{join as r}from"path";
3
4
  const i=["node_modules","dist","build","coverage",".next",".nuxt",".output","__pycache__",".cache",".turbo","out"],n=["*.test.js","*.spec.js","*.min.js","*.bundle.js","*.d.ts",".project-graph-cache.json"];
4
5
  let s={excludeDirs:[...i],excludePatterns:[...n],includeHidden:!1,useGitignore:!0,gitignorePatterns:[]};
5
6
  export function getFilters(){return{...s}}
@@ -8,7 +9,7 @@ export function addExcludes(e){return s.excludeDirs=[...new Set([...s.excludeDir
8
9
  export function removeExcludes(e){return s.excludeDirs=s.excludeDirs.filter(t=>!e.includes(t)),getFilters()}
9
10
  export function resetFilters(){return s={excludeDirs:[...i],excludePatterns:[...n],includeHidden:!1,useGitignore:!0,gitignorePatterns:[]},getFilters()}
10
11
  export function parseGitignore(i){const n=r(i,".gitignore");if(!t(n))return[];try{const t=e(n,"utf-8").split("\n").map(e=>e.trim()).filter(e=>e&&!e.startsWith("#")).map(e=>e.replace(/\/$/,""));return s.gitignorePatterns=t,t}catch(e){return[]}}
11
- export function shouldExcludeDir(e,t=""){if(!s.includeHidden&&e.startsWith("."))return!0;if(s.excludeDirs.includes(e))return!0;if(s.useGitignore)for(const r of s.gitignorePatterns)if(matchGitignorePattern(r,e,t))return!0;return!1}
12
- export function shouldExcludeFile(e,t=""){for(const t of s.excludePatterns)if(matchWildcard(t,e))return!0;if(s.useGitignore)for(const r of s.gitignorePatterns)if(matchGitignorePattern(r,e,t))return!0;return!1}
13
- function matchWildcard(e,t){const r=e.replace(/\./g,"\\.").replace(/\*/g,".*");return new RegExp(`^${r}$`).test(t)}
14
- function matchGitignorePattern(e,t,r){return e===t||(e.includes("*")?matchWildcard(e,t):!!(r?`${r}/${t}`:t).includes(e))}
12
+ export function shouldExcludeDir(e,t=""){if(!s.includeHidden&&e.startsWith("."))return!0;if(s.excludeDirs.includes(e))return!0;if(s.useGitignore)for(const r of s.gitignorePatterns)if(o(r,e,t))return!0;return!1}
13
+ export function shouldExcludeFile(e,t=""){for(const t of s.excludePatterns)if(u(t,e))return!0;if(s.useGitignore)for(const r of s.gitignorePatterns)if(o(r,e,t))return!0;return!1}
14
+ function u(e,t){const r=e.replace(/\./g,"\\.").replace(/\*/g,".*");return new RegExp(`^${r}$`).test(t)}
15
+ function o(e,t,r){return e===t||(e.includes("*")?u(e,t):!!(r?`${r}/${t}`:t).includes(e))}
@@ -1,12 +1,5 @@
1
1
  // @ctx .context/src/core/graph-builder.ctx
2
- export function minifyLegend(e){const s={},t=new Set;for(const o of e){let e=createShortName(o),n=1;for(;t.has(e);)e=createShortName(o)+n,n++;t.add(e),s[o]=e}return s}
3
- function createShortName(e){const s=e.replace(/[a-z]/g,"");if(s.length>=2)return s.slice(0,3);
4
- const t=e.match(/[A-Z]/g);return t&&t.length>0?e[0].toLowerCase()+t[0]:e.slice(0,2)}
5
- export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[]};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};
6
- const r=new Set;for(const e of f.edges){const s=e[2].split(".")[0];r.add(s)}for(const e of Object.keys(f.nodes))r.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);
7
- const l=Object.create(null);for(const e of s)for(const s of e.methods||[])l[s]||(l[s]=[]),l[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(l))s.length>1&&(f.duplicates[e]=s);return f}
8
- export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;
9
- const r={m:e};f>0&&(r.$=f),c.f&&(r.f=c.f),o[n]=r}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;
10
- const e=c.f||"?";n[e]||(n[e]=[]),n[e].push(o)}}const c=new Set;for(const e of Object.values(o))e.f&&c.add(e.f);for(const e of Object.keys(n))c.add(e);
11
- const f={};for(const s of e.files||[]){if(c.has(s))continue;
12
- const e=s.lastIndexOf("/"),t=e>=0?s.slice(0,e+1):"./",o=e>=0?s.slice(e+1):s;f[t]||(f[t]=[]),f[t].push(o)}const r={v:e.v,L:t,s:e.stats,n:o,X:n,e:e.edges.length,o:e.orphans.length,d:Object.keys(e.duplicates).length};if(Object.keys(f).length>0&&(r.f=f),s&&s.length>0){const t=new Set(e.files||[]),o=s.filter(e=>!t.has(e));if(o.length>0){const e={};for(const s of o){const t=s.lastIndexOf("/"),o=t>=0?s.slice(0,t+1):"./",n=t>=0?s.slice(t+1):s;e[o]||(e[o]=[]),e[o].push(n)}r.a=e}}return r}
2
+ export function minifyLegend(s){const t={},o=new Set;for(const n of s){let s=e(n),c=1;for(;o.has(s);)s=e(n)+c,c++;o.add(s),t[n]=s}return t}
3
+ function e(e){const s=e.replace(/[a-z]/g,"");if(s.length>=2)return s.slice(0,3);const t=e.match(/[A-Z]/g);return t&&t.length>0?e[0].toLowerCase()+t[0]:e.slice(0,2)}
4
+ export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[]};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"→",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};const l=new Set;for(const e of f.edges){const s=e[2].split(".")[0];l.add(s)}for(const e of Object.keys(f.nodes))l.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);const i=Object.create(null);for(const e of s)for(const s of e.methods||[])i[s]||(i[s]=[]),i[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(i))s.length>1&&(f.duplicates[e]=s);return f}
5
+ export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;const l={m:e};f>0&&(l.$=f),c.f&&(l.f=c.f),o[n]=l}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;const e=c.f||"?";n[e]||(n[e]=[]),n[e].push(o)}}const c=new Set;for(const e of Object.values(o))e.f&&c.add(e.f);for(const e of Object.keys(n))c.add(e);const f={};for(const s of e.files||[]){if(c.has(s))continue;const e=s.lastIndexOf("/"),t=e>=0?s.slice(0,e+1):"./",o=e>=0?s.slice(e+1):s;f[t]||(f[t]=[]),f[t].push(o)}const l={v:e.v,L:t,s:e.stats,n:o,X:n,e:e.edges.length,o:e.orphans.length,d:Object.keys(e.duplicates).length};if(Object.keys(f).length>0&&(l.f=f),s&&s.length>0){const t=new Set(e.files||[]),o=s.filter(e=>!t.has(e));if(o.length>0){const e={};for(const s of o){const t=s.lastIndexOf("/"),o=t>=0?s.slice(0,t+1):"./",n=t>=0?s.slice(t+1):s;e[o]||(e[o]=[]),e[o].push(n)}l.a=e}}return l}