diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcf8135..6b92c1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,10 @@ pnpm pack:check This performs a dry-run package build so the final tarball contents can be reviewed before publication. +## Dependency maintenance + +Scheduled Dependabot version-update pull requests are disabled. Review dependency updates manually when preparing a maintenance release. Check the Node.js 20 and 22 CI matrix before updating build or test tooling; a package that requires Node.js 22 cannot replace a tool used by the Node.js 20 jobs. Run `pnpm install` to update the lockfile, then `pnpm verify` and `pnpm release:check` before merging. Security advisories should still be reviewed as they arise. + ## Add a new SkillBench rule 1. Choose the category under `src/rules/` and add a focused rule module. A rule exports a typed `Rule` with `id`, `name`, `description`, `category`, `defaultSeverity`, `weight`, and `check()`. diff --git a/scripts/measure-scale-performance.ts b/scripts/measure-scale-performance.ts index 4605146..f42bf32 100644 --- a/scripts/measure-scale-performance.ts +++ b/scripts/measure-scale-performance.ts @@ -40,6 +40,15 @@ async function main(): Promise { `Performance fixture discovery mismatch: expected ${fileCount} files, received ${report.files.length}.`, ); } + const countedIssues = report.files.reduce( + (total, file) => total + file.issueCount, + 0, + ); + if (countedIssues !== report.issues.length) { + throw new Error( + `Per-file finding counts disagree with the report: ${countedIssues} versus ${report.issues.length}.`, + ); + } const result: PerformanceResult = { version: 1, diff --git a/src/core/analyze.ts b/src/core/analyze.ts index 77032d6..14d7f26 100644 --- a/src/core/analyze.ts +++ b/src/core/analyze.ts @@ -50,6 +50,10 @@ export function analyzeDocuments( critical: 0, }; for (const entry of issues) summary[entry.severity] += 1; + const issueCounts = new Map(); + for (const issue of issues) { + issueCounts.set(issue.path, (issueCounts.get(issue.path) ?? 0) + 1); + } return { schemaVersion: SCHEMA_VERSION, @@ -66,8 +70,7 @@ export function analyzeDocuments( path: context.document.relativePath, kind: context.document.kind, tokens: context.tokens, - issueCount: issues.filter((entry) => entry.path === context.document.relativePath) - .length, + issueCount: issueCounts.get(context.document.relativePath) ?? 0, })), }; } diff --git a/src/parser/discovery.ts b/src/parser/discovery.ts index 47d0fab..672ef85 100644 --- a/src/parser/discovery.ts +++ b/src/parser/discovery.ts @@ -24,6 +24,7 @@ const defaultIgnores = [ ]; const maxDocumentBytes = 2 * 1024 * 1024; +const maxConcurrentReads = 16; export class DiscoveryError extends Error { override readonly name = 'DiscoveryError'; @@ -57,7 +58,20 @@ export async function discoverDocuments( `No supported agent instruction files found under ${target}`, ); } - return Promise.all(paths.map((filePath) => readDocument(filePath, absoluteTarget))); + // Keep result order stable while limiting open files on large repositories. + const documents = new Array(paths.length); + let nextIndex = 0; + await Promise.all( + Array.from({ length: Math.min(maxConcurrentReads, paths.length) }, async () => { + while (nextIndex < paths.length) { + const index = nextIndex++; + const filePath = paths[index]; + if (filePath === undefined) continue; + documents[index] = await readDocument(filePath, absoluteTarget); + } + }), + ); + return documents; } async function walk( diff --git a/tests/unit/workflow-security.test.ts b/tests/unit/workflow-security.test.ts index 48683f2..46d9082 100644 --- a/tests/unit/workflow-security.test.ts +++ b/tests/unit/workflow-security.test.ts @@ -1,7 +1,6 @@ import { readFileSync, readdirSync } from 'node:fs'; import path from 'node:path'; -import YAML from 'yaml'; import { describe, expect, it } from 'vitest'; const root = path.resolve(import.meta.dirname, '../..'); @@ -57,46 +56,4 @@ describe('workflow supply-chain policy', () => { ).toBe(checkoutCount); } }); - - it('configures weekly dependency updates and preserves Node 20 majors', () => { - const config = YAML.parse(read('.github/dependabot.yml')) as { - version: number; - updates: { - 'package-ecosystem': string; - schedule: { interval: string }; - ignore?: { - 'dependency-name': string; - 'update-types': string[]; - }[]; - }[]; - }; - - expect(config.version).toBe(2); - expect(config.updates).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - 'package-ecosystem': 'npm', - schedule: { interval: 'weekly' }, - }), - expect.objectContaining({ - 'package-ecosystem': 'github-actions', - schedule: { interval: 'weekly' }, - }), - ]), - ); - - const npmUpdate = config.updates.find( - (update) => update['package-ecosystem'] === 'npm', - ); - expect(npmUpdate?.ignore).toEqual([ - { - 'dependency-name': 'chalk', - 'update-types': ['version-update:semver-major'], - }, - { - 'dependency-name': 'commander', - 'update-types': ['version-update:semver-major'], - }, - ]); - }); });