project-graph-mcp 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -8
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +13 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +16 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
|
@@ -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";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={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)}}},"set-mode":{requiresArg:!0,argError:"Usage: set-mode <path> <1|2|3>",handler:async r=>{const e=q(r[0]),s=parseInt(r[1],10);if(!s||![1,2,3].includes(s))throw new Error("Mode must be 1, 2, or 3");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 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 ↔ 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 — strips comments/whitespace (--dry-run)\n beautify <path> Beautify/expand all JS files — inverse of compact (--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 set-mode <path> <1|2|3> Set compact code mode (1=compact, 2=full, 3=IDE)\n filters Show current filter configuration\n instructions Show agent guidelines (JSDoc, Arch)\n help Show this help\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,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,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,13 @@
|
|
|
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()}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}%`}}
|
|
10
|
+
export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,l=e(t,"utf-8");
|
|
11
|
+
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}`);
|
|
12
|
+
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}:{}}}
|
|
13
|
+
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}}
|
|
@@ -0,0 +1,37 @@
|
|
|
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}}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @ctx .context/src/compact/framework-references.ctx
|
|
2
|
+
import{readFileSync as e,readdirSync as t,existsSync as r,writeFileSync as n}from"fs";import{join as o,basename as a,dirname as s}from"path";import{fileURLToPath as c}from"url";import{detectProjectRuleSets as i}from"../analysis/custom-rules.js";
|
|
3
|
+
const f=s(c(import.meta.url)),m=o(f,"..","..","docs","references"),l={"symbiote-3x":"https://raw.githubusercontent.com/symbiotejs/symbiote.js/main/AI_REFERENCE.md"},u=new Map;async function fetchReference(t){const a=l[t],s=o(m,`${t}.md`),c=u.get(t);if(c&&Date.now()-c.fetchedAt<36e5)return{content:c.content,source:"cache"};if(a)try{const e=await fetch(a,{signal:AbortSignal.timeout(5e3)});if(e.ok){const r=await e.text();u.set(t,{content:r,fetchedAt:Date.now()});try{n(s,r,"utf-8")}catch(e){}return{content:r,source:`github (${a})`}}}catch(e){}if(r(s)){const r=e(s,"utf-8");return u.set(t,{content:r,fetchedAt:Date.now()}),{content:r,source:"local"}}return{content:"",source:"not_found"}}const d={"symbiote-3x":"symbiote-3x","symbiote-2x":"symbiote-3x"};function listAvailable(){const e=new Set(Object.keys(l));if(r(m))for(const r of t(m))r.endsWith(".md")&&e.add(a(r,".md"));return[...e]}
|
|
4
|
+
export async function getFrameworkReference(e={}){const t=listAvailable();if(e.framework){if(!t.includes(e.framework))return{error:`Framework reference '${e.framework}' not found`,available:t};const{content:r,source:n}=await fetchReference(e.framework);return r?{framework:e.framework,source:n,lines:r.split("\n").length,content:r}:{error:`Failed to load reference '${e.framework}'`,available:t}}if(e.path){const{detected:r,reasons:n}=i(e.path),o=[];for(const e of r){const r=d[e];r&&t.includes(r)&&!o.includes(r)&&o.push(r)}if(0===o.length)return{error:"No framework references found for this project",detected:r,reasons:n,available:t};
|
|
5
|
+
const a=await Promise.all(o.map(fetchReference)),s=a.map(e=>e.content).filter(Boolean);return{frameworks:o,sources:a.map(e=>e.source),detected:{rulesets:r,reasons:n},lines:s.reduce((e,t)=>e+t.split("\n").length,0),content:s.join("\n\n---\n\n")}}return{error:"Specify framework name or path for auto-detection",available:t.map(e=>({name:e,remote:!!l[e],url:l[e]??null}))}}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// @ctx .context/src/compact/instructions.ctx
|
|
2
|
+
export const AGENT_INSTRUCTIONS='\n# 🤖 Project Guidelines for AI Agents\n\n## 1. Architecture Standards (Symbiote.js)\n- **Component Structure**: Always use Triple-File Partitioning for components:\n - `MyComponent.js`: Class logic (extends Symbiote)\n - `MyComponent.tpl.js`: HTML template (export template)\n - `MyComponent.css.js`: CSS styles (export rootStyles/shadowStyles)\n- **State Management**: Use `this.init$` for local state and `this.sub()` for reactivity.\n- **Directives**: Use `itemize` for lists, `js-d-kit` for static generation.\n\n## 2. General Coding Rules\n- **ESM Only**: Use `import` / `export`. No `require`.\n- **No Dependencies**: Avoid adding new npm packages unless critical.\n- **Comments**: Write clear JSDoc for all public methods.\n- **Async/Await**: Prefer async/await over promises.\n\n## 3. MCP Tools Usage\n- **Graph**: Use `get_skeleton` first to map the codebase.\n- **Deep Dive**: Use `expand` to read class details.\n- **Tests**: Use `get_pending_tests` to see what needs verification.\n- **Guidelines**: Use `get_agent_instructions` to refresh these rules.\n\n## 4. Custom Rules System\nConfigurable code analysis with auto-detection.\n\n### Available Tools\n- `get_custom_rules`: List all rulesets and their rules\n- `set_custom_rule`: Add or update a rule in a ruleset\n- `check_custom_rules`: Run analysis (auto-detects applicable rulesets)\n\n### Auto-Detection\nRulesets are applied automatically based on:\n1. `package.json` dependencies\n2. Import patterns in source code\n3. Code patterns (e.g., `extends Symbiote`)\n\n### Creating New Rules\nUse `set_custom_rule` to add framework-specific rules:\n```json\n{\n "ruleSet": "my-framework-2x",\n "rule": {\n "id": "my-rule-id",\n "name": "Rule Name",\n "description": "What this rule checks",\n "pattern": "badPattern",\n "patternType": "string",\n "replacement": "Use goodPattern instead",\n "severity": "warning",\n "filePattern": "*.js",\n "docs": "https://docs.example.com/rule"\n }\n}\n```\n\n### Severity Levels\n- `error`: Critical issues that must be fixed\n- `warning`: Important but not blocking\n- `info`: Suggestions and best practices\n';
|
|
3
|
+
export function getInstructions(){return AGENT_INSTRUCTIONS}
|
|
@@ -0,0 +1,8 @@
|
|
|
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);return{...c,...d}}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,3].includes(f.mode))throw new Error(`Invalid mode: ${f.mode}. Valid: 1 (compact), 2 (standard), 3 (hybrid)`);return t(i,JSON.stringify(f,null,2)+"\n","utf-8"),{saved:!0,path:i,config:f}}
|
|
7
|
+
export function getModeDescription(e){switch(e){case 1:return"Compact — code stored minified, agent edits directly";case 2:return"Standard — code stored formatted, agent uses get_compressed_file + edit_compressed";case 3:return"Hybrid — compact source + .expanded/ cache for tooling and human review";default:return`Unknown mode: ${e}`}}
|
|
8
|
+
export function getModeWorkflow(e){switch(e){case 1:return{read:"Read .js files directly (already compact)",edit:"Edit .js files directly",docs:"Read .ctx files for types and descriptions",validate:"Run validate-ctx to check .ctx ↔ AST consistency"};case 2:return{read:"Use get_compressed_file for token-efficient reading",edit:"Use edit_compressed(path, symbol, code) for AST-safe editing",docs:"Read .ctx files for types and descriptions",validate:"Run validate-ctx to check .ctx ↔ AST consistency"};case 3:return{read:"Read .js files directly (compact) for token efficiency",edit:"Use edit_compressed(path, symbol, code) → auto-regenerate .expanded/",docs:"Read .ctx files; JSDoc auto-injected into .expanded/ via expand",validate:"Run validate_pipeline → contracts + expand + AST verify",expand:"Run expand_project to regenerate .expanded/ from compact + .ctx"};default:return{read:"N/A",edit:"N/A",docs:"N/A",validate:"N/A"}}}
|
|
@@ -0,0 +1,9 @@
|
|
|
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}}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ctx .context/src/core/event-bus.ctx
|
|
2
|
+
import{EventEmitter as o}from"node:events";
|
|
3
|
+
const t=new o;t.setMaxListeners(50);
|
|
4
|
+
export function emitToolCall(o,e){t.emit("tool:call",{type:"tool_call",tool:o,args:e,ts:Date.now()})}
|
|
5
|
+
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
|
+
export function onToolCall(o){t.on("tool:call",o)}
|
|
7
|
+
export function onToolResult(o){t.on("tool:result",o)}
|
|
8
|
+
export function removeToolListener(o,e){t.off(o,e)}
|
|
9
|
+
export default t;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @ctx .context/src/core/filters.ctx
|
|
2
|
+
import{readFileSync as e,existsSync as t}from"fs";import{join as r}from"path";
|
|
3
|
+
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
|
+
let s={excludeDirs:[...i],excludePatterns:[...n],includeHidden:!1,useGitignore:!0,gitignorePatterns:[]};
|
|
5
|
+
export function getFilters(){return{...s}}
|
|
6
|
+
export function setFilters(e){return void 0!==e.excludeDirs&&(s.excludeDirs=e.excludeDirs),void 0!==e.excludePatterns&&(s.excludePatterns=e.excludePatterns),void 0!==e.includeHidden&&(s.includeHidden=e.includeHidden),void 0!==e.useGitignore&&(s.useGitignore=e.useGitignore),getFilters()}
|
|
7
|
+
export function addExcludes(e){return s.excludeDirs=[...new Set([...s.excludeDirs,...e])],getFilters()}
|
|
8
|
+
export function removeExcludes(e){return s.excludeDirs=s.excludeDirs.filter(t=>!e.includes(t)),getFilters()}
|
|
9
|
+
export function resetFilters(){return s={excludeDirs:[...i],excludePatterns:[...n],includeHidden:!1,useGitignore:!0,gitignorePatterns:[]},getFilters()}
|
|
10
|
+
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))}
|
|
@@ -0,0 +1,12 @@
|
|
|
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}
|