import fs from 'node:fs'; import path from 'node:path'; import ts from 'typescript'; const ROOT = path.resolve(import.meta.dirname, '..'); const PACKAGES = ['packages/agent-core-v2', 'packages/kap-server', 'packages/transcript']; const DIRS = ['src', 'test', 'scripts']; function collectLeaves(node, leaves, jsdocNodes) { if (ts.isJSDoc(node)) { jsdocNodes.push(node); return; } const children = node.getChildren(); if (children.length === 0) { if (node.getWidth() > 0) leaves.push(node); return; } for (const c of children) collectLeaves(c, leaves, jsdocNodes); } function extractGapComments(gap, offset, out) { let i = 0; while (i < gap.length) { const ch = gap[i]; if (' \t\n\r\f\v'.includes(ch)) { i++; continue; } if (ch === '/' && gap[i + 1] === '/') { let j = gap.indexOf('\n', i); if (j === -1) j = gap.length; out.push({ pos: offset + i, end: offset + j, jsdoc: false }); i = j; continue; } if (ch === '/' && gap[i + 1] === '*') { const close = gap.indexOf('*/', i + 2); const j = close === -1 ? gap.length : close + 2; out.push({ pos: offset + i, end: offset + j, jsdoc: gap.startsWith('/**', i) }); i = j; continue; } break; } } function checkFile(file) { const text = fs.readFileSync(file, 'utf8'); const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); const leaves = []; const jsdocNodes = []; collectLeaves(sf, leaves, jsdocNodes); leaves.sort((a, b) => a.getStart(sf) - b.getStart(sf)); const comments = []; let cursor = 0; if (text.startsWith('#!')) { const nl = text.indexOf('\n'); cursor = nl === -1 ? text.length : nl + 1; } for (const leaf of leaves) { const s = leaf.getStart(sf); if (s > cursor) extractGapComments(text.slice(cursor, s), cursor, comments); cursor = Math.max(cursor, leaf.getEnd()); } if (cursor < text.length) extractGapComments(text.slice(cursor), cursor, comments); for (const d of jsdocNodes) { comments.push({ pos: d.getStart(sf), end: d.getEnd(), jsdoc: true }); } const seen = new Set(); const violations = []; for (const c of comments) { const key = `${c.pos}:${c.end}`; if (seen.has(key)) continue; seen.add(key); const line = text.slice(0, c.pos).split('\n').length; const snippet = text.slice(c.pos, Math.min(c.end, c.pos + 60)).replaceAll(/\s+/g, ' '); if (/(?:oxlint|eslint)-disable/.test(snippet)) continue; const isDirective = /@ts-(expect-error|ignore|nocheck)|prettier-ignore|istanbul|c8 ignore/.test( snippet, ); violations.push({ line, snippet, isDirective, jsdoc: c.jsdoc }); } return violations; } const files = []; for (const pkg of PACKAGES) { for (const dir of DIRS) { const root = path.join(ROOT, pkg, dir); if (!fs.existsSync(root)) continue; const stack = [root]; while (stack.length > 0) { const d = stack.pop(); for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) { if (e.name !== 'node_modules') stack.push(p); } else if (/\.(ts|tsx|mts|mjs)$/.test(e.name)) { files.push(p); } } } } } let total = 0; for (const f of files) { const violations = checkFile(f); for (const v of violations) { total++; const rel = path.relative(ROOT, f); if (v.isDirective) { console.error(`${rel}:${v.line}: tooling directives are not allowed — fix the underlying lint/type problem instead: ${v.snippet}`); } else if (v.jsdoc) { console.error(`${rel}:${v.line}: JSDoc is not allowed in this package: ${v.snippet}`); } else { console.error(`${rel}:${v.line}: comments are not allowed in this package: ${v.snippet}`); } } } if (total > 0) { console.error(`check-no-comments: ${total} violation(s) in ${PACKAGES.join(', ')}`); process.exit(1); } console.log(`check-no-comments: OK (${files.length} files)`);