diff --git a/docs/reports/hardening-inventory.json b/docs/reports/hardening-inventory.json index 2a6cb358..e110b57e 100644 --- a/docs/reports/hardening-inventory.json +++ b/docs/reports/hardening-inventory.json @@ -1,118 +1,11 @@ { "scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}", "syncFs": { - "totalOccurrences": 2358, - "filesAffected": 253, - "hotpathOccurrences": 1991, - "hotpathFilesAffected": 202, + "totalOccurrences": 2378, + "filesAffected": 254, + "hotpathOccurrences": 1102, + "hotpathFilesAffected": 150, "topHotpathFiles": [ - { - "file": "src/cliproxy/__tests__/pool-routing-phase3.test.ts", - "count": 96, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readFileSync", - "rmSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/config/__tests__/config-generator.test.js", - "count": 88, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readFileSync", - "rmSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/config/__tests__/claude-model-neutral.test.ts", - "count": 63, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readFileSync", - "rmSync", - "statSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts", - "count": 48, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readFileSync", - "rmSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/accounts/__tests__/account-registry-integrity.test.ts", - "count": 45, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readdirSync", - "readFileSync", - "rmSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/executor/__tests__/variant-port-integration.test.js", - "count": 36, - "calls": [ - "existsSync", - "mkdirSync", - "readdirSync", - "readFileSync", - "rmSync", - "unlinkSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/executor/__tests__/composite-variant-service.test.ts", - "count": 33, - "calls": [ - "existsSync", - "mkdirSync", - "mkdtempSync", - "readFileSync", - "rmSync", - "writeFileSync" - ], - "markers": [] - }, - { - "file": "src/cliproxy/executor/__tests__/variant-port-edge-cases.test.js", - "count": 33, - "calls": [ - "existsSync", - "mkdirSync", - "readdirSync", - "rmSync", - "unlinkSync", - "writeFileSync" - ], - "markers": [] - }, { "file": "src/utils/browser/mcp-installer.ts", "count": 32, @@ -130,14 +23,136 @@ "markers": [] }, { - "file": "src/cliproxy/__tests__/session-tracker-port.test.js", - "count": 31, + "file": "src/utils/image-analysis/mcp-installer.ts", + "count": 30, + "calls": [ + "chmodSync", + "copyFileSync", + "existsSync", + "mkdirSync", + "readFileSync", + "renameSync", + "statSync", + "unlinkSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/utils/claude-symlink-manager.ts", + "count": 27, + "calls": [ + "copyFileSync", + "existsSync", + "lstatSync", + "mkdirSync", + "readdirSync", + "readlinkSync", + "renameSync", + "rmSync", + "statSync", + "symlinkSync", + "unlinkSync" + ], + "markers": [] + }, + { + "file": "src/cliproxy/config/env-builder.ts", + "count": 25, "calls": [ "existsSync", "mkdirSync", - "readdirSync", "readFileSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/management/shared-manager/migrations.ts", + "count": 25, + "calls": [ + "copyFileSync", + "cpSync", + "existsSync", + "lstatSync", + "mkdirSync", + "readdirSync", + "symlinkSync", + "unlinkSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/utils/websearch/mcp-installer.ts", + "count": 25, + "calls": [ + "chmodSync", + "copyFileSync", + "existsSync", + "mkdirSync", + "readFileSync", + "renameSync", + "statSync", + "unlinkSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/cliproxy/services/variant-settings.ts", + "count": 23, + "calls": [ + "existsSync", + "mkdirSync", + "readFileSync", + "renameSync", + "unlinkSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/management/recovery-manager.ts", + "count": 23, + "calls": [ + "copyFileSync", + "existsSync", + "lstatSync", + "mkdirSync", + "renameSync", + "statSync", + "unlinkSync", + "writeFileSync" + ], + "markers": [] + }, + { + "file": "src/utils/shell-completion.ts", + "count": 23, + "calls": [ + "appendFileSync", + "copyFileSync", + "existsSync", + "mkdirSync", + "readFileSync", + "statSync" + ], + "markers": [] + }, + { + "file": "src/utils/claude-dir-installer.ts", + "count": 21, + "calls": [ + "copyFileSync", + "cpSync", + "existsSync", + "lstatSync", + "mkdirSync", + "readdirSync", + "renameSync", "rmSync", + "statSync", "unlinkSync", "writeFileSync" ], @@ -285,8 +300,8 @@ ] }, "legacyShim": { - "totalMarkers": 454, - "filesAffected": 170, + "totalMarkers": 456, + "filesAffected": 171, "topFiles": [ { "file": "src/auth/profile-detector.ts", @@ -476,11 +491,11 @@ }, "maintainability": { "typedErrors": { - "totalThrows": 440, - "typedThrows": 68, + "totalThrows": 446, + "typedThrows": 74, "plainThrows": 312, "otherThrows": 60, - "adoptionRatio": 0.1545, + "adoptionRatio": 0.1659, "topSubdomainsByThrows": [ { "subdomain": "web-server", @@ -558,8 +573,8 @@ }, "loggerCoverage": { "filesWithCreateLogger": 65, - "totalSourceFiles": 751, - "coverageRatio": 0.0866, + "totalSourceFiles": 752, + "coverageRatio": 0.0864, "subdomainsWithZeroCreateLogger": [ "api", "bin", @@ -728,7 +743,11 @@ }, { "file": "src/web-server/routes/settings-routes.ts", - "loc": 1041 + "loc": 1042 + }, + { + "file": "src/cliproxy/config/generator.ts", + "loc": 1034 }, { "file": "src/cliproxy/proxy/tool-sanitization-proxy.ts", @@ -738,10 +757,6 @@ "file": "src/cliproxy/config/env-builder.ts", "loc": 1017 }, - { - "file": "src/cliproxy/config/generator.ts", - "loc": 1012 - }, { "file": "src/commands/cliproxy/variant-subcommand.ts", "loc": 997 diff --git a/docs/reports/hardening-inventory.md b/docs/reports/hardening-inventory.md index b708692d..1fc5fb2f 100644 --- a/docs/reports/hardening-inventory.md +++ b/docs/reports/hardening-inventory.md @@ -6,27 +6,27 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | Metric | Value | |---|---:| -| Sync fs occurrences (all) | 2358 | -| Sync fs files affected (all) | 253 | -| Sync fs occurrences (runtime hotpaths) | 1991 | -| Sync fs files affected (runtime hotpaths) | 202 | -| Legacy shim markers | 454 | -| Legacy shim files affected | 170 | +| Sync fs occurrences (all) | 2378 | +| Sync fs files affected (all) | 254 | +| Sync fs occurrences (runtime hotpaths) | 1102 | +| Sync fs files affected (runtime hotpaths) | 150 | +| Legacy shim markers | 456 | +| Legacy shim files affected | 171 | ## Top Runtime Hotpath Sync fs Files | File | Sync Calls | API Names | |---|---:|---| -| `src/cliproxy/__tests__/pool-routing-phase3.test.ts` | 96 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync | -| `src/cliproxy/config/__tests__/config-generator.test.js` | 88 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync | -| `src/cliproxy/config/__tests__/claude-model-neutral.test.ts` | 63 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync | -| `src/cliproxy/accounts/__tests__/account-safety-quota-exhaustion.test.ts` | 48 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync | -| `src/cliproxy/accounts/__tests__/account-registry-integrity.test.ts` | 45 | existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync | -| `src/cliproxy/executor/__tests__/variant-port-integration.test.js` | 36 | existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync | -| `src/cliproxy/executor/__tests__/composite-variant-service.test.ts` | 33 | existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync | -| `src/cliproxy/executor/__tests__/variant-port-edge-cases.test.js` | 33 | existsSync, mkdirSync, readdirSync, rmSync, unlinkSync, writeFileSync | | `src/utils/browser/mcp-installer.ts` | 32 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync | -| `src/cliproxy/__tests__/session-tracker-port.test.js` | 31 | existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync | +| `src/utils/image-analysis/mcp-installer.ts` | 30 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync | +| `src/utils/claude-symlink-manager.ts` | 27 | copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync | +| `src/cliproxy/config/env-builder.ts` | 25 | existsSync, mkdirSync, readFileSync, writeFileSync | +| `src/management/shared-manager/migrations.ts` | 25 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync, unlinkSync, writeFileSync | +| `src/utils/websearch/mcp-installer.ts` | 25 | chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync | +| `src/cliproxy/services/variant-settings.ts` | 23 | existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync | +| `src/management/recovery-manager.ts` | 23 | copyFileSync, existsSync, lstatSync, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync | +| `src/utils/shell-completion.ts` | 23 | appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync | +| `src/utils/claude-dir-installer.ts` | 21 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync | ## Top Legacy Shim Marker Files @@ -56,11 +56,11 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | Metric | Value | |---|---:| -| typed-error adoption (typed/total throws) | 15.4% (68/440) | +| typed-error adoption (typed/total throws) | 16.6% (74/446) | | typed-error adoption (P4 locked subdomains) | 91.7% (22/24), target 40% | | hotpath console.error/warn occurrences | 266 (571 total, 305 CLI-UX exempt) | | hotpath console.error/warn files | 82 | -| files with createLogger | 65/751 | +| files with createLogger | 65/752 | | subdomains with zero createLogger | 15 (api, bin, channels, cliproxy, cliproxy/accounts, cliproxy/ai-providers, cliproxy/binary, cliproxy/config, cliproxy/management, cliproxy/sync, cliproxy/types, config, dispatcher, shared, types) | | files > 400 LOC | 91 | | files > 600 LOC | 42 | @@ -95,10 +95,10 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | `src/cursor/cursor-executor.ts` | 1234 | | `src/web-server/model-pricing.ts` | 1105 | | `src/cliproxy/auth/oauth-process.ts` | 1048 | -| `src/web-server/routes/settings-routes.ts` | 1041 | +| `src/web-server/routes/settings-routes.ts` | 1042 | +| `src/cliproxy/config/generator.ts` | 1034 | | `src/cliproxy/proxy/tool-sanitization-proxy.ts` | 1020 | | `src/cliproxy/config/env-builder.ts` | 1017 | -| `src/cliproxy/config/generator.ts` | 1012 | | `src/commands/cliproxy/variant-subcommand.ts` | 997 | | `src/cliproxy/quota/quota-manager.ts` | 954 | | `src/web-server/services/codex-dashboard-service.ts` | 940 | diff --git a/scripts/hardening-inventory.js b/scripts/hardening-inventory.js index 1634b965..0cc2ca16 100644 --- a/scripts/hardening-inventory.js +++ b/scripts/hardening-inventory.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const { collectMaintainabilityMetrics } = require('./maintainability-metrics.js'); +const { isTestFile } = require('./runtime-source-classifier.js'); const ROOT_DIR = path.resolve(__dirname, '..'); const SRC_DIR = path.join(ROOT_DIR, 'src'); @@ -98,7 +99,7 @@ function isSourceFile(filePath) { } function isHotpath(filePath) { - return HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath)); + return !isTestFile(filePath) && HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath)); } function walkFiles(dirPath) { @@ -450,7 +451,9 @@ function renderMarkdown(report) { lines.push(`| Sync fs occurrences (all) | ${report.syncFs.totalOccurrences} |`); lines.push(`| Sync fs files affected (all) | ${report.syncFs.filesAffected} |`); lines.push(`| Sync fs occurrences (runtime hotpaths) | ${report.syncFs.hotpathOccurrences} |`); - lines.push(`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`); + lines.push( + `| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |` + ); lines.push(`| Legacy shim markers | ${report.legacyShim.totalMarkers} |`); lines.push(`| Legacy shim files affected | ${report.legacyShim.filesAffected} |`); lines.push(''); @@ -547,10 +550,39 @@ function renderMarkdown(report) { function main() { const report = buildReport(); + const jsonContent = JSON.stringify(report, null, 2) + '\n'; + const markdownContent = renderMarkdown(report); + const checkOnly = process.argv.includes('--check'); + + if (checkOnly) { + const staleReports = [ + [JSON_REPORT_PATH, jsonContent], + [MD_REPORT_PATH, markdownContent], + ].filter(([reportPath, expected]) => { + try { + return fs.readFileSync(reportPath, 'utf8') !== expected; + } catch { + return true; + } + }); + + if (staleReports.length > 0) { + console.error('[X] Hardening inventory does not match the current source tree:'); + for (const [reportPath] of staleReports) { + console.error(` ${relativePath(reportPath)}`); + } + console.error(' Regenerate with: bun run report:hardening'); + process.exitCode = 1; + return; + } + + console.log('[OK] Hardening inventory matches the current source tree.'); + return; + } fs.mkdirSync(REPORT_DIR, { recursive: true }); - fs.writeFileSync(JSON_REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8'); - fs.writeFileSync(MD_REPORT_PATH, renderMarkdown(report), 'utf8'); + fs.writeFileSync(JSON_REPORT_PATH, jsonContent, 'utf8'); + fs.writeFileSync(MD_REPORT_PATH, markdownContent, 'utf8'); const relJson = relativePath(JSON_REPORT_PATH); const relMd = relativePath(MD_REPORT_PATH); @@ -575,6 +607,7 @@ function main() { module.exports = { buildReport, collectSyncCallSites, + isTestFile, renderMarkdown, stripComments, }; diff --git a/scripts/maintainability-metrics.js b/scripts/maintainability-metrics.js index a9c68277..6f129f71 100644 --- a/scripts/maintainability-metrics.js +++ b/scripts/maintainability-metrics.js @@ -24,6 +24,7 @@ const fs = require('fs'); const path = require('path'); +const { isTestFile } = require('./runtime-source-classifier.js'); const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']); @@ -79,14 +80,6 @@ function isSourceFile(filePath) { return SOURCE_EXTENSIONS.has(path.extname(filePath)); } -function isTestFile(relPath) { - return ( - /(?:^|\/)(?:__tests__|tests?)\//.test(relPath) || - /\.test\./.test(relPath) || - /\.spec\./.test(relPath) - ); -} - function isCliUxExempt(relPath) { return CLI_UX_EXEMPT_PREFIXES.some(function (prefix) { return relPath.startsWith(prefix); @@ -350,6 +343,7 @@ module.exports = { classifyThrows: classifyThrows, countConsoleErrors: countConsoleErrors, hasCreateLogger: hasCreateLogger, + isTestFile: isTestFile, countLoc: countLoc, TYPED_ERROR_CLASSES: TYPED_ERROR_CLASSES, TYPED_ADOPTION_SUBDOMAINS: TYPED_ADOPTION_SUBDOMAINS, diff --git a/scripts/runtime-source-classifier.js b/scripts/runtime-source-classifier.js new file mode 100644 index 00000000..0fe3db64 --- /dev/null +++ b/scripts/runtime-source-classifier.js @@ -0,0 +1,12 @@ +function isTestFile(filePath) { + const normalizedPath = filePath.replace(/\\/g, '/'); + return ( + /(^|\/)(__tests__|__mocks__|fixtures)(\/|$)/.test(normalizedPath) || + /(^|\/)(tests?|test-fixtures)(\/|$)/.test(normalizedPath) || + /\.(test|spec)\.[^.]+$/.test(normalizedPath) + ); +} + +module.exports = { + isTestFile, +}; diff --git a/tests/unit/scripts/hardening-inventory.test.ts b/tests/unit/scripts/hardening-inventory.test.ts index b5f02281..e131a75b 100644 --- a/tests/unit/scripts/hardening-inventory.test.ts +++ b/tests/unit/scripts/hardening-inventory.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test'; -const { collectSyncCallSites } = require('../../../scripts/hardening-inventory.js'); +const { collectSyncCallSites, isTestFile } = require('../../../scripts/hardening-inventory.js'); describe('hardening-inventory sync call scanning', () => { test('ignores sync-call names inside regex literals after else', () => { @@ -34,3 +34,22 @@ describe('hardening-inventory sync call scanning', () => { expect(result.calls).toEqual(['readFileSync']); }); }); + +describe('hardening-inventory runtime file classification', () => { + test.each([ + 'src/cliproxy/__tests__/routing.test.ts', + 'src/commands/fixtures/help-output.ts', + 'src/auth/profile.spec.ts', + 'tests/unit/commands/profile.test.ts', + ])('excludes %s from runtime hotpaths', (filePath) => { + expect(isTestFile(filePath)).toBe(true); + }); + + test.each([ + 'src/cliproxy/routing/retry-settings.ts', + 'src/commands/help-command.ts', + 'src/utils/browser/mcp-installer.ts', + ])('keeps %s eligible for runtime hotpaths', (filePath) => { + expect(isTestFile(filePath)).toBe(false); + }); +}); diff --git a/tests/unit/scripts/maintainability-metrics.test.ts b/tests/unit/scripts/maintainability-metrics.test.ts index 8ca8fcfe..d1061f06 100644 --- a/tests/unit/scripts/maintainability-metrics.test.ts +++ b/tests/unit/scripts/maintainability-metrics.test.ts @@ -8,10 +8,31 @@ const { classifyThrows, countConsoleErrors, hasCreateLogger, + isTestFile, countLoc, collectMaintainabilityMetrics, } = require('../../../scripts/maintainability-metrics.js'); +describe('maintainability-metrics runtime file classification', () => { + test.each([ + 'src/cliproxy/__tests__/routing.test.ts', + 'src/commands/fixtures/help-output.ts', + 'src/auth/profile.spec.ts', + 'tests/unit/commands/profile.test.ts', + 'src\\auth\\__mocks__\\profile.ts', + ])('excludes %s from runtime metrics', (filePath) => { + expect(isTestFile(filePath)).toBe(true); + }); + + test.each([ + 'src/cliproxy/routing/retry-settings.ts', + 'src/commands/help-command.ts', + 'src/utils/browser/mcp-installer.ts', + ])('keeps %s in runtime metrics', (filePath) => { + expect(isTestFile(filePath)).toBe(false); + }); +}); + describe('maintainability-metrics.classifyThrows', () => { test('counts plain Error, typed, and other throws; ignores re-throws', () => { const source = [ @@ -94,14 +115,18 @@ describe('maintainability-metrics.collectMaintainabilityMetrics (fixtures tree)' fs.mkdirSync(path.join(root, 'src', 'commands'), { recursive: true }); fs.writeFileSync( path.join(root, 'src', 'commands', 'b.ts'), - ['console.error(\'cli print\');', "throw new Error('plain');"].join('\n') + ["console.error('cli print');", "throw new Error('plain');"].join('\n') ); // src/cliproxy/quota/q.ts: createLogger present, 1 plain throw, 0 console.error fs.mkdirSync(path.join(root, 'src', 'cliproxy', 'quota'), { recursive: true }); fs.writeFileSync( path.join(root, 'src', 'cliproxy', 'quota', 'q.ts'), - ["import { createLogger } from '../../../services/logging';", 'const logger = createLogger();', "throw new Error('quota plain');"].join('\n') + [ + "import { createLogger } from '../../../services/logging';", + 'const logger = createLogger();', + "throw new Error('quota plain');", + ].join('\n') ); // non-source file is ignored