From d947daaf3d2bd8857363e3808149434cd954c909 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 23 Feb 2026 19:57:27 -0800 Subject: [PATCH 1/8] chore: add eslint flat config (#290) Add ESLint 9 flat config with recommended rules and node globals. Fix two no-useless-escape errors in regex character classes and remove a stale eslint-disable directive in lens.js. --- eslint.config.js | 21 +++++++++++++++++++++ src/lens.js | 1 - src/remote.js | 2 +- src/validators.js | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..6792fd38 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js'; +import globals from 'globals'; + +export default [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node, + }, + }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + }, + }, + { + ignores: ['node_modules/', 'coverage/'], + }, +]; diff --git a/src/lens.js b/src/lens.js index 7f57ebeb..0b37f477 100644 --- a/src/lens.js +++ b/src/lens.js @@ -36,7 +36,6 @@ export function defineLens(name, filterFn, opts = {}) { throw new Error(`defineLens("${name}"): filterFn must be a function`); } if (builtInDefs.size > 0 && builtInDefs.has(name)) { - // eslint-disable-next-line no-console console.warn(`defineLens: overwriting built-in lens "${name}". Call resetLenses() to restore.`); } registry.set(name, { diff --git a/src/remote.js b/src/remote.js index c1d0ed7c..9b6ddb2a 100644 --- a/src/remote.js +++ b/src/remote.js @@ -5,7 +5,7 @@ */ /** @type {RegExp} Regex for cross-repo node IDs. */ -export const CROSS_REPO_ID_REGEX = /^repo:([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+):([a-z][a-z0-9-]*):([A-Za-z0-9._\/@-]+)$/; +export const CROSS_REPO_ID_REGEX = /^repo:([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+):([a-z][a-z0-9-]*):([A-Za-z0-9._/@-]+)$/; /** * @typedef {object} CrossRepoId diff --git a/src/validators.js b/src/validators.js index fdb69533..223ea005 100644 --- a/src/validators.js +++ b/src/validators.js @@ -9,7 +9,7 @@ import { CROSS_REPO_ID_REGEX } from './remote.js'; // ── Constants ──────────────────────────────────────────────────────── /** @type {RegExp} Canonical regex for node IDs (prefix:identifier) */ -export const NODE_ID_REGEX = /^[a-z][a-z0-9-]*:[A-Za-z0-9._\/@-]+$/; +export const NODE_ID_REGEX = /^[a-z][a-z0-9-]*:[A-Za-z0-9._/@-]+$/; /** @type {number} Maximum total length of a node ID */ export const NODE_ID_MAX_LENGTH = 256; From fe28ae3ffeeedd2edf32e0a7153b0f909ae246d0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 23 Feb 2026 20:04:43 -0800 Subject: [PATCH 2/8] fix: use execFileSync for git commands in processCommitCmd (#290) Replace shell-interpolated execSync with execFileSync array form to eliminate potential command injection via user-supplied SHA argument. --- src/cli/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/commands.js b/src/cli/commands.js index 92025f3e..5312384c 100644 --- a/src/cli/commands.js +++ b/src/cli/commands.js @@ -3,7 +3,7 @@ * Command implementations for the git-mind CLI. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import { writeFile, chmod, access, constants, readFile } from 'node:fs/promises'; import { join, extname } from 'node:path'; import { initGraph, loadGraph } from '../graph.js'; @@ -297,7 +297,7 @@ npx git-mind process-commit "$SHA" 2>/dev/null || true */ export async function processCommitCmd(cwd, sha) { try { - const message = execSync(`git log -1 --format=%B ${sha}`, { cwd, encoding: 'utf-8' }); + const message = execFileSync('git', ['log', '-1', '--format=%B', sha], { cwd, encoding: 'utf-8' }); const graph = await loadGraph(cwd); const directives = await processCommit(graph, { sha, message }); From c00609d0e2a0af033556adc7132cd612556076a0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 23 Feb 2026 20:06:03 -0800 Subject: [PATCH 3/8] ci: add lint and coverage to CI pipeline (#290) Add ESLint step to CI workflow before tests. Configure v8 coverage provider in vitest and add test:coverage script for local use. --- .github/workflows/ci.yml | 3 +++ package.json | 1 + vitest.config.js | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd62323e..29d3cb2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,5 +33,8 @@ jobs: - name: Install dependencies run: npm ci + - name: Run linter + run: npm run lint + - name: Run tests run: npm test diff --git a/package.json b/package.json index 7e3e0c5b..dfa3a4c5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "scripts": { "test": "vitest run", "test:watch": "vitest", + "test:coverage": "vitest run --coverage --exclude test/contracts.integration.test.js --exclude test/content.test.js --exclude test/version.test.js", "lint": "eslint src/ bin/", "format": "prettier --write 'src/**/*.js' 'bin/**/*.js'" }, diff --git a/vitest.config.js b/vitest.config.js index 3381f934..ffd50b87 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,5 +7,10 @@ export default defineConfig({ execArgv: ['--disable-warning=DEP0169'], }, }, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + reportsDirectory: './coverage', + }, }, }); From bf9f4e6115dd270f5b14d782d25ffddd1dfcfc9c Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 24 Feb 2026 11:41:18 -0800 Subject: [PATCH 4/8] fix: harden eslint config and resolve all lint violations (#290) Escalate no-unused-vars to error with varsIgnorePattern for _-prefixed vars. Add @eslint/js and globals as explicit devDependencies per ESLint 9 best practices. Remove 8 unused imports and prefix 2 unused variables. --- eslint.config.js | 2 +- package-lock.json | 21 ++++++++++++++++++--- package.json | 2 ++ src/cli/commands.js | 12 ++++++------ src/dag.js | 2 +- src/doctor.js | 2 +- src/nodes.js | 2 +- src/suggest.js | 2 +- 8 files changed, 31 insertions(+), 14 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6792fd38..bd758e3d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -12,7 +12,7 @@ export default [ }, }, rules: { - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], }, }, { diff --git a/package-lock.json b/package-lock.json index 461647fa..fb4ec217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,9 @@ "git-mind": "bin/git-mind.js" }, "devDependencies": { + "@eslint/js": "^9.39.3", "eslint": "^9.0.0", + "globals": "^14.0.0", "prettier": "^3.0.0", "vitest": "^3.0.0" }, @@ -690,9 +692,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -2198,6 +2200,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/package.json b/package.json index dfa3a4c5..f97ec161 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "js-yaml": "^4.1.1" }, "devDependencies": { + "@eslint/js": "^9.39.3", "eslint": "^9.0.0", + "globals": "^14.0.0", "prettier": "^3.0.0", "vitest": "^3.0.0" }, diff --git a/src/cli/commands.js b/src/cli/commands.js index 5312384c..4e2d77c0 100644 --- a/src/cli/commands.js +++ b/src/cli/commands.js @@ -7,8 +7,8 @@ import { execFileSync } from 'node:child_process'; import { writeFile, chmod, access, constants, readFile } from 'node:fs/promises'; import { join, extname } from 'node:path'; import { initGraph, loadGraph } from '../graph.js'; -import { createEdge, queryEdges, removeEdge, EDGE_TYPES } from '../edges.js'; -import { getNodes, hasNode, getNode, getNodesByPrefix, setNodeProperty, unsetNodeProperty } from '../nodes.js'; +import { createEdge, queryEdges, removeEdge } from '../edges.js'; +import { getNodes, getNode, getNodesByPrefix, setNodeProperty, unsetNodeProperty } from '../nodes.js'; import { computeStatus } from '../status.js'; import { importFile } from '../import.js'; import { importFromMarkdown } from '../frontmatter.js'; @@ -23,10 +23,10 @@ import { runDoctor, fixIssues } from '../doctor.js'; import { generateSuggestions } from '../suggest.js'; import { getPendingSuggestions, acceptSuggestion, rejectSuggestion, skipSuggestion, batchDecision } from '../review.js'; import { computeDiff } from '../diff.js'; -import { createContext, DEFAULT_CONTEXT } from '../context-envelope.js'; +import { DEFAULT_CONTEXT } from '../context-envelope.js'; import { loadExtension, registerExtension, removeExtension, listExtensions, validateExtension } from '../extension.js'; -import { writeContent, readContent, getContentMeta, hasContent, deleteContent } from '../content.js'; -import { success, error, info, warning, formatEdge, formatView, formatNode, formatNodeList, formatStatus, formatExportResult, formatImportResult, formatDoctorResult, formatSuggestions, formatReviewItem, formatDecisionSummary, formatAtStatus, formatDiff, formatExtensionList, formatContentMeta } from './format.js'; +import { writeContent, readContent, getContentMeta, deleteContent } from '../content.js'; +import { success, error, info, formatEdge, formatView, formatNode, formatNodeList, formatStatus, formatExportResult, formatImportResult, formatDoctorResult, formatSuggestions, formatReviewItem, formatDecisionSummary, formatAtStatus, formatDiff, formatExtensionList, formatContentMeta } from './format.js'; /** * Write structured JSON to stdout with schemaVersion and command fields. @@ -116,7 +116,7 @@ export async function resolveContext(cwd, envelope) { */ export async function init(cwd) { try { - const graph = await initGraph(cwd); + const _graph = await initGraph(cwd); console.log(success('Initialized git-mind graph')); } catch (err) { console.error(error(`Failed to initialize: ${err.message}`)); diff --git a/src/dag.js b/src/dag.js index 013173b0..9e67487e 100644 --- a/src/dag.js +++ b/src/dag.js @@ -46,7 +46,7 @@ export function topoSort(nodes, forward) { const inDegree = new Map(); for (const n of nodes) inDegree.set(n, 0); - for (const [src, targets] of forward) { + for (const [_src, targets] of forward) { for (const t of targets) { if (inDegree.has(t)) { inDegree.set(t, inDegree.get(t) + 1); diff --git a/src/doctor.js b/src/doctor.js index d983a79e..ef6b8e4b 100644 --- a/src/doctor.js +++ b/src/doctor.js @@ -4,7 +4,7 @@ * Composable checks that identify structural issues in the knowledge graph. */ -import { isLowConfidence, SYSTEM_PREFIXES, extractPrefix } from './validators.js'; +import { SYSTEM_PREFIXES, extractPrefix } from './validators.js'; import { removeEdge } from './edges.js'; /** Prefixes excluded from orphan-node detection (system-generated + review decisions). */ diff --git a/src/nodes.js b/src/nodes.js index fa44f182..4713cb5b 100644 --- a/src/nodes.js +++ b/src/nodes.js @@ -3,7 +3,7 @@ * Node query and inspection for git-mind. */ -import { validateNodeId, extractPrefix, classifyPrefix } from './validators.js'; +import { extractPrefix, classifyPrefix } from './validators.js'; /** * @typedef {object} NodeInfo diff --git a/src/suggest.js b/src/suggest.js index e9cc22ce..4501663d 100644 --- a/src/suggest.js +++ b/src/suggest.js @@ -6,7 +6,7 @@ import { spawn } from 'node:child_process'; import { validateNodeId, validateEdgeType, validateConfidence } from './validators.js'; -import { queryEdges } from './edges.js'; +// edges.js imported for future use in context-aware suggestions import { extractContext } from './context.js'; /** From f26a73ec535b9b61105ee80d40280392d142ef0e Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 24 Feb 2026 11:44:15 -0800 Subject: [PATCH 5/8] ci: add coverage reporting and npm cache to CI pipeline (#290) Add @vitest/coverage-v8 as devDependency, configure coverage.include for src/**/*.js, add coverage step with artifact upload to CI workflow, and enable npm cache in setup-node for faster installs. --- .github/workflows/ci.yml | 10 + package-lock.json | 650 +++++++++++++++++++++++++++++++++++++++ package.json | 1 + vitest.config.js | 1 + 4 files changed, 662 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d3cb2e..5e6b4fee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + cache: 'npm' - name: Install dependencies run: npm ci @@ -38,3 +39,12 @@ jobs: - name: Run tests run: npm test + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.node-version }} + path: coverage/lcov.info diff --git a/package-lock.json b/package-lock.json index fb4ec217..5ca4d88b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.3", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.0.0", "globals": "^14.0.0", "prettier": "^3.0.0", @@ -30,6 +31,80 @@ "node": ">=22.0.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", @@ -904,6 +979,67 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -916,6 +1052,37 @@ "node": ">=18.0.0" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -923,6 +1090,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", @@ -974,6 +1152,17 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -1356,6 +1545,40 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1618,6 +1841,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2481,6 +2723,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -2598,6 +2857,13 @@ "node": ">=8" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -2743,6 +3009,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -2856,6 +3192,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-fetch-happen": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", @@ -3255,6 +3619,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3578,6 +3949,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3673,6 +4057,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -3688,6 +4118,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3743,6 +4197,122 @@ "node": ">=18" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4156,6 +4726,86 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index f97ec161..0257fb6f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.3", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.0.0", "globals": "^14.0.0", "prettier": "^3.0.0", diff --git a/vitest.config.js b/vitest.config.js index ffd50b87..86f2b6bf 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -11,6 +11,7 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'lcov'], reportsDirectory: './coverage', + include: ['src/**/*.js'], }, }, }); From 0c6a1a567b848c67084f746fb7270166c70f9f09 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 24 Feb 2026 11:45:56 -0800 Subject: [PATCH 6/8] fix: validate SHA with rev-parse before git log in processCommitCmd (#290) Add git rev-parse --verify guard to reject malformed refs (including option-injection via leading dashes) before passing SHA to git log. Mirrors the validation pattern already used in src/epoch.js. --- src/cli/commands.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/commands.js b/src/cli/commands.js index 4e2d77c0..0fbdccd8 100644 --- a/src/cli/commands.js +++ b/src/cli/commands.js @@ -297,6 +297,7 @@ npx git-mind process-commit "$SHA" 2>/dev/null || true */ export async function processCommitCmd(cwd, sha) { try { + execFileSync('git', ['rev-parse', '--verify', sha], { cwd, encoding: 'utf-8' }); const message = execFileSync('git', ['log', '-1', '--format=%B', sha], { cwd, encoding: 'utf-8' }); const graph = await loadGraph(cwd); const directives = await processCommit(graph, { sha, message }); From 6cfc2be07b8595ea3fbcc24b0f248d9b73b4a0db Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 24 Feb 2026 12:40:55 -0800 Subject: [PATCH 7/8] fix: replace all execSync with execFileSync to eliminate shell injection vectors (#290) - context.js: all 3 execSync calls converted to execFileSync with args arrays - context.js: shell fallback (2>/dev/null || ...) replaced with try-catch - context.js: sanitizeGitArg strengthened to block \s and \ (defense-in-depth) - merge.js: execSync for git-remote-get-url converted to execFileSync - test: added assertions for space, tab, and backslash injection vectors --- src/context.js | 20 +++++++++++--------- src/merge.js | 4 ++-- test/context.test.js | 6 ++++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/context.js b/src/context.js index d3f6b042..7527397e 100644 --- a/src/context.js +++ b/src/context.js @@ -4,7 +4,7 @@ * Builds structured prompts from repository state and graph data. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import { EDGE_TYPES, CANONICAL_PREFIXES } from './validators.js'; /** @@ -86,7 +86,7 @@ function inferLanguage(filePath) { export function extractFileContext(cwd, opts = {}) { const limit = opts.limit ?? 200; try { - const output = execSync('git ls-files', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + const output = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); const files = output.trim().split('\n').filter(Boolean).slice(0, limit); return files.map(path => ({ path, language: inferLanguage(path) })); } catch { @@ -103,7 +103,7 @@ export function extractFileContext(cwd, opts = {}) { */ /** Validate that a string is safe for use as a git command argument. */ function sanitizeGitArg(value) { - if (/[;&|`$(){}!#<>\n\r]/.test(value)) { + if (/[;&|`$(){}!#<>\s\\]/.test(value)) { throw new Error(`Unsafe characters in git argument: ${value}`); } return value; @@ -115,10 +115,12 @@ export function extractCommitContext(cwd, opts = {}) { try { // Get commits with short sha and first line of message - const logOutput = execSync( - `git log --format="%h %s" ${range} 2>/dev/null || git log --format="%h %s" -${limit}`, - { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } - ); + let logOutput; + try { + logOutput = execFileSync('git', ['log', '--format=%h %s', range], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + } catch { + logOutput = execFileSync('git', ['log', '--format=%h %s', `-${limit}`], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + } const lines = logOutput.trim().split('\n').filter(Boolean); return lines.map(line => { @@ -132,8 +134,8 @@ export function extractCommitContext(cwd, opts = {}) { // Get changed files for this commit let files = []; try { - const filesOutput = execSync( - `git diff-tree --no-commit-id --name-only -r ${sha}`, + const filesOutput = execFileSync( + 'git', ['diff-tree', '--no-commit-id', '--name-only', '-r', sha], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ); files = filesOutput.trim().split('\n').filter(Boolean); diff --git a/src/merge.js b/src/merge.js index 265acbfe..50737fc4 100644 --- a/src/merge.js +++ b/src/merge.js @@ -3,7 +3,7 @@ * Multi-repo graph merge — import another repo's graph with cross-repo qualification. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import { initGraph } from './graph.js'; import { qualifyNodeId } from './remote.js'; @@ -16,7 +16,7 @@ import { qualifyNodeId } from './remote.js'; */ export function detectRepoIdentifier(repoPath) { try { - const url = execSync('git remote get-url origin', { + const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], diff --git a/test/context.test.js b/test/context.test.js index 7d30e68b..fdb469d8 100644 --- a/test/context.test.js +++ b/test/context.test.js @@ -136,6 +136,12 @@ describe('context', () => { expect(() => extractCommitContext(tempDir, { range: 'HEAD\necho pwned' })).toThrow(/Unsafe characters/); }); + it('rejects range values with spaces, tabs, and backslashes', () => { + expect(() => extractCommitContext(tempDir, { range: 'HEAD --pretty=format:%H' })).toThrow(/Unsafe characters/); + expect(() => extractCommitContext(tempDir, { range: 'HEAD\t--exec=whoami' })).toThrow(/Unsafe characters/); + expect(() => extractCommitContext(tempDir, { range: 'HEAD\\necho' })).toThrow(/Unsafe characters/); + }); + it('uses exact match for file node association', async () => { await createEdge(graph, { source: 'file:src/app.js', target: 'spec:main', type: 'implements' }); await createEdge(graph, { source: 'file:src/app.json', target: 'spec:other', type: 'implements' }); From 20df34f9083ca2de9f0ce76b50a5f815ceacd538 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 24 Feb 2026 12:55:27 -0800 Subject: [PATCH 8/8] fix: address CodeRabbit round-2 feedback (#290) - ci.yml: collapse redundant npm test + test:coverage into single coverage step - ci.yml: add if: always() to upload step for diagnosis on failure - context.js: block leading hyphens in sanitizeGitArg to prevent option injection - suggest.js: fix misleading "imported" comment to TODO forward-declaration - test: add regression tests for option-injection vectors (--all, -n99999, etc.) --- .github/workflows/ci.yml | 6 ++---- src/context.js | 3 +++ src/suggest.js | 2 +- test/context.test.js | 7 +++++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e6b4fee..f327f939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,13 +37,11 @@ jobs: - name: Run linter run: npm run lint - - name: Run tests - run: npm test - - name: Run tests with coverage - run: npm run test:coverage + run: npx vitest run --coverage - name: Upload coverage report + if: always() uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.node-version }} diff --git a/src/context.js b/src/context.js index 7527397e..83ad8766 100644 --- a/src/context.js +++ b/src/context.js @@ -103,6 +103,9 @@ export function extractFileContext(cwd, opts = {}) { */ /** Validate that a string is safe for use as a git command argument. */ function sanitizeGitArg(value) { + if (/^-/.test(value)) { + throw new Error(`Unsafe characters in git argument: ${value}`); + } if (/[;&|`$(){}!#<>\s\\]/.test(value)) { throw new Error(`Unsafe characters in git argument: ${value}`); } diff --git a/src/suggest.js b/src/suggest.js index 4501663d..f026db65 100644 --- a/src/suggest.js +++ b/src/suggest.js @@ -6,7 +6,7 @@ import { spawn } from 'node:child_process'; import { validateNodeId, validateEdgeType, validateConfidence } from './validators.js'; -// edges.js imported for future use in context-aware suggestions +// TODO: import from './edges.js' when context-aware suggestions are implemented import { extractContext } from './context.js'; /** diff --git a/test/context.test.js b/test/context.test.js index fdb469d8..7432ef73 100644 --- a/test/context.test.js +++ b/test/context.test.js @@ -142,6 +142,13 @@ describe('context', () => { expect(() => extractCommitContext(tempDir, { range: 'HEAD\\necho' })).toThrow(/Unsafe characters/); }); + it('rejects range values with leading hyphens (option injection)', () => { + expect(() => extractCommitContext(tempDir, { range: '--all' })).toThrow(/Unsafe characters/); + expect(() => extractCommitContext(tempDir, { range: '--all-match' })).toThrow(/Unsafe characters/); + expect(() => extractCommitContext(tempDir, { range: '-n99999' })).toThrow(/Unsafe characters/); + expect(() => extractCommitContext(tempDir, { range: '--pretty=format:%H' })).toThrow(/Unsafe characters/); + }); + it('uses exact match for file node association', async () => { await createEdge(graph, { source: 'file:src/app.js', target: 'spec:main', type: 'implements' }); await createEdge(graph, { source: 'file:src/app.json', target: 'spec:other', type: 'implements' });