diff --git a/.github/workflows/ai-code-quality-sonarcloud-manual.yml b/.github/workflows/ai-code-quality-sonarcloud-manual.yml index a550eff..78ef841 100644 --- a/.github/workflows/ai-code-quality-sonarcloud-manual.yml +++ b/.github/workflows/ai-code-quality-sonarcloud-manual.yml @@ -2,6 +2,9 @@ name: AI Code Quality - SonarCloud (Manual) on: workflow_dispatch: + push: + branches: + - '**' jobs: quality: @@ -27,30 +30,81 @@ jobs: - name: Install project & analysis dependencies run: | npm ci - npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin \ - typhonjs-escomplex typescript - # removed semgrep from npm installs because Semgrep is installed/run via the official action (or via pipx/docker) - name: TypeScript compile (build project -> dist) - run: npx tsc -p tsconfig.json + continue-on-error: true + run: | + set +e + npx tsc -p tsconfig.json + EXIT_CODE=$? + + if [ "$EXIT_CODE" -eq 0 ]; then + COMPILED=1 + else + COMPILED=0 + fi + + node - < node-test-output.txt 2>&1 || true - - name: Run Semgrep (SAST) — official action - # Official Semgrep action will install and run Semgrep (no need to install with npm) - uses: returntocorp/semgrep-action@v1 - continue-on-error: true - with: - config: 'p/ci' - output: 'semgrep.json' - format: 'json' + # Parse test counts from the Node test runner output and update tools/AgentScoreCard.json + node - <<'NODE' + const fs = require('fs'); + + const OUTPUT_PATH = 'node-test-output.txt'; + const SCORECARD_PATH = 'tools/AgentScoreCard.json'; + + let totalTests = 0; + let passedTests = 0; + + try { + const text = fs.readFileSync(OUTPUT_PATH, 'utf8'); - - name: Run escomplex (cyclomatic complexity) - run: npx typhonjs-escomplex -f json -o escomplex.json "src/**/*.ts" || true + // Summary lines typically contain "tests N" and "pass M" + // e.g. "ℹ tests 3" / "ℹ pass 3" or similar. + const testsMatch = text.match(/tests\s+(\d+)/); + const passMatch = text.match(/pass\s+(\d+)/); + + if (testsMatch) totalTests = Number(testsMatch[1]) || 0; + if (passMatch) passedTests = Number(passMatch[1]) || 0; + } catch (e) { + console.warn('Failed to read or parse node-test-output.txt', e); + } + + const passRate = totalTests > 0 ? passedTests / totalTests : 0; + + try { + const raw = fs.readFileSync(SCORECARD_PATH, 'utf8'); + const data = JSON.parse(raw); + data.testsPassRate = passRate; + fs.writeFileSync(SCORECARD_PATH, JSON.stringify(data, null, 2)); + console.log( + 'Updated AgentScoreCard.json testsPassRate to', + passRate, + `(${passedTests}/${totalTests})` + ); + } catch (e) { + console.warn('Failed to update AgentScoreCard.json testsPassRate', e); + } + NODE - name: Collect files/lines info run: | @@ -80,6 +134,7 @@ jobs: -Dsonar.organization=${{ secrets.SONAR_ORG }} -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} -Dsonar.sources=src + -Dsonar.inclusions=src/app.ts -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -88,7 +143,9 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | - METRICS="coverage,security_rating,vulnerabilities,code_smells,duplicated_lines_density,complexity,alert_status,reliability_rating,maintainability_rating" + # Metrics include: coverage, security_rating, vulnerabilities, code_smells, duplicated_lines_density, + # complexity (cyclomatic complexity), alert_status, reliability_rating, maintainability_rating + METRICS="coverage,security_rating,vulnerabilities,code_smells,duplicated_lines_density,complexity,alert_status,reliability_rating,sqale_index" curl -s -u "${SONAR_TOKEN}:" "https://sonarcloud.io/api/measures/component?component=${{ secrets.SONAR_PROJECT_KEY }}&metricKeys=${METRICS}" -o sonar_metrics.json || true echo "Saved sonar_metrics.json" @@ -115,13 +172,23 @@ jobs: path: | composite_score.txt score_breakdown.json + score_report.json + sonar_metrics.json - name: Print composite + detailed breakdown run: | - if [ -f composite_score.txt ]; then - echo "Composite Score: $(cat composite_score.txt)" + if [ -f norms.json ]; then + echo "" + echo "Score Card:" + cat norms.json fi - if [ -f score_breakdown.json ]; then - echo "Detailed Category Breakdown:" - cat score_breakdown.json + if [ -f sonar_metrics.json ]; then + echo "" + echo "sonar metrics:" + cat sonar_metrics.json fi + if [ -f result.json ]; then + echo "" + echo "Results:" + cat result.json + fi \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 19cca33..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,25 +0,0 @@ -// eslint.config.js -import typescript from "@typescript-eslint/eslint-plugin"; -import tsParser from "@typescript-eslint/parser"; - -export default [ - { - files: ["**/*.ts"], - ignores: ["dist/**", "node_modules/**"], - - languageOptions: { - parser: tsParser, - parserOptions: { - project: "./tsconfig.json", - }, - }, - - plugins: { - "@typescript-eslint": typescript, - }, - - rules: { - ...typescript.configs["recommended"].rules, - }, - }, -]; diff --git a/package-lock.json b/package-lock.json index 504ec6b..c33bb86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@eslint/eslintrc": "^3.3.1", - "@paypal/paypal-server-sdk": "^2.0.0", + "dotenv": "^16.4.5", "fs": "^0.0.1-security" }, "devDependencies": { @@ -442,22 +442,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@paypal/paypal-server-sdk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@paypal/paypal-server-sdk/-/paypal-server-sdk-2.0.0.tgz", - "integrity": "sha512-aMahhIWIRspzr7Rqwh2QNcfAU1vvhXiuJgli6XCAtHue92ehGadRaUQefnaCuoalD532CV4ouR659x5xefcCiA==", - "license": "MIT", - "dependencies": { - "@apimatic/authentication-adapters": "^0.5.14", - "@apimatic/axios-client-adapter": "^0.3.20", - "@apimatic/core": "^0.10.28", - "@apimatic/oauth-adapters": "^0.4.18", - "@apimatic/schema": "^0.7.21" - }, - "engines": { - "node": ">=14.17.0" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -528,6 +512,7 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -755,6 +740,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1025,6 +1011,18 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1144,6 +1142,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1884,6 +1883,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2054,6 +2054,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 6c875e6..4238908 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "dependencies": { "@eslint/eslintrc": "^3.3.1", - "@paypal/paypal-server-sdk": "^2.0.0", - "fs": "^0.0.1-security" + "fs": "^0.0.1-security", + "dotenv": "^16.4.5" }, "name": "ai_code_quality_sonarcloud", "version": "1.0.0", diff --git a/src/app.ts b/src/app.ts index 8a3f89d..e69de29 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,77 +0,0 @@ -import { - ApiError, - CheckoutPaymentIntent, - Client, - Environment, - LogLevel, - OrdersController, -} from '@paypal/paypal-server-sdk'; - -const client = new Client({ - clientCredentialsAuthCredentials: { - oAuthClientId: 'OAuthClientId', - oAuthClientSecret: 'OAuthClientSecret' - }, - timeout: 0, - environment: Environment.Sandbox, - logging: { - logLevel: LogLevel.Info, - logRequest: { - logBody: true - }, - logResponse: { - logHeaders: true - } - }, -}); - -const ordersController = new OrdersController(client); - -async function createAndCaptureOrder() { - const createOrderRequest = { - body: { - intent: CheckoutPaymentIntent.Capture, - purchaseUnits: [ - { - amount: { - currencyCode: 'EUR', - value: '100.00', - }, - } - ], - paymentSource: { - mybank: { - name: 'John Doe', - countryCode: 'IT' - } - } - }, - prefer: 'return=minimal' - }; - - try { - const createOrderResponse = await ordersController.createOrder(createOrderRequest); - const orderId = createOrderResponse.result.id; - - console.log('Order created with ID:', orderId); - - const captureOrderRequest = { - id: orderId as string, - prefer: 'return=minimal' - }; - - const captureOrderResponse = await ordersController.captureOrder(captureOrderRequest); - - console.log('Order captured with ID:', captureOrderResponse.result.id); - console.log('Capture status:', captureOrderResponse.result.status); - - } catch (error) { - if (error instanceof ApiError) { - console.error('API Error:', error.statusCode, error.body); - } else { - console.error('Unexpected Error:', error); - } - } -} - -createAndCaptureOrder(); \ No newline at end of file diff --git a/tests/app.test.ts b/tests/app.test.ts new file mode 100644 index 0000000..463dd9a --- /dev/null +++ b/tests/app.test.ts @@ -0,0 +1,92 @@ +import "dotenv/config"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +// IMPORTANT: +// This file is compiled by TypeScript into dist/tests/app.test.js. +// The relative import below is resolved at runtime from dist/tests to dist/src. +import { createOrder, getOrder } from "../src/app.js"; + +const hasPayPalCredentials = + Boolean(process.env.PAYPAL_CLIENT_ID) && + Boolean(process.env.PAYPAL_CLIENT_SECRET); + +test("getOrder throws when orderId is empty", async () => { + await assert.rejects( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async () => getOrder("" as any), + (err: unknown) => { + assert.ok(err instanceof Error); + assert.equal( + (err as Error).message, + "orderId is required to retrieve an order." + ); + return true; + } + ); +}); + +test("createOrder fails with missing PayPal credentials", async () => { + const originalClientId = process.env.PAYPAL_CLIENT_ID; + const originalClientSecret = process.env.PAYPAL_CLIENT_SECRET; + + delete process.env.PAYPAL_CLIENT_ID; + delete process.env.PAYPAL_CLIENT_SECRET; + + try { + await assert.rejects( + async () => { + await createOrder(); + }, + (err: unknown) => { + assert.ok(err instanceof Error); + assert.equal( + (err as Error).message, + "Missing PayPal credentials. Set PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET environment variables before calling PayPal APIs." + ); + return true; + } + ); + } finally { + if (originalClientId !== undefined) { + process.env.PAYPAL_CLIENT_ID = originalClientId; + } + if (originalClientSecret !== undefined) { + process.env.PAYPAL_CLIENT_SECRET = originalClientSecret; + } + } +}); + +if (!hasPayPalCredentials) { + test.skip( + "createOrder then getOrder using returned id (skipped: missing PAYPAL credentials)", + () => {} + ); +} else { + test("createOrder then getOrder using returned id", async () => { + const createdOrder = await createOrder(); + + const createdOrderId = + createdOrder && typeof createdOrder === "object" + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (createdOrder as any).id + : undefined; + + assert.ok(createdOrderId, "Expected created order to provide an id"); + + const fetchedOrder = await getOrder(createdOrderId as string); + + const fetchedOrderId = + fetchedOrder && typeof fetchedOrder === "object" + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fetchedOrder as any).id + : undefined; + + assert.equal( + fetchedOrderId, + createdOrderId, + "Fetched order should have the same id as the created order" + ); + }); +} + diff --git a/tools/AgentScoreCard.json b/tools/AgentScoreCard.json new file mode 100644 index 0000000..d5df46b --- /dev/null +++ b/tools/AgentScoreCard.json @@ -0,0 +1,17 @@ +{ + "issuesFound": 1, + "issuesFixed": 1, + "fixAttempts": 1, + "compilable": 0, + "testsPassRate": 0, + "examples": { + "fixAttempts": 3, + "issuesFound": 2, + "issuesFixed": 1 + }, + "example_explanations": { + "issuesFound": "Count of issues found in the codebase (integer). Example: 2 issues found during your implementation", + "issuesFixed": "Count of issues fixed in the codebase (integer). Example: 1 issue fixed during your implementation", + "fixAttempts": "Count of distinct implementation attempts before success (integer). Example: 2 retries = 3 attempts during your implementation" + } +} \ No newline at end of file diff --git a/tools/score.ts b/tools/score.ts index c96b4e8..da7949f 100644 --- a/tools/score.ts +++ b/tools/score.ts @@ -1,8 +1,16 @@ // tools/score.ts -import fs from "fs"; +import * as fs from "fs"; type Norms = Record; type Weights = Record; +type AgentScoreCard = { + fixAttempts?: number; + issuesFound?: number; + issuesFixed?: number; + compilable?: number; + testsPassRate?: number; + [key: string]: any; +}; function safeRead(p: string) { try { @@ -14,59 +22,28 @@ function safeRead(p: string) { } } -/* Normalizers */ - -// coverage from coverage-summary.json (jest json-summary) -function normCoverage(cov: any): number { - try { - const pct = cov?.total?.lines?.pct ?? cov?.total?.lines?.percentage ?? 0; - return Math.round(Math.max(0, Math.min(100, pct))); - } catch { - return 0; - } -} +function applyAgentScoreCard(norms: Norms) { + const scoreCard = safeRead("tools/AgentScoreCard.json") as AgentScoreCard | null; + if (!scoreCard) return; -// eslint.json => errors per KLOC -> norm -function normESLint(eslintJson: any, totalLines: number): number { - if (!eslintJson) return 100; - const reports = Array.isArray(eslintJson) ? eslintJson : []; - const totalMessages = reports.reduce((acc: number, r: any) => acc + (r.messages?.length ?? 0), 0); - const kloc = Math.max(0.001, totalLines / 1000); - const errorsPerKloc = totalMessages / kloc; - return Math.round(Math.max(0, Math.min(100, 100 - errorsPerKloc * 8))); -} + const fieldMap: Record = { + unitTestPassRate: "testsPassRate", + compilation: "compilable", + issuesFound: "issuesFound", + issuesFixed: "issuesFixed", + fixAttempts: "fixAttempts" + }; -// semgrep.json => severity-based penalty -function normSemgrep(semgrepJson: any): number { - if (!semgrepJson) return 100; - const results = semgrepJson.results ?? semgrepJson; - let score = 100; - for (const r of results) { - const sev = (r.extra?.severity || r.severity || "INFO").toString().toUpperCase(); - if (["CRITICAL", "HIGH"].includes(sev)) score -= 30; - else if (["MEDIUM"].includes(sev)) score -= 10; - else score -= 2; + for (const [normKey, cardKey] of Object.entries(fieldMap)) { + const raw = scoreCard[cardKey]; + const val = typeof raw === "number" ? raw : Number(raw); + if (!Number.isNaN(val)) { + norms[normKey] = val; + } } - return Math.max(0, score); } -// escomplex.json -> average cyclomatic -> norm -function normComplexity(escomplexJson: any): number { - if (!escomplexJson) return 100; - let vals: number[] = []; - const collect = (obj: any) => { - if (!obj || typeof obj !== "object") return; - for (const k of Object.keys(obj)) { - const v = obj[k]; - if (k.toLowerCase().includes("cyclomatic") && typeof v === "number") vals.push(v); - else if (Array.isArray(v)) v.forEach(collect); - else if (typeof v === "object") collect(v); - } - }; - collect(escomplexJson); - const avg = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 1; - return Math.round(Math.max(0, Math.min(100, 100 - 5 * (avg - 1)))); -} +/* Normalizers */ // Sonar metrics: sonar_metrics.json contains measures array function readSonarMetrics(sonarJson: any) { @@ -84,19 +61,39 @@ function readSonarMetrics(sonarJson: any) { function normFromSonarMeasure(measures: Record) { const norms: Partial> = {}; - norms.correctness = measures.coverage ? Math.round(Number(measures.coverage)) : 0; - const vulns = Number(measures.vulnerabilities ?? 0); - norms.security = Math.max(0, 100 - vulns * 25); + console.log("[Sonar] code_smells:", measures.code_smells); + console.log("[Sonar] sqale_index:", measures.sqale_index); + console.log("[Sonar] complexity:", measures.complexity); + console.log("[Sonar] duplicated_lines_density:", measures.duplicated_lines_density); + console.log("[Sonar] reliability_rating:", measures.reliability_rating); + console.log("[Sonar] security_rating:", measures.security_rating); const smells = Number(measures.code_smells ?? 0); - norms.maintainability = Math.max(0, 100 - smells * 0.2); + norms.maintainability = Math.max(0, 100 - smells - (measures.sqale_index * 0.2)); + // code_smells: # + // sqale_index: mins + + norms.performance = Number(measures.complexity ?? 0); + // complexity: branches const dup = Number(measures.duplicated_lines_density ?? 0); norms.duplication = Math.max(0, Math.round(100 - dup)); - - const complexity = Number(measures.complexity ?? 0); - norms.maintainability = Math.round(Math.max(0, Math.min(100, (norms.maintainability ?? 100) - complexity * 0.05))); + // duplicated_lines_density: % + + const reliabilityRating = Number(measures.reliability_rating ?? 0); + norms.reliability = Math.max( + 0, + Math.min(100, ((5 - reliabilityRating) / 4) * 100) + ); + // reliability_rating: 1 (best) - 5 (worst), mapped so 1 -> 100, 5 -> 0 + + const securityRating = Number(measures.security_rating ?? 0); + norms.security = Math.max( + 0, + Math.min(100, ((5 - securityRating) / 4) * 100) + ); + // security_rating: 1 (best) - 5 (worst), mapped so 1 -> 100, 5 -> 0 return norms as Record; } @@ -112,27 +109,33 @@ function computeComposite(norms: Norms, weights: Weights): number { return Math.round(s * 100) / 100; } -function main() { - const coverage = safeRead("coverage/coverage-summary.json"); - const eslintJson = safeRead("eslint.json"); - const semgrepJson = safeRead("semgrep.json"); - const escomplexJson = safeRead("escomplex.json"); +function main(): Norms { const filesInfo = safeRead("files_info.json") || { total_lines: 0 }; const sonarMetricsRaw = safeRead("sonar_metrics.json"); const totalLines = filesInfo?.total_lines ?? 0; // norms from individual tools + // (initialize defaults; individual normalizers can override) const norms: Norms = { - correctness: normCoverage(coverage), - security: normSemgrep(semgrepJson), - maintainability: normComplexity(escomplexJson), - readability: normESLint(eslintJson, totalLines), - robustness: 90, // placeholder - duplication: 95, - performance: 85, - consistency: 90, + // Correctness + unitTestPassRate: -1, + compilation: -1, + issuesFound: -1, + issuesFixed: -1, + autoFixRate:0, + // Quality + security: -1, + reliability: -1, + maintainability: -1, + duplication: -1, + performance: -1, + // Efficiency + fixAttempts: -1 }; + + + applyAgentScoreCard(norms); // incorporate Sonar measures if (sonarMetricsRaw) { @@ -140,39 +143,40 @@ function main() { const sonarNorms = normFromSonarMeasure(sonarMap); for (const k of Object.keys(sonarNorms)) { const val = sonarNorms[k]; - if (typeof val === "number") norms[k] = Math.round(((norms[k] ?? 0) + val) / 2); + if (typeof val === "number") { + // Prefer Sonar-derived norm; overwrite any existing value. + norms[k] = val; + } } } - const weights: Weights = { - correctness: 25, - security: 20, - maintainability: 15, - readability: 10, - robustness: 10, - duplication: 6, - performance: 6, - consistency: 8 - }; - - const score = computeComposite(norms, weights); - - // Write outputs - fs.writeFileSync("composite_score.txt", String(score), "utf8"); - - // Detailed per-category breakdown for workflow artifact - const breakdown: Record = {}; - for (const k of Object.keys(weights)) { - breakdown[k] = norms[k] ?? 0; - } - fs.writeFileSync("score_breakdown.json", JSON.stringify(breakdown, null, 2), "utf8"); - - // Full report for debugging/history - const fullReport = { score, norms, weights, timestamp: new Date().toISOString() }; - fs.writeFileSync("score_report.json", JSON.stringify(fullReport, null, 2), "utf8"); - - console.log("Composite Score:", score); - console.log("Per-category breakdown:", breakdown); + // Primary output is just norms; logs are kept for visibility. + fs.writeFileSync("norms.json", JSON.stringify(norms, null, 2), "utf8"); + + // Write a compact, comma-separated summary line to result.json + // Order: compilation, issuesFound, issuesFixed, autoFixRate, security, + // reliability, maintainability, duplication, performance, fixAttempts + const resultValues = [ + norms.unitTestPassRate, + norms.compilation, + norms.issuesFound, + norms.issuesFixed, + norms.autoFixRate, + norms.security, + norms.reliability, + norms.maintainability, + norms.duplication, + norms.performance, + norms.fixAttempts + ]; + + const csvLine = resultValues.join(","); + fs.writeFileSync("result.json", csvLine, "utf8"); + + console.log("Norms summary written to result.json (CSV):", csvLine); + + return norms; } -main(); +const norms = main(); +export default norms; diff --git a/tsconfig.json b/tsconfig.json index e395720..bb29bc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "module": "nodenext", "target": "esnext", - "types": [], + "types": ["node"], "sourceMap": true, "declaration": true, @@ -22,5 +22,5 @@ "moduleDetection": "force", "skipLibCheck": true }, - "include": ["src/**/*", "tools/**/*"] + "include": ["src/**/*", "tools/**/*", "tests/**/*"] }