From b1181e455d990ce4533e748f5005153868c48bb0 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Thu, 2 Jul 2026 21:38:33 -0700 Subject: [PATCH 1/9] added code complexity report --- ts/.gitignore | 3 + ts/package.json | 3 +- ts/pnpm-lock.yaml | 587 +++++++++++++++- ts/tools/package.json | 5 +- ts/tools/scripts/code/complexityReport.ts | 771 ++++++++++++++++++++++ 5 files changed, 1358 insertions(+), 11 deletions(-) create mode 100644 ts/tools/scripts/code/complexityReport.ts diff --git a/ts/.gitignore b/ts/.gitignore index f5813553e3..8c3ab39a4c 100644 --- a/ts/.gitignore +++ b/ts/.gitignore @@ -23,6 +23,9 @@ examples/schemaStudio/output/ # Contains actual account names, endpoints, and subdomain names — never commit. tools/scripts/pools.inventory.json +# Generated complexity report output (tools/scripts/code/complexityReport.ts). +tools/scripts/code/complexity-report/ + # Per-developer YAML config overrides loaded by @typeagent/config. # Holds local endpoints, deployment names, and (in Phase 1) secrets. # Never commit. diff --git a/ts/package.json b/ts/package.json index 5fc7276c72..afdf8104a6 100644 --- a/ts/package.json +++ b/ts/package.json @@ -25,6 +25,7 @@ "clean": "fluid-build . -t clean", "cli": "pnpm -C packages/cli run start", "cli:dev": "pnpm -C packages/cli run start:dev", + "code-complexity": "npx tsx tools/scripts/code/complexityReport.ts", "copilot": "node packages/copilot-plugin/scripts/launch.mjs", "copilot:dev": "node packages/copilot-plugin/scripts/launch-dev.mjs", "devtunnel:setup": "node tools/scripts/setup-devtunnel.mjs", @@ -107,7 +108,7 @@ "exifreader", "keytar", "koffi", - "protobufjs", + "onnxruntime-node", "puppeteer", "sharp" ], diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 9b051dc504..a7860d6e54 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -935,7 +935,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@25.9.3)(ts-node@10.9.2(@types/node@25.9.3)(typescript@5.4.5)) + version: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2695,7 +2695,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.3 - version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) packages/agents/montage: dependencies: @@ -3596,7 +3596,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.4.5)) + version: 29.7.0(@types/node@25.9.3)(ts-node@10.9.2(@types/node@25.9.3)(typescript@5.4.5)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -5617,7 +5617,7 @@ importers: version: 26.8.1(dmg-builder@26.8.1) electron-vite: specifier: ^4.0.1 - version: 4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@25.9.3)(ts-node@10.9.2(@types/node@25.9.3)(typescript@5.4.5)) @@ -5638,7 +5638,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.3 - version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) packages/studio-service: dependencies: @@ -6290,6 +6290,15 @@ importers: dotenv: specifier: ^16.5.0 version: 16.5.0 + eslint: + specifier: ^10.6.0 + version: 10.6.0(jiti@2.5.1) + eslint-plugin-sonarjs: + specifier: ^4.1.0 + version: 4.1.0(eslint@10.6.0(jiti@2.5.1)) + typescript-eslint: + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) tools/docsAutogen: dependencies: @@ -7820,6 +7829,36 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@esm-bundle/chai@4.3.4-fix.0': resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==} @@ -7934,6 +7973,26 @@ packages: '@huggingface/transformers@3.8.1': resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -9730,6 +9789,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -10063,6 +10125,65 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.62.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.2.2': resolution: {integrity: sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==} engines: {node: '>=18.0.0'} @@ -10341,6 +10462,11 @@ packages: peerDependencies: acorn: ^8.14.0 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.0: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'} @@ -10350,6 +10476,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@5.1.1: resolution: {integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==} engines: {node: '>= 6.0.0'} @@ -10817,6 +10948,10 @@ packages: builder-util@26.8.1: resolution: {integrity: sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==} + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -11638,6 +11773,9 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -12077,10 +12215,41 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-plugin-sonarjs@4.1.0: + resolution: {integrity: sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==} + peerDependencies: + eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@1.2.5: resolution: {integrity: sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==} engines: {node: '>=0.4.0'} @@ -12091,6 +12260,10 @@ packages: engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -12215,6 +12388,9 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-levenshtein@3.0.0: resolution: {integrity: sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==} @@ -12258,6 +12434,10 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + file-size@1.0.0: resolution: {integrity: sha512-tLIdonWTpABkU6Axg2yGChYdrOsy4V8xcm0IcyAP8fSsu6jiXLm5pgs083e4sq5fzNRZuAYolUbZyYmPvCKfwQ==} @@ -12302,6 +12482,10 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true @@ -12312,6 +12496,9 @@ packages: flatbuffers@25.9.23: resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -12414,6 +12601,9 @@ packages: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -12540,6 +12730,10 @@ packages: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -12870,6 +13064,10 @@ packages: resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} @@ -13535,6 +13733,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -13562,6 +13763,10 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils-x@0.1.0: + resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -13675,6 +13880,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lib0@0.2.108: resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==} engines: {node: '>=16'} @@ -13794,6 +14003,9 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -14172,6 +14384,10 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -14559,6 +14775,10 @@ packages: zod: optional: true + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -14881,6 +15101,10 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -15227,6 +15451,10 @@ packages: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -15237,6 +15465,10 @@ packages: regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regexp.escape@2.0.1: resolution: {integrity: sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg==} engines: {node: '>= 0.4'} @@ -15454,6 +15686,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + secretlint@9.3.2: resolution: {integrity: sha512-IuFrtWMGeVFSWpuhn1T6JC0mgfwD9rbFNZG1aWtpkBKOUCbcKZ+RoJcJjdJ0DXv8oa9vRg/+DZUl6Q6omZdPQQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -16161,6 +16397,12 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -16270,6 +16512,10 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -16336,6 +16582,13 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -16837,6 +17090,10 @@ packages: winreg@1.2.5: resolution: {integrity: sha512-uf7tHf+tw0B1y+x+mKTLHkykBgK2KMs3g+KlzmyMbLvICSHQyB/xOFjTT8qZ3oeTFyU7Bbj4FzXitGG6jvKhYw==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -17002,6 +17259,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -19087,6 +19349,36 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.5.1))': + dependencies: + eslint: 10.6.0(jiti@2.5.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@esm-bundle/chai@4.3.4-fix.0': dependencies: '@types/chai': 4.3.20 @@ -19220,6 +19512,22 @@ snapshots: onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4 sharp: 0.34.5 + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.3': @@ -21738,6 +22046,8 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} '@types/express-serve-static-core@4.17.41': @@ -22117,6 +22427,97 @@ snapshots: '@types/node': 22.19.21 optional: true + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5))(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 10.6.0(jiti@2.5.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + dependencies: + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.6.0(jiti@2.5.1) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.62.1(typescript@5.4.5)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.4.5) + '@typescript-eslint/types': 8.62.1 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.4.5)': + dependencies: + typescript: 5.4.5 + + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.6.0(jiti@2.5.1) + ts-api-utils: 2.5.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.62.1': {} + + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.4.5)': + dependencies: + '@typescript-eslint/project-service': 8.62.1(typescript@5.4.5) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.4.5) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.4 + semver: 7.8.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) + eslint: 10.6.0(jiti@2.5.1) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + eslint-visitor-keys: 5.0.1 + '@typespec/ts-http-runtime@0.2.2': dependencies: http-proxy-agent: 7.0.2 @@ -22609,10 +23010,16 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn-walk@8.3.0: {} acorn@8.15.0: {} + acorn@8.17.0: {} + agent-base@5.1.1: {} agent-base@6.0.2: @@ -23206,6 +23613,8 @@ snapshots: transitivePeerDependencies: - supports-color + builtin-modules@3.3.0: {} + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -24134,6 +24543,8 @@ snapshots: deep-extend@0.6.0: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.0: {} @@ -24408,7 +24819,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3)): + electron-vite@4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.7) @@ -24416,7 +24827,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.17 picocolors: 1.1.1 - vite: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -24732,15 +25143,90 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-plugin-sonarjs@4.1.0(eslint@10.6.0(jiti@2.5.1)): + dependencies: + '@eslint-community/regexpp': 4.12.2 + builtin-modules: 3.3.0 + bytes: 3.1.2 + eslint: 10.6.0(jiti@2.5.1) + functional-red-black-tree: 1.0.1 + globals: 17.7.0 + jsx-ast-utils-x: 0.1.0 + lodash.merge: 4.6.2 + minimatch: 10.2.5 + scslre: 0.3.0 + semver: 7.8.4 + ts-api-utils: 2.5.0(typescript@5.4.5) + typescript: 5.4.5 + yaml: 2.9.0 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.6.0(jiti@2.5.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.5.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.5.1 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + esprima@1.2.5: {} esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -24968,6 +25454,8 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-levenshtein@2.0.6: {} + fast-levenshtein@3.0.0: dependencies: fastest-levenshtein: 1.0.16 @@ -25015,6 +25503,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + file-size@1.0.0: {} file-uri-to-path@1.0.0: {} @@ -25078,12 +25570,19 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + flat@5.0.2: {} flatbuffers@24.3.25: {} flatbuffers@25.9.23: {} + flatted@3.4.2: {} + follow-redirects@1.16.0(debug@4.4.1): optionalDependencies: debug: 4.4.1 @@ -25186,6 +25685,8 @@ snapshots: hasown: 2.0.4 is-callable: 1.2.7 + functional-red-black-tree@1.0.1: {} + functions-have-names@1.2.3: {} gaxios@6.7.1(encoding@0.1.13): @@ -25343,6 +25844,8 @@ snapshots: semver: 7.8.0 serialize-error: 7.0.1 + globals@17.7.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -25778,6 +26281,8 @@ snapshots: ignore@7.0.4: {} + ignore@7.0.5: {} + image-size@0.5.5: optional: true @@ -26801,6 +27306,8 @@ snapshots: json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} json5@2.2.3: {} @@ -26842,6 +27349,8 @@ snapshots: ms: 2.1.3 semver: 7.8.0 + jsx-ast-utils-x@0.1.0: {} + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -26999,6 +27508,11 @@ snapshots: leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lib0@0.2.108: dependencies: isomorphic.js: 0.2.5 @@ -27115,6 +27629,8 @@ snapshots: lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} lodash.throttle@4.1.1: {} @@ -27706,6 +28222,10 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 @@ -28091,6 +28611,15 @@ snapshots: ws: 8.21.0 zod: 3.25.76 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -28427,6 +28956,8 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} + prettier@3.5.3: {} pretty-error@4.0.0: @@ -28868,6 +29399,10 @@ snapshots: dependencies: resolve: 1.22.8 + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -28883,6 +29418,11 @@ snapshots: regenerator-runtime@0.14.0: {} + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp.escape@2.0.1: dependencies: call-bind: 1.0.8 @@ -29182,6 +29722,12 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + secretlint@9.3.2: dependencies: '@secretlint/config-creator': 9.3.2 @@ -30081,6 +30627,10 @@ snapshots: ts-algebra@2.0.0: {} + ts-api-utils@2.5.0(typescript@5.4.5): + dependencies: + typescript: 5.4.5 + ts-dedent@2.2.0: {} ts-deepmerge@7.0.2: {} @@ -30247,6 +30797,10 @@ snapshots: tunnel@0.0.6: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-detect@4.0.8: {} type-fest@0.13.1: {} @@ -30326,6 +30880,17 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.8 + typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5): + dependencies: + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5))(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + eslint: 10.6.0(jiti@2.5.1) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + typescript@5.4.5: {} typical@4.0.0: {} @@ -30539,7 +31104,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -30554,7 +31119,7 @@ snapshots: less: 4.3.0 terser: 5.39.2 tsx: 4.21.0 - yaml: 2.8.3 + yaml: 2.9.0 vscode-jsonrpc@8.2.0: {} @@ -30893,6 +31458,8 @@ snapshots: winreg@1.2.5: {} + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wordwrapjs@5.1.0: {} @@ -31013,6 +31580,8 @@ snapshots: yaml@2.8.3: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/ts/tools/package.json b/ts/tools/package.json index 20a1ecd141..217e351cc2 100644 --- a/ts/tools/package.json +++ b/ts/tools/package.json @@ -27,6 +27,9 @@ "validate-npm-package-name": "^6.0.0" }, "devDependencies": { - "dotenv": "^16.5.0" + "dotenv": "^16.5.0", + "eslint": "^10.6.0", + "eslint-plugin-sonarjs": "^4.1.0", + "typescript-eslint": "^8.62.1" } } diff --git a/ts/tools/scripts/code/complexityReport.ts b/ts/tools/scripts/code/complexityReport.ts new file mode 100644 index 0000000000..16d9622d84 --- /dev/null +++ b/ts/tools/scripts/code/complexityReport.ts @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Cyclomatic + cognitive complexity report for the TypeAgent ts/ tree. + * + * The analysis engine is ESLint — the de-facto standard linter for JS/TS — so + * the numbers come from well-known, widely-used implementations rather than a + * bespoke metric: + * - Cyclomatic complexity: ESLint core `complexity` rule. This is the McCabe + * metric, the same one Visual Studio's Code Metrics reports. + * - Cognitive complexity: `eslint-plugin-sonarjs` `cognitive-complexity` + * rule (SonarSource's metric). Better than cyclomatic at surfacing code + * that is hard for a human to follow. Optional — if the plugin cannot be + * loaded the report still produces cyclomatic numbers. + * + * The repo does not need an ESLint configuration. This script builds a + * throwaway flat config in memory and runs it purely to harvest metrics, so it + * never interferes with the rest of the monorepo. + * + * Outputs (written to --out-dir, default tools/scripts/code/complexity-report): + * - functions.csv : every analyzed function, ranked by cyclomatic complexity + * - report.json : structured metrics (functions, per-file rollups, totals) + * - report.html : a self-contained, sortable report (open in a browser) + * plus a console summary (distribution + the worst offenders). + * + * Usage: + * npx tsx tools/scripts/code/complexityReport.ts [options] + * npm run code-complexity -- [options] + * + * Options: + * --include-tests Include test files (*.spec.*, *.test.*, test dirs). + * Excluded by default. + * --cyclomatic Cyclomatic "budget"; functions above it are counted as + * over budget (default 10, the classic McCabe limit). + * --cognitive Cognitive "budget" (default 15, SonarSource's default). + * --top Number of worst offenders to print / embed (default 25). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default tools/scripts/code/complexity-report). + * --help Show this help. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { ESLint, Linter } from "eslint"; +import tseslint from "typescript-eslint"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const SOURCE_GLOB = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"; + +// Generated / build-output directories that are never source. node_modules and +// .git are ignored by ESLint's flat config by default. +const IGNORE_DIRS = [ + "**/dist/**", + "**/build/**", + "**/out/**", + "**/coverage/**", + "**/bin/**", + "**/obj/**", + "**/.turbo/**", + "**/.next/**", + "**/bundle/**", +]; + +// Generated single-file artifacts (declarations, bundles, minified output). +const IGNORE_FILES = ["**/*.d.ts", "**/*.min.js", "**/*.bundle.js"]; + +// Test files and directories, excluded unless --include-tests is passed. +const TEST_GLOBS = [ + "**/test/**", + "**/tests/**", + "**/__tests__/**", + "**/*.spec.*", + "**/*.test.*", +]; + +// ESLint renders the metric into the message text; these pull the number back +// out. Core `complexity`: "Function 'foo' has a complexity of 12. Maximum ..." +const CYCLOMATIC_RE = /^(.*?) has a complexity of (\d+)\./; +// sonarjs: "Refactor this function to reduce its Cognitive Complexity from 21 .." +const COGNITIVE_RE = /Cognitive Complexity from (\d+)/; + +// Distribution buckets for the cyclomatic histogram. hi is inclusive. +const BUCKETS: { label: string; lo: number; hi: number; color: string }[] = [ + { label: "1-5 (low)", lo: 1, hi: 5, color: "#66bb6a" }, + { label: "6-10 (moderate)", lo: 6, hi: 10, color: "#9ccc65" }, + { label: "11-15 (high)", lo: 11, hi: 15, color: "#ffca28" }, + { label: "16-20 (very high)", lo: 16, hi: 20, color: "#ffa726" }, + { label: "21-30 (severe)", lo: 21, hi: 30, color: "#ef5350" }, + { label: "31+ (extreme)", lo: 31, hi: Infinity, color: "#e53935" }, +]; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + includeTests: boolean; + cyclomaticBudget: number; + cognitiveBudget: number; + top: number; + help: boolean; +} + +function parseIntArg(arg: string, next: string | undefined): number { + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error(`${arg} requires a non-negative integer value`); + } + return parseInt(next, 10); +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "complexity-report"), + includeTests: false, + cyclomaticBudget: 10, + cognitiveBudget: 15, + top: 25, + help: false, + }; + + // Normalize "--key=value" into ["--key", "value"]. + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--include-tests": + opts.includeTests = true; + break; + case "--cyclomatic": + opts.cyclomaticBudget = parseIntArg(arg, next); + i++; + break; + case "--cognitive": + opts.cognitiveBudget = parseIntArg(arg, next); + i++; + break; + case "--top": + opts.top = parseIntArg(arg, next); + i++; + break; + case "--root": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + + return opts; +} + +const HELP = `Cyclomatic + cognitive complexity report for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/complexityReport.ts [options] + npm run code-complexity -- [options] + +Options: + --include-tests Include test files (excluded by default). + --cyclomatic Cyclomatic budget; functions above it count as over + budget (default 10, the classic McCabe limit). + --cognitive Cognitive budget (default 15, SonarSource's default). + --top Number of worst offenders to print / embed (default 25). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/complexity-report). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// Analysis +// --------------------------------------------------------------------------- + +interface FuncRecord { + file: string; // repo-relative, forward-slash separated + line: number; + name: string; + cyclomatic: number; + cognitive: number; +} + +interface FileRollup { + file: string; + functions: number; + totalCyclomatic: number; + maxCyclomatic: number; + maxCognitive: number; +} + +interface AnalysisResult { + functions: FuncRecord[]; + filesAnalyzed: number; + parseErrorFiles: number; + cognitiveEnabled: boolean; + elapsedMs: number; +} + +/** Load the optional sonarjs plugin, returning undefined if unavailable. */ +async function loadSonar(): Promise { + try { + const mod = (await import("eslint-plugin-sonarjs")) as { + default?: unknown; + }; + return mod.default ?? mod; + } catch { + return undefined; + } +} + +async function analyze(opts: Options): Promise { + const started = Date.now(); + const sonar = await loadSonar(); + + const ignores = [...IGNORE_DIRS, ...IGNORE_FILES]; + if (!opts.includeTests) { + ignores.push(...TEST_GLOBS); + } + + // The core `complexity` rule reports when complexity > max, and the minimum + // complexity of any function is 1. max:0 therefore flags every function so + // we capture the whole distribution, not just the ones over some limit. + const rules: Linter.RulesRecord = { + complexity: ["warn", { max: 0 }], + }; + const plugins: Record = {}; + if (sonar) { + plugins.sonarjs = sonar as ESLint.Plugin; + rules["sonarjs/cognitive-complexity"] = ["warn", 0]; + } + + const eslint = new ESLint({ + cwd: opts.root, + errorOnUnmatchedPattern: false, + // Ignore any eslint config in the repo; use only what we define here. + overrideConfigFile: true, + overrideConfig: [ + { ignores }, + { + files: [SOURCE_GLOB], + languageOptions: { + parser: tseslint.parser as unknown as Linter.Parser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { jsx: true }, + }, + }, + plugins, + rules, + }, + ], + }); + + const results = await eslint.lintFiles([SOURCE_GLOB]); + + const functions: FuncRecord[] = []; + let parseErrorFiles = 0; + + for (const res of results) { + const rel = path + .relative(opts.root, res.filePath) + .split(path.sep) + .join("/"); + + // Collect cognitive complexity keyed by the function's start line so we + // can attach it to the matching cyclomatic record below. + const cognitiveByLine = new Map(); + let hadFatal = false; + for (const msg of res.messages) { + if (msg.fatal) { + hadFatal = true; + continue; + } + if (msg.ruleId === "sonarjs/cognitive-complexity") { + const m = COGNITIVE_RE.exec(msg.message); + if (m) { + cognitiveByLine.set(msg.line, parseInt(m[1], 10)); + } + } + } + if (hadFatal) { + parseErrorFiles++; + } + + for (const msg of res.messages) { + if (msg.ruleId !== "complexity") { + continue; + } + const m = CYCLOMATIC_RE.exec(msg.message); + if (!m) { + continue; + } + functions.push({ + file: rel, + line: msg.line, + name: m[1], + cyclomatic: parseInt(m[2], 10), + cognitive: cognitiveByLine.get(msg.line) ?? 0, + }); + } + } + + return { + functions, + filesAnalyzed: results.length, + parseErrorFiles, + cognitiveEnabled: sonar !== undefined, + elapsedMs: Date.now() - started, + }; +} + +// --------------------------------------------------------------------------- +// Rollups + formatting helpers +// --------------------------------------------------------------------------- + +function rollupByFile(functions: FuncRecord[]): FileRollup[] { + const map = new Map(); + for (const f of functions) { + let r = map.get(f.file); + if (!r) { + r = { + file: f.file, + functions: 0, + totalCyclomatic: 0, + maxCyclomatic: 0, + maxCognitive: 0, + }; + map.set(f.file, r); + } + r.functions++; + r.totalCyclomatic += f.cyclomatic; + r.maxCyclomatic = Math.max(r.maxCyclomatic, f.cyclomatic); + r.maxCognitive = Math.max(r.maxCognitive, f.cognitive); + } + return [...map.values()]; +} + +function distribution(functions: FuncRecord[]): number[] { + const counts = new Array(BUCKETS.length).fill(0); + for (const f of functions) { + const idx = BUCKETS.findIndex( + (b) => f.cyclomatic >= b.lo && f.cyclomatic <= b.hi, + ); + if (idx >= 0) { + counts[idx]++; + } + } + return counts; +} + +function csvEscape(value: string | number): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +/** Format an integer with thousands separators for human-readable output. */ +function fmt(n: number): string { + return n.toLocaleString("en-US"); +} + +function formatDate(d: Date): string { + const p = (n: number) => String(n).padStart(2, "0"); + return ( + `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` + + `${p(d.getHours())}:${p(d.getMinutes())}` + ); +} + +// --------------------------------------------------------------------------- +// Console summary +// --------------------------------------------------------------------------- + +function printSummary( + opts: Options, + analysis: AnalysisResult, + byCyclomatic: FuncRecord[], + byCognitive: FuncRecord[], + counts: number[], +): void { + const { functions } = analysis; + const overCyclomatic = functions.filter( + (f) => f.cyclomatic > opts.cyclomaticBudget, + ).length; + const overCognitive = functions.filter( + (f) => f.cognitive > opts.cognitiveBudget, + ).length; + + console.log(""); + console.log( + `Analyzed ${fmt(functions.length)} functions in ` + + `${fmt(analysis.filesAnalyzed)} files ` + + `(${(analysis.elapsedMs / 1000).toFixed(1)}s).`, + ); + if (!analysis.cognitiveEnabled) { + console.log( + " Cognitive complexity unavailable (eslint-plugin-sonarjs not loaded).", + ); + } + if (analysis.parseErrorFiles > 0) { + console.log( + ` ${fmt(analysis.parseErrorFiles)} file(s) could not be parsed and were skipped.`, + ); + } + + console.log(""); + console.log("Cyclomatic complexity distribution:"); + const maxCount = Math.max(...counts, 1); + const barWidth = 32; + BUCKETS.forEach((b, i) => { + const c = counts[i]; + const bar = "#".repeat(Math.round((c / maxCount) * barWidth)); + console.log(` ${b.label.padEnd(18)} ${fmt(c).padStart(8)} ${bar}`); + }); + + console.log(""); + console.log( + `Over budget: ${fmt(overCyclomatic)} function(s) > cyclomatic ${opts.cyclomaticBudget}` + + (analysis.cognitiveEnabled + ? `, ${fmt(overCognitive)} > cognitive ${opts.cognitiveBudget}` + : ""), + ); + + const printTable = (title: string, rows: FuncRecord[]): void => { + console.log(""); + console.log(title); + console.log(" CC Cog Location"); + for (const f of rows) { + console.log( + ` ${String(f.cyclomatic).padStart(3)} ${String( + f.cognitive, + ).padStart(4)} ${f.file}:${f.line} ${f.name}`, + ); + } + }; + + printTable( + `Top ${opts.top} functions by cyclomatic complexity:`, + byCyclomatic.slice(0, opts.top), + ); + if (analysis.cognitiveEnabled) { + printTable( + `Top ${opts.top} functions by cognitive complexity:`, + byCognitive.slice(0, opts.top), + ); + } +} + +// --------------------------------------------------------------------------- +// File outputs +// --------------------------------------------------------------------------- + +function writeCsv(filePath: string, functions: FuncRecord[]): void { + const sorted = [...functions].sort((a, b) => b.cyclomatic - a.cyclomatic); + const lines = ["File,Line,Function,Cyclomatic,Cognitive"]; + for (const f of sorted) { + lines.push( + [ + csvEscape(f.file), + csvEscape(f.line), + csvEscape(f.name), + csvEscape(f.cyclomatic), + csvEscape(f.cognitive), + ].join(","), + ); + } + fs.writeFileSync(filePath, lines.join("\n") + "\n", "utf8"); +} + +function writeJson( + filePath: string, + opts: Options, + analysis: AnalysisResult, + files: FileRollup[], + counts: number[], +): void { + const { functions } = analysis; + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + includeTests: opts.includeTests, + cognitiveEnabled: analysis.cognitiveEnabled, + thresholds: { + cyclomatic: opts.cyclomaticBudget, + cognitive: opts.cognitiveBudget, + }, + totals: { + functions: functions.length, + filesAnalyzed: analysis.filesAnalyzed, + parseErrorFiles: analysis.parseErrorFiles, + overCyclomatic: functions.filter( + (f) => f.cyclomatic > opts.cyclomaticBudget, + ).length, + overCognitive: functions.filter( + (f) => f.cognitive > opts.cognitiveBudget, + ).length, + }, + distribution: BUCKETS.map((b, i) => ({ + label: b.label, + lo: b.lo, + hi: b.hi === Infinity ? null : b.hi, + count: counts[i], + })), + files: [...files].sort((a, b) => b.totalCyclomatic - a.totalCyclomatic), + functions: [...functions].sort((a, b) => b.cyclomatic - a.cyclomatic), + }; + fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8"); +} + +function distributionBarsHtml(counts: number[]): string { + const max = Math.max(...counts, 1); + return BUCKETS.map((b, i) => { + const c = counts[i]; + const pct = ((c / max) * 100).toFixed(1); + return `
+
${htmlEscape(b.label)}
+
+
${fmt(c)}
+
`; + }).join("\n"); +} + +function funcRowsHtml(functions: FuncRecord[], budgetCc: number): string { + return functions + .map((f) => { + const over = f.cyclomatic > budgetCc ? " over" : ""; + return ` + ${fmt(f.cyclomatic)} + ${fmt(f.cognitive)} + ${htmlEscape(f.file)}:${f.line} + ${htmlEscape(f.name)} + `; + }) + .join("\n"); +} + +function fileRowsHtml(files: FileRollup[]): string { + return files + .map((f) => { + return ` + ${fmt(f.totalCyclomatic)} + ${fmt(f.maxCyclomatic)} + ${fmt(f.functions)} + ${htmlEscape(f.file)} + `; + }) + .join("\n"); +} + +function buildHtml( + opts: Options, + analysis: AnalysisResult, + byCyclomatic: FuncRecord[], + files: FileRollup[], + counts: number[], +): string { + const { functions } = analysis; + const overCyclomatic = functions.filter( + (f) => f.cyclomatic > opts.cyclomaticBudget, + ).length; + const topFns = byCyclomatic.slice(0, Math.max(opts.top, 100)); + const topFiles = [...files] + .sort((a, b) => b.totalCyclomatic - a.totalCyclomatic) + .slice(0, 50); + + const genDate = formatDate(new Date()); + const testNote = opts.includeTests ? "includes tests" : "excludes tests"; + const cogNote = analysis.cognitiveEnabled + ? "cyclomatic + cognitive" + : "cyclomatic only (sonarjs not loaded)"; + + return ` + + + + +TypeAgent ts/ — Complexity Report + + + +

TypeAgent ts/ — Complexity Report

+
${cogNote} · generated ${genDate} · ${testNote} · budgets: cyclomatic ${opts.cyclomaticBudget}, cognitive ${opts.cognitiveBudget}
+ +
+
${fmt(functions.length)}
functions
+
${fmt(analysis.filesAnalyzed)}
files
+
${fmt(overCyclomatic)}
over cyclomatic ${opts.cyclomaticBudget}
+
+ +

Cyclomatic complexity distribution

+
+${distributionBarsHtml(counts)} +
+ +

Worst offenders (top ${topFns.length} by cyclomatic complexity)

+ + + + + + + + +${funcRowsHtml(topFns, opts.cyclomaticBudget)} + +
CyclomaticCognitiveLocationFunction
+ +

Heaviest files (top ${topFiles.length} by total cyclomatic complexity)

+ + + + + + + + +${fileRowsHtml(topFiles)} + +
Total CCMax CCFunctionsFile
+ + + + +`; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + const opts = parseArgs(process.argv.slice(2)); + if (opts.help) { + console.log(HELP); + return; + } + + console.log(`Scanning ${opts.root} for complexity metrics...`); + const analysis = await analyze(opts); + + if (analysis.functions.length === 0) { + console.log("No functions found. Nothing to report."); + return; + } + + const byCyclomatic = [...analysis.functions].sort( + (a, b) => b.cyclomatic - a.cyclomatic || b.cognitive - a.cognitive, + ); + const byCognitive = [...analysis.functions].sort( + (a, b) => b.cognitive - a.cognitive || b.cyclomatic - a.cyclomatic, + ); + const files = rollupByFile(analysis.functions); + const counts = distribution(analysis.functions); + + printSummary(opts, analysis, byCyclomatic, byCognitive, counts); + + fs.mkdirSync(opts.outDir, { recursive: true }); + const csvPath = path.join(opts.outDir, "functions.csv"); + const jsonPath = path.join(opts.outDir, "report.json"); + const htmlPath = path.join(opts.outDir, "report.html"); + writeCsv(csvPath, analysis.functions); + writeJson(jsonPath, opts, analysis, files, counts); + fs.writeFileSync( + htmlPath, + buildHtml(opts, analysis, byCyclomatic, files, counts), + "utf8", + ); + + console.log(""); + console.log("Wrote:"); + console.log(` ${csvPath}`); + console.log(` ${jsonPath}`); + console.log(` ${htmlPath}`); +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); From 8939028b4c485f47cd30bb3a3c9b1432d96cf48a Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Thu, 2 Jul 2026 21:42:34 -0700 Subject: [PATCH 2/9] link to files --- ts/tools/scripts/code/complexityReport.ts | 28 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ts/tools/scripts/code/complexityReport.ts b/ts/tools/scripts/code/complexityReport.ts index 16d9622d84..83b6b083eb 100644 --- a/ts/tools/scripts/code/complexityReport.ts +++ b/ts/tools/scripts/code/complexityReport.ts @@ -405,6 +405,16 @@ function fmt(n: number): string { return n.toLocaleString("en-US"); } +/** + * Build an escaped vscode:// deep link that opens the file (optionally at a + * given line) in VS Code when the path is clicked in a browser. + */ +function vscodeLink(root: string, relFile: string, line?: number): string { + const abs = path.resolve(root, relFile).split(path.sep).join("/"); + const target = line !== undefined ? `${abs}:${line}` : abs; + return htmlEscape(encodeURI(`vscode://file/${target}`)); +} + function formatDate(d: Date): string { const p = (n: number) => String(n).padStart(2, "0"); return ( @@ -566,28 +576,32 @@ function distributionBarsHtml(counts: number[]): string { }).join("\n"); } -function funcRowsHtml(functions: FuncRecord[], budgetCc: number): string { +function funcRowsHtml( + root: string, + functions: FuncRecord[], + budgetCc: number, +): string { return functions .map((f) => { const over = f.cyclomatic > budgetCc ? " over" : ""; return ` ${fmt(f.cyclomatic)} ${fmt(f.cognitive)} - ${htmlEscape(f.file)}:${f.line} + ${htmlEscape(f.file)}:${f.line} ${htmlEscape(f.name)} `; }) .join("\n"); } -function fileRowsHtml(files: FileRollup[]): string { +function fileRowsHtml(root: string, files: FileRollup[]): string { return files .map((f) => { return ` ${fmt(f.totalCyclomatic)} ${fmt(f.maxCyclomatic)} ${fmt(f.functions)} - ${htmlEscape(f.file)} + ${htmlEscape(f.file)} `; }) .join("\n"); @@ -648,6 +662,8 @@ function buildHtml( td.num { text-align: right; font-variant-numeric: tabular-nums; color: #ffcc80; } td.num.over { color: #ef5350; font-weight: 700; } td.path { font-family: 'Cascadia Code', Consolas, monospace; font-size: 12px; color: #b0bec5; } + td.path a { color: inherit; text-decoration: none; } + td.path a:hover { color: #4fc3f7; text-decoration: underline; } @@ -674,7 +690,7 @@ ${distributionBarsHtml(counts)} Function -${funcRowsHtml(topFns, opts.cyclomaticBudget)} +${funcRowsHtml(opts.root, topFns, opts.cyclomaticBudget)} @@ -687,7 +703,7 @@ ${funcRowsHtml(topFns, opts.cyclomaticBudget)} File -${fileRowsHtml(topFiles)} +${fileRowsHtml(opts.root, topFiles)} From a81d4926d939dad8f2f670ef0f3b0dbecf03552e Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Thu, 2 Jul 2026 22:46:07 -0700 Subject: [PATCH 3/9] complexity report now has a ratcheting cap on existing and new files. And added to build-ts to gate builds. --- .github/workflows/build-ts.yml | 14 + ts/tools/scripts/code/complexityReport.ts | 429 ++++++++++++++++++++-- 2 files changed, 407 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build-ts.yml b/.github/workflows/build-ts.yml index 014de520b0..a90df10b97 100644 --- a/.github/workflows/build-ts.yml +++ b/.github/workflows/build-ts.yml @@ -88,6 +88,20 @@ jobs: else npm run lint fi + # Complexity gate (PRs only): the files this PR changes may not add + # functions over the cyclomatic/cognitive budget versus the base branch + # (a stateless ratchet — the base branch is the baseline, so the count + # can only trend down), and any brand-new file is additionally held to a + # hard cap. Tune the thresholds down as hotspots get refactored. + - name: Complexity ratchet + if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.ts != 'false' }} + working-directory: ts + shell: bash + run: | + git fetch --no-tags origin "${{ github.base_ref }}" + npm run code-complexity -- --ratchet --base "origin/${{ github.base_ref }}" \ + --cyclomatic 25 --cognitive 30 \ + --new-file-cyclomatic 25 --new-file-cognitive 30 - name: Restore better-sqlite3 for Node.js if: ${{ github.event_name != 'pull_request' || steps.filter.outputs.ts != 'false' }} working-directory: ts diff --git a/ts/tools/scripts/code/complexityReport.ts b/ts/tools/scripts/code/complexityReport.ts index 83b6b083eb..faea351d89 100644 --- a/ts/tools/scripts/code/complexityReport.ts +++ b/ts/tools/scripts/code/complexityReport.ts @@ -37,12 +37,22 @@ * --top Number of worst offenders to print / embed (default 25). * --root Directory to scan (default: the ts/ root). * --out-dir Output directory (default tools/scripts/code/complexity-report). + * --ratchet CI gate: exit non-zero if the files changed since + * --base contain more over-budget functions than they did + * at the merge base. The base branch is the baseline (no + * baseline file), so the count only ratchets down. + * --base Base git ref for --ratchet (default origin/main). + * --new-file-cyclomatic With --ratchet, fail if any function in a newly + * added file exceeds cyclomatic (0 = off, default). + * --new-file-cognitive Same, for cognitive complexity. * --help Show this help. */ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; import { ESLint, Linter } from "eslint"; import tseslint from "typescript-eslint"; @@ -108,6 +118,10 @@ interface Options { cyclomaticBudget: number; cognitiveBudget: number; top: number; + ratchet: boolean; + base: string; + newCyclomaticCap: number; + newCognitiveCap: number; help: boolean; } @@ -126,6 +140,10 @@ function parseArgs(argv: string[]): Options { cyclomaticBudget: 10, cognitiveBudget: 15, top: 25, + ratchet: false, + base: "origin/main", + newCyclomaticCap: 0, + newCognitiveCap: 0, help: false, }; @@ -163,6 +181,24 @@ function parseArgs(argv: string[]): Options { opts.top = parseIntArg(arg, next); i++; break; + case "--ratchet": + opts.ratchet = true; + break; + case "--base": + if (next === undefined) { + throw new Error(`${arg} requires a git ref`); + } + opts.base = next; + i++; + break; + case "--new-file-cyclomatic": + opts.newCyclomaticCap = parseIntArg(arg, next); + i++; + break; + case "--new-file-cognitive": + opts.newCognitiveCap = parseIntArg(arg, next); + i++; + break; case "--root": if (next === undefined) { throw new Error(`${arg} requires a path`); @@ -205,6 +241,13 @@ Options: --top Number of worst offenders to print / embed (default 25). --root Directory to scan (default: the ts/ root). --out-dir Output directory (default: tools/scripts/code/complexity-report). + --ratchet CI gate: fail if changed files add complexity vs --base. + --base Base git ref for --ratchet (default origin/main). + --new-file-cyclomatic + With --ratchet, fail if any function in a NEW file exceeds + cyclomatic (0 = disabled, the default). + --new-file-cognitive + Same, for cognitive complexity. --help Show this help.`; // --------------------------------------------------------------------------- @@ -247,15 +290,12 @@ async function loadSonar(): Promise { } } -async function analyze(opts: Options): Promise { - const started = Date.now(); - const sonar = await loadSonar(); - - const ignores = [...IGNORE_DIRS, ...IGNORE_FILES]; - if (!opts.includeTests) { - ignores.push(...TEST_GLOBS); - } - +/** Build the throwaway flat config used purely to harvest metrics. */ +function buildComplexityConfig( + sonar: unknown, + useIgnores: boolean, + includeTests: boolean, +): Linter.Config[] { // The core `complexity` rule reports when complexity > max, and the minimum // complexity of any function is 1. max:0 therefore flags every function so // we capture the whole distribution, not just the ones over some limit. @@ -268,39 +308,40 @@ async function analyze(opts: Options): Promise { rules["sonarjs/cognitive-complexity"] = ["warn", 0]; } - const eslint = new ESLint({ - cwd: opts.root, - errorOnUnmatchedPattern: false, - // Ignore any eslint config in the repo; use only what we define here. - overrideConfigFile: true, - overrideConfig: [ - { ignores }, - { - files: [SOURCE_GLOB], - languageOptions: { - parser: tseslint.parser as unknown as Linter.Parser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { jsx: true }, - }, - }, - plugins, - rules, + const config: Linter.Config[] = []; + if (useIgnores) { + const ignores = [...IGNORE_DIRS, ...IGNORE_FILES]; + if (!includeTests) { + ignores.push(...TEST_GLOBS); + } + config.push({ ignores } as Linter.Config); + } + config.push({ + files: [SOURCE_GLOB], + languageOptions: { + parser: tseslint.parser as unknown as Linter.Parser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { jsx: true }, }, - ], + }, + plugins, + rules, }); + return config; +} - const results = await eslint.lintFiles([SOURCE_GLOB]); - +/** Turn ESLint results into FuncRecords, keyed relative to `cwd`. */ +function parseEslintResults( + results: ESLint.LintResult[], + cwd: string, +): { functions: FuncRecord[]; parseErrorFiles: number } { const functions: FuncRecord[] = []; let parseErrorFiles = 0; for (const res of results) { - const rel = path - .relative(opts.root, res.filePath) - .split(path.sep) - .join("/"); + const rel = path.relative(cwd, res.filePath).split(path.sep).join("/"); // Collect cognitive complexity keyed by the function's start line so we // can attach it to the matching cyclomatic record below. @@ -340,9 +381,48 @@ async function analyze(opts: Options): Promise { } } + return { functions, parseErrorFiles }; +} + +interface LintOutput { + functions: FuncRecord[]; + parseErrorFiles: number; + filesAnalyzed: number; +} + +/** Run ESLint over `patterns` (globs or explicit paths) relative to `cwd`. */ +async function lintToFunctions( + cwd: string, + patterns: string[], + sonar: unknown, + useIgnores: boolean, + includeTests: boolean, +): Promise { + const eslint = new ESLint({ + cwd, + errorOnUnmatchedPattern: false, + // Ignore any eslint config in the repo; use only what we define here. + overrideConfigFile: true, + overrideConfig: buildComplexityConfig(sonar, useIgnores, includeTests), + }); + const results = await eslint.lintFiles(patterns); + const { functions, parseErrorFiles } = parseEslintResults(results, cwd); + return { functions, parseErrorFiles, filesAnalyzed: results.length }; +} + +async function analyze(opts: Options): Promise { + const started = Date.now(); + const sonar = await loadSonar(); + const { functions, parseErrorFiles, filesAnalyzed } = await lintToFunctions( + opts.root, + [SOURCE_GLOB], + sonar, + true, + opts.includeTests, + ); return { functions, - filesAnalyzed: results.length, + filesAnalyzed, parseErrorFiles, cognitiveEnabled: sonar !== undefined, elapsedMs: Date.now() - started, @@ -732,6 +812,278 @@ function sortTable(id, col, numeric) { `; } +// --------------------------------------------------------------------------- +// Ratchet (CI gate) +// --------------------------------------------------------------------------- + +// Path predicates mirroring the ESLint ignores above, for filtering the raw +// file list that `git diff` returns. +const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/; +const IGNORE_PATH_RE = + /(^|\/)(node_modules|dist|build|out|coverage|bin|obj|\.turbo|\.next|bundle)\//; +const GENERATED_FILE_RE = /(\.d\.ts|\.min\.js|\.bundle\.js)$/; +const TEST_PATH_RE = + /(^|\/)(test|tests|__tests__)\/|\.(spec|test)\.[cm]?[jt]sx?$/; + +const EMPTY_LINT: LintOutput = { + functions: [], + parseErrorFiles: 0, + filesAnalyzed: 0, +}; + +function isReportableSource(relPath: string, includeTests: boolean): boolean { + if (!SOURCE_EXT_RE.test(relPath)) { + return false; + } + if (IGNORE_PATH_RE.test(relPath) || GENERATED_FILE_RE.test(relPath)) { + return false; + } + if (!includeTests && TEST_PATH_RE.test(relPath)) { + return false; + } + return true; +} + +function git(args: string[], cwd: string): string { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + }); +} + +interface DiffEntry { + head: string; // path in HEAD + base: string | null; // path at the merge base, or null if newly added +} + +/** Parse `git diff --name-status -M` into HEAD/base path pairs. */ +function parseNameStatus(raw: string): DiffEntry[] { + const entries: DiffEntry[] = []; + for (const line of raw.split(/\r?\n/)) { + if (!line) { + continue; + } + const parts = line.split("\t"); + const status = parts[0]; + if (status.startsWith("R") || status.startsWith("C")) { + entries.push({ base: parts[1], head: parts[2] }); // rename/copy + } else if (status === "A") { + entries.push({ base: null, head: parts[1] }); // added + } else if (status === "D") { + continue; // deleted in HEAD — nothing to lint + } else { + entries.push({ base: parts[1], head: parts[1] }); // modified, etc. + } + } + return entries; +} + +function countOver( + functions: FuncRecord[], + opts: Options, +): { overCyclomatic: number; overCognitive: number } { + return { + overCyclomatic: functions.filter( + (f) => f.cyclomatic > opts.cyclomaticBudget, + ).length, + overCognitive: functions.filter( + (f) => f.cognitive > opts.cognitiveBudget, + ).length, + }; +} + +/** + * Compare the files changed since --base against their content at the merge + * base. Fails (exit 1) if the changed files contain more over-budget functions + * than they did before, so complexity in touched code can only ratchet down. + * There is no committed baseline: the base branch itself is the baseline. + * Returns the desired process exit code. + */ +async function runRatchet(opts: Options): Promise { + let repoRoot: string; + let mergeBase: string; + try { + repoRoot = git(["rev-parse", "--show-toplevel"], opts.root).trim(); + mergeBase = git(["merge-base", opts.base, "HEAD"], opts.root).trim(); + } catch { + console.error( + `Ratchet: could not resolve base ref "${opts.base}" via git. ` + + "Pass --base (e.g. origin/main) and ensure it is fetched.", + ); + return 2; + } + + const entries = parseNameStatus( + git(["diff", "--name-status", "-M", mergeBase, "HEAD"], opts.root), + ).filter((e) => { + if (!isReportableSource(e.head, opts.includeTests)) { + return false; + } + const relToRoot = path.relative( + opts.root, + path.resolve(repoRoot, e.head), + ); + return !relToRoot.startsWith("..") && !path.isAbsolute(relToRoot); + }); + + if (entries.length === 0) { + console.log("Ratchet: no changed source files to check. OK."); + return 0; + } + + const sonar = await loadSonar(); + + // HEAD side: the changed files as they are in the working tree. + const headPaths = entries + .map((e) => path.resolve(repoRoot, e.head)) + .filter((p) => fs.existsSync(p)); + const head = headPaths.length + ? await lintToFunctions( + repoRoot, + headPaths, + sonar, + false, + opts.includeTests, + ) + : EMPTY_LINT; + + // BASE side: the same files' content at the merge base, materialized into a + // temp dir so we compare like-for-like without any committed baseline. + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "complexity-base-")); + let base: LintOutput = EMPTY_LINT; + try { + const basePaths: string[] = []; + for (const e of entries) { + if (!e.base || !isReportableSource(e.base, opts.includeTests)) { + continue; + } + let content: string; + try { + content = git(["show", `${mergeBase}:${e.base}`], repoRoot); + } catch { + continue; // not present at the base + } + const dest = path.join(tmp, e.base); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, content, "utf8"); + basePaths.push(dest); + } + if (basePaths.length) { + base = await lintToFunctions( + tmp, + basePaths, + sonar, + false, + opts.includeTests, + ); + } + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + + const headOver = countOver(head.functions, opts); + const baseOver = countOver(base.functions, opts); + const dCyc = headOver.overCyclomatic - baseOver.overCyclomatic; + const dCog = headOver.overCognitive - baseOver.overCognitive; + const sign = (n: number) => (n >= 0 ? `+${n}` : `${n}`); + + console.log(""); + console.log( + `Ratchet vs ${opts.base} (merge-base ${mergeBase.slice(0, 9)}): ` + + `${entries.length} changed source file(s).`, + ); + console.log( + ` Over cyclomatic ${opts.cyclomaticBudget}: ` + + `${fmt(baseOver.overCyclomatic)} -> ${fmt(headOver.overCyclomatic)} ` + + `(${sign(dCyc)})`, + ); + if (sonar) { + console.log( + ` Over cognitive ${opts.cognitiveBudget}: ` + + `${fmt(baseOver.overCognitive)} -> ${fmt(headOver.overCognitive)} ` + + `(${sign(dCog)})`, + ); + } + + // Absolute cap on brand-new files: they have no baseline to ratchet + // against, so hold them to a hard ceiling instead of just "no worse". + const newFiles = new Set( + entries.filter((e) => e.base === null).map((e) => e.head), + ); + const capEnabled = + newFiles.size > 0 && + (opts.newCyclomaticCap > 0 || opts.newCognitiveCap > 0); + const capViolations = capEnabled + ? head.functions + .filter( + (f) => + newFiles.has(f.file) && + ((opts.newCyclomaticCap > 0 && + f.cyclomatic > opts.newCyclomaticCap) || + (opts.newCognitiveCap > 0 && + f.cognitive > opts.newCognitiveCap)), + ) + .sort((a, b) => b.cyclomatic - a.cyclomatic) + : []; + + if (capEnabled) { + const parts: string[] = []; + if (opts.newCyclomaticCap > 0) { + parts.push(`cyclomatic ${opts.newCyclomaticCap}`); + } + if (opts.newCognitiveCap > 0) { + parts.push(`cognitive ${opts.newCognitiveCap}`); + } + console.log( + ` New-file cap (${parts.join(", ")}): ${newFiles.size} new file(s), ` + + `${capViolations.length} function(s) over.`, + ); + } + + const printTable = (title: string, rows: FuncRecord[]): void => { + console.log(""); + console.log(title); + console.log(" CC Cog Location"); + for (const f of rows.slice(0, opts.top)) { + console.log( + ` ${String(f.cyclomatic).padStart(3)} ${String( + f.cognitive, + ).padStart(4)} ${f.file}:${f.line} ${f.name}`, + ); + } + }; + + const regressed = dCyc > 0 || dCog > 0; + + if (regressed) { + const offenders = head.functions + .filter( + (f) => + f.cyclomatic > opts.cyclomaticBudget || + f.cognitive > opts.cognitiveBudget, + ) + .sort((a, b) => b.cyclomatic - a.cyclomatic); + printTable( + "Ratchet FAILED — reduce complexity in the changed files:", + offenders, + ); + } + + if (capViolations.length > 0) { + printTable( + "New-file cap FAILED — new files must be simpler than the cap:", + capViolations, + ); + } + + if (!regressed && capViolations.length === 0) { + console.log("Ratchet: OK — changed files did not add complexity."); + return 0; + } + return 1; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -743,6 +1095,11 @@ async function main(): Promise { return; } + if (opts.ratchet) { + process.exitCode = await runRatchet(opts); + return; + } + console.log(`Scanning ${opts.root} for complexity metrics...`); const analysis = await analyze(opts); From 8210a539f4ed41e7a8b1cffadc6ffaa36d2c63fa Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Thu, 2 Jul 2026 23:06:24 -0700 Subject: [PATCH 4/9] added more code reporting tools --- ts/.gitignore | 6 + ts/package.json | 8 +- ts/pnpm-lock.yaml | 395 +++++++++- ts/tools/scripts/code/consistencyReport.ts | 739 +++++++++++++++++++ ts/tools/scripts/code/duplicationReport.ts | 798 +++++++++++++++++++++ 5 files changed, 1932 insertions(+), 14 deletions(-) create mode 100644 ts/tools/scripts/code/consistencyReport.ts create mode 100644 ts/tools/scripts/code/duplicationReport.ts diff --git a/ts/.gitignore b/ts/.gitignore index 8c3ab39a4c..56c92868f8 100644 --- a/ts/.gitignore +++ b/ts/.gitignore @@ -26,6 +26,12 @@ tools/scripts/pools.inventory.json # Generated complexity report output (tools/scripts/code/complexityReport.ts). tools/scripts/code/complexity-report/ +# Generated duplication report output (tools/scripts/code/duplicationReport.ts). +tools/scripts/code/duplication-report/ + +# Generated consistency report output (tools/scripts/code/consistencyReport.ts). +tools/scripts/code/consistency-report/ + # Per-developer YAML config overrides loaded by @typeagent/config. # Holds local endpoints, deployment names, and (in Phase 1) secrets. # Never commit. diff --git a/ts/package.json b/ts/package.json index afdf8104a6..a52c7e3059 100644 --- a/ts/package.json +++ b/ts/package.json @@ -26,6 +26,8 @@ "cli": "pnpm -C packages/cli run start", "cli:dev": "pnpm -C packages/cli run start:dev", "code-complexity": "npx tsx tools/scripts/code/complexityReport.ts", + "code-consistency": "npx tsx tools/scripts/code/consistencyReport.ts", + "code-duplication": "npx tsx tools/scripts/code/duplicationReport.ts", "copilot": "node packages/copilot-plugin/scripts/launch.mjs", "copilot:dev": "node packages/copilot-plugin/scripts/launch-dev.mjs", "devtunnel:setup": "node tools/scripts/setup-devtunnel.mjs", @@ -74,10 +76,14 @@ "devDependencies": { "@fluidframework/build-tools": "^0.57.0", "@types/node": "^22.0.0", + "eslint": "^10.6.0", + "eslint-plugin-sonarjs": "^4.1.0", + "jscpd": "^4.2.5", "markdown-link-check": "^3.14.2", "prettier": "^3.5.3", "shx": "^0.4.0", - "tsx": "^4.21.0" + "tsx": "^4.21.0", + "typescript-eslint": "^8.62.1" }, "packageManager": "pnpm@10.34.4+sha512.8768be55200ae3f2226b6527fcca2687e14bc4e5f12d7721a0f25da3df47915177058648db4177baf348120fa0ba2752d8d8d93f6beaf1fe64ae18da8de961af", "engines": { diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index a7860d6e54..16b32d1573 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -27,6 +27,15 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.15.18 + eslint: + specifier: ^10.6.0 + version: 10.6.0(jiti@2.5.1) + eslint-plugin-sonarjs: + specifier: ^4.1.0 + version: 4.1.0(eslint@10.6.0(jiti@2.5.1)) + jscpd: + specifier: ^4.2.5 + version: 4.2.5 markdown-link-check: specifier: ^3.14.2 version: 3.14.2 @@ -39,6 +48,9 @@ importers: tsx: specifier: ^4.21.0 version: 4.21.0 + typescript-eslint: + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) examples/agentExamples/echo: dependencies: @@ -8509,6 +8521,21 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jscpd/badge-reporter@4.2.5': + resolution: {integrity: sha512-ktXrjPeRaRyUDktxTroSA2/w5sshXpQplWkUuq/e6XqEpKBSbGEnwZLIaegSijOrMwIcCXPQ9k4feXIz5eVJNA==} + + '@jscpd/core@4.2.5': + resolution: {integrity: sha512-Esf2deHxaoNEjePwf2jqP6Urzj+BAOsJVPFLbnnSsV+q7rLNMcn0UEEoKBXIOOt4qMkrkhl9DfwpMyPPOr6GkQ==} + + '@jscpd/finder@4.2.5': + resolution: {integrity: sha512-Rw0dtwp/EeLANbujOubuQeJIuXXXkAlT+f5geZhwkB9TxEYP0hqNrdOJUK/TDBKQjRGrOizEtdNy+S4UlbdzOQ==} + + '@jscpd/html-reporter@4.2.5': + resolution: {integrity: sha512-zMMIKbvi43dMgeNeHXlHQy1ovf+KJrzNlUubaBvCAVatqP23ksW8d3fmsevIQG9mMMTH0D1xOz+SxUn1FREOPg==} + + '@jscpd/tokenizer@4.2.5': + resolution: {integrity: sha512-UM8Wx/jwahmflqQExlcKMQTYOAy58N/fn7Pv6NYrkD3EZm/FTk7gW97wkXy5aDE1Ts9oBUpT9tLY2rz7ogCHAQ==} + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -10471,6 +10498,11 @@ packages: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -10668,6 +10700,9 @@ packages: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -10675,6 +10710,9 @@ packages: resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} engines: {node: '>=12.0.0'} + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -10758,6 +10796,13 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + + badgen@3.3.2: + resolution: {integrity: sha512-fbQwK9norfdzbdsoPwbLIAmgBXDGEme3jeIyqPAH7o6vp9lmuLHS7uXULvOiQ6XnMLkYNG4gDjILf74hgtTAug==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -10844,6 +10889,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blamer@1.0.7: + resolution: {integrity: sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==} + engines: {node: '>=8.9'} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -11061,6 +11110,9 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} @@ -11254,6 +11306,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -11278,6 +11334,10 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -11329,6 +11389,9 @@ packages: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -11929,6 +11992,9 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -12297,6 +12363,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -12313,6 +12382,10 @@ packages: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -13021,6 +13094,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -13236,6 +13313,9 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -13344,6 +13424,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -13662,6 +13745,9 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -13680,6 +13766,13 @@ packages: jsbi@2.0.5: resolution: {integrity: sha512-TzO/62Hxeb26QMb4IGlI/5X+QLr9Uqp1FPkwp2+KOICW+Q+vSuFj61c8pkT6wAns4WcK56X7CmSHhJeDGWOqxQ==} + jscpd-sarif-reporter@4.2.5: + resolution: {integrity: sha512-O8LcM9grAS5yO5x1Q0yegYaYcUX//IEBEyvzGFSYCeo1YzHbMnAI6EK7oTrwD+7Csjvfg9m8B8G7OOxzcSlr9w==} + + jscpd@4.2.5: + resolution: {integrity: sha512-KDpApYw1ChGelfHb7MwYTEx694OnW52pv3McAasidUV4ILcGDQMiVJzB+vI8ox+ZPVfOSvdXQCk8uRa9B0LXnw==} + hasBin: true + jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -13763,6 +13856,9 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + jsx-ast-utils-x@0.1.0: resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -14112,6 +14208,9 @@ packages: markdown-link-extractor@4.0.3: resolution: {integrity: sha512-aEltJiQ4/oC0h6Jbw/uuATGSHZPkcH8DIunNH1A0e+GSFkvZ6BbBkdvBTVfIV8r6HapCU3yTd0eFdi3ZeM1eAQ==} + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -14633,6 +14732,10 @@ packages: resolution: {integrity: sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==} engines: {node: '>=14'} + node-sarif-builder@4.1.0: + resolution: {integrity: sha512-IWqZF6u0EI/07HTBm+zZ+MgXgWl09dnSJRGaDCPBSlOqilDcx6pj3Mpb3HvPN8V2Gr+ISw7ZrMsL7STWs1F++w==} + engines: {node: '>=20'} + nodemailer@8.0.4: resolution: {integrity: sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==} engines: {node: '>=6.0.0'} @@ -15143,6 +15246,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -15230,6 +15336,42 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.4: + resolution: {integrity: sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.4: + resolution: {integrity: sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -15502,6 +15644,10 @@ packages: renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -15984,6 +16130,9 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} @@ -16351,6 +16500,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -16857,6 +17009,10 @@ packages: yaml: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -17090,6 +17246,10 @@ packages: winreg@1.2.5: resolution: {integrity: sha512-uf7tHf+tw0B1y+x+mKTLHkykBgK2KMs3g+KlzmyMbLvICSHQyB/xOFjTT8qZ3oeTFyU7Bbj4FzXitGG6jvKhYw==} + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -18399,7 +18559,7 @@ snapshots: '@babel/generator@7.28.3': dependencies: '@babel/parser': 7.29.7 - '@babel/types': 7.28.4 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -19003,7 +19163,7 @@ snapshots: node-gyp: 12.3.0 ora: 5.4.1 read-binary-file-arch: 1.0.6 - semver: 7.8.0 + semver: 7.8.4 tar: 7.5.16 yargs: 17.7.2 transitivePeerDependencies: @@ -20121,6 +20281,40 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jscpd/badge-reporter@4.2.5': + dependencies: + badgen: 3.3.2 + colors: 1.4.0 + fs-extra: 11.3.4 + + '@jscpd/core@4.2.5': + dependencies: + eventemitter3: 5.0.4 + + '@jscpd/finder@4.2.5': + dependencies: + '@jscpd/core': 4.2.5 + '@jscpd/tokenizer': 4.2.5 + blamer: 1.0.7 + bytes: 3.1.2 + cli-table3: 0.6.5 + colors: 1.4.0 + fast-glob: 3.3.3 + fs-extra: 11.3.4 + markdown-table: 2.0.0 + pug: 3.0.4 + + '@jscpd/html-reporter@4.2.5': + dependencies: + colors: 1.4.0 + fs-extra: 11.3.4 + pug: 3.0.4 + + '@jscpd/tokenizer@4.2.5': + dependencies: + '@jscpd/core': 4.2.5 + spark-md5: 3.0.2 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -22642,7 +22836,7 @@ snapshots: '@vue/compiler-core@3.5.16': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.29.7 '@vue/shared': 3.5.16 entities: 4.5.0 estree-walker: 2.0.2 @@ -23016,6 +23210,8 @@ snapshots: acorn-walk@8.3.0: {} + acorn@7.4.1: {} + acorn@8.15.0: {} acorn@8.17.0: {} @@ -23172,7 +23368,7 @@ snapshots: js-yaml: 4.3.0 json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 10.2.4 + minimatch: 10.2.5 plist: 3.1.0 proper-lockfile: 4.1.2 resedit: 1.7.2 @@ -23264,6 +23460,8 @@ snapshots: arrify@2.0.1: {} + asap@2.0.6: {} + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -23274,6 +23472,8 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + assert-never@1.4.0: {} + assert-plus@1.0.0: optional: true @@ -23373,6 +23573,12 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.29.7 + + badgen@3.3.2: {} + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -23445,6 +23651,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blamer@1.0.7: + dependencies: + execa: 4.1.0 + which: 2.0.2 + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -23721,6 +23932,10 @@ snapshots: character-entities@2.0.2: {} + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + chardet@2.1.0: {} cheerio-select@2.1.0: @@ -23951,6 +24166,8 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -23975,6 +24192,8 @@ snapshots: commander@14.0.2: {} + commander@15.0.0: {} + commander@2.20.3: {} commander@5.1.0: {} @@ -24032,6 +24251,11 @@ snapshots: connect-history-api-fallback@2.0.0: {} + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -24679,6 +24903,8 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 + doctypes@1.1.0: {} + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -25255,6 +25481,8 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.4: {} + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -25273,6 +25501,18 @@ snapshots: signal-exit: 3.0.7 strip-eof: 1.0.0 + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -25841,7 +26081,7 @@ snapshots: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.8.0 + semver: 7.8.4 serialize-error: 7.0.1 globals@17.7.0: {} @@ -26247,6 +26487,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@1.1.1: {} + human-signals@2.1.0: {} humanize-ms@1.2.1: @@ -26444,6 +26686,11 @@ snapshots: is-docker@3.0.0: {} + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + is-extendable@0.1.1: {} is-extglob@2.1.1: {} @@ -26519,6 +26766,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -26630,7 +26879,7 @@ snapshots: '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 7.8.0 + semver: 7.8.4 transitivePeerDependencies: - supports-color @@ -27114,7 +27363,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.8.0 + semver: 7.8.4 transitivePeerDependencies: - supports-color @@ -27204,6 +27453,8 @@ snapshots: jose@6.1.3: {} + js-stringify@1.0.2: {} + js-tokens@4.0.0: {} js-yaml@3.15.0: @@ -27221,6 +27472,24 @@ snapshots: jsbi@2.0.5: {} + jscpd-sarif-reporter@4.2.5: + dependencies: + colors: 1.4.0 + fs-extra: 11.3.4 + node-sarif-builder: 4.1.0 + + jscpd@4.2.5: + dependencies: + '@jscpd/badge-reporter': 4.2.5 + '@jscpd/core': 4.2.5 + '@jscpd/finder': 4.2.5 + '@jscpd/html-reporter': 4.2.5 + '@jscpd/tokenizer': 4.2.5 + colors: 1.4.0 + commander: 15.0.0 + fs-extra: 11.3.4 + jscpd-sarif-reporter: 4.2.5 + jsdom@20.0.3: dependencies: abab: 2.0.6 @@ -27347,7 +27616,12 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.0 + semver: 7.8.4 + + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 jsx-ast-utils-x@0.1.0: {} @@ -27715,7 +27989,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.4 make-error@1.3.6: {} @@ -27755,6 +28029,10 @@ snapshots: html-link-extractor: 1.0.5 marked: 17.0.6 + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + markdown-table@3.0.4: {} marked-terminal@7.3.0(marked@15.0.12): @@ -28398,7 +28676,7 @@ snapshots: node-abi@3.77.0: dependencies: - semver: 7.8.0 + semver: 7.8.4 node-abi@4.24.0: dependencies: @@ -28467,6 +28745,11 @@ snapshots: '@types/sarif': 2.1.7 fs-extra: 10.1.0 + node-sarif-builder@4.1.0: + dependencies: + '@types/sarif': 2.1.7 + fs-extra: 11.3.4 + nodemailer@8.0.4: {} noms@0.0.0: @@ -28988,6 +29271,10 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise@7.3.1: + dependencies: + asap: 2.0.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -29127,6 +29414,73 @@ snapshots: dependencies: punycode: 2.3.1 + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.4: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.8 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.4: + dependencies: + pug-code-gen: 3.0.4 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -29501,6 +29855,8 @@ snapshots: lodash: 4.18.1 strip-ansi: 6.0.1 + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -30073,7 +30429,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.4 sisteransi@1.0.5: {} @@ -30166,6 +30522,8 @@ snapshots: source-map@0.7.4: {} + spark-md5@3.0.2: {} + sparse-bitfield@3.0.3: dependencies: memory-pager: 1.5.0 @@ -30415,7 +30773,7 @@ snapshots: tar-stream@2.2.0: dependencies: bl: 4.1.0 - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 @@ -30472,7 +30830,7 @@ snapshots: terser@5.27.0: dependencies: '@jridgewell/source-map': 0.3.5 - acorn: 8.15.0 + acorn: 8.17.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -30584,6 +30942,8 @@ snapshots: toidentifier@1.0.1: {} + token-stream@1.0.0: {} + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -31121,6 +31481,8 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 + void-elements@3.1.0: {} + vscode-jsonrpc@8.2.0: {} vscode-jsonrpc@8.2.1: {} @@ -31458,6 +31820,13 @@ snapshots: winreg@1.2.5: {} + with@7.0.2: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/ts/tools/scripts/code/consistencyReport.ts b/ts/tools/scripts/code/consistencyReport.ts new file mode 100644 index 0000000000..8bce6b72c6 --- /dev/null +++ b/ts/tools/scripts/code/consistencyReport.ts @@ -0,0 +1,739 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Pattern-consistency report for the TypeAgent ts/ tree. + * + * Where complexityReport.ts and duplicationReport.ts wrap standard engines + * (ESLint, jscpd) to produce metrics, this report performs cross-file + * *structural* analysis that those per-file engines cannot: it surfaces places + * that stray from established repo patterns and candidates for consolidation. + * Its findings are heuristic — they are candidates to review, not hard errors. + * + * Three checks, each targeting a known TypeAgent convention: + * + * 1. Duplicate exports across packages. The same exported function/const/ + * class name defined in N different packages usually means a utility that + * should live in one shared package (e.g. delay, ensureDir, withTimeout). + * Complements jscpd, which finds copy-paste but misses re-implementations + * that drifted apart. Bare lifecycle names (instantiate, activate, ...) + * are ignored. + * + * 2. Direct process.env access in packages/. The convention is to read + * configuration through @typeagent/config (loadConfigSync) and the + * aiclient runtime config, not process.env scattered through the code. + * The canonical readers (packages/config, aiclient runtimeConfig) are + * excluded. + * + * 3. Agent layout conformance. Each agent under packages/agents should ship a + * Manifest.json and a handler that exports instantiate(). Agents + * missing either are flagged. + * + * Outputs (written to --out-dir, default tools/scripts/code/consistency-report): + * - duplicate-exports.csv : every cross-package duplicate export + * - report.json : structured results for all three checks + * - report.html : a self-contained, sortable report + * plus a console summary. + * + * Usage: + * npx tsx tools/scripts/code/consistencyReport.ts [options] + * npm run code-consistency -- [options] + * + * Options: + * --include-tests Include test files (excluded by default). + * --min-packages Report an export only if it appears in at least this + * many packages (default 3). + * --top Number of rows to print / embed (default 30). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default consistency-report). + * --help Show this help. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const SCAN_SUBDIRS = ["packages", "examples", "extensions", "tools"]; + +// process.env check is scoped to production packages only. +const ENV_SCAN_PREFIX = "packages/"; + +const CODE_EXTS = new Set([ + ".ts", + ".mts", + ".cts", + ".tsx", + ".js", + ".mjs", + ".cjs", + ".jsx", +]); + +const IGNORE_DIR_NAMES = new Set([ + "node_modules", + "dist", + "build", + "out", + "coverage", + "bin", + "obj", + ".turbo", + ".next", + "bundle", + ".git", + ".jscpd", + "complexity-report", + "duplication-report", + "consistency-report", +]); + +// Bare export names that are conventions/lifecycle, not shareable utilities. +const EXPORT_DENYLIST = new Set([ + "instantiate", + "activate", + "deactivate", + "run", + "main", + "default", + "handlers", + "getCommands", + "register", + "start", + "stop", +]); + +// Canonical config readers that are allowed to touch process.env directly. +const ENV_ALLOWED = (rel: string): boolean => + rel.startsWith("packages/config/") || + rel === "packages/aiclient/src/runtimeConfig.ts"; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + includeTests: boolean; + minPackages: number; + top: number; + help: boolean; +} + +function parseIntArg(arg: string, next: string | undefined): number { + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error(`${arg} requires a non-negative integer value`); + } + return parseInt(next, 10); +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "consistency-report"), + includeTests: false, + minPackages: 3, + top: 30, + help: false, + }; + + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--include-tests": + opts.includeTests = true; + break; + case "--min-packages": + opts.minPackages = parseIntArg(arg, next); + i++; + break; + case "--top": + opts.top = parseIntArg(arg, next); + i++; + break; + case "--root": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + if (opts.minPackages < 2) { + throw new Error("--min-packages must be at least 2"); + } + + return opts; +} + +const HELP = `Pattern-consistency report for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/consistencyReport.ts [options] + npm run code-consistency -- [options] + +Options: + --include-tests Include test files (excluded by default). + --min-packages Report an export only if it appears in at least this many + packages (default 3). + --top Number of rows to print / embed (default 30). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/consistency-report). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// File walking +// --------------------------------------------------------------------------- + +function* walk(dir: string): Generator { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + if (!IGNORE_DIR_NAMES.has(e.name)) { + yield* walk(full); + } + } else if (e.isFile()) { + yield full; + } + } +} + +function isCodeFile(name: string): boolean { + if (/\.d\.(ts|mts|cts)$/.test(name)) { + return false; + } + return CODE_EXTS.has(path.extname(name)); +} + +function isTestFile(rel: string): boolean { + return ( + /\.(spec|test)\./.test(rel) || + /(^|\/)(test|tests|__tests__)\//.test(rel) + ); +} + +/** Package key: everything before the first `/src/`, else leading segments. */ +function packageKeyOf(rel: string): string { + const f = rel.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +// --------------------------------------------------------------------------- +// Analysis +// --------------------------------------------------------------------------- + +const EXPORT_RES = [ + /^export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm, + /^export\s+(?:const|let|var)\s+(?!enum\b)([A-Za-z_$][\w$]*)/gm, + /^export\s+(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/gm, +]; + +interface DuplicateExport { + name: string; + packageCount: number; + packages: string[]; + files: string[]; +} + +interface EnvFile { + file: string; + pkg: string; + refs: number; +} + +interface EnvPackage { + pkg: string; + refs: number; + files: number; +} + +interface AgentStatus { + agent: string; + hasManifest: boolean; + hasInstantiate: boolean; +} + +interface AnalysisResult { + filesScanned: number; + duplicateExports: DuplicateExport[]; + envPackages: EnvPackage[]; + envFiles: EnvFile[]; + envTotalRefs: number; + agents: AgentStatus[]; + agentIssues: AgentStatus[]; + elapsedMs: number; +} + +function analyze(opts: Options): AnalysisResult { + const started = Date.now(); + + const files: string[] = []; + for (const sub of SCAN_SUBDIRS) { + const abs = path.join(opts.root, sub); + if (!fs.existsSync(abs)) { + continue; + } + for (const full of walk(abs)) { + if (!isCodeFile(full)) { + continue; + } + const rel = path + .relative(opts.root, full) + .split(path.sep) + .join("/"); + if (!opts.includeTests && isTestFile(rel)) { + continue; + } + files.push(rel); + } + } + + // name -> packageKey -> set(files) + const exportMap = new Map>>(); + const envByFile: EnvFile[] = []; + + for (const rel of files) { + let content: string; + try { + content = fs.readFileSync(path.join(opts.root, rel), "utf8"); + } catch { + continue; + } + const pkg = packageKeyOf(rel); + + // Check 1: exported symbol names. + for (const re of EXPORT_RES) { + re.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const name = m[1]; + if (EXPORT_DENYLIST.has(name)) { + continue; + } + let byPkg = exportMap.get(name); + if (!byPkg) { + byPkg = new Map(); + exportMap.set(name, byPkg); + } + let set = byPkg.get(pkg); + if (!set) { + set = new Set(); + byPkg.set(pkg, set); + } + set.add(rel); + } + } + + // Check 2: direct process.env access (production packages only). + if (rel.startsWith(ENV_SCAN_PREFIX) && !ENV_ALLOWED(rel)) { + const matches = content.match(/process\.env\b/g); + if (matches && matches.length > 0) { + envByFile.push({ file: rel, pkg, refs: matches.length }); + } + } + } + + // Duplicate exports: names in >= minPackages different packages. + const duplicateExports: DuplicateExport[] = []; + for (const [name, byPkg] of exportMap) { + if (byPkg.size >= opts.minPackages) { + const packages = [...byPkg.keys()].sort(); + const fileList: string[] = []; + for (const set of byPkg.values()) { + fileList.push(...set); + } + duplicateExports.push({ + name, + packageCount: byPkg.size, + packages, + files: fileList.sort(), + }); + } + } + duplicateExports.sort( + (a, b) => + b.packageCount - a.packageCount || a.name.localeCompare(b.name), + ); + + // process.env rollups. + const envPkgMap = new Map(); + let envTotalRefs = 0; + for (const e of envByFile) { + envTotalRefs += e.refs; + const r = envPkgMap.get(e.pkg) ?? { pkg: e.pkg, refs: 0, files: 0 }; + r.refs += e.refs; + r.files += 1; + envPkgMap.set(e.pkg, r); + } + const envPackages = [...envPkgMap.values()].sort((a, b) => b.refs - a.refs); + const envFiles = envByFile.sort((a, b) => b.refs - a.refs); + + // Check 3: agent conformance. + const agents = analyzeAgents(opts.root); + const agentIssues = agents.filter( + (a) => !a.hasManifest || !a.hasInstantiate, + ); + + return { + filesScanned: files.length, + duplicateExports, + envPackages, + envFiles, + envTotalRefs, + agents, + agentIssues, + elapsedMs: Date.now() - started, + }; +} + +function analyzeAgents(root: string): AgentStatus[] { + const agentsDir = path.join(root, "packages", "agents"); + if (!fs.existsSync(agentsDir)) { + return []; + } + const result: AgentStatus[] = []; + for (const e of fs.readdirSync(agentsDir, { withFileTypes: true })) { + if (!e.isDirectory() || IGNORE_DIR_NAMES.has(e.name)) { + continue; + } + const dir = path.join(agentsDir, e.name); + if (!fs.existsSync(path.join(dir, "package.json"))) { + continue; + } + let hasManifest = false; + let hasInstantiate = false; + for (const f of walk(dir)) { + const base = path.basename(f); + if (/manifest\.json$/i.test(base)) { + hasManifest = true; + } + if (isCodeFile(f) && !hasInstantiate) { + try { + if (/\binstantiate\b/.test(fs.readFileSync(f, "utf8"))) { + hasInstantiate = true; + } + } catch { + /* ignore */ + } + } + if (hasManifest && hasInstantiate) { + break; + } + } + result.push({ + agent: `packages/agents/${e.name}`, + hasManifest, + hasInstantiate, + }); + } + return result.sort((a, b) => a.agent.localeCompare(b.agent)); +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function csvEscape(value: string | number | boolean): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +// --------------------------------------------------------------------------- +// Report writers +// --------------------------------------------------------------------------- + +function writeCsv(outDir: string, dups: DuplicateExport[]): string { + const header = ["Export", "PackageCount", "Packages"].join(","); + const rows = dups.map((d) => + [d.name, d.packageCount, d.packages.join(" | ")] + .map(csvEscape) + .join(","), + ); + const file = path.join(outDir, "duplicate-exports.csv"); + fs.writeFileSync(file, [header, ...rows].join("\n") + "\n", "utf8"); + return file; +} + +function writeJson( + outDir: string, + opts: Options, + result: AnalysisResult, +): string { + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + includeTests: opts.includeTests, + minPackages: opts.minPackages, + filesScanned: result.filesScanned, + duplicateExports: result.duplicateExports, + processEnv: { + totalRefs: result.envTotalRefs, + totalFiles: result.envFiles.length, + perPackage: result.envPackages, + files: result.envFiles, + }, + agents: { + total: result.agents.length, + conforming: result.agents.length - result.agentIssues.length, + issues: result.agentIssues, + all: result.agents, + }, + }; + const file = path.join(outDir, "report.json"); + fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", "utf8"); + return file; +} + +function writeHtml( + outDir: string, + opts: Options, + result: AnalysisResult, +): string { + const dupRows = result.duplicateExports + .slice(0, Math.max(opts.top, 100)) + .map( + (d) => + `${htmlEscape(d.name)}${d.packageCount} + ${d.packages.map(htmlEscape).join("
")}`, + ) + .join("\n"); + + const envRows = result.envPackages + .slice(0, Math.max(opts.top, 60)) + .map( + (p) => + `${htmlEscape(p.pkg)}${num(p.refs)}${num(p.files)}`, + ) + .join("\n"); + + const agentRows = result.agents + .map((a) => { + const ok = a.hasManifest && a.hasInstantiate; + return `${htmlEscape(a.agent)} + ${a.hasManifest ? "yes" : "MISSING"} + ${a.hasInstantiate ? "yes" : "MISSING"}`; + }) + .join("\n"); + + return ` + +TypeAgent consistency report + +

TypeAgent consistency report

+
generated ${htmlEscape(new Date().toISOString())} · + ${num(result.filesScanned)} files · min-packages ${opts.minPackages} · + tests ${opts.includeTests ? "included" : "excluded"} · heuristic — review candidates
+ +
+
${num(result.duplicateExports.length)}
cross-package duplicate exports
+
${num(result.envTotalRefs)}
direct process.env refs
+
${num(result.envFiles.length)}
files using process.env
+
${num(result.agentIssues.length)}
non-conforming agents
+
+ +

Duplicate exports across packages

+ +${dupRows || ''}
ExportPackagesWhere
None
+ +

Direct process.env usage (packages/, excl. config)

+ +${envRows || ''}
PackageRefsFiles
None
+ +

Agent layout conformance

+ +${agentRows}
AgentManifestinstantiate()
+ + +`; +} + +// --------------------------------------------------------------------------- +// Console summary +// --------------------------------------------------------------------------- + +function printSummary(opts: Options, result: AnalysisResult): void { + console.log(""); + console.log("Consistency report (heuristic — review candidates)"); + console.log( + `Scanned ${num(result.filesScanned)} files | elapsed ${(result.elapsedMs / 1000).toFixed(1)}s`, + ); + + console.log(""); + console.log( + `1. Duplicate exports across >=${opts.minPackages} packages: ${num(result.duplicateExports.length)}`, + ); + for (const d of result.duplicateExports.slice(0, opts.top)) { + console.log( + ` ${String(d.packageCount).padStart(2)} pkgs ${d.name.padEnd(26)} ${d.packages.join(", ")}`, + ); + } + + console.log(""); + console.log( + `2. Direct process.env in packages/ (excl. config): ${num(result.envTotalRefs)} refs in ${num(result.envFiles.length)} files`, + ); + for (const p of result.envPackages.slice(0, opts.top)) { + console.log( + ` ${String(p.refs).padStart(4)} refs / ${String(p.files).padStart(2)} files ${p.pkg}`, + ); + } + + console.log(""); + console.log( + `3. Agent conformance: ${result.agents.length - result.agentIssues.length}/${result.agents.length} conform`, + ); + if (result.agentIssues.length === 0) { + console.log(" All agents ship a Manifest + instantiate()."); + } else { + for (const a of result.agentIssues) { + const miss = [ + a.hasManifest ? null : "Manifest", + a.hasInstantiate ? null : "instantiate()", + ] + .filter(Boolean) + .join(" + "); + console.log(` ${a.agent} — missing ${miss}`); + } + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + fs.mkdirSync(opts.outDir, { recursive: true }); + const result = analyze(opts); + + const csv = writeCsv(opts.outDir, result.duplicateExports); + const json = writeJson(opts.outDir, opts, result); + const html = writeHtml(opts.outDir, opts, result); + fs.writeFileSync(path.join(opts.outDir, "report.html"), html, "utf8"); + + printSummary(opts, result); + console.log(""); + console.log("Reports written to:"); + console.log(` ${path.relative(opts.root, csv).split(path.sep).join("/")}`); + console.log( + ` ${path.relative(opts.root, json).split(path.sep).join("/")}`, + ); + console.log( + ` ${path.relative(opts.root, path.join(opts.outDir, "report.html")).split(path.sep).join("/")}`, + ); +} + +main(); diff --git a/ts/tools/scripts/code/duplicationReport.ts b/ts/tools/scripts/code/duplicationReport.ts new file mode 100644 index 0000000000..db94c682a4 --- /dev/null +++ b/ts/tools/scripts/code/duplicationReport.ts @@ -0,0 +1,798 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Copy/paste (code duplication) report for the TypeAgent ts/ tree. + * + * The analysis engine is jscpd — the de-facto-standard copy/paste detector for + * source code — so the numbers come from a well-known, widely-used token-based + * implementation (Rabin-Karp fingerprinting) rather than a bespoke heuristic. + * This mirrors the sibling complexityReport.ts, which harvests its metrics from + * ESLint. + * + * jscpd@4 ships a programmatic API, but its ESM entry pulls a dependency + * (`colors/safe`) that breaks under Node's native ESM loader. We therefore load + * its CommonJS build via `createRequire`, which resolves that dependency + * correctly while keeping this script itself an ES module. + * + * On top of the raw jscpd output this report adds two things that matter for a + * maintainability pass: + * - Cross-package clones: clone pairs whose two sides live in different + * packages. These are the real consolidation targets (shared helpers), + * as opposed to within-file/within-package repetition. + * - Per-package rollups: which packages carry the most duplicated lines. + * + * Outputs (written to --out-dir, default tools/scripts/code/duplication-report): + * - clones.csv : every detected clone pair, ranked by duplicated lines + * - report.json : structured metrics (totals, per-format, per-package, clones) + * - report.html : a self-contained, sortable report (open in a browser) + * plus a console summary (totals + the worst offenders + cross-package hotspots). + * + * Usage: + * npx tsx tools/scripts/code/duplicationReport.ts [options] + * npm run code-duplication -- [options] + * + * Options: + * --include-tests Include test files (*.spec.*, *.test.*, test dirs). + * Excluded by default. + * --min-tokens Minimum token length of a clone (default 50, jscpd's + * default). Lower = more, smaller clones. + * --min-lines Minimum line length of a clone (default 5). + * --top Number of worst offenders to print / embed (default 25). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default tools/scripts/code/duplication-report). + * --help Show this help. + */ + +import { createRequire } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +// Source subtrees scanned by default (relative to --root). Restricting to these +// keeps jscpd away from node_modules / build output entirely. +const SCAN_SUBDIRS = ["packages", "examples", "extensions", "tools"]; + +// jscpd formats to consider, plus the extension map so .mts/.cts/.mjs/.cjs are +// treated as TypeScript/JavaScript (the repo uses .mts heavily for agents). +const CODE_FORMATS = "typescript,tsx,javascript"; +const FORMATS_EXTS = "typescript:ts,tsx,mts,cts;javascript:js,jsx,mjs,cjs"; + +// Generated / build-output / vendored directories that are never source. +const IGNORE_DIRS = [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/out/**", + "**/coverage/**", + "**/bin/**", + "**/obj/**", + "**/.turbo/**", + "**/.next/**", + "**/bundle/**", + "**/complexity-report/**", + "**/duplication-report/**", +]; + +// Generated single-file artifacts (declarations, bundles, minified, sourcemaps). +const IGNORE_FILES = [ + "**/*.d.ts", + "**/*.d.mts", + "**/*.min.js", + "**/*.bundle.js", + "**/*.map", +]; + +// Test files and directories, excluded unless --include-tests is passed. +const TEST_GLOBS = [ + "**/test/**", + "**/tests/**", + "**/__tests__/**", + "**/*.spec.*", + "**/*.test.*", +]; + +// Distribution buckets for the clone-size histogram (by duplicated lines). +const BUCKETS: { label: string; lo: number; hi: number; color: string }[] = [ + { label: "5-9 lines", lo: 5, hi: 9, color: "#9ccc65" }, + { label: "10-19 lines", lo: 10, hi: 19, color: "#ffca28" }, + { label: "20-49 lines", lo: 20, hi: 49, color: "#ffa726" }, + { label: "50-99 lines", lo: 50, hi: 99, color: "#ef5350" }, + { label: "100+ lines", lo: 100, hi: Infinity, color: "#e53935" }, +]; + +// --------------------------------------------------------------------------- +// jscpd report shape (only the fields we consume) +// --------------------------------------------------------------------------- + +interface JscpdFileRef { + name: string; + start: number; + end: number; +} + +interface JscpdDuplicate { + format: string; + lines: number; + tokens: number; + firstFile: JscpdFileRef; + secondFile: JscpdFileRef; +} + +interface JscpdTotals { + lines: number; + tokens: number; + sources: number; + clones: number; + duplicatedLines: number; + duplicatedTokens: number; + percentage: number; + percentageTokens: number; +} + +interface JscpdReport { + statistics: { detectionDate?: string; total: JscpdTotals }; + duplicates: JscpdDuplicate[]; +} + +type JscpdFn = ( + argv: string[], + exitCallback?: (code: number) => {}, +) => Promise; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + includeTests: boolean; + minTokens: number; + minLines: number; + top: number; + help: boolean; +} + +function parseIntArg(arg: string, next: string | undefined): number { + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error(`${arg} requires a non-negative integer value`); + } + return parseInt(next, 10); +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "duplication-report"), + includeTests: false, + minTokens: 50, + minLines: 5, + top: 25, + help: false, + }; + + // Normalize "--key=value" into ["--key", "value"]. + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--include-tests": + opts.includeTests = true; + break; + case "--min-tokens": + opts.minTokens = parseIntArg(arg, next); + i++; + break; + case "--min-lines": + opts.minLines = parseIntArg(arg, next); + i++; + break; + case "--top": + opts.top = parseIntArg(arg, next); + i++; + break; + case "--root": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + + return opts; +} + +const HELP = `Copy/paste (code duplication) report for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/duplicationReport.ts [options] + npm run code-duplication -- [options] + +Options: + --include-tests Include test files (excluded by default). + --min-tokens Minimum token length of a clone (default 50). + --min-lines Minimum line length of a clone (default 5). + --top Number of worst offenders to print / embed (default 25). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/duplication-report). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// Analysis +// --------------------------------------------------------------------------- + +interface CloneRecord { + format: string; + lines: number; + tokens: number; + crossPackage: boolean; + packageA: string; + fileA: string; + startA: number; + endA: number; + packageB: string; + fileB: string; + startB: number; + endB: number; +} + +interface PackageRollup { + pkg: string; + clones: number; + duplicatedLines: number; + crossPackageClones: number; +} + +interface AnalysisResult { + clones: CloneRecord[]; + totals: JscpdTotals; + perFormat: { format: string; clones: number; duplicatedLines: number }[]; + perPackage: PackageRollup[]; + crossPackagePairs: { + a: string; + b: string; + clones: number; + lines: number; + }[]; + elapsedMs: number; +} + +/** + * Derive a package key for a repo-relative file path. Everything before the + * first `/src/` segment identifies the package (handles nested packages such as + * `packages/dispatcher/dispatcher`). Falls back to the leading path segments. + */ +function packageKeyOf(file: string): string { + const f = file.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +async function analyze(opts: Options): Promise { + const started = Date.now(); + + // Scan relative to the root so jscpd emits root-relative paths. + const originalCwd = process.cwd(); + process.chdir(opts.root); + + const rawDir = path.join(opts.outDir, ".jscpd"); + fs.rmSync(rawDir, { recursive: true, force: true }); + fs.mkdirSync(rawDir, { recursive: true }); + + const scanTargets = SCAN_SUBDIRS.filter((d) => fs.existsSync(d)); + if (scanTargets.length === 0) { + throw new Error( + `No source subdirectories (${SCAN_SUBDIRS.join(", ")}) found under ${opts.root}`, + ); + } + + const ignores = [...IGNORE_DIRS, ...IGNORE_FILES]; + if (!opts.includeTests) { + ignores.push(...TEST_GLOBS); + } + + const argv = [ + process.execPath, + "jscpd", + ...scanTargets, + "--min-tokens", + String(opts.minTokens), + "--min-lines", + String(opts.minLines), + "--format", + CODE_FORMATS, + "--formats-exts", + FORMATS_EXTS, + "--reporters", + "json", + "--mode", + "mild", + "--output", + rawDir, + "--ignore", + ignores.join(","), + "--gitignore", + "--silent", + ]; + + const { jscpd } = require("jscpd") as { jscpd: JscpdFn }; + // The exit callback intercepts jscpd's process.exit (it exits non-zero when + // a duplication threshold is crossed); we only want the report. + await jscpd(argv, (() => ({})) as (code: number) => {}); + + process.chdir(originalCwd); + + const reportPath = path.join(rawDir, "jscpd-report.json"); + if (!fs.existsSync(reportPath)) { + throw new Error(`jscpd did not produce a report at ${reportPath}`); + } + const report = JSON.parse( + fs.readFileSync(reportPath, "utf8"), + ) as JscpdReport; + + const norm = (p: string) => p.replace(/\\/g, "/"); + const clones: CloneRecord[] = (report.duplicates ?? []).map((d) => { + const fileA = norm(d.firstFile.name); + const fileB = norm(d.secondFile.name); + const packageA = packageKeyOf(fileA); + const packageB = packageKeyOf(fileB); + return { + format: d.format, + lines: d.lines, + tokens: d.tokens, + crossPackage: packageA !== packageB, + packageA, + fileA, + startA: d.firstFile.start, + endA: d.firstFile.end, + packageB, + fileB, + startB: d.secondFile.start, + endB: d.secondFile.end, + }; + }); + clones.sort((a, b) => b.lines - a.lines); + + // Per-format rollup. + const fmtMap = new Map< + string, + { clones: number; duplicatedLines: number } + >(); + for (const c of clones) { + const r = fmtMap.get(c.format) ?? { clones: 0, duplicatedLines: 0 }; + r.clones++; + r.duplicatedLines += c.lines; + fmtMap.set(c.format, r); + } + const perFormat = [...fmtMap.entries()] + .map(([format, v]) => ({ format, ...v })) + .sort((a, b) => b.duplicatedLines - a.duplicatedLines); + + // Per-package rollup (a clone contributes to both of its packages). + const pkgMap = new Map(); + const bump = (pkg: string, lines: number, cross: boolean) => { + const r = + pkgMap.get(pkg) ?? + ({ + pkg, + clones: 0, + duplicatedLines: 0, + crossPackageClones: 0, + } as PackageRollup); + r.clones++; + r.duplicatedLines += lines; + if (cross) { + r.crossPackageClones++; + } + pkgMap.set(pkg, r); + }; + for (const c of clones) { + bump(c.packageA, c.lines, c.crossPackage); + if (c.packageB !== c.packageA) { + bump(c.packageB, c.lines, c.crossPackage); + } + } + const perPackage = [...pkgMap.values()].sort( + (a, b) => b.duplicatedLines - a.duplicatedLines, + ); + + // Cross-package pair rollup (the consolidation targets). + const pairMap = new Map< + string, + { a: string; b: string; clones: number; lines: number } + >(); + for (const c of clones) { + if (!c.crossPackage) { + continue; + } + const [a, b] = [c.packageA, c.packageB].sort(); + const key = `${a}\u0000${b}`; + const r = pairMap.get(key) ?? { a, b, clones: 0, lines: 0 }; + r.clones++; + r.lines += c.lines; + pairMap.set(key, r); + } + const crossPackagePairs = [...pairMap.values()].sort( + (x, y) => y.lines - x.lines, + ); + + return { + clones, + totals: report.statistics.total, + perFormat, + perPackage, + crossPackagePairs, + elapsedMs: Date.now() - started, + }; +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function distribution(clones: CloneRecord[]): number[] { + const counts = new Array(BUCKETS.length).fill(0); + for (const c of clones) { + const idx = BUCKETS.findIndex( + (b) => c.lines >= b.lo && c.lines <= b.hi, + ); + if (idx >= 0) { + counts[idx]++; + } + } + return counts; +} + +function csvEscape(value: string | number | boolean): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +// --------------------------------------------------------------------------- +// Report writers +// --------------------------------------------------------------------------- + +function writeCsv(outDir: string, clones: CloneRecord[]): string { + const header = [ + "DuplicatedLines", + "Tokens", + "Format", + "CrossPackage", + "PackageA", + "FileA", + "StartA", + "EndA", + "PackageB", + "FileB", + "StartB", + "EndB", + ].join(","); + const rows = clones.map((c) => + [ + c.lines, + c.tokens, + c.format, + c.crossPackage, + c.packageA, + c.fileA, + c.startA, + c.endA, + c.packageB, + c.fileB, + c.startB, + c.endB, + ] + .map(csvEscape) + .join(","), + ); + const file = path.join(outDir, "clones.csv"); + fs.writeFileSync(file, [header, ...rows].join("\n") + "\n", "utf8"); + return file; +} + +function writeJson( + outDir: string, + opts: Options, + result: AnalysisResult, +): string { + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + includeTests: opts.includeTests, + thresholds: { minTokens: opts.minTokens, minLines: opts.minLines }, + totals: result.totals, + crossPackageClones: result.clones.filter((c) => c.crossPackage).length, + distribution: BUCKETS.map((b, i) => ({ + label: b.label, + lo: b.lo, + hi: b.hi === Infinity ? null : b.hi, + count: distribution(result.clones)[i], + })), + perFormat: result.perFormat, + perPackage: result.perPackage, + crossPackagePairs: result.crossPackagePairs, + clones: result.clones, + }; + const file = path.join(outDir, "report.json"); + fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", "utf8"); + return file; +} + +function writeHtml( + outDir: string, + opts: Options, + result: AnalysisResult, +): string { + const t = result.totals; + const dist = distribution(result.clones); + const maxDist = Math.max(1, ...dist); + const crossCount = result.clones.filter((c) => c.crossPackage).length; + + const distRows = BUCKETS.map((b, i) => { + const w = Math.round((dist[i] / maxDist) * 100); + return `${htmlEscape(b.label)}${num(dist[i])} + `; + }).join("\n"); + + const pkgRows = result.perPackage + .slice(0, 40) + .map( + (p) => + `${htmlEscape(p.pkg)}${num(p.duplicatedLines)} + ${num(p.clones)}${num(p.crossPackageClones)}`, + ) + .join("\n"); + + const pairRows = result.crossPackagePairs + .slice(0, opts.top) + .map( + (p) => + `${num(p.lines)}${num(p.clones)} + ${htmlEscape(p.a)}${htmlEscape(p.b)}`, + ) + .join("\n"); + + const cloneRows = result.clones + .slice(0, Math.max(opts.top, 100)) + .map( + (c) => + ` + ${num(c.lines)}${num(c.tokens)} + ${c.crossPackage ? "yes" : ""} + ${htmlEscape(c.fileA)}:${c.startA}-${c.endA} + ${htmlEscape(c.fileB)}:${c.startB}-${c.endB}`, + ) + .join("\n"); + + return ` + +TypeAgent duplication report + +

TypeAgent duplication report

+
engine: jscpd · generated ${htmlEscape(new Date().toISOString())} · + min-tokens ${opts.minTokens}, min-lines ${opts.minLines} · + tests ${opts.includeTests ? "included" : "excluded"}
+ +
+
${num(t.clones)}
clone pairs
+
${num(t.duplicatedLines)}
duplicated lines (${t.percentage}%)
+
${num(crossCount)}
cross-package clones
+
${num(t.sources)}
files scanned
+
${num(t.lines)}
lines scanned
+
+ +

Clone size distribution

+ +${distRows}
BucketClones 
+ +

Cross-package hotspots (consolidation targets)

+ +${pairRows || ''}
Dup linesClonesPackage APackage B
None
+ +

Packages by duplicated lines

+ +${pkgRows}
PackageDup linesClonesCross-pkg
+ +

Largest clones

+ +${cloneRows}
LinesTokensCrossLocation ALocation B
+ + +`; +} + +// --------------------------------------------------------------------------- +// Console summary +// --------------------------------------------------------------------------- + +function printSummary(opts: Options, result: AnalysisResult): void { + const t = result.totals; + const dist = distribution(result.clones); + const crossCount = result.clones.filter((c) => c.crossPackage).length; + + console.log(""); + console.log("Duplication report (engine: jscpd)"); + console.log( + `Scanned ${num(t.sources)} files, ${num(t.lines)} lines | ` + + `elapsed ${(result.elapsedMs / 1000).toFixed(1)}s | ` + + `min-tokens ${opts.minTokens}, min-lines ${opts.minLines}`, + ); + console.log(""); + console.log( + `Clones: ${num(t.clones)} | Duplicated: ${num(t.duplicatedLines)} lines ` + + `(${t.percentage}%), ${num(t.duplicatedTokens)} tokens (${t.percentageTokens}%)`, + ); + console.log(`Cross-package clones: ${num(crossCount)}`); + + console.log(""); + console.log("Clone size distribution:"); + BUCKETS.forEach((b, i) => { + console.log(` ${b.label.padEnd(14)} ${num(dist[i])}`); + }); + + const topClones = result.clones.slice(0, opts.top); + if (topClones.length) { + console.log(""); + console.log(`Largest clones (top ${topClones.length}):`); + for (const c of topClones) { + const tag = c.crossPackage ? " [cross-pkg]" : ""; + console.log( + ` ${String(c.lines).padStart(4)}L ${c.fileA}:${c.startA}-${c.endA}` + + ` <-> ${c.fileB}:${c.startB}-${c.endB}${tag}`, + ); + } + } + + if (result.crossPackagePairs.length) { + console.log(""); + console.log("Cross-package hotspots (consolidation targets):"); + for (const p of result.crossPackagePairs.slice(0, opts.top)) { + console.log( + ` ${String(p.lines).padStart(4)}L / ${String(p.clones).padStart(2)} clones ` + + `${p.a} <-> ${p.b}`, + ); + } + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + fs.mkdirSync(opts.outDir, { recursive: true }); + const result = await analyze(opts); + + const csv = writeCsv(opts.outDir, result.clones); + const json = writeJson(opts.outDir, opts, result); + const html = writeHtml(opts.outDir, opts, result); + fs.writeFileSync(path.join(opts.outDir, "report.html"), html, "utf8"); + + printSummary(opts, result); + console.log(""); + console.log("Reports written to:"); + console.log(` ${path.relative(opts.root, csv).split(path.sep).join("/")}`); + console.log( + ` ${path.relative(opts.root, json).split(path.sep).join("/")}`, + ); + console.log( + ` ${path.relative(opts.root, path.join(opts.outDir, "report.html")).split(path.sep).join("/")}`, + ); +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); From 7f17f02a28e64bb4131102f230a1e7050c31c22c Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Jul 2026 13:45:52 -0700 Subject: [PATCH 5/9] added more code quality tooling. --- ts/.gitignore | 12 + ts/package.json | 6 + ts/pnpm-lock.yaml | 1343 +++++++++++++++++-- ts/tools/scripts/code/circularDepsReport.ts | 654 +++++++++ ts/tools/scripts/code/deadCodeReport.ts | 528 ++++++++ ts/tools/scripts/code/debtMarkersReport.ts | 678 ++++++++++ ts/tools/scripts/code/knip.jsonc | 14 + ts/tools/scripts/code/lintReport.ts | 878 ++++++++++++ 8 files changed, 4003 insertions(+), 110 deletions(-) create mode 100644 ts/tools/scripts/code/circularDepsReport.ts create mode 100644 ts/tools/scripts/code/deadCodeReport.ts create mode 100644 ts/tools/scripts/code/debtMarkersReport.ts create mode 100644 ts/tools/scripts/code/knip.jsonc create mode 100644 ts/tools/scripts/code/lintReport.ts diff --git a/ts/.gitignore b/ts/.gitignore index 56c92868f8..292c219962 100644 --- a/ts/.gitignore +++ b/ts/.gitignore @@ -32,6 +32,18 @@ tools/scripts/code/duplication-report/ # Generated consistency report output (tools/scripts/code/consistencyReport.ts). tools/scripts/code/consistency-report/ +# Generated lint report output (tools/scripts/code/lintReport.ts). +tools/scripts/code/lint-report/ + +# Generated circular-dependency report output (tools/scripts/code/circularDepsReport.ts). +tools/scripts/code/circular-report/ + +# Generated dead-code report output (tools/scripts/code/deadCodeReport.ts). +tools/scripts/code/deadcode-report/ + +# Generated debt-markers report output (tools/scripts/code/debtMarkersReport.ts). +tools/scripts/code/debt-report/ + # Per-developer YAML config overrides loaded by @typeagent/config. # Holds local endpoints, deployment names, and (in Phase 1) secrets. # Never commit. diff --git a/ts/package.json b/ts/package.json index a52c7e3059..e67f8bd1a5 100644 --- a/ts/package.json +++ b/ts/package.json @@ -25,9 +25,13 @@ "clean": "fluid-build . -t clean", "cli": "pnpm -C packages/cli run start", "cli:dev": "pnpm -C packages/cli run start:dev", + "code-circular": "npx tsx tools/scripts/code/circularDepsReport.ts", "code-complexity": "npx tsx tools/scripts/code/complexityReport.ts", "code-consistency": "npx tsx tools/scripts/code/consistencyReport.ts", + "code-deadcode": "npx tsx tools/scripts/code/deadCodeReport.ts", + "code-debt": "npx tsx tools/scripts/code/debtMarkersReport.ts", "code-duplication": "npx tsx tools/scripts/code/duplicationReport.ts", + "code-lint": "npx tsx tools/scripts/code/lintReport.ts", "copilot": "node packages/copilot-plugin/scripts/launch.mjs", "copilot:dev": "node packages/copilot-plugin/scripts/launch-dev.mjs", "devtunnel:setup": "node tools/scripts/setup-devtunnel.mjs", @@ -79,6 +83,8 @@ "eslint": "^10.6.0", "eslint-plugin-sonarjs": "^4.1.0", "jscpd": "^4.2.5", + "knip": "^6.24.0", + "madge": "^8.0.0", "markdown-link-check": "^3.14.2", "prettier": "^3.5.3", "shx": "^0.4.0", diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 16b32d1573..9ea89b9954 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -29,13 +29,19 @@ importers: version: 22.15.18 eslint: specifier: ^10.6.0 - version: 10.6.0(jiti@2.5.1) + version: 10.6.0(jiti@2.7.0) eslint-plugin-sonarjs: specifier: ^4.1.0 - version: 4.1.0(eslint@10.6.0(jiti@2.5.1)) + version: 4.1.0(eslint@10.6.0(jiti@2.7.0)) jscpd: specifier: ^4.2.5 version: 4.2.5 + knip: + specifier: ^6.24.0 + version: 6.24.0 + madge: + specifier: ^8.0.0 + version: 8.0.0(typescript@5.9.3) markdown-link-check: specifier: ^3.14.2 version: 3.14.2 @@ -50,7 +56,7 @@ importers: version: 4.21.0 typescript-eslint: specifier: ^8.62.1 - version: 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + version: 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) examples/agentExamples/echo: dependencies: @@ -2098,7 +2104,7 @@ importers: version: 9.5.2(typescript@5.4.5)(webpack@5.105.0(esbuild@0.28.1)) vite: specifier: ^6.4.3 - version: 6.4.3(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.3(@types/node@22.15.18)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3) packages/agents/calendar: dependencies: @@ -2707,7 +2713,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.3 - version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) packages/agents/montage: dependencies: @@ -5629,7 +5635,7 @@ importers: version: 26.8.1(dmg-builder@26.8.1) electron-vite: specifier: ^4.0.1 - version: 4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@25.9.3)(ts-node@10.9.2(@types/node@25.9.3)(typescript@5.4.5)) @@ -5650,7 +5656,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.3 - version: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) packages/studio-service: dependencies: @@ -6304,13 +6310,13 @@ importers: version: 16.5.0 eslint: specifier: ^10.6.0 - version: 10.6.0(jiti@2.5.1) + version: 10.6.0(jiti@2.7.0) eslint-plugin-sonarjs: specifier: ^4.1.0 - version: 4.1.0(eslint@10.6.0(jiti@2.5.1)) + version: 4.1.0(eslint@10.6.0(jiti@2.7.0)) typescript-eslint: specifier: ^8.62.1 - version: 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + version: 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) tools/docsAutogen: dependencies: @@ -7158,6 +7164,10 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@dependents/detective-less@5.0.3': + resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==} + engines: {node: '>=18'} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -7166,6 +7176,10 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@discoveryjs/json-ext@1.1.0': + resolution: {integrity: sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==} + engines: {node: '>=14.17.0'} + '@elastic/elasticsearch@9.3.4': resolution: {integrity: sha512-Mp14fPEYx+WTfZdcvAaZ9WkLYGHQCbwMx6EP5VCucYdhv4cn/g2sbnMT5HzK+gX3XEpBnnkEK/+WysCKzxuo3A==} engines: {node: '>=18'} @@ -7224,12 +7238,24 @@ packages: engines: {node: '>=14.14'} hasBin: true + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} '@emnapi/runtime@1.4.0': resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild-plugins/node-globals-polyfill@0.2.3': resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} peerDependencies: @@ -8940,6 +8966,12 @@ packages: resolution: {integrity: sha512-ypTJ/DXzsJbTU3o7qXFlWmZGgEbh42JWQl7v5/i+DJz/HURELcSnq9ler9e1ukqma70JzmCQcIseiE/Xs6sczw==} engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -9036,6 +9068,239 @@ packages: resolution: {integrity: sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==} engines: {node: '>=14'} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.137.0': + resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.137.0': + resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.137.0': + resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.137.0': + resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.3': + resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.21.3': + resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} + cpu: [x64] + os: [win32] + '@peculiar/asn1-cms@2.8.0': resolution: {integrity: sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==} @@ -9606,6 +9871,22 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@ts-graphviz/adapter@2.0.6': + resolution: {integrity: sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==} + engines: {node: '>=18'} + + '@ts-graphviz/ast@2.0.7': + resolution: {integrity: sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==} + engines: {node: '>=18'} + + '@ts-graphviz/common@2.1.5': + resolution: {integrity: sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==} + engines: {node: '>=18'} + + '@ts-graphviz/core@2.0.7': + resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} + engines: {node: '>=18'} + '@tsconfig/node10@1.0.9': resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -9618,6 +9899,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -10290,15 +10574,27 @@ packages: '@vue/compiler-core@3.5.16': resolution: {integrity: sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==} + '@vue/compiler-core@3.5.39': + resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} + '@vue/compiler-dom@3.5.16': resolution: {integrity: sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==} + '@vue/compiler-dom@3.5.39': + resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} + '@vue/compiler-sfc@3.5.16': resolution: {integrity: sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==} + '@vue/compiler-sfc@3.5.39': + resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==} + '@vue/compiler-ssr@3.5.16': resolution: {integrity: sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==} + '@vue/compiler-ssr@3.5.39': + resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==} + '@vue/reactivity@3.5.16': resolution: {integrity: sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==} @@ -10316,6 +10612,9 @@ packages: '@vue/shared@3.5.16': resolution: {integrity: sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==} + '@vue/shared@3.5.39': + resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} + '@web/browser-logs@0.4.1': resolution: {integrity: sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==} engines: {node: '>=18.0.0'} @@ -10640,6 +10939,9 @@ packages: dmg-builder: 26.8.1 electron-builder-squirrel-windows: 26.8.1 + app-module-path@2.2.0: + resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} + archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -10721,6 +11023,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-module-types@6.0.2: + resolution: {integrity: sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==} + engines: {node: '>=18'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -11357,6 +11663,9 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compare-version@0.1.2: resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} engines: {node: '>=0.10.0'} @@ -11912,6 +12221,11 @@ packages: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} + dependency-tree@11.5.0: + resolution: {integrity: sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg==} + engines: {node: '>=18'} + hasBin: true + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -11947,6 +12261,49 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + detective-amd@6.1.0: + resolution: {integrity: sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==} + engines: {node: '>=18'} + hasBin: true + + detective-cjs@6.1.1: + resolution: {integrity: sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==} + engines: {node: '>=18'} + + detective-es6@5.0.2: + resolution: {integrity: sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==} + engines: {node: '>=18'} + + detective-postcss@8.0.4: + resolution: {integrity: sha512-DZ7M/hWPZyr17ZUdoQ+TVXaPj70mYr4XXrAE+GeJbca44haCvZgb191L/jLJmFYewhxRJuBd4lUtNSu986TXag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4.47 + + detective-sass@6.0.2: + resolution: {integrity: sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==} + engines: {node: '>=18'} + + detective-scss@5.0.2: + resolution: {integrity: sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==} + engines: {node: '>=18'} + + detective-stylus@5.0.1: + resolution: {integrity: sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==} + engines: {node: '>=18'} + + detective-typescript@14.1.2: + resolution: {integrity: sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + + detective-vue2@2.3.0: + resolution: {integrity: sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -12156,6 +12513,10 @@ packages: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.24.1: + resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} + engines: {node: '>=10.13.0'} + entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} @@ -12167,6 +12528,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -12495,6 +12860,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -12520,6 +12888,11 @@ packages: filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + filing-cabinet@5.5.1: + resolution: {integrity: sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==} + engines: {node: '>=18'} + hasBin: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -12608,6 +12981,11 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + formdata-node@4.4.1: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} @@ -12696,6 +13074,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-amd-module-type@6.0.2: + resolution: {integrity: sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==} + engines: {node: '>=18'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -12713,6 +13095,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -12831,6 +13216,11 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} + gonzales-pe@4.3.0: + resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} + engines: {node: '>=0.6.0'} + hasBin: true + google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} @@ -13295,6 +13685,10 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} @@ -13397,6 +13791,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -13434,6 +13832,10 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + is-relative-url@4.1.0: resolution: {integrity: sha512-vhIXKasjAuxS7n+sdv7pJQykEAgS+YU8VBQOENXwo/VZpOHDgBBsIbHo7zFKaWBjYWF4qxERdhbPRRtFAeJKfg==} engines: {node: '>=14.16'} @@ -13478,6 +13880,10 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-url-superb@4.0.0: + resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} + engines: {node: '>=10'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -13736,6 +14142,10 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -13915,6 +14325,11 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + knip@6.24.0: + resolution: {integrity: sha512-PokLlgeEjLh1rAsB7ts+52wZ37HBr1nDhE6NNONwEaXdeZGCJOkP7ZlIAI2Gtu8xohquzTWy75bc/1diI9shQw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + koa-compose@4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} @@ -14171,9 +14586,22 @@ packages: resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} engines: {node: '>=16.14'} + madge@8.0.0: + resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: ^5.4.4 + peerDependenciesMeta: + typescript: + optional: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mailparser@3.9.6: resolution: {integrity: sha512-EJYTDWMrOS1kddK1mTsRkrx2Ngh2nYsg54SRMWVVWGVEGbHH4tod8tqqU9hIRPgGQVboSjFubDn9cboSitbM3Q==} @@ -14558,6 +14986,16 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + module-definition@6.0.2: + resolution: {integrity: sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==} + engines: {node: '>=18'} + hasBin: true + + module-lookup-amd@9.1.3: + resolution: {integrity: sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==} + engines: {node: '>=18'} + hasBin: true + mongodb-connection-string-url@3.0.0: resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==} @@ -14620,6 +15058,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@5.1.5: resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} @@ -14736,6 +15179,10 @@ packages: resolution: {integrity: sha512-IWqZF6u0EI/07HTBm+zZ+MgXgWl09dnSJRGaDCPBSlOqilDcx6pj3Mpb3HvPN8V2Gr+ISw7ZrMsL7STWs1F++w==} engines: {node: '>=20'} + node-source-walk@7.0.2: + resolution: {integrity: sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==} + engines: {node: '>=18'} + nodemailer@8.0.4: resolution: {integrity: sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==} engines: {node: '>=6.0.0'} @@ -14901,6 +15348,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.137.0: + resolution: {integrity: sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.21.3: + resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -14988,6 +15442,10 @@ packages: resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} engines: {node: '>=16'} + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -15189,10 +15647,20 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-values-parser@6.0.2: + resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.2.9 + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} @@ -15204,6 +15672,11 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + precinct@12.3.2: + resolution: {integrity: sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==} + engines: {node: '>=18'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -15220,6 +15693,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + priorityqueuejs@2.0.0: resolution: {integrity: sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==} @@ -15509,6 +15986,9 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + quote-unquote@1.0.0: + resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -15656,6 +16136,15 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requirejs-config-file@4.0.0: + resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} + engines: {node: '>=10.13.0'} + + requirejs@2.3.8: + resolution: {integrity: sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==} + engines: {node: '>=0.4.0'} + hasBin: true + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -15670,6 +16159,10 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} + resolve-dependency-path@4.0.1: + resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -15692,6 +16185,11 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -15814,6 +16312,11 @@ packages: sanitize-filename@1.6.3: resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + sass-lookup@6.1.2: + resolution: {integrity: sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==} + engines: {node: '>=18'} + hasBin: true + sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} @@ -16090,6 +16593,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} + engines: {node: '>= 18'} + sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} @@ -16201,6 +16708,9 @@ packages: stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + stream-to-array@2.3.0: + resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} + streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} @@ -16244,6 +16754,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -16256,6 +16770,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -16276,6 +16794,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -16288,6 +16810,11 @@ packages: stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + stylus-lookup@6.1.2: + resolution: {integrity: sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==} + engines: {node: '>=18'} + hasBin: true + sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -16339,6 +16866,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -16459,6 +16990,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tlds@1.261.0: resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} hasBin: true @@ -16563,6 +17098,10 @@ packages: resolution: {integrity: sha512-akcpDTPuez4xzULo5NwuoKwYRtjQJ9eoNfBACiBMaXwNAx7B1PKfe5wqUFJuW5uKzQ68YjDFwPaWHDG1KnFGsA==} engines: {node: '>=14.13.1'} + ts-graphviz@2.1.6: + resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==} + engines: {node: '>=18'} + ts-jest@29.3.3: resolution: {integrity: sha512-y6jLm19SL4GroiBmHwFK4dSHUfDNmOrJbRfp6QmDIlI9p5tT5Q8ItccB4pTIslCIqOZuQnBwpTR0bQ5eUMYwkw==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -16635,6 +17174,10 @@ packages: '@swc/wasm': optional: true + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -16746,6 +17289,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + typical@4.0.0: resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} engines: {node: '>=8'} @@ -16762,6 +17310,10 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + unbash@4.0.2: + resolution: {integrity: sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -17057,6 +17609,14 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + walkdir@0.4.1: + resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} + engines: {node: '>=6.0.0'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -19056,6 +19616,11 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@dependents/detective-less@5.0.3': + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.15.0 @@ -19063,6 +19628,8 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@discoveryjs/json-ext@1.1.0': {} + '@elastic/elasticsearch@9.3.4': dependencies: '@elastic/transport': 9.3.5 @@ -19192,6 +19759,23 @@ snapshots: - supports-color optional: true + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 @@ -19202,6 +19786,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.28.1)': dependencies: esbuild: 0.28.1 @@ -19509,9 +20098,9 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.5.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))': dependencies: - eslint: 10.6.0(jiti@2.5.1) + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -21075,6 +21664,20 @@ snapshots: '@napi-rs/canvas-win32-x64-msvc': 0.1.72 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@noble/hashes@1.4.0': {} '@nodable/entities@2.1.0': {} @@ -21184,66 +21787,193 @@ snapshots: - react-devtools-core - utf-8-validate - '@oozcitak/dom@2.0.2': - dependencies: - '@oozcitak/infra': 2.0.2 - '@oozcitak/url': 3.0.0 - '@oozcitak/util': 10.0.0 + '@oozcitak/dom@2.0.2': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/url': 3.0.0 + '@oozcitak/util': 10.0.0 + + '@oozcitak/infra@2.0.2': + dependencies: + '@oozcitak/util': 10.0.0 + + '@oozcitak/url@3.0.0': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + + '@oozcitak/util@10.0.0': {} + + '@open-wc/dedupe-mixin@2.0.1': {} + + '@open-wc/scoped-elements@3.0.6': + dependencies: + '@open-wc/dedupe-mixin': 2.0.1 + lit: 3.3.2 + + '@open-wc/semantic-dom-diff@0.20.1': + dependencies: + '@types/chai': 4.3.20 + '@web/test-runner-commands': 0.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@open-wc/testing-helpers@3.0.1': + dependencies: + '@open-wc/scoped-elements': 3.0.6 + lit: 3.3.2 + lit-html: 3.3.2 + + '@open-wc/testing@4.0.0': + dependencies: + '@esm-bundle/chai': 4.3.4-fix.0 + '@open-wc/semantic-dom-diff': 0.20.1 + '@open-wc/testing-helpers': 3.0.1 + '@types/chai-dom': 1.11.3 + '@types/sinon-chai': 3.2.12 + chai-a11y-axe: 1.5.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.34.0 + + '@opentelemetry/semantic-conventions@1.34.0': {} + + '@oxc-parser/binding-android-arm-eabi@0.137.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.137.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + optional: true + + '@oxc-project/types@0.137.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + optional: true + + '@oxc-resolver/binding-android-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.21.3': + optional: true - '@oozcitak/infra@2.0.2': - dependencies: - '@oozcitak/util': 10.0.0 + '@oxc-resolver/binding-freebsd-x64@11.21.3': + optional: true - '@oozcitak/url@3.0.0': - dependencies: - '@oozcitak/infra': 2.0.2 - '@oozcitak/util': 10.0.0 + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + optional: true - '@oozcitak/util@10.0.0': {} + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + optional: true - '@open-wc/dedupe-mixin@2.0.1': {} + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + optional: true - '@open-wc/scoped-elements@3.0.6': - dependencies: - '@open-wc/dedupe-mixin': 2.0.1 - lit: 3.3.2 + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + optional: true - '@open-wc/semantic-dom-diff@0.20.1': - dependencies: - '@types/chai': 4.3.20 - '@web/test-runner-commands': 0.9.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + optional: true - '@open-wc/testing-helpers@3.0.1': - dependencies: - '@open-wc/scoped-elements': 3.0.6 - lit: 3.3.2 - lit-html: 3.3.2 + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + optional: true - '@open-wc/testing@4.0.0': - dependencies: - '@esm-bundle/chai': 4.3.4-fix.0 - '@open-wc/semantic-dom-diff': 0.20.1 - '@open-wc/testing-helpers': 3.0.1 - '@types/chai-dom': 1.11.3 - '@types/sinon-chai': 3.2.12 - chai-a11y-axe: 1.5.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + optional: true - '@opentelemetry/api@1.9.0': {} + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + optional: true - '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)': + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.34.0 + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true - '@opentelemetry/semantic-conventions@1.34.0': {} + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + optional: true '@peculiar/asn1-cms@2.8.0': dependencies: @@ -21977,6 +22707,21 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + '@ts-graphviz/adapter@2.0.6': + dependencies: + '@ts-graphviz/common': 2.1.5 + + '@ts-graphviz/ast@2.0.7': + dependencies: + '@ts-graphviz/common': 2.1.5 + + '@ts-graphviz/common@2.1.5': {} + + '@ts-graphviz/core@2.0.7': + dependencies: + '@ts-graphviz/ast': 2.0.7 + '@ts-graphviz/common': 2.1.5 + '@tsconfig/node10@1.0.9': {} '@tsconfig/node12@1.0.11': {} @@ -21985,6 +22730,11 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/accepts@1.3.7': dependencies: '@types/node': 22.19.21 @@ -22621,40 +23371,40 @@ snapshots: '@types/node': 22.19.21 optional: true - '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5))(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.62.1 - eslint: 10.6.0(jiti@2.5.1) + eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.62.1 '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0(jiti@2.5.1) - typescript: 5.4.5 + eslint: 10.6.0(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.1(typescript@5.4.5)': + '@typescript-eslint/project-service@8.62.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.4.5) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) '@typescript-eslint/types': 8.62.1 debug: 4.4.3(supports-color@8.1.1) - typescript: 5.4.5 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -22663,47 +23413,47 @@ snapshots: '@typescript-eslint/types': 8.62.1 '@typescript-eslint/visitor-keys': 8.62.1 - '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.4.5)': + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)': dependencies: - typescript: 5.4.5 + typescript: 5.9.3 - '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0(jiti@2.5.1) - ts-api-utils: 2.5.0(typescript@5.4.5) - typescript: 5.4.5 + eslint: 10.6.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/typescript-estree@8.62.1(typescript@5.4.5)': + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.62.1(typescript@5.4.5) - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.4.5) + '@typescript-eslint/project-service': 8.62.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) '@typescript-eslint/types': 8.62.1 '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.8.4 tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.4.5) - typescript: 5.4.5 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5)': + '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.62.1 '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) - eslint: 10.6.0(jiti@2.5.1) - typescript: 5.4.5 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + eslint: 10.6.0(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -22842,11 +23592,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.39 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.16': dependencies: '@vue/compiler-core': 3.5.16 '@vue/shared': 3.5.16 + '@vue/compiler-dom@3.5.39': + dependencies: + '@vue/compiler-core': 3.5.39 + '@vue/shared': 3.5.39 + '@vue/compiler-sfc@3.5.16': dependencies: '@babel/parser': 7.28.4 @@ -22859,11 +23622,28 @@ snapshots: postcss: 8.5.14 source-map-js: 1.2.1 + '@vue/compiler-sfc@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.39 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.16 + source-map-js: 1.2.1 + '@vue/compiler-ssr@3.5.16': dependencies: '@vue/compiler-dom': 3.5.16 '@vue/shared': 3.5.16 + '@vue/compiler-ssr@3.5.39': + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 + '@vue/reactivity@3.5.16': dependencies: '@vue/shared': 3.5.16 @@ -22888,6 +23668,8 @@ snapshots: '@vue/shared@3.5.16': {} + '@vue/shared@3.5.39': {} + '@web/browser-logs@0.4.1': dependencies: errorstacks: 2.4.1 @@ -23380,6 +24162,8 @@ snapshots: transitivePeerDependencies: - supports-color + app-module-path@2.2.0: {} + archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -23479,6 +24263,8 @@ snapshots: assertion-error@2.0.1: {} + ast-module-types@6.0.2: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -24204,6 +24990,8 @@ snapshots: commander@9.5.0: {} + commondir@1.0.1: {} + compare-version@0.1.2: {} compress-commons@2.1.1: @@ -24831,6 +25619,16 @@ snapshots: dependency-graph@0.11.0: {} + dependency-tree@11.5.0: + dependencies: + '@discoveryjs/json-ext': 1.1.0 + commander: 12.1.0 + filing-cabinet: 5.5.1 + precinct: 12.3.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + dequal@2.0.3: {} destroy@1.2.0: {} @@ -24849,6 +25647,62 @@ snapshots: detect-node@2.1.0: {} + detective-amd@6.1.0: + dependencies: + ast-module-types: 6.0.2 + escodegen: 2.1.0 + get-amd-module-type: 6.0.2 + node-source-walk: 7.0.2 + + detective-cjs@6.1.1: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + detective-es6@5.0.2: + dependencies: + node-source-walk: 7.0.2 + + detective-postcss@8.0.4(postcss@8.5.14): + dependencies: + is-url-superb: 4.0.0 + postcss: 8.5.14 + postcss-values-parser: 6.0.2(postcss@8.5.14) + + detective-sass@6.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-scss@5.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-stylus@5.0.1: {} + + detective-typescript@14.1.2(typescript@5.9.3): + dependencies: + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + detective-vue2@2.3.0(typescript@5.9.3): + dependencies: + '@dependents/detective-less': 5.0.3 + '@vue/compiler-sfc': 3.5.39 + detective-es6: 5.0.2 + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -25045,7 +25899,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)): + electron-vite@4.0.1(vite@6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.7) @@ -25053,7 +25907,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.17 picocolors: 1.1.1 - vite: 6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -25121,12 +25975,19 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enhanced-resolve@5.24.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@2.2.0: {} entities@4.5.0: {} entities@6.0.1: {} + entities@7.0.1: {} + env-paths@2.2.1: {} envinfo@7.11.0: {} @@ -25369,12 +26230,12 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-sonarjs@4.1.0(eslint@10.6.0(jiti@2.5.1)): + eslint-plugin-sonarjs@4.1.0(eslint@10.6.0(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.6.0(jiti@2.5.1) + eslint: 10.6.0(jiti@2.7.0) functional-red-black-tree: 1.0.1 globals: 17.7.0 jsx-ast-utils-x: 0.1.0 @@ -25402,9 +26263,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0(jiti@2.5.1): + eslint@10.6.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -25435,7 +26296,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.5.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -25735,6 +26596,10 @@ snapshots: dependencies: bser: 2.1.1 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -25755,6 +26620,20 @@ snapshots: dependencies: minimatch: 5.1.9 + filing-cabinet@5.5.1: + dependencies: + app-module-path: 2.2.0 + commander: 12.1.0 + enhanced-resolve: 5.24.1 + module-definition: 6.0.2 + module-lookup-amd: 9.1.3 + resolve: 1.22.12 + resolve-dependency-path: 4.0.1 + sass-lookup: 6.1.2 + stylus-lookup: 6.1.2 + tsconfig-paths: 4.2.0 + typescript: 5.9.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -25854,6 +26733,10 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formdata-node@4.4.1: dependencies: node-domexception: 1.0.0 @@ -25953,6 +26836,11 @@ snapshots: gensync@1.0.0-beta.2: {} + get-amd-module-type@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} @@ -25972,6 +26860,8 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 + get-own-enumerable-property-symbols@3.0.2: {} + get-package-type@0.1.0: {} get-proto@1.0.1: @@ -26140,6 +27030,10 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + gonzales-pe@4.3.0: + dependencies: + minimist: 1.2.8 + google-auth-library@9.15.1(encoding@0.1.13): dependencies: base64-js: 1.5.1 @@ -26671,6 +27565,10 @@ snapshots: dependencies: hasown: 2.0.4 + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-data-view@1.0.2: dependencies: call-bound: 1.0.4 @@ -26750,6 +27648,8 @@ snapshots: is-number@7.0.0: {} + is-obj@1.0.1: {} + is-path-inside@3.0.3: {} is-plain-obj@2.1.0: {} @@ -26777,6 +27677,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + is-regexp@1.0.0: {} + is-relative-url@4.1.0: dependencies: is-absolute-url: 4.0.1 @@ -26812,6 +27714,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-url-superb@4.0.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -27447,6 +28351,8 @@ snapshots: jiti@2.5.1: {} + jiti@2.7.0: {} + jju@1.4.0: {} jose@5.10.0: {} @@ -27689,6 +28595,22 @@ snapshots: kleur@3.0.3: {} + knip@6.24.0: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + oxc-parser: 0.137.0 + oxc-resolver: 11.21.3 + picomatch: 4.0.4 + smol-toml: 1.7.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.2 + yaml: 2.9.0 + zod: 4.3.6 + koa-compose@4.1.0: {} koa-convert@2.0.0: @@ -27964,10 +28886,33 @@ snapshots: lru-cache@8.0.5: {} + madge@8.0.0(typescript@5.9.3): + dependencies: + chalk: 4.1.2 + commander: 7.2.0 + commondir: 1.0.1 + debug: 4.4.3(supports-color@8.1.1) + dependency-tree: 11.5.0 + ora: 5.4.1 + pluralize: 8.0.0 + pretty-ms: 7.0.1 + rc: 1.2.8 + stream-to-array: 2.3.0 + ts-graphviz: 2.1.6 + walkdir: 0.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + mailparser@3.9.6: dependencies: '@zone-eu/mailsplit': 5.4.8 @@ -28600,6 +29545,17 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 + module-definition@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + module-lookup-amd@9.1.3: + dependencies: + commander: 12.1.0 + requirejs: 2.3.8 + requirejs-config-file: 4.0.0 + mongodb-connection-string-url@3.0.0: dependencies: '@types/whatwg-url': 11.0.4 @@ -28644,6 +29600,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.15: {} + nanoid@5.1.5: {} napi-build-utils@2.0.0: {} @@ -28750,6 +29708,10 @@ snapshots: '@types/sarif': 2.1.7 fs-extra: 11.3.4 + node-source-walk@7.0.2: + dependencies: + '@babel/parser': 7.29.7 + nodemailer@8.0.4: {} noms@0.0.0: @@ -28937,6 +29899,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.137.0: + dependencies: + '@oxc-project/types': 0.137.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.137.0 + '@oxc-parser/binding-android-arm64': 0.137.0 + '@oxc-parser/binding-darwin-arm64': 0.137.0 + '@oxc-parser/binding-darwin-x64': 0.137.0 + '@oxc-parser/binding-freebsd-x64': 0.137.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.137.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.137.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.137.0 + '@oxc-parser/binding-linux-arm64-musl': 0.137.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.137.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-musl': 0.137.0 + '@oxc-parser/binding-openharmony-arm64': 0.137.0 + '@oxc-parser/binding-wasm32-wasi': 0.137.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.137.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.137.0 + '@oxc-parser/binding-win32-x64-msvc': 0.137.0 + + oxc-resolver@11.21.3: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.21.3 + '@oxc-resolver/binding-android-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-x64': 11.21.3 + '@oxc-resolver/binding-freebsd-x64': 11.21.3 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.3 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.3 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-musl': 11.21.3 + '@oxc-resolver/binding-openharmony-arm64': 11.21.3 + '@oxc-resolver/binding-wasm32-wasi': 11.21.3 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 + p-cancelable@2.1.1: {} p-event@4.2.0: @@ -29037,6 +30046,8 @@ snapshots: lines-and-columns: 2.0.4 type-fest: 3.13.1 + parse-ms@2.1.0: {} + parse-node-version@1.0.1: {} parse-semver@1.1.1: @@ -29214,12 +30225,25 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-values-parser@6.0.2(postcss@8.5.14): + dependencies: + color-name: 1.1.4 + is-url-superb: 4.0.0 + postcss: 8.5.14 + quote-unquote: 1.0.0 + postcss@8.5.14: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 @@ -29239,6 +30263,26 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + precinct@12.3.2: + dependencies: + '@dependents/detective-less': 5.0.3 + commander: 12.1.0 + detective-amd: 6.1.0 + detective-cjs: 6.1.1 + detective-es6: 5.0.2 + detective-postcss: 8.0.4(postcss@8.5.14) + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + detective-vue2: 2.3.0(typescript@5.9.3) + module-definition: 6.0.2 + node-source-walk: 7.0.2 + postcss: 8.5.14 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + prelude-ls@1.2.1: {} prettier@3.5.3: {} @@ -29254,6 +30298,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + priorityqueuejs@2.0.0: {} prismjs@1.30.0: {} @@ -29634,6 +30682,8 @@ snapshots: quick-lru@5.1.1: {} + quote-unquote@1.0.0: {} + range-parser@1.2.1: {} raw-body@2.5.2: @@ -29861,6 +30911,13 @@ snapshots: require-from-string@2.0.2: {} + requirejs-config-file@4.0.0: + dependencies: + esprima: 4.0.1 + stringify-object: 3.3.0 + + requirejs@2.3.8: {} + requires-port@1.0.0: {} resedit@1.7.2: @@ -29873,6 +30930,8 @@ snapshots: dependencies: resolve-from: 5.0.0 + resolve-dependency-path@4.0.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -29890,6 +30949,13 @@ snapshots: resolve.exports@2.0.3: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.8: dependencies: is-core-module: 2.13.1 @@ -30054,6 +31120,11 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 + sass-lookup@6.1.2: + dependencies: + commander: 12.1.0 + enhanced-resolve: 5.24.1 + sax@1.3.0: {} saxes@6.0.0: @@ -30466,6 +31537,8 @@ snapshots: smart-buffer@4.2.0: {} + smol-toml@1.7.0: {} + sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 @@ -30603,6 +31676,10 @@ snapshots: dependencies: duplexer: 0.1.2 + stream-to-array@2.3.0: + dependencies: + any-promise: 1.3.0 + streamx@2.22.0: dependencies: fast-fifo: 1.3.2 @@ -30670,6 +31747,12 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -30682,6 +31765,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom@3.0.0: {} + strip-bom@4.0.0: {} strip-eof@1.0.0: {} @@ -30692,6 +31777,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strnum@2.2.3: {} structured-source@4.0.0: @@ -30702,6 +31789,10 @@ snapshots: stylis@4.4.0: {} + stylus-lookup@6.1.2: + dependencies: + commander: 12.1.0 + sumchecker@3.0.1: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -30753,6 +31844,8 @@ snapshots: tapable@2.3.0: {} + tapable@2.3.3: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -30908,6 +32001,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tlds@1.261.0: {} tldts-core@5.7.112: {} @@ -30991,10 +32089,21 @@ snapshots: dependencies: typescript: 5.4.5 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-dedent@2.2.0: {} ts-deepmerge@7.0.2: {} + ts-graphviz@2.1.6: + dependencies: + '@ts-graphviz/adapter': 2.0.6 + '@ts-graphviz/ast': 2.0.7 + '@ts-graphviz/common': 2.1.5 + '@ts-graphviz/core': 2.0.7 + ts-jest@29.3.3(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.28.1)(jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 @@ -31132,6 +32241,12 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@1.14.1: {} tslib@2.6.2: {} @@ -31240,19 +32355,21 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.8 - typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5): + typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5))(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) - '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.4.5) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.5.1))(typescript@5.4.5) - eslint: 10.6.0(jiti@2.5.1) - typescript: 5.4.5 + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.6.0(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color typescript@5.4.5: {} + typescript@5.9.3: {} + typical@4.0.0: {} typical@7.3.0: {} @@ -31262,6 +32379,8 @@ snapshots: uglify-js@3.19.3: optional: true + unbash@4.0.2: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -31447,7 +32566,7 @@ snapshots: less: 4.3.0 terser: 5.39.2 - vite@6.4.3(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.3(@types/node@22.15.18)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -31458,13 +32577,13 @@ snapshots: optionalDependencies: '@types/node': 22.15.18 fsevents: 2.3.3 - jiti: 2.5.1 + jiti: 2.7.0 less: 4.3.0 terser: 5.39.2 tsx: 4.21.0 yaml: 2.8.3 - vite@6.4.3(@types/node@25.9.3)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.3(@types/node@25.9.3)(jiti@2.7.0)(less@4.3.0)(terser@5.39.2)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -31475,7 +32594,7 @@ snapshots: optionalDependencies: '@types/node': 25.9.3 fsevents: 2.3.3 - jiti: 2.5.1 + jiti: 2.7.0 less: 4.3.0 terser: 5.39.2 tsx: 4.21.0 @@ -31526,6 +32645,10 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} + + walkdir@0.4.1: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 diff --git a/ts/tools/scripts/code/circularDepsReport.ts b/ts/tools/scripts/code/circularDepsReport.ts new file mode 100644 index 0000000000..4e350e5a7d --- /dev/null +++ b/ts/tools/scripts/code/circularDepsReport.ts @@ -0,0 +1,654 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Circular-dependency report + ratchet gate for the TypeAgent ts/ tree. + * + * The engine is madge — the de-facto-standard dependency-graph tool — driven + * through its Node API (loaded via createRequire because the npx shim cannot + * resolve local bins in this environment). Type-only imports are excluded + * (skipTypeImports), so the cycles reported are real runtime import cycles, the + * ones that cause initialization-order bugs. + * + * Two modes: + * - Report (default): scan the source subtrees and write CSV/JSON/HTML + a + * console summary (cycle count, size distribution, per-package rollup, + * which cycles cross package boundaries). + * - Ratchet (--ratchet --base ): a stateless CI gate. It builds the + * cycle set for HEAD and for the merge base (checked out into a throwaway + * git worktree) and fails if HEAD introduces any cycle that is not already + * present at the base. The base branch is the baseline, so cycles can only + * trend down. Unlike the per-file lint/complexity ratchets, cycles are a + * whole-graph property, hence the worktree. + * + * Outputs (written to --out-dir, default tools/scripts/code/circular-report): + * - cycles.csv : every cycle, ranked by length + * - report.json : structured metrics + * - report.html : a self-contained, sortable report + * + * Usage: + * npx tsx tools/scripts/code/circularDepsReport.ts [options] + * npm run code-circular -- [options] + * + * Options: + * --include-tests Include test files (excluded by default). + * --top Number of cycles to print / embed (default 25). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default tools/scripts/code/circular-report). + * --ratchet CI gate: fail if changed code introduces new cycles vs --base. + * --base Base git ref for --ratchet (default origin/main). + * --help Show this help. + */ + +import { createRequire } from "node:module"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const SCAN_SUBDIRS = ["packages", "examples", "extensions", "tools"]; + +const FILE_EXTENSIONS = ["ts", "mts", "cts", "tsx"]; + +// madge excludeRegExp entries (matched against file paths). +const BASE_EXCLUDES = [ + "/dist/", + "/node_modules/", + "\\.d\\.ts$", + "/bin/", + "/build/", + "/out/", +]; +const TEST_EXCLUDES = [ + "/test/", + "/tests/", + "/__tests__/", + "\\.spec\\.", + "\\.test\\.", +]; + +// Cycle-length distribution buckets. +const BUCKETS: { label: string; lo: number; hi: number }[] = [ + { label: "2 files", lo: 2, hi: 2 }, + { label: "3 files", lo: 3, hi: 3 }, + { label: "4-5 files", lo: 4, hi: 5 }, + { label: "6+ files", lo: 6, hi: Infinity }, +]; + +// --------------------------------------------------------------------------- +// madge Node API +// --------------------------------------------------------------------------- + +interface MadgeResult { + circular(): string[][]; + obj(): Record; +} +type MadgeFn = ( + pathOrPaths: string | string[], + opts?: Record, +) => Promise; + +function madgeOptions(includeTests: boolean): Record { + return { + fileExtensions: FILE_EXTENSIONS, + excludeRegExp: includeTests + ? BASE_EXCLUDES + : [...BASE_EXCLUDES, ...TEST_EXCLUDES], + detectiveOptions: { + ts: { skipTypeImports: true }, + tsx: { skipTypeImports: true }, + }, + }; +} + +const norm = (p: string) => p.replace(/\\/g, "/"); + +/** Run madge from `cwd` over the existing SCAN_SUBDIRS; return cycles + file count. */ +async function runMadge( + cwd: string, + includeTests: boolean, +): Promise<{ cycles: string[][]; files: number }> { + const madge = require("madge") as MadgeFn; + const prev = process.cwd(); + process.chdir(cwd); + try { + const targets = SCAN_SUBDIRS.filter((d) => fs.existsSync(d)); + if (targets.length === 0) { + return { cycles: [], files: 0 }; + } + const res = await madge(targets, madgeOptions(includeTests)); + const cycles = res.circular().map((c) => c.map(norm)); + return { cycles, files: Object.keys(res.obj()).length }; + } finally { + process.chdir(prev); + } +} + +/** Rotate a cycle to start at its lexicographically smallest node (rotation-invariant key). */ +function canonicalKey(cycle: string[]): string { + if (cycle.length === 0) { + return ""; + } + let minIdx = 0; + for (let i = 1; i < cycle.length; i++) { + if (cycle[i] < cycle[minIdx]) { + minIdx = i; + } + } + return [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)].join(" > "); +} + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + includeTests: boolean; + top: number; + ratchet: boolean; + base: string; + help: boolean; +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "circular-report"), + includeTests: false, + top: 25, + ratchet: false, + base: "origin/main", + help: false, + }; + + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--include-tests": + opts.includeTests = true; + break; + case "--top": + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error("--top requires a positive integer"); + } + opts.top = parseInt(next, 10); + i++; + break; + case "--ratchet": + opts.ratchet = true; + break; + case "--base": + if (next === undefined) { + throw new Error("--base requires a git ref"); + } + opts.base = next; + i++; + break; + case "--root": + if (next === undefined) { + throw new Error("--root requires a path"); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error("--out-dir requires a path"); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + return opts; +} + +const HELP = `Circular-dependency report + ratchet gate for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/circularDepsReport.ts [options] + npm run code-circular -- [options] + +Options: + --include-tests Include test files (excluded by default). + --top Number of cycles to print / embed (default 25). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/circular-report). + --ratchet CI gate: fail if changed code introduces new cycles vs --base. + --base Base git ref for --ratchet (default origin/main). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// Rollups +// --------------------------------------------------------------------------- + +function packageKeyOf(rel: string): string { + const f = rel.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +interface CycleRecord { + length: number; + crossPackage: boolean; + packages: string[]; + nodes: string[]; +} + +interface AnalysisResult { + cycles: CycleRecord[]; + files: number; + perPackage: { pkg: string; cycles: number }[]; + crossPackageCount: number; + elapsedMs: number; +} + +function toRecords(cycles: string[][]): CycleRecord[] { + return cycles + .map((nodes) => { + const packages = [...new Set(nodes.map(packageKeyOf))].sort(); + return { + length: nodes.length, + crossPackage: packages.length > 1, + packages, + nodes, + }; + }) + .sort((a, b) => b.length - a.length); +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function csvEscape(value: string | number | boolean): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +function distribution(cycles: CycleRecord[]): number[] { + const counts = new Array(BUCKETS.length).fill(0); + for (const c of cycles) { + const idx = BUCKETS.findIndex( + (b) => c.length >= b.lo && c.length <= b.hi, + ); + if (idx >= 0) { + counts[idx]++; + } + } + return counts; +} + +// --------------------------------------------------------------------------- +// Report writers +// --------------------------------------------------------------------------- + +function writeCsv(outDir: string, cycles: CycleRecord[]): string { + const header = ["Length", "CrossPackage", "Packages", "Cycle"].join(","); + const rows = cycles.map((c) => + [c.length, c.crossPackage, c.packages.join(" | "), c.nodes.join(" > ")] + .map(csvEscape) + .join(","), + ); + const file = path.join(outDir, "cycles.csv"); + fs.writeFileSync(file, [header, ...rows].join("\n") + "\n", "utf8"); + return file; +} + +function writeJson(outDir: string, opts: Options, r: AnalysisResult): string { + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + includeTests: opts.includeTests, + totals: { + cycles: r.cycles.length, + files: r.files, + crossPackageCycles: r.crossPackageCount, + }, + distribution: BUCKETS.map((b, i) => ({ + label: b.label, + count: distribution(r.cycles)[i], + })), + perPackage: r.perPackage, + cycles: r.cycles, + }; + const file = path.join(outDir, "report.json"); + fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", "utf8"); + return file; +} + +function writeHtml(outDir: string, opts: Options, r: AnalysisResult): string { + const dist = distribution(r.cycles); + const maxDist = Math.max(1, ...dist); + const distRows = BUCKETS.map((b, i) => { + const w = Math.round((dist[i] / maxDist) * 100); + return `${htmlEscape(b.label)}${num(dist[i])} + `; + }).join("\n"); + + const pkgRows = r.perPackage + .slice(0, 60) + .map( + (p) => + `${htmlEscape(p.pkg)}${num(p.cycles)}`, + ) + .join("\n"); + + const cycleRows = r.cycles + .slice(0, Math.max(opts.top, 100)) + .map( + (c) => + `${c.length} + ${c.crossPackage ? "yes" : ""}${htmlEscape(c.nodes.join(" → "))}`, + ) + .join("\n"); + + const html = ` + +TypeAgent circular-dependency report + +

TypeAgent circular-dependency report

+
engine: madge (runtime imports only) · generated ${htmlEscape(new Date().toISOString())} · + tests ${opts.includeTests ? "included" : "excluded"}
+ +
+
${num(r.cycles.length)}
cycles
+
${num(r.crossPackageCount)}
cross-package cycles
+
${num(r.files)}
files in graph
+
+ +

Cycle size distribution

+ +${distRows}
BucketCycles 
+ +

Packages by cycle count

+ +${pkgRows || ''}
PackageCycles
None
+ +

Cycles

+ +${cycleRows || ''}
LengthCrossCycle
None found
+ + +`; + const file = path.join(outDir, "report.html"); + fs.writeFileSync(file, html, "utf8"); + return file; +} + +// --------------------------------------------------------------------------- +// Report mode +// --------------------------------------------------------------------------- + +async function runReport(opts: Options): Promise { + const started = Date.now(); + const { cycles, files } = await runMadge(opts.root, opts.includeTests); + const records = toRecords(cycles); + + const pkgMap = new Map(); + for (const c of records) { + for (const p of c.packages) { + pkgMap.set(p, (pkgMap.get(p) ?? 0) + 1); + } + } + const perPackage = [...pkgMap.entries()] + .map(([pkg, n]) => ({ pkg, cycles: n })) + .sort((a, b) => b.cycles - a.cycles || a.pkg.localeCompare(b.pkg)); + + const result: AnalysisResult = { + cycles: records, + files, + perPackage, + crossPackageCount: records.filter((c) => c.crossPackage).length, + elapsedMs: Date.now() - started, + }; + + fs.mkdirSync(opts.outDir, { recursive: true }); + const csv = writeCsv(opts.outDir, records); + const json = writeJson(opts.outDir, opts, result); + const html = writeHtml(opts.outDir, opts, result); + + const dist = distribution(records); + console.log(""); + console.log("Circular-dependency report (engine: madge, runtime imports)"); + console.log( + `Graph: ${num(files)} files | elapsed ${(result.elapsedMs / 1000).toFixed(1)}s`, + ); + console.log(""); + console.log( + `Cycles: ${num(records.length)} | cross-package: ${num(result.crossPackageCount)}`, + ); + console.log(""); + console.log("Size distribution:"); + BUCKETS.forEach((b, i) => + console.log(` ${b.label.padEnd(10)} ${num(dist[i])}`), + ); + console.log(""); + console.log( + `Top ${Math.min(opts.top, perPackage.length)} packages by cycles:`, + ); + for (const p of perPackage.slice(0, opts.top)) { + console.log(` ${String(p.cycles).padStart(4)} ${p.pkg}`); + } + console.log(""); + console.log(`Largest cycles (top ${Math.min(opts.top, records.length)}):`); + for (const c of records.slice(0, opts.top)) { + console.log( + ` ${String(c.length).padStart(2)} ${c.nodes.join(" > ")}${c.crossPackage ? " [cross-pkg]" : ""}`, + ); + } + console.log(""); + console.log("Reports written to:"); + for (const f of [csv, json, html]) { + console.log( + ` ${path.relative(opts.root, f).split(path.sep).join("/")}`, + ); + } + return 0; +} + +// --------------------------------------------------------------------------- +// Ratchet mode (CI gate) +// --------------------------------------------------------------------------- + +function git(args: string[], cwd: string): string { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + }); +} + +async function runRatchet(opts: Options): Promise { + let repoRoot: string; + let mergeBase: string; + try { + repoRoot = git(["rev-parse", "--show-toplevel"], opts.root).trim(); + mergeBase = git(["merge-base", opts.base, "HEAD"], opts.root).trim(); + } catch { + console.error( + `Ratchet: could not resolve base ref "${opts.base}" via git. ` + + "Pass --base (e.g. origin/main) and ensure it is fetched.", + ); + return 2; + } + + // HEAD cycles (current working tree). + const head = await runMadge(opts.root, opts.includeTests); + const headKeys = new Map(); + for (const c of head.cycles) { + headKeys.set(canonicalKey(c), c); + } + + // BASE cycles: check the merge base out into a throwaway worktree. + // Use a path git creates itself (worktree add refuses a pre-existing dir). + const worktree = path.join( + os.tmpdir(), + `circular-base-${process.pid}-${Date.now()}`, + ); + const baseKeys = new Set(); + try { + git(["worktree", "add", "--detach", worktree, mergeBase], repoRoot); + // opts.root is the ts/ dir relative to the repo root; mirror it in the worktree. + const rootRel = path.relative(repoRoot, opts.root); + const baseRoot = path.join(worktree, rootRel); + const base = await runMadge(baseRoot, opts.includeTests); + for (const c of base.cycles) { + baseKeys.add(canonicalKey(c)); + } + } catch (e) { + console.error(`Ratchet: failed to analyze base worktree: ${e}`); + return 2; + } finally { + try { + git(["worktree", "remove", "--force", worktree], repoRoot); + } catch { + fs.rmSync(worktree, { recursive: true, force: true }); + } + } + + const newCycles: string[][] = []; + for (const [key, cycle] of headKeys) { + if (!baseKeys.has(key)) { + newCycles.push(cycle); + } + } + + console.log( + `Ratchet: cycles base ${baseKeys.size} -> head ${headKeys.size}`, + ); + + if (newCycles.length > 0) { + console.error( + `\nRatchet FAILED: ${newCycles.length} new circular dependency(ies) introduced vs the base:`, + ); + for (const c of newCycles.slice(0, 50)) { + console.error(` ${c.join(" > ")}`); + } + console.error( + "\nBreak the cycle (e.g. extract the shared piece into a third module) before merging.", + ); + return 1; + } + console.log("Ratchet OK: no new circular dependencies."); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + const code = opts.ratchet ? await runRatchet(opts) : await runReport(opts); + process.exitCode = code; +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/ts/tools/scripts/code/deadCodeReport.ts b/ts/tools/scripts/code/deadCodeReport.ts new file mode 100644 index 0000000000..6b1eee5cb2 --- /dev/null +++ b/ts/tools/scripts/code/deadCodeReport.ts @@ -0,0 +1,528 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Dead-code report for the TypeAgent ts/ tree. + * + * The engine is knip — the standard unused-files / exports / dependencies + * detector for JS/TS monorepos. This wrapper runs knip's JSON reporter (via + * `pnpm exec`, because the npx shim cannot resolve local bins in this + * environment), then rolls the results up per category and per package and + * writes CSV/JSON/HTML + a console summary. + * + * Important: knip's accuracy depends entirely on its configuration. Out of the + * box it cannot know which files are entry points (agent action handlers loaded + * via manifest, CLI/webpack entries, benchmark harnesses), so its raw numbers + * heavily overcount "unused" files/exports. Tune tools/scripts/code/knip.jsonc + * (declare entry points, ignore generated output) and the numbers become real. + * For that reason this tool is report-only — there is no ratchet gate until the + * configuration produces a trustworthy baseline. + * + * Outputs (written to --out-dir, default tools/scripts/code/deadcode-report): + * - deadcode.csv : every finding (file, category, item) + * - report.json : structured metrics (per-category, per-package) + * - report.html : a self-contained, sortable report + * + * Usage: + * npx tsx tools/scripts/code/deadCodeReport.ts [options] + * npm run code-deadcode -- [options] + * + * Options: + * --top Number of rows to print / embed (default 25). + * --root Directory knip runs in (default: the ts/ root). + * --config knip config (default tools/scripts/code/knip.jsonc). + * --out-dir Output directory (default tools/scripts/code/deadcode-report). + * --help Show this help. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Categories +// --------------------------------------------------------------------------- + +// Categories that represent dead code within our source. +const DEAD_CODE_CATS = [ + "files", + "exports", + "types", + "enumMembers", + "namespaceMembers", + "duplicates", +]; + +// Categories that represent dependency / manifest hygiene. +const DEPENDENCY_CATS = [ + "dependencies", + "devDependencies", + "optionalPeerDependencies", + "unlisted", + "binaries", + "unresolved", +]; + +const ALL_CATS = [...DEAD_CODE_CATS, ...DEPENDENCY_CATS]; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + config: string; + top: number; + help: boolean; +} + +function parseArgs(argv: string[]): Options { + const root = path.resolve(__dirname, "..", "..", ".."); + const opts: Options = { + root, + outDir: path.join(__dirname, "deadcode-report"), + config: path.join(__dirname, "knip.jsonc"), + top: 25, + help: false, + }; + + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--top": + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error("--top requires a positive integer"); + } + opts.top = parseInt(next, 10); + i++; + break; + case "--root": + if (next === undefined) { + throw new Error("--root requires a path"); + } + opts.root = path.resolve(next); + i++; + break; + case "--config": + if (next === undefined) { + throw new Error("--config requires a path"); + } + opts.config = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error("--out-dir requires a path"); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + return opts; +} + +const HELP = `Dead-code report for the TypeAgent ts/ tree (engine: knip). + +Usage: + npx tsx tools/scripts/code/deadCodeReport.ts [options] + npm run code-deadcode -- [options] + +Options: + --top Number of rows to print / embed (default 25). + --root Directory knip runs in (default: the ts/ root). + --config knip config (default: tools/scripts/code/knip.jsonc). + --out-dir Output directory (default: tools/scripts/code/deadcode-report). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// knip invocation + parsing +// --------------------------------------------------------------------------- + +interface KnipIssue { + file: string; + [category: string]: unknown; +} + +function runKnip(opts: Options): KnipIssue[] { + const configArg = + opts.config && fs.existsSync(opts.config) + ? ` --config "${opts.config}"` + : ""; + const cmd = `pnpm exec knip --reporter json --no-progress${configArg}`; + let stdout: string; + try { + stdout = execSync(cmd, { + cwd: opts.root, + encoding: "utf8", + maxBuffer: 512 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (e) { + // knip exits non-zero when it finds issues; the JSON is still on stdout. + const err = e as { stdout?: Buffer | string; message?: string }; + stdout = err.stdout ? err.stdout.toString() : ""; + if (!stdout) { + throw new Error(`knip failed: ${err.message ?? e}`); + } + } + const data = JSON.parse(stdout) as { issues?: KnipIssue[] }; + return data.issues ?? []; +} + +/** Extract the human-readable item names for a category value. */ +function itemsOf(cat: string, value: unknown, file: string): string[] { + if (cat === "files") { + const flagged = Array.isArray(value) + ? value.length > 0 + : value === true; + return flagged ? [file] : []; + } + if (value && typeof value === "object" && !Array.isArray(value)) { + // enumMembers: { EnumName: [member, ...] } + const out: string[] = []; + for (const [k, arr] of Object.entries( + value as Record, + )) { + const members = Array.isArray(arr) ? arr : []; + for (const m of members) { + out.push( + `${k}.${typeof m === "string" ? m : ((m as { name?: string })?.name ?? String(m))}`, + ); + } + } + return out; + } + if (Array.isArray(value)) { + return value.map((x) => { + if (typeof x === "string") { + return x; + } + if (Array.isArray(x)) { + return x + .map((y) => (y as { name?: string })?.name ?? String(y)) + .join(" + "); + } + return (x as { name?: string })?.name ?? String(x); + }); + } + return []; +} + +// --------------------------------------------------------------------------- +// Rollups +// --------------------------------------------------------------------------- + +function packageKeyOf(rel: string): string { + const f = rel.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +interface Finding { + file: string; + pkg: string; + category: string; + item: string; + deadCode: boolean; +} + +interface AnalysisResult { + findings: Finding[]; + perCategory: { name: string; count: number; deadCode: boolean }[]; + perPackage: { pkg: string; deadCode: number; total: number }[]; + deadCodeTotal: number; + dependencyTotal: number; + elapsedMs: number; +} + +function analyze(issues: KnipIssue[], startedAt: number): AnalysisResult { + const findings: Finding[] = []; + for (const issue of issues) { + const file = (issue.file ?? "").replace(/\\/g, "/"); + const pkg = packageKeyOf(file); + for (const cat of ALL_CATS) { + const items = itemsOf(cat, issue[cat], file); + const deadCode = DEAD_CODE_CATS.includes(cat); + for (const item of items) { + findings.push({ file, pkg, category: cat, item, deadCode }); + } + } + } + + const catMap = new Map(); + for (const f of findings) { + catMap.set(f.category, (catMap.get(f.category) ?? 0) + 1); + } + const perCategory = ALL_CATS.map((name) => ({ + name, + count: catMap.get(name) ?? 0, + deadCode: DEAD_CODE_CATS.includes(name), + })).sort((a, b) => b.count - a.count); + + const pkgMap = new Map(); + for (const f of findings) { + const r = pkgMap.get(f.pkg) ?? { deadCode: 0, total: 0 }; + r.total++; + if (f.deadCode) { + r.deadCode++; + } + pkgMap.set(f.pkg, r); + } + const perPackage = [...pkgMap.entries()] + .map(([pkg, v]) => ({ pkg, ...v })) + .sort((a, b) => b.deadCode - a.deadCode || b.total - a.total); + + return { + findings, + perCategory, + perPackage, + deadCodeTotal: findings.filter((f) => f.deadCode).length, + dependencyTotal: findings.filter((f) => !f.deadCode).length, + elapsedMs: Date.now() - startedAt, + }; +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function csvEscape(value: string | number | boolean): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +// --------------------------------------------------------------------------- +// Report writers +// --------------------------------------------------------------------------- + +function writeCsv(outDir: string, findings: Finding[]): string { + const header = ["Category", "DeadCode", "Package", "File", "Item"].join( + ",", + ); + const rows = findings.map((f) => + [f.category, f.deadCode, f.pkg, f.file, f.item] + .map(csvEscape) + .join(","), + ); + const file = path.join(outDir, "deadcode.csv"); + fs.writeFileSync(file, [header, ...rows].join("\n") + "\n", "utf8"); + return file; +} + +function writeJson(outDir: string, opts: Options, r: AnalysisResult): string { + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + config: fs.existsSync(opts.config) ? opts.config : null, + totals: { + deadCode: r.deadCodeTotal, + dependency: r.dependencyTotal, + findings: r.findings.length, + }, + perCategory: r.perCategory, + perPackage: r.perPackage, + findings: r.findings, + }; + const file = path.join(outDir, "report.json"); + fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", "utf8"); + return file; +} + +function writeHtml(outDir: string, opts: Options, r: AnalysisResult): string { + const catRows = r.perCategory + .map( + (c) => + `${htmlEscape(c.name)}${c.deadCode ? "dead code" : "dependency"} + ${num(c.count)}`, + ) + .join("\n"); + + const pkgRows = r.perPackage + .slice(0, Math.max(opts.top, 60)) + .map( + (p) => + `${htmlEscape(p.pkg)}${num(p.deadCode)}${num(p.total)}`, + ) + .join("\n"); + + const html = ` + +TypeAgent dead-code report + +

TypeAgent dead-code report

+
engine: knip · generated ${htmlEscape(new Date().toISOString())} · + config ${fs.existsSync(opts.config) ? htmlEscape(path.basename(opts.config)) : "(none — raw)"}
+
Numbers depend on knip's entry-point configuration. Untuned, "unused" + files/exports are heavily overcounted. Treat these as candidates to review, not deletions.
+ +
+
${num(r.deadCodeTotal)}
dead-code findings
+
${num(r.dependencyTotal)}
dependency findings
+
+ +

By category

+ +${catRows}
CategoryKindCount
+ +

Packages by dead-code findings

+ +${pkgRows || ''}
PackageDead codeTotal
None
+ + +`; + const file = path.join(outDir, "report.html"); + fs.writeFileSync(file, html, "utf8"); + return file; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + const started = Date.now(); + console.log("Running knip (this analyzes the whole workspace)..."); + const issues = runKnip(opts); + const result = analyze(issues, started); + + fs.mkdirSync(opts.outDir, { recursive: true }); + const csv = writeCsv(opts.outDir, result.findings); + const json = writeJson(opts.outDir, opts, result); + const html = writeHtml(opts.outDir, opts, result); + + console.log(""); + console.log("Dead-code report (engine: knip)"); + console.log( + `Config: ${fs.existsSync(opts.config) ? path.relative(opts.root, opts.config).split(path.sep).join("/") : "(none — RAW, heavily overcounted)"} | ` + + `elapsed ${(result.elapsedMs / 1000).toFixed(1)}s`, + ); + console.log(""); + console.log( + `Dead-code findings: ${num(result.deadCodeTotal)} | dependency findings: ${num(result.dependencyTotal)}`, + ); + console.log(""); + console.log("By category:"); + for (const c of result.perCategory) { + console.log( + ` ${String(c.count).padStart(6)} ${c.name.padEnd(26)} ${c.deadCode ? "(dead code)" : "(dependency)"}`, + ); + } + console.log(""); + console.log(`Top ${opts.top} packages by dead-code findings:`); + for (const p of result.perPackage.slice(0, opts.top)) { + if (p.deadCode === 0) { + break; + } + console.log(` ${String(p.deadCode).padStart(5)} ${p.pkg}`); + } + console.log(""); + console.log( + "NOTE: tune tools/scripts/code/knip.jsonc (entry points) before trusting these numbers.", + ); + console.log("Reports written to:"); + for (const f of [csv, json, html]) { + console.log( + ` ${path.relative(opts.root, f).split(path.sep).join("/")}`, + ); + } +} + +main(); diff --git a/ts/tools/scripts/code/debtMarkersReport.ts b/ts/tools/scripts/code/debtMarkersReport.ts new file mode 100644 index 0000000000..d62ca5ec73 --- /dev/null +++ b/ts/tools/scripts/code/debtMarkersReport.ts @@ -0,0 +1,678 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Debt-markers report + hard gate for the TypeAgent ts/ tree. + * + * A lightweight, dependency-free scan (line-level regex, so treat hits as + * candidates) for debt markers that the heavier engines do not cover: + * - TODO / FIXME / HACK / XXX comments + * - @deprecated annotations (APIs kept alive past their use-by date) + * - Skipped tests: .skip( / xit( / xdescribe( + * - Focused tests: .only( / fit( / fdescribe( (should never be committed) + * + * Unlike lint and circular deps (large problems -> ratchet), skipped/focused + * tests are a small, fixable problem, so this ships a *hard gate* rather than a + * ratchet: + * - Gate (--gate --base ): fail if any changed file contains a focused + * test, or introduces a new skipped test versus the base. TODO/@deprecated + * are reported but not gated (tracked, not blocked). + * + * Outputs (written to --out-dir, default tools/scripts/code/debt-report): + * - markers.csv : every marker (file, line, type, text) + * - report.json : structured metrics (per-type, per-package) + * - report.html : a self-contained, sortable report + * + * Usage: + * npx tsx tools/scripts/code/debtMarkersReport.ts [options] + * npm run code-debt -- [options] + * + * Options: + * --top Number of rows to print / embed (default 25). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default tools/scripts/code/debt-report). + * --gate CI gate: fail on focused tests / new skipped tests vs --base. + * --base Base git ref for --gate (default origin/main). + * --help Show this help. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const SCAN_SUBDIRS = ["packages", "examples", "extensions", "tools"]; + +const CODE_EXTS = new Set([ + ".ts", + ".mts", + ".cts", + ".tsx", + ".js", + ".mjs", + ".cjs", + ".jsx", +]); + +const IGNORE_DIR_NAMES = new Set([ + "node_modules", + "dist", + "build", + "out", + "coverage", + "bin", + "obj", + ".turbo", + ".next", + "bundle", + ".git", + ".jscpd", + "complexity-report", + "duplication-report", + "consistency-report", + "lint-report", + "circular-report", + "deadcode-report", + "debt-report", +]); + +// Marker matchers. Each returns the count of matches on a line. +const MARKERS: { + type: string; + re: RegExp; + gate: "focused" | "skip" | "none"; +}[] = [ + { type: "TODO", re: /\bTODO\b/g, gate: "none" }, + { type: "FIXME", re: /\bFIXME\b/g, gate: "none" }, + { type: "HACK", re: /\bHACK\b/g, gate: "none" }, + { type: "XXX", re: /\bXXX\b/g, gate: "none" }, + { type: "@deprecated", re: /@deprecated\b/g, gate: "none" }, + { + type: "skipped-test", + re: /\b(?:it|test|describe)\.skip\s*\(|\bxit\s*\(|\bxdescribe\s*\(/g, + gate: "skip", + }, + { + type: "focused-test", + re: /\b(?:it|test|describe)\.only\s*\(|\bfit\s*\(|\bfdescribe\s*\(/g, + gate: "focused", + }, +]; + +// Focused/skipped test markers are only meaningful in test files. +const FOCUSED_RE = + /\b(?:it|test|describe)\.only\s*\(|\bfit\s*\(|\bfdescribe\s*\(/g; +const SKIP_RE = + /\b(?:it|test|describe)\.skip\s*\(|\bxit\s*\(|\bxdescribe\s*\(/g; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + top: number; + gate: boolean; + base: string; + help: boolean; +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "debt-report"), + top: 25, + gate: false, + base: "origin/main", + help: false, + }; + + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--top": + if (next === undefined || !/^\d+$/.test(next)) { + throw new Error("--top requires a positive integer"); + } + opts.top = parseInt(next, 10); + i++; + break; + case "--gate": + opts.gate = true; + break; + case "--base": + if (next === undefined) { + throw new Error("--base requires a git ref"); + } + opts.base = next; + i++; + break; + case "--root": + if (next === undefined) { + throw new Error("--root requires a path"); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error("--out-dir requires a path"); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + return opts; +} + +const HELP = `Debt-markers report + hard gate for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/debtMarkersReport.ts [options] + npm run code-debt -- [options] + +Options: + --top Number of rows to print / embed (default 25). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/debt-report). + --gate CI gate: fail on focused tests / new skipped tests vs --base. + --base Base git ref for --gate (default origin/main). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// Scanning +// --------------------------------------------------------------------------- + +function* walk(dir: string): Generator { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + if (!IGNORE_DIR_NAMES.has(e.name)) { + yield* walk(full); + } + } else if (e.isFile()) { + yield full; + } + } +} + +function isCodeFile(name: string): boolean { + if (/\.d\.(ts|mts|cts)$/.test(name)) { + return false; + } + return CODE_EXTS.has(path.extname(name)); +} + +function isTestFile(rel: string): boolean { + return ( + /\.(spec|test)\./.test(rel) || + /(^|\/)(test|tests|__tests__)\//.test(rel) + ); +} + +function packageKeyOf(rel: string): string { + const f = rel.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +interface Marker { + file: string; + line: number; + type: string; + text: string; +} + +function scanContent(rel: string, content: string, isTest: boolean): Marker[] { + const markers: Marker[] = []; + const lines = content.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + for (const m of MARKERS) { + // Skipped/focused test markers only count in test files. + if ((m.gate === "skip" || m.gate === "focused") && !isTest) { + continue; + } + m.re.lastIndex = 0; + const hits = line.match(m.re); + if (hits) { + for (let h = 0; h < hits.length; h++) { + markers.push({ + file: rel, + line: i + 1, + type: m.type, + text: line.trim().slice(0, 200), + }); + } + } + } + } + return markers; +} + +function countMatches(content: string, re: RegExp): number { + let n = 0; + for (const line of content.split(/\r?\n/)) { + re.lastIndex = 0; + const hits = line.match(re); + if (hits) { + n += hits.length; + } + } + return n; +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function csvEscape(value: string | number): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +// --------------------------------------------------------------------------- +// Report mode +// --------------------------------------------------------------------------- + +function collect(root: string): Marker[] { + const markers: Marker[] = []; + for (const sub of SCAN_SUBDIRS) { + const abs = path.join(root, sub); + if (!fs.existsSync(abs)) { + continue; + } + for (const full of walk(abs)) { + if (!isCodeFile(full)) { + continue; + } + const rel = path.relative(root, full).split(path.sep).join("/"); + let content: string; + try { + content = fs.readFileSync(full, "utf8"); + } catch { + continue; + } + markers.push(...scanContent(rel, content, isTestFile(rel))); + } + } + return markers; +} + +function runReport(opts: Options): number { + const started = Date.now(); + const markers = collect(opts.root); + + const byType = new Map(); + for (const m of MARKERS) { + byType.set(m.type, 0); + } + for (const mk of markers) { + byType.set(mk.type, (byType.get(mk.type) ?? 0) + 1); + } + + const byPkg = new Map(); + for (const mk of markers) { + const p = packageKeyOf(mk.file); + byPkg.set(p, (byPkg.get(p) ?? 0) + 1); + } + const perPackage = [...byPkg.entries()] + .map(([pkg, count]) => ({ pkg, count })) + .sort((a, b) => b.count - a.count || a.pkg.localeCompare(b.pkg)); + + const byFile = new Map(); + for (const mk of markers) { + byFile.set(mk.file, (byFile.get(mk.file) ?? 0) + 1); + } + const perFile = [...byFile.entries()] + .map(([file, count]) => ({ file, count })) + .sort((a, b) => b.count - a.count); + + fs.mkdirSync(opts.outDir, { recursive: true }); + + // CSV + const header = ["Type", "Package", "File", "Line", "Text"].join(","); + const rows = markers.map((mk) => + [mk.type, packageKeyOf(mk.file), mk.file, mk.line, mk.text] + .map(csvEscape) + .join(","), + ); + const csv = path.join(opts.outDir, "markers.csv"); + fs.writeFileSync(csv, [header, ...rows].join("\n") + "\n", "utf8"); + + // JSON + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + totals: { + markers: markers.length, + byType: Object.fromEntries(byType), + }, + perPackage, + perFile, + }; + const json = path.join(opts.outDir, "report.json"); + fs.writeFileSync(json, JSON.stringify(payload, null, 2) + "\n", "utf8"); + + // HTML + const typeRows = [...byType.entries()] + .sort((a, b) => b[1] - a[1]) + .map( + ([t, c]) => + `${htmlEscape(t)}${num(c)}`, + ) + .join("\n"); + const pkgRows = perPackage + .slice(0, Math.max(opts.top, 60)) + .map( + (p) => + `${htmlEscape(p.pkg)}${num(p.count)}`, + ) + .join("\n"); + const html = ` + +TypeAgent debt-markers report + +

TypeAgent debt-markers report

+
generated ${htmlEscape(new Date().toISOString())} · ${num(markers.length)} markers · line-level scan
+
+
${num(byType.get("skipped-test") ?? 0)}
skipped tests
+
${num(byType.get("focused-test") ?? 0)}
focused tests
+
${num(byType.get("@deprecated") ?? 0)}
@deprecated
+
${num((byType.get("TODO") ?? 0) + (byType.get("FIXME") ?? 0) + (byType.get("HACK") ?? 0) + (byType.get("XXX") ?? 0))}
TODO/FIXME/HACK/XXX
+
+

By type

+ +${typeRows}
TypeCount
+

By package

+ +${pkgRows}
PackageCount
+ +`; + fs.writeFileSync(path.join(opts.outDir, "report.html"), html, "utf8"); + + console.log(""); + console.log("Debt-markers report (line-level scan)"); + console.log( + `Markers: ${num(markers.length)} | elapsed ${((Date.now() - started) / 1000).toFixed(1)}s`, + ); + console.log(""); + console.log("By type:"); + for (const [t, c] of [...byType.entries()].sort((a, b) => b[1] - a[1])) { + console.log(` ${String(c).padStart(6)} ${t}`); + } + console.log(""); + console.log(`Top ${opts.top} packages:`); + for (const p of perPackage.slice(0, opts.top)) { + console.log(` ${String(p.count).padStart(6)} ${p.pkg}`); + } + console.log(""); + console.log("Reports written to:"); + for (const f of ["markers.csv", "report.json", "report.html"]) { + console.log( + ` ${path.relative(opts.root, path.join(opts.outDir, f)).split(path.sep).join("/")}`, + ); + } + return 0; +} + +// --------------------------------------------------------------------------- +// Gate mode (CI hard gate) +// --------------------------------------------------------------------------- + +const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/; +const IGNORE_PATH_RE = + /(^|\/)(node_modules|dist|build|out|coverage|bin|obj|\.turbo|\.next|bundle)\//; +const GENERATED_FILE_RE = /(\.d\.ts|\.min\.js|\.bundle\.js)$/; + +function git(args: string[], cwd: string): string { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + }); +} + +interface DiffEntry { + head: string; + base: string | null; +} + +function parseNameStatus(raw: string): DiffEntry[] { + const entries: DiffEntry[] = []; + for (const line of raw.split(/\r?\n/)) { + if (!line) { + continue; + } + const parts = line.split("\t"); + const status = parts[0]; + if (status.startsWith("R") || status.startsWith("C")) { + entries.push({ base: parts[1], head: parts[2] }); + } else if (status === "A") { + entries.push({ base: null, head: parts[1] }); + } else if (status === "D") { + continue; + } else { + entries.push({ base: parts[1], head: parts[1] }); + } + } + return entries; +} + +function isSource(rel: string): boolean { + return ( + SOURCE_EXT_RE.test(rel) && + !IGNORE_PATH_RE.test(rel) && + !GENERATED_FILE_RE.test(rel) + ); +} + +function runGate(opts: Options): number { + let repoRoot: string; + let mergeBase: string; + try { + repoRoot = git(["rev-parse", "--show-toplevel"], opts.root).trim(); + mergeBase = git(["merge-base", opts.base, "HEAD"], opts.root).trim(); + } catch { + console.error( + `Gate: could not resolve base ref "${opts.base}" via git. ` + + "Pass --base (e.g. origin/main) and ensure it is fetched.", + ); + return 2; + } + + const entries = parseNameStatus( + git(["diff", "--name-status", "-M", mergeBase, "HEAD"], opts.root), + ).filter((e) => { + if (!isSource(e.head)) { + return false; + } + const relToRoot = path.relative( + opts.root, + path.resolve(repoRoot, e.head), + ); + return !relToRoot.startsWith("..") && !path.isAbsolute(relToRoot); + }); + + if (entries.length === 0) { + console.log("Gate: no changed source files to check. OK."); + return 0; + } + + const focusedHits: string[] = []; + let headSkips = 0; + let baseSkips = 0; + + for (const e of entries) { + if (!isTestFile(e.head)) { + continue; // focused/skipped tests only matter in test files + } + const headAbs = path.resolve(repoRoot, e.head); + if (fs.existsSync(headAbs)) { + const content = fs.readFileSync(headAbs, "utf8"); + const lines = content.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + FOCUSED_RE.lastIndex = 0; + if (FOCUSED_RE.test(lines[i])) { + focusedHits.push( + ` ${e.head}:${i + 1} ${lines[i].trim()}`, + ); + } + } + headSkips += countMatches(content, SKIP_RE); + } + if (e.base && isTestFile(e.base)) { + try { + const baseContent = git( + ["show", `${mergeBase}:${e.base}`], + repoRoot, + ); + baseSkips += countMatches(baseContent, SKIP_RE); + } catch { + /* file did not exist at base */ + } + } + } + + console.log( + `Gate: ${entries.length} changed source file(s) | skipped tests base ${baseSkips} -> head ${headSkips}`, + ); + + let failed = false; + if (focusedHits.length > 0) { + failed = true; + console.error( + `\nGate FAILED: focused test(s) must not be committed (.only/fit/fdescribe):`, + ); + focusedHits.slice(0, 50).forEach((h) => console.error(h)); + } + if (headSkips > baseSkips) { + failed = true; + console.error( + `\nGate FAILED: changed files add ${headSkips - baseSkips} skipped test(s) ` + + "(.skip/xit/xdescribe). Un-skip or delete them.", + ); + } + + if (failed) { + return 1; + } + console.log("Gate OK: no focused tests and no new skipped tests."); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main(): void { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + process.exitCode = opts.gate ? runGate(opts) : runReport(opts); +} + +main(); diff --git a/ts/tools/scripts/code/knip.jsonc b/ts/tools/scripts/code/knip.jsonc new file mode 100644 index 0000000000..f8d389aa55 --- /dev/null +++ b/ts/tools/scripts/code/knip.jsonc @@ -0,0 +1,14 @@ +{ + // Starter knip config for the dead-code report (tools/scripts/code/deadCodeReport.ts). + // + // knip auto-detects the pnpm workspaces and infers each package's entry + // points from package.json (main / bin / exports). The repo-wide ignores + // below just drop generated output. The real accuracy work — and the reason + // the raw counts overcount "unused" code — is declaring the entry points + // knip cannot infer: agent action handlers loaded at runtime via manifest, + // webpack/CLI entries, and benchmark/test harnesses. Add `entry` patterns + // (per workspace via the `workspaces` key) as hotspots are triaged. + "$schema": "https://unpkg.com/knip@6/schema.jsonc", + "ignore": ["**/dist/**", "**/*.d.ts", "**/bundle/**", "**/*.min.js"], + "ignoreBinaries": ["fluid-build", "shx", "rimraf"], +} diff --git a/ts/tools/scripts/code/lintReport.ts b/ts/tools/scripts/code/lintReport.ts new file mode 100644 index 0000000000..5b3f31bc14 --- /dev/null +++ b/ts/tools/scripts/code/lintReport.ts @@ -0,0 +1,878 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Lint report + ratchet gate for the TypeAgent ts/ tree. + * + * The engine is ESLint (with typescript-eslint), the de-facto standard, run + * against a throwaway in-memory flat config so the repo needs no committed + * ESLint configuration — the same approach as the sibling complexityReport.ts. + * It harvests a curated, high-signal rule set rather than enforcing a full + * style guide: + * + * Syntactic (always on — fast, no type information required): + * - @typescript-eslint/no-explicit-any (erodes type safety) + * - no-console (allow warn/error) (stray console.log; use `debug`) + * - @typescript-eslint/no-unused-vars (dead code) + * - no-debugger, no-var, prefer-const + * + * Type-aware (opt-in via --type-aware — slower, needs the TS project): + * - @typescript-eslint/no-floating-promises (unawaited promises: the + * single highest-value bug + * class in an async agent system) + * - @typescript-eslint/no-misused-promises + * - @typescript-eslint/no-deprecated (use of @deprecated APIs) + * + * Two modes: + * - Report (default): scan the tree and write CSV/JSON/HTML + a console + * summary (per-rule, per-package, worst files). + * - Ratchet (--ratchet --base ): a stateless CI gate. It lints only the + * files changed since the merge base, on both their HEAD content and their + * content at the base, and fails if the change introduces more violations + * than it removes. The base branch is the baseline (no committed baseline + * file), so violations in touched code can only trend down. Ratchet uses + * the syntactic rules only, so HEAD and the base (materialized to a temp + * dir, where the TS project is unavailable) are compared like-for-like. + * + * Outputs (written to --out-dir, default tools/scripts/code/lint-report): + * - violations.csv : every violation, ranked + * - report.json : structured metrics (per-rule, per-package, per-file) + * - report.html : a self-contained, sortable report + * + * Usage: + * npx tsx tools/scripts/code/lintReport.ts [options] + * npm run code-lint -- [options] + * + * Options: + * --include-tests Include test files (excluded by default). + * --type-aware Also run the type-aware rules (slower; needs the TS + * project). Report mode only. + * --top Number of worst offenders to print / embed (default 25). + * --root Directory to scan (default: the ts/ root). + * --out-dir Output directory (default tools/scripts/code/lint-report). + * --ratchet CI gate: fail if changed files add violations vs --base. + * --base Base git ref for --ratchet (default origin/main). + * --new-file-max With --ratchet, also fail if any NEW file has more than + * violations (-1 = disabled, the default). + * --help Show this help. + */ + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { ESLint, Linter } from "eslint"; +import tseslint from "typescript-eslint"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const SOURCE_GLOB = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"; + +// node_modules and .git are ignored by ESLint's flat config by default. +const IGNORE_DIRS = [ + "**/dist/**", + "**/build/**", + "**/out/**", + "**/coverage/**", + "**/bin/**", + "**/obj/**", + "**/.turbo/**", + "**/.next/**", + "**/bundle/**", +]; + +const IGNORE_FILES = ["**/*.d.ts", "**/*.min.js", "**/*.bundle.js"]; + +const TEST_GLOBS = [ + "**/test/**", + "**/tests/**", + "**/__tests__/**", + "**/*.spec.*", + "**/*.test.*", +]; + +const SYNTACTIC_RULES: Linter.RulesRecord = { + "no-console": ["warn", { allow: ["warn", "error"] }], + "no-debugger": "warn", + "no-var": "warn", + "prefer-const": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrors: "none", + }, + ], +}; + +const TYPE_AWARE_RULES: Linter.RulesRecord = { + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-deprecated": "warn", +}; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Options { + root: string; + outDir: string; + includeTests: boolean; + typeAware: boolean; + top: number; + ratchet: boolean; + base: string; + newFileMax: number; + help: boolean; +} + +function parseIntArg(arg: string, next: string | undefined): number { + if (next === undefined || !/^-?\d+$/.test(next)) { + throw new Error(`${arg} requires an integer value`); + } + return parseInt(next, 10); +} + +function parseArgs(argv: string[]): Options { + const opts: Options = { + root: path.resolve(__dirname, "..", "..", ".."), + outDir: path.join(__dirname, "lint-report"), + includeTests: false, + typeAware: false, + top: 25, + ratchet: false, + base: "origin/main", + newFileMax: -1, + help: false, + }; + + const tokens: string[] = []; + for (const raw of argv) { + const m = /^(--[\w-]+)=(.*)$/.exec(raw); + if (m) { + tokens.push(m[1], m[2]); + } else { + tokens.push(raw); + } + } + + for (let i = 0; i < tokens.length; i++) { + const arg = tokens[i]; + const next = tokens[i + 1]; + switch (arg) { + case "--help": + case "-h": + opts.help = true; + break; + case "--include-tests": + opts.includeTests = true; + break; + case "--type-aware": + opts.typeAware = true; + break; + case "--top": + opts.top = parseIntArg(arg, next); + i++; + break; + case "--ratchet": + opts.ratchet = true; + break; + case "--base": + if (next === undefined) { + throw new Error(`${arg} requires a git ref`); + } + opts.base = next; + i++; + break; + case "--new-file-max": + opts.newFileMax = parseIntArg(arg, next); + i++; + break; + case "--root": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.root = path.resolve(next); + i++; + break; + case "--out-dir": + case "--outDir": + if (next === undefined) { + throw new Error(`${arg} requires a path`); + } + opts.outDir = path.resolve(next); + i++; + break; + default: + console.warn(`Ignoring unrecognized argument: ${arg}`); + break; + } + } + + if (opts.top <= 0) { + throw new Error("--top must be greater than 0"); + } + + return opts; +} + +const HELP = `Lint report + ratchet gate for the TypeAgent ts/ tree. + +Usage: + npx tsx tools/scripts/code/lintReport.ts [options] + npm run code-lint -- [options] + +Options: + --include-tests Include test files (excluded by default). + --type-aware Also run type-aware rules (no-floating-promises, + no-misused-promises, no-deprecated). Slower; report only. + --top Number of worst offenders to print / embed (default 25). + --root Directory to scan (default: the ts/ root). + --out-dir Output directory (default: tools/scripts/code/lint-report). + --ratchet CI gate: fail if changed files add violations vs --base. + --base Base git ref for --ratchet (default origin/main). + --new-file-max With --ratchet, fail if any NEW file exceeds + violations (-1 = disabled, the default). + --help Show this help.`; + +// --------------------------------------------------------------------------- +// ESLint plumbing +// --------------------------------------------------------------------------- + +interface Violation { + file: string; // repo-relative, forward-slash separated + line: number; + column: number; + rule: string; + severity: number; // 1 = warn, 2 = error + message: string; +} + +interface LintOutput { + violations: Violation[]; + parseErrorFiles: number; + filesAnalyzed: number; +} + +function buildConfig( + typeAware: boolean, + useIgnores: boolean, + includeTests: boolean, + tsconfigRootDir: string, +): Linter.Config[] { + const rules: Linter.RulesRecord = { + ...SYNTACTIC_RULES, + ...(typeAware ? TYPE_AWARE_RULES : {}), + }; + + const config: Linter.Config[] = []; + if (useIgnores) { + const ignores = [...IGNORE_DIRS, ...IGNORE_FILES]; + if (!includeTests) { + ignores.push(...TEST_GLOBS); + } + config.push({ ignores } as Linter.Config); + } + + const parserOptions: Linter.ParserOptions = typeAware + ? { + projectService: true, + tsconfigRootDir, + ecmaFeatures: { jsx: true }, + } + : { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { jsx: true }, + }; + + config.push({ + files: [SOURCE_GLOB], + languageOptions: { + parser: tseslint.parser as unknown as Linter.Parser, + parserOptions, + }, + plugins: { + "@typescript-eslint": tseslint.plugin as unknown as ESLint.Plugin, + }, + rules, + }); + return config; +} + +function parseResults( + results: ESLint.LintResult[], + cwd: string, +): { violations: Violation[]; parseErrorFiles: number } { + const violations: Violation[] = []; + let parseErrorFiles = 0; + for (const res of results) { + const rel = path.relative(cwd, res.filePath).split(path.sep).join("/"); + let hadFatal = false; + for (const msg of res.messages) { + if (msg.fatal) { + hadFatal = true; + continue; + } + violations.push({ + file: rel, + line: msg.line ?? 0, + column: msg.column ?? 0, + rule: msg.ruleId ?? "(unknown)", + severity: msg.severity, + message: msg.message, + }); + } + if (hadFatal) { + parseErrorFiles++; + } + } + return { violations, parseErrorFiles }; +} + +async function lint( + cwd: string, + patterns: string[], + opts: { typeAware: boolean; useIgnores: boolean; includeTests: boolean }, + tsconfigRootDir: string, +): Promise { + const eslint = new ESLint({ + cwd, + errorOnUnmatchedPattern: false, + overrideConfigFile: true, + overrideConfig: buildConfig( + opts.typeAware, + opts.useIgnores, + opts.includeTests, + tsconfigRootDir, + ), + }); + const results = await eslint.lintFiles(patterns); + const { violations, parseErrorFiles } = parseResults(results, cwd); + return { violations, parseErrorFiles, filesAnalyzed: results.length }; +} + +// --------------------------------------------------------------------------- +// Rollups +// --------------------------------------------------------------------------- + +function packageKeyOf(rel: string): string { + const f = rel.replace(/\\/g, "/"); + const i = f.indexOf("/src/"); + if (i >= 0) { + return f.slice(0, i); + } + const parts = f.split("/"); + if (parts[0] === "packages" && parts[1] === "agents") { + return parts.slice(0, 3).join("/"); + } + if ( + parts[0] === "packages" || + parts[0] === "examples" || + parts[0] === "extensions" + ) { + return parts.slice(0, 2).join("/"); + } + return parts.slice(0, Math.min(2, parts.length)).join("/"); +} + +function countBy(items: T[], key: (t: T) => string): Map { + const m = new Map(); + for (const it of items) { + const k = key(it); + m.set(k, (m.get(k) ?? 0) + 1); + } + return m; +} + +function sortedEntries( + m: Map, +): { name: string; count: number }[] { + return [...m.entries()] + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function csvEscape(value: string | number): string { + const s = String(value); + if (/[",\r\n]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function htmlEscape(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +function num(n: number): string { + return n.toLocaleString("en-US"); +} + +// --------------------------------------------------------------------------- +// Report writers +// --------------------------------------------------------------------------- + +function writeCsv(outDir: string, violations: Violation[]): string { + const header = [ + "File", + "Line", + "Column", + "Rule", + "Severity", + "Message", + ].join(","); + const rows = violations.map((v) => + [ + v.file, + v.line, + v.column, + v.rule, + v.severity === 2 ? "error" : "warn", + v.message, + ] + .map(csvEscape) + .join(","), + ); + const file = path.join(outDir, "violations.csv"); + fs.writeFileSync(file, [header, ...rows].join("\n") + "\n", "utf8"); + return file; +} + +function writeJson( + outDir: string, + opts: Options, + out: LintOutput, + perRule: { name: string; count: number }[], + perPackage: { name: string; count: number }[], + perFile: { name: string; count: number }[], +): string { + const payload = { + generatedAt: new Date().toISOString(), + root: opts.root, + includeTests: opts.includeTests, + typeAware: opts.typeAware, + totals: { + violations: out.violations.length, + filesAnalyzed: out.filesAnalyzed, + filesWithViolations: perFile.length, + parseErrorFiles: out.parseErrorFiles, + }, + perRule, + perPackage, + perFile, + }; + const file = path.join(outDir, "report.json"); + fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", "utf8"); + return file; +} + +function writeHtml( + outDir: string, + opts: Options, + out: LintOutput, + perRule: { name: string; count: number }[], + perPackage: { name: string; count: number }[], + perFile: { name: string; count: number }[], +): string { + const row2 = (a: string, b: number) => + `${htmlEscape(a)}${num(b)}`; + + const ruleRows = perRule.map((r) => row2(r.name, r.count)).join("\n"); + const pkgRows = perPackage + .slice(0, 60) + .map((r) => row2(r.name, r.count)) + .join("\n"); + const fileRows = perFile + .slice(0, Math.max(opts.top, 100)) + .map((r) => row2(r.name, r.count)) + .join("\n"); + + const html = ` + +TypeAgent lint report + +

TypeAgent lint report

+
engine: ESLint + typescript-eslint · generated ${htmlEscape(new Date().toISOString())} · + ${opts.typeAware ? "syntactic + type-aware" : "syntactic"} · + tests ${opts.includeTests ? "included" : "excluded"}
+ +
+
${num(out.violations.length)}
violations
+
${num(perFile.length)}
files with violations
+
${num(out.filesAnalyzed)}
files analyzed
+
${num(perRule.length)}
rules triggered
+
+ +

Violations by rule

+ +${ruleRows || ''}
RuleCount
None
+ +

Violations by package

+ +${pkgRows || ''}
PackageCount
None
+ +

Worst files

+ +${fileRows || ''}
FileCount
None
+ + +`; + const file = path.join(outDir, "report.html"); + fs.writeFileSync(file, html, "utf8"); + return file; +} + +// --------------------------------------------------------------------------- +// Report mode +// --------------------------------------------------------------------------- + +async function runReport(opts: Options): Promise { + const started = Date.now(); + if (opts.typeAware) { + console.log( + "Type-aware rules enabled — this uses the TS project service and is slower.", + ); + } + const out = await lint( + opts.root, + [SOURCE_GLOB], + { + typeAware: opts.typeAware, + useIgnores: true, + includeTests: opts.includeTests, + }, + opts.root, + ); + + const perRule = sortedEntries(countBy(out.violations, (v) => v.rule)); + const perPackage = sortedEntries( + countBy(out.violations, (v) => packageKeyOf(v.file)), + ); + const perFile = sortedEntries(countBy(out.violations, (v) => v.file)); + + fs.mkdirSync(opts.outDir, { recursive: true }); + const csv = writeCsv(opts.outDir, out.violations); + const json = writeJson( + opts.outDir, + opts, + out, + perRule, + perPackage, + perFile, + ); + const html = writeHtml( + opts.outDir, + opts, + out, + perRule, + perPackage, + perFile, + ); + + const elapsed = ((Date.now() - started) / 1000).toFixed(1); + console.log(""); + console.log("Lint report (engine: ESLint + typescript-eslint)"); + console.log( + `Analyzed ${num(out.filesAnalyzed)} files | elapsed ${elapsed}s | ` + + `${opts.typeAware ? "syntactic + type-aware" : "syntactic"}`, + ); + console.log(""); + console.log( + `Violations: ${num(out.violations.length)} in ${num(perFile.length)} files` + + (out.parseErrorFiles + ? ` | parse errors in ${num(out.parseErrorFiles)} files` + : ""), + ); + console.log(""); + console.log("By rule:"); + for (const r of perRule) { + console.log(` ${String(r.count).padStart(6)} ${r.name}`); + } + console.log(""); + console.log(`Top ${opts.top} packages:`); + for (const p of perPackage.slice(0, opts.top)) { + console.log(` ${String(p.count).padStart(6)} ${p.name}`); + } + console.log(""); + console.log("Reports written to:"); + for (const f of [csv, json, html]) { + console.log( + ` ${path.relative(opts.root, f).split(path.sep).join("/")}`, + ); + } + return 0; +} + +// --------------------------------------------------------------------------- +// Ratchet mode (CI gate) +// --------------------------------------------------------------------------- + +const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/; +const IGNORE_PATH_RE = + /(^|\/)(node_modules|dist|build|out|coverage|bin|obj|\.turbo|\.next|bundle)\//; +const GENERATED_FILE_RE = /(\.d\.ts|\.min\.js|\.bundle\.js)$/; +const TEST_PATH_RE = + /(^|\/)(test|tests|__tests__)\/|\.(spec|test)\.[cm]?[jt]sx?$/; + +function isReportableSource(relPath: string, includeTests: boolean): boolean { + if (!SOURCE_EXT_RE.test(relPath)) { + return false; + } + if (IGNORE_PATH_RE.test(relPath) || GENERATED_FILE_RE.test(relPath)) { + return false; + } + if (!includeTests && TEST_PATH_RE.test(relPath)) { + return false; + } + return true; +} + +function git(args: string[], cwd: string): string { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + }); +} + +interface DiffEntry { + head: string; + base: string | null; +} + +function parseNameStatus(raw: string): DiffEntry[] { + const entries: DiffEntry[] = []; + for (const line of raw.split(/\r?\n/)) { + if (!line) { + continue; + } + const parts = line.split("\t"); + const status = parts[0]; + if (status.startsWith("R") || status.startsWith("C")) { + entries.push({ base: parts[1], head: parts[2] }); + } else if (status === "A") { + entries.push({ base: null, head: parts[1] }); + } else if (status === "D") { + continue; + } else { + entries.push({ base: parts[1], head: parts[1] }); + } + } + return entries; +} + +async function runRatchet(opts: Options): Promise { + let repoRoot: string; + let mergeBase: string; + try { + repoRoot = git(["rev-parse", "--show-toplevel"], opts.root).trim(); + mergeBase = git(["merge-base", opts.base, "HEAD"], opts.root).trim(); + } catch { + console.error( + `Ratchet: could not resolve base ref "${opts.base}" via git. ` + + "Pass --base (e.g. origin/main) and ensure it is fetched.", + ); + return 2; + } + + const entries = parseNameStatus( + git(["diff", "--name-status", "-M", mergeBase, "HEAD"], opts.root), + ).filter((e) => { + if (!isReportableSource(e.head, opts.includeTests)) { + return false; + } + const relToRoot = path.relative( + opts.root, + path.resolve(repoRoot, e.head), + ); + return !relToRoot.startsWith("..") && !path.isAbsolute(relToRoot); + }); + + if (entries.length === 0) { + console.log("Ratchet: no changed source files to check. OK."); + return 0; + } + + const lintOpts = { + typeAware: false, + useIgnores: false, + includeTests: opts.includeTests, + }; + + // HEAD side. + const headPaths = entries + .map((e) => path.resolve(repoRoot, e.head)) + .filter((p) => fs.existsSync(p)); + const head = headPaths.length + ? await lint(repoRoot, headPaths, lintOpts, opts.root) + : { violations: [], parseErrorFiles: 0, filesAnalyzed: 0 }; + + // BASE side: materialize each changed file's base content into a temp dir. + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "lint-base-")); + let base: LintOutput = { + violations: [], + parseErrorFiles: 0, + filesAnalyzed: 0, + }; + try { + const basePaths: string[] = []; + for (const e of entries) { + if (!e.base || !isReportableSource(e.base, opts.includeTests)) { + continue; + } + let content: string; + try { + content = git(["show", `${mergeBase}:${e.base}`], repoRoot); + } catch { + continue; + } + const dest = path.join(tmp, e.base); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, content, "utf8"); + basePaths.push(dest); + } + if (basePaths.length) { + base = await lint(tmp, basePaths, lintOpts, opts.root); + } + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + + // Compare per-rule and totals. + const headTotal = head.violations.length; + const baseTotal = base.violations.length; + const headByRule = countBy(head.violations, (v) => v.rule); + const baseByRule = countBy(base.violations, (v) => v.rule); + + console.log( + `Ratchet: ${entries.length} changed source file(s) | ` + + `violations base ${baseTotal} -> head ${headTotal}`, + ); + + const worsened: string[] = []; + for (const rule of new Set([...headByRule.keys(), ...baseByRule.keys()])) { + const h = headByRule.get(rule) ?? 0; + const b = baseByRule.get(rule) ?? 0; + if (h > b) { + worsened.push(` ${rule}: ${b} -> ${h} (+${h - b})`); + } + } + + // New-file cap. + const newFileFailures: string[] = []; + if (opts.newFileMax >= 0) { + const headByFile = countBy(head.violations, (v) => + path + .relative(repoRoot, path.resolve(repoRoot, v.file)) + .split(path.sep) + .join("/"), + ); + for (const e of entries) { + if (e.base !== null) { + continue; // not a new file + } + const rel = e.head.replace(/\\/g, "/"); + const count = headByFile.get(rel) ?? 0; + if (count > opts.newFileMax) { + newFileFailures.push(` ${rel}: ${count} > ${opts.newFileMax}`); + } + } + } + + let failed = false; + if (headTotal > baseTotal) { + failed = true; + console.error( + `\nRatchet FAILED: changed files add ${headTotal - baseTotal} ` + + "violation(s) versus the base. Rules that worsened:", + ); + worsened.forEach((w) => console.error(w)); + } + if (newFileFailures.length) { + failed = true; + console.error( + `\nRatchet FAILED: new file(s) exceed --new-file-max ${opts.newFileMax}:`, + ); + newFileFailures.forEach((w) => console.error(w)); + } + + if (failed) { + console.error( + "\nRun `npm run code-lint` and fix the flagged lines in the files this PR changes.", + ); + return 1; + } + console.log("Ratchet OK: changed files do not add lint violations."); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + let opts: Options; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error((e as Error).message); + process.exitCode = 2; + return; + } + + if (opts.help) { + console.log(HELP); + return; + } + + const code = opts.ratchet ? await runRatchet(opts) : await runReport(opts); + process.exitCode = code; +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); From e0223f766e61c08e033e3a788404a710361f6733 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Jul 2026 13:51:49 -0700 Subject: [PATCH 6/9] added code quality build checks (ratchet-down) --- .github/workflows/build-ts.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/build-ts.yml b/.github/workflows/build-ts.yml index a90df10b97..3728366800 100644 --- a/.github/workflows/build-ts.yml +++ b/.github/workflows/build-ts.yml @@ -102,6 +102,38 @@ jobs: npm run code-complexity -- --ratchet --base "origin/${{ github.base_ref }}" \ --cyclomatic 25 --cognitive 30 \ --new-file-cyclomatic 25 --new-file-cognitive 30 + # Lint ratchet (PRs only): the files this PR changes may not add ESLint + # violations (no-explicit-any, no-console, no-unused-vars, ...) versus the + # base branch. Syntactic rules only, so it is fast; the count can only + # trend down. Run `npm run code-lint` locally to see the full report. + - name: Lint ratchet + if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.ts != 'false' }} + working-directory: ts + shell: bash + run: | + git fetch --no-tags origin "${{ github.base_ref }}" + npm run code-lint -- --ratchet --base "origin/${{ github.base_ref }}" + # Circular-dependency ratchet (PRs only): fail if the change introduces a + # runtime import cycle not already present at the base. Builds the cycle + # set for HEAD and for the merge base (via a throwaway git worktree), so + # this step is heavier than the others (madge runs twice). + - name: Circular dependency ratchet + if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.ts != 'false' }} + working-directory: ts + shell: bash + run: | + git fetch --no-tags origin "${{ github.base_ref }}" + npm run code-circular -- --ratchet --base "origin/${{ github.base_ref }}" + # Test-debt gate (PRs only): zero tolerance for focused tests + # (.only/fit/fdescribe) and no newly skipped tests (.skip/xit/xdescribe) + # in changed files. A small, fixable problem -> a hard gate, not a ratchet. + - name: Test debt gate + if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.ts != 'false' }} + working-directory: ts + shell: bash + run: | + git fetch --no-tags origin "${{ github.base_ref }}" + npm run code-debt -- --gate --base "origin/${{ github.base_ref }}" - name: Restore better-sqlite3 for Node.js if: ${{ github.event_name != 'pull_request' || steps.filter.outputs.ts != 'false' }} working-directory: ts From 4abd2591bb79ae7ac465b91d54e30e2d50067e8d Mon Sep 17 00:00:00 2001 From: robgruen Date: Fri, 3 Jul 2026 13:57:05 -0700 Subject: [PATCH 7/9] Potential fix for pull request finding 'CodeQL / Incomplete HTML attribute sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ts/tools/scripts/code/complexityReport.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/tools/scripts/code/complexityReport.ts b/ts/tools/scripts/code/complexityReport.ts index faea351d89..352fb889c1 100644 --- a/ts/tools/scripts/code/complexityReport.ts +++ b/ts/tools/scripts/code/complexityReport.ts @@ -477,7 +477,12 @@ function csvEscape(value: string | number): string { } function htmlEscape(s: string): string { - return s.replace(/&/g, "&").replace(//g, ">"); + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } /** Format an integer with thousands separators for human-readable output. */ From 589c30db1422a278da1e4a83fdff66d5894b21d4 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Jul 2026 14:10:04 -0700 Subject: [PATCH 8/9] refactored --- ts/tools/scripts/code/consistencyReport.ts | 169 ++++++++++++--------- 1 file changed, 99 insertions(+), 70 deletions(-) diff --git a/ts/tools/scripts/code/consistencyReport.ts b/ts/tools/scripts/code/consistencyReport.ts index 8bce6b72c6..e8ee5e969e 100644 --- a/ts/tools/scripts/code/consistencyReport.ts +++ b/ts/tools/scripts/code/consistencyReport.ts @@ -323,12 +323,10 @@ interface AnalysisResult { elapsedMs: number; } -function analyze(opts: Options): AnalysisResult { - const started = Date.now(); - +function collectFiles(root: string, includeTests: boolean): string[] { const files: string[] = []; for (const sub of SCAN_SUBDIRS) { - const abs = path.join(opts.root, sub); + const abs = path.join(root, sub); if (!fs.existsSync(abs)) { continue; } @@ -336,85 +334,89 @@ function analyze(opts: Options): AnalysisResult { if (!isCodeFile(full)) { continue; } - const rel = path - .relative(opts.root, full) - .split(path.sep) - .join("/"); - if (!opts.includeTests && isTestFile(rel)) { - continue; + const rel = path.relative(root, full).split(path.sep).join("/"); + if (includeTests || !isTestFile(rel)) { + files.push(rel); } - files.push(rel); } } + return files; +} - // name -> packageKey -> set(files) - const exportMap = new Map>>(); - const envByFile: EnvFile[] = []; - - for (const rel of files) { - let content: string; - try { - content = fs.readFileSync(path.join(opts.root, rel), "utf8"); - } catch { - continue; - } - const pkg = packageKeyOf(rel); - - // Check 1: exported symbol names. - for (const re of EXPORT_RES) { - re.lastIndex = 0; - let m: RegExpExecArray | null; - while ((m = re.exec(content)) !== null) { - const name = m[1]; - if (EXPORT_DENYLIST.has(name)) { - continue; - } - let byPkg = exportMap.get(name); - if (!byPkg) { - byPkg = new Map(); - exportMap.set(name, byPkg); - } - let set = byPkg.get(pkg); - if (!set) { - set = new Set(); - byPkg.set(pkg, set); - } - set.add(rel); +/** Record every top-level exported name in `content` under exportMap[name][pkg]. */ +function scanExports( + content: string, + pkg: string, + rel: string, + exportMap: Map>>, +): void { + for (const re of EXPORT_RES) { + re.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const name = m[1]; + if (EXPORT_DENYLIST.has(name)) { + continue; } - } - - // Check 2: direct process.env access (production packages only). - if (rel.startsWith(ENV_SCAN_PREFIX) && !ENV_ALLOWED(rel)) { - const matches = content.match(/process\.env\b/g); - if (matches && matches.length > 0) { - envByFile.push({ file: rel, pkg, refs: matches.length }); + let byPkg = exportMap.get(name); + if (!byPkg) { + byPkg = new Map(); + exportMap.set(name, byPkg); } + const set = byPkg.get(pkg) ?? new Set(); + set.add(rel); + byPkg.set(pkg, set); } } +} - // Duplicate exports: names in >= minPackages different packages. - const duplicateExports: DuplicateExport[] = []; +/** Direct process.env access in a production package (excluding the config readers). */ +function scanEnv( + content: string, + rel: string, + pkg: string, +): EnvFile | undefined { + if (!rel.startsWith(ENV_SCAN_PREFIX) || ENV_ALLOWED(rel)) { + return undefined; + } + const matches = content.match(/process\.env\b/g); + return matches && matches.length > 0 + ? { file: rel, pkg, refs: matches.length } + : undefined; +} + +/** Exported names defined in at least `minPackages` different packages. */ +function buildDuplicateExports( + exportMap: Map>>, + minPackages: number, +): DuplicateExport[] { + const dups: DuplicateExport[] = []; for (const [name, byPkg] of exportMap) { - if (byPkg.size >= opts.minPackages) { - const packages = [...byPkg.keys()].sort(); - const fileList: string[] = []; - for (const set of byPkg.values()) { - fileList.push(...set); - } - duplicateExports.push({ - name, - packageCount: byPkg.size, - packages, - files: fileList.sort(), - }); + if (byPkg.size < minPackages) { + continue; + } + const fileList: string[] = []; + for (const set of byPkg.values()) { + fileList.push(...set); } + dups.push({ + name, + packageCount: byPkg.size, + packages: [...byPkg.keys()].sort(), + files: fileList.sort(), + }); } - duplicateExports.sort( + return dups.sort( (a, b) => b.packageCount - a.packageCount || a.name.localeCompare(b.name), ); +} - // process.env rollups. +function buildEnvRollups(envByFile: EnvFile[]): { + envPackages: EnvPackage[]; + envFiles: EnvFile[]; + envTotalRefs: number; +} { const envPkgMap = new Map(); let envTotalRefs = 0; for (const e of envByFile) { @@ -424,10 +426,37 @@ function analyze(opts: Options): AnalysisResult { r.files += 1; envPkgMap.set(e.pkg, r); } - const envPackages = [...envPkgMap.values()].sort((a, b) => b.refs - a.refs); - const envFiles = envByFile.sort((a, b) => b.refs - a.refs); + return { + envPackages: [...envPkgMap.values()].sort((a, b) => b.refs - a.refs), + envFiles: envByFile.sort((a, b) => b.refs - a.refs), + envTotalRefs, + }; +} + +function analyze(opts: Options): AnalysisResult { + const started = Date.now(); + const files = collectFiles(opts.root, opts.includeTests); + + // name -> packageKey -> set(files) + const exportMap = new Map>>(); + const envByFile: EnvFile[] = []; + for (const rel of files) { + let content: string; + try { + content = fs.readFileSync(path.join(opts.root, rel), "utf8"); + } catch { + continue; + } + const pkg = packageKeyOf(rel); + scanExports(content, pkg, rel, exportMap); + const env = scanEnv(content, rel, pkg); + if (env) { + envByFile.push(env); + } + } - // Check 3: agent conformance. + const duplicateExports = buildDuplicateExports(exportMap, opts.minPackages); + const { envPackages, envFiles, envTotalRefs } = buildEnvRollups(envByFile); const agents = analyzeAgents(opts.root); const agentIssues = agents.filter( (a) => !a.hasManifest || !a.hasInstantiate, From db2e89da707913e35235fea55d93942fabaa5122 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Fri, 3 Jul 2026 16:26:26 -0700 Subject: [PATCH 9/9] excluded tool folder --- ts/tools/scripts/code/lintReport.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ts/tools/scripts/code/lintReport.ts b/ts/tools/scripts/code/lintReport.ts index 5b3f31bc14..204fef4ca0 100644 --- a/ts/tools/scripts/code/lintReport.ts +++ b/ts/tools/scripts/code/lintReport.ts @@ -263,6 +263,14 @@ interface LintOutput { filesAnalyzed: number; } +// CLI scripts and build/analysis tooling under tools/scripts write to stdout via +// console by design, so `no-console` does not apply to them. The leading **/ +// keeps this matching whether ESLint's cwd is ts/ (report mode) or the repo root +// (ratchet mode). +const CONSOLE_ALLOWED_GLOBS = [ + "**/tools/scripts/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", +]; + function buildConfig( typeAware: boolean, useIgnores: boolean, @@ -306,6 +314,11 @@ function buildConfig( }, rules, }); + // CLI/build scripts legitimately use console for their output. + config.push({ + files: CONSOLE_ALLOWED_GLOBS, + rules: { "no-console": "off" }, + } as Linter.Config); return config; }