project-graph-mcp 2.1.2 → 2.1.3
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/package.json +1 -1
- package/rules/test-rules.json +15 -0
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +3 -1
- package/src/analysis/complexity.js +9 -13
- package/src/analysis/custom-rules.js +16 -35
- package/src/analysis/db-analysis.js +2 -6
- package/src/analysis/dead-code.js +8 -18
- package/src/analysis/full-analysis.js +9 -17
- package/src/analysis/jsdoc-checker.js +11 -23
- package/src/analysis/jsdoc-generator.js +8 -9
- package/src/analysis/similar-functions.js +8 -15
- package/src/analysis/test-annotations.js +12 -20
- package/src/analysis/type-checker.js +5 -7
- package/src/analysis/undocumented.js +10 -13
- package/src/cli/cli-handlers.js +4 -3
- package/src/compact/ai-context.js +2 -2
- package/src/compact/compact-migrate.js +8 -16
- package/src/compact/compact.js +3 -5
- package/src/compact/compress.js +7 -13
- package/src/compact/ctx-resolver.js +5 -0
- package/src/compact/ctx-to-jsdoc.js +13 -28
- package/src/compact/doc-dialect.js +18 -29
- package/src/compact/expand.js +10 -36
- package/src/compact/jsdoc-builder.js +5 -0
- package/src/compact/mode-config.js +6 -6
- package/src/compact/split-declarations.js +2 -0
- package/src/compact/validate-pipeline.js +7 -8
- package/src/core/event-bus.js +2 -1
- package/src/core/file-walker.js +4 -0
- package/src/core/filters.js +6 -5
- package/src/core/graph-builder.js +4 -11
- package/src/core/parser.js +19 -29
- package/src/core/utils.js +2 -0
- package/src/lang/lang-sql.js +7 -20
- package/src/mcp/mcp-server.js +2 -3
- package/src/mcp/tool-defs.js +1 -1
- package/src/mcp/tools.js +13 -21
- package/src/network/backend-lifecycle.js +15 -18
- package/src/network/local-gateway.js +10 -22
- package/src/network/mdns.js +5 -11
- package/src/network/server.js +1 -2
- package/src/network/web-server.js +7 -33
- package/web/app.js +19 -14
- package/web/components/code-block.js +1 -0
- package/web/components/quick-open.js +1 -0
- package/web/dashboard-state.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +5 -4
- 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 -4
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
- package/web/panels/ProjectItem/ProjectItem.js +3 -4
- package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +5 -4
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +1 -0
- package/web/panels/ctx-panel.js +1 -0
- package/web/panels/dep-graph.js +1 -0
- package/web/panels/file-tree.js +4 -188
- package/web/panels/health-panel.js +1 -0
- package/web/panels/live-monitor.js +1 -0
- package/web/state.js +7 -10
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/jsdoc-checker.ctx
|
|
2
|
-
import{readFileSync as e,readdirSync as t,statSync as n}from"fs";import{join as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function
|
|
10
|
-
function
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
function hasReturnValue(e){let t=!1;try{a.simple(e.body,{ReturnStatement(e){e.argument&&(t=!0)},FunctionDeclaration(){},FunctionExpression(){},ArrowFunctionExpression(){}})}catch(e){}return t}
|
|
14
|
-
function validateFunction(e,t,n,r,s,i){const o=[];if(!e)return o;
|
|
15
|
-
const a=e.params;a.length>0&&a.length!==t.length&&o.push({file:s,line:i,name:r,severity:"error",message:`Param count mismatch: JSDoc has ${a.length}, function has ${t.length}`});
|
|
16
|
-
const c=Math.min(a.length,t.length);for(let e=0;e<c;e++){const n=a[e].name,c=extractParamName(t[e]);n!==c&&"options"!==c&&"args"!==c&&"param"!==c&&o.push({file:s,line:i,name:r,severity:"error",message:`Param name mismatch at position ${e}: JSDoc says "${n}", code has "${c}"`})}!e.hasReturns&&hasReturnValue(n)&&o.push({file:s,line:i,name:r,severity:"warning",message:"Function returns a value but JSDoc has no @returns"});for(let e=0;e<c;e++){const n=a[e].type,c=inferTypeFromDefault(t[e]);if(c&&n&&"*"!==n){let t=n.toLowerCase().includes(c.toLowerCase());!t&&"string"===c&&n.includes("'")&&n.includes("|")&&(t=!0),!t&&"Array"===c&&n.includes("[]")&&(t=!0),t||o.push({file:s,line:i,name:r,severity:"warning",message:`Type mismatch for "${a[e].name}": JSDoc says {${n}}, default value suggests {${c}}`})}}return o}
|
|
17
|
-
export function checkJSDocFile(e,t){const n=[];
|
|
18
|
-
let r;try{r=o(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return n}const s=extractJSDocComments(e);return a.simple(r,{FunctionDeclaration(e){if(!e.id)return;
|
|
19
|
-
const r=findJSDocBefore(s,e.loc.start.line);r&&n.push(...validateFunction(r,e.params,e,e.id.name,t,e.loc.start.line))},VariableDeclaration(e){for(const r of e.declarations){if(!r.init)continue;
|
|
20
|
-
const i="ArrowFunctionExpression"===r.init.type||"FunctionExpression"===r.init.type?r.init:null;if(!i||!r.id?.name)continue;
|
|
21
|
-
const o=findJSDocBefore(s,e.loc.start.line);o&&n.push(...validateFunction(o,i.params,i,r.id.name,t,e.loc.start.line))}},ClassDeclaration(e){const r=e.id?.name||"Anonymous";for(const i of e.body.body){if("MethodDefinition"!==i.type)continue;
|
|
22
|
-
const e=i.key.name||i.key.value;if(!e||"constructor"===e)continue;if("method"!==i.kind)continue;
|
|
23
|
-
const o=i.value,a=findJSDocBefore(s,i.loc.start.line);a&&n.push(...validateFunction(a,o.params,o,`${r}.${e}`,t,i.loc.start.line))}}}),n}
|
|
24
|
-
export function checkJSDocConsistency(t){const n=i(t),r=findJSFiles(t),o=[];for(const t of r){let r;try{r=e(t,"utf-8")}catch(e){continue}const i=checkJSDocFile(r,s(n,t));o.push(...i)}const a=o.filter(e=>"error"===e.severity).length,c=o.filter(e=>"warning"===e.severity).length,l={};for(const e of o)l[e.file]=(l[e.file]||0)+1;return{issues:o,summary:{total:o.length,errors:a,warnings:c,byFile:l}}}
|
|
2
|
+
import{readFileSync as e,readdirSync as t,statSync as n}from"fs";import{join as s,relative as r,resolve as i}from"path";import{parse as o}from"../../vendor/acorn.mjs";
|
|
3
|
+
import*as a from"../../vendor/walk.mjs";
|
|
4
|
+
import{shouldExcludeDir as c,shouldExcludeFile as l,parseGitignore as u}from"../core/filters.js";
|
|
5
|
+
function f(e,i=e){e===i&&u(i);const o=[];try{for(const a of t(e)){const t=s(e,a),u=r(i,t);n(t).isDirectory()?c(a,u)||o.push(...f(t,i)):!a.endsWith(".js")||a.endsWith(".css.js")||a.endsWith(".tpl.js")||l(a,u)||o.push(t)}}catch(e){}return o}
|
|
6
|
+
function m(e,t){for(const n of e){const e=t-n.endLine;if(e>=0&&e<=2)return n}return null}
|
|
7
|
+
function p(e){return"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name:"RestElement"===e.type&&"Identifier"===e.argument.type?e.argument.name:"ObjectPattern"===e.type?"options":"ArrayPattern"===e.type?"args":"param"}
|
|
8
|
+
function h(e){if("AssignmentPattern"!==e.type)return null;const t=e.right;if("Literal"===t.type){if("string"==typeof t.value)return"string";if("number"==typeof t.value)return"number";if("boolean"==typeof t.value)return"boolean"}return"ArrayExpression"===t.type?"Array":"ObjectExpression"===t.type?"Object":null}
|
|
9
|
+
function y(e){let t=!1;try{a.simple(e.body,{ReturnStatement(e){e.argument&&(t=!0)},FunctionDeclaration(){},FunctionExpression(){},ArrowFunctionExpression(){}})}catch(e){}return t}
|
|
10
|
+
function d(e,t,n,s,r,i){const o=[];if(!e)return o;const a=e.params;a.length>0&&a.length!==t.length&&o.push({file:r,line:i,name:s,severity:"error",message:`Param count mismatch: JSDoc has ${a.length}, function has ${t.length}`});const c=Math.min(a.length,t.length);for(let e=0;e<c;e++){const n=a[e].name,c=p(t[e]);n!==c&&"options"!==c&&"args"!==c&&"param"!==c&&o.push({file:r,line:i,name:s,severity:"error",message:`Param name mismatch at position ${e}: JSDoc says "${n}", code has "${c}"`})}!e.hasReturns&&y(n)&&o.push({file:r,line:i,name:s,severity:"warning",message:"Function returns a value but JSDoc has no @returns"});for(let e=0;e<c;e++){const n=a[e].type,c=h(t[e]);if(c&&n&&"*"!==n){let t=n.toLowerCase().includes(c.toLowerCase());!t&&"string"===c&&n.includes("'")&&n.includes("|")&&(t=!0),!t&&"Array"===c&&n.includes("[]")&&(t=!0),t||o.push({file:r,line:i,name:s,severity:"warning",message:`Type mismatch for "${a[e].name}": JSDoc says {${n}}, default value suggests {${c}}`})}}return o}
|
|
11
|
+
export function checkJSDocFile(e,t){const n=[];let s;try{s=o(e,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){return n}const r=function(e){const t=[],n=/\/\*\*[\s\S]*?\*\//g;let s;for(;null!==(s=n.exec(e));){const n=s[0],r=e.slice(0,s.index+n.length).split("\n").length,i=[],o=/@param\s+\{/g;let a;for(;null!==(a=o.exec(n));){let e=1,t=a.index+a[0].length;for(;t<n.length&&e>0;)"{"===n[t]?e++:"}"===n[t]&&e--,t++;if(0!==e)continue;const s=n.slice(a.index+a[0].length,t-1),r=n.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let o=r[1];o.startsWith("[")&&(o=o.slice(1)),o.endsWith("]")&&(o=o.slice(0,-1)),o.includes(".")||i.push({name:o,type:s})}const c=/@returns?\s/.test(n);t.push({text:n,endLine:r,params:i,hasReturns:c})}return t}(e);return a.simple(s,{FunctionDeclaration(e){if(!e.id)return;const s=m(r,e.loc.start.line);s&&n.push(...d(s,e.params,e,e.id.name,t,e.loc.start.line))},VariableDeclaration(e){for(const s of e.declarations){if(!s.init)continue;const i="ArrowFunctionExpression"===s.init.type||"FunctionExpression"===s.init.type?s.init:null;if(!i||!s.id?.name)continue;const o=m(r,e.loc.start.line);o&&n.push(...d(o,i.params,i,s.id.name,t,e.loc.start.line))}},ClassDeclaration(e){const s=e.id?.name||"Anonymous";for(const i of e.body.body){if("MethodDefinition"!==i.type)continue;const e=i.key.name||i.key.value;if(!e||"constructor"===e)continue;if("method"!==i.kind)continue;const o=i.value,a=m(r,i.loc.start.line);a&&n.push(...d(a,o.params,o,`${s}.${e}`,t,i.loc.start.line))}}}),n}
|
|
12
|
+
export function checkJSDocConsistency(t){const n=i(t),s=f(t),o=[];for(const t of s){let s;try{s=e(t,"utf-8")}catch(e){continue}const i=checkJSDocFile(s,r(n,t));o.push(...i)}const a=o.filter(e=>"error"===e.severity).length,c=o.filter(e=>"warning"===e.severity).length,l={};for(const e of o)l[e.file]=(l[e.file]||0)+1;return{issues:o,summary:{total:o.length,errors:a,warnings:c,byFile:l}}}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/jsdoc-generator.ctx
|
|
2
|
-
import{readFileSync as t}from"fs";import{relative as e}from"path";import{parse as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const e=
|
|
6
|
-
const
|
|
7
|
-
function
|
|
8
|
-
function
|
|
9
|
-
function
|
|
10
|
-
export function generateJSDocFor(t,e,r={}){return generateJSDoc(t,r).find(t=>t.name===e||t.name.endsWith(`.${e}`))||null}
|
|
2
|
+
import{readFileSync as t}from"fs";import{relative as e}from"path";import{parse as n}from"../../vendor/acorn.mjs";
|
|
3
|
+
import*as r from"../../vendor/walk.mjs";
|
|
4
|
+
import{getWorkspaceRoot as a}from"../core/workspace.js";
|
|
5
|
+
export function generateJSDoc(o,s={}){const c=[],f=t(o,"utf-8"),m=e(a(),o);let p;try{p=n(f,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return c}const u=t=>{const e=f.split("\n");for(let n=t-2;n>=Math.max(0,t-15);n--){const t=e[n]?.trim();if(t){if("*/"===t||t.endsWith("*/")){for(let t=n-1;t>=Math.max(0,n-20);t--){const n=e[t]?.trim();if(n?.startsWith("/**"))return!0;if(n&&!n.startsWith("*"))break}return!1}if(!t.startsWith("*")&&!t.startsWith("//"))break}}return!1};return r.simple(p,{FunctionDeclaration(t){if(!t.id)return;if(u(t.loc.start.line))return;const e=i({name:t.id.name,params:t.params,async:t.async});c.push({name:t.id.name,type:"function",file:m,line:t.loc.start.line,jsdoc:e})},ClassDeclaration(t){if(t.id)for(const e of t.body.body)if("MethodDefinition"===e.type){const n=e.key.name||e.key.value;if("method"!==e.kind)continue;if(n.startsWith("_"))continue;if(u(e.loc.start.line))continue;const r=e.value,a=i({name:n,params:r.params,async:r.async});c.push({name:`${t.id.name}.${n}`,type:"method",file:m,line:e.loc.start.line,jsdoc:a})}}}),c}
|
|
6
|
+
function i(t){const e=["/**"];e.push(` * TODO: Add description for ${t.name}`);for(const n of t.params){const t=o(n),r=s(n);e.push(` * @param {${r}} ${t}`)}return e.push(` * @returns {${t.async?"Promise<*>":"*"}}`),e.push(" */"),e.join("\n")}
|
|
7
|
+
function o(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?`[${t.left.name}]`:"RestElement"===t.type&&"Identifier"===t.argument.type?`...${t.argument.name}`:"ObjectPattern"===t.type?"options":"ArrayPattern"===t.type?"args":"param"}
|
|
8
|
+
function s(t){if("AssignmentPattern"===t.type){const e=t.right;if("Literal"===e.type){if("string"==typeof e.value)return"string";if("number"==typeof e.value)return"number";if("boolean"==typeof e.value)return"boolean"}if("ArrayExpression"===e.type)return"Array";if("ObjectExpression"===e.type)return"Object"}return"RestElement"===t.type?"Array":"ObjectPattern"===t.type?"Object":"ArrayPattern"===t.type?"Array":"*"}
|
|
9
|
+
export function generateJSDocFor(t,e,n={}){return generateJSDoc(t,n).find(t=>t.name===e||t.name.endsWith(`.${e}`))||null}
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/similar-functions.ctx
|
|
2
|
-
import{readFileSync as t,readdirSync as e,statSync as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const e=t.key.name||t.key.value;e.startsWith("_")||
|
|
7
|
-
function
|
|
8
|
-
const r=
|
|
9
|
-
function
|
|
10
|
-
function hashBodyStructure(t){const e=[];return l.simple(t,{IfStatement(){e.push("IF")},ForStatement(){e.push("FOR")},ForOfStatement(){e.push("FOROF")},ForInStatement(){e.push("FORIN")},WhileStatement(){e.push("WHILE")},SwitchStatement(){e.push("SWITCH")},TryStatement(){e.push("TRY")},ReturnStatement(){e.push("RET")},ThrowStatement(){e.push("THROW")},AwaitExpression(){e.push("AWAIT")}}),e.join("|")}
|
|
11
|
-
function calculateSimilarity(t,e){const a=[];
|
|
12
|
-
let n=0,s=0;s+=30,t.paramCount===e.paramCount&&(n+=30,a.push("Same param count")),s+=20;
|
|
13
|
-
const r=t.paramNames.filter(t=>e.paramNames.includes(t));if(r.length>0&&t.paramNames.length>0){const s=r.length/Math.max(t.paramNames.length,e.paramNames.length);n+=Math.round(20*s),s>=.5&&a.push(`Similar params: ${r.join(", ")}`)}if(s+=10,t.async===e.async&&(n+=10),s+=25,t.bodyHash===e.bodyHash&&t.bodyHash.length>0)n+=25,a.push("Identical structure");else if(t.bodyHash.length>0&&e.bodyHash.length>0){const s=t.bodyHash.split("|"),r=e.bodyHash.split("|"),i=s.filter(t=>r.includes(t));if(i.length>0){const t=i.length/Math.max(s.length,r.length);n+=Math.round(25*t),t>=.5&&a.push("Similar control flow")}}s+=15;
|
|
14
|
-
const i=t.calls.filter(t=>e.calls.includes(t));if(i.length>0&&t.calls.length>0&&e.calls.length>0){const s=i.length/Math.max(t.calls.length,e.calls.length);n+=Math.round(15*s),i.length>=2&&a.push(`Common calls: ${i.slice(0,3).join(", ")}`)}return{similarity:Math.round(n/100*100),reasons:a}}
|
|
15
|
-
export async function getSimilarFunctions(t,e={}){const a=e.threshold||60,n=r(t),s=findJSFiles(t),i=[];for(const t of s)i.push(...extractSignatures(t,n));
|
|
16
|
-
const l=[];for(let t=0;t<i.length;t++)for(let e=t+1;e<i.length;e++){const n=i[t],s=i[e];if(n.file===s.file&&n.name===s.name)continue;if(n.bodyHash.length<3&&s.bodyHash.length<3)continue;const{similarity:r,reasons:o}=calculateSimilarity(n,s);r>=a&&o.length>0&&l.push({a:n,b:s,similarity:r,reasons:o})}return l.sort((t,e)=>e.similarity-t.similarity),{total:l.length,pairs:l.slice(0,20)}}
|
|
2
|
+
import{readFileSync as t,readdirSync as e,statSync as n}from"fs";import{join as s,relative as a,resolve as r}from"path";import{parse as l}from"../../vendor/acorn.mjs";
|
|
3
|
+
import*as o from"../../vendor/walk.mjs";
|
|
4
|
+
import{shouldExcludeDir as i,shouldExcludeFile as h,parseGitignore as c}from"../core/filters.js";
|
|
5
|
+
function m(t,r=t){t===r&&c(r);const l=[];try{for(const o of e(t)){const e=s(t,o),c=a(r,e);n(e).isDirectory()?i(o,c)||l.push(...m(e,r)):!o.endsWith(".js")||o.endsWith(".css.js")||o.endsWith(".tpl.js")||h(o,c)||l.push(e)}}catch(t){}return l}
|
|
6
|
+
function p(e,n){const s=t(e,"utf-8"),r=a(n,e),i=[];let h;try{h=l(s,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return i}return o.simple(h,{FunctionDeclaration(t){t.id&&i.push(u(t,t.id.name,r))},MethodDefinition(t){if("method"!==t.kind)return;const e=t.key.name||t.key.value;e.startsWith("_")||i.push(u(t.value,e,r))}}),i}
|
|
7
|
+
function u(t,e,n){const s=t.params.map(t=>function(t){return"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument.type?t.argument.name:"param"}(t)),a=[];o.simple(t.body,{CallExpression(t){"Identifier"===t.callee.type?a.push(t.callee.name):"MemberExpression"===t.callee.type&&"Identifier"===t.callee.property.type&&a.push(t.callee.property.name)}});const r=function(t){const e=[];return o.simple(t,{IfStatement(){e.push("IF")},ForStatement(){e.push("FOR")},ForOfStatement(){e.push("FOROF")},ForInStatement(){e.push("FORIN")},WhileStatement(){e.push("WHILE")},SwitchStatement(){e.push("SWITCH")},TryStatement(){e.push("TRY")},ReturnStatement(){e.push("RET")},ThrowStatement(){e.push("THROW")},AwaitExpression(){e.push("AWAIT")}}),e.join("|")}(t.body);return{name:e,file:n,line:t.loc?.start?.line||0,paramCount:t.params.length,paramNames:s,async:t.async||!1,bodyHash:r,calls:[...new Set(a)]}}
|
|
8
|
+
function f(t,e){const n=[];let s=0,a=0;a+=30,t.paramCount===e.paramCount&&(s+=30,n.push("Same param count")),a+=20;const r=t.paramNames.filter(t=>e.paramNames.includes(t));if(r.length>0&&t.paramNames.length>0){const a=r.length/Math.max(t.paramNames.length,e.paramNames.length);s+=Math.round(20*a),a>=.5&&n.push(`Similar params: ${r.join(", ")}`)}if(a+=10,t.async===e.async&&(s+=10),a+=25,t.bodyHash===e.bodyHash&&t.bodyHash.length>0)s+=25,n.push("Identical structure");else if(t.bodyHash.length>0&&e.bodyHash.length>0){const a=t.bodyHash.split("|"),r=e.bodyHash.split("|"),l=a.filter(t=>r.includes(t));if(l.length>0){const t=l.length/Math.max(a.length,r.length);s+=Math.round(25*t),t>=.5&&n.push("Similar control flow")}}a+=15;const l=t.calls.filter(t=>e.calls.includes(t));if(l.length>0&&t.calls.length>0&&e.calls.length>0){const a=l.length/Math.max(t.calls.length,e.calls.length);s+=Math.round(15*a),l.length>=2&&n.push(`Common calls: ${l.slice(0,3).join(", ")}`)}return{similarity:Math.round(s/100*100),reasons:n}}
|
|
9
|
+
export async function getSimilarFunctions(t,e={}){const n=e.threshold||60,s=r(t),a=m(t),l=[];for(const t of a)l.push(...p(t,s));const o=[];for(let t=0;t<l.length;t++)for(let e=t+1;e<l.length;e++){const s=l[t],a=l[e];if(s.file===a.file&&s.name===a.name)continue;if(s.bodyHash.length<3&&a.bodyHash.length<3)continue;const{similarity:r,reasons:i}=f(s,a);r>=n&&i.length>0&&o.push({a:s,b:a,similarity:r,reasons:i})}return o.sort((t,e)=>e.similarity-t.similarity),{total:o.length,pairs:o.slice(0,20)}}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/test-annotations.ctx
|
|
2
|
-
import{readFileSync as t,readdirSync as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const s=t.match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(!
|
|
6
|
-
let
|
|
7
|
-
const
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
export function
|
|
13
|
-
export function
|
|
14
|
-
function updateTestState(e,s,r,a){const i=process.cwd(),c=findCtxMdFiles(o(i,".context")),f=parseInt(s.split(".")[1],10);for(const o of c)try{const i=t(o,"utf-8").split("\n");
|
|
15
|
-
let c=!1,u=0;for(let t=0;t<i.length;t++){if(i[t].startsWith("## ")){c=i[t].startsWith("## Tests");continue}if(!c)continue;
|
|
16
|
-
const l=i[t].match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(l&&l[2]===e){if(u===f){const c=l[3].replace(/\s*\(FAILED:.*\)$/,""),f=a?` (FAILED: ${a})`:"";return i[t]=`- [${r}] ${e}: ${c}${f}`,n(o,i.join("\n"),"utf-8"),{success:!0,testId:s,...a?{reason:a}:{}}}u++}}}catch(t){}return{success:!1,testId:s,error:"Test not found"}}
|
|
17
|
-
export function getTestSummary(t){const e=getAllFeatures(t);
|
|
18
|
-
let s=0,n=0,o=0,r=0;
|
|
19
|
-
const a=[];for(const t of e)for(const e of t.tests)s++,"passed"===e.status?n++:"failed"===e.status?(o++,a.push({id:e.id,reason:e.failReason})):r++;return{total:s,passed:n,failed:o,pending:r,progress:s>0?Math.round((n+o)/s*100):0,failures:a}}
|
|
20
|
-
export function resetTestState(){const e=process.cwd(),s=findCtxMdFiles(o(e,".context"));for(const e of s)try{let s=t(e,"utf-8");
|
|
21
|
-
const o=s.replace(/^(- )\[([x!])\] (\w+:\s*.+?)(?:\s*\(FAILED:.*\))?$/gm,"$1[ ] $3");o!==s&&n(e,o,"utf-8")}catch(t){}return{success:!0}}
|
|
2
|
+
import{readFileSync as t,readdirSync as s,statSync as e,writeFileSync as n}from"fs";
|
|
3
|
+
import{join as o,relative as r,resolve as c}from"path";
|
|
4
|
+
function a(t){const n=[];try{for(const r of s(t)){const s=o(t,r);e(s).isDirectory()&&!r.startsWith(".")?n.push(...a(s)):r.endsWith(".ctx.md")&&n.push(s)}}catch(t){}return n}
|
|
5
|
+
export function parseAnnotations(t,s){const e=t.split("\n"),n=[];let o=!1,r=[];for(const t of e){if(t.startsWith("## ")){o&&r.length&&(n.push(...i(r,s)),r=[]),o=t.startsWith("## Tests");continue}if(!o)continue;const e=t.match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(!e)continue;const[,c,a,f]=e,u=f.split("→").map(t=>t.trim()),l=u[0],p=u[1]||null;let d=null,h="pending";if("x"===c&&(h="passed"),"!"===c){h="failed";const t=l.match(/\(FAILED:\s*(.+)\)$/);t&&(d=t[1].trim())}r.push({name:a,action:l,expected:p,status:h,failReason:d})}return o&&r.length&&n.push(...i(r,s)),n}
|
|
6
|
+
function i(t,s){const e={};let n={};for(const s of t)e[s.name]||(e[s.name]=[],n[s.name]=0),e[s.name].push({id:`${s.name}.${n[s.name]++}`,action:s.action,expected:s.expected,status:s.status,failReason:s.failReason});return Object.entries(e).map(([t,e])=>({name:t,tests:e,file:s}))}
|
|
7
|
+
export function getAllFeatures(s){const e=a(o(c(s),".context")),n=[];for(const s of e)try{const e=parseAnnotations(t(s,"utf-8"),s);n.push(...e)}catch(t){}return n}
|
|
8
|
+
export function getPendingTests(t){const s=c(t),e=getAllFeatures(t),n=[];for(const t of e)for(const e of t.tests)"pending"===e.status&&n.push({...e,feature:t.name,file:r(s,t.file)});return n}
|
|
9
|
+
export function markTestPassed(t){return f(t.split(".")[0],t,"x")}
|
|
10
|
+
export function markTestFailed(t,s){return f(t.split(".")[0],t,"!",s)}
|
|
11
|
+
function f(s,e,r,c){const i=process.cwd(),f=a(o(i,".context")),u=parseInt(e.split(".")[1],10);for(const o of f)try{const a=t(o,"utf-8").split("\n");let i=!1,f=0;for(let t=0;t<a.length;t++){if(a[t].startsWith("## ")){i=a[t].startsWith("## Tests");continue}if(!i)continue;const l=a[t].match(/^- \[([ x!])\] (\w+):\s*(.+)$/);if(l&&l[2]===s){if(f===u){const i=l[3].replace(/\s*\(FAILED:.*\)$/,""),f=c?` (FAILED: ${c})`:"";return a[t]=`- [${r}] ${s}: ${i}${f}`,n(o,a.join("\n"),"utf-8"),{success:!0,testId:e,...c?{reason:c}:{}}}f++}}}catch(t){}return{success:!1,testId:e,error:"Test not found"}}
|
|
12
|
+
export function getTestSummary(t){const s=getAllFeatures(t);let e=0,n=0,o=0,r=0;const c=[];for(const t of s)for(const s of t.tests)e++,"passed"===s.status?n++:"failed"===s.status?(o++,c.push({id:s.id,reason:s.failReason})):r++;return{total:e,passed:n,failed:o,pending:r,progress:e>0?Math.round((n+o)/e*100):0,failures:c}}
|
|
13
|
+
export function resetTestState(){const s=process.cwd(),e=a(o(s,".context"));for(const s of e)try{let e=t(s,"utf-8");const o=e.replace(/^(- )\[([x!])\] (\w+:\s*.+?)(?:\s*\(FAILED:.*\))?$/gm,"$1[ ] $3");o!==e&&n(s,o,"utf-8")}catch(t){}return{success:!0}}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/type-checker.ctx
|
|
2
|
-
import{execSync as t,spawn as e}from"child_process";import{existsSync as s}from"fs";import{resolve as n,join as
|
|
3
|
-
function
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let n="",i="";s.stdout.on("data",t=>{n+=t}),s.stderr.on("data",t=>{i+=t});
|
|
8
|
-
const o=setTimeout(()=>{s.kill("SIGTERM"),t({stdout:n,stderr:i,killed:!0})},6e4);s.on("close",()=>{clearTimeout(o),t({stdout:n,stderr:i,killed:!1})}),s.on("error",e=>{clearTimeout(o),t({stdout:"",stderr:e.message,killed:!1})})}),p=((u.stdout||"")+(u.stderr||"")).split("\n").filter(t=>t.trim()),d=[];for(const t of p){const e=parseDiagnosticLine(t);e&&d.length<i&&d.push(e)}const h=d.filter(t=>"error"===t.severity).length,m=d.filter(t=>"warning"===t.severity).length,f={};for(const t of d)f[t.file]=(f[t.file]||0)+1;return{available:!0,version:o.version,diagnostics:d,summary:{total:d.length,errors:h,warnings:m,byFile:f},hint:null}}
|
|
2
|
+
import{execSync as t,spawn as e}from"child_process";import{existsSync as s}from"fs";import{resolve as n,join as r}from"path";
|
|
3
|
+
function o(){try{return{available:!0,version:t("tsc --version",{encoding:"utf-8",timeout:5e3}).trim(),path:t("which tsc",{encoding:"utf-8",timeout:5e3}).trim()}}catch(e){try{return{available:!0,version:t("npx tsc --version",{encoding:"utf-8",timeout:15e3}).trim(),path:"npx tsc"}}catch(t){return{available:!1,version:null,path:null}}}}
|
|
4
|
+
function i(t,e){const s=t.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);return s?{file:s[1],line:parseInt(s[2]),column:parseInt(s[3]),severity:s[4],message:s[6],code:s[5]}:null}
|
|
5
|
+
function l(t,e={}){const n=["--noEmit"],o=r(t,"tsconfig.json"),i=r(t,"jsconfig.json");return s(o)?n.push("--project",o):s(i)?n.push("--project",i):(n.push("--allowJs","--checkJs"),n.push("--target","ESNext"),n.push("--module","NodeNext"),n.push("--moduleResolution","NodeNext"),n.push("--skipLibCheck"),e.files?.length?n.push(...e.files):n.push("--rootDir",t)),n}
|
|
6
|
+
export async function checkTypes(t,s={}){const r=s.maxDiagnostics||50,a=n(t),c=o();if(!c.available)return{available:!1,version:null,diagnostics:[],summary:{total:0,errors:0,warnings:0},hint:"TypeScript not found. Install: npm i -g typescript"};const u=l(a,s),p=c.path.includes("npx")?"npx":"tsc",d=c.path.includes("npx")?["tsc",...u]:u,h=await new Promise(t=>{const s=e(p,d,{cwd:a,stdio:["ignore","pipe","pipe"]});let n="",r="";s.stdout.on("data",t=>{n+=t}),s.stderr.on("data",t=>{r+=t});const o=setTimeout(()=>{s.kill("SIGTERM"),t({stdout:n,stderr:r,killed:!0})},6e4);s.on("close",()=>{clearTimeout(o),t({stdout:n,stderr:r,killed:!1})}),s.on("error",e=>{clearTimeout(o),t({stdout:"",stderr:e.message,killed:!1})})}),m=((h.stdout||"")+(h.stderr||"")).split("\n").filter(t=>t.trim()),f=[];for(const t of m){const e=i(t);e&&f.length<r&&f.push(e)}const g=f.filter(t=>"error"===t.severity).length,v=f.filter(t=>"warning"===t.severity).length,y={};for(const t of f)y[t.file]=(y[t.file]||0)+1;return{available:!0,version:c.version,diagnostics:f,summary:{total:f.length,errors:g,warnings:v,byFile:y},hint:null}}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// @ctx .context/src/analysis/undocumented.ctx
|
|
2
|
-
import{readFileSync as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
let o;try{o=
|
|
10
|
-
const r=
|
|
11
|
-
const
|
|
12
|
-
const i=checkMissing(findJSDocBefore(c,e.loc.start.line),n);i.length>0&&s.push({name:o,type:"function",file:t,line:e.loc.start.line,reason:i.join(", ")})}}),s}
|
|
13
|
-
export function getUndocumented(t,n="tests"){const s=c(t),i=findJSFiles(t),r=[];for(const t of i){let c;try{c=e(t,"utf-8")}catch(e){continue}const i=checkUndocumentedFile(c,o(s,t),n);r.push(...i)}return r}
|
|
14
|
-
export function getUndocumentedSummary(e,t="tests"){const n=getUndocumented(e,t),s={class:n.filter(e=>"class"===e.type).length,function:n.filter(e=>"function"===e.type).length,method:n.filter(e=>"method"===e.type).length},o={};for(const e of n)o[e.reason]=(o[e.reason]||0)+1;return{total:n.length,byType:s,byReason:o,items:n.slice(0,20)}}
|
|
2
|
+
import{readFileSync as t,readdirSync as e,statSync as n}from"fs";import{join as s,relative as o,resolve as r}from"path";import{parse as c}from"../../vendor/acorn.mjs";
|
|
3
|
+
import*as i from"../../vendor/walk.mjs";
|
|
4
|
+
import{shouldExcludeDir as l,shouldExcludeFile as a,parseGitignore as u}from"../core/filters.js";
|
|
5
|
+
function d(t,r=t){t===r&&u(r);const c=[];try{for(const i of e(t)){const e=s(t,i),u=o(r,e);n(e).isDirectory()?l(i,u)||c.push(...d(e,r)):!i.endsWith(".js")||i.endsWith(".css.js")||i.endsWith(".tpl.js")||a(i,u)||c.push(e)}}catch(t){}return c}
|
|
6
|
+
function f(t,e){for(const n of t){const t=e-n.endLine;if(t>=0&&t<=2)return n.text}return null}
|
|
7
|
+
function p(t,e){const n=[];return t?("params"!==e&&"all"!==e||(t.includes("@param")||n.push("@param"),t.includes("@returns")||t.includes("@return")||n.push("@returns")),n):("all"===e&&n.push("description"),"params"!==e&&"all"!==e||n.push("@param","@returns"),n)}
|
|
8
|
+
const m=["constructor","connectedCallback","disconnectedCallback","attributeChangedCallback","renderCallback"];
|
|
9
|
+
export function checkUndocumentedFile(t,e,n){const s=[];let o;try{o=c(t,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(t){return s}const r=function(t){const e=[],n=/\/\*\*[\s\S]*?\*\//g;let s;for(;null!==(s=n.exec(t));){const n=t.slice(0,s.index+s[0].length).split("\n").length;e.push({text:s[0],endLine:n})}return e}(t);return i.simple(o,{ClassDeclaration(t){const o=t.id?.name||"Anonymous";"all"===n&&(f(r,t.loc.start.line)||s.push({name:o,type:"class",file:e,line:t.loc.start.line,reason:"No JSDoc"}));for(const c of t.body.body)if("MethodDefinition"===c.type){const t=c.key.name||c.key.value;if("get"===c.kind||"set"===c.kind)continue;if(t?.startsWith("_"))continue;if(m.includes(t))continue;const i=p(f(r,c.loc.start.line),n);i.length>0&&s.push({name:`${o}.${t}`,type:"method",file:e,line:c.loc.start.line,reason:i.join(", ")})}},FunctionDeclaration(t){if(!t.id)return;const o=t.id.name;if(o.startsWith("_"))return;const c=p(f(r,t.loc.start.line),n);c.length>0&&s.push({name:o,type:"function",file:e,line:t.loc.start.line,reason:c.join(", ")})}}),s}
|
|
10
|
+
export function getUndocumented(e,n="tests"){const s=r(e),c=d(e),i=[];for(const e of c){let r;try{r=t(e,"utf-8")}catch(t){continue}const c=checkUndocumentedFile(r,o(s,e),n);i.push(...c)}return i}
|
|
11
|
+
export function getUndocumentedSummary(t,e="tests"){const n=getUndocumented(t,e),s={class:n.filter(t=>"class"===t.type).length,function:n.filter(t=>"function"===t.type).length,method:n.filter(t=>"method"===t.type).length},o={};for(const t of n)o[t.reason]=(o[t.reason]||0)+1;return{total:n.length,byType:s,byReason:o,items:n.slice(0,20)}}
|
package/src/cli/cli-handlers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ctx .context/src/cli/cli-handlers.ctx
|
|
2
|
-
import{getSkeleton as r,expand as e,deps as s,usages as
|
|
3
|
-
function
|
|
4
|
-
|
|
2
|
+
import{getSkeleton as r,expand as e,deps as s,usages as a}from"../mcp/tools.js";import{getPendingTests as t,getTestSummary as n}from"../analysis/test-annotations.js";import{getFilters as o}from"../core/filters.js";import{getInstructions as c}from"../compact/instructions.js";import{getUndocumentedSummary as i}from"../analysis/undocumented.js";import{getDeadCode as d}from"../analysis/dead-code.js";import{generateJSDoc as l}from"../analysis/jsdoc-generator.js";import{getSimilarFunctions as m}from"../analysis/similar-functions.js";import{getComplexity as u}from"../analysis/complexity.js";import{getLargeFiles as p}from"../analysis/large-files.js";import{getOutdatedPatterns as g}from"../analysis/outdated-patterns.js";import{getFullAnalysis as y}from"../analysis/full-analysis.js";import{compressFile as h}from"../compact/compress.js";import{getProjectDocs as f,generateContextFiles as j}from"../compact/doc-dialect.js";import{getGraph as b}from"../mcp/tools.js";import{parseProject as x}from"../core/parser.js";import{resolvePath as q}from"../core/workspace.js";import{checkJSDocConsistency as A}from"../analysis/jsdoc-checker.js";import{checkTypes as w}from"../analysis/type-checker.js";import{compactProject as E,expandProject as S}from"../compact/compact.js";import{injectJSDoc as U,stripJSDoc as P,validateCtxContracts as k}from"../compact/ctx-to-jsdoc.js";import{getConfig as v,setConfig as C,getModeDescription as D,getModeWorkflow as I}from"../compact/mode-config.js";import{compactMigrate as F}from"../compact/compact-migrate.js";
|
|
3
|
+
function O(r,e){const s=r.find(r=>r.startsWith(`--${e}=`));return s?s.split("=")[1]:void 0}
|
|
4
|
+
function R(r){const e=r.find(r=>!r.startsWith("--"))||".";return q(e)}
|
|
5
|
+
export const CLI_HANDLERS={config:{rawOutput:!0,handler:async()=>{const{execSync:r}=await import("child_process"),{dirname:e}=await import("path");let s,a;try{s=r("which npx",{encoding:"utf-8"}).trim()}catch{s="npx"}try{a=r("which node",{encoding:"utf-8"}).trim()}catch{a=""}const t={mcpServers:{"project-graph":{command:s,args:["-y","project-graph-mcp"],env:a?{PATH:`${e(a)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`}:{}}}};return console.log("Add this to your MCP config:\n"),JSON.stringify(t,null,2)}},skeleton:{requiresArg:!0,argError:"Path required: skeleton <path>",handler:async e=>r(q(e[0]))},expand:{requiresArg:!0,argError:"Symbol required: expand <symbol>",handler:async r=>e(r[0])},deps:{requiresArg:!0,argError:"Symbol required: deps <symbol>",handler:async r=>s(r[0])},usages:{requiresArg:!0,argError:"Symbol required: usages <symbol>",handler:async r=>a(r[0])},pending:{handler:async r=>t(R(r))},summary:{handler:async r=>n(R(r))},filters:{handler:async()=>o()},instructions:{rawOutput:!0,handler:async()=>c()},undocumented:{handler:async r=>{const e=O(r,"level")||"tests";return i(R(r),e)}},deadcode:{handler:async r=>d(R(r))},jsdoc:{requiresArg:!0,argError:"Usage: jsdoc <file>",handler:async r=>l(q(r[0]))},similar:{handler:async r=>{const e=parseInt(O(r,"threshold"))||60;return m(R(r),{threshold:e})}},complexity:{handler:async r=>{const e=parseInt(O(r,"min"))||1,s=r.includes("--problematic");return u(R(r),{minComplexity:e,onlyProblematic:s})}},largefiles:{handler:async r=>{const e=r.includes("--problematic");return p(R(r),{onlyProblematic:e})}},outdated:{handler:async r=>{const e=r.includes("--code"),s=r.includes("--deps");return g(R(r),{codeOnly:e,depsOnly:s})}},analyze:{handler:async r=>{const e=r.includes("--items");return y(R(r),{includeItems:e})}},"jsdoc-check":{handler:async r=>A(R(r))},types:{handler:async r=>{const e=parseInt(O(r,"max"))||50;return w(R(r),{maxDiagnostics:e})}},compress:{requiresArg:!0,argError:"Usage: compress <file> [--no-beautify] [--no-legend]",handler:async r=>{const e=!r.includes("--no-beautify"),s=!r.includes("--no-legend");return h(q(r[0]),{beautify:e,legend:s})}},docs:{requiresArg:!0,argError:"Usage: docs <path> [--file=<filename>]",handler:async r=>{const e=q(r[0]),s=await b(e),a=r.find(r=>r.startsWith("--file="))?.split("=")[1];return f(s,e,{file:a})}},"generate-ctx":{requiresArg:!0,argError:"Usage: generate-ctx <path> [--overwrite] [--scope=focus|all]",handler:async r=>{const e=q(r[0]),s=await b(e),a=await x(e),t=r.includes("--overwrite"),n=r.find(r=>r.startsWith("--scope="))?.split("=")[1]||"all";return j(s,e,a,{overwrite:t,scope:n})}},compact:{requiresArg:!0,argError:"Usage: compact <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return E(e,{dryRun:s})}},beautify:{requiresArg:!0,argError:"Usage: beautify <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return S(e,{dryRun:s})}},"inject-jsdoc":{requiresArg:!0,argError:"Usage: inject-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return U(e,{dryRun:s})}},"strip-jsdoc":{requiresArg:!0,argError:"Usage: strip-jsdoc <path> [--dry-run]",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return P(e,{dryRun:s})}},"validate-ctx":{requiresArg:!0,argError:"Usage: validate-ctx <path> [--strict]",handler:async r=>{const e=q(r[0]),s=r.includes("--strict");return k(e,{strict:s})}},mode:{requiresArg:!0,argError:"Usage: mode <path>",handler:async r=>{const e=q(r[0]),s=v(e);return{...s,description:D(s.mode),workflow:I(s.mode)}}},"compact-migrate":{requiresArg:!0,argError:"Usage: compact-migrate <path>",handler:async r=>{const e=q(r[0]),s=r.includes("--dry-run");return F(e,{dryRun:s})}},"set-mode":{requiresArg:!0,argError:"Usage: set-mode <path> <1|2>",handler:async r=>{const e=q(r[0]),s=parseInt(r[1],10);if(!s||![1,2].includes(s))throw new Error("Mode must be 1 (compact) or 2 (full)");return C(e,{mode:s})}}};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ctx .context/src/compact/ai-context.ctx
|
|
2
|
-
import{resolve as e,extname as t}from"path";import{getSkeleton as s,getGraph as o}from"../mcp/tools.js";import{getProjectDocs as n}from"./doc-dialect.js";import{compressFile as i}from"./compress.js";import{findJSFiles as r}from"../core/parser.js";
|
|
3
|
-
const c=new Set([".js",".mjs",".ts",".tsx"]);
|
|
2
|
+
import{estimateTokens}from"../core/utils.js";import{resolve as e,extname as t}from"path";import{getSkeleton as s,getGraph as o}from"../mcp/tools.js";import{getProjectDocs as n}from"./doc-dialect.js";import{compressFile as i}from"./compress.js";import{findJSFiles as r}from"../core/parser.js";
|
|
3
|
+
const c=new Set([".js",".mjs",".ts",".tsx"]);
|
|
4
4
|
export async function getAiContext(a,l={}){const{includeFiles:f=[],includeDocs:m=!0,includeSkeleton:d=!0}=l,p=e(a),u={};
|
|
5
5
|
let g=0;if(d&&(u.skeleton=await s(p),g+=estimateTokens(u.skeleton)),m){const e=await o(p);u.docs=n(e,p),g+=estimateTokens(u.docs)}if(f.length>0){u.files={};
|
|
6
6
|
const e=r(p);for(const s of f){const o=e.find(e=>e.endsWith(s)||e.endsWith("/"+s));if(!o){u.files[s]={error:`File not found: ${s}`};continue}const n=t(o).toLowerCase();if(c.has(n))try{const e=await i(o,{beautify:!0,legend:!0});u.files[s]=e.code,g+=e.compressed}catch(e){u.files[s]={error:e.message}}else u.files[s]={error:`Unsupported file type: ${n}`}}}const h=r(p);
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
// @ctx .context/src/compact/compact-migrate.ctx
|
|
2
|
-
import{readFileSync as
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const n=
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
function
|
|
9
|
-
function
|
|
10
|
-
function S(e){return s(e)}
|
|
11
|
-
export async function compactMigrate(e,t={}){const{dryRun:n=!1}=t;checkGitClean(e);
|
|
12
|
-
const o=walkJS(e),p=[];for(const t of o){const n=a(e,t),o=await extractNames(t),s=R(t,"utf-8");p.push({file:n,namesCount:o.length,names:o,originalSize:s.length})}
|
|
13
|
-
if(n)return{dryRun:!0,files:p.length,fileSummary:p.map(e=>({file:e.file,identifiers:e.namesCount,originalSize:e.originalSize}))};
|
|
14
|
-
const v=await d(e,{dryRun:!1});for(const t of o){const n=R(t,"utf-8"),o=p.find(e=>t.endsWith(e.file));if(o&&o.names.length>0){const s=buildNamesDirective(o.names,n);s&&updateCtxNames(t,s,e)}}
|
|
15
|
-
let b;try{b=await m(e,{strict:!1})}catch(e){b={status:"SKIP",reason:e.message}}
|
|
16
|
-
f(e,{mode:1});
|
|
17
|
-
return{migrated:!0,files:v.files,savings:v.savings,originalBytes:v.originalBytes,compactedBytes:v.compactedBytes,validation:b?.status||"SKIP",mode:"compact (1)",hint:"Run 'expand_project' to generate .expanded/ cache for human review"}}
|
|
2
|
+
import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as e,writeFileSync as t,existsSync as s}from"fs";import{join as o,extname as i,relative as r,basename as c,dirname as l}from"path";import{execSync as m}from"child_process";import{compactProject as f}from"./compact.js";import{validatePipeline as d}from"./validate-pipeline.js";import{setConfig as u}from"./mode-config.js";
|
|
3
|
+
const p=new Set([".js",".mjs"]),g=new Set(["node_modules",".git","vendor",".context","dev-docs",".agent",".agents",".expanded"]);
|
|
4
|
+
|
|
5
|
+
async function y(t){const n=e(t,"utf-8"),a=new Set;try{const{parse:e}=await import("../../vendor/acorn.mjs"),{simple:t}=await import("../../vendor/walk.mjs");t(e(n,{ecmaVersion:"latest",sourceType:"module"}),{FunctionDeclaration(e){e.id?.name&&a.add(e.id.name)},ClassDeclaration(e){e.id?.name&&a.add(e.id.name)},VariableDeclarator(e){e.id?.name&&a.add(e.id.name)},ImportSpecifier(e){e.local?.name&&a.add(e.local.name)},ImportDefaultSpecifier(e){e.local?.name&&a.add(e.local.name)},AssignmentExpression(e){"Identifier"===e.left?.type&&a.add(e.left.name)}})}catch{const e=/(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))|(?:const|let|var)\s+(\w+))/g;let t;for(;t=e.exec(n);){const e=t[1]||t[2]||t[3];e&&e.length>1&&a.add(e)}}return[...a].filter(e=>e.length>1)}
|
|
6
|
+
function w(e,t){const n=t.split("\n"),a=new Set,s=/(?:^|[^a-zA-Z_$])([a-z])(?:\s*[=,)}\];:]|$)/g;for(const e of n){let t;for(;t=s.exec(e);)a.add(t[1])}const o=new Map;for(const t of e){const e=t.charAt(0).toLowerCase();a.has(e)&&!o.has(e)&&o.set(e,t)}return 0===o.size?null:"@names "+[...o.entries()].map(([e,t])=>`${e}=${t}`).join(",")}
|
|
7
|
+
function S(n,a,s){const m=r(s,n),f=c(m,i(m))+".ctx",d=l(m),u=o(s,".context",d,f);if(x(u))try{let n=e(u,"utf-8");if(n.includes("@names"))n=n.replace(/@names .*/,a);else{const e=n.indexOf("\n");n=-1===e?a+"\n"+n:n.slice(0,e+1)+a+"\n"+n.slice(e+1)}t(u,n,"utf-8")}catch{}}
|
|
8
|
+
function x(e){return s(e)}
|
|
9
|
+
export async function compactMigrate(t,n={}){const{dryRun:a=!1}=n;!function(e){try{const t=m("git status --porcelain",{cwd:e,encoding:"utf-8"}).trim();if(t)throw new Error("Working directory is not clean. Commit or stash changes first.\n\nDirty files:\n"+t)}catch(e){if(e.message.includes("not clean"))throw e;throw new Error("Not a git repository or git not available: "+e.message)}}(t);const s=h(t),o=[];for(const n of s){const a=r(t,n),s=await y(n),i=e(n,"utf-8");o.push({file:a,namesCount:s.length,names:s,originalSize:i.length})}if(a)return{dryRun:!0,files:o.length,fileSummary:o.map(e=>({file:e.file,identifiers:e.namesCount,originalSize:e.originalSize}))};const i=await f(t,{dryRun:!1});for(const n of s){const a=e(n,"utf-8"),s=o.find(e=>n.endsWith(e.file));if(s&&s.names.length>0){const e=w(s.names,a);e&&S(n,e,t)}}let c;try{c=await d(t,{strict:!1})}catch(t){c={status:"SKIP",reason:t.message}}return u(t,{mode:1}),{migrated:!0,files:i.files,savings:i.savings,originalBytes:i.originalBytes,compactedBytes:i.compactedBytes,validation:c?.status||"SKIP",mode:"compact (1)",hint:"Run 'expand_project' to generate .expanded/ cache for human review"}}
|
package/src/compact/compact.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// @ctx .context/src/compact/compact.ctx
|
|
2
|
-
import{readFileSync as e,writeFileSync as t,
|
|
3
|
-
|
|
4
|
-
const n=r(e,a);o(n).isDirectory()?f.has(a)||s.push(...walkJSFiles(n,t)):d.has(c(a).toLowerCase())&&s.push(n)}}catch{}return s}
|
|
2
|
+
import{resolveCtxRelPath as resolveCtxPath}from"./ctx-resolver.js";import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as e,writeFileSync as t,existsSync as s}from"fs";import{join as r,extname as c,relative as a,basename as i,dirname as l}from"path";import{minify as u}from"../../vendor/terser.mjs";
|
|
3
|
+
|
|
5
4
|
function addTopLevelNewlines(e){return e.replace(/;(import )/g,";\n$1").replace(/;(export )/g,";\n$1").replace(/\}(export )/g,"}\n$1").replace(/\}(function )/g,"}\n$1").replace(/\}(async function )/g,"}\n$1").replace(/\}(class )/g,"}\n$1").replace(/;(const |let |var )/g,";\n$1")}
|
|
6
|
-
|
|
7
|
-
const f=r(t,u,o);return s(f)?u+"/"+o:null}
|
|
5
|
+
|
|
8
6
|
async function compactFile(n,o){const s=e(n,"utf-8"),r=s.length;if(!s.trim())return{original:0,compacted:0};
|
|
9
7
|
const c=await u(s,{compress:{dead_code:!0,drop_console:!1,passes:1,reduce_funcs:!1,inline:!1},mangle:{keep_fnames:!0,module:!0},module:!0,output:{beautify:!1,comments:!1,semicolons:!0}});if(c.error)throw c.error;
|
|
10
8
|
let a=addTopLevelNewlines(c.code);if(o){const e=resolveCtxPath(n,o);e&&(a.startsWith("#!")?a=a.replace(/^(#![^\n]*\n)/,"$1// @ctx "+e+"\n"):a="// @ctx "+e+"\n"+a)}return t(n,a,"utf-8"),{original:r,compacted:a.length}}
|
package/src/compact/compress.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
// @ctx .context/src/compact/compress.ctx
|
|
2
|
-
import{readFileSync as e}from"fs";import{basename as t,extname as n}from"path";import{minify as a}from"../../vendor/terser.mjs";import{parse as s}from"../../vendor/acorn.mjs";import{simple as r}from"../../vendor/walk.mjs";
|
|
3
|
-
const o=new Set([".js",".mjs",".ts",".tsx"]);
|
|
4
|
-
|
|
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:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let p;try{const e=await a(l,d);if(e.error)throw e.error;p=e.code}catch(e){p=l.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}// Join consecutive import lines into single line
|
|
10
|
-
p=p.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");const f=i?extractLegend(l,t):"",u=f?`/*\n${f}\n*/\n${p}`:p,y=estimateTokens(u);return{code:u,legend:f,original:m,compressed:y,savings:`${m>0?Math.round(100*(1-y/m)):0}%`}}
|
|
11
|
-
export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,l=e(t,"utf-8");
|
|
12
|
-
let m;try{m=s(l,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){throw new Error(`Failed to parse ${t}: ${e.message}`)}const d=findSymbolRange(m,l,n);if(!d)throw new Error(`Symbol "${n}" not found in ${t}`);
|
|
13
|
-
let p=l.slice(0,d.start)+r+l.slice(d.end);if(i)try{const e=await a(p,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!0,semicolons:!1}});e.code&&(p=e.code)}catch{}try{s(p,{ecmaVersion:"latest",sourceType:"module"})}catch(e){throw new Error(`Edit would create invalid syntax: ${e.message}`)}if(!c){const{writeFileSync:e}=await import("fs");e(t,p,"utf-8")}return{success:!0,file:t,symbol:n,oldRange:{start:d.start,end:d.end},newLength:r.length,...c?{dryRun:!0}:{}}}
|
|
14
|
-
function findSymbolRange(e,t,n){let a=null;return r(e,{FunctionDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"FunctionDeclaration"})},ClassDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"ClassDeclaration"})},VariableDeclaration(e){for(const t of e.declarations)t.id?.name===n&&(a={start:e.start,end:e.end,type:"VariableDeclaration"})},ExportNamedDeclaration(e){if(e.declaration){const t=e.declaration;(t.id?.name||t.declarations?.[0]?.id?.name)===n&&(a={start:e.start,end:e.end,type:"ExportNamedDeclaration"})}},ExportDefaultDeclaration(e){e.declaration?.id?.name===n&&(a={start:e.start,end:e.end,type:"ExportDefaultDeclaration"})}}),a}
|
|
2
|
+
import{estimateTokens as i}from"../core/utils.js";import{readFileSync as e}from"fs";import{basename as t,extname as n}from"path";import{minify as a}from"../../vendor/terser.mjs";import{parse as s}from"../../vendor/acorn.mjs";import{simple as r}from"../../vendor/walk.mjs";
|
|
3
|
+
const o=new Set([".js",".mjs",".ts",".tsx"]);
|
|
4
|
+
|
|
5
|
+
function c(e,n){const a=[];a.push(`--- ${t(n)} ---`);try{const t=s(e,{ecmaVersion:2022,sourceType:"module",locations:!0});r(t,{ExportNamedDeclaration(t){const n=t.declaration;if(!n)return;let s="";if(t.start>0){const n=Math.max(0,t.start-500),a=e.slice(n,t.start).trimEnd().match(/\/\*\*[\s\S]*?\*\/\s*$/);if(a&&e.slice(n+a.index+a[0].length,t.start).split("\n").length<=3){const e=a[0].replace(/\/\*\*\s*\n?/,"").replace(/\s*\*\//,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"").trim()).filter(e=>e&&!e.startsWith("@")).join(" ").trim();e&&(s=e.length>80?e.slice(0,77)+"...":e)}}if("FunctionDeclaration"===n.type){const e=n.id?.name||"anonymous",t=n.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(","),r=`${n.async?"async ":""}${e}(${t})`;a.push(s?`${r}|${s}`:r)}if("ClassDeclaration"===n.type){const e=n.id?.name||"AnonymousClass",t=n.superClass?` extends ${n.superClass.name||"?"}`:"";a.push(`class ${e}${t}${s?"|"+s:""}`);for(const e of n.body.body)if("MethodDefinition"===e.type&&e.key?.name){const t=e.value.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"...").join(",");a.push(` .${e.key.name}(${t})`)}}if("VariableDeclaration"===n.type)for(const e of n.declarations)e.id?.name&&a.push(`${n.kind} ${e.id.name}${s?"|"+s:""}`)}})}catch(e){a.push(`PARSE_ERROR: ${e.message}`)}return a.join("\n")}
|
|
6
|
+
export async function compressFile(t,s={}){const{beautify:r=!0,legend:l=!0}=s,m=n(t).toLowerCase();if(!o.has(m))throw new Error(`Unsupported file type: ${m}. Supported: ${[...o].join(", ")}`);const d=e(t,"utf-8"),p=i(d);if(!d.trim())return{code:"",legend:"",original:0,compressed:0,savings:"0%"};const u={compress:{dead_code:!0,drop_console:!1,passes:2},mangle:!1,module:!0,output:{beautify:r,comments:!1,semicolons:!r}};let f;try{const e=await a(d,u);if(e.error)throw e.error;f=e.code}catch(e){f=d.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*/g,"").replace(/\n{3,}/g,"\n\n").trim()}f=f.replace(/(import\s.+?;)\n+(?=import\s)/g,"$1");const y=l?c(d,t):"",h=y?`/*\n${y}\n*/\n${f}`:f,$=i(h);return{code:h,legend:y,original:p,compressed:$,savings:`${p>0?Math.round(100*(1-$/p)):0}%`}}
|
|
7
|
+
export async function editCompressed(t,n,r,o={}){const{beautify:i=!0,dryRun:c=!1}=o,m=e(t,"utf-8");let d;try{d=s(m,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch(e){throw new Error(`Failed to parse ${t}: ${e.message}`)}const p=l(d,m,n);if(!p)throw new Error(`Symbol "${n}" not found in ${t}`);let u=m.slice(0,p.start)+r+m.slice(p.end);if(i)try{const e=await a(u,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!0,semicolons:!1}});e.code&&(u=e.code)}catch{}try{s(u,{ecmaVersion:"latest",sourceType:"module"})}catch(e){throw new Error(`Edit would create invalid syntax: ${e.message}`)}if(!c){const{writeFileSync:e}=await import("fs");e(t,u,"utf-8")}return{success:!0,file:t,symbol:n,oldRange:{start:p.start,end:p.end},newLength:r.length,...c?{dryRun:!0}:{}}}
|
|
8
|
+
function l(e,t,n){let a=null;return r(e,{FunctionDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"FunctionDeclaration"})},ClassDeclaration(e){e.id?.name===n&&(a={start:e.start,end:e.end,type:"ClassDeclaration"})},VariableDeclaration(e){for(const t of e.declarations)t.id?.name===n&&(a={start:e.start,end:e.end,type:"VariableDeclaration"})},ExportNamedDeclaration(e){if(e.declaration){const t=e.declaration;(t.id?.name||t.declarations?.[0]?.id?.name)===n&&(a={start:e.start,end:e.end,type:"ExportNamedDeclaration"})}},ExportDefaultDeclaration(e){e.declaration?.id?.name===n&&(a={start:e.start,end:e.end,type:"ExportDefaultDeclaration"})}}),a}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @ctx .context/src/compact/ctx-resolver.ctx
|
|
2
|
+
import{readFileSync as t,existsSync as s}from"fs";import{join as i,basename as a,extname as c,dirname as p,relative as l}from"path";
|
|
3
|
+
export function resolveCtxPath(e,n){const o=a(n,c(n))+".ctx",r=p(n),d=i(e,".context",r,o);if(s(d))return d;const f=i(e,r,o);return s(f)?f:null}
|
|
4
|
+
export function resolveCtxRelPath(e,n){const o=l(n,e),r=a(o,c(o))+".ctx",u=p(o),d=i(n,".context",u,r);if(s(d))return".context/"+u+"/"+r;const f=i(n,u,r);return s(f)?u+"/"+r:null}
|
|
5
|
+
export function readCtxFile(e,n){const o=resolveCtxPath(e,n);return o?t(o,"utf-8"):null}
|
|
@@ -1,29 +1,14 @@
|
|
|
1
1
|
// @ctx .context/src/compact/ctx-to-jsdoc.ctx
|
|
2
|
-
import{readFileSync as t,writeFileSync as e,readdirSync as n,statSync as s,existsSync as o}from"fs";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let p=0,f=0;
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const s=
|
|
14
|
-
const o=
|
|
15
|
-
export function stripJSDoc(n,s={}){const{dryRun:o=!1}=s,r=walkJSFiles(n);
|
|
16
|
-
let i=0,l=0;
|
|
17
|
-
const p=[];for(const s of r){const r=t(s,"utf-8"),f=[];
|
|
18
|
-
let u,m=!1;try{c(r,{ecmaVersion:"latest",sourceType:"module",onComment:f}),m=!0}catch{}if(m){const t=f.filter(t=>"Block"===t.type&&t.value.startsWith("*")).sort((t,e)=>e.start-t.start);u=r;for(const{start:e,end:n}of t){let t=n;for(;t<u.length&&("\n"===u[t]||"\r"===u[t]);)t++;u=u.slice(0,e)+u.slice(t)}}else u=r.replace(/\/\*\*[\s\S]*?\*\/\s*\n?/g,"");
|
|
19
|
-
const d=u.replace(/\n{3,}/g,"\n\n"),h=r.length-d.length;h>0&&(i++,l+=h,p.push({file:a(n,s),saved:h}),o||e(s,d,"utf-8"))}return{files:r.length,stripped:i,savedBytes:l,dryRun:o,details:p}}
|
|
20
|
-
function walkJSFiles(t){const e=[];try{for(const o of n(t)){if(o.startsWith(".")&&"."!==o)continue;
|
|
21
|
-
const n=r(t,o);s(n).isDirectory()?f.has(o)||e.push(...walkJSFiles(n)):p.has(i(o).toLowerCase())&&e.push(n)}}catch{}return e}
|
|
22
|
-
function splitTopLevelParams(t){const e=[];
|
|
23
|
-
let n=0,s="";for(const o of t)if("{"===o||"<"===o||"("===o?n++:"}"!==o&&">"!==o&&")"!==o||n--,","===o&&0===n){const t=s.trim();t&&e.push(t),s=""}else s+=o;
|
|
24
|
-
const o=s.trim();return o&&e.push(o),e}
|
|
25
|
-
export function validateCtxContracts(e,n={}){const s=n.strict||!1,o=walkJSFiles(e),r=[];
|
|
26
|
-
let i=0;for(const n of o){const o=a(e,n),p=findCtxFile(o,e);if(!p)continue;i++;
|
|
27
|
-
const f=parseCtxFile(t(p,"utf-8"));
|
|
28
|
-
let u,m;try{u=t(n,"utf-8")}catch{continue}try{m=c(u,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{continue}const d=new Map;l(m,{FunctionDeclaration(t){t.id&&d.set(t.id.name,{paramCount:t.params.length,params:t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&t.left?.name?t.left.name:"RestElement"===t.type&&t.argument?.name?t.argument.name:"ObjectPattern"===t.type?"options":"?"),async:t.async||!1,line:t.loc.start.line})}});
|
|
29
|
-
const h=new Set;l(m,{ExportNamedDeclaration(t){if(t.declaration?.id&&h.add(t.declaration.id.name),t.specifiers)for(const e of t.specifiers)h.add(e.exported.name)}});for(const t of f.functions){const e=d.get(t.name);if(!e){r.push({file:o,severity:t.exported?"error":"warning",message:`Function "${t.name}" in .ctx not found in source`});continue}const n=t.params?splitTopLevelParams(t.params):[];n.length!==e.paramCount&&r.push({file:o,severity:"error",message:`"${t.name}": .ctx has ${n.length} params, AST has ${e.paramCount}`});for(let s=0;s<Math.min(n.length,e.params.length);s++){const i=n[s].replace(/^\.\.\./,"").replace(/:.*/,"").replace(/=$/,""),a=e.params[s];i!==a&&"?"!==i&&"?"!==a&&r.push({file:o,severity:"warning",message:`"${t.name}" param ${s}: .ctx="${i}", AST="${a}"`})}const s=h.has(t.name);t.exported!==s&&r.push({file:o,severity:"warning",message:`"${t.name}": .ctx says ${t.exported?"exported":"private"}, AST says ${s?"exported":"private"}`}),d.delete(t.name)}if(s&&d.size>0)for(const[t]of d)r.push({file:o,severity:"info",message:`Function "${t}" in source (line ${d.get(t)?.line}) not documented in .ctx`})}const p=r.filter(t=>"error"===t.severity).length,f=r.filter(t=>"warning"===t.severity).length;return{files:i,violations:r,summary:{errors:p,warnings:f}}}
|
|
2
|
+
import{walkJSFiles}from"../core/file-walker.js";import{resolveCtxPath}from"./ctx-resolver.js";import{buildJSDocFromRaw as u}from"./jsdoc-builder.js";import{readFileSync as t,writeFileSync as e,readdirSync as n,statSync as s,existsSync as o}from"fs";
|
|
3
|
+
import{join as r,extname as i,relative as a}from"path";
|
|
4
|
+
import{parse as c}from"../../vendor/acorn.mjs";
|
|
5
|
+
import{simple as l}from"../../vendor/walk.mjs";
|
|
6
|
+
|
|
7
|
+
export function parseCtxFile(t){const e=t.split("\n"),n={file:null,functions:[]};for(const t of e){const e=t.match(/^--- (.+) ---$/);if(e){n.file=e[1];continue}const s=t.match(/^(export\s+)?(\w+)\(([^)]*)\)((?:→[^→|]+)*)(?:\|(.*))?$/);if(s){const[,t,e,o,r,i]=s;let a="";if(r){const t=r.split("→").filter(Boolean);t.length>0&&/^[A-Z]|^Promise|^Array|^Object|^string|^number|^boolean|^void|^null/.test(t[0])&&(a=t[0])}const c=i&&"{DESCRIBE}"!==i?i.trim():"";n.functions.push({name:e,params:o||"",exported:!!t,description:c,returns:a});continue}t.match(/^class\s+(\w+)/)}return n}
|
|
8
|
+
|
|
9
|
+
const m=(t,e)=>resolveCtxPath(e,t);
|
|
10
|
+
export function injectJSDoc(n,s={}){const{dryRun:o=!1}=s,r=n,i=d(n);let p=0,f=0;const h=[];for(const y of i){const g=a(r,y),x=m(g,r);if(!x){f++;continue}const $=parseCtxFile(t(x,"utf-8"));if(0===$.functions.length){f++;continue}let v,w=t(y,"utf-8"),S=!1,j=0;try{v=c(w,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{f++;continue}const C=[];function F(t){for(const e of v.body)if("ExportNamedDeclaration"===e.type&&e.declaration===t)return e.start;return t.start}l(v,{FunctionDeclaration(t){if(!t.id)return;const e=t.id.name,n=$.functions.find(t=>t.name===e);if(!n)return;const s=F(t);if(w.slice(0,s).trimEnd().endsWith("*/"))return;const o=u(n);C.push({position:s,jsdoc:o}),j++}}),C.sort((t,e)=>e.position-t.position);for(const{position:D,jsdoc:A}of C){const E=w.slice(0,D).lastIndexOf("\n")+1,R=w.slice(E,D).match(/^(\s*)/)?.[1]||"",T=A.split("\n").map(t=>R+t).join("\n")+"\n";w=w.slice(0,D)+T+w.slice(D),S=!0}S&&!o&&e(y,w,"utf-8"),j>0&&(p+=j,h.push({file:g,injected:j}))}return{files:i.length,injected:p,skipped:f,dryRun:o,details:h}}
|
|
11
|
+
export function stripJSDoc(n,s={}){const{dryRun:o=!1}=s,r=d(n);let i=0,l=0;const p=[];for(const s of r){const r=t(s,"utf-8"),f=[];let u,m=!1;try{c(r,{ecmaVersion:"latest",sourceType:"module",onComment:f}),m=!0}catch{}if(m){const t=f.filter(t=>"Block"===t.type&&t.value.startsWith("*")).sort((t,e)=>e.start-t.start);u=r;for(const{start:e,end:n}of t){let t=n;for(;t<u.length&&("\n"===u[t]||"\r"===u[t]);)t++;u=u.slice(0,e)+u.slice(t)}}else u=r.replace(/\/\*\*[\s\S]*?\*\/\s*\n?/g,"");const d=u.replace(/\n{3,}/g,"\n\n"),h=r.length-d.length;h>0&&(i++,l+=h,p.push({file:a(n,s),saved:h}),o||e(s,d,"utf-8"))}return{files:r.length,stripped:i,savedBytes:l,dryRun:o,details:p}}
|
|
12
|
+
const d=walkJSFiles;
|
|
13
|
+
function h(t){const e=[];let n=0,s="";for(const o of t)if("{"===o||"<"===o||"("===o?n++:"}"!==o&&">"!==o&&")"!==o||n--,","===o&&0===n){const t=s.trim();t&&e.push(t),s=""}else s+=o;const o=s.trim();return o&&e.push(o),e}
|
|
14
|
+
export function validateCtxContracts(e,n={}){const s=n.strict||!1,o=d(e),r=[];let i=0;for(const n of o){const o=a(e,n),p=m(o,e);if(!p)continue;i++;const f=parseCtxFile(t(p,"utf-8"));let u,d;try{u=t(n,"utf-8")}catch{continue}try{d=c(u,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{continue}const y=new Map;l(d,{FunctionDeclaration(t){t.id&&y.set(t.id.name,{paramCount:t.params.length,params:t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&t.left?.name?t.left.name:"RestElement"===t.type&&t.argument?.name?t.argument.name:"ObjectPattern"===t.type?"options":"?"),async:t.async||!1,line:t.loc.start.line})}});const g=new Set;l(d,{ExportNamedDeclaration(t){if(t.declaration?.id&&g.add(t.declaration.id.name),t.specifiers)for(const e of t.specifiers)g.add(e.exported.name)}});for(const t of f.functions){const e=y.get(t.name);if(!e){r.push({file:o,severity:t.exported?"error":"warning",message:`Function "${t.name}" in .ctx not found in source`});continue}const n=t.params?h(t.params):[];n.length!==e.paramCount&&r.push({file:o,severity:"error",message:`"${t.name}": .ctx has ${n.length} params, AST has ${e.paramCount}`});for(let s=0;s<Math.min(n.length,e.params.length);s++){const i=n[s].replace(/^\.\.\./,"").replace(/:.*/,"").replace(/=$/,""),a=e.params[s];i!==a&&"?"!==i&&"?"!==a&&r.push({file:o,severity:"warning",message:`"${t.name}" param ${s}: .ctx="${i}", AST="${a}"`})}const s=g.has(t.name);t.exported!==s&&r.push({file:o,severity:"warning",message:`"${t.name}": .ctx says ${t.exported?"exported":"private"}, AST says ${s?"exported":"private"}`}),y.delete(t.name)}if(s&&y.size>0)for(const[t]of y)r.push({file:o,severity:"info",message:`Function "${t}" in source (line ${y.get(t)?.line}) not documented in .ctx`})}const p=r.filter(t=>"error"===t.severity).length,f=r.filter(t=>"warning"===t.severity).length;return{files:i,violations:r,summary:{errors:p,warnings:f}}}
|