From b6a823335d2e67ac6138d434c39fa7a0ad82e283 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 18:53:18 +0800 Subject: [PATCH 01/34] chore: renaming the package to @makerx/verify --- .release-please-manifest.json | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 37fcefa..e18ee07 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "0.0.0" } diff --git a/package.json b/package.json index 9b57e12..72545f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@makerx/complexity-verifier", - "version": "1.0.0", + "name": "@makerx/verify", + "version": "0.0.0", "description": "Maintainability-index checker for TypeScript: computes cyclomatic complexity, Halstead volume, SLOC and a maintainability index, and fails files below a threshold.", "keywords": [ "cli", From 607541095dcbcc915f323a3c469f0a70774d2328 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 20:17:49 +0800 Subject: [PATCH 02/34] feat: orchestrate multiple checks with a convention-based verify CLI Generalise the tool from a single complexity gate into a collection of verifications that give AI coding agents back-pressure. The `verify` CLI (commander) runs checks by convention: with no `verify:*` scripts it runs the built-in default set in-process; with `verify:*` scripts it runs those in parallel, suppressing output unless a check fails. - Check registry (src/checks): native complexity, comment-block, block-comments, hardcoded-colors, forbidden-strings; external adapters for knip, circular-deps (skott), duplicate-code (jscpd), lint (oxlint) that skip gracefully when the tool is absent. - Orchestrator (src/orchestrator): resolveEntries + parallel npm-script runner with buffered/flush-on-fail output, --measure, --all, diff filters; in-process default runner. - Scaffolding (src/scaffold): interactive `verify init` and idempotent `verify upgrade-docs`, emitting agent commands/skills to .claude and .agent-skills. Templates shipped under templates/. - block-comments/hardcoded-colors/forbidden-strings and the verify runner are ported from staff0rd/assist; agent-file writing follows the MakerX data-streams CLI. Reimplemented on the TS compiler API and native fs to avoid ts-morph/grep/yaml deps (cross-platform). - Package renamed to @makerx/verify: bin `verify`, commander + enquirer deps, optional peerDependencies for the external tools, dogfoods itself via verify:* scripts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .oxlintrc.json | 2 +- package-lock.json | 100 ++++++++++++++--------- package.json | 52 +++++++++--- src/checks/block-comments.ts | 78 ++++++++++++++++++ src/checks/comment-block.ts | 31 +++++++ src/checks/complexity.ts | 32 ++++++++ src/checks/external.ts | 44 ++++++++++ src/checks/forbidden-strings.ts | 68 +++++++++++++++ src/checks/hardcoded-colors.ts | 56 +++++++++++++ src/checks/registry.test.ts | 40 +++++++++ src/checks/registry.ts | 64 +++++++++++++++ src/checks/types.ts | 27 ++++++ src/cli-core.ts | 70 ---------------- src/cli.test.ts | 60 -------------- src/cli.ts | 36 +++++++- src/commands/registerChecks.ts | 75 +++++++++++++++++ src/commands/registerInit.ts | 85 +++++++++++++++++++ src/commands/registerList.ts | 18 ++++ src/commands/registerUpgradeDocs.ts | 26 ++++++ src/index.ts | 11 +++ src/orchestrator/filterByChangedFiles.ts | 17 ++++ src/orchestrator/measure.ts | 35 ++++++++ src/orchestrator/resolveEntries.test.ts | 38 +++++++++ src/orchestrator/resolveEntries.ts | 44 ++++++++++ src/orchestrator/run.ts | 54 ++++++++++++ src/orchestrator/runDefaults.ts | 40 +++++++++ src/report.ts | 12 +-- src/scaffold/agentFiles.ts | 40 +++++++++ src/scaffold/init.test.ts | 49 +++++++++++ src/scaffold/init.ts | 39 +++++++++ src/scaffold/packageScripts.ts | 28 +++++++ src/scaffold/writeManaged.test.ts | 58 +++++++++++++ src/scaffold/writeManaged.ts | 46 +++++++++++ src/shared/color.ts | 10 +++ src/shared/comment-scan.ts | 65 +++++++++++++++ src/shared/config.ts | 39 +++++++++ src/shared/diff.test.ts | 37 +++++++++ src/shared/diff.ts | 43 ++++++++++ src/shared/git.ts | 20 +++++ src/shared/spawn.ts | 46 +++++++++++ templates/commands/verify.md | 15 ++++ templates/skills/verify/SKILL.md | 30 +++++++ 42 files changed, 1588 insertions(+), 192 deletions(-) create mode 100644 src/checks/block-comments.ts create mode 100644 src/checks/comment-block.ts create mode 100644 src/checks/complexity.ts create mode 100644 src/checks/external.ts create mode 100644 src/checks/forbidden-strings.ts create mode 100644 src/checks/hardcoded-colors.ts create mode 100644 src/checks/registry.test.ts create mode 100644 src/checks/registry.ts create mode 100644 src/checks/types.ts delete mode 100644 src/cli-core.ts delete mode 100644 src/cli.test.ts create mode 100644 src/commands/registerChecks.ts create mode 100644 src/commands/registerInit.ts create mode 100644 src/commands/registerList.ts create mode 100644 src/commands/registerUpgradeDocs.ts create mode 100644 src/orchestrator/filterByChangedFiles.ts create mode 100644 src/orchestrator/measure.ts create mode 100644 src/orchestrator/resolveEntries.test.ts create mode 100644 src/orchestrator/resolveEntries.ts create mode 100644 src/orchestrator/run.ts create mode 100644 src/orchestrator/runDefaults.ts create mode 100644 src/scaffold/agentFiles.ts create mode 100644 src/scaffold/init.test.ts create mode 100644 src/scaffold/init.ts create mode 100644 src/scaffold/packageScripts.ts create mode 100644 src/scaffold/writeManaged.test.ts create mode 100644 src/scaffold/writeManaged.ts create mode 100644 src/shared/color.ts create mode 100644 src/shared/comment-scan.ts create mode 100644 src/shared/config.ts create mode 100644 src/shared/diff.test.ts create mode 100644 src/shared/diff.ts create mode 100644 src/shared/git.ts create mode 100644 src/shared/spawn.ts create mode 100644 templates/commands/verify.md create mode 100644 templates/skills/verify/SKILL.md diff --git a/.oxlintrc.json b/.oxlintrc.json index 8e0fcff..e7ed544 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -26,7 +26,7 @@ "overrides": [ { // The CLI and its reporting surface legitimately write to stdout/stderr. - "files": ["src/cli.ts", "src/cli-core.ts", "src/report.ts"], + "files": ["src/cli.ts", "src/report.ts", "src/commands/**", "src/orchestrator/**", "src/checks/**"], "rules": { "no-console": "off" } diff --git a/package-lock.json b/package-lock.json index f0584fe..8a9106d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { - "name": "@makerx/complexity-verifier", - "version": "1.0.0", + "name": "@makerx/verify", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@makerx/complexity-verifier", - "version": "1.0.0", + "name": "@makerx/verify", + "version": "0.0.0", "license": "MIT", "dependencies": { + "commander": "^14.0.1", + "enquirer": "^2.4.1", "minimatch": "^10.2.5", "typescript": "^6.0.3" }, "bin": { - "complexity-verifier": "dist/cli.mjs" + "verify": "dist/cli.mjs" }, "devDependencies": { "@rollup/plugin-typescript": "^12.3.0", @@ -30,6 +32,26 @@ }, "engines": { "node": ">=24" + }, + "peerDependencies": { + "@jscpd/finder": "^4.0.1", + "jscpd": "^4.0.5", + "knip": "^5.64.2", + "skott": "^0.35.4" + }, + "peerDependenciesMeta": { + "@jscpd/finder": { + "optional": true + }, + "jscpd": { + "optional": true + }, + "knip": { + "optional": true + }, + "skott": { + "optional": true + } } }, "node_modules/@babel/helper-string-parser": { @@ -92,31 +114,6 @@ "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1721,11 +1718,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "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" @@ -1815,6 +1820,16 @@ "node": ">= 8.12" } }, + "node_modules/better-npm-audit/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -1858,13 +1873,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=20" } }, "node_modules/convert-source-map": { @@ -1936,6 +1950,19 @@ "dev": true, "license": "MIT" }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -3102,7 +3129,6 @@ "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" diff --git a/package.json b/package.json index 72545f4..e12150f 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,35 @@ { "name": "@makerx/verify", "version": "0.0.0", - "description": "Maintainability-index checker for TypeScript: computes cyclomatic complexity, Halstead volume, SLOC and a maintainability index, and fails files below a threshold.", + "description": "A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a CLI that orchestrates native + external checks by convention, plus a scaffolder for the checks and agent commands.", "keywords": [ + "ai", + "back-pressure", "cli", "code-quality", "complexity", - "cyclomatic-complexity", - "halstead", + "knip", "maintainability", - "maintainability-index", - "typescript" + "typescript", + "verification", + "verify" ], - "homepage": "https://github.com/MakerXStudio/complexity-verifier#readme", + "homepage": "https://github.com/MakerXStudio/verify#readme", "bugs": { - "url": "https://github.com/MakerXStudio/complexity-verifier/issues" + "url": "https://github.com/MakerXStudio/verify/issues" }, "license": "MIT", "author": "MakerX", "repository": { "type": "git", - "url": "git+https://github.com/MakerXStudio/complexity-verifier.git" + "url": "git+https://github.com/MakerXStudio/verify.git" }, "bin": { - "complexity-verifier": "./dist/cli.mjs" + "verify": "./dist/cli.mjs" }, "files": [ - "dist" + "dist", + "templates" ], "type": "module", "main": "./dist/index.mjs", @@ -50,8 +53,11 @@ "lint:fix": "oxlint --fix", "format": "oxfmt .", "format:check": "oxfmt --check .", - "verify": "run-p lint:fix format verify:complexity", - "verify:complexity": "node src/cli.ts --threshold 50 --max-comment-block-lines 1 --comment-block-pushback \"src/**/*.ts\"", + "verify": "node src/cli.ts", + "verify:lint": "oxlint --fix .", + "verify:format": "oxfmt .", + "verify:complexity": "node src/cli.ts complexity --threshold 50 \"src/**/*.ts\"", + "verify:comment-block": "node src/cli.ts comment-block --max-lines 1 --pushback \"src/**/*.ts\"", "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml", @@ -59,6 +65,8 @@ "prepublishOnly": "run-s build" }, "dependencies": { + "commander": "^14.0.1", + "enquirer": "^2.4.1", "minimatch": "^10.2.5", "typescript": "^6.0.3" }, @@ -75,6 +83,26 @@ "tslib": "^2.8.1", "vitest": "^4.1.10" }, + "peerDependencies": { + "@jscpd/finder": "^4.0.1", + "jscpd": "^4.0.5", + "knip": "^5.64.2", + "skott": "^0.35.4" + }, + "peerDependenciesMeta": { + "@jscpd/finder": { + "optional": true + }, + "jscpd": { + "optional": true + }, + "knip": { + "optional": true + }, + "skott": { + "optional": true + } + }, "engines": { "node": ">=24" } diff --git a/src/checks/block-comments.ts b/src/checks/block-comments.ts new file mode 100644 index 0000000..792978f --- /dev/null +++ b/src/checks/block-comments.ts @@ -0,0 +1,78 @@ +import fs from 'node:fs' + +import { minimatch } from 'minimatch' + +import { color } from '../shared/color.ts' +import { scanFileComments } from '../shared/comment-scan.ts' +import { loadVerifyConfig } from '../shared/config.ts' +import { parseDiffAddedLines } from '../shared/diff.ts' +import { gitDiffHead } from '../shared/git.ts' +import type { CheckResult } from './types.ts' + +const SCANNED_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts', '.js', '.jsx', '.mjs', '.cjs', '.yml', '.yaml'] + +// context: machine directives steer tooling, not humans, so a changed line carrying one is never a "comment" +// worth removing; `context:` is this project's durable-context escape hatch and is likewise exempt. +const MACHINE_DIRECTIVES = [ + 'oxlint-disable', + 'oxlint-enable', + '@ts-expect-error', + '@ts-ignore', + '@ts-nocheck', + 'eslint-disable', + 'eslint-enable', + 'prettier-ignore', + 'istanbul ignore', + 'v8 ignore', + 'c8 ignore', + '@vitest-environment', +] + +function isCommentExempt(text: string): boolean { + const lower = text.toLowerCase() + if (MACHINE_DIRECTIVES.some((d) => lower.includes(d))) return true + const stripped = text.replace(/^\s*(?:\/\/+|\/\*+|\*|#)\s*/, '') + return stripped.toLowerCase().startsWith('context:') +} + +function shouldScan(file: string, ignoreGlobs: readonly string[]): boolean { + if (!SCANNED_EXTENSIONS.some((ext) => file.endsWith(ext))) return false + if (ignoreGlobs.some((glob) => minimatch(file, glob))) return false + return fs.existsSync(file) +} + +function toSingleLine(text: string): string { + return text.replace(/\s+/g, ' ').trim() +} + +export type BlockCommentsOptions = { ignore?: readonly string[] } + +/** Fail on any comment sitting on a line changed against HEAD. Machine directives and `context:` are exempt. */ +export function runBlockComments(opts: BlockCommentsOptions = {}): CheckResult { + const ignoreGlobs = opts.ignore ?? loadVerifyConfig().blockComments?.ignore ?? [] + const added = parseDiffAddedLines(gitDiffHead()) + + const findings: Array<{ file: string; line: number; text: string }> = [] + for (const [file, lines] of added) { + if (!shouldScan(file, ignoreGlobs)) continue + for (const comment of scanFileComments(file)) { + if (!lines.has(comment.line)) continue + if (isCommentExempt(comment.text)) continue + findings.push({ file, line: comment.line, text: toSingleLine(comment.text) }) + } + } + findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line) + + if (findings.length === 0) { + console.log(color.green('No comments on changed lines.')) + return { name: 'block-comments', ok: true } + } + + console.error(color.red('\nComments on changed lines:\n')) + for (const { file, line, text } of findings) console.error(` ${file}:${line} → ${text}`) + console.error(color.red(`\nTotal: ${findings.length} comment(s)`)) + console.error( + '\nEvery comment on a changed line fails this gate, whether you added or edited it. Remove them and let the code document itself.', + ) + return { name: 'block-comments', ok: false } +} diff --git a/src/checks/comment-block.ts b/src/checks/comment-block.ts new file mode 100644 index 0000000..6a53a09 --- /dev/null +++ b/src/checks/comment-block.ts @@ -0,0 +1,31 @@ +import { DEFAULT_IGNORE, DEFAULT_PATTERN, findSourceFiles, resolvePattern } from '../analyze.ts' +import { findLongCommentBlocks } from '../comments.ts' +import { printCommentBlockReport } from '../report.ts' +import { color } from '../shared/color.ts' +import type { CheckResult } from './types.ts' + +export const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 + +export type CommentBlockOptions = { + pattern?: string + ignore?: readonly string[] + maxLines?: number + pushback?: boolean + warn?: boolean +} + +/** Native check: flag comment blocks longer than `maxLines`. JSDoc and `context:`-prefixed blocks are exempt. */ +export function runCommentBlock(opts: CommentBlockOptions = {}): CheckResult { + const maxLines = opts.maxLines ?? DEFAULT_MAX_COMMENT_BLOCK_LINES + const pattern = opts.pattern ?? DEFAULT_PATTERN + const files = findSourceFiles(resolvePattern(pattern), [...DEFAULT_IGNORE, ...(opts.ignore ?? [])]) + + const violations = findLongCommentBlocks(files, maxLines) + if (violations.length === 0) { + console.log(color.green(`No comment block over ${maxLines} line(s)`)) + return { name: 'comment-block', ok: true } + } + + printCommentBlockReport(violations, maxLines, { pushback: !!opts.pushback, warn: !!opts.warn }) + return { name: 'comment-block', ok: !!opts.warn } +} diff --git a/src/checks/complexity.ts b/src/checks/complexity.ts new file mode 100644 index 0000000..dbfb2f8 --- /dev/null +++ b/src/checks/complexity.ts @@ -0,0 +1,32 @@ +import { analyzeComplexity } from '../analyze.ts' +import { printFailure, printFileDetail, printMaintainabilityReport } from '../report.ts' +import { color } from '../shared/color.ts' +import type { CheckResult } from './types.ts' + +export const DEFAULT_THRESHOLD = 50 + +export type ComplexityOptions = { + pattern?: string + ignore?: readonly string[] + threshold?: number +} + +/** Native maintainability-index check. A single matched file prints a per-metric breakdown instead of the gate. */ +export function runComplexity(opts: ComplexityOptions = {}): CheckResult { + const threshold = opts.threshold ?? DEFAULT_THRESHOLD + const analysis = analyzeComplexity({ pattern: opts.pattern, ignore: opts.ignore, threshold }) + + if (analysis.files.length === 0) { + console.log(color.yellow('complexity: no files matched — skipping')) + return { name: 'complexity', ok: true } + } + if (analysis.files.length === 1) { + printFileDetail(analysis.files[0] as string) + return { name: 'complexity', ok: true } + } + + printMaintainabilityReport(analysis.results, analysis.failing, threshold) + const ok = analysis.failing.length === 0 + if (!ok) printFailure(analysis.failing, threshold) + return { name: 'complexity', ok } +} diff --git a/src/checks/external.ts b/src/checks/external.ts new file mode 100644 index 0000000..7151ca2 --- /dev/null +++ b/src/checks/external.ts @@ -0,0 +1,44 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { color } from '../shared/color.ts' +import { runCommand } from '../shared/spawn.ts' +import type { Check, CheckResult } from './types.ts' + +const BIN_EXTENSIONS = ['', '.cmd', '.ps1', '.exe'] + +/** True when a project-local binary is installed under node_modules/.bin (cross-platform). */ +export function hasLocalBin(bin: string, cwd: string = process.cwd()): boolean { + const dir = path.join(cwd, 'node_modules', '.bin') + return BIN_EXTENSIONS.some((ext) => fs.existsSync(path.join(dir, bin + ext))) +} + +export type ExternalCheckSpec = { + name: string + description: string + /** The node_modules/.bin executable that must be present for the check to run. */ + bin: string + /** The shell command run for the check (and written as the `verify:` script by init). */ + command: string + devDeps: string[] + inDefaultRun?: boolean +} + +/** Build a Check that shells out to an external tool, skipping gracefully when the tool is not installed. */ +export function defineExternalCheck(spec: ExternalCheckSpec): Check { + return { + name: spec.name, + description: spec.description, + kind: 'external', + inDefaultRun: spec.inDefaultRun ?? true, + scaffold: { script: spec.command, devDeps: spec.devDeps }, + async runDefault(): Promise { + if (!hasLocalBin(spec.bin)) { + console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`verify init\`)`)) + return { name: spec.name, ok: true, skipped: true } + } + const code = await runCommand(spec.command) + return { name: spec.name, ok: code === 0 } + }, + } +} diff --git a/src/checks/forbidden-strings.ts b/src/checks/forbidden-strings.ts new file mode 100644 index 0000000..1d04592 --- /dev/null +++ b/src/checks/forbidden-strings.ts @@ -0,0 +1,68 @@ +import fs from 'node:fs' + +import { minimatch } from 'minimatch' + +import { color } from '../shared/color.ts' +import { type ForbiddenStringsRule, loadVerifyConfig } from '../shared/config.ts' +import type { CheckResult } from './types.ts' + +export type ForbiddenStringViolation = { file: string; path: string; value: string } + +// Ported from https://github.com/staff0rd/assist verify/forbiddenStrings/findForbiddenStrings.ts +export function resolveStringsAtPath(data: unknown, path: string): string[] { + let current: unknown = data + for (const segment of path.split('.')) { + if (current === null || typeof current !== 'object') return [] + current = (current as Record)[segment] + } + if (typeof current === 'string') return [current] + if (Array.isArray(current)) return current.filter((value): value is string => typeof value === 'string') + return [] +} + +export function findRuleViolations(data: unknown, rule: ForbiddenStringsRule): ForbiddenStringViolation[] { + const violations: ForbiddenStringViolation[] = [] + for (const path of rule.paths) { + for (const value of resolveStringsAtPath(data, path)) { + if (minimatch(value, rule.disallowed)) violations.push({ file: rule.file, path, value }) + } + } + return violations +} + +export function findForbiddenStrings( + rules: readonly ForbiddenStringsRule[], + readJson: (file: string) => unknown, +): ForbiddenStringViolation[] { + return rules.flatMap((rule) => findRuleViolations(readJson(rule.file), rule)) +} + +function readJsonFile(file: string): unknown { + try { + return JSON.parse(fs.readFileSync(file, 'utf-8')) + } catch { + return undefined + } +} + +export type ForbiddenStringsOptions = { rules?: readonly ForbiddenStringsRule[] } + +/** Fail when configured JSON values match a `disallowed` glob. Rules come from verify config. */ +export function runForbiddenStrings(opts: ForbiddenStringsOptions = {}): CheckResult { + const rules = opts.rules ?? loadVerifyConfig().forbiddenStrings ?? [] + if (rules.length === 0) { + console.log(color.dim('forbidden-strings: no rules configured — skipping')) + return { name: 'forbidden-strings', ok: true } + } + + const violations = findForbiddenStrings(rules, readJsonFile) + if (violations.length === 0) { + console.log(color.green('No forbidden strings found.')) + return { name: 'forbidden-strings', ok: true } + } + + console.error(color.red('Forbidden strings found:\n')) + for (const { file, path, value } of violations) console.error(` ${file} → ${path}: ${value}`) + console.error(color.red(`\nTotal: ${violations.length} forbidden string(s)`)) + return { name: 'forbidden-strings', ok: false } +} diff --git a/src/checks/hardcoded-colors.ts b/src/checks/hardcoded-colors.ts new file mode 100644 index 0000000..ea41889 --- /dev/null +++ b/src/checks/hardcoded-colors.ts @@ -0,0 +1,56 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { minimatch } from 'minimatch' + +import { color } from '../shared/color.ts' +import { loadVerifyConfig } from '../shared/config.ts' +import type { CheckResult } from './types.ts' + +const COLOR_PATTERN = /0x[0-9a-fA-F]{6}|#[0-9a-fA-F]{3,8}/ +const SCANNED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.css', '.scss', '.sass', '.less', '.styl', '.vue', '.svelte', '.html'] + +function walk(dir: string, out: string[]): void { + if (!fs.existsSync(dir)) return + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + if (entry.name !== 'node_modules' && entry.name !== '.git') walk(full, out) + } else if (entry.isFile() && SCANNED_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { + out.push(full) + } + } +} + +export type HardcodedColorsOptions = { root?: string; ignore?: readonly string[] } + +/** Fail on literal hex / 0x colours in source. Cross-platform (no `grep`); suggests the design-token path. */ +export function runHardcodedColors(opts: HardcodedColorsOptions = {}): CheckResult { + const config = loadVerifyConfig() + const root = opts.root ?? config.hardcodedColors?.root ?? 'src' + const ignoreGlobs = opts.ignore ?? config.hardcodedColors?.ignore ?? [] + + const files: string[] = [] + walk(root, files) + + const findings: Array<{ file: string; line: number; value: string }> = [] + for (const file of files) { + if (ignoreGlobs.some((glob) => minimatch(file, glob))) continue + const lines = fs.readFileSync(file, 'utf-8').split('\n') + for (let i = 0; i < lines.length; i++) { + const match = (lines[i] as string).match(COLOR_PATTERN) + if (match) findings.push({ file, line: i + 1, value: match[0] }) + } + } + + if (findings.length === 0) { + console.log(color.green('No hardcoded colors found.')) + return { name: 'hardcoded-colors', ok: true } + } + + console.error(color.red('Hardcoded colors found:\n')) + for (const { file, line, value } of findings) console.error(` ${file}:${line} → ${value}`) + console.error(color.red(`\nTotal: ${findings.length} hardcoded color(s)`)) + console.error('\nUse named tokens from your design system / color library instead of literal hex values.') + return { name: 'hardcoded-colors', ok: false } +} diff --git a/src/checks/registry.test.ts b/src/checks/registry.test.ts new file mode 100644 index 0000000..02702fe --- /dev/null +++ b/src/checks/registry.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' + +import { CHECKS, defaultChecks, getCheck } from './registry.ts' + +describe('check registry', () => { + it('includes the native and external checks', () => { + const names = CHECKS.map((c) => c.name) + expect(names).toEqual( + expect.arrayContaining([ + 'complexity', + 'comment-block', + 'block-comments', + 'hardcoded-colors', + 'forbidden-strings', + 'knip', + 'circular-deps', + 'duplicate-code', + 'lint', + ]), + ) + }) + + it('marks external tool checks with scaffold devDeps', () => { + const knip = getCheck('knip') + expect(knip?.kind).toBe('external') + expect(knip?.scaffold.devDeps).toContain('knip') + }) + + it('scaffolds native checks back into the verify CLI', () => { + expect(getCheck('complexity')?.scaffold.script).toBe('verify complexity') + }) + + it('returns undefined for unknown checks', () => { + expect(getCheck('nope')).toBeUndefined() + }) + + it('every default check is in the registry', () => { + for (const check of defaultChecks()) expect(CHECKS).toContain(check) + }) +}) diff --git a/src/checks/registry.ts b/src/checks/registry.ts new file mode 100644 index 0000000..f2769f1 --- /dev/null +++ b/src/checks/registry.ts @@ -0,0 +1,64 @@ +import { runBlockComments } from './block-comments.ts' +import { runCommentBlock } from './comment-block.ts' +import { runComplexity } from './complexity.ts' +import { defineExternalCheck } from './external.ts' +import { runForbiddenStrings } from './forbidden-strings.ts' +import { runHardcodedColors } from './hardcoded-colors.ts' +import type { Check, CheckResult } from './types.ts' + +function nativeCheck(name: string, description: string, inDefaultRun: boolean, run: () => CheckResult): Check { + return { + name, + description, + kind: 'native', + inDefaultRun, + // Native checks scaffold as a call back into this CLI's own subcommand. + scaffold: { script: `verify ${name}` }, + runDefault: async () => run(), + } +} + +/** Every verification this package knows about. Order here is the order shown by `verify list`. */ +export const CHECKS: Check[] = [ + nativeCheck('complexity', 'Maintainability-index gate (cyclomatic + Halstead + SLOC)', true, () => runComplexity()), + nativeCheck('comment-block', 'Flag comment blocks longer than the limit (JSDoc / context: exempt)', true, () => runCommentBlock()), + nativeCheck('block-comments', 'Fail on any comment added to a line changed against HEAD', true, () => runBlockComments()), + nativeCheck('hardcoded-colors', 'Fail on literal hex / 0x colour values in source', true, () => runHardcodedColors()), + nativeCheck('forbidden-strings', 'Fail on disallowed JSON config values (rules from verify config)', true, () => runForbiddenStrings()), + defineExternalCheck({ + name: 'knip', + description: 'Unused files, exports and dependencies', + bin: 'knip', + command: 'knip --no-progress --treat-config-hints-as-errors', + devDeps: ['knip'], + }), + defineExternalCheck({ + name: 'circular-deps', + description: 'Circular dependency detection (skott)', + bin: 'skott', + command: 'skott --displayMode=raw --showCircularDependencies --exitCodeOnCircularDependencies=1', + devDeps: ['skott'], + }), + defineExternalCheck({ + name: 'duplicate-code', + description: 'Copy-paste detection (jscpd)', + bin: 'jscpd', + command: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src', + devDeps: ['jscpd'], + }), + defineExternalCheck({ + name: 'lint', + description: 'Lint + autofix (oxlint)', + bin: 'oxlint', + command: 'oxlint --fix .', + devDeps: ['oxlint'], + }), +] + +export function getCheck(name: string): Check | undefined { + return CHECKS.find((check) => check.name === name) +} + +export function defaultChecks(): Check[] { + return CHECKS.filter((check) => check.inDefaultRun) +} diff --git a/src/checks/types.ts b/src/checks/types.ts new file mode 100644 index 0000000..e64163d --- /dev/null +++ b/src/checks/types.ts @@ -0,0 +1,27 @@ +export type CheckKind = 'native' | 'external' + +export type CheckResult = { + name: string + ok: boolean + /** True when the check could not run because its underlying tool is not installed. */ + skipped?: boolean + durationMs?: number +} + +/** A single verification. Native checks run in-process; external checks shell out to a tool. */ +export type Check = { + name: string + description: string + kind: CheckKind + /** Whether this check runs as part of the default set when the project has no `verify:*` scripts. */ + inDefaultRun: boolean + /** Run the check with its default options and print its own report. Resolves to the outcome. */ + runDefault: () => Promise + /** How `verify init` wires this check into a consuming project. */ + scaffold: { + /** The npm script body written as `verify:`. */ + script: string + /** Extra devDependencies the check needs, installed on opt-in. */ + devDeps?: string[] + } +} diff --git a/src/cli-core.ts b/src/cli-core.ts deleted file mode 100644 index b554243..0000000 --- a/src/cli-core.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { analyzeComplexity, DEFAULT_IGNORE, DEFAULT_PATTERN, findLongCommentBlocks, findSourceFiles, resolvePattern } from './index.ts' -import { color, printCommentBlockReport, printFailure, printFileDetail, printMaintainabilityReport } from './report.ts' - -export type CliArgs = { - pattern?: string - ignore: string[] - threshold?: number - maxCommentBlockLines?: number - commentBlockPushback: boolean - commentBlockWarn: boolean -} - -export function parseArgs(argv: readonly string[]): CliArgs { - const ignore: string[] = [] - let pattern: string | undefined - let threshold: number | undefined - let maxCommentBlockLines: number | undefined - let commentBlockPushback = false - let commentBlockWarn = false - for (let i = 0; i < argv.length; i++) { - const arg = argv[i] - if (arg === '--threshold') { - threshold = Number(argv[++i]) - } else if (arg === '--ignore') { - ignore.push(argv[++i] as string) - } else if (arg === '--max-comment-block-lines') { - maxCommentBlockLines = Number(argv[++i]) - } else if (arg === '--comment-block-pushback') { - commentBlockPushback = true - } else if (arg === '--comment-block-warn') { - commentBlockWarn = true - } else { - pattern = arg - } - } - return { pattern, ignore, threshold, maxCommentBlockLines, commentBlockPushback, commentBlockWarn } -} - -/** Run the opt-in comment-block check, report any violations, and return whether it should fail the run. */ -function checkCommentBlocks(files: readonly string[], args: CliArgs): boolean { - if (args.maxCommentBlockLines === undefined) return false - const violations = findLongCommentBlocks(files, args.maxCommentBlockLines) - if (violations.length === 0) return false - printCommentBlockReport(violations, args.maxCommentBlockLines, { pushback: args.commentBlockPushback, warn: args.commentBlockWarn }) - return !args.commentBlockWarn -} - -/** Run the CLI against the given argv (already sliced of `node` + script). Returns the process exit code. */ -export function run(argv: readonly string[]): number { - const args = parseArgs(argv) - const { pattern, ignore, threshold } = args - const resolvedPattern = pattern ?? DEFAULT_PATTERN - const files = findSourceFiles(resolvePattern(resolvedPattern), [...DEFAULT_IGNORE, ...ignore]) - - if (files.length === 0) { - console.log(color.yellow('No files found matching pattern')) - return threshold !== undefined ? 1 : 0 - } - if (files.length === 1) { - printFileDetail(files[0] as string) - return checkCommentBlocks(files, args) ? 1 : 0 - } - - const { results, failing } = analyzeComplexity({ pattern: resolvedPattern, ignore, threshold }) - printMaintainabilityReport(results, failing, threshold) - const thresholdFailed = threshold !== undefined && failing.length > 0 - if (thresholdFailed) printFailure(failing, threshold) - const commentFailed = checkCommentBlocks(files, args) - return thresholdFailed || commentFailed ? 1 : 0 -} diff --git a/src/cli.test.ts b/src/cli.test.ts deleted file mode 100644 index 326031a..0000000 --- a/src/cli.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { type CliArgs, parseArgs } from './cli-core.ts' - -const DEFAULTS: CliArgs = { - pattern: undefined, - ignore: [], - threshold: undefined, - maxCommentBlockLines: undefined, - commentBlockPushback: false, - commentBlockWarn: false, -} - -describe('parseArgs', () => { - it('parses a positional pattern', () => { - expect(parseArgs(['src/**/*.ts'])).toEqual({ ...DEFAULTS, pattern: 'src/**/*.ts' }) - }) - - it('parses --threshold as a number', () => { - expect(parseArgs(['--threshold', '50'])).toEqual({ ...DEFAULTS, threshold: 50 }) - }) - - it('accumulates repeated --ignore flags', () => { - const { ignore } = parseArgs(['--ignore', '**/a.ts', '--ignore', '**/b.ts']) - expect(ignore).toEqual(['**/a.ts', '**/b.ts']) - }) - - it('parses a full combination of args', () => { - expect(parseArgs(['src/**/*.ts', '--threshold', '60', '--ignore', '**/gen.ts'])).toEqual({ - ...DEFAULTS, - pattern: 'src/**/*.ts', - ignore: ['**/gen.ts'], - threshold: 60, - }) - }) - - it('keeps the last positional when several are given', () => { - expect(parseArgs(['first', 'second']).pattern).toBe('second') - }) - - it('parses --max-comment-block-lines as a number', () => { - expect(parseArgs(['--max-comment-block-lines', '2']).maxCommentBlockLines).toBe(2) - }) - - it('parses the comment-block boolean flags', () => { - const { commentBlockPushback, commentBlockWarn } = parseArgs(['--comment-block-pushback', '--comment-block-warn']) - expect(commentBlockPushback).toBe(true) - expect(commentBlockWarn).toBe(true) - }) - - it('parses comment-block flags alongside the existing flags', () => { - expect(parseArgs(['src/**/*.ts', '--threshold', '50', '--max-comment-block-lines', '3', '--comment-block-pushback'])).toEqual({ - ...DEFAULTS, - pattern: 'src/**/*.ts', - threshold: 50, - maxCommentBlockLines: 3, - commentBlockPushback: true, - }) - }) -}) diff --git a/src/cli.ts b/src/cli.ts index a2c319e..e71b0e5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,36 @@ #!/usr/bin/env node -import { run } from './cli-core.ts' +import { createRequire } from 'node:module' -process.exit(run(process.argv.slice(2))) +import { Command } from 'commander' + +import { registerChecks } from './commands/registerChecks.ts' +import { registerInit } from './commands/registerInit.ts' +import { registerList } from './commands/registerList.ts' +import { registerUpgradeDocs } from './commands/registerUpgradeDocs.ts' +import { orchestrate } from './orchestrator/run.ts' + +const require = createRequire(import.meta.url) +const pkg = require('../package.json') as { version: string } + +const program = new Command() + +program + .name('verify') + .description('A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code.') + .version(pkg.version) + .option('--all', 'run all verify:* scripts, ignoring diff-based filters') + .option('--measure', "print a summary table of each verification's status and duration") + .option('--verbose', 'stream all output instead of suppressing passing runs') + .action(async (opts: { all?: boolean; measure?: boolean; verbose?: boolean }) => { + process.exitCode = await orchestrate(opts) + }) + +registerChecks(program) +registerList(program) +registerInit(program) +registerUpgradeDocs(program) + +program.parseAsync().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/src/commands/registerChecks.ts b/src/commands/registerChecks.ts new file mode 100644 index 0000000..d80ed16 --- /dev/null +++ b/src/commands/registerChecks.ts @@ -0,0 +1,75 @@ +import type { Command } from 'commander' + +import { runBlockComments } from '../checks/block-comments.ts' +import { runCommentBlock } from '../checks/comment-block.ts' +import { runComplexity } from '../checks/complexity.ts' +import { runForbiddenStrings } from '../checks/forbidden-strings.ts' +import { runHardcodedColors } from '../checks/hardcoded-colors.ts' +import { CHECKS } from '../checks/registry.ts' + +function finish(ok: boolean): void { + process.exitCode = ok ? 0 : 1 +} + +function collect(value: string, previous: string[]): string[] { + return [...previous, value] +} + +/** Register a directly-invocable subcommand for every built-in check (`verify complexity`, `verify knip`, …). */ +export function registerChecks(program: Command): void { + program + .command('complexity') + .description('Maintainability-index gate') + .argument('[pattern]', 'glob, directory, or file to analyse') + .option('--threshold ', 'fail files whose minimum MI is below this', Number) + .option('--ignore ', 'ignore glob (repeatable)', collect, []) + .action((pattern: string | undefined, opts: { threshold?: number; ignore: string[] }) => { + finish(runComplexity({ pattern, threshold: opts.threshold, ignore: opts.ignore }).ok) + }) + + program + .command('comment-block') + .description('Flag comment blocks longer than the limit (JSDoc / context: exempt)') + .argument('[pattern]', 'glob, directory, or file to scan') + .option('--max-lines ', 'maximum comment-block length', Number) + .option('--pushback', 'add AI back-pressure framing to the failure message') + .option('--warn', 'report without failing the run') + .option('--ignore ', 'ignore glob (repeatable)', collect, []) + .action((pattern: string | undefined, opts: { maxLines?: number; pushback?: boolean; warn?: boolean; ignore: string[] }) => { + finish(runCommentBlock({ pattern, maxLines: opts.maxLines, pushback: opts.pushback, warn: opts.warn, ignore: opts.ignore }).ok) + }) + + program + .command('block-comments') + .description('Fail on any comment added to a line changed against HEAD') + .option('--ignore ', 'ignore glob (repeatable)', collect, []) + .action((opts: { ignore: string[] }) => { + finish(runBlockComments({ ignore: opts.ignore }).ok) + }) + + program + .command('hardcoded-colors') + .description('Fail on literal hex / 0x colour values in source') + .option('--root ', 'directory to scan') + .option('--ignore ', 'ignore glob (repeatable)', collect, []) + .action((opts: { root?: string; ignore: string[] }) => { + finish(runHardcodedColors({ root: opts.root, ignore: opts.ignore }).ok) + }) + + program + .command('forbidden-strings') + .description('Fail on disallowed JSON config values (rules from verify config)') + .action(() => { + finish(runForbiddenStrings().ok) + }) + + for (const check of CHECKS.filter((c) => c.kind === 'external')) { + program + .command(check.name) + .description(check.description) + .action(async () => { + const result = await check.runDefault() + finish(result.ok) + }) + } +} diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts new file mode 100644 index 0000000..f5f8430 --- /dev/null +++ b/src/commands/registerInit.ts @@ -0,0 +1,85 @@ +import type { Command } from 'commander' +import enquirer from 'enquirer' + +import { CHECKS, defaultChecks } from '../checks/registry.ts' +import type { AgentTarget } from '../scaffold/agentFiles.ts' +import { applyInit, type InitResult } from '../scaffold/init.ts' +import { color } from '../shared/color.ts' +import { runCommand } from '../shared/spawn.ts' + +type Choice = { name: string; message: string; enabled?: boolean } + +function collect(value: string, previous: string[]): string[] { + return [...previous, value] +} + +async function multiselect(message: string, choices: Choice[]): Promise { + const response = (await enquirer.prompt({ type: 'multiselect', name: 'selected', message, choices })) as { selected: string[] } + return response.selected +} + +type InitCliOptions = { + defaultsOnly?: boolean + yes?: boolean + check: string[] + claude?: boolean + agents?: boolean +} + +async function resolveSelections(opts: InitCliOptions): Promise<{ checks: string[]; targets: AgentTarget[] }> { + const nonInteractive = !!opts.yes || !process.stdin.isTTY + if (nonInteractive) { + const targets: AgentTarget[] = [] + if (opts.claude !== false) targets.push('claude') + if (opts.agents) targets.push('agents') + return { checks: opts.check.length > 0 ? opts.check : defaultChecks().map((c) => c.name), targets } + } + + const checks = await multiselect( + 'Select checks to wire up', + CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.inDefaultRun })), + ) + const targets = (await multiselect('Select agent targets', [ + { name: 'claude', message: 'Claude (.claude/commands + skill)', enabled: true }, + { name: 'agents', message: 'Other agents (.agent-skills)', enabled: false }, + ])) as AgentTarget[] + return { checks, targets } +} + +function report(result: InitResult, defaultsOnly: boolean): void { + if (defaultsOnly) { + console.log(color.dim('\nDefaults-only: no verify:* scripts written — `verify` will run the built-in default set.')) + } + console.log(color.green(`\nScripts added: ${result.addedScripts.join(', ') || '(none new)'}`)) + for (const file of result.agentFiles) { + if (file.action === 'unchanged') continue + console.log(` ${file.action === 'created' ? '+' : '~'} ${file.path}`) + } + console.log(color.dim('\nRun `verify` to run your verifications.')) +} + +/** `verify init` — interactively scaffold checks + agent files into the current project. */ +export function registerInit(program: Command): void { + program + .command('init') + .description('Scaffold verifications and agent commands into this project') + .option('--defaults-only', 'do not write verify:* scripts; rely on `verify` built-in defaults') + .option('--yes', 'non-interactive: use flag selections (or defaults) without prompting') + .option('--check ', 'preselect a check (repeatable, non-interactive)', collect, []) + .option('--no-claude', 'do not write .claude/ files (non-interactive)') + .option('--agents', 'also write .agent-skills/ files (non-interactive)') + .action(async (opts: InitCliOptions) => { + const cwd = process.cwd() + const defaultsOnly = !!opts.defaultsOnly + const { checks, targets } = await resolveSelections(opts) + + const result = applyInit({ cwd, checks, targets, defaultsOnly }) + + if (result.devDeps.length > 0) { + console.log(`Installing ${result.devDeps.length} devDependenc(ies): ${result.devDeps.join(', ')}`) + const code = await runCommand(`npm install --save-dev ${result.devDeps.join(' ')}`, { cwd }) + if (code !== 0) console.error(color.yellow('npm install failed — install those devDependencies manually.')) + } + report(result, defaultsOnly) + }) +} diff --git a/src/commands/registerList.ts b/src/commands/registerList.ts new file mode 100644 index 0000000..32b2a28 --- /dev/null +++ b/src/commands/registerList.ts @@ -0,0 +1,18 @@ +import type { Command } from 'commander' + +import { CHECKS } from '../checks/registry.ts' +import { color } from '../shared/color.ts' + +/** `verify list` — show every built-in check, its kind, and whether it runs in the default set. */ +export function registerList(program: Command): void { + program + .command('list') + .description('List all built-in checks') + .action(() => { + console.log(color.heading('Built-in checks')) + for (const check of CHECKS) { + const tags = color.dim(`(${check.kind}, ${check.inDefaultRun ? 'default' : 'opt-in'})`) + console.log(` ${color.cyan(check.name.padEnd(18))} ${tags} ${check.description}`) + } + }) +} diff --git a/src/commands/registerUpgradeDocs.ts b/src/commands/registerUpgradeDocs.ts new file mode 100644 index 0000000..e84f51e --- /dev/null +++ b/src/commands/registerUpgradeDocs.ts @@ -0,0 +1,26 @@ +import type { Command } from 'commander' + +import { type AgentTarget, writeAgentFiles } from '../scaffold/agentFiles.ts' +import { summarise } from '../scaffold/writeManaged.ts' + +/** `verify upgrade-docs` — idempotently create/refresh the managed agent command + skill files. */ +export function registerUpgradeDocs(program: Command): void { + program + .command('upgrade-docs') + .description('Create or refresh managed agent command/skill files (.claude, .agent-skills)') + .option('--no-claude', 'skip .claude/ files') + .option('--no-agents', 'skip .agent-skills/ files') + .action((opts: { claude?: boolean; agents?: boolean }) => { + const targets: AgentTarget[] = [] + if (opts.claude !== false) targets.push('claude') + if (opts.agents !== false) targets.push('agents') + + const results = writeAgentFiles(process.cwd(), targets) + for (const result of results) { + if (result.action === 'unchanged') continue + console.log(` ${result.action === 'created' ? '+' : '~'} ${result.path}`) + } + const summary = summarise(results) + console.log(`\n${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged.`) + }) +} diff --git a/src/index.ts b/src/index.ts index 8b603c4..9ec217f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,13 @@ export { resolvePattern, scoreFiles, } from './analyze.ts' +export { runBlockComments } from './checks/block-comments.ts' +export { runCommentBlock } from './checks/comment-block.ts' +export { runComplexity } from './checks/complexity.ts' +export { runForbiddenStrings } from './checks/forbidden-strings.ts' +export { runHardcodedColors } from './checks/hardcoded-colors.ts' +export { CHECKS, defaultChecks, getCheck } from './checks/registry.ts' +export type { Check, CheckKind, CheckResult } from './checks/types.ts' export { type CommentBlockViolation, findLongCommentBlocks } from './comments.ts' export { type FunctionCallback, forEachFunction } from './functions.ts' export { @@ -18,3 +25,7 @@ export { countSloc, type HalsteadMetrics, } from './metrics.ts' +export { orchestrate } from './orchestrator/run.ts' +export { runDefaults } from './orchestrator/runDefaults.ts' +export { applyInit, type InitOptions, type InitResult } from './scaffold/init.ts' +export { type ForbiddenStringsRule, loadVerifyConfig, type VerifyConfig } from './shared/config.ts' diff --git a/src/orchestrator/filterByChangedFiles.ts b/src/orchestrator/filterByChangedFiles.ts new file mode 100644 index 0000000..a004cb3 --- /dev/null +++ b/src/orchestrator/filterByChangedFiles.ts @@ -0,0 +1,17 @@ +import { minimatch } from 'minimatch' + +import { getChangedFiles } from '../shared/git.ts' +import type { VerifyEntry } from './resolveEntries.ts' + +// Ported from https://github.com/staff0rd/assist verify/run/filterByChangedFiles.ts +/** Keep entries with no filter; drop filtered entries whose glob matches nothing in the working-tree diff. */ +export function filterByChangedFiles(entries: readonly VerifyEntry[]): VerifyEntry[] { + if (!entries.some((entry) => entry.filter)) return [...entries] + + const changedFiles = getChangedFiles() + return entries.filter((entry) => { + if (!entry.filter) return true + if (changedFiles.length === 0) return false + return changedFiles.some((file) => minimatch(file, entry.filter as string)) + }) +} diff --git a/src/orchestrator/measure.ts b/src/orchestrator/measure.ts new file mode 100644 index 0000000..88048eb --- /dev/null +++ b/src/orchestrator/measure.ts @@ -0,0 +1,35 @@ +// Ported from https://github.com/staff0rd/assist verify/run/printMeasureTable.ts +export type MeasureRecord = { + script: string + code: number + durationMs: number +} + +export function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + return `${(ms / 1000).toFixed(1)}s` +} + +/** Print a status/duration summary table for a set of runs, sorted slowest-first, with a TOTAL row. */ +export function printMeasureTable(records: readonly MeasureRecord[], totalMs: number): void { + const rows = [...records] + .sort((a, b) => b.durationMs - a.durationMs) + .map((record) => ({ + status: record.code === 0 ? '✓' : '✗', + name: record.script, + duration: formatDuration(record.durationMs), + })) + + const nameWidth = Math.max('Command'.length, 'TOTAL'.length, ...rows.map((row) => row.name.length)) + const durationWidth = Math.max('Duration'.length, formatDuration(totalMs).length, ...rows.map((row) => row.duration.length)) + + const line = (status: string, name: string, duration: string): string => + ` ${status.padEnd(6)} ${name.padEnd(nameWidth)} ${duration.padStart(durationWidth)}` + const separator = ` ${' '.repeat(6)} ${'─'.repeat(nameWidth)} ${'─'.repeat(durationWidth)}` + + console.log() + console.log(line('Status', 'Command', 'Duration')) + for (const row of rows) console.log(line(row.status, row.name, row.duration)) + console.log(separator) + console.log(line('', 'TOTAL', formatDuration(totalMs))) +} diff --git a/src/orchestrator/resolveEntries.test.ts b/src/orchestrator/resolveEntries.test.ts new file mode 100644 index 0000000..726c294 --- /dev/null +++ b/src/orchestrator/resolveEntries.test.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { resolveEntries } from './resolveEntries.ts' + +let dir: string + +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-entries-')) +}) +afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) +}) + +describe('resolveEntries', () => { + it('collects only verify:* scripts as npm run entries', () => { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { 'verify:a': 'x', build: 'y', 'verify:b': 'z' } })) + const entries = resolveEntries(dir) + expect(entries.map((e) => e.name).sort()).toEqual(['verify:a', 'verify:b']) + expect(entries.find((e) => e.name === 'verify:a')?.command).toBe('npm run verify:a') + }) + + it('returns [] when there are no verify:* scripts', () => { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { build: 'x' } })) + expect(resolveEntries(dir)).toEqual([]) + }) + + it('attaches diff filters from the verify config', () => { + fs.writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ scripts: { 'verify:web': 'x' }, verify: { filters: { 'verify:web': 'web/**' } } }), + ) + expect(resolveEntries(dir).find((e) => e.name === 'verify:web')?.filter).toBe('web/**') + }) +}) diff --git a/src/orchestrator/resolveEntries.ts b/src/orchestrator/resolveEntries.ts new file mode 100644 index 0000000..2321bd3 --- /dev/null +++ b/src/orchestrator/resolveEntries.ts @@ -0,0 +1,44 @@ +import fs from 'node:fs' +import path from 'node:path' + +export type VerifyEntry = { + name: string + command: string + cwd: string + filter?: string +} + +/** Walk up from `startDir` to the nearest package.json. */ +function findPackageJson(startDir: string): string | null { + let dir = path.resolve(startDir) + for (;;) { + const candidate = path.join(dir, 'package.json') + if (fs.existsSync(candidate)) return candidate + const parent = path.dirname(dir) + if (parent === dir) return null + dir = parent + } +} + +type PackageJson = { + scripts?: Record + verify?: { filters?: Record } +} + +/** Collect the project's `verify:*` npm scripts as parallelisable entries, nearest package.json wins. */ +export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { + const pkgPath = findPackageJson(cwd) + if (!pkgPath) return [] + let pkg: PackageJson + try { + pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as PackageJson + } catch { + return [] + } + const scripts = pkg.scripts ?? {} + const filters = pkg.verify?.filters ?? {} + const dir = path.dirname(pkgPath) + return Object.keys(scripts) + .filter((name) => name.startsWith('verify:')) + .map((name) => ({ name, command: `npm run ${name}`, cwd: dir, filter: filters[name] })) +} diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts new file mode 100644 index 0000000..29d2505 --- /dev/null +++ b/src/orchestrator/run.ts @@ -0,0 +1,54 @@ +import { color } from '../shared/color.ts' +import { runCommand, setVerbose } from '../shared/spawn.ts' +import { filterByChangedFiles } from './filterByChangedFiles.ts' +import { type MeasureRecord, printMeasureTable } from './measure.ts' +import { resolveEntries, type VerifyEntry } from './resolveEntries.ts' +import { runDefaults } from './runDefaults.ts' + +export type OrchestrateOptions = { + all?: boolean + measure?: boolean + verbose?: boolean +} + +async function runEntry(entry: VerifyEntry): Promise { + const startTime = Date.now() + const code = await runCommand(entry.command, { cwd: entry.cwd, quiet: true }) + return { script: entry.name, code, durationMs: Date.now() - startTime } +} + +function reportScriptResults(records: readonly MeasureRecord[], total: number): number { + const failed = records.filter((record) => record.code !== 0) + if (failed.length > 0) { + console.error(color.red(`\n${failed.length} script(s) failed:`)) + for (const record of failed) console.error(` - ${record.script} (exit code ${record.code})`) + return 1 + } + console.log(color.green(`\nAll ${total} verify script(s) passed`)) + return 0 +} + +/** + * The default `verify` action. Convention: if the project defines `verify:*` scripts, run those in parallel + * (output buffered, flushed only on failure); otherwise run the built-in default check set in-process. + */ +export async function orchestrate(opts: OrchestrateOptions = {}): Promise { + setVerbose(!!opts.verbose) + + const allEntries = resolveEntries() + if (allEntries.length === 0) return runDefaults({ measure: opts.measure }) + + const entries = opts.all ? allEntries : filterByChangedFiles(allEntries) + if (entries.length === 0) { + console.log('No verify scripts matched changed files — skipping') + return 0 + } + + console.log(`Running ${entries.length} verify script(s) in parallel:`) + for (const entry of entries) console.log(` - ${entry.name}`) + + const startTime = Date.now() + const records = await Promise.all(entries.map(runEntry)) + if (opts.measure) printMeasureTable(records, Date.now() - startTime) + return reportScriptResults(records, entries.length) +} diff --git a/src/orchestrator/runDefaults.ts b/src/orchestrator/runDefaults.ts new file mode 100644 index 0000000..690c24e --- /dev/null +++ b/src/orchestrator/runDefaults.ts @@ -0,0 +1,40 @@ +import { defaultChecks } from '../checks/registry.ts' +import type { CheckResult } from '../checks/types.ts' +import { color } from '../shared/color.ts' +import { type MeasureRecord, printMeasureTable } from './measure.ts' + +export type RunDefaultsOptions = { measure?: boolean } + +/** + * Run the built-in default check set in-process (the convention when a project has no `verify:*` scripts). + * Checks run sequentially with clear headers so their reports stay readable; each degrades to a pass/skip + * when not applicable (no files, no diff, tool absent, no rules). + */ +export async function runDefaults(opts: RunDefaultsOptions = {}): Promise { + const checks = defaultChecks() + console.log(`Running ${checks.length} built-in verification(s):`) + for (const check of checks) console.log(` - ${check.name}`) + console.log() + + const startTime = Date.now() + const records: MeasureRecord[] = [] + const results: CheckResult[] = [] + for (const check of checks) { + const checkStart = Date.now() + console.log(color.heading(`▶ ${check.name}`)) + const result = await check.runDefault() + results.push(result) + records.push({ script: check.name, code: result.ok ? 0 : 1, durationMs: Date.now() - checkStart }) + console.log() + } + + if (opts.measure) printMeasureTable(records, Date.now() - startTime) + + const failed = results.filter((result) => !result.ok) + if (failed.length > 0) { + console.error(color.red(`\n${failed.length} verification(s) failed: ${failed.map((f) => f.name).join(', ')}`)) + return 1 + } + console.log(color.green(`\nAll ${checks.length} verification(s) passed`)) + return 0 +} diff --git a/src/report.ts b/src/report.ts index b31cbdb..0aafa1b 100644 --- a/src/report.ts +++ b/src/report.ts @@ -4,17 +4,9 @@ import type { FileScore } from './analyze.ts' import type { CommentBlockViolation } from './comments.ts' import { forEachFunction } from './functions.ts' import { calculateCyclomaticComplexity, calculateHalstead, calculateMaintainabilityIndex, countSloc } from './metrics.ts' +import { color } from './shared/color.ts' -export const color = { - red: (s: string | number) => `\x1b[31m${s}\x1b[39m`, - green: (s: string | number) => `\x1b[32m${s}\x1b[39m`, - yellow: (s: string | number) => `\x1b[33m${s}\x1b[39m`, - magenta: (s: string | number) => `\x1b[35m${s}\x1b[39m`, - cyan: (s: string | number) => `\x1b[36m${s}\x1b[39m`, - dim: (s: string | number) => `\x1b[2m${s}\x1b[22m`, - bold: (s: string | number) => `\x1b[1m${s}\x1b[22m`, - heading: (s: string | number) => `\x1b[1m\x1b[4m${s}\x1b[24m\x1b[22m`, -} +export { color } const FORMULA = '171 - 5.2*ln(HalsteadVolume) - 0.23*CyclomaticComplexity - 16.2*ln(SLOC), clamped 0-100' diff --git a/src/scaffold/agentFiles.ts b/src/scaffold/agentFiles.ts new file mode 100644 index 0000000..e6acddf --- /dev/null +++ b/src/scaffold/agentFiles.ts @@ -0,0 +1,40 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { type ManagedFileResult, writeManaged } from './writeManaged.ts' + +export type AgentTarget = 'claude' | 'agents' + +type ManagedFile = { template: string; dest: string } + +const moduleDir = path.dirname(fileURLToPath(import.meta.url)) +// src/scaffold/*.ts and dist/scaffold/*.mjs both sit two levels below the package root, where templates/ lives. +const TEMPLATES_DIR = path.join(moduleDir, '..', '..', 'templates') + +export function readTemplate(relativePath: string): string { + return fs.readFileSync(path.join(TEMPLATES_DIR, ...relativePath.split('/')), 'utf-8') +} + +/** The managed agent files to emit for the chosen targets. Claude gets a slash command + skill; others get a skill. */ +export function managedFilesFor(targets: readonly AgentTarget[]): ManagedFile[] { + const files: ManagedFile[] = [] + if (targets.includes('claude')) { + files.push({ template: 'commands/verify.md', dest: '.claude/commands/verify.md' }) + files.push({ template: 'skills/verify/SKILL.md', dest: '.claude/skills/verify/SKILL.md' }) + } + if (targets.includes('agents')) { + files.push({ template: 'skills/verify/SKILL.md', dest: '.agent-skills/verify/SKILL.md' }) + } + return files +} + +/** Write (idempotently) the managed agent files for the chosen targets under `cwd`. */ +export function writeAgentFiles(cwd: string, targets: readonly AgentTarget[]): ManagedFileResult[] { + const results: ManagedFileResult[] = [] + for (const file of managedFilesFor(targets)) { + writeManaged(path.join(cwd, ...file.dest.split('/')), readTemplate(file.template), results) + } + results.sort((a, b) => a.path.localeCompare(b.path)) + return results +} diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts new file mode 100644 index 0000000..234b0fa --- /dev/null +++ b/src/scaffold/init.test.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { applyInit } from './init.ts' + +let dir: string + +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-init-')) + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'scratch' })) +}) +afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) +}) + +function readScripts(): Record { + return (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as { scripts: Record }).scripts +} + +describe('applyInit', () => { + it('writes verify:* scripts, agent files, and collects external devDeps', () => { + const result = applyInit({ cwd: dir, checks: ['complexity', 'knip'], targets: ['claude'], defaultsOnly: false }) + + const scripts = readScripts() + expect(scripts['verify:complexity']).toBe('verify complexity') + expect(scripts['verify:knip']).toContain('knip') + expect(scripts.verify).toBe('verify') + expect(result.devDeps).toContain('knip') + expect(fs.existsSync(path.join(dir, '.claude', 'commands', 'verify.md'))).toBe(true) + expect(fs.existsSync(path.join(dir, '.claude', 'skills', 'verify', 'SKILL.md'))).toBe(true) + }) + + it('defaults-only writes no verify:* scripts but keeps devDeps and agent files', () => { + const result = applyInit({ cwd: dir, checks: ['knip'], targets: ['agents'], defaultsOnly: true }) + + expect(Object.keys(readScripts())).toEqual(['verify']) + expect(result.devDeps).toContain('knip') + expect(fs.existsSync(path.join(dir, '.agent-skills', 'verify', 'SKILL.md'))).toBe(true) + }) + + it('does not clobber an existing verify:* script', () => { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { 'verify:complexity': 'custom' } })) + applyInit({ cwd: dir, checks: ['complexity'], targets: [], defaultsOnly: false }) + expect(readScripts()['verify:complexity']).toBe('custom') + }) +}) diff --git a/src/scaffold/init.ts b/src/scaffold/init.ts new file mode 100644 index 0000000..43d0fdf --- /dev/null +++ b/src/scaffold/init.ts @@ -0,0 +1,39 @@ +import path from 'node:path' + +import { getCheck } from '../checks/registry.ts' +import { type AgentTarget, writeAgentFiles } from './agentFiles.ts' +import { addVerifyScripts } from './packageScripts.ts' +import type { ManagedFileResult } from './writeManaged.ts' + +export type InitOptions = { + cwd: string + /** Selected check names. */ + checks: readonly string[] + targets: readonly AgentTarget[] + /** When true, do not write `verify:*` scripts — rely on `verify`'s built-in defaults. */ + defaultsOnly: boolean +} + +export type InitResult = { + addedScripts: string[] + /** devDependencies the selected external checks need (deduped). */ + devDeps: string[] + agentFiles: ManagedFileResult[] +} + +/** Pure scaffolding step: write package.json scripts + agent files, and report the devDeps to install. */ +export function applyInit(opts: InitOptions): InitResult { + const devDeps: string[] = [] + const scripts: Record = {} + + for (const name of opts.checks) { + const check = getCheck(name) + if (!check) continue + if (check.scaffold.devDeps) devDeps.push(...check.scaffold.devDeps) + if (!opts.defaultsOnly) scripts[`verify:${name}`] = check.scaffold.script + } + + const addedScripts = addVerifyScripts(path.join(opts.cwd, 'package.json'), scripts) + const agentFiles = writeAgentFiles(opts.cwd, opts.targets) + return { addedScripts, devDeps: [...new Set(devDeps)], agentFiles } +} diff --git a/src/scaffold/packageScripts.ts b/src/scaffold/packageScripts.ts new file mode 100644 index 0000000..2a8f8a3 --- /dev/null +++ b/src/scaffold/packageScripts.ts @@ -0,0 +1,28 @@ +import fs from 'node:fs' + +type PackageJson = { scripts?: Record } & Record + +/** + * Add the given `verify:*` scripts to package.json without clobbering existing ones, and ensure a top-level + * `verify` script that invokes the CLI. Returns the names of scripts actually added. + */ +export function addVerifyScripts(packageJsonPath: string, scripts: Record): string[] { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson + const existing = pkg.scripts ?? {} + const added: string[] = [] + + for (const [name, body] of Object.entries(scripts)) { + if (existing[name] === undefined) { + existing[name] = body + added.push(name) + } + } + if (existing.verify === undefined) { + existing.verify = 'verify' + added.push('verify') + } + + pkg.scripts = existing + fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`) + return added +} diff --git a/src/scaffold/writeManaged.test.ts b/src/scaffold/writeManaged.test.ts new file mode 100644 index 0000000..d2e5c32 --- /dev/null +++ b/src/scaffold/writeManaged.test.ts @@ -0,0 +1,58 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { type ManagedFileResult, summarise, writeManaged } from './writeManaged.ts' + +let dir: string + +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-managed-')) +}) +afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) +}) + +describe('writeManaged', () => { + it('creates a file (and parent dirs), then reports unchanged, then updated', () => { + const file = path.join(dir, 'nested', 'file.md') + + const created: ManagedFileResult[] = [] + writeManaged(file, 'a', created) + expect(created[0]?.action).toBe('created') + expect(fs.readFileSync(file, 'utf-8')).toBe('a') + + const unchanged: ManagedFileResult[] = [] + writeManaged(file, 'a', unchanged) + expect(unchanged[0]?.action).toBe('unchanged') + + const updated: ManagedFileResult[] = [] + writeManaged(file, 'b', updated) + expect(updated[0]?.action).toBe('updated') + expect(fs.readFileSync(file, 'utf-8')).toBe('b') + }) + + it('refuses to write through a symlink', () => { + const target = path.join(dir, 'target.md') + fs.writeFileSync(target, 'x') + const link = path.join(dir, 'link.md') + try { + fs.symlinkSync(target, link) + } catch { + return // symlink creation is privileged on some platforms — skip + } + expect(() => writeManaged(link, 'y', [])).toThrow(/symlink/) + }) + + it('summarise counts each action', () => { + expect( + summarise([ + { path: 'a', action: 'created' }, + { path: 'b', action: 'unchanged' }, + { path: 'c', action: 'created' }, + ]), + ).toEqual({ created: 2, updated: 0, unchanged: 1 }) + }) +}) diff --git a/src/scaffold/writeManaged.ts b/src/scaffold/writeManaged.ts new file mode 100644 index 0000000..56c7b06 --- /dev/null +++ b/src/scaffold/writeManaged.ts @@ -0,0 +1,46 @@ +import fs from 'node:fs' +import path from 'node:path' + +// Ported from https://github.com/MakerXStudio data-streams CLI upgrade-docs.ts writeManaged. +export type ManagedAction = 'unchanged' | 'updated' | 'created' + +export type ManagedFileResult = { + path: string + action: ManagedAction +} + +function readIfExists(file: string): string | null { + if (!fs.existsSync(file)) return null + return fs.readFileSync(file, 'utf-8') +} + +/** + * Idempotently write a CLI-managed file: create if missing, rewrite if changed, leave alone if identical. + * Refuses to write through a symlink or over a non-regular file. + */ +export function writeManaged(file: string, contents: string, results: ManagedFileResult[]): void { + const existing = readIfExists(file) + if (existing === null) { + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, contents, { encoding: 'utf8', flag: 'wx' }) + results.push({ path: file, action: 'created' }) + return + } + if (existing === contents) { + results.push({ path: file, action: 'unchanged' }) + return + } + const stat = fs.lstatSync(file) + if (stat.isSymbolicLink()) throw new Error(`Refusing to write through symlink at ${file}`) + if (!stat.isFile()) throw new Error(`Refusing to overwrite non-regular file at ${file}`) + fs.writeFileSync(file, contents, { encoding: 'utf8', flag: 'w' }) + results.push({ path: file, action: 'updated' }) +} + +export function summarise(results: readonly ManagedFileResult[]): { created: number; updated: number; unchanged: number } { + return { + created: results.filter((r) => r.action === 'created').length, + updated: results.filter((r) => r.action === 'updated').length, + unchanged: results.filter((r) => r.action === 'unchanged').length, + } +} diff --git a/src/shared/color.ts b/src/shared/color.ts new file mode 100644 index 0000000..6e52610 --- /dev/null +++ b/src/shared/color.ts @@ -0,0 +1,10 @@ +export const color = { + red: (s: string | number) => `\x1b[31m${s}\x1b[39m`, + green: (s: string | number) => `\x1b[32m${s}\x1b[39m`, + yellow: (s: string | number) => `\x1b[33m${s}\x1b[39m`, + magenta: (s: string | number) => `\x1b[35m${s}\x1b[39m`, + cyan: (s: string | number) => `\x1b[36m${s}\x1b[39m`, + dim: (s: string | number) => `\x1b[2m${s}\x1b[22m`, + bold: (s: string | number) => `\x1b[1m${s}\x1b[22m`, + heading: (s: string | number) => `\x1b[1m\x1b[4m${s}\x1b[24m\x1b[22m`, +} diff --git a/src/shared/comment-scan.ts b/src/shared/comment-scan.ts new file mode 100644 index 0000000..2ffe9ed --- /dev/null +++ b/src/shared/comment-scan.ts @@ -0,0 +1,65 @@ +import fs from 'node:fs' +import path from 'node:path' + +import ts from 'typescript' + +export type ScannedComment = { line: number; text: string } + +const YAML_EXTENSIONS = ['.yml', '.yaml'] + +function isYamlFile(file: string): boolean { + return YAML_EXTENSIONS.some((ext) => file.endsWith(ext)) +} + +function scriptKindFor(file: string): ts.ScriptKind { + return file.endsWith('.tsx') || file.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS +} + +function scanCodeComments(file: string, content: string): ScannedComment[] { + const sourceFile = ts.createSourceFile(path.basename(file), content, ts.ScriptTarget.Latest, true, scriptKindFor(file)) + const seen = new Set() + const out: ScannedComment[] = [] + const addRanges = (ranges: readonly ts.CommentRange[] | undefined): void => { + for (const range of ranges ?? []) { + if (seen.has(range.pos)) continue + seen.add(range.pos) + const { line } = sourceFile.getLineAndCharacterOfPosition(range.pos) + out.push({ line: line + 1, text: content.slice(range.pos, range.end) }) + } + } + const visit = (node: ts.Node): void => { + addRanges(ts.getLeadingCommentRanges(content, node.getFullStart())) + addRanges(ts.getTrailingCommentRanges(content, node.getEnd())) + ts.forEachChild(node, visit) + } + visit(sourceFile) + out.sort((a, b) => a.line - b.line) + return out +} + +/** A `#` starts a YAML comment only at line start or after whitespace, and never inside a quoted scalar. */ +function scanYamlComments(content: string): ScannedComment[] { + const out: ScannedComment[] = [] + const lines = content.split('\n') + for (let i = 0; i < lines.length; i++) { + const line = lines[i] as string + let inSingle = false + let inDouble = false + for (let j = 0; j < line.length; j++) { + const ch = line[j] + if (ch === "'" && !inDouble) inSingle = !inSingle + else if (ch === '"' && !inSingle) inDouble = !inDouble + else if (ch === '#' && !inSingle && !inDouble && (j === 0 || /\s/.test(line[j - 1] as string))) { + out.push({ line: i + 1, text: line.slice(j) }) + break + } + } + } + return out +} + +/** Extract every comment (with its 1-based line) from a source file, dispatching on extension. */ +export function scanFileComments(file: string): ScannedComment[] { + const content = fs.readFileSync(file, 'utf-8') + return isYamlFile(file) ? scanYamlComments(content) : scanCodeComments(file, content) +} diff --git a/src/shared/config.ts b/src/shared/config.ts new file mode 100644 index 0000000..c1d2a07 --- /dev/null +++ b/src/shared/config.ts @@ -0,0 +1,39 @@ +import fs from 'node:fs' +import path from 'node:path' + +/** A rule for the forbidden-strings check: JSON values at `paths` in `file` must not match `disallowed`. */ +export type ForbiddenStringsRule = { + file: string + paths: string[] + disallowed: string +} + +export type VerifyConfig = { + blockComments?: { ignore?: string[] } + hardcodedColors?: { ignore?: string[]; root?: string } + forbiddenStrings?: ForbiddenStringsRule[] +} + +const CONFIG_FILE = 'verify.config.json' + +function readJsonIfExists(file: string): unknown { + if (!fs.existsSync(file)) return undefined + try { + return JSON.parse(fs.readFileSync(file, 'utf-8')) + } catch { + return undefined + } +} + +/** Load per-repo check configuration from `verify.config.json`, falling back to a `verify` key in package.json. */ +export function loadVerifyConfig(cwd: string = process.cwd()): VerifyConfig { + const fromFile = readJsonIfExists(path.join(cwd, CONFIG_FILE)) + if (fromFile && typeof fromFile === 'object') return fromFile as VerifyConfig + + const pkg = readJsonIfExists(path.join(cwd, 'package.json')) + if (pkg && typeof pkg === 'object' && 'verify' in pkg) { + const verify = (pkg as { verify?: unknown }).verify + if (verify && typeof verify === 'object') return verify as VerifyConfig + } + return {} +} diff --git a/src/shared/diff.test.ts b/src/shared/diff.test.ts new file mode 100644 index 0000000..d6788df --- /dev/null +++ b/src/shared/diff.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' + +import { parseDiffAddedLines } from './diff.ts' + +const DIFF = `diff --git a/src/a.ts b/src/a.ts +index 111..222 100644 +--- a/src/a.ts ++++ b/src/a.ts +@@ -1,2 +1,3 @@ + const x = 1 ++const y = 2 + const z = 3 +diff --git a/src/new.ts b/src/new.ts +new file mode 100644 +--- /dev/null ++++ b/src/new.ts +@@ -0,0 +1,2 @@ ++export const a = 1 ++export const b = 2 +` + +describe('parseDiffAddedLines', () => { + it('maps changed files to their added line numbers', () => { + const added = parseDiffAddedLines(DIFF) + expect([...(added.get('src/a.ts') ?? [])]).toEqual([2]) + expect([...(added.get('src/new.ts') ?? [])]).toEqual([1, 2]) + }) + + it('ignores deletions (/dev/null targets)', () => { + const del = `--- a/gone.ts\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-const gone = 1\n` + expect(parseDiffAddedLines(del).size).toBe(0) + }) + + it('returns an empty map for an empty diff', () => { + expect(parseDiffAddedLines('').size).toBe(0) + }) +}) diff --git a/src/shared/diff.ts b/src/shared/diff.ts new file mode 100644 index 0000000..a565394 --- /dev/null +++ b/src/shared/diff.ts @@ -0,0 +1,43 @@ +// Ported from https://github.com/staff0rd/assist verify/blockComments/parseDiffAddedLines.ts +export type AddedLines = Map> + +const FILE_HEADER = /^\+\+\+ (?:b\/)?(.+)$/ +const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/ + +/** Map each file in a unified diff to the set of 1-based line numbers that were added or changed. */ +export function parseDiffAddedLines(diff: string): AddedLines { + const added: AddedLines = new Map() + let currentFile: string | null = null + let newLine = 0 + + for (const line of diff.split('\n')) { + const fileMatch = line.match(FILE_HEADER) + if (fileMatch) { + const file = fileMatch[1] as string + currentFile = file === '/dev/null' ? null : file + continue + } + + const hunkMatch = line.match(HUNK_HEADER) + if (hunkMatch) { + newLine = Number(hunkMatch[1]) + continue + } + + if (currentFile === null) continue + + if (line.startsWith('+')) { + let set = added.get(currentFile) + if (!set) { + set = new Set() + added.set(currentFile, set) + } + set.add(newLine) + newLine++ + } else if (!line.startsWith('-')) { + newLine++ + } + } + + return added +} diff --git a/src/shared/git.ts b/src/shared/git.ts new file mode 100644 index 0000000..1ef1c14 --- /dev/null +++ b/src/shared/git.ts @@ -0,0 +1,20 @@ +import { execSync } from 'node:child_process' + +/** Full working-tree diff against HEAD. Returns '' when git is unavailable or this is not a repo. */ +export function gitDiffHead(): string { + try { + return execSync('git diff HEAD', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }) + } catch { + return '' + } +} + +/** Files changed against HEAD (staged + unstaged). Empty when git is unavailable or nothing changed. */ +export function getChangedFiles(): string[] { + try { + const output = execSync('git diff --name-only HEAD', { encoding: 'utf8' }).trim() + return output === '' ? [] : output.split(/\r?\n/) + } catch { + return [] + } +} diff --git a/src/shared/spawn.ts b/src/shared/spawn.ts new file mode 100644 index 0000000..a7c8fcf --- /dev/null +++ b/src/shared/spawn.ts @@ -0,0 +1,46 @@ +import { spawn } from 'node:child_process' + +let verboseMode = false + +/** When verbose, per-command output is always streamed; otherwise it is buffered and only shown on failure. */ +export function setVerbose(verbose: boolean): void { + verboseMode = verbose +} + +function shouldSuppress(quiet?: boolean): boolean { + if (verboseMode) return false + return !!quiet || !!process.env.CLAUDECODE +} + +export type RunCommandOptions = { + cwd?: string + env?: Record + quiet?: boolean +} + +/** + * Run a shell command, returning its exit code. Suppressed output (quiet, or under Claude Code) is buffered + * and flushed to stdout only if the command fails, keeping passing runs quiet. + */ +export function runCommand(command: string, opts: RunCommandOptions = {}): Promise { + return new Promise((resolve) => { + const suppress = shouldSuppress(opts.quiet) + const child = spawn(command, [], { + stdio: suppress ? 'pipe' : 'inherit', + shell: true, + cwd: opts.cwd ?? process.cwd(), + env: opts.env ? { ...process.env, ...opts.env } : undefined, + }) + const chunks: Buffer[] = [] + if (suppress) { + child.stdout?.on('data', (data: Buffer) => chunks.push(data)) + child.stderr?.on('data', (data: Buffer) => chunks.push(data)) + } + child.on('close', (code) => { + const exitCode = code ?? 1 + if (suppress && exitCode !== 0 && chunks.length > 0) process.stdout.write(Buffer.concat(chunks)) + resolve(exitCode) + }) + child.on('error', () => resolve(127)) + }) +} diff --git a/templates/commands/verify.md b/templates/commands/verify.md new file mode 100644 index 0000000..eb45cee --- /dev/null +++ b/templates/commands/verify.md @@ -0,0 +1,15 @@ +--- +description: Run all verifications and fix everything they report +--- + +Run this project's verifications with `npx verify $ARGUMENTS 2>&1` (or the project's `verify` npm script — never the global binary). + +If anything fails: + +- Fix every reported issue, then run `verify` again. Repeat until it passes cleanly. +- Do **not** silence failures with `--warn`, and do not delete or weaken checks to make them pass. +- Treat escape hatches (like a `context:` comment prefix) as a last resort — prefer making the code self-explanatory. + +The point of these checks is back-pressure: they exist to stop hard-to-maintain code from landing. Take them seriously. + +ARGUMENTS: $ARGUMENTS diff --git a/templates/skills/verify/SKILL.md b/templates/skills/verify/SKILL.md new file mode 100644 index 0000000..77c8ca9 --- /dev/null +++ b/templates/skills/verify/SKILL.md @@ -0,0 +1,30 @@ +--- +name: verify +description: Run this project's code verifications (@makerx/verify) and fix what they report. Use before considering any code change complete, when asked to "verify", "run checks", or "run verify". +--- + +# verify + +This project uses [`@makerx/verify`](https://github.com/MakerXStudio/verify) — a collection of code +verifications that give AI coding agents back-pressure against writing hard-to-maintain code. + +## How to run + +Run the project's verifications via its npm script or `npx verify` (a pinned dev dependency — never a global install): + +``` +npx verify +``` + +- With no `verify:*` scripts defined, `verify` runs the built-in default checks in their default modes. +- With `verify:*` scripts defined, `verify` runs those in parallel; output is suppressed unless a check fails. +- Run a single built-in directly, e.g. `npx verify complexity` or `npx verify knip`. +- `npx verify list` shows every built-in check. + +## Working with failures + +1. Read each failure carefully — it explains what is wrong and how to fix it. +2. Fix the underlying issue. For complexity failures, **split the file**; do not game the metric by deleting + comments, joining lines, or shortening names. +3. Re-run `verify` until it passes. +4. Do not bypass checks with `--warn`, and reach for escape hatches (like `context:` comments) only as a last resort. From 513d5956564459bf5363da54f125e1d387ee8935 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 20:22:46 +0800 Subject: [PATCH 03/34] ci: run the non-editing verify in CI and refresh dependencies Point the shared node-ci workflow's lint-script at `npm run verify` in both pr.yml and publish.yml so CI runs the verifications. Make every `verify:*` script non-editing so the same command is safe in CI: - verify:lint -> `oxlint .` (was `--fix`) - verify:format -> `oxfmt --check .` (was rewriting) - add verify:check-types (`tsc --noEmit`) - add a local `fix` script (`lint:fix` + `format`) for auto-fixing Also refresh dependencies to latest: commander ^15, @types/node ^26, oxfmt ^0.58; peer ranges knip ^6, jscpd ^5, skott ^0.35.11; drop the unused @jscpd/finder peer (the check shells out to the jscpd bin). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pr.yml | 2 + .github/workflows/publish.yml | 2 + package-lock.json | 209 +++++++++++++++++++--------------- package.json | 22 ++-- 4 files changed, 130 insertions(+), 105 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7eeb522..2ed97f4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,5 +16,7 @@ jobs: node-version: 24.x run-build: true build-script: npm run build + # Runs the non-editing verifications (lint/format checks, complexity, comment-block, types). + lint-script: npm run verify test-script: npm run test:ci audit-script: npm run audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ed3fa9b..68dfd39 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,8 @@ jobs: with: working-directory: . node-version: 24.x + # Runs the non-editing verifications (lint/format checks, complexity, comment-block, types). + lint-script: npm run verify audit-script: npm run audit test-script: npm run test:ci output-test-results: true diff --git a/package-lock.json b/package-lock.json index 8a9106d..d09305e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "commander": "^14.0.1", + "commander": "^15.0.0", "enquirer": "^2.4.1", "minimatch": "^10.2.5", "typescript": "^6.0.3" @@ -19,11 +19,11 @@ }, "devDependencies": { "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^24.11.0", + "@types/node": "^26.1.1", "@vitest/coverage-v8": "^4.1.10", "better-npm-audit": "^3.11.0", "npm-run-all2": "^9.0.2", - "oxfmt": "^0.54.0", + "oxfmt": "^0.58.0", "oxlint": "^1.69.0", "rimraf": "^6.1.3", "rollup": "^4.62.2", @@ -183,9 +183,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.54.0.tgz", - "integrity": "sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.58.0.tgz", + "integrity": "sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==", "cpu": [ "arm" ], @@ -200,9 +200,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.54.0.tgz", - "integrity": "sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.58.0.tgz", + "integrity": "sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==", "cpu": [ "arm64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.54.0.tgz", - "integrity": "sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.58.0.tgz", + "integrity": "sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==", "cpu": [ "arm64" ], @@ -234,9 +234,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.54.0.tgz", - "integrity": "sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.58.0.tgz", + "integrity": "sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==", "cpu": [ "x64" ], @@ -251,9 +251,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.54.0.tgz", - "integrity": "sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.58.0.tgz", + "integrity": "sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==", "cpu": [ "x64" ], @@ -268,9 +268,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.54.0.tgz", - "integrity": "sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.58.0.tgz", + "integrity": "sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==", "cpu": [ "arm" ], @@ -285,9 +285,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.54.0.tgz", - "integrity": "sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.58.0.tgz", + "integrity": "sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==", "cpu": [ "arm" ], @@ -302,9 +302,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.54.0.tgz", - "integrity": "sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.58.0.tgz", + "integrity": "sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==", "cpu": [ "arm64" ], @@ -319,9 +319,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.54.0.tgz", - "integrity": "sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.58.0.tgz", + "integrity": "sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==", "cpu": [ "arm64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.54.0.tgz", - "integrity": "sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.58.0.tgz", + "integrity": "sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==", "cpu": [ "ppc64" ], @@ -353,9 +353,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.54.0.tgz", - "integrity": "sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.58.0.tgz", + "integrity": "sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==", "cpu": [ "riscv64" ], @@ -370,9 +370,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.54.0.tgz", - "integrity": "sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.58.0.tgz", + "integrity": "sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==", "cpu": [ "riscv64" ], @@ -387,9 +387,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.54.0.tgz", - "integrity": "sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.58.0.tgz", + "integrity": "sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==", "cpu": [ "s390x" ], @@ -404,9 +404,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.54.0.tgz", - "integrity": "sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.58.0.tgz", + "integrity": "sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==", "cpu": [ "x64" ], @@ -421,9 +421,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.54.0.tgz", - "integrity": "sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.58.0.tgz", + "integrity": "sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==", "cpu": [ "x64" ], @@ -438,9 +438,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.54.0.tgz", - "integrity": "sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.58.0.tgz", + "integrity": "sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==", "cpu": [ "arm64" ], @@ -455,9 +455,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.54.0.tgz", - "integrity": "sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.58.0.tgz", + "integrity": "sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==", "cpu": [ "arm64" ], @@ -472,9 +472,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.54.0.tgz", - "integrity": "sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.58.0.tgz", + "integrity": "sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==", "cpu": [ "ia32" ], @@ -489,9 +489,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.54.0.tgz", - "integrity": "sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.58.0.tgz", + "integrity": "sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==", "cpu": [ "x64" ], @@ -1051,6 +1051,29 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", @@ -1536,14 +1559,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~8.3.0" } }, "node_modules/@vitest/coverage-v8": { @@ -1873,12 +1896,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/convert-source-map": { @@ -2640,9 +2663,9 @@ } }, "node_modules/oxfmt": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.54.0.tgz", - "integrity": "sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.58.0.tgz", + "integrity": "sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==", "dev": true, "license": "MIT", "dependencies": { @@ -2658,25 +2681,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.54.0", - "@oxfmt/binding-android-arm64": "0.54.0", - "@oxfmt/binding-darwin-arm64": "0.54.0", - "@oxfmt/binding-darwin-x64": "0.54.0", - "@oxfmt/binding-freebsd-x64": "0.54.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.54.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.54.0", - "@oxfmt/binding-linux-arm64-gnu": "0.54.0", - "@oxfmt/binding-linux-arm64-musl": "0.54.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.54.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.54.0", - "@oxfmt/binding-linux-riscv64-musl": "0.54.0", - "@oxfmt/binding-linux-s390x-gnu": "0.54.0", - "@oxfmt/binding-linux-x64-gnu": "0.54.0", - "@oxfmt/binding-linux-x64-musl": "0.54.0", - "@oxfmt/binding-openharmony-arm64": "0.54.0", - "@oxfmt/binding-win32-arm64-msvc": "0.54.0", - "@oxfmt/binding-win32-ia32-msvc": "0.54.0", - "@oxfmt/binding-win32-x64-msvc": "0.54.0" + "@oxfmt/binding-android-arm-eabi": "0.58.0", + "@oxfmt/binding-android-arm64": "0.58.0", + "@oxfmt/binding-darwin-arm64": "0.58.0", + "@oxfmt/binding-darwin-x64": "0.58.0", + "@oxfmt/binding-freebsd-x64": "0.58.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.58.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.58.0", + "@oxfmt/binding-linux-arm64-gnu": "0.58.0", + "@oxfmt/binding-linux-arm64-musl": "0.58.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.58.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.58.0", + "@oxfmt/binding-linux-riscv64-musl": "0.58.0", + "@oxfmt/binding-linux-s390x-gnu": "0.58.0", + "@oxfmt/binding-linux-x64-gnu": "0.58.0", + "@oxfmt/binding-linux-x64-musl": "0.58.0", + "@oxfmt/binding-openharmony-arm64": "0.58.0", + "@oxfmt/binding-win32-arm64-msvc": "0.58.0", + "@oxfmt/binding-win32-ia32-msvc": "0.58.0", + "@oxfmt/binding-win32-x64-msvc": "0.58.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -3257,9 +3280,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index e12150f..918298a 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "lint:fix": "oxlint --fix", "format": "oxfmt .", "format:check": "oxfmt --check .", + "fix": "run-s lint:fix format", "verify": "node src/cli.ts", - "verify:lint": "oxlint --fix .", - "verify:format": "oxfmt .", + "verify:lint": "oxlint .", + "verify:format": "oxfmt --check .", + "verify:check-types": "tsc --noEmit", "verify:complexity": "node src/cli.ts complexity --threshold 50 \"src/**/*.ts\"", "verify:comment-block": "node src/cli.ts comment-block --max-lines 1 --pushback \"src/**/*.ts\"", "test": "vitest run", @@ -65,18 +67,18 @@ "prepublishOnly": "run-s build" }, "dependencies": { - "commander": "^14.0.1", + "commander": "^15.0.0", "enquirer": "^2.4.1", "minimatch": "^10.2.5", "typescript": "^6.0.3" }, "devDependencies": { "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^24.11.0", + "@types/node": "^26.1.1", "@vitest/coverage-v8": "^4.1.10", "better-npm-audit": "^3.11.0", "npm-run-all2": "^9.0.2", - "oxfmt": "^0.54.0", + "oxfmt": "^0.58.0", "oxlint": "^1.69.0", "rimraf": "^6.1.3", "rollup": "^4.62.2", @@ -84,15 +86,11 @@ "vitest": "^4.1.10" }, "peerDependencies": { - "@jscpd/finder": "^4.0.1", - "jscpd": "^4.0.5", - "knip": "^5.64.2", - "skott": "^0.35.4" + "jscpd": "^5.0.11", + "knip": "^6.25.0", + "skott": "^0.35.11" }, "peerDependenciesMeta": { - "@jscpd/finder": { - "optional": true - }, "jscpd": { "optional": true }, From c8e8b51e9d5ba270a81698f39a3a6e2663b0b6a5 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 20:22:56 +0800 Subject: [PATCH 04/34] docs: rewrite README and CLAUDE.md for @makerx/verify Rewrite the README as full user docs for the multi-check tool: install as a pinned dev dependency, the run-by-convention model, the built-in check table, `verify init` / `verify upgrade-docs`, per-repo config, CI/CD usage, and the programmatic API. Update CLAUDE.md to the new architecture, the non-editing verify + `fix` workflow, and the console/lint conventions. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 +- CLAUDE.md | 19 +++-- README.md | 197 ++++++++++++++++++++++++++++++++------------------- 3 files changed, 143 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f861af0..67f5c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ ## 1.0.0 (2026-07-08) - ### Features -* initial complexity-verifier CLI ([eeccd7c](https://github.com/MakerXStudio/complexity-verifier/commit/eeccd7c2238f07b02511574b5d7012b973ba07f1)) -* strengthen maintainability failure guidance ([0cb0193](https://github.com/MakerXStudio/complexity-verifier/commit/0cb01934bbd199142ca23d2927df472d0ef70737)) +- initial complexity-verifier CLI ([eeccd7c](https://github.com/MakerXStudio/complexity-verifier/commit/eeccd7c2238f07b02511574b5d7012b973ba07f1)) +- strengthen maintainability failure guidance ([0cb0193](https://github.com/MakerXStudio/complexity-verifier/commit/0cb01934bbd199142ca23d2927df472d0ef70737)) diff --git a/CLAUDE.md b/CLAUDE.md index 0504e1f..02d4fd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,26 @@ -# @makerx/complexity-verifier +# @makerx/verify -A maintainability-index + comment-block checker that gives AI agents back-pressure against writing hard-to-maintain code. Node >= 24; run the CLI from source with `node src/cli.ts`. +A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verify` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verify init` / `verify upgrade-docs`) that drops the checks and agent commands into a project. Node >= 24; run the CLI from source with `node src/cli.ts`. ## After making changes, run `npm run verify` -`npm run verify` runs (via `run-p`) `lint:fix`, `format`, and `verify:complexity` — the tool checking its own source. Fix anything it reports before finishing. +`npm run verify` runs `node src/cli.ts` — the tool checking its own source. With `verify:*` scripts defined in `package.json`, the orchestrator runs them in parallel and suppresses output unless one fails. The `verify:*` scripts are all **non-editing** (`verify:lint` = `oxlint .`, `verify:format` = `oxfmt --check .`, plus `verify:check-types`, `verify:complexity`, `verify:comment-block`) so the same command runs safely in CI (both workflows call `npm run verify` via the shared workflow's `lint-script`). To auto-fix lint/format locally, run `npm run fix` (`lint:fix` + `format`), then `npm run verify`. -`verify:complexity` runs with `--comment-block-pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. +`verify:comment-block` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. + +## Architecture + +- `src/cli.ts` — commander root program. Default action (no subcommand) = `orchestrate()`. Each built-in is a subcommand; plus `init`, `upgrade-docs`, `list`. +- `src/checks/` — one module per check + `registry.ts` (the `CHECKS` array). Native checks run in-process; external checks (`external.ts`) shell out and skip when the tool is absent. +- `src/orchestrator/` — the convention: `resolveEntries` finds `verify:*` scripts; if present, `run.ts` runs them in parallel; if absent, `runDefaults.ts` runs the built-in default set in-process. +- `src/scaffold/` — `init.ts` (pure), `agentFiles.ts` + `writeManaged.ts` (idempotent agent-file emission), `packageScripts.ts`. +- `src/shared/` — `color`, `git`, `diff`, `comment-scan` (TS compiler API — no ts-morph), `config`, `spawn`. +- `templates/` — shipped agent files (`.claude` command + skill), read at runtime relative to the module; listed in package.json `files`. ## Conventions - Comments explain _why_, not _what_. No comment block longer than 2 lines unless it is JSDoc (`/**`) or prefixed `context:`. - Keep functions small so files stay above the MI threshold; the fix for a failing file is to split it, never to game the metric. +- `console` is only allowed in the CLI's reporting surface (`src/cli.ts`, `src/report.ts`, `src/commands/**`, `src/orchestrator/**`, `src/checks/**`) — see `.oxlintrc.json`. +- Prefer the TypeScript compiler API and existing `src/shared` helpers over new dependencies. - Tests are co-located `*.test.ts` (vitest). diff --git a/README.md b/README.md index 7a6d3bd..fe6c21b 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,181 @@ -# @makerx/complexity-verifier +# @makerx/verify -A maintainability-index checker for TypeScript aimed at providing better back-pressure to AI agents. It parses your `.ts`/`.tsx` sources with the TypeScript compiler API and, for every function, computes: +A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code — and improve code quality for everyone. -- **Cyclomatic complexity** — the number of independent paths through the code. -- **Halstead volume** — a size measure derived from the operators and operands used. -- **SLOC** — source lines of code (blank lines and comments excluded). -- **Maintainability index (MI)** — a 0–100 score combining the three above. +`verify` ships both: -A file's score is the **minimum MI across its functions**, and the check **fails any file below the `--threshold`**. Lower MI means harder to maintain. +- a **CLI** that orchestrates a set of checks by convention, and +- the **agent commands / skills** (`.claude/`, `.agent-skills/`) that steer AI assistants to run those checks and fix what they report. + +Complexity was the first check. It is now just one of several, and the set grows over time. ## Install +Install as a **pinned dev dependency** — never globally. A locked version means the exact same tool runs on your machine and in CI/CD: + ```sh -npm i -D @makerx/complexity-verifier +npm install --save-dev @makerx/verify ``` -Requires Node.js >= 24. +Requires Node.js >= 24. Invoke it via an npm script or `npx verify` — not a global binary on `PATH`. -## CLI usage +The quickest way to wire it into a project: ```sh -# Fail if any file scores below a maintainability index of 50 -npx complexity-verifier --threshold 50 "src/**/*.ts" +npx verify init +``` + +## How `verify` decides what to run + +Running `verify` with no subcommand follows a convention: + +- **No `verify:*` scripts in `package.json`** → it runs the **built-in default checks** in their default modes. Each check degrades gracefully — it passes or skips when it does not apply (no files, no diff, tool not installed, no rules configured). +- **`verify:*` scripts present** → it runs **those** in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code). Add `--verbose` to stream everything. + +You take control by adding `verify:*` scripts. Each can call a built-in or run your own command: + +```jsonc +{ + "scripts": { + "verify": "verify", + "verify:complexity": "verify complexity --threshold 50 \"src/**/*.ts\"", + "verify:knip": "knip --no-progress --treat-config-hints-as-errors", + "verify:types": "tsc --noEmit", + }, +} ``` -Arguments: +Run a single built-in directly: `verify complexity`, `verify knip`, … and `verify list` shows them all. -- **``** (positional) — a glob, a directory, or a single file. Defaults to `{src,server,shared}/**/*.ts`. A bare filename with no glob or slash is treated as a recursive search (`**/`). -- **`--threshold `** — fail (exit code `1`) when any file's minimum MI is below `n`. Without a threshold the tool just reports. -- **`--ignore `** — exclude matching files. Repeatable. Appended to the default ignore of `**/*test.ts*`. +Flags on the bare `verify` command: -When exactly one file is matched, the tool prints a detailed per-function breakdown (SLOC, cyclomatic complexity, Halstead metrics, and MI) instead of the pass/fail report — handy when diagnosing a single file. +- `--measure` — print a status/duration summary table. +- `--all` — run every `verify:*` script, ignoring diff-based filters. +- `--verbose` — stream all output instead of suppressing passing runs. -## Comment-block check (opt-in) +## Built-in checks -AI agents love to pad code with long comment blocks that narrate _what_ the code does rather than _why_. Such comments add no value, rot as the surrounding code changes (and LLMs sometimes trust the stale comment over the code), and inflate complexity. This opt-in check pushes back on them. +| Check | Kind | What it catches | +| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | +| `comment-block` | native | Comment blocks longer than the limit. JSDoc (`/**`) and `context:`-prefixed blocks are exempt. | +| `block-comments` | native | Any comment added to a line changed against `HEAD`. Machine directives (`eslint-disable`, `@ts-expect-error`, …) and `context:` are exempt. | +| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | +| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `knip` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | +| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | +| `lint` | external | Lint + autofix ([oxlint](https://oxc.rs)). | + +External checks shell out to their tool and **skip gracefully when it is not installed** — `verify init` installs the ones you opt into. They are declared as optional `peerDependencies`. + +### `complexity` ```sh -# Fail if any comment block is longer than 2 lines -npx complexity-verifier --max-comment-block-lines 2 "src/**/*.ts" +verify complexity --threshold 50 "src/**/*.ts" ``` -- **`--max-comment-block-lines `** — enable the check and fail (exit code `1`) on any comment block with **more than** `n` lines. A block is a run of consecutive whole-line `//` comments or a `/* … */` block comment. Runs independently of `--threshold`. -- **`--comment-block-pushback`** — add AI back-pressure framing to the failure message (a warning that keeping the comment pages a human for approval). Colleagues have found this framing stops an agent from reflexively silencing the check. -- **`--comment-block-warn`** — report violations without failing (exit code stays `0`). +- `[pattern]` — glob, directory, or file. Defaults to `{src,server,shared}/**/*.ts`. +- `--threshold ` — fail when any file's minimum maintainability index is below `n`. +- `--ignore ` — exclude files (repeatable; appended to the default `**/*test.ts*`). -**Exemptions:** +A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown is printed instead of the gate — handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). -- **JSDoc** — blocks opening with `/**` are never flagged. -- **Trailing/inline comments** — `const x = 1 // note` is not a block and is ignored. -- **`context:` escape hatch** — if a comment is genuinely durable context the code cannot express, prefix its first line with `context:` (case-insensitive) to keep it: +### `comment-block` - ```ts - // context: the upstream API returns seconds, not milliseconds — do not "fix" this - const timeoutMs = timeout * 1000 - ``` +```sh +verify comment-block --max-lines 2 --pushback "src/**/*.ts" +``` -Example in `package.json`: +- `--max-lines ` — fail on comment blocks longer than `n` lines (default 2). +- `--pushback` — add AI back-pressure framing to the failure (keeping the comment "pages a human"). +- `--warn` — report without failing. +- `--ignore ` — exclude files (repeatable). -```json -{ - "scripts": { - "verify:complexity": "complexity-verifier --threshold 50 \"src/**/*.ts\"" - } -} +Prefix a comment's first line with `context:` to keep genuinely durable context: + +```ts +// context: the upstream API returns seconds, not milliseconds — do not "fix" this +const timeoutMs = timeout * 1000 ``` -## Fixing failures — one file at a time +## Scaffolding a project -When the check fails, **diagnose and fix one file at a time** — do not investigate or fix multiple files in parallel. Run the CLI against a single file to see all of its metrics: +### `verify init` + +Interactively wire verifications and agent files into the current project: ```sh -npx complexity-verifier src/some/hard-to-maintain-file.ts +verify init ``` -For larger files, start by extracting responsibilities into smaller files; then reduce branching and simplify expressions in the worst-scoring functions. +It lets you multi-select **checks** and **agent targets** (Claude `.claude/`, and/or cross-vendor `.agent-skills/`), then: -## Programmatic API +- writes the selected `verify:*` scripts to `package.json` (never clobbering existing ones), +- installs the external checks' tools as `--save-dev`, +- emits the agent command/skill files. -The package is also usable as a library: +Options: -```ts -import { analyzeComplexity } from '@makerx/complexity-verifier' - -const { results, failing, passed } = analyzeComplexity({ - pattern: 'src/**/*.ts', - ignore: ['**/*.generated.ts'], - threshold: 50, -}) - -if (!passed) { - for (const { file, min, avg } of failing) { - console.error(`${file}: min MI ${min.toFixed(1)} (avg ${avg.toFixed(1)})`) - } -} +- `--defaults-only` — do **not** write `verify:*` scripts; rely on `verify`'s built-in defaults (still installs opted-in tools and writes agent files). +- `--yes` — non-interactive; use `--check ` (repeatable), `--no-claude`, `--agents`. + +### `verify upgrade-docs` + +Idempotently create/refresh the managed agent files (created / updated / unchanged; refuses to write through symlinks): + +```sh +verify upgrade-docs # both targets +verify upgrade-docs --no-agents # only .claude/ ``` -Lower-level metric helpers are also exported: `calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, and `forEachFunction`. +## Configuration -## The maintainability index formula +Some checks read per-repo config from `verify.config.json`, or a `verify` key in `package.json`: +```jsonc +{ + "verify": { + "blockComments": { "ignore": ["**/*.generated.ts"] }, + "hardcodedColors": { "root": "src", "ignore": ["**/tokens.ts"] }, + "forbiddenStrings": [{ "file": "app.json", "paths": ["env.LOG_LEVEL"], "disallowed": "debug" }], + "filters": { "verify:web": "web/**" }, + }, +} ``` -MI = 171 - 5.2 * ln(HalsteadVolume) - 0.23 * CyclomaticComplexity - 16.2 * ln(SLOC) + +`filters` scopes a `verify:*` script to a diff glob: it is skipped unless a changed file matches (bypass with `verify --all`). + +## CI/CD + +Because it is a pinned dev dependency, CI runs the identical tool: + +```yaml +- run: npm ci +- run: npx verify --measure ``` -The result is clamped to the range 0–100. A function with zero Halstead volume or zero SLOC scores 100. +## Programmatic API -Rough interpretation: +```ts +import { analyzeComplexity, orchestrate, CHECKS } from '@makerx/verify' -- **> 65** — good maintainability. -- **50–65** — moderate; watch for growth. -- **< 50** — hard to maintain; consider refactoring. +const { failing, passed } = analyzeComplexity({ pattern: 'src/**/*.ts', threshold: 50 }) +``` -Thresholds are a matter of taste — pick one that fits your codebase and enforce it in CI. +Exports include `analyzeComplexity`, the check registry (`CHECKS`, `getCheck`, `defaultChecks`), `orchestrate`, `runDefaults`, `applyInit`, `loadVerifyConfig`, the individual `run*` check functions, and the lower-level complexity helpers (`calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, `forEachFunction`). -## Publishing +## The maintainability index formula + +``` +MI = 171 - 5.2 * ln(HalsteadVolume) - 0.23 * CyclomaticComplexity - 16.2 * ln(SLOC) +``` -Releases are automated with [release-please](https://github.com/googleapis/release-please) and published to npm via **npm trusted publishing (OIDC)**. A trusted publisher for this package must be configured on npmjs.com (linking the GitHub repository and the release workflow) before the first automated publish will succeed. +Clamped to 0–100. Rough interpretation: **> 65** good, **50–65** moderate, **< 50** hard to maintain. ## Attribution -The metric algorithms are ported from [staff0rd/assist](https://github.com/staff0rd/assist/tree/75a75899d7578769a433fb8058c96dd29410c254/src/commands/complexity). +The `verify` runner, `block-comments`, `hardcoded-colors`, and `forbidden-strings` checks are ported from [staff0rd/assist](https://github.com/staff0rd/assist); the maintainability metrics originate there too. The idempotent agent-file scaffolding follows the MakerX data-streams CLI's `upgrade-docs`. See [Steering the Vibe: Verify](https://staffordwilliams.com/blog/2025/12/14/steering-the-vibe-verify/) and [Complexity](https://staffordwilliams.com/blog/2026/02/22/steering-the-vibe-complexity/). ## License From 327f2ee1dabdce26ed5dffcacb34a8f19e4c7781 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 23:21:49 +0800 Subject: [PATCH 05/34] fix: auto-fix locally and check-only in CI; call built-ins from verify:* scripts Correct the run model so the person or AI agent running `verify` locally gets auto-fixes (no token churn hand-fixing lint/format), while CI fails instead of rewriting: - Fixable checks (lint, format) fix by default and switch to check-only when CI is set (or `--check`). `--fix`/`--check` on the root force a mode; it propagates to spawned verify:* scripts via the VERIFY_MODE env. - Add `format` (oxfmt) and `check-types` (tsc, skipped without tsconfig) as built-in checks; `lint` now fixes locally / checks in CI. - Scaffold every check (native and external) as `verify ` so the fix-vs-check logic lives in the tool, not in hand-rolled raw commands; the repo's own verify:* scripts now call `node src/cli.ts `. - Resolve external tools from the local node_modules/.bin (prepended to PATH) so they run however verify was invoked, not only via npm scripts. - Drop the redundant per-subcommand --check/--fix and init's --check (renamed to --select): duplicating the root option blanks commander's parsed options. - Simplify package.json scripts (prepublishOnly -> npm run build, drop the now-redundant fix script). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 4 +++- README.md | 19 +++++++++++------ package.json | 9 ++++---- src/checks/external.ts | 31 ++++++++++++++++++++++----- src/checks/registry.test.ts | 7 ++++-- src/checks/registry.ts | 39 +++++++++++++++++++++++++--------- src/checks/types.ts | 3 +++ src/cli.ts | 4 +++- src/commands/registerChecks.ts | 1 + src/commands/registerInit.ts | 6 +++--- src/orchestrator/run.ts | 5 +++++ src/shared/mode.ts | 19 +++++++++++++++++ 12 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 src/shared/mode.ts diff --git a/CLAUDE.md b/CLAUDE.md index 02d4fd4..21cc3ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,9 @@ A growing collection of code **verifications** that give AI coding agents back-p ## After making changes, run `npm run verify` -`npm run verify` runs `node src/cli.ts` — the tool checking its own source. With `verify:*` scripts defined in `package.json`, the orchestrator runs them in parallel and suppresses output unless one fails. The `verify:*` scripts are all **non-editing** (`verify:lint` = `oxlint .`, `verify:format` = `oxfmt --check .`, plus `verify:check-types`, `verify:complexity`, `verify:comment-block`) so the same command runs safely in CI (both workflows call `npm run verify` via the shared workflow's `lint-script`). To auto-fix lint/format locally, run `npm run fix` (`lint:fix` + `format`), then `npm run verify`. +`npm run verify` runs `node src/cli.ts` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`node src/cli.ts lint|format|check-types|complexity|comment-block`). It runs them in parallel and suppresses output unless one fails. + +**Fix locally, check in CI.** Run locally, `verify` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verify --check` / `verify --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) `verify:comment-block` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. diff --git a/README.md b/README.md index fe6c21b..450b503 100644 --- a/README.md +++ b/README.md @@ -32,23 +32,28 @@ Running `verify` with no subcommand follows a convention: - **No `verify:*` scripts in `package.json`** → it runs the **built-in default checks** in their default modes. Each check degrades gracefully — it passes or skips when it does not apply (no files, no diff, tool not installed, no rules configured). - **`verify:*` scripts present** → it runs **those** in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code). Add `--verbose` to stream everything. -You take control by adding `verify:*` scripts. Each can call a built-in or run your own command: +You take control by adding `verify:*` scripts. Prefer calling the built-ins (`verify `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc { "scripts": { "verify": "verify", "verify:complexity": "verify complexity --threshold 50 \"src/**/*.ts\"", - "verify:knip": "knip --no-progress --treat-config-hints-as-errors", - "verify:types": "tsc --noEmit", + "verify:lint": "verify lint", + "verify:custom": "node ./scripts/my-check.mjs", }, } ``` Run a single built-in directly: `verify complexity`, `verify knip`, … and `verify list` shows them all. +### Fix locally, check in CI + +Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verify` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Override explicitly with `verify --fix` or `verify --check`. + Flags on the bare `verify` command: +- `--check` / `--fix` — force check-only or auto-fix (defaults: fix locally, check under CI). - `--measure` — print a status/duration summary table. - `--all` — run every `verify:*` script, ignoring diff-based filters. - `--verbose` — stream all output instead of suppressing passing runs. @@ -62,12 +67,14 @@ Flags on the bare `verify` command: | `block-comments` | native | Any comment added to a line changed against `HEAD`. Machine directives (`eslint-disable`, `@ts-expect-error`, …) and `context:` are exempt. | | `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | | `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `lint` | external | Lint — auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | +| `format` | external | Formatting — writes locally, checks in CI ([oxfmt](https://oxc.rs)). | +| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | | `knip` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | | `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | | `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | -| `lint` | external | Lint + autofix ([oxlint](https://oxc.rs)). | -External checks shell out to their tool and **skip gracefully when it is not installed** — `verify init` installs the ones you opt into. They are declared as optional `peerDependencies`. +External checks shell out to their tool and **skip gracefully when it is not installed** — `verify init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verify` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. ### `complexity` @@ -118,7 +125,7 @@ It lets you multi-select **checks** and **agent targets** (Claude `.claude/`, an Options: - `--defaults-only` — do **not** write `verify:*` scripts; rely on `verify`'s built-in defaults (still installs opted-in tools and writes agent files). -- `--yes` — non-interactive; use `--check ` (repeatable), `--no-claude`, `--agents`. +- `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. ### `verify upgrade-docs` diff --git a/package.json b/package.json index 918298a..baba234 100644 --- a/package.json +++ b/package.json @@ -53,18 +53,17 @@ "lint:fix": "oxlint --fix", "format": "oxfmt .", "format:check": "oxfmt --check .", - "fix": "run-s lint:fix format", "verify": "node src/cli.ts", - "verify:lint": "oxlint .", - "verify:format": "oxfmt --check .", - "verify:check-types": "tsc --noEmit", + "verify:lint": "node src/cli.ts lint", + "verify:format": "node src/cli.ts format", + "verify:check-types": "node src/cli.ts check-types", "verify:complexity": "node src/cli.ts complexity --threshold 50 \"src/**/*.ts\"", "verify:comment-block": "node src/cli.ts comment-block --max-lines 1 --pushback \"src/**/*.ts\"", "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml", "audit": "better-npm-audit audit", - "prepublishOnly": "run-s build" + "prepublishOnly": "npm run build" }, "dependencies": { "commander": "^15.0.0", diff --git a/src/checks/external.ts b/src/checks/external.ts index 7151ca2..98034c6 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -2,6 +2,7 @@ import fs from 'node:fs' import path from 'node:path' import { color } from '../shared/color.ts' +import { resolveMode } from '../shared/mode.ts' import { runCommand } from '../shared/spawn.ts' import type { Check, CheckResult } from './types.ts' @@ -13,31 +14,51 @@ export function hasLocalBin(bin: string, cwd: string = process.cwd()): boolean { return BIN_EXTENSIONS.some((ext) => fs.existsSync(path.join(dir, bin + ext))) } +/** + * Put the project's node_modules/.bin on PATH so tools resolve however `verify` was invoked + * (npm script, npx, or directly) — npm only augments PATH when it runs a script itself. + */ +function envWithLocalBin(cwd: string = process.cwd()): Record { + const binDir = path.join(cwd, 'node_modules', '.bin') + const pathKey = Object.keys(process.env).find((k) => k.toLowerCase() === 'path') ?? 'PATH' + return { [pathKey]: `${binDir}${path.delimiter}${process.env[pathKey] ?? ''}` } +} + export type ExternalCheckSpec = { name: string description: string /** The node_modules/.bin executable that must be present for the check to run. */ bin: string - /** The shell command run for the check (and written as the `verify:` script by init). */ - command: string + /** Command run in check mode (report + fail, never rewrite). */ + checkCommand: string + /** Command run in fix mode. When omitted, the check is not fixable and always runs `checkCommand`. */ + fixCommand?: string devDeps: string[] inDefaultRun?: boolean + /** Extra guard beyond bin presence (e.g. require a tsconfig). */ + canRun?: () => boolean } -/** Build a Check that shells out to an external tool, skipping gracefully when the tool is not installed. */ +/** Build a Check that shells out to an external tool, skipping gracefully when the tool cannot run. */ export function defineExternalCheck(spec: ExternalCheckSpec): Check { return { name: spec.name, description: spec.description, kind: 'external', inDefaultRun: spec.inDefaultRun ?? true, - scaffold: { script: spec.command, devDeps: spec.devDeps }, + // Scaffold as a call into this CLI so fix-vs-check lives in one place, not the consumer's script. + scaffold: { script: `verify ${spec.name}`, devDeps: spec.devDeps }, async runDefault(): Promise { if (!hasLocalBin(spec.bin)) { console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`verify init\`)`)) return { name: spec.name, ok: true, skipped: true } } - const code = await runCommand(spec.command) + if (spec.canRun && !spec.canRun()) { + console.log(color.dim(`${spec.name}: not applicable here — skipping`)) + return { name: spec.name, ok: true, skipped: true } + } + const command = resolveMode() === 'fix' && spec.fixCommand ? spec.fixCommand : spec.checkCommand + const code = await runCommand(command, { env: envWithLocalBin() }) return { name: spec.name, ok: code === 0 } }, } diff --git a/src/checks/registry.test.ts b/src/checks/registry.test.ts index 02702fe..3809ab8 100644 --- a/src/checks/registry.test.ts +++ b/src/checks/registry.test.ts @@ -12,18 +12,21 @@ describe('check registry', () => { 'block-comments', 'hardcoded-colors', 'forbidden-strings', + 'lint', + 'format', + 'check-types', 'knip', 'circular-deps', 'duplicate-code', - 'lint', ]), ) }) - it('marks external tool checks with scaffold devDeps', () => { + it('marks external tool checks with scaffold devDeps and a verify-CLI script', () => { const knip = getCheck('knip') expect(knip?.kind).toBe('external') expect(knip?.scaffold.devDeps).toContain('knip') + expect(knip?.scaffold.script).toBe('verify knip') }) it('scaffolds native checks back into the verify CLI', () => { diff --git a/src/checks/registry.ts b/src/checks/registry.ts index f2769f1..d6fef2e 100644 --- a/src/checks/registry.ts +++ b/src/checks/registry.ts @@ -1,3 +1,5 @@ +import fs from 'node:fs' + import { runBlockComments } from './block-comments.ts' import { runCommentBlock } from './comment-block.ts' import { runComplexity } from './complexity.ts' @@ -25,34 +27,51 @@ export const CHECKS: Check[] = [ nativeCheck('block-comments', 'Fail on any comment added to a line changed against HEAD', true, () => runBlockComments()), nativeCheck('hardcoded-colors', 'Fail on literal hex / 0x colour values in source', true, () => runHardcodedColors()), nativeCheck('forbidden-strings', 'Fail on disallowed JSON config values (rules from verify config)', true, () => runForbiddenStrings()), + defineExternalCheck({ + name: 'lint', + description: 'Lint, auto-fixing locally and checking in CI (oxlint)', + bin: 'oxlint', + checkCommand: 'oxlint .', + fixCommand: 'oxlint --fix .', + devDeps: ['oxlint'], + }), + defineExternalCheck({ + name: 'format', + description: 'Formatting, writing locally and checking in CI (oxfmt)', + bin: 'oxfmt', + checkCommand: 'oxfmt --check .', + fixCommand: 'oxfmt .', + devDeps: ['oxfmt'], + }), + defineExternalCheck({ + name: 'check-types', + description: 'TypeScript type check (tsc --noEmit)', + bin: 'tsc', + checkCommand: 'tsc --noEmit', + devDeps: ['typescript'], + canRun: () => fs.existsSync('tsconfig.json'), + }), defineExternalCheck({ name: 'knip', description: 'Unused files, exports and dependencies', bin: 'knip', - command: 'knip --no-progress --treat-config-hints-as-errors', + checkCommand: 'knip --no-progress --treat-config-hints-as-errors', devDeps: ['knip'], }), defineExternalCheck({ name: 'circular-deps', description: 'Circular dependency detection (skott)', bin: 'skott', - command: 'skott --displayMode=raw --showCircularDependencies --exitCodeOnCircularDependencies=1', + checkCommand: 'skott --displayMode=raw --showCircularDependencies --exitCodeOnCircularDependencies=1', devDeps: ['skott'], }), defineExternalCheck({ name: 'duplicate-code', description: 'Copy-paste detection (jscpd)', bin: 'jscpd', - command: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src', + checkCommand: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src', devDeps: ['jscpd'], }), - defineExternalCheck({ - name: 'lint', - description: 'Lint + autofix (oxlint)', - bin: 'oxlint', - command: 'oxlint --fix .', - devDeps: ['oxlint'], - }), ] export function getCheck(name: string): Check | undefined { diff --git a/src/checks/types.ts b/src/checks/types.ts index e64163d..38effdb 100644 --- a/src/checks/types.ts +++ b/src/checks/types.ts @@ -1,5 +1,8 @@ export type CheckKind = 'native' | 'external' +/** Fixable checks auto-fix in `fix` mode and only report (failing on issues) in `check` mode. */ +export type CheckMode = 'fix' | 'check' + export type CheckResult = { name: string ok: boolean diff --git a/src/cli.ts b/src/cli.ts index e71b0e5..1cc5424 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,7 +21,9 @@ program .option('--all', 'run all verify:* scripts, ignoring diff-based filters') .option('--measure', "print a summary table of each verification's status and duration") .option('--verbose', 'stream all output instead of suppressing passing runs') - .action(async (opts: { all?: boolean; measure?: boolean; verbose?: boolean }) => { + .option('--check', 'check only — never auto-fix (the default under CI)') + .option('--fix', 'auto-fix where possible (the default locally)') + .action(async (opts: { all?: boolean; measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { process.exitCode = await orchestrate(opts) }) diff --git a/src/commands/registerChecks.ts b/src/commands/registerChecks.ts index d80ed16..02c9133 100644 --- a/src/commands/registerChecks.ts +++ b/src/commands/registerChecks.ts @@ -63,6 +63,7 @@ export function registerChecks(program: Command): void { finish(runForbiddenStrings().ok) }) + // Mode flows via the VERIFY_MODE env / CI, not per-subcommand flags (which collide with the root's --check). for (const check of CHECKS.filter((c) => c.kind === 'external')) { program .command(check.name) diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts index f5f8430..5b7271f 100644 --- a/src/commands/registerInit.ts +++ b/src/commands/registerInit.ts @@ -21,7 +21,7 @@ async function multiselect(message: string, choices: Choice[]): Promise 0 ? opts.check : defaultChecks().map((c) => c.name), targets } + return { checks: opts.select.length > 0 ? opts.select : defaultChecks().map((c) => c.name), targets } } const checks = await multiselect( @@ -65,7 +65,7 @@ export function registerInit(program: Command): void { .description('Scaffold verifications and agent commands into this project') .option('--defaults-only', 'do not write verify:* scripts; rely on `verify` built-in defaults') .option('--yes', 'non-interactive: use flag selections (or defaults) without prompting') - .option('--check ', 'preselect a check (repeatable, non-interactive)', collect, []) + .option('--select ', 'preselect a check by name (repeatable, non-interactive)', collect, []) .option('--no-claude', 'do not write .claude/ files (non-interactive)') .option('--agents', 'also write .agent-skills/ files (non-interactive)') .action(async (opts: InitCliOptions) => { diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index 29d2505..e65c588 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -1,4 +1,5 @@ import { color } from '../shared/color.ts' +import { configureMode } from '../shared/mode.ts' import { runCommand, setVerbose } from '../shared/spawn.ts' import { filterByChangedFiles } from './filterByChangedFiles.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' @@ -9,6 +10,8 @@ export type OrchestrateOptions = { all?: boolean measure?: boolean verbose?: boolean + check?: boolean + fix?: boolean } async function runEntry(entry: VerifyEntry): Promise { @@ -34,6 +37,8 @@ function reportScriptResults(records: readonly MeasureRecord[], total: number): */ export async function orchestrate(opts: OrchestrateOptions = {}): Promise { setVerbose(!!opts.verbose) + // Propagate an explicit --check/--fix via VERIFY_MODE so it reaches spawned verify:* scripts too. + configureMode(opts) const allEntries = resolveEntries() if (allEntries.length === 0) return runDefaults({ measure: opts.measure }) diff --git a/src/shared/mode.ts b/src/shared/mode.ts new file mode 100644 index 0000000..92d79ea --- /dev/null +++ b/src/shared/mode.ts @@ -0,0 +1,19 @@ +import type { CheckMode } from '../checks/types.ts' + +/** + * Resolve the run mode. The AI/human running `verify` locally gets `fix` (auto-fix, no token churn); + * CI gets `check` (fail on issues, never rewrite). An explicit `--check`/`--fix` sets `VERIFY_MODE`, + * which wins; otherwise a truthy `CI` env means check. + */ +export function resolveMode(): CheckMode { + const explicit = process.env.VERIFY_MODE + if (explicit === 'check' || explicit === 'fix') return explicit + if (process.env.CI) return 'check' + return 'fix' +} + +/** Apply a `--check`/`--fix` override so it propagates to in-process checks and spawned child scripts. */ +export function configureMode(opts?: { check?: boolean; fix?: boolean }): void { + if (opts?.check) process.env.VERIFY_MODE = 'check' + else if (opts?.fix) process.env.VERIFY_MODE = 'fix' +} From c2a2911a4e2dc484fdf466719576ce73bfe4475a Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 23:26:47 +0800 Subject: [PATCH 06/34] test: cover fix-vs-check mode resolution and command selection Add unit tests for the run-mode logic that previously only had manual verification: - resolveMode: fix locally by default, check under CI, VERIFY_MODE overrides CI (both directions), unrecognised VERIFY_MODE ignored. - configureMode: --check/--fix set VERIFY_MODE (and propagate), no flag leaves the env untouched. - selectCommand: fixable checks pick the fix command only in fix mode; non-fixable checks always run the check command. Extract selectCommand as a pure helper in external.ts to make the fix-vs-check choice directly testable. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/checks/external.test.ts | 21 ++++++++++++ src/checks/external.ts | 9 ++++-- src/shared/mode.test.ts | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/checks/external.test.ts create mode 100644 src/shared/mode.test.ts diff --git a/src/checks/external.test.ts b/src/checks/external.test.ts new file mode 100644 index 0000000..74a2638 --- /dev/null +++ b/src/checks/external.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +import { selectCommand } from './external.ts' + +const fixable = { checkCommand: 'oxfmt --check .', fixCommand: 'oxfmt .' } +const notFixable = { checkCommand: 'tsc --noEmit' } + +describe('selectCommand', () => { + it('uses the fix command in fix mode when the check is fixable', () => { + expect(selectCommand(fixable, 'fix')).toBe('oxfmt .') + }) + + it('uses the check command in check mode', () => { + expect(selectCommand(fixable, 'check')).toBe('oxfmt --check .') + }) + + it('always uses the check command when the check has no fix command', () => { + expect(selectCommand(notFixable, 'fix')).toBe('tsc --noEmit') + expect(selectCommand(notFixable, 'check')).toBe('tsc --noEmit') + }) +}) diff --git a/src/checks/external.ts b/src/checks/external.ts index 98034c6..76db84a 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { color } from '../shared/color.ts' import { resolveMode } from '../shared/mode.ts' import { runCommand } from '../shared/spawn.ts' -import type { Check, CheckResult } from './types.ts' +import type { Check, CheckMode, CheckResult } from './types.ts' const BIN_EXTENSIONS = ['', '.cmd', '.ps1', '.exe'] @@ -39,6 +39,11 @@ export type ExternalCheckSpec = { canRun?: () => boolean } +/** Pick the command for the run mode: the fix command only in fix mode and only when the check is fixable. */ +export function selectCommand(spec: Pick, mode: CheckMode): string { + return mode === 'fix' && spec.fixCommand ? spec.fixCommand : spec.checkCommand +} + /** Build a Check that shells out to an external tool, skipping gracefully when the tool cannot run. */ export function defineExternalCheck(spec: ExternalCheckSpec): Check { return { @@ -57,7 +62,7 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { console.log(color.dim(`${spec.name}: not applicable here — skipping`)) return { name: spec.name, ok: true, skipped: true } } - const command = resolveMode() === 'fix' && spec.fixCommand ? spec.fixCommand : spec.checkCommand + const command = selectCommand(spec, resolveMode()) const code = await runCommand(command, { env: envWithLocalBin() }) return { name: spec.name, ok: code === 0 } }, diff --git a/src/shared/mode.test.ts b/src/shared/mode.test.ts new file mode 100644 index 0000000..590f28f --- /dev/null +++ b/src/shared/mode.test.ts @@ -0,0 +1,64 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { configureMode, resolveMode } from './mode.ts' + +const saved: { ci?: string; mode?: string } = {} + +beforeEach(() => { + saved.ci = process.env.CI + saved.mode = process.env.VERIFY_MODE + delete process.env.CI + delete process.env.VERIFY_MODE +}) +afterEach(() => { + if (saved.ci === undefined) delete process.env.CI + else process.env.CI = saved.ci + if (saved.mode === undefined) delete process.env.VERIFY_MODE + else process.env.VERIFY_MODE = saved.mode +}) + +describe('resolveMode', () => { + it('defaults to fix locally (no CI, no override)', () => { + expect(resolveMode()).toBe('fix') + }) + + it('is check under CI', () => { + process.env.CI = 'true' + expect(resolveMode()).toBe('check') + }) + + it('lets VERIFY_MODE override CI (fix in CI)', () => { + process.env.CI = 'true' + process.env.VERIFY_MODE = 'fix' + expect(resolveMode()).toBe('fix') + }) + + it('honours VERIFY_MODE=check with no CI', () => { + process.env.VERIFY_MODE = 'check' + expect(resolveMode()).toBe('check') + }) + + it('ignores an unrecognised VERIFY_MODE value', () => { + process.env.VERIFY_MODE = 'nonsense' + expect(resolveMode()).toBe('fix') + }) +}) + +describe('configureMode', () => { + it('sets VERIFY_MODE=check for --check so it propagates to child scripts', () => { + configureMode({ check: true }) + expect(process.env.VERIFY_MODE).toBe('check') + expect(resolveMode()).toBe('check') + }) + + it('sets VERIFY_MODE=fix for --fix, overriding CI', () => { + process.env.CI = 'true' + configureMode({ fix: true }) + expect(resolveMode()).toBe('fix') + }) + + it('leaves the environment untouched when neither flag is given', () => { + configureMode({}) + expect(process.env.VERIFY_MODE).toBeUndefined() + }) +}) From 10c4b7a4891c2ea91b03136ef0bd568ecefe7d84 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Wed, 8 Jul 2026 23:41:29 +0800 Subject: [PATCH 07/34] docs: slimming down superfluous stuff in claude.md --- CLAUDE.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 21cc3ed..9db6132 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # @makerx/verify -A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verify` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verify init` / `verify upgrade-docs`) that drops the checks and agent commands into a project. Node >= 24; run the CLI from source with `node src/cli.ts`. +A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verify` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verify init` / `verify upgrade-docs`) that drops the checks and agent commands into a project. ## After making changes, run `npm run verify` @@ -9,20 +9,3 @@ A growing collection of code **verifications** that give AI coding agents back-p **Fix locally, check in CI.** Run locally, `verify` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verify --check` / `verify --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) `verify:comment-block` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. - -## Architecture - -- `src/cli.ts` — commander root program. Default action (no subcommand) = `orchestrate()`. Each built-in is a subcommand; plus `init`, `upgrade-docs`, `list`. -- `src/checks/` — one module per check + `registry.ts` (the `CHECKS` array). Native checks run in-process; external checks (`external.ts`) shell out and skip when the tool is absent. -- `src/orchestrator/` — the convention: `resolveEntries` finds `verify:*` scripts; if present, `run.ts` runs them in parallel; if absent, `runDefaults.ts` runs the built-in default set in-process. -- `src/scaffold/` — `init.ts` (pure), `agentFiles.ts` + `writeManaged.ts` (idempotent agent-file emission), `packageScripts.ts`. -- `src/shared/` — `color`, `git`, `diff`, `comment-scan` (TS compiler API — no ts-morph), `config`, `spawn`. -- `templates/` — shipped agent files (`.claude` command + skill), read at runtime relative to the module; listed in package.json `files`. - -## Conventions - -- Comments explain _why_, not _what_. No comment block longer than 2 lines unless it is JSDoc (`/**`) or prefixed `context:`. -- Keep functions small so files stay above the MI threshold; the fix for a failing file is to split it, never to game the metric. -- `console` is only allowed in the CLI's reporting surface (`src/cli.ts`, `src/report.ts`, `src/commands/**`, `src/orchestrator/**`, `src/checks/**`) — see `.oxlintrc.json`. -- Prefer the TypeScript compiler API and existing `src/shared` helpers over new dependencies. -- Tests are co-located `*.test.ts` (vitest). From 28ca3ee46caab20a554fdb6ff725ec1a3d1e1278 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 01:23:33 +0800 Subject: [PATCH 08/34] fix: rename CLI binary to verifyx and deny the native watcher install script `verify` is a Windows cmd builtin, so `npx verify` and `npm run` script bodies that call bare `verify` run the builtin instead of this tool. Rename the executable to `verifyx` (verify + fix) so every invocation works cross-platform. The package stays `@makerx/verify` and the npm script stays named `verify` (a script lookup, not command resolution), so `npm run verify` works everywhere. - bin, commander program name, scaffold script bodies, init/skip messages, agent templates, tests, README (with a "why verifyx" callout) and CLAUDE.md all updated to `verifyx`. - Add a dev-only `prepare` shim (scripts/dev-verifyx-bin.mjs) that links node_modules/.bin/verifyx -> node src/cli.ts, so the repo's own verify:* scripts read `verifyx ` exactly like a consumer's. `prepare` never runs for registry consumers. - Deny @parcel/watcher's native install script via npm's allowScripts (skott's watch dep; the prebuilt binary is used, so no node-gyp build runs). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 8 +- README.md | 51 +- package-lock.json | 3852 ++++++++++++++++++++++----- package.json | 29 +- scripts/dev-verifyx-bin.mjs | 24 + src/checks/external.ts | 4 +- src/checks/registry.test.ts | 4 +- src/checks/registry.ts | 2 +- src/cli.ts | 2 +- src/commands/registerChecks.ts | 2 +- src/commands/registerInit.ts | 8 +- src/commands/registerList.ts | 2 +- src/commands/registerUpgradeDocs.ts | 2 +- src/scaffold/init.test.ts | 4 +- src/scaffold/packageScripts.ts | 2 +- templates/commands/verify.md | 10 +- templates/skills/verify/SKILL.md | 17 +- 17 files changed, 3357 insertions(+), 666 deletions(-) create mode 100644 scripts/dev-verifyx-bin.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 9db6132..79a6cd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,13 @@ # @makerx/verify -A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verify` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verify init` / `verify upgrade-docs`) that drops the checks and agent commands into a project. +A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verifyx` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verifyx init` / `verifyx upgrade-docs`) that drops the checks and agent commands into a project. + +**The CLI binary is `verifyx`, not `verify`.** `verify` is a Windows `cmd` builtin, and `npm run` bodies + `npx` resolve through `cmd` there, so a bare `verify` runs the builtin. `verifyx` (verify + fix) avoids that on all platforms. The npm **script** stays named `verify` (`npm run verify` is a script lookup, not command resolution) and the package stays `@makerx/verify`. Locally, `prepare` (`scripts/dev-verifyx-bin.mjs`) writes a `node_modules/.bin/verifyx` shim → `node src/cli.ts`, so the repo's own `verify:*` scripts call `verifyx ` exactly like a consumer's; `prepare` never runs for registry consumers. ## After making changes, run `npm run verify` -`npm run verify` runs `node src/cli.ts` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`node src/cli.ts lint|format|check-types|complexity|comment-block`). It runs them in parallel and suppresses output unless one fails. +`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comment-block|…`). It runs them in parallel and suppresses output unless one fails. -**Fix locally, check in CI.** Run locally, `verify` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verify --check` / `verify --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) +**Fix locally, check in CI.** Run locally, `verifyx` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verifyx --check` / `verifyx --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) `verify:comment-block` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. diff --git a/README.md b/README.md index 450b503..b615bad 100644 --- a/README.md +++ b/README.md @@ -17,41 +17,50 @@ Install as a **pinned dev dependency** — never globally. A locked version mean npm install --save-dev @makerx/verify ``` -Requires Node.js >= 24. Invoke it via an npm script or `npx verify` — not a global binary on `PATH`. +Requires Node.js >= 24. Invoke it via an npm script or `npx verifyx` — not a global binary on `PATH`. The quickest way to wire it into a project: ```sh -npx verify init +npx verifyx init ``` -## How `verify` decides what to run +> ### Why is the command `verifyx`, not `verify`? +> +> The package is `@makerx/verify`, but the CLI binary is **`verifyx`**. `verify` is a built-in `cmd.exe` +> command on Windows, and both `npm run` script bodies and `npx` resolve commands through `cmd` there — so a +> bare `verify` runs the Windows builtin ("VERIFY is off."), not this tool. Renaming the binary to `verifyx` +> (a nod to the fact it **fixes** as well as verifies) makes every invocation — `npx verifyx`, npm scripts, +> and a typed `verifyx` — work identically on macOS, Linux, and Windows. Your npm **script** can still be named +> `verify` (that's a script lookup, not command resolution), so `npm run verify` works everywhere. -Running `verify` with no subcommand follows a convention: +## How `verifyx` decides what to run + +Running `verifyx` with no subcommand follows a convention: - **No `verify:*` scripts in `package.json`** → it runs the **built-in default checks** in their default modes. Each check degrades gracefully — it passes or skips when it does not apply (no files, no diff, tool not installed, no rules configured). - **`verify:*` scripts present** → it runs **those** in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code). Add `--verbose` to stream everything. -You take control by adding `verify:*` scripts. Prefer calling the built-ins (`verify `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: +You take control by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc { "scripts": { - "verify": "verify", - "verify:complexity": "verify complexity --threshold 50 \"src/**/*.ts\"", - "verify:lint": "verify lint", + "verify": "verifyx", + "verify:complexity": "verifyx complexity --threshold 50 \"src/**/*.ts\"", + "verify:lint": "verifyx lint", "verify:custom": "node ./scripts/my-check.mjs", }, } ``` -Run a single built-in directly: `verify complexity`, `verify knip`, … and `verify list` shows them all. +Run a single built-in directly: `verifyx complexity`, `verifyx knip`, … and `verifyx list` shows them all. ### Fix locally, check in CI -Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verify` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Override explicitly with `verify --fix` or `verify --check`. +Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verifyx` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Override explicitly with `verifyx --fix` or `verifyx --check`. -Flags on the bare `verify` command: +Flags on the bare `verifyx` command: - `--check` / `--fix` — force check-only or auto-fix (defaults: fix locally, check under CI). - `--measure` — print a status/duration summary table. @@ -74,12 +83,12 @@ Flags on the bare `verify` command: | `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | | `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | -External checks shell out to their tool and **skip gracefully when it is not installed** — `verify init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verify` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. +External checks shell out to their tool and **skip gracefully when it is not installed** — `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. ### `complexity` ```sh -verify complexity --threshold 50 "src/**/*.ts" +verifyx complexity --threshold 50 "src/**/*.ts" ``` - `[pattern]` — glob, directory, or file. Defaults to `{src,server,shared}/**/*.ts`. @@ -91,7 +100,7 @@ A file's score is the **minimum MI across its functions**. When exactly one file ### `comment-block` ```sh -verify comment-block --max-lines 2 --pushback "src/**/*.ts" +verifyx comment-block --max-lines 2 --pushback "src/**/*.ts" ``` - `--max-lines ` — fail on comment blocks longer than `n` lines (default 2). @@ -108,12 +117,12 @@ const timeoutMs = timeout * 1000 ## Scaffolding a project -### `verify init` +### `verifyx init` Interactively wire verifications and agent files into the current project: ```sh -verify init +verifyx init ``` It lets you multi-select **checks** and **agent targets** (Claude `.claude/`, and/or cross-vendor `.agent-skills/`), then: @@ -127,13 +136,13 @@ Options: - `--defaults-only` — do **not** write `verify:*` scripts; rely on `verify`'s built-in defaults (still installs opted-in tools and writes agent files). - `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. -### `verify upgrade-docs` +### `verifyx upgrade-docs` Idempotently create/refresh the managed agent files (created / updated / unchanged; refuses to write through symlinks): ```sh -verify upgrade-docs # both targets -verify upgrade-docs --no-agents # only .claude/ +verifyx upgrade-docs # both targets +verifyx upgrade-docs --no-agents # only .claude/ ``` ## Configuration @@ -151,7 +160,7 @@ Some checks read per-repo config from `verify.config.json`, or a `verify` key in } ``` -`filters` scopes a `verify:*` script to a diff glob: it is skipped unless a changed file matches (bypass with `verify --all`). +`filters` scopes a `verify:*` script to a diff glob: it is skipped unless a changed file matches (bypass with `verifyx --all`). ## CI/CD @@ -159,7 +168,7 @@ Because it is a pinned dev dependency, CI runs the identical tool: ```yaml - run: npm ci -- run: npx verify --measure +- run: npm run verify ``` ## Programmatic API diff --git a/package-lock.json b/package-lock.json index d09305e..f148a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,14 @@ "@types/node": "^26.1.1", "@vitest/coverage-v8": "^4.1.10", "better-npm-audit": "^3.11.0", + "jscpd": "^5.0.11", + "knip": "^6.25.0", "npm-run-all2": "^9.0.2", "oxfmt": "^0.58.0", - "oxlint": "^1.69.0", + "oxlint": "^1.73.0", "rimraf": "^6.1.3", "rollup": "^4.62.2", + "skott": "^0.35.11", "tslib": "^2.8.1", "vitest": "^4.1.10" }, @@ -34,15 +37,11 @@ "node": ">=24" }, "peerDependencies": { - "@jscpd/finder": "^4.0.1", - "jscpd": "^4.0.5", - "knip": "^5.64.2", - "skott": "^0.35.4" + "jscpd": "^5.0.11", + "knip": "^6.25.0", + "skott": "^0.35.11" }, "peerDependenciesMeta": { - "@jscpd/finder": { - "optional": true - }, "jscpd": { "optional": true }, @@ -54,10 +53,57 @@ } } }, + "node_modules/@arr/every": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -66,8 +112,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -76,8 +120,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { @@ -90,10 +132,38 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { @@ -106,14 +176,35 @@ }, "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/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -125,10 +216,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "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": { @@ -137,15 +235,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "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": { @@ -172,20 +266,10 @@ "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.58.0.tgz", - "integrity": "sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==", + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz", + "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==", "cpu": [ "arm" ], @@ -199,10 +283,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.58.0.tgz", - "integrity": "sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==", + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz", + "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==", "cpu": [ "arm64" ], @@ -216,10 +300,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.58.0.tgz", - "integrity": "sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==", + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz", + "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==", "cpu": [ "arm64" ], @@ -233,10 +317,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.58.0.tgz", - "integrity": "sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==", + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz", + "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==", "cpu": [ "x64" ], @@ -250,10 +334,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.58.0.tgz", - "integrity": "sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==", + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz", + "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==", "cpu": [ "x64" ], @@ -267,10 +351,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.58.0.tgz", - "integrity": "sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==", + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz", + "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==", "cpu": [ "arm" ], @@ -284,10 +368,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.58.0.tgz", - "integrity": "sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==", + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz", + "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==", "cpu": [ "arm" ], @@ -301,14 +385,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.58.0.tgz", - "integrity": "sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==", + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz", + "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -318,14 +405,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.58.0.tgz", - "integrity": "sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==", + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz", + "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -335,14 +425,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.58.0.tgz", - "integrity": "sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==", + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz", + "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -352,14 +445,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.58.0.tgz", - "integrity": "sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==", + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz", + "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -369,14 +465,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.58.0.tgz", - "integrity": "sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==", + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz", + "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -386,14 +485,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.58.0.tgz", - "integrity": "sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==", + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz", + "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -403,14 +505,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.58.0.tgz", - "integrity": "sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==", + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz", + "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -420,14 +525,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.58.0.tgz", - "integrity": "sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==", + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz", + "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -437,10 +545,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.58.0.tgz", - "integrity": "sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==", + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz", + "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==", "cpu": [ "arm64" ], @@ -454,10 +562,29 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.58.0.tgz", - "integrity": "sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==", + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz", + "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", + "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==", "cpu": [ "arm64" ], @@ -471,10 +598,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.58.0.tgz", - "integrity": "sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==", + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz", + "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==", "cpu": [ "ia32" ], @@ -488,10 +615,8 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.58.0.tgz", - "integrity": "sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==", + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.137.0", "cpu": [ "x64" ], @@ -505,10 +630,18 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz", - "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==", + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", "cpu": [ "arm" ], @@ -517,15 +650,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz", - "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==", + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", "cpu": [ "arm64" ], @@ -534,15 +664,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz", - "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==", + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", "cpu": [ "arm64" ], @@ -551,15 +678,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz", - "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==", + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", "cpu": [ "x64" ], @@ -568,15 +692,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz", - "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==", + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", "cpu": [ "x64" ], @@ -585,15 +706,12 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz", - "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==", + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", "cpu": [ "arm" ], @@ -602,15 +720,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz", - "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==", + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", "cpu": [ "arm" ], @@ -619,151 +734,148 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz", - "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==", - "cpu": [ - "arm64" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz", - "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==", + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz", - "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==", + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz", - "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==", + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz", - "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==", + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz", - "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==", + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz", - "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==", + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz", - "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==", + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz", - "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==", + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", "cpu": [ "arm64" ], @@ -772,15 +884,54 @@ "optional": true, "os": [ "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14.0.0" } }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz", - "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", "cpu": [ "arm64" ], @@ -789,49 +940,41 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz", - "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==", + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz", - "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==", + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.58.0.tgz", + "integrity": "sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.58.0.tgz", + "integrity": "sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==", "cpu": [ "arm64" ], @@ -845,10 +988,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.58.0.tgz", + "integrity": "sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==", "cpu": [ "arm64" ], @@ -862,10 +1005,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.58.0.tgz", + "integrity": "sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==", "cpu": [ "x64" ], @@ -879,10 +1022,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.58.0.tgz", + "integrity": "sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==", "cpu": [ "x64" ], @@ -896,10 +1039,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.58.0.tgz", + "integrity": "sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==", "cpu": [ "arm" ], @@ -913,12 +1056,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.58.0.tgz", + "integrity": "sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", @@ -930,14 +1073,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.58.0.tgz", + "integrity": "sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -947,14 +1093,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.58.0.tgz", + "integrity": "sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -964,14 +1113,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.58.0.tgz", + "integrity": "sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -981,14 +1133,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.58.0.tgz", + "integrity": "sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==", "cpu": [ - "x64" + "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -998,14 +1153,17 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.58.0.tgz", + "integrity": "sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==", "cpu": [ - "x64" + "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1015,63 +1173,1048 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", - "cpu": [ - "arm64" + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.58.0.tgz", + "integrity": "sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.58.0.tgz", + "integrity": "sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.58.0.tgz", + "integrity": "sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.58.0.tgz", + "integrity": "sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.58.0.tgz", + "integrity": "sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.58.0.tgz", + "integrity": "sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.58.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz", + "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz", + "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz", + "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz", + "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz", + "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz", + "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz", + "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz", + "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz", + "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz", + "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz", + "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz", + "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz", + "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz", + "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz", + "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz", + "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz", + "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz", + "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.73.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@polka/url": { + "version": "0.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { + "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ - "wasm32" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, + "os": [ + "linux" + ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { @@ -1093,8 +2236,6 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -1110,15 +2251,11 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-typescript": { "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", - "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", "dev": true, "license": "MIT", "dependencies": { @@ -1144,8 +2281,6 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1257,6 +2392,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1271,6 +2409,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1285,6 +2426,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1299,6 +2443,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1313,6 +2460,9 @@ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1327,6 +2477,9 @@ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1341,6 +2494,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1355,6 +2511,9 @@ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1369,6 +2528,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1383,6 +2545,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1397,6 +2562,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1411,6 +2579,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1425,6 +2596,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1489,8 +2663,6 @@ }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1503,8 +2675,6 @@ }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1517,8 +2687,6 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -1535,8 +2703,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -1546,36 +2712,125 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~8.3.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.10", @@ -1603,8 +2858,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { @@ -1621,8 +2874,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -1648,8 +2899,6 @@ }, "node_modules/@vitest/mocker/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -1658,8 +2907,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1671,8 +2918,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { @@ -1685,8 +2930,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { @@ -1701,8 +2944,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -1711,8 +2952,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { @@ -1724,10 +2963,59 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "dev": true, + "license": "MIT", + "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" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "dev": true, + "license": "MIT", + "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.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "dev": true, + "license": "MIT" + }, "node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -1743,8 +3031,6 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "license": "MIT", "engines": { "node": ">=6" @@ -1752,30 +3038,56 @@ }, "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==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -1784,8 +3096,6 @@ }, "node_modules/ast-v8-to-istanbul": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", - "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1796,8 +3106,6 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -1806,8 +3114,6 @@ }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "engines": { @@ -1816,8 +3122,6 @@ }, "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==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -1825,8 +3129,6 @@ }, "node_modules/better-npm-audit": { "version": "3.11.0", - "resolved": "https://registry.npmjs.org/better-npm-audit/-/better-npm-audit-3.11.0.tgz", - "integrity": "sha512-/Pt05DK6HQaRjWDc5McsCkJBZYfhgQGneKnxzPJExtRq38NttO1Hm30m0GVQeZogE94LVNBVrhWwVsoCo+at3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1845,8 +3147,6 @@ }, "node_modules/better-npm-audit/node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "license": "MIT", "engines": { @@ -1855,8 +3155,6 @@ }, "node_modules/brace-expansion": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1865,20 +3163,71 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1890,31 +3239,183 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { "node": ">=22.12.0" } }, + "node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cpd-darwin-arm64": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/cpd-darwin-arm64/-/cpd-darwin-arm64-5.0.11.tgz", + "integrity": "sha512-3QvH+4Dv7A7esVFM2tsRVWN3kn9EDu8dMYog6gYAVsCtxEf4xyxAwS/ef6LjC7/dh4+ATADFbg3H09A2fD//Qw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/cpd-darwin-x64": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/cpd-darwin-x64/-/cpd-darwin-x64-5.0.11.tgz", + "integrity": "sha512-OvgM2ps0OFR5jUzx7+FK9URdJGxUzzM5KKk2F1V3vf1LooGDKwkivfIDyKsqEwp37zcbyUo7COvBpJXOT0dZmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/cpd-linux-arm64-gnu": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/cpd-linux-arm64-gnu/-/cpd-linux-arm64-gnu-5.0.11.tgz", + "integrity": "sha512-pXMINibAeruglni8ZajlXEefZHDs7QFSG+vPtkBDu7uiIMpNU8aoktgO2vP+PbIRFD0vHkqTMb64kDtIOqQcwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/cpd-linux-x64-gnu": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/cpd-linux-x64-gnu/-/cpd-linux-x64-gnu-5.0.11.tgz", + "integrity": "sha512-rQ7DuF0lH1HLzjGxlE0aEP2ycfhXgZH/CLSeS7FXNJ38lRVp+iXkrlcrrY6mC4WW/NgbL+DkF7/0lv3tFvGmvg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/cpd-linux-x64-musl": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/cpd-linux-x64-musl/-/cpd-linux-x64-musl-5.0.11.tgz", + "integrity": "sha512-Yh+7Go5+fA++I5ssAZg7gUkDCT5CxnzCPvrspbwDrfnwaY6nNM5g1C6Vs0+GJhsspuAKwydJl4nf7jkxzMwRQw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/cpd-windows-x64-msvc": { + "version": "5.0.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1928,15 +3429,11 @@ }, "node_modules/cross-spawn/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -1951,32 +3448,136 @@ }, "node_modules/dayjs": { "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depcheck": { + "version": "1.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/deps-regex": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-file": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/digraph-js": { + "version": "2.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.uniqwith": "^4.5.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/effect": { + "version": "3.21.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "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/enquirer": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", @@ -1986,10 +3587,27 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "7.0.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -1998,39 +3616,92 @@ }, "node_modules/es-module-lexer": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", - "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, + "node_modules/fast-check": { + "version": "3.23.2", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { @@ -2044,10 +3715,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -2062,6 +3739,58 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/formatly": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, + "node_modules/fp-ts": { + "version": "2.16.11", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-tree-structure": { + "version": "0.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2079,18 +3808,33 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2105,10 +3849,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -2117,8 +3903,6 @@ }, "node_modules/hasown": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2128,17 +3912,111 @@ "node": ">= 0.4" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "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/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, + "node_modules/io-ts": { + "version": "2.2.22", + "dev": true, + "license": "MIT", + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", "dev": true, "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -2151,20 +4029,96 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2173,8 +4127,6 @@ }, "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": { @@ -2183,8 +4135,6 @@ }, "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": { @@ -2198,8 +4148,6 @@ }, "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": { @@ -2210,17 +4158,63 @@ "node": ">=8" } }, + "node_modules/jiti": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "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/js-yaml": { + "version": "3.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jscpd": { + "version": "5.0.11", + "dev": true, + "license": "MIT", + "bin": { + "jscpd": "run-jscpd.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "cpd-darwin-arm64": "5.0.11", + "cpd-darwin-x64": "5.0.11", + "cpd-linux-arm64-gnu": "5.0.11", + "cpd-linux-x64-gnu": "5.0.11", + "cpd-linux-x64-musl": "5.0.11", + "cpd-windows-x64-msvc": "5.0.11" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-parse-even-better-errors": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", - "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", "dev": true, "license": "MIT", "engines": { @@ -2229,15 +4223,67 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/knip": { + "version": "6.25.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "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.6.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.1", + "yaml": "^2.9.0", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -2377,6 +4423,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2398,6 +4447,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2419,6 +4471,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2440,6 +4495,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2476,8 +4534,6 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -2495,25 +4551,38 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, "node_modules/lodash.truncate": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqwith": { + "version": "4.5.0", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2522,8 +4591,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2532,8 +4599,6 @@ }, "node_modules/magicast": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", - "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { @@ -2544,8 +4609,6 @@ }, "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": { @@ -2558,19 +4621,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/matchit": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@arr/every": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/memorystream": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", "dev": true, "engines": { "node": ">= 0.10.0" } }, + "node_modules/meriyah": { + "version": "4.5.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -2584,18 +4693,70 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/multimatch": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.16", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nanoid": { "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -2611,10 +4772,31 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanospinner": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/npm-normalize-package-bin": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", - "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", "dev": true, "license": "ISC", "engines": { @@ -2623,8 +4805,6 @@ }, "node_modules/npm-run-all2": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.2.tgz", - "integrity": "sha512-+dd4SO2jAlLE06OzmJKzIe6QvvjXezcbmobnh8usR0a8BzQCABTdqTXqVPji0ICOhSQpIIrkGd7IzNl5iDaRSA==", "dev": true, "license": "MIT", "dependencies": { @@ -2650,8 +4830,6 @@ }, "node_modules/obug": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -2662,10 +4840,89 @@ "node": ">=12.20.0" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/oxc-parser": { + "version": "0.137.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.137.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "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" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.137.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "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" + } + }, "node_modules/oxfmt": { "version": "0.58.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.58.0.tgz", - "integrity": "sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==", "dev": true, "license": "MIT", "dependencies": { @@ -2716,8 +4973,6 @@ }, "node_modules/oxlint": { "version": "1.73.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz", - "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==", "dev": true, "license": "MIT", "bin": { @@ -2765,15 +5020,60 @@ }, "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", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-gitignore": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -2782,15 +5082,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2804,24 +5100,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -2833,8 +5131,6 @@ }, "node_modules/pidtree": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", - "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", "dev": true, "license": "MIT", "bin": { @@ -2844,10 +5140,25 @@ "node": ">=18" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/polka": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^0.5.0", + "trouter": "^2.0.1" + } + }, "node_modules/postcss": { "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -2870,37 +5181,81 @@ "source-map-js": "^1.2.1" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/read-package-json-fast": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "node_modules/read-package-json-fast": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", - "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", "dev": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^6.0.0", - "npm-normalize-package-bin": "^6.0.0" + "license": "MIT", + "engines": { + "node": ">=8.6" }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": ">=0.10.0" } }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-package-name": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { @@ -2919,10 +5274,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rimraf": { "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2941,8 +5322,6 @@ }, "node_modules/rolldown": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { @@ -2975,11 +5354,8 @@ }, "node_modules/rollup": { "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -3019,10 +5395,27 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -3032,10 +5425,13 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -3047,8 +5443,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -3057,8 +5451,6 @@ }, "node_modules/shell-quote": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -3070,15 +5462,126 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, + "node_modules/sirv": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sirv/node_modules/@polka/url": { + "version": "1.0.0-next.29", + "dev": true, + "license": "MIT" + }, + "node_modules/skott": { + "version": "0.35.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.4", + "@typescript-eslint/typescript-estree": "^8.53.0", + "commander": "^11.0.0", + "compression": "^1.7.4", + "depcheck": "^1.4.7", + "digraph-js": "^2.2.4", + "effect": "^3.20.0", + "estree-walker": "^3.0.3", + "fp-ts": "^2.5.0", + "fs-tree-structure": "^0.0.5", + "ignore-walk": "^6.0.3", + "io-ts": "^2.2.20", + "is-wsl": "^3.0.0", + "json5": "^2.2.3", + "kleur": "^4.1.5", + "lodash-es": "^4.17.21", + "meriyah": "^4.3.7", + "minimatch": "^9.0.3", + "nanospinner": "^1.2.2", + "parse-gitignore": "^2.0.0", + "polka": "^0.5.2", + "sirv": "^2.0.3", + "skott-webapp": "^2.3.0", + "typescript": "^5.9.3" + }, + "bin": { + "skott": "dist/bin/cli.js" + } + }, + "node_modules/skott-webapp": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "digraph-js": "^2.2.3" + } + }, + "node_modules/skott/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/skott/node_modules/brace-expansion": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/skott/node_modules/commander": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/skott/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/skott/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skott/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3095,8 +5598,6 @@ }, "node_modules/slice-ansi/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": { @@ -3109,34 +5610,42 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smol-toml": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, "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": { @@ -3150,8 +5659,6 @@ }, "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==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3160,10 +5667,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3175,8 +5691,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -3188,8 +5702,6 @@ }, "node_modules/table": { "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3205,15 +5717,11 @@ }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -3222,8 +5730,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3239,8 +5745,6 @@ }, "node_modules/tinypool": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, "license": "MIT", "engines": { @@ -3249,28 +5753,61 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/trouter": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "matchit": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/typescript": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3279,20 +5816,31 @@ "node": ">=14.17" } }, + "node_modules/unbash": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/undici-types": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3367,11 +5915,8 @@ }, "node_modules/vitest": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", @@ -3456,10 +6001,16 @@ } } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/which": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", - "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", "dev": true, "license": "ISC", "dependencies": { @@ -3474,8 +6025,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -3488,6 +6037,91 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "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/node_modules/ansi-styles": { + "version": "4.3.0", + "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/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index baba234..fd56c7d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "url": "git+https://github.com/MakerXStudio/verify.git" }, "bin": { - "verify": "./dist/cli.mjs" + "verifyx": "./dist/cli.mjs" }, "files": [ "dist", @@ -53,12 +53,19 @@ "lint:fix": "oxlint --fix", "format": "oxfmt .", "format:check": "oxfmt --check .", - "verify": "node src/cli.ts", - "verify:lint": "node src/cli.ts lint", - "verify:format": "node src/cli.ts format", - "verify:check-types": "node src/cli.ts check-types", - "verify:complexity": "node src/cli.ts complexity --threshold 50 \"src/**/*.ts\"", - "verify:comment-block": "node src/cli.ts comment-block --max-lines 1 --pushback \"src/**/*.ts\"", + "prepare": "node scripts/dev-verifyx-bin.mjs", + "verify": "verifyx", + "verify:lint": "verifyx lint", + "verify:format": "verifyx format", + "verify:check-types": "verifyx check-types", + "verify:complexity": "verifyx complexity --threshold 50 \"src/**/*.ts\"", + "verify:comment-block": "verifyx comment-block --max-lines 1 --pushback \"src/**/*.ts\"", + "verify:block-comments": "verifyx block-comments", + "verify:hardcoded-colors": "verifyx hardcoded-colors", + "verify:forbidden-strings": "verifyx forbidden-strings", + "verify:knip": "verifyx knip", + "verify:circular-deps": "verifyx circular-deps", + "verify:duplicate-code": "verifyx duplicate-code", "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml", @@ -76,11 +83,14 @@ "@types/node": "^26.1.1", "@vitest/coverage-v8": "^4.1.10", "better-npm-audit": "^3.11.0", + "jscpd": "^5.0.11", + "knip": "^6.25.0", "npm-run-all2": "^9.0.2", "oxfmt": "^0.58.0", - "oxlint": "^1.69.0", + "oxlint": "^1.73.0", "rimraf": "^6.1.3", "rollup": "^4.62.2", + "skott": "^0.35.11", "tslib": "^2.8.1", "vitest": "^4.1.10" }, @@ -102,5 +112,8 @@ }, "engines": { "node": ">=24" + }, + "allowScripts": { + "@parcel/watcher": false } } diff --git a/scripts/dev-verifyx-bin.mjs b/scripts/dev-verifyx-bin.mjs new file mode 100644 index 0000000..fbc3206 --- /dev/null +++ b/scripts/dev-verifyx-bin.mjs @@ -0,0 +1,24 @@ +// Dev-only: create a local `verifyx` bin that proxies `node src/cli.ts`, so this repo's package.json can use +// `verifyx ` exactly like a consumer's. Wired via `prepare`, so it runs on local install / before +// publish but never when @makerx/verify is installed as a registry dependency. +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const binDir = path.join(repoRoot, 'node_modules', '.bin') + +// Nothing to link into until dependencies are installed. +if (!fs.existsSync(path.join(repoRoot, 'node_modules'))) process.exit(0) + +const cliNative = path.join(repoRoot, 'src', 'cli.ts') +const cliPosix = cliNative.split(path.sep).join('/') + +fs.mkdirSync(binDir, { recursive: true }) + +fs.writeFileSync(path.join(binDir, 'verifyx'), `#!/bin/sh\nexec node "${cliPosix}" "$@"\n`) +fs.chmodSync(path.join(binDir, 'verifyx'), 0o755) +fs.writeFileSync(path.join(binDir, 'verifyx.cmd'), `@node "${cliNative}" %*\r\n`) +fs.writeFileSync(path.join(binDir, 'verifyx.ps1'), `#!/usr/bin/env pwsh\nnode "${cliPosix}" $args\nexit $LASTEXITCODE\n`) + +process.stdout.write('dev bin ready: node_modules/.bin/verifyx -> node src/cli.ts\n') diff --git a/src/checks/external.ts b/src/checks/external.ts index 76db84a..09d24e8 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -52,10 +52,10 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { kind: 'external', inDefaultRun: spec.inDefaultRun ?? true, // Scaffold as a call into this CLI so fix-vs-check lives in one place, not the consumer's script. - scaffold: { script: `verify ${spec.name}`, devDeps: spec.devDeps }, + scaffold: { script: `verifyx ${spec.name}`, devDeps: spec.devDeps }, async runDefault(): Promise { if (!hasLocalBin(spec.bin)) { - console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`verify init\`)`)) + console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`verifyx init\`)`)) return { name: spec.name, ok: true, skipped: true } } if (spec.canRun && !spec.canRun()) { diff --git a/src/checks/registry.test.ts b/src/checks/registry.test.ts index 3809ab8..7c81fcf 100644 --- a/src/checks/registry.test.ts +++ b/src/checks/registry.test.ts @@ -26,11 +26,11 @@ describe('check registry', () => { const knip = getCheck('knip') expect(knip?.kind).toBe('external') expect(knip?.scaffold.devDeps).toContain('knip') - expect(knip?.scaffold.script).toBe('verify knip') + expect(knip?.scaffold.script).toBe('verifyx knip') }) it('scaffolds native checks back into the verify CLI', () => { - expect(getCheck('complexity')?.scaffold.script).toBe('verify complexity') + expect(getCheck('complexity')?.scaffold.script).toBe('verifyx complexity') }) it('returns undefined for unknown checks', () => { diff --git a/src/checks/registry.ts b/src/checks/registry.ts index d6fef2e..d01310c 100644 --- a/src/checks/registry.ts +++ b/src/checks/registry.ts @@ -15,7 +15,7 @@ function nativeCheck(name: string, description: string, inDefaultRun: boolean, r kind: 'native', inDefaultRun, // Native checks scaffold as a call back into this CLI's own subcommand. - scaffold: { script: `verify ${name}` }, + scaffold: { script: `verifyx ${name}` }, runDefault: async () => run(), } } diff --git a/src/cli.ts b/src/cli.ts index 1cc5424..8941c06 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ const pkg = require('../package.json') as { version: string } const program = new Command() program - .name('verify') + .name('verifyx') .description('A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code.') .version(pkg.version) .option('--all', 'run all verify:* scripts, ignoring diff-based filters') diff --git a/src/commands/registerChecks.ts b/src/commands/registerChecks.ts index 02c9133..8351ecf 100644 --- a/src/commands/registerChecks.ts +++ b/src/commands/registerChecks.ts @@ -15,7 +15,7 @@ function collect(value: string, previous: string[]): string[] { return [...previous, value] } -/** Register a directly-invocable subcommand for every built-in check (`verify complexity`, `verify knip`, …). */ +/** Register a directly-invocable subcommand for every built-in check (`verifyx complexity`, `verifyx knip`, …). */ export function registerChecks(program: Command): void { program .command('complexity') diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts index 5b7271f..b61d813 100644 --- a/src/commands/registerInit.ts +++ b/src/commands/registerInit.ts @@ -48,22 +48,22 @@ async function resolveSelections(opts: InitCliOptions): Promise<{ checks: string function report(result: InitResult, defaultsOnly: boolean): void { if (defaultsOnly) { - console.log(color.dim('\nDefaults-only: no verify:* scripts written — `verify` will run the built-in default set.')) + console.log(color.dim('\nDefaults-only: no verify:* scripts written — `verifyx` will run the built-in default set.')) } console.log(color.green(`\nScripts added: ${result.addedScripts.join(', ') || '(none new)'}`)) for (const file of result.agentFiles) { if (file.action === 'unchanged') continue console.log(` ${file.action === 'created' ? '+' : '~'} ${file.path}`) } - console.log(color.dim('\nRun `verify` to run your verifications.')) + console.log(color.dim('\nRun `verifyx` (or `npm run verify`) to run your verifications.')) } -/** `verify init` — interactively scaffold checks + agent files into the current project. */ +/** `verifyx init` — interactively scaffold checks + agent files into the current project. */ export function registerInit(program: Command): void { program .command('init') .description('Scaffold verifications and agent commands into this project') - .option('--defaults-only', 'do not write verify:* scripts; rely on `verify` built-in defaults') + .option('--defaults-only', 'do not write verify:* scripts; rely on `verifyx` built-in defaults') .option('--yes', 'non-interactive: use flag selections (or defaults) without prompting') .option('--select ', 'preselect a check by name (repeatable, non-interactive)', collect, []) .option('--no-claude', 'do not write .claude/ files (non-interactive)') diff --git a/src/commands/registerList.ts b/src/commands/registerList.ts index 32b2a28..55c4e7f 100644 --- a/src/commands/registerList.ts +++ b/src/commands/registerList.ts @@ -3,7 +3,7 @@ import type { Command } from 'commander' import { CHECKS } from '../checks/registry.ts' import { color } from '../shared/color.ts' -/** `verify list` — show every built-in check, its kind, and whether it runs in the default set. */ +/** `verifyx list` — show every built-in check, its kind, and whether it runs in the default set. */ export function registerList(program: Command): void { program .command('list') diff --git a/src/commands/registerUpgradeDocs.ts b/src/commands/registerUpgradeDocs.ts index e84f51e..b53e393 100644 --- a/src/commands/registerUpgradeDocs.ts +++ b/src/commands/registerUpgradeDocs.ts @@ -3,7 +3,7 @@ import type { Command } from 'commander' import { type AgentTarget, writeAgentFiles } from '../scaffold/agentFiles.ts' import { summarise } from '../scaffold/writeManaged.ts' -/** `verify upgrade-docs` — idempotently create/refresh the managed agent command + skill files. */ +/** `verifyx upgrade-docs` — idempotently create/refresh the managed agent command + skill files. */ export function registerUpgradeDocs(program: Command): void { program .command('upgrade-docs') diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts index 234b0fa..48f1e11 100644 --- a/src/scaffold/init.test.ts +++ b/src/scaffold/init.test.ts @@ -25,9 +25,9 @@ describe('applyInit', () => { const result = applyInit({ cwd: dir, checks: ['complexity', 'knip'], targets: ['claude'], defaultsOnly: false }) const scripts = readScripts() - expect(scripts['verify:complexity']).toBe('verify complexity') + expect(scripts['verify:complexity']).toBe('verifyx complexity') expect(scripts['verify:knip']).toContain('knip') - expect(scripts.verify).toBe('verify') + expect(scripts.verify).toBe('verifyx') expect(result.devDeps).toContain('knip') expect(fs.existsSync(path.join(dir, '.claude', 'commands', 'verify.md'))).toBe(true) expect(fs.existsSync(path.join(dir, '.claude', 'skills', 'verify', 'SKILL.md'))).toBe(true) diff --git a/src/scaffold/packageScripts.ts b/src/scaffold/packageScripts.ts index 2a8f8a3..395aa54 100644 --- a/src/scaffold/packageScripts.ts +++ b/src/scaffold/packageScripts.ts @@ -18,7 +18,7 @@ export function addVerifyScripts(packageJsonPath: string, scripts: Record&1` (or the project's `verify` npm script — never the global binary). +Run this project's verifications. Prefer the npm script, which works on every platform: + +``` +npm run verify +``` + +(or invoke the binary directly with `npx verifyx` — the CLI is named `verifyx`, not `verify`, because `verify` is a Windows `cmd` builtin; see the project README. Never use a global binary.) If anything fails: -- Fix every reported issue, then run `verify` again. Repeat until it passes cleanly. +- Fix every reported issue, then run it again. Repeat until it passes cleanly. - Do **not** silence failures with `--warn`, and do not delete or weaken checks to make them pass. - Treat escape hatches (like a `context:` comment prefix) as a last resort — prefer making the code self-explanatory. diff --git a/templates/skills/verify/SKILL.md b/templates/skills/verify/SKILL.md index 77c8ca9..695a896 100644 --- a/templates/skills/verify/SKILL.md +++ b/templates/skills/verify/SKILL.md @@ -10,21 +10,24 @@ verifications that give AI coding agents back-pressure against writing hard-to-m ## How to run -Run the project's verifications via its npm script or `npx verify` (a pinned dev dependency — never a global install): +Run the project's verifications via its npm script (works on every platform): ``` -npx verify +npm run verify ``` -- With no `verify:*` scripts defined, `verify` runs the built-in default checks in their default modes. -- With `verify:*` scripts defined, `verify` runs those in parallel; output is suppressed unless a check fails. -- Run a single built-in directly, e.g. `npx verify complexity` or `npx verify knip`. -- `npx verify list` shows every built-in check. +The CLI binary is `verifyx` (not `verify` — that name is a Windows `cmd` builtin). You can also invoke it +directly with `npx verifyx`. It's a pinned dev dependency — never a global install. + +- With no `verify:*` scripts defined, `verifyx` runs the built-in default checks in their default modes. +- With `verify:*` scripts defined, `verifyx` runs those in parallel; output is suppressed unless a check fails. +- Run a single built-in directly, e.g. `npx verifyx complexity` or `npx verifyx knip`. +- `npx verifyx list` shows every built-in check. ## Working with failures 1. Read each failure carefully — it explains what is wrong and how to fix it. 2. Fix the underlying issue. For complexity failures, **split the file**; do not game the metric by deleting comments, joining lines, or shortening names. -3. Re-run `verify` until it passes. +3. Re-run until it passes. 4. Do not bypass checks with `--warn`, and reach for escape hatches (like `context:` comments) only as a last resort. From 8d1659360c559963acecaa760eb273fc354d567c Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 01:36:31 +0800 Subject: [PATCH 09/34] feat: run only defined verify:* scripts by default; add `verifyx all` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the gate explicit: bare `verifyx` runs only the project's own `verify:*` scripts (nothing when none are defined), and a new `verifyx all` runs every built-in check. A `verify:` script overrides its matching built-in, so you can swap one check's implementation without redefining the rest. - Add orchestrator `runAll` (per-check override via verify:); bare `orchestrate` no longer falls back to a built-in default run. Rename the filter-bypass flag to `--no-filter` to avoid clashing with the `all` command. - Name checks for their function, not the tool: `knip` -> `unused-code`, and drop tool names from descriptions (the tool is a bin/devDeps detail). - Replace the `inDefaultRun` flag with `recommended` (only affects what `verifyx init` preselects); defaults-only init now wires `verify` to `verifyx all`. - Un-export internal-only symbols the unused-code check flagged, and add a knip.json (ignore the runtime-invoked jscpd/skott, ignore the verifyx bin). - Drop `verify:block-comments` from this repo's own gate — it fails on any comment on a changed line, which conflicts with the repo's deliberate (brief, why-focused) comment style. It stays available via `verifyx all`. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 12 ++++----- knip.json | 6 +++++ package.json | 3 +-- src/checks/comment-block.ts | 2 +- src/checks/complexity.ts | 2 +- src/checks/external.ts | 6 ++--- src/checks/forbidden-strings.ts | 11 +++----- src/checks/registry.test.ts | 30 ++++++++++++++-------- src/checks/registry.ts | 31 +++++++++++++---------- src/checks/types.ts | 4 +-- src/cli.ts | 20 +++++++++++++-- src/commands/registerInit.ts | 6 ++--- src/commands/registerList.ts | 2 +- src/index.ts | 6 ++--- src/orchestrator/measure.ts | 2 +- src/orchestrator/run.ts | 16 +++++++----- src/orchestrator/runAll.ts | 45 +++++++++++++++++++++++++++++++++ src/orchestrator/runDefaults.ts | 40 ----------------------------- src/report.ts | 2 -- src/scaffold/agentFiles.ts | 4 +-- src/scaffold/init.test.ts | 7 ++--- src/scaffold/init.ts | 3 ++- src/scaffold/packageScripts.ts | 6 ++--- src/scaffold/writeManaged.ts | 2 +- 25 files changed, 155 insertions(+), 115 deletions(-) create mode 100644 knip.json create mode 100644 src/orchestrator/runAll.ts delete mode 100644 src/orchestrator/runDefaults.ts diff --git a/CLAUDE.md b/CLAUDE.md index 79a6cd8..822d9e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ A growing collection of code **verifications** that give AI coding agents back-p ## After making changes, run `npm run verify` -`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comment-block|…`). It runs them in parallel and suppresses output unless one fails. +`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comment-block|…`). It runs them in parallel and suppresses output unless one fails. Bare `verifyx` runs **only** the defined `verify:*` scripts (nothing if none); `verifyx all` runs **every** built-in check, with a `verify:` script overriding its matching built-in. **Fix locally, check in CI.** Run locally, `verifyx` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verifyx --check` / `verifyx --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) diff --git a/README.md b/README.md index b615bad..d3bd66a 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ npx verifyx init ## How `verifyx` decides what to run -Running `verifyx` with no subcommand follows a convention: +Your project's `verify:*` scripts **are** the gate, and `verifyx` runs exactly what you define — nothing implicit: -- **No `verify:*` scripts in `package.json`** → it runs the **built-in default checks** in their default modes. Each check degrades gracefully — it passes or skips when it does not apply (no files, no diff, tool not installed, no rules configured). -- **`verify:*` scripts present** → it runs **those** in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code). Add `--verbose` to stream everything. +- **`verifyx`** (no subcommand) runs your `verify:*` scripts in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code); add `--verbose` to stream everything. With **no `verify:*` scripts, nothing runs.** +- **`verifyx all`** runs **every built-in check** (the explicit "run everything"). A `verify:` script **overrides** its matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. -You take control by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: +You curate the gate by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc { @@ -54,7 +54,7 @@ You take control by adding `verify:*` scripts. Prefer calling the built-ins (`ve } ``` -Run a single built-in directly: `verifyx complexity`, `verifyx knip`, … and `verifyx list` shows them all. +Run a single built-in directly: `verifyx complexity`, `verifyx unused-code`, … and `verifyx list` shows them all. ### Fix locally, check in CI @@ -133,7 +133,7 @@ It lets you multi-select **checks** and **agent targets** (Claude `.claude/`, an Options: -- `--defaults-only` — do **not** write `verify:*` scripts; rely on `verify`'s built-in defaults (still installs opted-in tools and writes agent files). +- `--defaults-only` — do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes agent files). - `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. ### `verifyx upgrade-docs` diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..039e5a9 --- /dev/null +++ b/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "project": ["src/**/*.ts"], + "ignoreBinaries": ["verifyx"], + "ignoreDependencies": ["jscpd", "skott"] +} diff --git a/package.json b/package.json index fd56c7d..eba771a 100644 --- a/package.json +++ b/package.json @@ -60,10 +60,9 @@ "verify:check-types": "verifyx check-types", "verify:complexity": "verifyx complexity --threshold 50 \"src/**/*.ts\"", "verify:comment-block": "verifyx comment-block --max-lines 1 --pushback \"src/**/*.ts\"", - "verify:block-comments": "verifyx block-comments", "verify:hardcoded-colors": "verifyx hardcoded-colors", "verify:forbidden-strings": "verifyx forbidden-strings", - "verify:knip": "verifyx knip", + "verify:unused-code": "verifyx unused-code", "verify:circular-deps": "verifyx circular-deps", "verify:duplicate-code": "verifyx duplicate-code", "test": "vitest run", diff --git a/src/checks/comment-block.ts b/src/checks/comment-block.ts index 6a53a09..99fe827 100644 --- a/src/checks/comment-block.ts +++ b/src/checks/comment-block.ts @@ -4,7 +4,7 @@ import { printCommentBlockReport } from '../report.ts' import { color } from '../shared/color.ts' import type { CheckResult } from './types.ts' -export const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 +const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 export type CommentBlockOptions = { pattern?: string diff --git a/src/checks/complexity.ts b/src/checks/complexity.ts index dbfb2f8..6444114 100644 --- a/src/checks/complexity.ts +++ b/src/checks/complexity.ts @@ -3,7 +3,7 @@ import { printFailure, printFileDetail, printMaintainabilityReport } from '../re import { color } from '../shared/color.ts' import type { CheckResult } from './types.ts' -export const DEFAULT_THRESHOLD = 50 +const DEFAULT_THRESHOLD = 50 export type ComplexityOptions = { pattern?: string diff --git a/src/checks/external.ts b/src/checks/external.ts index 09d24e8..a2e5d4e 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -9,7 +9,7 @@ import type { Check, CheckMode, CheckResult } from './types.ts' const BIN_EXTENSIONS = ['', '.cmd', '.ps1', '.exe'] /** True when a project-local binary is installed under node_modules/.bin (cross-platform). */ -export function hasLocalBin(bin: string, cwd: string = process.cwd()): boolean { +function hasLocalBin(bin: string, cwd: string = process.cwd()): boolean { const dir = path.join(cwd, 'node_modules', '.bin') return BIN_EXTENSIONS.some((ext) => fs.existsSync(path.join(dir, bin + ext))) } @@ -34,7 +34,7 @@ export type ExternalCheckSpec = { /** Command run in fix mode. When omitted, the check is not fixable and always runs `checkCommand`. */ fixCommand?: string devDeps: string[] - inDefaultRun?: boolean + recommended?: boolean /** Extra guard beyond bin presence (e.g. require a tsconfig). */ canRun?: () => boolean } @@ -50,7 +50,7 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { name: spec.name, description: spec.description, kind: 'external', - inDefaultRun: spec.inDefaultRun ?? true, + recommended: spec.recommended ?? false, // Scaffold as a call into this CLI so fix-vs-check lives in one place, not the consumer's script. scaffold: { script: `verifyx ${spec.name}`, devDeps: spec.devDeps }, async runDefault(): Promise { diff --git a/src/checks/forbidden-strings.ts b/src/checks/forbidden-strings.ts index 1d04592..441ddd7 100644 --- a/src/checks/forbidden-strings.ts +++ b/src/checks/forbidden-strings.ts @@ -6,10 +6,10 @@ import { color } from '../shared/color.ts' import { type ForbiddenStringsRule, loadVerifyConfig } from '../shared/config.ts' import type { CheckResult } from './types.ts' -export type ForbiddenStringViolation = { file: string; path: string; value: string } +type ForbiddenStringViolation = { file: string; path: string; value: string } // Ported from https://github.com/staff0rd/assist verify/forbiddenStrings/findForbiddenStrings.ts -export function resolveStringsAtPath(data: unknown, path: string): string[] { +function resolveStringsAtPath(data: unknown, path: string): string[] { let current: unknown = data for (const segment of path.split('.')) { if (current === null || typeof current !== 'object') return [] @@ -20,7 +20,7 @@ export function resolveStringsAtPath(data: unknown, path: string): string[] { return [] } -export function findRuleViolations(data: unknown, rule: ForbiddenStringsRule): ForbiddenStringViolation[] { +function findRuleViolations(data: unknown, rule: ForbiddenStringsRule): ForbiddenStringViolation[] { const violations: ForbiddenStringViolation[] = [] for (const path of rule.paths) { for (const value of resolveStringsAtPath(data, path)) { @@ -30,10 +30,7 @@ export function findRuleViolations(data: unknown, rule: ForbiddenStringsRule): F return violations } -export function findForbiddenStrings( - rules: readonly ForbiddenStringsRule[], - readJson: (file: string) => unknown, -): ForbiddenStringViolation[] { +function findForbiddenStrings(rules: readonly ForbiddenStringsRule[], readJson: (file: string) => unknown): ForbiddenStringViolation[] { return rules.flatMap((rule) => findRuleViolations(readJson(rule.file), rule)) } diff --git a/src/checks/registry.test.ts b/src/checks/registry.test.ts index 7c81fcf..d3606ee 100644 --- a/src/checks/registry.test.ts +++ b/src/checks/registry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { CHECKS, defaultChecks, getCheck } from './registry.ts' +import { CHECKS, getCheck, recommendedChecks } from './registry.ts' describe('check registry', () => { it('includes the native and external checks', () => { @@ -15,21 +15,28 @@ describe('check registry', () => { 'lint', 'format', 'check-types', - 'knip', + 'unused-code', 'circular-deps', 'duplicate-code', ]), ) }) - it('marks external tool checks with scaffold devDeps and a verify-CLI script', () => { - const knip = getCheck('knip') - expect(knip?.kind).toBe('external') - expect(knip?.scaffold.devDeps).toContain('knip') - expect(knip?.scaffold.script).toBe('verifyx knip') + it('names checks for their function, not the underlying tool', () => { + expect(getCheck('knip')).toBeUndefined() + expect(getCheck('skott')).toBeUndefined() + expect(getCheck('jscpd')).toBeUndefined() }) - it('scaffolds native checks back into the verify CLI', () => { + it('marks external tool checks with scaffold devDeps and a verifyx-CLI script', () => { + const unused = getCheck('unused-code') + expect(unused?.kind).toBe('external') + // The check is named for its function; the tool (knip) is only an install detail. + expect(unused?.scaffold.devDeps).toContain('knip') + expect(unused?.scaffold.script).toBe('verifyx unused-code') + }) + + it('scaffolds native checks back into the verifyx CLI', () => { expect(getCheck('complexity')?.scaffold.script).toBe('verifyx complexity') }) @@ -37,7 +44,10 @@ describe('check registry', () => { expect(getCheck('nope')).toBeUndefined() }) - it('every default check is in the registry', () => { - for (const check of defaultChecks()) expect(CHECKS).toContain(check) + it('recommends a subset for init preselection, all within the registry', () => { + const recommended = recommendedChecks() + expect(recommended.length).toBeGreaterThan(0) + expect(recommended.length).toBeLessThan(CHECKS.length) + for (const check of recommended) expect(CHECKS).toContain(check) }) }) diff --git a/src/checks/registry.ts b/src/checks/registry.ts index d01310c..799fd37 100644 --- a/src/checks/registry.ts +++ b/src/checks/registry.ts @@ -8,51 +8,54 @@ import { runForbiddenStrings } from './forbidden-strings.ts' import { runHardcodedColors } from './hardcoded-colors.ts' import type { Check, CheckResult } from './types.ts' -function nativeCheck(name: string, description: string, inDefaultRun: boolean, run: () => CheckResult): Check { +function nativeCheck(name: string, description: string, recommended: boolean, run: () => CheckResult): Check { return { name, description, kind: 'native', - inDefaultRun, + recommended, // Native checks scaffold as a call back into this CLI's own subcommand. scaffold: { script: `verifyx ${name}` }, runDefault: async () => run(), } } -/** Every verification this package knows about. Order here is the order shown by `verify list`. */ +// context: checks are named for their function, never the tool behind them (see each check's bin/devDeps). export const CHECKS: Check[] = [ nativeCheck('complexity', 'Maintainability-index gate (cyclomatic + Halstead + SLOC)', true, () => runComplexity()), nativeCheck('comment-block', 'Flag comment blocks longer than the limit (JSDoc / context: exempt)', true, () => runCommentBlock()), - nativeCheck('block-comments', 'Fail on any comment added to a line changed against HEAD', true, () => runBlockComments()), - nativeCheck('hardcoded-colors', 'Fail on literal hex / 0x colour values in source', true, () => runHardcodedColors()), - nativeCheck('forbidden-strings', 'Fail on disallowed JSON config values (rules from verify config)', true, () => runForbiddenStrings()), + nativeCheck('block-comments', 'Fail on any comment added to a line changed against HEAD', false, () => runBlockComments()), + nativeCheck('hardcoded-colors', 'Fail on literal hex / 0x colour values in source', false, () => runHardcodedColors()), + nativeCheck('forbidden-strings', 'Fail on disallowed JSON config values (rules from verify config)', false, () => runForbiddenStrings()), defineExternalCheck({ name: 'lint', - description: 'Lint, auto-fixing locally and checking in CI (oxlint)', + description: 'Lint — auto-fixes locally, checks in CI', bin: 'oxlint', checkCommand: 'oxlint .', fixCommand: 'oxlint --fix .', devDeps: ['oxlint'], + recommended: true, }), defineExternalCheck({ name: 'format', - description: 'Formatting, writing locally and checking in CI (oxfmt)', + description: 'Formatting — writes locally, checks in CI', bin: 'oxfmt', checkCommand: 'oxfmt --check .', fixCommand: 'oxfmt .', devDeps: ['oxfmt'], + recommended: true, }), defineExternalCheck({ name: 'check-types', - description: 'TypeScript type check (tsc --noEmit)', + description: 'TypeScript type check', bin: 'tsc', checkCommand: 'tsc --noEmit', devDeps: ['typescript'], canRun: () => fs.existsSync('tsconfig.json'), + recommended: true, }), defineExternalCheck({ - name: 'knip', + name: 'unused-code', description: 'Unused files, exports and dependencies', bin: 'knip', checkCommand: 'knip --no-progress --treat-config-hints-as-errors', @@ -60,14 +63,14 @@ export const CHECKS: Check[] = [ }), defineExternalCheck({ name: 'circular-deps', - description: 'Circular dependency detection (skott)', + description: 'Circular dependency detection', bin: 'skott', checkCommand: 'skott --displayMode=raw --showCircularDependencies --exitCodeOnCircularDependencies=1', devDeps: ['skott'], }), defineExternalCheck({ name: 'duplicate-code', - description: 'Copy-paste detection (jscpd)', + description: 'Copy-paste / duplicate-code detection', bin: 'jscpd', checkCommand: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src', devDeps: ['jscpd'], @@ -78,6 +81,6 @@ export function getCheck(name: string): Check | undefined { return CHECKS.find((check) => check.name === name) } -export function defaultChecks(): Check[] { - return CHECKS.filter((check) => check.inDefaultRun) +export function recommendedChecks(): Check[] { + return CHECKS.filter((check) => check.recommended) } diff --git a/src/checks/types.ts b/src/checks/types.ts index 38effdb..35abe7f 100644 --- a/src/checks/types.ts +++ b/src/checks/types.ts @@ -16,8 +16,8 @@ export type Check = { name: string description: string kind: CheckKind - /** Whether this check runs as part of the default set when the project has no `verify:*` scripts. */ - inDefaultRun: boolean + /** Whether `verifyx init` preselects this check as a recommended default. */ + recommended: boolean /** Run the check with its default options and print its own report. Resolves to the outcome. */ runDefault: () => Promise /** How `verify init` wires this check into a consuming project. */ diff --git a/src/cli.ts b/src/cli.ts index 8941c06..4a9a46b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,9 @@ import { registerInit } from './commands/registerInit.ts' import { registerList } from './commands/registerList.ts' import { registerUpgradeDocs } from './commands/registerUpgradeDocs.ts' import { orchestrate } from './orchestrator/run.ts' +import { runAll } from './orchestrator/runAll.ts' +import { configureMode } from './shared/mode.ts' +import { setVerbose } from './shared/spawn.ts' const require = createRequire(import.meta.url) const pkg = require('../package.json') as { version: string } @@ -18,15 +21,28 @@ program .name('verifyx') .description('A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code.') .version(pkg.version) - .option('--all', 'run all verify:* scripts, ignoring diff-based filters') + .option('--no-filter', 'run every verify:* script, ignoring diff-based filters') .option('--measure', "print a summary table of each verification's status and duration") .option('--verbose', 'stream all output instead of suppressing passing runs') .option('--check', 'check only — never auto-fix (the default under CI)') .option('--fix', 'auto-fix where possible (the default locally)') - .action(async (opts: { all?: boolean; measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { + .action(async (opts: { filter?: boolean; measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { process.exitCode = await orchestrate(opts) }) +program + .command('all') + .description('Run every built-in check (verify: scripts override the matching built-in)') + .option('--measure', "print a summary table of each verification's status and duration") + .option('--verbose', 'stream all output instead of suppressing passing runs') + .option('--check', 'check only — never auto-fix (the default under CI)') + .option('--fix', 'auto-fix where possible (the default locally)') + .action(async (opts: { measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { + setVerbose(!!opts.verbose) + configureMode(opts) + process.exitCode = await runAll({ measure: opts.measure }) + }) + registerChecks(program) registerList(program) registerInit(program) diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts index b61d813..4fd4fea 100644 --- a/src/commands/registerInit.ts +++ b/src/commands/registerInit.ts @@ -1,7 +1,7 @@ import type { Command } from 'commander' import enquirer from 'enquirer' -import { CHECKS, defaultChecks } from '../checks/registry.ts' +import { CHECKS, recommendedChecks } from '../checks/registry.ts' import type { AgentTarget } from '../scaffold/agentFiles.ts' import { applyInit, type InitResult } from '../scaffold/init.ts' import { color } from '../shared/color.ts' @@ -32,12 +32,12 @@ async function resolveSelections(opts: InitCliOptions): Promise<{ checks: string const targets: AgentTarget[] = [] if (opts.claude !== false) targets.push('claude') if (opts.agents) targets.push('agents') - return { checks: opts.select.length > 0 ? opts.select : defaultChecks().map((c) => c.name), targets } + return { checks: opts.select.length > 0 ? opts.select : recommendedChecks().map((c) => c.name), targets } } const checks = await multiselect( 'Select checks to wire up', - CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.inDefaultRun })), + CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.recommended })), ) const targets = (await multiselect('Select agent targets', [ { name: 'claude', message: 'Claude (.claude/commands + skill)', enabled: true }, diff --git a/src/commands/registerList.ts b/src/commands/registerList.ts index 55c4e7f..7f120c2 100644 --- a/src/commands/registerList.ts +++ b/src/commands/registerList.ts @@ -11,7 +11,7 @@ export function registerList(program: Command): void { .action(() => { console.log(color.heading('Built-in checks')) for (const check of CHECKS) { - const tags = color.dim(`(${check.kind}, ${check.inDefaultRun ? 'default' : 'opt-in'})`) + const tags = color.dim(`(${check.kind}${check.recommended ? ', recommended' : ''})`) console.log(` ${color.cyan(check.name.padEnd(18))} ${tags} ${check.description}`) } }) diff --git a/src/index.ts b/src/index.ts index 9ec217f..d45299f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,8 +14,8 @@ export { runCommentBlock } from './checks/comment-block.ts' export { runComplexity } from './checks/complexity.ts' export { runForbiddenStrings } from './checks/forbidden-strings.ts' export { runHardcodedColors } from './checks/hardcoded-colors.ts' -export { CHECKS, defaultChecks, getCheck } from './checks/registry.ts' -export type { Check, CheckKind, CheckResult } from './checks/types.ts' +export { CHECKS, getCheck, recommendedChecks } from './checks/registry.ts' +export type { Check, CheckKind, CheckMode, CheckResult } from './checks/types.ts' export { type CommentBlockViolation, findLongCommentBlocks } from './comments.ts' export { type FunctionCallback, forEachFunction } from './functions.ts' export { @@ -26,6 +26,6 @@ export { type HalsteadMetrics, } from './metrics.ts' export { orchestrate } from './orchestrator/run.ts' -export { runDefaults } from './orchestrator/runDefaults.ts' +export { runAll } from './orchestrator/runAll.ts' export { applyInit, type InitOptions, type InitResult } from './scaffold/init.ts' export { type ForbiddenStringsRule, loadVerifyConfig, type VerifyConfig } from './shared/config.ts' diff --git a/src/orchestrator/measure.ts b/src/orchestrator/measure.ts index 88048eb..c4df0a3 100644 --- a/src/orchestrator/measure.ts +++ b/src/orchestrator/measure.ts @@ -5,7 +5,7 @@ export type MeasureRecord = { durationMs: number } -export function formatDuration(ms: number): string { +function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms` return `${(ms / 1000).toFixed(1)}s` } diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index e65c588..ac7d24b 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -4,10 +4,10 @@ import { runCommand, setVerbose } from '../shared/spawn.ts' import { filterByChangedFiles } from './filterByChangedFiles.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' import { resolveEntries, type VerifyEntry } from './resolveEntries.ts' -import { runDefaults } from './runDefaults.ts' export type OrchestrateOptions = { - all?: boolean + /** When false (via --no-filter), run every verify:* script ignoring diff-based filters. */ + filter?: boolean measure?: boolean verbose?: boolean check?: boolean @@ -32,8 +32,9 @@ function reportScriptResults(records: readonly MeasureRecord[], total: number): } /** - * The default `verify` action. Convention: if the project defines `verify:*` scripts, run those in parallel - * (output buffered, flushed only on failure); otherwise run the built-in default check set in-process. + * The default `verifyx` action. Convention: run the project's own `verify:*` scripts in parallel (output + * buffered, flushed only on failure). With no `verify:*` scripts, nothing runs — use `verifyx all` to run + * every built-in check. */ export async function orchestrate(opts: OrchestrateOptions = {}): Promise { setVerbose(!!opts.verbose) @@ -41,9 +42,12 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise` script, in which case that script runs instead (per-check override). + */ +export async function runAll(opts: RunAllOptions = {}): Promise { + const overrides = new Map(resolveEntries().map((entry) => [entry.name, entry])) + + console.log(`Running all ${CHECKS.length} built-in verification(s):`) + for (const check of CHECKS) { + console.log(` - ${check.name}${overrides.has(`verify:${check.name}`) ? color.dim(' (overridden)') : ''}`) + } + console.log() + + const startTime = Date.now() + const records: MeasureRecord[] = [] + const results: CheckResult[] = [] + for (const check of CHECKS) { + const checkStart = Date.now() + console.log(color.heading(`▶ ${check.name}`)) + const override = overrides.get(`verify:${check.name}`) + const ok = override ? (await runCommand(override.command, { cwd: override.cwd })) === 0 : (await check.runDefault()).ok + results.push({ name: check.name, ok }) + records.push({ script: check.name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) + console.log() + } + + if (opts.measure) printMeasureTable(records, Date.now() - startTime) + + const failed = results.filter((result) => !result.ok) + if (failed.length > 0) { + console.error(color.red(`\n${failed.length} verification(s) failed: ${failed.map((f) => f.name).join(', ')}`)) + return 1 + } + console.log(color.green(`\nAll ${CHECKS.length} verification(s) passed`)) + return 0 +} diff --git a/src/orchestrator/runDefaults.ts b/src/orchestrator/runDefaults.ts deleted file mode 100644 index 690c24e..0000000 --- a/src/orchestrator/runDefaults.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { defaultChecks } from '../checks/registry.ts' -import type { CheckResult } from '../checks/types.ts' -import { color } from '../shared/color.ts' -import { type MeasureRecord, printMeasureTable } from './measure.ts' - -export type RunDefaultsOptions = { measure?: boolean } - -/** - * Run the built-in default check set in-process (the convention when a project has no `verify:*` scripts). - * Checks run sequentially with clear headers so their reports stay readable; each degrades to a pass/skip - * when not applicable (no files, no diff, tool absent, no rules). - */ -export async function runDefaults(opts: RunDefaultsOptions = {}): Promise { - const checks = defaultChecks() - console.log(`Running ${checks.length} built-in verification(s):`) - for (const check of checks) console.log(` - ${check.name}`) - console.log() - - const startTime = Date.now() - const records: MeasureRecord[] = [] - const results: CheckResult[] = [] - for (const check of checks) { - const checkStart = Date.now() - console.log(color.heading(`▶ ${check.name}`)) - const result = await check.runDefault() - results.push(result) - records.push({ script: check.name, code: result.ok ? 0 : 1, durationMs: Date.now() - checkStart }) - console.log() - } - - if (opts.measure) printMeasureTable(records, Date.now() - startTime) - - const failed = results.filter((result) => !result.ok) - if (failed.length > 0) { - console.error(color.red(`\n${failed.length} verification(s) failed: ${failed.map((f) => f.name).join(', ')}`)) - return 1 - } - console.log(color.green(`\nAll ${checks.length} verification(s) passed`)) - return 0 -} diff --git a/src/report.ts b/src/report.ts index 0aafa1b..bb48499 100644 --- a/src/report.ts +++ b/src/report.ts @@ -6,8 +6,6 @@ import { forEachFunction } from './functions.ts' import { calculateCyclomaticComplexity, calculateHalstead, calculateMaintainabilityIndex, countSloc } from './metrics.ts' import { color } from './shared/color.ts' -export { color } - const FORMULA = '171 - 5.2*ln(HalsteadVolume) - 0.23*CyclomaticComplexity - 16.2*ln(SLOC), clamped 0-100' export function printMaintainabilityReport( diff --git a/src/scaffold/agentFiles.ts b/src/scaffold/agentFiles.ts index e6acddf..8f4d9c6 100644 --- a/src/scaffold/agentFiles.ts +++ b/src/scaffold/agentFiles.ts @@ -12,12 +12,12 @@ const moduleDir = path.dirname(fileURLToPath(import.meta.url)) // src/scaffold/*.ts and dist/scaffold/*.mjs both sit two levels below the package root, where templates/ lives. const TEMPLATES_DIR = path.join(moduleDir, '..', '..', 'templates') -export function readTemplate(relativePath: string): string { +function readTemplate(relativePath: string): string { return fs.readFileSync(path.join(TEMPLATES_DIR, ...relativePath.split('/')), 'utf-8') } /** The managed agent files to emit for the chosen targets. Claude gets a slash command + skill; others get a skill. */ -export function managedFilesFor(targets: readonly AgentTarget[]): ManagedFile[] { +function managedFilesFor(targets: readonly AgentTarget[]): ManagedFile[] { const files: ManagedFile[] = [] if (targets.includes('claude')) { files.push({ template: 'commands/verify.md', dest: '.claude/commands/verify.md' }) diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts index 48f1e11..27a8582 100644 --- a/src/scaffold/init.test.ts +++ b/src/scaffold/init.test.ts @@ -22,11 +22,11 @@ function readScripts(): Record { describe('applyInit', () => { it('writes verify:* scripts, agent files, and collects external devDeps', () => { - const result = applyInit({ cwd: dir, checks: ['complexity', 'knip'], targets: ['claude'], defaultsOnly: false }) + const result = applyInit({ cwd: dir, checks: ['complexity', 'unused-code'], targets: ['claude'], defaultsOnly: false }) const scripts = readScripts() expect(scripts['verify:complexity']).toBe('verifyx complexity') - expect(scripts['verify:knip']).toContain('knip') + expect(scripts['verify:unused-code']).toBe('verifyx unused-code') expect(scripts.verify).toBe('verifyx') expect(result.devDeps).toContain('knip') expect(fs.existsSync(path.join(dir, '.claude', 'commands', 'verify.md'))).toBe(true) @@ -34,9 +34,10 @@ describe('applyInit', () => { }) it('defaults-only writes no verify:* scripts but keeps devDeps and agent files', () => { - const result = applyInit({ cwd: dir, checks: ['knip'], targets: ['agents'], defaultsOnly: true }) + const result = applyInit({ cwd: dir, checks: ['unused-code'], targets: ['agents'], defaultsOnly: true }) expect(Object.keys(readScripts())).toEqual(['verify']) + expect(readScripts().verify).toBe('verifyx all') expect(result.devDeps).toContain('knip') expect(fs.existsSync(path.join(dir, '.agent-skills', 'verify', 'SKILL.md'))).toBe(true) }) diff --git a/src/scaffold/init.ts b/src/scaffold/init.ts index 43d0fdf..871611b 100644 --- a/src/scaffold/init.ts +++ b/src/scaffold/init.ts @@ -33,7 +33,8 @@ export function applyInit(opts: InitOptions): InitResult { if (!opts.defaultsOnly) scripts[`verify:${name}`] = check.scaffold.script } - const addedScripts = addVerifyScripts(path.join(opts.cwd, 'package.json'), scripts) + // Defaults-only wires the top `verify` script to `verifyx all` so it runs every built-in with no verify:* list. + const addedScripts = addVerifyScripts(path.join(opts.cwd, 'package.json'), scripts, opts.defaultsOnly ? 'verifyx all' : 'verifyx') const agentFiles = writeAgentFiles(opts.cwd, opts.targets) return { addedScripts, devDeps: [...new Set(devDeps)], agentFiles } } diff --git a/src/scaffold/packageScripts.ts b/src/scaffold/packageScripts.ts index 395aa54..09a4fdf 100644 --- a/src/scaffold/packageScripts.ts +++ b/src/scaffold/packageScripts.ts @@ -4,9 +4,9 @@ type PackageJson = { scripts?: Record } & Record): string[] { +export function addVerifyScripts(packageJsonPath: string, scripts: Record, topScript = 'verifyx'): string[] { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson const existing = pkg.scripts ?? {} const added: string[] = [] @@ -18,7 +18,7 @@ export function addVerifyScripts(packageJsonPath: string, scripts: Record Date: Thu, 9 Jul 2026 01:44:35 +0800 Subject: [PATCH 10/34] feat: support verify::fix override variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When overriding a check with a `verify:` script, a `verify::fix` variant can be defined too. `verifyx` runs the `:fix` variant in fix mode (locally) and the base script in check mode (CI), and never both — so an override backed by a non-mode-aware tool still fixes locally and only checks in CI (e.g. `verify:lint` = `eslint .`, `verify:lint:fix` = `eslint . --fix`). - Add `selectEntries` (bare verifyx: collapse verify:/:fix pairs to the variant for the current mode) and `resolveOverride` (verifyx all: pick the per-check override variant), both pure and unit-tested. - Wire them into `orchestrate` and `runAll`. - Document the fix/check override variants in the README. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 11 ++++ src/orchestrator/resolveEntries.test.ts | 69 ++++++++++++++++++++++++- src/orchestrator/resolveEntries.ts | 42 ++++++++++++++- src/orchestrator/run.ts | 8 +-- src/orchestrator/runAll.ts | 13 +++-- 5 files changed, 133 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d3bd66a..6d27513 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,17 @@ Your project's `verify:*` scripts **are** the gate, and `verifyx` runs exactly w - **`verifyx`** (no subcommand) runs your `verify:*` scripts in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code); add `--verbose` to stream everything. With **no `verify:*` scripts, nothing runs.** - **`verifyx all`** runs **every built-in check** (the explicit "run everything"). A `verify:` script **overrides** its matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. +**Fix/check overrides.** Alongside a `verify:` (check-mode) script you can define a `verify::fix` variant. `verifyx` runs the `:fix` variant in fix mode (locally) and the base in check mode (CI), and never both — so an override with a non-mode-aware tool still fixes locally and only checks in CI: + +```jsonc +{ + "scripts": { + "verify:lint": "eslint .", + "verify:lint:fix": "eslint . --fix", + }, +} +``` + You curate the gate by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc diff --git a/src/orchestrator/resolveEntries.test.ts b/src/orchestrator/resolveEntries.test.ts index 726c294..7bafe5b 100644 --- a/src/orchestrator/resolveEntries.test.ts +++ b/src/orchestrator/resolveEntries.test.ts @@ -4,7 +4,11 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { resolveEntries } from './resolveEntries.ts' +import { entryCheckName, resolveEntries, resolveOverride, selectEntries, type VerifyEntry } from './resolveEntries.ts' + +function entry(name: string): VerifyEntry { + return { name, command: `npm run ${name}`, cwd: '.' } +} let dir: string @@ -36,3 +40,66 @@ describe('resolveEntries', () => { expect(resolveEntries(dir).find((e) => e.name === 'verify:web')?.filter).toBe('web/**') }) }) + +describe('entryCheckName', () => { + it('strips the verify: prefix', () => { + expect(entryCheckName('verify:lint')).toBe('lint') + }) + it('strips a trailing :fix so a variant maps to its base check', () => { + expect(entryCheckName('verify:lint:fix')).toBe('lint') + expect(entryCheckName('verify:comment-block:fix')).toBe('comment-block') + }) +}) + +describe('selectEntries (bare verifyx: verify: / verify::fix pairs)', () => { + it('runs the base script in both modes when there is no :fix variant', () => { + const entries = [entry('verify:lint')] + expect(selectEntries(entries, 'check').map((e) => e.name)).toEqual(['verify:lint']) + expect(selectEntries(entries, 'fix').map((e) => e.name)).toEqual(['verify:lint']) + }) + + it('prefers the :fix variant in fix mode and the base in check mode', () => { + const entries = [entry('verify:lint'), entry('verify:lint:fix')] + expect(selectEntries(entries, 'fix').map((e) => e.name)).toEqual(['verify:lint:fix']) + expect(selectEntries(entries, 'check').map((e) => e.name)).toEqual(['verify:lint']) + }) + + it('never runs both the base and the :fix variant of the same check', () => { + const entries = [entry('verify:lint'), entry('verify:lint:fix')] + expect(selectEntries(entries, 'fix')).toHaveLength(1) + expect(selectEntries(entries, 'check')).toHaveLength(1) + }) + + it('uses a lone :fix variant in fix mode but skips it in check mode', () => { + const entries = [entry('verify:lint:fix')] + expect(selectEntries(entries, 'fix').map((e) => e.name)).toEqual(['verify:lint:fix']) + expect(selectEntries(entries, 'check')).toEqual([]) + }) + + it('keeps distinct checks independent', () => { + const entries = [entry('verify:lint'), entry('verify:lint:fix'), entry('verify:complexity')] + expect( + selectEntries(entries, 'fix') + .map((e) => e.name) + .sort(), + ).toEqual(['verify:complexity', 'verify:lint:fix']) + }) +}) + +describe('resolveOverride (verifyx all per-check override)', () => { + const entries = [entry('verify:lint'), entry('verify:lint:fix'), entry('verify:complexity')] + + it('returns the :fix variant in fix mode, the base in check mode', () => { + expect(resolveOverride(entries, 'lint', 'fix')?.name).toBe('verify:lint:fix') + expect(resolveOverride(entries, 'lint', 'check')?.name).toBe('verify:lint') + }) + + it('falls back to the base when there is no :fix variant', () => { + expect(resolveOverride(entries, 'complexity', 'fix')?.name).toBe('verify:complexity') + }) + + it('returns undefined when no override is defined (built-in is used)', () => { + expect(resolveOverride(entries, 'knip', 'fix')).toBeUndefined() + expect(resolveOverride([entry('verify:lint:fix')], 'lint', 'check')).toBeUndefined() + }) +}) diff --git a/src/orchestrator/resolveEntries.ts b/src/orchestrator/resolveEntries.ts index 2321bd3..2544b11 100644 --- a/src/orchestrator/resolveEntries.ts +++ b/src/orchestrator/resolveEntries.ts @@ -1,6 +1,8 @@ import fs from 'node:fs' import path from 'node:path' +import type { CheckMode } from '../checks/types.ts' + export type VerifyEntry = { name: string command: string @@ -8,6 +10,15 @@ export type VerifyEntry = { filter?: string } +const VERIFY_PREFIX = 'verify:' +const FIX_SUFFIX = ':fix' + +/** The check a `verify:*` script targets, ignoring a trailing `:fix`. `verify:lint` and `verify:lint:fix` → `lint`. */ +export function entryCheckName(entryName: string): string { + const withoutPrefix = entryName.startsWith(VERIFY_PREFIX) ? entryName.slice(VERIFY_PREFIX.length) : entryName + return withoutPrefix.endsWith(FIX_SUFFIX) ? withoutPrefix.slice(0, -FIX_SUFFIX.length) : withoutPrefix +} + /** Walk up from `startDir` to the nearest package.json. */ function findPackageJson(startDir: string): string | null { let dir = path.resolve(startDir) @@ -39,6 +50,35 @@ export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { const filters = pkg.verify?.filters ?? {} const dir = path.dirname(pkgPath) return Object.keys(scripts) - .filter((name) => name.startsWith('verify:')) + .filter((name) => name.startsWith(VERIFY_PREFIX)) .map((name) => ({ name, command: `npm run ${name}`, cwd: dir, filter: filters[name] })) } + +/** + * Collapse `verify:` / `verify::fix` pairs to one entry per check for the run mode: fix mode + * prefers the `:fix` variant (falling back to the base), check mode uses the base only. This is what bare + * `verifyx` runs. + */ +export function selectEntries(entries: readonly VerifyEntry[], mode: CheckMode): VerifyEntry[] { + const byCheck = new Map() + for (const entry of entries) { + const key = entryCheckName(entry.name) + const group = byCheck.get(key) ?? {} + if (entry.name.endsWith(FIX_SUFFIX)) group.fix = entry + else group.base = entry + byCheck.set(key, group) + } + const selected: VerifyEntry[] = [] + for (const { base, fix } of byCheck.values()) { + const chosen = mode === 'fix' ? (fix ?? base) : base + if (chosen) selected.push(chosen) + } + return selected +} + +/** The override script for a built-in `` under `verifyx all`: the `:fix` variant in fix mode, else the base. */ +export function resolveOverride(entries: readonly VerifyEntry[], checkName: string, mode: CheckMode): VerifyEntry | undefined { + const fix = entries.find((entry) => entry.name === `${VERIFY_PREFIX}${checkName}${FIX_SUFFIX}`) + const base = entries.find((entry) => entry.name === `${VERIFY_PREFIX}${checkName}`) + return mode === 'fix' ? (fix ?? base) : base +} diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index ac7d24b..09995d0 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -1,9 +1,9 @@ import { color } from '../shared/color.ts' -import { configureMode } from '../shared/mode.ts' +import { configureMode, resolveMode } from '../shared/mode.ts' import { runCommand, setVerbose } from '../shared/spawn.ts' import { filterByChangedFiles } from './filterByChangedFiles.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' -import { resolveEntries, type VerifyEntry } from './resolveEntries.ts' +import { resolveEntries, selectEntries, type VerifyEntry } from './resolveEntries.ts' export type OrchestrateOptions = { /** When false (via --no-filter), run every verify:* script ignoring diff-based filters. */ @@ -47,7 +47,9 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise / verify::fix pairs to the variant for the current mode. + const modeEntries = selectEntries(allEntries, resolveMode()) + const entries = opts.filter === false ? modeEntries : filterByChangedFiles(modeEntries) if (entries.length === 0) { console.log('No verify scripts matched changed files — skipping') return 0 diff --git a/src/orchestrator/runAll.ts b/src/orchestrator/runAll.ts index ad6c57c..28f2a4d 100644 --- a/src/orchestrator/runAll.ts +++ b/src/orchestrator/runAll.ts @@ -1,22 +1,25 @@ import { CHECKS } from '../checks/registry.ts' import type { CheckResult } from '../checks/types.ts' import { color } from '../shared/color.ts' +import { resolveMode } from '../shared/mode.ts' import { runCommand } from '../shared/spawn.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' -import { resolveEntries } from './resolveEntries.ts' +import { resolveEntries, resolveOverride } from './resolveEntries.ts' export type RunAllOptions = { measure?: boolean } /** * Run every built-in check (the explicit `verifyx all` opt-in). A check runs in-process unless the project - * defines a matching `verify:` script, in which case that script runs instead (per-check override). + * defines a matching `verify:` script (or `verify::fix` in fix mode), in which case that script + * runs instead (per-check override). */ export async function runAll(opts: RunAllOptions = {}): Promise { - const overrides = new Map(resolveEntries().map((entry) => [entry.name, entry])) + const entries = resolveEntries() + const mode = resolveMode() console.log(`Running all ${CHECKS.length} built-in verification(s):`) for (const check of CHECKS) { - console.log(` - ${check.name}${overrides.has(`verify:${check.name}`) ? color.dim(' (overridden)') : ''}`) + console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) } console.log() @@ -26,7 +29,7 @@ export async function runAll(opts: RunAllOptions = {}): Promise { for (const check of CHECKS) { const checkStart = Date.now() console.log(color.heading(`▶ ${check.name}`)) - const override = overrides.get(`verify:${check.name}`) + const override = resolveOverride(entries, check.name, mode) const ok = override ? (await runCommand(override.command, { cwd: override.cwd })) === 0 : (await check.runDefault()).ok results.push({ name: check.name, ok }) records.push({ script: check.name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) From cfdeccd9ba73b2a8643def6d54c2775d128f3260 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 02:19:48 +0800 Subject: [PATCH 11/34] feat: ship a verify skill + CLAUDE.md/AGENTS.md pointer instead of a slash command Replace the /verify slash command with a skill, so the integration is identical across Claude and other agents and nothing has to be invoked manually: - init / upgrade-docs write the same terse `SKILL.md` to `.claude/skills/verify/` (Claude) and `.agent-skills/verify/` (cross-vendor), and append a one-line pointer to `CLAUDE.md` / `AGENTS.md`. - The pointer is only appended when the file doesn't already mention `npm run verify` (idempotent); existing content is never rewritten (`ensurePointer`). Symlinks / non-regular files are refused. - Trim the shipped skill to ~50 words (assist-style density); the verbose command/skill copy is gone. - Dedupe writeManaged/ensurePointer boilerplate (createFile, assertRegularFile) and share the report ACTION_MARK, so verify:duplicate-code stays clean. - Dogfood: this repo now ships `.claude/skills/verify/SKILL.md`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/verify/SKILL.md | 6 ++++ CLAUDE.md | 2 +- README.md | 19 +++++----- src/checks/external.ts | 2 +- src/commands/registerInit.ts | 11 +++--- src/commands/registerUpgradeDocs.ts | 14 ++++---- src/scaffold/agentFiles.ts | 39 +++++++++++--------- src/scaffold/init.test.ts | 10 ++++-- src/scaffold/writeManaged.test.ts | 50 ++++++++++++++++++++++++-- src/scaffold/writeManaged.ts | 55 +++++++++++++++++++++++------ templates/commands/verify.md | 21 ----------- templates/skills/verify/SKILL.md | 31 ++-------------- templates/verify-guidance.md | 3 ++ 13 files changed, 158 insertions(+), 105 deletions(-) create mode 100644 .claude/skills/verify/SKILL.md delete mode 100644 templates/commands/verify.md create mode 100644 templates/verify-guidance.md diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 0000000..3d5e0f7 --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,6 @@ +--- +name: verify +description: Run this project's code verifications and fix what they report. Use after making code changes, or when asked to "verify", "run checks", or "run verify". +--- + +Run `npm run verify` (or `npx verifyx` — the CLI binary is `verifyx`, since `verify` is a Windows builtin). If it fails, fix every error and run again until it passes. Don't silence checks or game the metrics. diff --git a/CLAUDE.md b/CLAUDE.md index 822d9e6..9fced2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # @makerx/verify -A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verifyx` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verifyx init` / `verifyx upgrade-docs`) that drops the checks and agent commands into a project. +A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verifyx` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verifyx init` / `verifyx upgrade-docs`) that drops the checks, a `verify` skill (`.claude/skills` + `.agent-skills`), and a `CLAUDE.md`/`AGENTS.md` pointer into a project. **The CLI binary is `verifyx`, not `verify`.** `verify` is a Windows `cmd` builtin, and `npm run` bodies + `npx` resolve through `cmd` there, so a bare `verify` runs the builtin. `verifyx` (verify + fix) avoids that on all platforms. The npm **script** stays named `verify` (`npm run verify` is a script lookup, not command resolution) and the package stays `@makerx/verify`. Locally, `prepare` (`scripts/dev-verifyx-bin.mjs`) writes a `node_modules/.bin/verifyx` shim → `node src/cli.ts`, so the repo's own `verify:*` scripts call `verifyx ` exactly like a consumer's; `prepare` never runs for registry consumers. diff --git a/README.md b/README.md index 6d27513..591db35 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A growing collection of code **verifications** that give AI coding agents back-p `verify` ships both: - a **CLI** that orchestrates a set of checks by convention, and -- the **agent commands / skills** (`.claude/`, `.agent-skills/`) that steer AI assistants to run those checks and fix what they report. +- a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run those checks and fix what they report. Complexity was the first check. It is now just one of several, and the set grows over time. @@ -130,30 +130,33 @@ const timeoutMs = timeout * 1000 ### `verifyx init` -Interactively wire verifications and agent files into the current project: +Interactively wire verifications and the agent integration into the current project: ```sh verifyx init ``` -It lets you multi-select **checks** and **agent targets** (Claude `.claude/`, and/or cross-vendor `.agent-skills/`), then: +It lets you multi-select **checks** and **agent targets** (Claude and/or other agents), then: - writes the selected `verify:*` scripts to `package.json` (never clobbering existing ones), - installs the external checks' tools as `--save-dev`, -- emits the agent command/skill files. +- writes the **`verify` skill** — the same `SKILL.md` to `.claude/skills/verify/` (Claude) and `.agent-skills/verify/` (cross-vendor), so the integration is identical everywhere, +- appends a one-line pointer to `CLAUDE.md` / `AGENTS.md` (only if not already present; existing content is never rewritten). + +The skill auto-triggers on "verify"/"run checks", so agents run the checks proactively; the pointer reinforces it for tools that read `CLAUDE.md`/`AGENTS.md` as standing instructions. Options: -- `--defaults-only` — do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes agent files). +- `--defaults-only` — do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes the skill + pointer). - `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. ### `verifyx upgrade-docs` -Idempotently create/refresh the managed agent files (created / updated / unchanged; refuses to write through symlinks): +Idempotently create/refresh the skill and the `CLAUDE.md`/`AGENTS.md` pointer (created / appended / updated / unchanged; refuses to write through symlinks, never rewrites your instruction files): ```sh -verifyx upgrade-docs # both targets -verifyx upgrade-docs --no-agents # only .claude/ +verifyx upgrade-docs # Claude + other agents +verifyx upgrade-docs --no-agents # only .claude/ + CLAUDE.md ``` ## Configuration diff --git a/src/checks/external.ts b/src/checks/external.ts index a2e5d4e..f4d27b9 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -55,7 +55,7 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { scaffold: { script: `verifyx ${spec.name}`, devDeps: spec.devDeps }, async runDefault(): Promise { if (!hasLocalBin(spec.bin)) { - console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`verifyx init\`)`)) + console.log(color.dim(`${spec.name}: ${spec.bin} not installed — skipping (add it with \`npx verifyx init\`)`)) return { name: spec.name, ok: true, skipped: true } } if (spec.canRun && !spec.canRun()) { diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts index 4fd4fea..f47621a 100644 --- a/src/commands/registerInit.ts +++ b/src/commands/registerInit.ts @@ -4,6 +4,7 @@ import enquirer from 'enquirer' import { CHECKS, recommendedChecks } from '../checks/registry.ts' import type { AgentTarget } from '../scaffold/agentFiles.ts' import { applyInit, type InitResult } from '../scaffold/init.ts' +import { ACTION_MARK } from '../scaffold/writeManaged.ts' import { color } from '../shared/color.ts' import { runCommand } from '../shared/spawn.ts' @@ -40,22 +41,22 @@ async function resolveSelections(opts: InitCliOptions): Promise<{ checks: string CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.recommended })), ) const targets = (await multiselect('Select agent targets', [ - { name: 'claude', message: 'Claude (.claude/commands + skill)', enabled: true }, - { name: 'agents', message: 'Other agents (.agent-skills)', enabled: false }, + { name: 'claude', message: 'Claude (.claude/skills + CLAUDE.md)', enabled: true }, + { name: 'agents', message: 'Other agents (.agent-skills + AGENTS.md)', enabled: false }, ])) as AgentTarget[] return { checks, targets } } function report(result: InitResult, defaultsOnly: boolean): void { if (defaultsOnly) { - console.log(color.dim('\nDefaults-only: no verify:* scripts written — `verifyx` will run the built-in default set.')) + console.log(color.dim('\nDefaults-only: no verify:* scripts written — the `verify` script runs `verifyx all` (every built-in).')) } console.log(color.green(`\nScripts added: ${result.addedScripts.join(', ') || '(none new)'}`)) for (const file of result.agentFiles) { if (file.action === 'unchanged') continue - console.log(` ${file.action === 'created' ? '+' : '~'} ${file.path}`) + console.log(` ${ACTION_MARK[file.action]} ${file.path} (${file.action})`) } - console.log(color.dim('\nRun `verifyx` (or `npm run verify`) to run your verifications.')) + console.log(color.dim('\nRun `npm run verify` (or `npx verifyx`) to run your verifications.')) } /** `verifyx init` — interactively scaffold checks + agent files into the current project. */ diff --git a/src/commands/registerUpgradeDocs.ts b/src/commands/registerUpgradeDocs.ts index b53e393..5ea16ba 100644 --- a/src/commands/registerUpgradeDocs.ts +++ b/src/commands/registerUpgradeDocs.ts @@ -1,15 +1,15 @@ import type { Command } from 'commander' import { type AgentTarget, writeAgentFiles } from '../scaffold/agentFiles.ts' -import { summarise } from '../scaffold/writeManaged.ts' +import { ACTION_MARK, summarise } from '../scaffold/writeManaged.ts' -/** `verifyx upgrade-docs` — idempotently create/refresh the managed agent command + skill files. */ +/** `verifyx upgrade-docs` — refresh the verify skill and append the pointer to CLAUDE.md / AGENTS.md. */ export function registerUpgradeDocs(program: Command): void { program .command('upgrade-docs') - .description('Create or refresh managed agent command/skill files (.claude, .agent-skills)') - .option('--no-claude', 'skip .claude/ files') - .option('--no-agents', 'skip .agent-skills/ files') + .description('Create/refresh the verify skill and the pointer in CLAUDE.md / AGENTS.md') + .option('--no-claude', 'skip .claude/skills and CLAUDE.md') + .option('--no-agents', 'skip .agent-skills and AGENTS.md') .action((opts: { claude?: boolean; agents?: boolean }) => { const targets: AgentTarget[] = [] if (opts.claude !== false) targets.push('claude') @@ -18,9 +18,9 @@ export function registerUpgradeDocs(program: Command): void { const results = writeAgentFiles(process.cwd(), targets) for (const result of results) { if (result.action === 'unchanged') continue - console.log(` ${result.action === 'created' ? '+' : '~'} ${result.path}`) + console.log(` ${ACTION_MARK[result.action]} ${result.path} (${result.action})`) } const summary = summarise(results) - console.log(`\n${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged.`) + console.log(`\n${summary.created} created, ${summary.appended} appended, ${summary.updated} updated, ${summary.unchanged} unchanged.`) }) } diff --git a/src/scaffold/agentFiles.ts b/src/scaffold/agentFiles.ts index 8f4d9c6..15ab9e3 100644 --- a/src/scaffold/agentFiles.ts +++ b/src/scaffold/agentFiles.ts @@ -2,39 +2,44 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { type ManagedFileResult, writeManaged } from './writeManaged.ts' +import { ensurePointer, type ManagedFileResult, writeManaged } from './writeManaged.ts' export type AgentTarget = 'claude' | 'agents' -type ManagedFile = { template: string; dest: string } - const moduleDir = path.dirname(fileURLToPath(import.meta.url)) // src/scaffold/*.ts and dist/scaffold/*.mjs both sit two levels below the package root, where templates/ lives. const TEMPLATES_DIR = path.join(moduleDir, '..', '..', 'templates') +// A file already telling agents to run `npm run verify` is treated as covered — the pointer is not re-added. +const POINTER_MARKER = 'npm run verify' + function readTemplate(relativePath: string): string { return fs.readFileSync(path.join(TEMPLATES_DIR, ...relativePath.split('/')), 'utf-8') } -/** The managed agent files to emit for the chosen targets. Claude gets a slash command + skill; others get a skill. */ -function managedFilesFor(targets: readonly AgentTarget[]): ManagedFile[] { - const files: ManagedFile[] = [] +/** + * Emit the `verify` integration for the chosen targets under `cwd`: + * - the same `SKILL.md` goes to Claude (`.claude/skills/verify/`) and the cross-vendor tree + * (`.agent-skills/verify/`), so the integration is identical across agents; + * - a one-line pointer is appended to the matching instruction file (`CLAUDE.md` / `AGENTS.md`). + * + * Skills are CLI-owned (created/updated as a whole). The instruction files are user-owned, so the pointer is + * only appended when absent and existing content is never rewritten. + */ +export function writeAgentFiles(cwd: string, targets: readonly AgentTarget[]): ManagedFileResult[] { + const results: ManagedFileResult[] = [] + const skill = readTemplate('skills/verify/SKILL.md') + const guidance = readTemplate('verify-guidance.md') + if (targets.includes('claude')) { - files.push({ template: 'commands/verify.md', dest: '.claude/commands/verify.md' }) - files.push({ template: 'skills/verify/SKILL.md', dest: '.claude/skills/verify/SKILL.md' }) + writeManaged(path.join(cwd, '.claude', 'skills', 'verify', 'SKILL.md'), skill, results) + ensurePointer(path.join(cwd, 'CLAUDE.md'), guidance, POINTER_MARKER, results) } if (targets.includes('agents')) { - files.push({ template: 'skills/verify/SKILL.md', dest: '.agent-skills/verify/SKILL.md' }) + writeManaged(path.join(cwd, '.agent-skills', 'verify', 'SKILL.md'), skill, results) + ensurePointer(path.join(cwd, 'AGENTS.md'), guidance, POINTER_MARKER, results) } - return files -} -/** Write (idempotently) the managed agent files for the chosen targets under `cwd`. */ -export function writeAgentFiles(cwd: string, targets: readonly AgentTarget[]): ManagedFileResult[] { - const results: ManagedFileResult[] = [] - for (const file of managedFilesFor(targets)) { - writeManaged(path.join(cwd, ...file.dest.split('/')), readTemplate(file.template), results) - } results.sort((a, b) => a.path.localeCompare(b.path)) return results } diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts index 27a8582..6b21ce5 100644 --- a/src/scaffold/init.test.ts +++ b/src/scaffold/init.test.ts @@ -21,7 +21,7 @@ function readScripts(): Record { } describe('applyInit', () => { - it('writes verify:* scripts, agent files, and collects external devDeps', () => { + it('writes verify:* scripts, the verify skill, a CLAUDE.md pointer, and collects external devDeps', () => { const result = applyInit({ cwd: dir, checks: ['complexity', 'unused-code'], targets: ['claude'], defaultsOnly: false }) const scripts = readScripts() @@ -29,17 +29,21 @@ describe('applyInit', () => { expect(scripts['verify:unused-code']).toBe('verifyx unused-code') expect(scripts.verify).toBe('verifyx') expect(result.devDeps).toContain('knip') - expect(fs.existsSync(path.join(dir, '.claude', 'commands', 'verify.md'))).toBe(true) + // The skill (not a slash command) is the Claude integration. expect(fs.existsSync(path.join(dir, '.claude', 'skills', 'verify', 'SKILL.md'))).toBe(true) + expect(fs.existsSync(path.join(dir, '.claude', 'commands'))).toBe(false) + // CLAUDE.md is created with the verify pointer. + expect(fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8')).toContain('npm run verify') }) - it('defaults-only writes no verify:* scripts but keeps devDeps and agent files', () => { + it('defaults-only writes no verify:* scripts, points verify at `verifyx all`, and writes the agents skill + AGENTS.md', () => { const result = applyInit({ cwd: dir, checks: ['unused-code'], targets: ['agents'], defaultsOnly: true }) expect(Object.keys(readScripts())).toEqual(['verify']) expect(readScripts().verify).toBe('verifyx all') expect(result.devDeps).toContain('knip') expect(fs.existsSync(path.join(dir, '.agent-skills', 'verify', 'SKILL.md'))).toBe(true) + expect(fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8')).toContain('npm run verify') }) it('does not clobber an existing verify:* script', () => { diff --git a/src/scaffold/writeManaged.test.ts b/src/scaffold/writeManaged.test.ts index d2e5c32..028f4c4 100644 --- a/src/scaffold/writeManaged.test.ts +++ b/src/scaffold/writeManaged.test.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { type ManagedFileResult, summarise, writeManaged } from './writeManaged.ts' +import { ensurePointer, type ManagedFileResult, summarise, writeManaged } from './writeManaged.ts' let dir: string @@ -52,7 +52,53 @@ describe('writeManaged', () => { { path: 'a', action: 'created' }, { path: 'b', action: 'unchanged' }, { path: 'c', action: 'created' }, + { path: 'd', action: 'appended' }, ]), - ).toEqual({ created: 2, updated: 0, unchanged: 1 }) + ).toEqual({ created: 2, updated: 0, unchanged: 1, appended: 1 }) + }) +}) + +describe('ensurePointer', () => { + const block = '## Verification\n\nRun `/verify` and fix what it reports.\n' + + it('creates the file with the block when missing', () => { + const file = path.join(dir, 'CLAUDE.md') + const results: ManagedFileResult[] = [] + ensurePointer(file, block, '/verify', results) + expect(results[0]?.action).toBe('created') + expect(fs.readFileSync(file, 'utf-8')).toBe(block) + }) + + it('leaves the file untouched when the marker is already present', () => { + const file = path.join(dir, 'CLAUDE.md') + const original = '# My project\n\nSome guidance mentioning /verify already.\n' + fs.writeFileSync(file, original) + const results: ManagedFileResult[] = [] + ensurePointer(file, block, '/verify', results) + expect(results[0]?.action).toBe('unchanged') + expect(fs.readFileSync(file, 'utf-8')).toBe(original) + }) + + it('appends the block (keeping existing content) when the marker is absent', () => { + const file = path.join(dir, 'CLAUDE.md') + const original = '# My project\n\nExisting instructions.\n' + fs.writeFileSync(file, original) + const results: ManagedFileResult[] = [] + ensurePointer(file, block, '/verify', results) + expect(results[0]?.action).toBe('appended') + const after = fs.readFileSync(file, 'utf-8') + expect(after.startsWith(original)).toBe(true) + expect(after).toContain(block) + }) + + it('is idempotent — a second call after appending is a no-op', () => { + const file = path.join(dir, 'CLAUDE.md') + fs.writeFileSync(file, '# My project\n') + ensurePointer(file, block, '/verify', []) + const afterFirst = fs.readFileSync(file, 'utf-8') + const results: ManagedFileResult[] = [] + ensurePointer(file, block, '/verify', results) + expect(results[0]?.action).toBe('unchanged') + expect(fs.readFileSync(file, 'utf-8')).toBe(afterFirst) }) }) diff --git a/src/scaffold/writeManaged.ts b/src/scaffold/writeManaged.ts index c7fa24f..222fcaf 100644 --- a/src/scaffold/writeManaged.ts +++ b/src/scaffold/writeManaged.ts @@ -2,45 +2,78 @@ import fs from 'node:fs' import path from 'node:path' // Ported from https://github.com/MakerXStudio data-streams CLI upgrade-docs.ts writeManaged. -type ManagedAction = 'unchanged' | 'updated' | 'created' +type ManagedAction = 'unchanged' | 'updated' | 'created' | 'appended' export type ManagedFileResult = { path: string action: ManagedAction } +/** One-character prefix shown per action in scaffold reports. */ +export const ACTION_MARK: Record = { created: '+', appended: '»', updated: '~', unchanged: ' ' } + function readIfExists(file: string): string | null { if (!fs.existsSync(file)) return null return fs.readFileSync(file, 'utf-8') } +function createFile(file: string, contents: string, results: ManagedFileResult[]): void { + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, contents, { encoding: 'utf8', flag: 'wx' }) + results.push({ path: file, action: 'created' }) +} + +/** Guard before overwriting an existing path: never write through a symlink or over a non-regular file. */ +function assertRegularFile(file: string): void { + const stat = fs.lstatSync(file) + if (stat.isSymbolicLink()) throw new Error(`Refusing to write through symlink at ${file}`) + if (!stat.isFile()) throw new Error(`Refusing to write to non-regular file at ${file}`) +} + /** * Idempotently write a CLI-managed file: create if missing, rewrite if changed, leave alone if identical. * Refuses to write through a symlink or over a non-regular file. */ export function writeManaged(file: string, contents: string, results: ManagedFileResult[]): void { const existing = readIfExists(file) - if (existing === null) { - fs.mkdirSync(path.dirname(file), { recursive: true }) - fs.writeFileSync(file, contents, { encoding: 'utf8', flag: 'wx' }) - results.push({ path: file, action: 'created' }) - return - } + if (existing === null) return createFile(file, contents, results) if (existing === contents) { results.push({ path: file, action: 'unchanged' }) return } - const stat = fs.lstatSync(file) - if (stat.isSymbolicLink()) throw new Error(`Refusing to write through symlink at ${file}`) - if (!stat.isFile()) throw new Error(`Refusing to overwrite non-regular file at ${file}`) + assertRegularFile(file) fs.writeFileSync(file, contents, { encoding: 'utf8', flag: 'w' }) results.push({ path: file, action: 'updated' }) } -export function summarise(results: readonly ManagedFileResult[]): { created: number; updated: number; unchanged: number } { +/** + * Ensure a user-owned file (CLAUDE.md / AGENTS.md) contains a pointer block. Creates the file with the block + * when missing; appends the block when the file exists but does not already contain `marker`; otherwise leaves + * the file untouched. Never rewrites existing content. + */ +export function ensurePointer(file: string, block: string, marker: string, results: ManagedFileResult[]): void { + const existing = readIfExists(file) + if (existing === null) return createFile(file, block, results) + if (existing.includes(marker)) { + results.push({ path: file, action: 'unchanged' }) + return + } + assertRegularFile(file) + const separator = existing.endsWith('\n') ? '\n' : '\n\n' + fs.writeFileSync(file, existing + separator + block, { encoding: 'utf8', flag: 'w' }) + results.push({ path: file, action: 'appended' }) +} + +export function summarise(results: readonly ManagedFileResult[]): { + created: number + updated: number + unchanged: number + appended: number +} { return { created: results.filter((r) => r.action === 'created').length, updated: results.filter((r) => r.action === 'updated').length, unchanged: results.filter((r) => r.action === 'unchanged').length, + appended: results.filter((r) => r.action === 'appended').length, } } diff --git a/templates/commands/verify.md b/templates/commands/verify.md deleted file mode 100644 index 5c55085..0000000 --- a/templates/commands/verify.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -description: Run all verifications and fix everything they report ---- - -Run this project's verifications. Prefer the npm script, which works on every platform: - -``` -npm run verify -``` - -(or invoke the binary directly with `npx verifyx` — the CLI is named `verifyx`, not `verify`, because `verify` is a Windows `cmd` builtin; see the project README. Never use a global binary.) - -If anything fails: - -- Fix every reported issue, then run it again. Repeat until it passes cleanly. -- Do **not** silence failures with `--warn`, and do not delete or weaken checks to make them pass. -- Treat escape hatches (like a `context:` comment prefix) as a last resort — prefer making the code self-explanatory. - -The point of these checks is back-pressure: they exist to stop hard-to-maintain code from landing. Take them seriously. - -ARGUMENTS: $ARGUMENTS diff --git a/templates/skills/verify/SKILL.md b/templates/skills/verify/SKILL.md index 695a896..3d5e0f7 100644 --- a/templates/skills/verify/SKILL.md +++ b/templates/skills/verify/SKILL.md @@ -1,33 +1,6 @@ --- name: verify -description: Run this project's code verifications (@makerx/verify) and fix what they report. Use before considering any code change complete, when asked to "verify", "run checks", or "run verify". +description: Run this project's code verifications and fix what they report. Use after making code changes, or when asked to "verify", "run checks", or "run verify". --- -# verify - -This project uses [`@makerx/verify`](https://github.com/MakerXStudio/verify) — a collection of code -verifications that give AI coding agents back-pressure against writing hard-to-maintain code. - -## How to run - -Run the project's verifications via its npm script (works on every platform): - -``` -npm run verify -``` - -The CLI binary is `verifyx` (not `verify` — that name is a Windows `cmd` builtin). You can also invoke it -directly with `npx verifyx`. It's a pinned dev dependency — never a global install. - -- With no `verify:*` scripts defined, `verifyx` runs the built-in default checks in their default modes. -- With `verify:*` scripts defined, `verifyx` runs those in parallel; output is suppressed unless a check fails. -- Run a single built-in directly, e.g. `npx verifyx complexity` or `npx verifyx knip`. -- `npx verifyx list` shows every built-in check. - -## Working with failures - -1. Read each failure carefully — it explains what is wrong and how to fix it. -2. Fix the underlying issue. For complexity failures, **split the file**; do not game the metric by deleting - comments, joining lines, or shortening names. -3. Re-run until it passes. -4. Do not bypass checks with `--warn`, and reach for escape hatches (like `context:` comments) only as a last resort. +Run `npm run verify` (or `npx verifyx` — the CLI binary is `verifyx`, since `verify` is a Windows builtin). If it fails, fix every error and run again until it passes. Don't silence checks or game the metrics. diff --git a/templates/verify-guidance.md b/templates/verify-guidance.md new file mode 100644 index 0000000..e73171a --- /dev/null +++ b/templates/verify-guidance.md @@ -0,0 +1,3 @@ +## Verification + +After making code changes, run `npm run verify` and fix everything it reports before finishing. Don't silence checks or game the metrics. From 512c8eabd4d903766f48c321d0d36c718b05f96f Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 02:23:06 +0800 Subject: [PATCH 12/34] feat: name the tool + command + docs when an external check fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks are named for their function, which hides the underlying tool. On failure an external check now prints a one-line hint — the tool it used, the exact command it ran, and a docs link — so an agent can set up the tool's config (e.g. knip.json) without guessing. Emitted only on failure; passing runs stay silent to save tokens. - Add `docs` to ExternalCheckSpec and `externalFailureHint` (pure, tested); populate docs for oxlint/oxfmt/tsc/knip/skott/jscpd. - `verifyx all` override failures show the `npm run verify:` that ran (not the built-in tool). - Document the failure-hint + buffer-and-flush-on-failure behaviour in README. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 ++ src/checks/external.test.ts | 19 ++++++++++++++++++- src/checks/external.ts | 13 +++++++++++++ src/checks/registry.ts | 6 ++++++ src/orchestrator/runAll.ts | 2 ++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 591db35..6f3885e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ Flags on the bare `verifyx` command: External checks shell out to their tool and **skip gracefully when it is not installed** — `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. +Because checks are named for their function, **on failure** an external check prints the tool it used, the exact command it ran, and a docs link — so you (or an agent) can add the tool's config (e.g. `knip.json`) without guessing. On success it prints nothing (output is buffered and flushed only on failure, to keep runs quiet and cheap). If you override a check with your own `verify:` script, a failure shows that `npm run verify:` was what ran. + ### `complexity` ```sh diff --git a/src/checks/external.test.ts b/src/checks/external.test.ts index 74a2638..d0491d0 100644 --- a/src/checks/external.test.ts +++ b/src/checks/external.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { selectCommand } from './external.ts' +import { externalFailureHint, selectCommand } from './external.ts' const fixable = { checkCommand: 'oxfmt --check .', fixCommand: 'oxfmt .' } const notFixable = { checkCommand: 'tsc --noEmit' } @@ -19,3 +19,20 @@ describe('selectCommand', () => { expect(selectCommand(notFixable, 'check')).toBe('tsc --noEmit') }) }) + +describe('externalFailureHint', () => { + it('names the tool, the exact command that ran, and the docs link', () => { + const hint = externalFailureHint({ name: 'unused-code', bin: 'knip', docs: 'https://knip.dev' }, 'knip --no-progress') + expect(hint).toContain('unused-code') + expect(hint).toContain('knip') + expect(hint).toContain('knip --no-progress') + expect(hint).toContain('https://knip.dev') + }) + + it('still names the tool and command when no docs link is set', () => { + const hint = externalFailureHint({ name: 'circular-deps', bin: 'skott' }, 'skott src') + expect(hint).toContain('skott') + expect(hint).toContain('skott src') + expect(hint).not.toContain('undefined') + }) +}) diff --git a/src/checks/external.ts b/src/checks/external.ts index f4d27b9..c0938d6 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -35,6 +35,8 @@ export type ExternalCheckSpec = { fixCommand?: string devDeps: string[] recommended?: boolean + /** Docs / config reference for the underlying tool, surfaced when the check fails. */ + docs?: string /** Extra guard beyond bin presence (e.g. require a tsconfig). */ canRun?: () => boolean } @@ -44,6 +46,15 @@ export function selectCommand(spec: Pick, command: string): string { + return `↳ ${spec.name} uses ${spec.bin}: ran \`${command}\`. Configure ${spec.bin}${spec.docs ? ` — ${spec.docs}` : ''}.` +} + /** Build a Check that shells out to an external tool, skipping gracefully when the tool cannot run. */ export function defineExternalCheck(spec: ExternalCheckSpec): Check { return { @@ -64,6 +75,8 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { } const command = selectCommand(spec, resolveMode()) const code = await runCommand(command, { env: envWithLocalBin() }) + // Only on failure — passing runs stay silent to save tokens. + if (code !== 0) console.error(color.dim(externalFailureHint(spec, command))) return { name: spec.name, ok: code === 0 } }, } diff --git a/src/checks/registry.ts b/src/checks/registry.ts index 799fd37..5402b65 100644 --- a/src/checks/registry.ts +++ b/src/checks/registry.ts @@ -35,6 +35,7 @@ export const CHECKS: Check[] = [ fixCommand: 'oxlint --fix .', devDeps: ['oxlint'], recommended: true, + docs: 'https://oxc.rs/docs/guide/usage/linter.html', }), defineExternalCheck({ name: 'format', @@ -44,6 +45,7 @@ export const CHECKS: Check[] = [ fixCommand: 'oxfmt .', devDeps: ['oxfmt'], recommended: true, + docs: 'https://oxc.rs', }), defineExternalCheck({ name: 'check-types', @@ -53,6 +55,7 @@ export const CHECKS: Check[] = [ devDeps: ['typescript'], canRun: () => fs.existsSync('tsconfig.json'), recommended: true, + docs: 'https://www.typescriptlang.org/tsconfig', }), defineExternalCheck({ name: 'unused-code', @@ -60,6 +63,7 @@ export const CHECKS: Check[] = [ bin: 'knip', checkCommand: 'knip --no-progress --treat-config-hints-as-errors', devDeps: ['knip'], + docs: 'https://knip.dev/reference/configuration', }), defineExternalCheck({ name: 'circular-deps', @@ -67,6 +71,7 @@ export const CHECKS: Check[] = [ bin: 'skott', checkCommand: 'skott --displayMode=raw --showCircularDependencies --exitCodeOnCircularDependencies=1', devDeps: ['skott'], + docs: 'https://github.com/antoine-coulon/skott', }), defineExternalCheck({ name: 'duplicate-code', @@ -74,6 +79,7 @@ export const CHECKS: Check[] = [ bin: 'jscpd', checkCommand: 'jscpd --format typescript,tsx --exit-code 1 --ignore "**/*.test.*" -r consoleFull src', devDeps: ['jscpd'], + docs: 'https://github.com/kucherenko/jscpd/tree/master/apps/jscpd#config', }), ] diff --git a/src/orchestrator/runAll.ts b/src/orchestrator/runAll.ts index 28f2a4d..58a19d8 100644 --- a/src/orchestrator/runAll.ts +++ b/src/orchestrator/runAll.ts @@ -31,6 +31,8 @@ export async function runAll(opts: RunAllOptions = {}): Promise { console.log(color.heading(`▶ ${check.name}`)) const override = resolveOverride(entries, check.name, mode) const ok = override ? (await runCommand(override.command, { cwd: override.cwd })) === 0 : (await check.runDefault()).ok + // On an override failure, show the script that actually ran (only on failure — passing runs stay silent). + if (override && !ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) results.push({ name: check.name, ok }) records.push({ script: check.name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) console.log() From 89b2c3b2793a96e9f9e959fd5c48632be6c02d96 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 02:35:44 +0800 Subject: [PATCH 13/34] =?UTF-8?q?feat:=20silent=20on=20success=20=E2=80=94?= =?UTF-8?q?=20gate=20orchestrator=20chatter=20behind=20--verbose/--measure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A clean run now prints nothing (just exit 0), instead of a "Running N…" preamble and an "All passed" footer. Failures are always reported. - Extract a shared `report.ts` (`chatty`, `reportOutcomes`) used by both bare `verifyx` and `verifyx all`, so the preamble + success footer only print when `--verbose` or `--measure` is set. (Also dedups the reporter across the two orchestrators.) - External checks now buffer their tool output and flush only on failure (streamed under --verbose), so `verifyx all` isn't noisy on success either. - README: document that a clean run is silent. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- src/checks/external.ts | 3 +- src/orchestrator/report.test.ts | 73 +++++++++++++++++++++++++++++++++ src/orchestrator/report.ts | 26 ++++++++++++ src/orchestrator/run.ts | 30 ++++++-------- src/orchestrator/runAll.ts | 27 ++++++------ src/shared/spawn.ts | 4 ++ 7 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 src/orchestrator/report.test.ts create mode 100644 src/orchestrator/report.ts diff --git a/README.md b/README.md index 6f3885e..e5aab6c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ npx verifyx init Your project's `verify:*` scripts **are** the gate, and `verifyx` runs exactly what you define — nothing implicit: -- **`verifyx`** (no subcommand) runs your `verify:*` scripts in parallel. Output from each is buffered and shown **only if it fails**, keeping passing runs quiet (and quieter still under Claude Code); add `--verbose` to stream everything. With **no `verify:*` scripts, nothing runs.** +- **`verifyx`** (no subcommand) runs your `verify:*` scripts in parallel. A clean run is **silent** — no preamble, no per-script output, just exit `0` — so it's cheap to run in a loop or an agent. Each script's output is buffered and shown **only if it fails**. Add `--verbose` to stream everything, or `--measure` for a status/duration table. With **no `verify:*` scripts, nothing runs.** - **`verifyx all`** runs **every built-in check** (the explicit "run everything"). A `verify:` script **overrides** its matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. **Fix/check overrides.** Alongside a `verify:` (check-mode) script you can define a `verify::fix` variant. `verifyx` runs the `:fix` variant in fix mode (locally) and the base in check mode (CI), and never both — so an override with a non-mode-aware tool still fixes locally and only checks in CI: diff --git a/src/checks/external.ts b/src/checks/external.ts index c0938d6..8a6c1ca 100644 --- a/src/checks/external.ts +++ b/src/checks/external.ts @@ -74,7 +74,8 @@ export function defineExternalCheck(spec: ExternalCheckSpec): Check { return { name: spec.name, ok: true, skipped: true } } const command = selectCommand(spec, resolveMode()) - const code = await runCommand(command, { env: envWithLocalBin() }) + // quiet: buffer the tool's output and flush only on failure (streamed live under --verbose). + const code = await runCommand(command, { env: envWithLocalBin(), quiet: true }) // Only on failure — passing runs stay silent to save tokens. if (code !== 0) console.error(color.dim(externalFailureHint(spec, command))) return { name: spec.name, ok: code === 0 } diff --git a/src/orchestrator/report.test.ts b/src/orchestrator/report.test.ts new file mode 100644 index 0000000..398e5b3 --- /dev/null +++ b/src/orchestrator/report.test.ts @@ -0,0 +1,73 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { setVerbose } from '../shared/spawn.ts' +import { chatty, reportOutcomes } from './report.ts' + +let log: ReturnType +let err: ReturnType + +beforeEach(() => { + log = vi.spyOn(console, 'log').mockImplementation(() => {}) + err = vi.spyOn(console, 'error').mockImplementation(() => {}) +}) +afterEach(() => { + log.mockRestore() + err.mockRestore() + setVerbose(false) + delete process.env.CI + delete process.env.VERIFY_MODE +}) + +describe('chatty', () => { + it('is quiet by default (a clean run prints nothing)', () => { + setVerbose(false) + expect(chatty(false)).toBe(false) + }) + + it('is loud when --measure is set', () => { + setVerbose(false) + expect(chatty(true)).toBe(true) + }) + + it('is loud when verbose is set', () => { + setVerbose(true) + expect(chatty(false)).toBe(true) + }) +}) + +describe('reportOutcomes', () => { + const passing = [ + { name: 'lint', ok: true }, + { name: 'complexity', ok: true }, + ] + + it('is silent and returns 0 on success when not chatty', () => { + const code = reportOutcomes(passing, 'verification', false) + expect(code).toBe(0) + expect(log).not.toHaveBeenCalled() + expect(err).not.toHaveBeenCalled() + }) + + it('prints the pass line and returns 0 on success when chatty', () => { + const code = reportOutcomes(passing, 'verification', true) + expect(code).toBe(0) + expect(log).toHaveBeenCalledTimes(1) + expect(log.mock.calls[0]?.[0]).toContain('All 2 verification(s) passed') + }) + + it('always reports failures (and returns 1) regardless of chatty', () => { + const outcomes = [ + { name: 'lint', ok: true }, + { name: 'unused-code', ok: false }, + { name: 'circular-deps', ok: false }, + ] + const code = reportOutcomes(outcomes, 'verification', false) + expect(code).toBe(1) + expect(err).toHaveBeenCalledTimes(1) + const message = String(err.mock.calls[0]?.[0]) + expect(message).toContain('2 verification(s) failed') + expect(message).toContain('unused-code') + expect(message).toContain('circular-deps') + expect(log).not.toHaveBeenCalled() + }) +}) diff --git a/src/orchestrator/report.ts b/src/orchestrator/report.ts new file mode 100644 index 0000000..bce74a0 --- /dev/null +++ b/src/orchestrator/report.ts @@ -0,0 +1,26 @@ +import { color } from '../shared/color.ts' +import { isVerbose } from '../shared/spawn.ts' + +/** + * Whether to print the preamble + success footer. A clean run is otherwise silent (just exit 0) to save + * tokens; chatter is shown only when streaming (`--verbose`) or measuring (`--measure`). + */ +export function chatty(measure?: boolean): boolean { + return isVerbose() || !!measure +} + +export type RunOutcome = { name: string; ok: boolean } + +/** + * Report a batch of outcomes: failures are always printed; the "all passed" line only when `showPass`. + * Returns the process exit code (1 if anything failed, else 0). + */ +export function reportOutcomes(outcomes: readonly RunOutcome[], noun: string, showPass: boolean): number { + const failed = outcomes.filter((outcome) => !outcome.ok) + if (failed.length > 0) { + console.error(color.red(`\n${failed.length} ${noun}(s) failed: ${failed.map((f) => f.name).join(', ')}`)) + return 1 + } + if (showPass) console.log(color.green(`\nAll ${outcomes.length} ${noun}(s) passed`)) + return 0 +} diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index 09995d0..019329f 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -1,8 +1,8 @@ -import { color } from '../shared/color.ts' import { configureMode, resolveMode } from '../shared/mode.ts' import { runCommand, setVerbose } from '../shared/spawn.ts' import { filterByChangedFiles } from './filterByChangedFiles.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' +import { chatty, reportOutcomes } from './report.ts' import { resolveEntries, selectEntries, type VerifyEntry } from './resolveEntries.ts' export type OrchestrateOptions = { @@ -20,21 +20,10 @@ async function runEntry(entry: VerifyEntry): Promise { return { script: entry.name, code, durationMs: Date.now() - startTime } } -function reportScriptResults(records: readonly MeasureRecord[], total: number): number { - const failed = records.filter((record) => record.code !== 0) - if (failed.length > 0) { - console.error(color.red(`\n${failed.length} script(s) failed:`)) - for (const record of failed) console.error(` - ${record.script} (exit code ${record.code})`) - return 1 - } - console.log(color.green(`\nAll ${total} verify script(s) passed`)) - return 0 -} - /** * The default `verifyx` action. Convention: run the project's own `verify:*` scripts in parallel (output - * buffered, flushed only on failure). With no `verify:*` scripts, nothing runs — use `verifyx all` to run - * every built-in check. + * buffered, flushed only on failure). A clean run is silent — use `--verbose` or `--measure` for detail. + * With no `verify:*` scripts, nothing runs — use `verifyx all` to run every built-in check. */ export async function orchestrate(opts: OrchestrateOptions = {}): Promise { setVerbose(!!opts.verbose) @@ -55,11 +44,18 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise ({ name: r.script, ok: r.code === 0 })), + 'verify script', + loud, + ) } diff --git a/src/orchestrator/runAll.ts b/src/orchestrator/runAll.ts index 58a19d8..140deea 100644 --- a/src/orchestrator/runAll.ts +++ b/src/orchestrator/runAll.ts @@ -4,6 +4,7 @@ import { color } from '../shared/color.ts' import { resolveMode } from '../shared/mode.ts' import { runCommand } from '../shared/spawn.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' +import { chatty, reportOutcomes } from './report.ts' import { resolveEntries, resolveOverride } from './resolveEntries.ts' export type RunAllOptions = { measure?: boolean } @@ -11,40 +12,36 @@ export type RunAllOptions = { measure?: boolean } /** * Run every built-in check (the explicit `verifyx all` opt-in). A check runs in-process unless the project * defines a matching `verify:` script (or `verify::fix` in fix mode), in which case that script - * runs instead (per-check override). + * runs instead (per-check override). A clean run is quiet — use `--verbose` / `--measure` for the roll-call. */ export async function runAll(opts: RunAllOptions = {}): Promise { const entries = resolveEntries() const mode = resolveMode() + const loud = chatty(opts.measure) - console.log(`Running all ${CHECKS.length} built-in verification(s):`) - for (const check of CHECKS) { - console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) + if (loud) { + console.log(`Running all ${CHECKS.length} built-in verification(s):`) + for (const check of CHECKS) { + console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) + } + console.log() } - console.log() const startTime = Date.now() const records: MeasureRecord[] = [] const results: CheckResult[] = [] for (const check of CHECKS) { const checkStart = Date.now() - console.log(color.heading(`▶ ${check.name}`)) + if (loud) console.log(color.heading(`▶ ${check.name}`)) const override = resolveOverride(entries, check.name, mode) const ok = override ? (await runCommand(override.command, { cwd: override.cwd })) === 0 : (await check.runDefault()).ok // On an override failure, show the script that actually ran (only on failure — passing runs stay silent). if (override && !ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) results.push({ name: check.name, ok }) records.push({ script: check.name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) - console.log() + if (loud) console.log() } if (opts.measure) printMeasureTable(records, Date.now() - startTime) - - const failed = results.filter((result) => !result.ok) - if (failed.length > 0) { - console.error(color.red(`\n${failed.length} verification(s) failed: ${failed.map((f) => f.name).join(', ')}`)) - return 1 - } - console.log(color.green(`\nAll ${CHECKS.length} verification(s) passed`)) - return 0 + return reportOutcomes(results, 'verification', loud) } diff --git a/src/shared/spawn.ts b/src/shared/spawn.ts index a7c8fcf..0fc75b0 100644 --- a/src/shared/spawn.ts +++ b/src/shared/spawn.ts @@ -7,6 +7,10 @@ export function setVerbose(verbose: boolean): void { verboseMode = verbose } +export function isVerbose(): boolean { + return verboseMode +} + function shouldSuppress(quiet?: boolean): boolean { if (verboseMode) return false return !!quiet || !!process.env.CLAUDECODE From aa212a06cb937e4ac1e1a937317da1273e684ca4 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 02:48:43 +0800 Subject: [PATCH 14/34] refactor: drop the diff-filter feature (unused assist port) The per-script diff filter (ported from assist) was off by default, required hand-written `verify.filters` globs no one set, and added the confusing `--no-filter` flag. Our checks scan the whole tree anyway, so skip-unless-a- matching-file-changed was low value. Remove it entirely: - delete filterByChangedFiles + getChangedFiles; drop the `filter` field on VerifyEntry and the `verify.filters` config parsing. - remove the `--no-filter` flag; bare `verifyx` now simply runs your verify:* scripts (via selectEntries for :fix pairing). - drop the filter docs from the README. - dedupe the shared run options (--measure/--verbose/--check/--fix) across the root and `all` commands so duplicate-code stays clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 -- src/cli.ts | 54 +++++++++++++----------- src/orchestrator/filterByChangedFiles.ts | 17 -------- src/orchestrator/resolveEntries.test.ts | 8 ---- src/orchestrator/resolveEntries.ts | 9 +--- src/orchestrator/run.ts | 8 +--- src/shared/git.ts | 10 ----- 7 files changed, 33 insertions(+), 77 deletions(-) delete mode 100644 src/orchestrator/filterByChangedFiles.ts diff --git a/README.md b/README.md index e5aab6c..b858157 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ Flags on the bare `verifyx` command: - `--check` / `--fix` — force check-only or auto-fix (defaults: fix locally, check under CI). - `--measure` — print a status/duration summary table. -- `--all` — run every `verify:*` script, ignoring diff-based filters. - `--verbose` — stream all output instead of suppressing passing runs. ## Built-in checks @@ -171,13 +170,10 @@ Some checks read per-repo config from `verify.config.json`, or a `verify` key in "blockComments": { "ignore": ["**/*.generated.ts"] }, "hardcodedColors": { "root": "src", "ignore": ["**/tokens.ts"] }, "forbiddenStrings": [{ "file": "app.json", "paths": ["env.LOG_LEVEL"], "disallowed": "debug" }], - "filters": { "verify:web": "web/**" }, }, } ``` -`filters` scopes a `verify:*` script to a diff glob: it is skipped unless a changed file matches (bypass with `verifyx --all`). - ## CI/CD Because it is a pinned dev dependency, CI runs the identical tool: diff --git a/src/cli.ts b/src/cli.ts index 4a9a46b..01b2924 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,33 +15,37 @@ import { setVerbose } from './shared/spawn.ts' const require = createRequire(import.meta.url) const pkg = require('../package.json') as { version: string } +type RunOptions = { measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean } + +/** The options shared by the default run and `verifyx all`. */ +function withRunOptions(command: Command): Command { + return command + .option('--measure', "print a summary table of each verification's status and duration") + .option('--verbose', 'stream all output instead of suppressing passing runs') + .option('--check', 'check only — never auto-fix (the default under CI)') + .option('--fix', 'auto-fix where possible (the default locally)') +} + const program = new Command() -program - .name('verifyx') - .description('A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code.') - .version(pkg.version) - .option('--no-filter', 'run every verify:* script, ignoring diff-based filters') - .option('--measure', "print a summary table of each verification's status and duration") - .option('--verbose', 'stream all output instead of suppressing passing runs') - .option('--check', 'check only — never auto-fix (the default under CI)') - .option('--fix', 'auto-fix where possible (the default locally)') - .action(async (opts: { filter?: boolean; measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { - process.exitCode = await orchestrate(opts) - }) - -program - .command('all') - .description('Run every built-in check (verify: scripts override the matching built-in)') - .option('--measure', "print a summary table of each verification's status and duration") - .option('--verbose', 'stream all output instead of suppressing passing runs') - .option('--check', 'check only — never auto-fix (the default under CI)') - .option('--fix', 'auto-fix where possible (the default locally)') - .action(async (opts: { measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean }) => { - setVerbose(!!opts.verbose) - configureMode(opts) - process.exitCode = await runAll({ measure: opts.measure }) - }) +withRunOptions( + program + .name('verifyx') + .description( + 'A growing collection of code verifications that give AI coding agents back-pressure against writing hard-to-maintain code.', + ) + .version(pkg.version), +).action(async (opts: RunOptions) => { + process.exitCode = await orchestrate(opts) +}) + +withRunOptions( + program.command('all').description('Run every built-in check (verify: scripts override the matching built-in)'), +).action(async (opts: RunOptions) => { + setVerbose(!!opts.verbose) + configureMode(opts) + process.exitCode = await runAll({ measure: opts.measure }) +}) registerChecks(program) registerList(program) diff --git a/src/orchestrator/filterByChangedFiles.ts b/src/orchestrator/filterByChangedFiles.ts deleted file mode 100644 index a004cb3..0000000 --- a/src/orchestrator/filterByChangedFiles.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { minimatch } from 'minimatch' - -import { getChangedFiles } from '../shared/git.ts' -import type { VerifyEntry } from './resolveEntries.ts' - -// Ported from https://github.com/staff0rd/assist verify/run/filterByChangedFiles.ts -/** Keep entries with no filter; drop filtered entries whose glob matches nothing in the working-tree diff. */ -export function filterByChangedFiles(entries: readonly VerifyEntry[]): VerifyEntry[] { - if (!entries.some((entry) => entry.filter)) return [...entries] - - const changedFiles = getChangedFiles() - return entries.filter((entry) => { - if (!entry.filter) return true - if (changedFiles.length === 0) return false - return changedFiles.some((file) => minimatch(file, entry.filter as string)) - }) -} diff --git a/src/orchestrator/resolveEntries.test.ts b/src/orchestrator/resolveEntries.test.ts index 7bafe5b..d723a4c 100644 --- a/src/orchestrator/resolveEntries.test.ts +++ b/src/orchestrator/resolveEntries.test.ts @@ -31,14 +31,6 @@ describe('resolveEntries', () => { fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { build: 'x' } })) expect(resolveEntries(dir)).toEqual([]) }) - - it('attaches diff filters from the verify config', () => { - fs.writeFileSync( - path.join(dir, 'package.json'), - JSON.stringify({ scripts: { 'verify:web': 'x' }, verify: { filters: { 'verify:web': 'web/**' } } }), - ) - expect(resolveEntries(dir).find((e) => e.name === 'verify:web')?.filter).toBe('web/**') - }) }) describe('entryCheckName', () => { diff --git a/src/orchestrator/resolveEntries.ts b/src/orchestrator/resolveEntries.ts index 2544b11..26c995a 100644 --- a/src/orchestrator/resolveEntries.ts +++ b/src/orchestrator/resolveEntries.ts @@ -7,7 +7,6 @@ export type VerifyEntry = { name: string command: string cwd: string - filter?: string } const VERIFY_PREFIX = 'verify:' @@ -31,10 +30,7 @@ function findPackageJson(startDir: string): string | null { } } -type PackageJson = { - scripts?: Record - verify?: { filters?: Record } -} +type PackageJson = { scripts?: Record } /** Collect the project's `verify:*` npm scripts as parallelisable entries, nearest package.json wins. */ export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { @@ -47,11 +43,10 @@ export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { return [] } const scripts = pkg.scripts ?? {} - const filters = pkg.verify?.filters ?? {} const dir = path.dirname(pkgPath) return Object.keys(scripts) .filter((name) => name.startsWith(VERIFY_PREFIX)) - .map((name) => ({ name, command: `npm run ${name}`, cwd: dir, filter: filters[name] })) + .map((name) => ({ name, command: `npm run ${name}`, cwd: dir })) } /** diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index 019329f..c15606e 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -1,13 +1,10 @@ import { configureMode, resolveMode } from '../shared/mode.ts' import { runCommand, setVerbose } from '../shared/spawn.ts' -import { filterByChangedFiles } from './filterByChangedFiles.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' import { chatty, reportOutcomes } from './report.ts' import { resolveEntries, selectEntries, type VerifyEntry } from './resolveEntries.ts' export type OrchestrateOptions = { - /** When false (via --no-filter), run every verify:* script ignoring diff-based filters. */ - filter?: boolean measure?: boolean verbose?: boolean check?: boolean @@ -37,10 +34,9 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise / verify::fix pairs to the variant for the current mode. - const modeEntries = selectEntries(allEntries, resolveMode()) - const entries = opts.filter === false ? modeEntries : filterByChangedFiles(modeEntries) + const entries = selectEntries(allEntries, resolveMode()) if (entries.length === 0) { - console.log('No verify scripts matched changed files — skipping') + console.log('No verify:* scripts to run for this mode.') return 0 } diff --git a/src/shared/git.ts b/src/shared/git.ts index 1ef1c14..3e7798b 100644 --- a/src/shared/git.ts +++ b/src/shared/git.ts @@ -8,13 +8,3 @@ export function gitDiffHead(): string { return '' } } - -/** Files changed against HEAD (staged + unstaged). Empty when git is unavailable or nothing changed. */ -export function getChangedFiles(): string[] { - try { - const output = execSync('git diff --name-only HEAD', { encoding: 'utf8' }).trim() - return output === '' ? [] : output.split(/\r?\n/) - } catch { - return [] - } -} From 347404e27f9666e455db04b5c4ffd41b2208f481 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 02:51:57 +0800 Subject: [PATCH 15/34] =?UTF-8?q?chore:=20drop=20redundant=20lint/format/c?= =?UTF-8?q?heck-types=20scripts=20=E2=80=94=20verifyx=20owns=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the standalone `check-types`, `lint`, `lint:fix`, `format`, `format:check` scripts. They duplicated the `verify:*` scripts (`verifyx lint`, `verifyx format`, `verifyx check-types`); ad-hoc use is `verifyx ` with --fix/--check. Deleting `check-types` also drops CI's default `npm run check-types --if-present` to a no-op, removing the double tsc run (verify already type-checks via verify:check-types). Add oxlint + oxfmt to knip.json ignoreDependencies: with the scripts gone they are only invoked by verifyx at runtime (via node_modules/.bin), which knip can't see — the same reason jscpd/skott are already ignored. The list is now the honest set of tools verifyx shells out to. Every script now routes through verifyx. Co-Authored-By: Claude Opus 4.8 (1M context) --- knip.json | 2 +- package.json | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/knip.json b/knip.json index 039e5a9..0e2c32e 100644 --- a/knip.json +++ b/knip.json @@ -2,5 +2,5 @@ "$schema": "https://unpkg.com/knip@6/schema.json", "project": ["src/**/*.ts"], "ignoreBinaries": ["verifyx"], - "ignoreDependencies": ["jscpd", "skott"] + "ignoreDependencies": ["oxlint", "oxfmt", "jscpd", "skott"] } diff --git a/package.json b/package.json index eba771a..981bd18 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,6 @@ "build": "run-s clean build:rollup", "clean": "rimraf dist", "build:rollup": "rollup --config rollup.config.mjs", - "check-types": "tsc --noEmit", - "lint": "oxlint", - "lint:fix": "oxlint --fix", - "format": "oxfmt .", - "format:check": "oxfmt --check .", "prepare": "node scripts/dev-verifyx-bin.mjs", "verify": "verifyx", "verify:lint": "verifyx lint", From 5618ad927ea6377d3185ce57b51b1344da249997 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:03:53 +0800 Subject: [PATCH 16/34] feat: init teaches knip to ignore the tools verifyx runs (no consumer setup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `unused-code` (knip) is selected, `verifyx init` now ensures the other external tools verifyx invokes at runtime (oxlint/oxfmt/skott/jscpd) are in knip's ignoreDependencies — knip can't see runtime node_modules/.bin calls and would otherwise flag them as unused. So consumers get it for free instead of hand-editing knip config. - ensureKnipIgnores: merge into knip.json or package.json#knip (create knip.json if neither exists), adding only missing entries, never rewriting other content; code-based knip.ts/js configs are left untouched. Idempotent. - Wired into applyInit (only when unused-code is selected; ignores the other selected external checks' devDeps, minus typescript). - Tests for create/merge/unchanged/package.json/code-config + the init wiring. - README documents it. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 +- src/scaffold/init.test.ts | 12 ++++++ src/scaffold/init.ts | 14 +++++++ src/scaffold/knipConfig.test.ts | 71 +++++++++++++++++++++++++++++++++ src/scaffold/knipConfig.ts | 61 ++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/scaffold/knipConfig.test.ts create mode 100644 src/scaffold/knipConfig.ts diff --git a/README.md b/README.md index b858157..08eb75f 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,8 @@ It lets you multi-select **checks** and **agent targets** (Claude and/or other a - writes the selected `verify:*` scripts to `package.json` (never clobbering existing ones), - installs the external checks' tools as `--save-dev`, - writes the **`verify` skill** — the same `SKILL.md` to `.claude/skills/verify/` (Claude) and `.agent-skills/verify/` (cross-vendor), so the integration is identical everywhere, -- appends a one-line pointer to `CLAUDE.md` / `AGENTS.md` (only if not already present; existing content is never rewritten). +- appends a one-line pointer to `CLAUDE.md` / `AGENTS.md` (only if not already present; existing content is never rewritten), +- if `unused-code` is selected, adds the other external tools (`oxlint`/`oxfmt`/`skott`/`jscpd`) to knip's `ignoreDependencies` — verifyx runs them at runtime, so knip can't see them and would otherwise report them as unused. Merged into `knip.json` or `package.json#knip` (created if neither exists), adding only what's missing; a code-based `knip.ts`/`knip.js` is left for you to edit. The skill auto-triggers on "verify"/"run checks", so agents run the checks proactively; the pointer reinforces it for tools that read `CLAUDE.md`/`AGENTS.md` as standing instructions. diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts index 6b21ce5..1785210 100644 --- a/src/scaffold/init.test.ts +++ b/src/scaffold/init.test.ts @@ -51,4 +51,16 @@ describe('applyInit', () => { applyInit({ cwd: dir, checks: ['complexity'], targets: [], defaultsOnly: false }) expect(readScripts()['verify:complexity']).toBe('custom') }) + + it('teaches knip to ignore the external tools verifyx runs when unused-code is selected', () => { + applyInit({ cwd: dir, checks: ['unused-code', 'lint', 'format'], targets: [], defaultsOnly: false }) + const knip = JSON.parse(fs.readFileSync(path.join(dir, 'knip.json'), 'utf-8')) as { ignoreDependencies?: string[] } + expect(knip.ignoreDependencies).toEqual(expect.arrayContaining(['oxlint', 'oxfmt'])) + expect(knip.ignoreDependencies).not.toContain('knip') // the runner isn't flagged + }) + + it('does not create a knip config when unused-code is not selected', () => { + applyInit({ cwd: dir, checks: ['lint', 'format'], targets: [], defaultsOnly: false }) + expect(fs.existsSync(path.join(dir, 'knip.json'))).toBe(false) + }) }) diff --git a/src/scaffold/init.ts b/src/scaffold/init.ts index 871611b..8b2c496 100644 --- a/src/scaffold/init.ts +++ b/src/scaffold/init.ts @@ -1,7 +1,9 @@ import path from 'node:path' import { getCheck } from '../checks/registry.ts' +import type { Check } from '../checks/types.ts' import { type AgentTarget, writeAgentFiles } from './agentFiles.ts' +import { ensureKnipIgnores } from './knipConfig.ts' import { addVerifyScripts } from './packageScripts.ts' import type { ManagedFileResult } from './writeManaged.ts' @@ -35,6 +37,18 @@ export function applyInit(opts: InitOptions): InitResult { // Defaults-only wires the top `verify` script to `verifyx all` so it runs every built-in with no verify:* list. const addedScripts = addVerifyScripts(path.join(opts.cwd, 'package.json'), scripts, opts.defaultsOnly ? 'verifyx all' : 'verifyx') + const agentFiles = writeAgentFiles(opts.cwd, opts.targets) + + // context: with unused-code selected, teach knip to ignore the other external tools verifyx runs at runtime. + if (opts.checks.includes('unused-code')) { + const toolDeps = opts.checks + .map(getCheck) + .filter((check): check is Check => !!check && check.kind === 'external' && check.name !== 'unused-code') + .flatMap((check) => check.scaffold.devDeps ?? []) + .filter((dep) => dep !== 'typescript') + ensureKnipIgnores(opts.cwd, [...new Set(toolDeps)], agentFiles) + } + return { addedScripts, devDeps: [...new Set(devDeps)], agentFiles } } diff --git a/src/scaffold/knipConfig.test.ts b/src/scaffold/knipConfig.test.ts new file mode 100644 index 0000000..1089249 --- /dev/null +++ b/src/scaffold/knipConfig.test.ts @@ -0,0 +1,71 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { ensureKnipIgnores } from './knipConfig.ts' +import type { ManagedFileResult } from './writeManaged.ts' + +let dir: string + +beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-knip-')) + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'scratch' })) +}) +afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) +}) + +const readKnip = () => JSON.parse(fs.readFileSync(path.join(dir, 'knip.json'), 'utf-8')) as { ignoreDependencies?: string[] } + +describe('ensureKnipIgnores', () => { + it('does nothing when there are no deps to ignore', () => { + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, [], results) + expect(results).toEqual([]) + expect(fs.existsSync(path.join(dir, 'knip.json'))).toBe(false) + }) + + it('creates a minimal knip.json when there is no config', () => { + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, ['oxlint', 'oxfmt'], results) + expect(results[0]?.action).toBe('created') + expect(readKnip().ignoreDependencies).toEqual(['oxlint', 'oxfmt']) + }) + + it('merges missing deps into an existing knip.json without touching other content', () => { + fs.writeFileSync(path.join(dir, 'knip.json'), JSON.stringify({ entry: ['src/x.ts'], ignoreDependencies: ['oxlint'] }, null, 2)) + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, ['oxlint', 'jscpd'], results) + expect(results[0]?.action).toBe('updated') + const cfg = readKnip() as { entry?: string[]; ignoreDependencies?: string[] } + expect(cfg.ignoreDependencies).toEqual(['oxlint', 'jscpd']) // oxlint not duplicated + expect(cfg.entry).toEqual(['src/x.ts']) // untouched + }) + + it('is unchanged when every dep is already ignored', () => { + fs.writeFileSync(path.join(dir, 'knip.json'), JSON.stringify({ ignoreDependencies: ['oxlint', 'oxfmt'] })) + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, ['oxlint'], results) + expect(results[0]?.action).toBe('unchanged') + }) + + it('merges into package.json#knip when present and there is no knip.json', () => { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'scratch', knip: { ignoreDependencies: ['skott'] } }, null, 2)) + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, ['oxlint'], results) + expect(results[0]?.path).toContain('package.json') + const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as { knip: { ignoreDependencies: string[] } } + expect(pkg.knip.ignoreDependencies).toEqual(['skott', 'oxlint']) + expect(fs.existsSync(path.join(dir, 'knip.json'))).toBe(false) + }) + + it('leaves a code-based knip config untouched', () => { + fs.writeFileSync(path.join(dir, 'knip.ts'), 'export default {}\n') + const results: ManagedFileResult[] = [] + ensureKnipIgnores(dir, ['oxlint'], results) + expect(results).toEqual([]) + expect(fs.existsSync(path.join(dir, 'knip.json'))).toBe(false) + }) +}) diff --git a/src/scaffold/knipConfig.ts b/src/scaffold/knipConfig.ts new file mode 100644 index 0000000..13a337c --- /dev/null +++ b/src/scaffold/knipConfig.ts @@ -0,0 +1,61 @@ +import fs from 'node:fs' +import path from 'node:path' + +import type { ManagedFileResult } from './writeManaged.ts' + +// Code-based knip configs are the user's to manage — we won't rewrite JS/TS. +const CODE_CONFIGS = ['knip.ts', 'knip.js', 'knip.config.ts', 'knip.config.js', 'knip.jsonc'] +const SCHEMA = 'https://unpkg.com/knip/schema.json' + +type KnipConfig = { ignoreDependencies?: string[] } & Record + +/** Append any missing `deps` to `existing`, preserving order; report whether anything changed. */ +function addMissing(existing: string[] | undefined, deps: readonly string[]): { list: string[]; changed: boolean } { + const list = [...(existing ?? [])] + let changed = false + for (const dep of deps) { + if (!list.includes(dep)) { + list.push(dep) + changed = true + } + } + return { list, changed } +} + +function mergeInto(config: KnipConfig, deps: readonly string[]): boolean { + const { list, changed } = addMissing(config.ignoreDependencies, deps) + if (changed) config.ignoreDependencies = list + return changed +} + +/** + * Ensure the project's knip config ignores `deps` — the tools verifyx invokes at runtime (via node_modules/.bin), + * which knip can't see and would otherwise flag as unused. Adds only what's missing (idempotent), never removes + * or rewrites unrelated content. Merges into an existing `knip.json` or `package.json#knip`, creates a minimal + * `knip.json` if there's no config, and leaves code-based configs (knip.ts/js) untouched. + */ +export function ensureKnipIgnores(cwd: string, deps: readonly string[], results: ManagedFileResult[]): void { + if (deps.length === 0) return + if (CODE_CONFIGS.some((file) => fs.existsSync(path.join(cwd, file)))) return + + const jsonPath = path.join(cwd, 'knip.json') + if (fs.existsSync(jsonPath)) { + const config = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as KnipConfig + const changed = mergeInto(config, deps) + if (changed) fs.writeFileSync(jsonPath, `${JSON.stringify(config, null, 2)}\n`) + results.push({ path: jsonPath, action: changed ? 'updated' : 'unchanged' }) + return + } + + const pkgPath = path.join(cwd, 'package.json') + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { knip?: KnipConfig } & Record + if (pkg.knip && typeof pkg.knip === 'object') { + const changed = mergeInto(pkg.knip, deps) + if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`) + results.push({ path: pkgPath, action: changed ? 'updated' : 'unchanged' }) + return + } + + fs.writeFileSync(jsonPath, `${JSON.stringify({ $schema: SCHEMA, ignoreDependencies: [...deps] }, null, 2)}\n`) + results.push({ path: jsonPath, action: 'created' }) +} From e52b6977c3cc14ef107ee2e7549ef64aeabf648c Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:48:19 +0800 Subject: [PATCH 17/34] feat: init offers "run everything with defaults" as an up-front choice `verifyx init` now asks how verify should run before anything else: run all built-in checks (verifyx all, no verify:* scripts) or hand-pick checks to wire up. The "defaults" path routes to the existing defaultsOnly scaffolding and passes every check so all external devDeps get installed. --defaults-only remains the non-interactive equivalent. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- src/commands/registerInit.ts | 42 +++++++++++++++++++++++++----------- src/scaffold/init.test.ts | 10 +++++++++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 08eb75f..f195199 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Interactively wire verifications and the agent integration into the current proj verifyx init ``` -It lets you multi-select **checks** and **agent targets** (Claude and/or other agents), then: +It first asks how `verify` should run — **run all built-in checks** (`verifyx all`, no `verify:*` scripts) or **pick specific checks** to wire up. Then you multi-select **agent targets** (Claude and/or other agents) — and, if you chose to pick, the **checks** — after which it: - writes the selected `verify:*` scripts to `package.json` (never clobbering existing ones), - installs the external checks' tools as `--save-dev`, @@ -149,7 +149,7 @@ The skill auto-triggers on "verify"/"run checks", so agents run the checks proac Options: -- `--defaults-only` — do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes the skill + pointer). +- `--defaults-only` — the non-interactive form of the "run all built-in checks" choice: do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes the skill + pointer). - `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. ### `verifyx upgrade-docs` diff --git a/src/commands/registerInit.ts b/src/commands/registerInit.ts index f47621a..d962c9c 100644 --- a/src/commands/registerInit.ts +++ b/src/commands/registerInit.ts @@ -14,8 +14,8 @@ function collect(value: string, previous: string[]): string[] { return [...previous, value] } -async function multiselect(message: string, choices: Choice[]): Promise { - const response = (await enquirer.prompt({ type: 'multiselect', name: 'selected', message, choices })) as { selected: string[] } +async function ask(type: 'select' | 'multiselect', message: string, choices: Choice[]): Promise { + const response = (await enquirer.prompt({ type, name: 'selected', message, choices })) as { selected: T } return response.selected } @@ -27,24 +27,41 @@ type InitCliOptions = { agents?: boolean } -async function resolveSelections(opts: InitCliOptions): Promise<{ checks: string[]; targets: AgentTarget[] }> { +type Selections = { checks: string[]; targets: AgentTarget[]; defaultsOnly: boolean } + +async function resolveSelections(opts: InitCliOptions): Promise { const nonInteractive = !!opts.yes || !process.stdin.isTTY if (nonInteractive) { const targets: AgentTarget[] = [] if (opts.claude !== false) targets.push('claude') if (opts.agents) targets.push('agents') - return { checks: opts.select.length > 0 ? opts.select : recommendedChecks().map((c) => c.name), targets } + return { + checks: opts.select.length > 0 ? opts.select : recommendedChecks().map((c) => c.name), + targets, + defaultsOnly: !!opts.defaultsOnly, + } } - const checks = await multiselect( - 'Select checks to wire up', - CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.recommended })), - ) - const targets = (await multiselect('Select agent targets', [ + // context: an up-front choice — run everything with defaults (verifyx all, no verify:* scripts), or hand-pick checks. + const mode = await ask('select', 'How should verify run?', [ + { name: 'defaults', message: 'Run all built-in checks (verifyx all) with default options', enabled: true }, + { name: 'pick', message: 'Pick specific checks to wire up as verify:* scripts' }, + ]) + const defaultsOnly = mode === 'defaults' + + // Defaults-only still passes every check so applyInit installs all their devDeps; hand-picking narrows the list. + const checks = defaultsOnly + ? CHECKS.map((c) => c.name) + : await ask( + 'multiselect', + 'Select checks to wire up', + CHECKS.map((c) => ({ name: c.name, message: `${c.name} — ${c.description}`, enabled: c.recommended })), + ) + const targets = await ask('multiselect', 'Select agent targets', [ { name: 'claude', message: 'Claude (.claude/skills + CLAUDE.md)', enabled: true }, { name: 'agents', message: 'Other agents (.agent-skills + AGENTS.md)', enabled: false }, - ])) as AgentTarget[] - return { checks, targets } + ]) + return { checks, targets, defaultsOnly } } function report(result: InitResult, defaultsOnly: boolean): void { @@ -71,8 +88,7 @@ export function registerInit(program: Command): void { .option('--agents', 'also write .agent-skills/ files (non-interactive)') .action(async (opts: InitCliOptions) => { const cwd = process.cwd() - const defaultsOnly = !!opts.defaultsOnly - const { checks, targets } = await resolveSelections(opts) + const { checks, targets, defaultsOnly } = await resolveSelections(opts) const result = applyInit({ cwd, checks, targets, defaultsOnly }) diff --git a/src/scaffold/init.test.ts b/src/scaffold/init.test.ts index 1785210..84cea28 100644 --- a/src/scaffold/init.test.ts +++ b/src/scaffold/init.test.ts @@ -4,6 +4,7 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { CHECKS } from '../checks/registry.ts' import { applyInit } from './init.ts' let dir: string @@ -46,6 +47,15 @@ describe('applyInit', () => { expect(fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf-8')).toContain('npm run verify') }) + it('defaults-only with every check installs all external devDeps but writes only the verify script', () => { + const result = applyInit({ cwd: dir, checks: CHECKS.map((c) => c.name), targets: [], defaultsOnly: true }) + + expect(Object.keys(readScripts())).toEqual(['verify']) + expect(readScripts().verify).toBe('verifyx all') + // Every external check's tool is installed, so `verifyx all` can actually run them. + expect(result.devDeps).toEqual(expect.arrayContaining(['knip', 'jscpd', 'skott'])) + }) + it('does not clobber an existing verify:* script', () => { fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { 'verify:complexity': 'custom' } })) applyInit({ cwd: dir, checks: ['complexity'], targets: [], defaultsOnly: false }) From 7dae650773ddcf98aa8ad076bc78723222a5ecc2 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:54:29 +0800 Subject: [PATCH 18/34] feat: merge comment-block + block-comments into one `comments` check The long-comment-block detector (comment-block) and the diff-based "no comments on changed lines" gate (block-comments) are now one native `comments` check. Default behaviour is long-block detection; the diff-based gate is opt-in via --block-new-comments. Existing flags (--max-lines, --pushback, --warn, --ignore, [pattern]) are preserved, as are the JSDoc/context: and machine-directive exemptions. The recommended default runs with --max-lines 2 --pushback (both the `verifyx all` built-in and the scaffolded verify:comments script). The verify config key blockComments is renamed to comments. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 4 +- README.md | 44 +++++---- package.json | 2 +- src/checks/block-comments.ts | 78 --------------- src/checks/comment-block.ts | 31 ------ src/checks/comments.test.ts | 49 ++++++++++ src/checks/comments.ts | 121 ++++++++++++++++++++++++ src/checks/registry.test.ts | 3 +- src/checks/registry.ts | 16 ++-- src/commands/registerChecks.ts | 36 ++++--- src/index.ts | 3 +- src/orchestrator/resolveEntries.test.ts | 2 +- src/shared/config.ts | 2 +- 13 files changed, 233 insertions(+), 158 deletions(-) delete mode 100644 src/checks/block-comments.ts delete mode 100644 src/checks/comment-block.ts create mode 100644 src/checks/comments.test.ts create mode 100644 src/checks/comments.ts diff --git a/CLAUDE.md b/CLAUDE.md index 9fced2f..e72e3da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ A growing collection of code **verifications** that give AI coding agents back-p ## After making changes, run `npm run verify` -`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comment-block|…`). It runs them in parallel and suppresses output unless one fails. Bare `verifyx` runs **only** the defined `verify:*` scripts (nothing if none); `verifyx all` runs **every** built-in check, with a `verify:` script overriding its matching built-in. +`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comments|…`). It runs them in parallel and suppresses output unless one fails. Bare `verifyx` runs **only** the defined `verify:*` scripts (nothing if none); `verifyx all` runs **every** built-in check, with a `verify:` script overriding its matching built-in. **Fix locally, check in CI.** Run locally, `verifyx` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verifyx --check` / `verifyx --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) -`verify:comment-block` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. +`verify:comments` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. diff --git a/README.md b/README.md index f195199..4178945 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A growing collection of code **verifications** that give AI coding agents back-p `verify` ships both: -- a **CLI** that orchestrates a set of checks by convention, and +- a **CLI** that orchestrates a set of checks by convention that are designed to be executed by AI agents as well as CI servers, and - a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run those checks and fix what they report. Complexity was the first check. It is now just one of several, and the set grows over time. @@ -79,19 +79,18 @@ Flags on the bare `verifyx` command: ## Built-in checks -| Check | Kind | What it catches | -| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | -| `comment-block` | native | Comment blocks longer than the limit. JSDoc (`/**`) and `context:`-prefixed blocks are exempt. | -| `block-comments` | native | Any comment added to a line changed against `HEAD`. Machine directives (`eslint-disable`, `@ts-expect-error`, …) and `context:` are exempt. | -| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | -| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | -| `lint` | external | Lint — auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | -| `format` | external | Formatting — writes locally, checks in CI ([oxfmt](https://oxc.rs)). | -| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | -| `knip` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | -| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | -| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | +| Check | Kind | What it catches | +| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | +| `comments` | native | Comment blocks longer than the limit (JSDoc / `context:` exempt); with `--block-new-comments`, also any comment on a line changed against `HEAD` (machine directives like `eslint-disable` / `@ts-expect-error`, and `context:`, exempt). | +| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | +| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `lint` | external | Lint — auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | +| `format` | external | Formatting — writes locally, checks in CI ([oxfmt](https://oxc.rs)). | +| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | +| `knip` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | +| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | External checks shell out to their tool and **skip gracefully when it is not installed** — `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. @@ -109,15 +108,19 @@ verifyx complexity --threshold 50 "src/**/*.ts" A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown is printed instead of the gate — handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). -### `comment-block` +### `comments` ```sh -verifyx comment-block --max-lines 2 --pushback "src/**/*.ts" +verifyx comments --max-lines 2 --pushback "src/**/*.ts" ``` +By default it flags **long comment blocks**. `--block-new-comments` adds a stricter, diff-based gate on top: any comment on a line changed against `HEAD` fails (machine directives like `eslint-disable` / `@ts-expect-error` and `context:`-prefixed comments are exempt). + +- `[pattern]` — glob, directory, or file to scan. - `--max-lines ` — fail on comment blocks longer than `n` lines (default 2). - `--pushback` — add AI back-pressure framing to the failure (keeping the comment "pages a human"). -- `--warn` — report without failing. +- `--warn` — report the long-block violations without failing. +- `--block-new-comments` — also fail on any comment on a line changed against `HEAD`. - `--ignore ` — exclude files (repeatable). Prefix a comment's first line with `context:` to keep genuinely durable context: @@ -168,7 +171,7 @@ Some checks read per-repo config from `verify.config.json`, or a `verify` key in ```jsonc { "verify": { - "blockComments": { "ignore": ["**/*.generated.ts"] }, + "comments": { "ignore": ["**/*.generated.ts"] }, "hardcodedColors": { "root": "src", "ignore": ["**/tokens.ts"] }, "forbiddenStrings": [{ "file": "app.json", "paths": ["env.LOG_LEVEL"], "disallowed": "debug" }], }, @@ -189,7 +192,10 @@ Because it is a pinned dev dependency, CI runs the identical tool: ```ts import { analyzeComplexity, orchestrate, CHECKS } from '@makerx/verify' -const { failing, passed } = analyzeComplexity({ pattern: 'src/**/*.ts', threshold: 50 }) +const { failing, passed } = analyzeComplexity({ + pattern: 'src/**/*.ts', + threshold: 50, +}) ``` Exports include `analyzeComplexity`, the check registry (`CHECKS`, `getCheck`, `defaultChecks`), `orchestrate`, `runDefaults`, `applyInit`, `loadVerifyConfig`, the individual `run*` check functions, and the lower-level complexity helpers (`calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, `forEachFunction`). diff --git a/package.json b/package.json index 981bd18..647fcc1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "verify:format": "verifyx format", "verify:check-types": "verifyx check-types", "verify:complexity": "verifyx complexity --threshold 50 \"src/**/*.ts\"", - "verify:comment-block": "verifyx comment-block --max-lines 1 --pushback \"src/**/*.ts\"", + "verify:comments": "verifyx comments --max-lines 1 --pushback \"src/**/*.ts\"", "verify:hardcoded-colors": "verifyx hardcoded-colors", "verify:forbidden-strings": "verifyx forbidden-strings", "verify:unused-code": "verifyx unused-code", diff --git a/src/checks/block-comments.ts b/src/checks/block-comments.ts deleted file mode 100644 index 792978f..0000000 --- a/src/checks/block-comments.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'node:fs' - -import { minimatch } from 'minimatch' - -import { color } from '../shared/color.ts' -import { scanFileComments } from '../shared/comment-scan.ts' -import { loadVerifyConfig } from '../shared/config.ts' -import { parseDiffAddedLines } from '../shared/diff.ts' -import { gitDiffHead } from '../shared/git.ts' -import type { CheckResult } from './types.ts' - -const SCANNED_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts', '.js', '.jsx', '.mjs', '.cjs', '.yml', '.yaml'] - -// context: machine directives steer tooling, not humans, so a changed line carrying one is never a "comment" -// worth removing; `context:` is this project's durable-context escape hatch and is likewise exempt. -const MACHINE_DIRECTIVES = [ - 'oxlint-disable', - 'oxlint-enable', - '@ts-expect-error', - '@ts-ignore', - '@ts-nocheck', - 'eslint-disable', - 'eslint-enable', - 'prettier-ignore', - 'istanbul ignore', - 'v8 ignore', - 'c8 ignore', - '@vitest-environment', -] - -function isCommentExempt(text: string): boolean { - const lower = text.toLowerCase() - if (MACHINE_DIRECTIVES.some((d) => lower.includes(d))) return true - const stripped = text.replace(/^\s*(?:\/\/+|\/\*+|\*|#)\s*/, '') - return stripped.toLowerCase().startsWith('context:') -} - -function shouldScan(file: string, ignoreGlobs: readonly string[]): boolean { - if (!SCANNED_EXTENSIONS.some((ext) => file.endsWith(ext))) return false - if (ignoreGlobs.some((glob) => minimatch(file, glob))) return false - return fs.existsSync(file) -} - -function toSingleLine(text: string): string { - return text.replace(/\s+/g, ' ').trim() -} - -export type BlockCommentsOptions = { ignore?: readonly string[] } - -/** Fail on any comment sitting on a line changed against HEAD. Machine directives and `context:` are exempt. */ -export function runBlockComments(opts: BlockCommentsOptions = {}): CheckResult { - const ignoreGlobs = opts.ignore ?? loadVerifyConfig().blockComments?.ignore ?? [] - const added = parseDiffAddedLines(gitDiffHead()) - - const findings: Array<{ file: string; line: number; text: string }> = [] - for (const [file, lines] of added) { - if (!shouldScan(file, ignoreGlobs)) continue - for (const comment of scanFileComments(file)) { - if (!lines.has(comment.line)) continue - if (isCommentExempt(comment.text)) continue - findings.push({ file, line: comment.line, text: toSingleLine(comment.text) }) - } - } - findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line) - - if (findings.length === 0) { - console.log(color.green('No comments on changed lines.')) - return { name: 'block-comments', ok: true } - } - - console.error(color.red('\nComments on changed lines:\n')) - for (const { file, line, text } of findings) console.error(` ${file}:${line} → ${text}`) - console.error(color.red(`\nTotal: ${findings.length} comment(s)`)) - console.error( - '\nEvery comment on a changed line fails this gate, whether you added or edited it. Remove them and let the code document itself.', - ) - return { name: 'block-comments', ok: false } -} diff --git a/src/checks/comment-block.ts b/src/checks/comment-block.ts deleted file mode 100644 index 99fe827..0000000 --- a/src/checks/comment-block.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DEFAULT_IGNORE, DEFAULT_PATTERN, findSourceFiles, resolvePattern } from '../analyze.ts' -import { findLongCommentBlocks } from '../comments.ts' -import { printCommentBlockReport } from '../report.ts' -import { color } from '../shared/color.ts' -import type { CheckResult } from './types.ts' - -const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 - -export type CommentBlockOptions = { - pattern?: string - ignore?: readonly string[] - maxLines?: number - pushback?: boolean - warn?: boolean -} - -/** Native check: flag comment blocks longer than `maxLines`. JSDoc and `context:`-prefixed blocks are exempt. */ -export function runCommentBlock(opts: CommentBlockOptions = {}): CheckResult { - const maxLines = opts.maxLines ?? DEFAULT_MAX_COMMENT_BLOCK_LINES - const pattern = opts.pattern ?? DEFAULT_PATTERN - const files = findSourceFiles(resolvePattern(pattern), [...DEFAULT_IGNORE, ...(opts.ignore ?? [])]) - - const violations = findLongCommentBlocks(files, maxLines) - if (violations.length === 0) { - console.log(color.green(`No comment block over ${maxLines} line(s)`)) - return { name: 'comment-block', ok: true } - } - - printCommentBlockReport(violations, maxLines, { pushback: !!opts.pushback, warn: !!opts.warn }) - return { name: 'comment-block', ok: !!opts.warn } -} diff --git a/src/checks/comments.test.ts b/src/checks/comments.test.ts new file mode 100644 index 0000000..b05ab6d --- /dev/null +++ b/src/checks/comments.test.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { runComments } from './comments.ts' + +let dir: string + +beforeEach(() => { + dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'verify-comments-'))) + vi.spyOn(console, 'log').mockImplementation(() => {}) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) +}) +afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +// A forward-slashed path resolvePattern leaves untouched, so findSourceFiles matches the single file directly. +function write(name: string, content: string): string { + const file = path.join(dir, name) + fs.writeFileSync(file, content) + return file.replaceAll('\\', '/') +} + +describe('runComments', () => { + it('passes and is named `comments` when no block exceeds max-lines', () => { + const file = write('ok.ts', ['// one line', 'const a = 1'].join('\n')) + expect(runComments({ pattern: file, maxLines: 2 })).toEqual({ name: 'comments', ok: true }) + }) + + it('fails on a comment block longer than max-lines', () => { + const file = write('bad.ts', ['// one', '// two', '// three', 'const a = 1'].join('\n')) + expect(runComments({ pattern: file, maxLines: 2 })).toEqual({ name: 'comments', ok: false }) + }) + + it('reports without failing under warn', () => { + const file = write('warn.ts', ['// one', '// two', '// three', 'const a = 1'].join('\n')) + expect(runComments({ pattern: file, maxLines: 2, warn: true }).ok).toBe(true) + }) + + it('leaves JSDoc and context: blocks alone regardless of length', () => { + const file = write('exempt.ts', ['/**', ' * a', ' * b', ' * c', ' */', '// context: durable', '// more', 'const a = 1'].join('\n')) + expect(runComments({ pattern: file, maxLines: 1 }).ok).toBe(true) + }) +}) diff --git a/src/checks/comments.ts b/src/checks/comments.ts new file mode 100644 index 0000000..918e5d1 --- /dev/null +++ b/src/checks/comments.ts @@ -0,0 +1,121 @@ +import fs from 'node:fs' + +import { minimatch } from 'minimatch' + +import { DEFAULT_IGNORE, DEFAULT_PATTERN, findSourceFiles, resolvePattern } from '../analyze.ts' +import { findLongCommentBlocks } from '../comments.ts' +import { printCommentBlockReport } from '../report.ts' +import { color } from '../shared/color.ts' +import { scanFileComments } from '../shared/comment-scan.ts' +import { loadVerifyConfig } from '../shared/config.ts' +import { parseDiffAddedLines } from '../shared/diff.ts' +import { gitDiffHead } from '../shared/git.ts' +import type { CheckResult } from './types.ts' + +const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 + +const SCANNED_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts', '.js', '.jsx', '.mjs', '.cjs', '.yml', '.yaml'] + +// context: machine directives steer tooling, not humans, so a changed line carrying one is never a "comment" +// worth removing; `context:` is this project's durable-context escape hatch and is likewise exempt. +const MACHINE_DIRECTIVES = [ + 'oxlint-disable', + 'oxlint-enable', + '@ts-expect-error', + '@ts-ignore', + '@ts-nocheck', + 'eslint-disable', + 'eslint-enable', + 'prettier-ignore', + 'istanbul ignore', + 'v8 ignore', + 'c8 ignore', + '@vitest-environment', +] + +function isCommentExempt(text: string): boolean { + const lower = text.toLowerCase() + if (MACHINE_DIRECTIVES.some((d) => lower.includes(d))) return true + const stripped = text.replace(/^\s*(?:\/\/+|\/\*+|\*|#)\s*/, '') + return stripped.toLowerCase().startsWith('context:') +} + +function shouldScan(file: string, ignoreGlobs: readonly string[]): boolean { + if (!SCANNED_EXTENSIONS.some((ext) => file.endsWith(ext))) return false + if (ignoreGlobs.some((glob) => minimatch(file, glob))) return false + return fs.existsSync(file) +} + +function toSingleLine(text: string): string { + return text.replace(/\s+/g, ' ').trim() +} + +type NewComment = { file: string; line: number; text: string } + +// context: the --block-new-comments behaviour — flag any comment sitting on a line changed against HEAD. +function findCommentsOnChangedLines(ignoreGlobs: readonly string[]): NewComment[] { + const added = parseDiffAddedLines(gitDiffHead()) + const findings: NewComment[] = [] + for (const [file, lines] of added) { + if (!shouldScan(file, ignoreGlobs)) continue + for (const comment of scanFileComments(file)) { + if (!lines.has(comment.line)) continue + if (isCommentExempt(comment.text)) continue + findings.push({ file, line: comment.line, text: toSingleLine(comment.text) }) + } + } + findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line) + return findings +} + +function reportCommentsOnChangedLines(findings: readonly NewComment[]): void { + console.error(color.red('\nComments on changed lines:\n')) + for (const { file, line, text } of findings) console.error(` ${file}:${line} → ${text}`) + console.error(color.red(`\nTotal: ${findings.length} comment(s)`)) + console.error( + '\nWith --block-new-comments, every comment on a changed line fails this gate, whether you added or edited it. Remove them and let the code document itself.', + ) +} + +export type CommentsOptions = { + pattern?: string + ignore?: readonly string[] + maxLines?: number + pushback?: boolean + warn?: boolean + /** Also fail on any comment on a line changed against HEAD (machine directives / context: exempt). */ + blockNewComments?: boolean +} + +/** + * Native check: flag comment blocks longer than `maxLines` (JSDoc and `context:`-prefixed blocks exempt). + * With `blockNewComments`, additionally fail on any comment on a line changed against HEAD. + */ +export function runComments(opts: CommentsOptions = {}): CheckResult { + const maxLines = opts.maxLines ?? DEFAULT_MAX_COMMENT_BLOCK_LINES + const pattern = opts.pattern ?? DEFAULT_PATTERN + const files = findSourceFiles(resolvePattern(pattern), [...DEFAULT_IGNORE, ...(opts.ignore ?? [])]) + + const blocks = findLongCommentBlocks(files, maxLines) + let blocksOk = true + if (blocks.length === 0) { + console.log(color.green(`No comment block over ${maxLines} line(s)`)) + } else { + printCommentBlockReport(blocks, maxLines, { pushback: !!opts.pushback, warn: !!opts.warn }) + blocksOk = !!opts.warn + } + + let changedLinesOk = true + if (opts.blockNewComments) { + const ignoreGlobs = opts.ignore ?? loadVerifyConfig().comments?.ignore ?? [] + const findings = findCommentsOnChangedLines(ignoreGlobs) + if (findings.length === 0) { + console.log(color.green('No comments on changed lines.')) + } else { + reportCommentsOnChangedLines(findings) + changedLinesOk = false + } + } + + return { name: 'comments', ok: blocksOk && changedLinesOk } +} diff --git a/src/checks/registry.test.ts b/src/checks/registry.test.ts index d3606ee..05a7e91 100644 --- a/src/checks/registry.test.ts +++ b/src/checks/registry.test.ts @@ -8,8 +8,7 @@ describe('check registry', () => { expect(names).toEqual( expect.arrayContaining([ 'complexity', - 'comment-block', - 'block-comments', + 'comments', 'hardcoded-colors', 'forbidden-strings', 'lint', diff --git a/src/checks/registry.ts b/src/checks/registry.ts index 5402b65..a1bbd9b 100644 --- a/src/checks/registry.ts +++ b/src/checks/registry.ts @@ -1,21 +1,20 @@ import fs from 'node:fs' -import { runBlockComments } from './block-comments.ts' -import { runCommentBlock } from './comment-block.ts' +import { runComments } from './comments.ts' import { runComplexity } from './complexity.ts' import { defineExternalCheck } from './external.ts' import { runForbiddenStrings } from './forbidden-strings.ts' import { runHardcodedColors } from './hardcoded-colors.ts' import type { Check, CheckResult } from './types.ts' -function nativeCheck(name: string, description: string, recommended: boolean, run: () => CheckResult): Check { +function nativeCheck(name: string, description: string, recommended: boolean, run: () => CheckResult, script = `verifyx ${name}`): Check { return { name, description, kind: 'native', recommended, // Native checks scaffold as a call back into this CLI's own subcommand. - scaffold: { script: `verifyx ${name}` }, + scaffold: { script }, runDefault: async () => run(), } } @@ -23,8 +22,13 @@ function nativeCheck(name: string, description: string, recommended: boolean, ru // context: checks are named for their function, never the tool behind them (see each check's bin/devDeps). export const CHECKS: Check[] = [ nativeCheck('complexity', 'Maintainability-index gate (cyclomatic + Halstead + SLOC)', true, () => runComplexity()), - nativeCheck('comment-block', 'Flag comment blocks longer than the limit (JSDoc / context: exempt)', true, () => runCommentBlock()), - nativeCheck('block-comments', 'Fail on any comment added to a line changed against HEAD', false, () => runBlockComments()), + nativeCheck( + 'comments', + 'Flag long comment blocks (JSDoc / context: exempt); --block-new-comments also fails comments on changed lines', + true, + () => runComments({ pushback: true }), + 'verifyx comments --pushback', + ), nativeCheck('hardcoded-colors', 'Fail on literal hex / 0x colour values in source', false, () => runHardcodedColors()), nativeCheck('forbidden-strings', 'Fail on disallowed JSON config values (rules from verify config)', false, () => runForbiddenStrings()), defineExternalCheck({ diff --git a/src/commands/registerChecks.ts b/src/commands/registerChecks.ts index 8351ecf..791e699 100644 --- a/src/commands/registerChecks.ts +++ b/src/commands/registerChecks.ts @@ -1,7 +1,6 @@ import type { Command } from 'commander' -import { runBlockComments } from '../checks/block-comments.ts' -import { runCommentBlock } from '../checks/comment-block.ts' +import { runComments } from '../checks/comments.ts' import { runComplexity } from '../checks/complexity.ts' import { runForbiddenStrings } from '../checks/forbidden-strings.ts' import { runHardcodedColors } from '../checks/hardcoded-colors.ts' @@ -28,24 +27,31 @@ export function registerChecks(program: Command): void { }) program - .command('comment-block') - .description('Flag comment blocks longer than the limit (JSDoc / context: exempt)') + .command('comments') + .description('Flag long comment blocks (JSDoc / context: exempt); --block-new-comments also fails comments on changed lines') .argument('[pattern]', 'glob, directory, or file to scan') .option('--max-lines ', 'maximum comment-block length', Number) .option('--pushback', 'add AI back-pressure framing to the failure message') .option('--warn', 'report without failing the run') + .option('--block-new-comments', 'also fail on any comment on a line changed against HEAD') .option('--ignore ', 'ignore glob (repeatable)', collect, []) - .action((pattern: string | undefined, opts: { maxLines?: number; pushback?: boolean; warn?: boolean; ignore: string[] }) => { - finish(runCommentBlock({ pattern, maxLines: opts.maxLines, pushback: opts.pushback, warn: opts.warn, ignore: opts.ignore }).ok) - }) - - program - .command('block-comments') - .description('Fail on any comment added to a line changed against HEAD') - .option('--ignore ', 'ignore glob (repeatable)', collect, []) - .action((opts: { ignore: string[] }) => { - finish(runBlockComments({ ignore: opts.ignore }).ok) - }) + .action( + ( + pattern: string | undefined, + opts: { maxLines?: number; pushback?: boolean; warn?: boolean; blockNewComments?: boolean; ignore: string[] }, + ) => { + finish( + runComments({ + pattern, + maxLines: opts.maxLines, + pushback: opts.pushback, + warn: opts.warn, + blockNewComments: opts.blockNewComments, + ignore: opts.ignore, + }).ok, + ) + }, + ) program .command('hardcoded-colors') diff --git a/src/index.ts b/src/index.ts index d45299f..6f47bf1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,7 @@ export { resolvePattern, scoreFiles, } from './analyze.ts' -export { runBlockComments } from './checks/block-comments.ts' -export { runCommentBlock } from './checks/comment-block.ts' +export { type CommentsOptions, runComments } from './checks/comments.ts' export { runComplexity } from './checks/complexity.ts' export { runForbiddenStrings } from './checks/forbidden-strings.ts' export { runHardcodedColors } from './checks/hardcoded-colors.ts' diff --git a/src/orchestrator/resolveEntries.test.ts b/src/orchestrator/resolveEntries.test.ts index d723a4c..d5c503c 100644 --- a/src/orchestrator/resolveEntries.test.ts +++ b/src/orchestrator/resolveEntries.test.ts @@ -39,7 +39,7 @@ describe('entryCheckName', () => { }) it('strips a trailing :fix so a variant maps to its base check', () => { expect(entryCheckName('verify:lint:fix')).toBe('lint') - expect(entryCheckName('verify:comment-block:fix')).toBe('comment-block') + expect(entryCheckName('verify:comments:fix')).toBe('comments') }) }) diff --git a/src/shared/config.ts b/src/shared/config.ts index c1d2a07..b277c81 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -9,7 +9,7 @@ export type ForbiddenStringsRule = { } export type VerifyConfig = { - blockComments?: { ignore?: string[] } + comments?: { ignore?: string[] } hardcodedColors?: { ignore?: string[]; root?: string } forbiddenStrings?: ForbiddenStringsRule[] } From be38a14fa74f10c1fac4200208079fb9eb9ef78d Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:56:10 +0800 Subject: [PATCH 19/34] docs: clarify "how verifyx decides what to run" + expand complexity formula MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the run-selection section around the three invocation modes (verifyx / verifyx all / verifyx ) with a lead-in table, and order the details gate → all → fix-vs-check so the :fix variant lands next to the fix/check explanation it belongs with. Expand the complexity docs with the metrics it computes, the maintainability-index formula, the clamp, and rough interpretation bands (ported from the pre-transformation README). Also fix the built-in checks table: the unused-code check was mislabelled `knip`. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 51 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4178945..5860b9e 100644 --- a/README.md +++ b/README.md @@ -36,21 +36,19 @@ npx verifyx init ## How `verifyx` decides what to run -Your project's `verify:*` scripts **are** the gate, and `verifyx` runs exactly what you define — nothing implicit: +There are three ways to invoke it, from "my curated gate" to "one specific check": -- **`verifyx`** (no subcommand) runs your `verify:*` scripts in parallel. A clean run is **silent** — no preamble, no per-script output, just exit `0` — so it's cheap to run in a loop or an agent. Each script's output is buffered and shown **only if it fails**. Add `--verbose` to stream everything, or `--measure` for a status/duration table. With **no `verify:*` scripts, nothing runs.** -- **`verifyx all`** runs **every built-in check** (the explicit "run everything"). A `verify:` script **overrides** its matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. +| Command | What it runs | +| ----------------- | --------------------------------------------------------------------------------------- | +| `verifyx` | Your project's `verify:*` scripts — the gate you curate. No `verify:*` scripts → nothing runs. | +| `verifyx all` | Every built-in check, with default options. | +| `verifyx ` | A single built-in, e.g. `verifyx complexity`. `verifyx list` shows them all. | -**Fix/check overrides.** Alongside a `verify:` (check-mode) script you can define a `verify::fix` variant. `verifyx` runs the `:fix` variant in fix mode (locally) and the base in check mode (CI), and never both — so an override with a non-mode-aware tool still fixes locally and only checks in CI: +### `verifyx` — your curated gate -```jsonc -{ - "scripts": { - "verify:lint": "eslint .", - "verify:lint:fix": "eslint . --fix", - }, -} -``` +`verifyx` with no subcommand is what you run day-to-day and in CI. It runs every `verify:*` script in `package.json` **in parallel**, and nothing implicit — the scripts you define **are** the gate. + +A clean run is **completely silent**: no preamble, no per-script output, just exit `0`. Each script's output is buffered and printed **only if that script fails**, so `verifyx` is cheap to run in a loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table.) You curate the gate by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: @@ -65,11 +63,24 @@ You curate the gate by adding `verify:*` scripts. Prefer calling the built-ins ( } ``` -Run a single built-in directly: `verifyx complexity`, `verifyx unused-code`, … and `verifyx list` shows them all. +### `verifyx all` — run every built-in + +`verifyx all` skips your curated list and runs **every** built-in check with its default options — a quick "run everything" without wiring up scripts. Where you've defined a `verify:` script, it **overrides** the matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. ### Fix locally, check in CI -Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verifyx` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Override explicitly with `verifyx --fix` or `verifyx --check`. +Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verifyx` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Force a mode with `verifyx --fix` or `verifyx --check`. + +To give a script-based override that same split, pair a `verify:` (check-mode) script with a `verify::fix` variant. `verifyx` runs the `:fix` variant locally and the base script in CI — never both — so even an override wrapping a tool that doesn't know about fix-vs-check still fixes locally and only checks in CI: + +```jsonc +{ + "scripts": { + "verify:lint": "eslint .", + "verify:lint:fix": "eslint . --fix", + }, +} +``` Flags on the bare `verifyx` command: @@ -88,7 +99,7 @@ Flags on the bare `verifyx` command: | `lint` | external | Lint — auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | | `format` | external | Formatting — writes locally, checks in CI ([oxfmt](https://oxc.rs)). | | `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | -| `knip` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | | `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | | `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | @@ -106,7 +117,15 @@ verifyx complexity --threshold 50 "src/**/*.ts" - `--threshold ` — fail when any file's minimum maintainability index is below `n`. - `--ignore ` — exclude files (repeatable; appended to the default `**/*test.ts*`). -A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown is printed instead of the gate — handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). +It parses your `.ts`/`.tsx` sources with the TypeScript compiler API and, for every function, computes three metrics — **cyclomatic complexity** (independent paths through the code), **Halstead volume** (a size measure derived from the operators and operands used), and **SLOC** (source lines of code, excluding blanks and comments) — then combines them into a single **maintainability index (MI)**, a 0–100 score where lower means harder to maintain: + +``` +MI = 171 - 5.2 * ln(HalsteadVolume) - 0.23 * CyclomaticComplexity - 16.2 * ln(SLOC) +``` + +The result is clamped to 0–100; a function with zero Halstead volume or zero SLOC scores 100. As a rough guide: **> 65** is good, **50–65** is moderate (watch for growth), and **< 50** is hard to maintain. Thresholds are a matter of taste — pick one that fits your codebase and enforce it in CI. + +A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown (SLOC, cyclomatic complexity, Halstead metrics, and MI) is printed instead of the gate — handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). ### `comments` From a81ed9bdd03ef9a14310fafb72f7a68efef5002a Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:57:10 +0800 Subject: [PATCH 20/34] docs: define verify:* scripts before the run-modes table The table referenced `verify:*` scripts before the concept was introduced. Lead with a plain-language definition (any npm script named verify:, each a check, run in parallel to form the gate) and frame the modes as "curated checks" rather than a single "gate". Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5860b9e..f0fd953 100644 --- a/README.md +++ b/README.md @@ -36,21 +36,23 @@ npx verifyx init ## How `verifyx` decides what to run -There are three ways to invoke it, from "my curated gate" to "one specific check": +A **`verify:*` script** is any npm script in your `package.json` whose name starts with `verify:` — `verify:lint`, `verify:complexity`, `verify:custom`, and so on. Each one is a single check. `verifyx` runs them all **in parallel**, and together they form your project's verification gate. You decide which checks make up that gate by adding these scripts (`verifyx init` scaffolds a starting set). -| Command | What it runs | -| ----------------- | --------------------------------------------------------------------------------------- | -| `verifyx` | Your project's `verify:*` scripts — the gate you curate. No `verify:*` scripts → nothing runs. | -| `verifyx all` | Every built-in check, with default options. | -| `verifyx ` | A single built-in, e.g. `verifyx complexity`. `verifyx list` shows them all. | +There are three ways to invoke the CLI, from "all my checks" down to "one specific check": -### `verifyx` — your curated gate +| Command | What it runs | +| ----------------- | -------------------------------------------------------------------------------------------- | +| `verifyx` | Every `verify:*` script — the set of checks you've curated. With none defined, nothing runs. | +| `verifyx all` | Every built-in check, with default options. | +| `verifyx ` | A single built-in, e.g. `verifyx complexity`. `verifyx list` shows them all. | -`verifyx` with no subcommand is what you run day-to-day and in CI. It runs every `verify:*` script in `package.json` **in parallel**, and nothing implicit — the scripts you define **are** the gate. +### `verifyx` — run your curated checks + +`verifyx` with no subcommand is what you run day-to-day and in CI. It runs every `verify:*` script in parallel — nothing implicit, the scripts you define **are** the gate. A clean run is **completely silent**: no preamble, no per-script output, just exit `0`. Each script's output is buffered and printed **only if that script fails**, so `verifyx` is cheap to run in a loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table.) -You curate the gate by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: +You curate your checks by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc { From cfafa6cead0ecf4f60a921e49ec45adb796c36d1 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 03:57:29 +0800 Subject: [PATCH 21/34] docs: describe bare verifyx by what it does, not "what you run" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What you run day-to-day is `npm run verify`, which maps to whatever the `verify` script points at — not inherent to the subcommand-less invocation. Reframe around the mode a `"verify": "verifyx"` script uses. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0fd953..4fef64c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ There are three ways to invoke the CLI, from "all my checks" down to "one specif ### `verifyx` — run your curated checks -`verifyx` with no subcommand is what you run day-to-day and in CI. It runs every `verify:*` script in parallel — nothing implicit, the scripts you define **are** the gate. +With no subcommand, `verifyx` runs every `verify:*` script in parallel — nothing implicit, the scripts you define **are** the gate. This is the mode a top-level `"verify": "verifyx"` script points at, so `npm run verify` runs your whole gate. A clean run is **completely silent**: no preamble, no per-script output, just exit `0`. Each script's output is buffered and printed **only if that script fails**, so `verifyx` is cheap to run in a loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table.) From 97e551ffa4bf7510bc5474336df83dec0a8344a3 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:00:57 +0800 Subject: [PATCH 22/34] docs: explain built-in vs custom checks in the run-modes section The `verifyx ` row referenced "built-in" without defining it. Add an explicit built-in (ships with verifyx, invoked as a subcommand) vs custom (any command wired as a verify:* script) breakdown, and clarify that `verifyx ` runs a single built-in by name. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4fef64c..43fe04b 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,20 @@ npx verifyx init ## How `verifyx` decides what to run -A **`verify:*` script** is any npm script in your `package.json` whose name starts with `verify:` — `verify:lint`, `verify:complexity`, `verify:custom`, and so on. Each one is a single check. `verifyx` runs them all **in parallel**, and together they form your project's verification gate. You decide which checks make up that gate by adding these scripts (`verifyx init` scaffolds a starting set). +A **`verify:*` script** is any npm script in your `package.json` whose name starts with `verify:` — `verify:lint`, `verify:complexity`, `verify:custom`, and so on. Each one is a single **check**. `verifyx` runs them all **in parallel**, and together they form your project's verification gate. You decide which checks make up that gate by adding these scripts (`verifyx init` scaffolds a starting set). + +A check is either **built-in** or **custom**: + +- **Built-in checks** ship with `verifyx` (see [Built-in checks](#built-in-checks) below) and are invoked as subcommands — `verifyx lint`, `verifyx complexity`, … A `verify:*` script wires one in by calling it: `"verify:lint": "verifyx lint"`. +- **Custom checks** are any command of your own, wired as a `verify:*` script that runs whatever you like: `"verify:custom": "node ./scripts/my-check.mjs"`. They aren't `verifyx` subcommands — you run them through the gate, or directly via `npm run verify:custom`. There are three ways to invoke the CLI, from "all my checks" down to "one specific check": -| Command | What it runs | -| ----------------- | -------------------------------------------------------------------------------------------- | -| `verifyx` | Every `verify:*` script — the set of checks you've curated. With none defined, nothing runs. | -| `verifyx all` | Every built-in check, with default options. | -| `verifyx ` | A single built-in, e.g. `verifyx complexity`. `verifyx list` shows them all. | +| Command | What it runs | +| ----------------- | ---------------------------------------------------------------------------------------------------- | +| `verifyx` | Every `verify:*` script — the set of checks you've curated (built-in and custom). None defined → nothing runs. | +| `verifyx all` | Every built-in check, with default options (ignores your `verify:*` list, except as overrides). | +| `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | ### `verifyx` — run your curated checks From 34a09cc053f7461698fe3a359a28a21f4f491468 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:02:20 +0800 Subject: [PATCH 23/34] docs: lead with selling points, fix verifyx all + install framing - Rewrite the top-of-README intro to lead with the concrete selling points: auto-fix locally / fail in CI on one command, silent-on-green for cheap agent loops, agent-actionable failure output, and convention-over-configuration verify:* scripts. - Correct the `verifyx all` description: it honours matching verify: overrides rather than ignoring the verify:* list; note that custom-only scripts run via bare verifyx. - Soften the install guidance from "never globally" to a positive recommendation to pin it as a dev dependency for CI parity. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 43fe04b..4e5c342 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,30 @@ # @makerx/verify -A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code — and improve code quality for everyone. +**One command your AI agent runs after every change — and your CI runs on every push.** `verifyx` bundles a project's checks (lint, format, type-check, complexity, dead code, and more) behind a single command designed to give AI coding agents real back-pressure against shipping hard-to-maintain code, while keeping quality high for everyone. + +What makes it worth wiring in: + +- **Auto-fixes locally, fails in CI — same command.** Run it on your machine and it _fixes_ what it can (lint, formatting) instead of just complaining. Run it under CI and the identical command is check-only, so a PR can't merge with problems that should have been fixed. +- **Silent when green, so it's cheap to loop.** A passing run prints nothing and exits `0` — no output to burn an agent's tokens or bury the one failure that matters. Agents can run it as often as they like. +- **Failure output written for an agent to act on.** When a check fails it names the tool it ran, the exact command, and a docs link — so the agent (or you) knows what to fix and how, instead of guessing. +- **Convention over configuration.** Checks are just `verify:*` npm scripts run in parallel. Add, drop, or override any of them — no bespoke config format to learn. `verify` ships both: -- a **CLI** that orchestrates a set of checks by convention that are designed to be executed by AI agents as well as CI servers, and -- a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run those checks and fix what they report. +- a **CLI** (`verifyx`) that orchestrates those checks by convention, for AI agents and CI servers alike, and +- a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run the checks and fix what they report. Complexity was the first check. It is now just one of several, and the set grows over time. ## Install -Install as a **pinned dev dependency** — never globally. A locked version means the exact same tool runs on your machine and in CI/CD: +Install it as a **dev dependency** so the version is pinned in `package.json` — that way the exact same tool runs on your machine and in CI/CD: ```sh npm install --save-dev @makerx/verify ``` -Requires Node.js >= 24. Invoke it via an npm script or `npx verifyx` — not a global binary on `PATH`. +Requires Node.js >= 24. Invoke it via an npm script or `npx verifyx`. The quickest way to wire it into a project: @@ -48,7 +55,7 @@ There are three ways to invoke the CLI, from "all my checks" down to "one specif | Command | What it runs | | ----------------- | ---------------------------------------------------------------------------------------------------- | | `verifyx` | Every `verify:*` script — the set of checks you've curated (built-in and custom). None defined → nothing runs. | -| `verifyx all` | Every built-in check, with default options (ignores your `verify:*` list, except as overrides). | +| `verifyx all` | Every built-in check with default options; a matching `verify:` script overrides that built-in. | | `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | ### `verifyx` — run your curated checks @@ -72,7 +79,7 @@ You curate your checks by adding `verify:*` scripts. Prefer calling the built-in ### `verifyx all` — run every built-in -`verifyx all` skips your curated list and runs **every** built-in check with its default options — a quick "run everything" without wiring up scripts. Where you've defined a `verify:` script, it **overrides** the matching built-in, so you can swap one check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. +`verifyx all` runs **every** built-in check with its default options — a quick "run everything" without wiring up scripts. Where you've defined a `verify:` script for one of them, it **overrides** that built-in, so you can swap a single check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. (Custom `verify:*` scripts with no matching built-in aren't run by `verifyx all` — use bare `verifyx` for those.) ### Fix locally, check in CI From e86fbeea7d2db0c5a6f9fa02f4be2fe13ee05416 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:08:35 +0800 Subject: [PATCH 24/34] docs: reorder intro, drop stale line, make verifyx callout a NOTE box - Move "verify ships both" above the selling-points list and drop the "Complexity was the first check" line. - Convert the "Why is the command verifyx?" aside into a GitHub [!NOTE] alert box. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4e5c342..aac8b96 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ **One command your AI agent runs after every change — and your CI runs on every push.** `verifyx` bundles a project's checks (lint, format, type-check, complexity, dead code, and more) behind a single command designed to give AI coding agents real back-pressure against shipping hard-to-maintain code, while keeping quality high for everyone. +`verify` ships both: + +- a **CLI** (`verifyx`) that orchestrates those checks by convention, for AI agents and CI servers alike, and +- a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run the checks and fix what they report. + What makes it worth wiring in: - **Auto-fixes locally, fails in CI — same command.** Run it on your machine and it _fixes_ what it can (lint, formatting) instead of just complaining. Run it under CI and the identical command is check-only, so a PR can't merge with problems that should have been fixed. @@ -9,13 +14,6 @@ What makes it worth wiring in: - **Failure output written for an agent to act on.** When a check fails it names the tool it ran, the exact command, and a docs link — so the agent (or you) knows what to fix and how, instead of guessing. - **Convention over configuration.** Checks are just `verify:*` npm scripts run in parallel. Add, drop, or override any of them — no bespoke config format to learn. -`verify` ships both: - -- a **CLI** (`verifyx`) that orchestrates those checks by convention, for AI agents and CI servers alike, and -- a **verify skill** (`.claude/skills/`, `.agent-skills/`) + a one-line `CLAUDE.md`/`AGENTS.md` pointer that steer AI assistants to run the checks and fix what they report. - -Complexity was the first check. It is now just one of several, and the set grows over time. - ## Install Install it as a **dev dependency** so the version is pinned in `package.json` — that way the exact same tool runs on your machine and in CI/CD: @@ -32,7 +30,8 @@ The quickest way to wire it into a project: npx verifyx init ``` -> ### Why is the command `verifyx`, not `verify`? +> [!NOTE] +> **Why is the command `verifyx`, not `verify`?** > > The package is `@makerx/verify`, but the CLI binary is **`verifyx`**. `verify` is a built-in `cmd.exe` > command on Windows, and both `npm run` script bodies and `npx` resolve commands through `cmd` there — so a From dc40558fc09313ae18a86838447f7349fae0ed1d Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:13:32 +0800 Subject: [PATCH 25/34] docs: remove em-dashes, clarify comments row, fix verifyx all + API - Strip all em-dashes from the README (colons, commas, parentheses). - verifyx all: document that it also runs custom verify:* scripts, not just built-ins (matches the behaviour change in the runAll rework). - Note the verify::fix split works for custom scripts too, not only built-in overrides. - Rewrite the `comments` built-in row to be concrete: name --max-lines (default 2) instead of "the limit". - Fix stale Programmatic API export names (recommendedChecks / runAll, not defaultChecks / runDefaults); drop the duplicate standalone formula section now that the complexity docs cover it. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 128 +++++++++++++++++++++++++----------------------------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index aac8b96..3279562 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @makerx/verify -**One command your AI agent runs after every change — and your CI runs on every push.** `verifyx` bundles a project's checks (lint, format, type-check, complexity, dead code, and more) behind a single command designed to give AI coding agents real back-pressure against shipping hard-to-maintain code, while keeping quality high for everyone. +**One command your AI agent runs after every change, and your CI runs on every push.** `verifyx` bundles a project's checks (lint, format, type-check, complexity, dead code, and more) behind a single command designed to give AI coding agents real back-pressure against shipping hard-to-maintain code, while keeping quality high for everyone. `verify` ships both: @@ -9,14 +9,14 @@ What makes it worth wiring in: -- **Auto-fixes locally, fails in CI — same command.** Run it on your machine and it _fixes_ what it can (lint, formatting) instead of just complaining. Run it under CI and the identical command is check-only, so a PR can't merge with problems that should have been fixed. -- **Silent when green, so it's cheap to loop.** A passing run prints nothing and exits `0` — no output to burn an agent's tokens or bury the one failure that matters. Agents can run it as often as they like. -- **Failure output written for an agent to act on.** When a check fails it names the tool it ran, the exact command, and a docs link — so the agent (or you) knows what to fix and how, instead of guessing. -- **Convention over configuration.** Checks are just `verify:*` npm scripts run in parallel. Add, drop, or override any of them — no bespoke config format to learn. +- **Auto-fixes locally, fails in CI (same command).** Run it on your machine and it _fixes_ what it can (lint, formatting) instead of just complaining. Run it under CI and the identical command is check-only, so a PR can't merge with problems that should have been fixed. +- **Silent when green, so it's cheap to loop.** A passing run prints nothing and exits `0`, with no output to burn an agent's tokens or bury the one failure that matters. Agents can run it as often as they like. +- **Failure output written for an agent to act on.** When a check fails it names the tool it ran, the exact command, and a docs link, so the agent (or you) knows what to fix and how instead of guessing. +- **Convention over configuration.** Checks are just `verify:*` npm scripts run in parallel. Add, drop, or override any of them; there's no bespoke config format to learn. ## Install -Install it as a **dev dependency** so the version is pinned in `package.json` — that way the exact same tool runs on your machine and in CI/CD: +Install it as a **dev dependency** so the version is pinned in `package.json`, so the exact same tool runs on your machine and in CI/CD: ```sh npm install --save-dev @makerx/verify @@ -34,32 +34,32 @@ npx verifyx init > **Why is the command `verifyx`, not `verify`?** > > The package is `@makerx/verify`, but the CLI binary is **`verifyx`**. `verify` is a built-in `cmd.exe` -> command on Windows, and both `npm run` script bodies and `npx` resolve commands through `cmd` there — so a +> command on Windows, and both `npm run` script bodies and `npx` resolve commands through `cmd` there, so a > bare `verify` runs the Windows builtin ("VERIFY is off."), not this tool. Renaming the binary to `verifyx` -> (a nod to the fact it **fixes** as well as verifies) makes every invocation — `npx verifyx`, npm scripts, -> and a typed `verifyx` — work identically on macOS, Linux, and Windows. Your npm **script** can still be named +> (a nod to the fact it **fixes** as well as verifies) makes every invocation (`npx verifyx`, npm scripts, +> and a typed `verifyx`) work identically on macOS, Linux, and Windows. Your npm **script** can still be named > `verify` (that's a script lookup, not command resolution), so `npm run verify` works everywhere. ## How `verifyx` decides what to run -A **`verify:*` script** is any npm script in your `package.json` whose name starts with `verify:` — `verify:lint`, `verify:complexity`, `verify:custom`, and so on. Each one is a single **check**. `verifyx` runs them all **in parallel**, and together they form your project's verification gate. You decide which checks make up that gate by adding these scripts (`verifyx init` scaffolds a starting set). +A **`verify:*` script** is any npm script in your `package.json` whose name starts with `verify:` (for example `verify:lint`, `verify:complexity`, `verify:custom`). Each one is a single **check**. `verifyx` runs them all **in parallel**, and together they form your project's verification gate. You decide which checks make up that gate by adding these scripts (`verifyx init` scaffolds a starting set). A check is either **built-in** or **custom**: -- **Built-in checks** ship with `verifyx` (see [Built-in checks](#built-in-checks) below) and are invoked as subcommands — `verifyx lint`, `verifyx complexity`, … A `verify:*` script wires one in by calling it: `"verify:lint": "verifyx lint"`. -- **Custom checks** are any command of your own, wired as a `verify:*` script that runs whatever you like: `"verify:custom": "node ./scripts/my-check.mjs"`. They aren't `verifyx` subcommands — you run them through the gate, or directly via `npm run verify:custom`. +- **Built-in checks** ship with `verifyx` (see [Built-in checks](#built-in-checks) below) and are invoked as subcommands (`verifyx lint`, `verifyx complexity`, and so on). A `verify:*` script wires one in by calling it: `"verify:lint": "verifyx lint"`. +- **Custom checks** are any command of your own, wired as a `verify:*` script that runs whatever you like: `"verify:custom": "node ./scripts/my-check.mjs"`. They aren't `verifyx` subcommands; you run them through the gate, or directly via `npm run verify:custom`. There are three ways to invoke the CLI, from "all my checks" down to "one specific check": -| Command | What it runs | -| ----------------- | ---------------------------------------------------------------------------------------------------- | -| `verifyx` | Every `verify:*` script — the set of checks you've curated (built-in and custom). None defined → nothing runs. | -| `verifyx all` | Every built-in check with default options; a matching `verify:` script overrides that built-in. | -| `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | +| Command | What it runs | +| ----------------- | ------------------------------------------------------------------------------------------------------------------- | +| `verifyx` | Every `verify:*` script: the checks you've curated (built-in and custom). With none defined, nothing runs. | +| `verifyx all` | Every built-in check with default options, plus any custom `verify:*` scripts. A `verify:` overrides its built-in. | +| `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | -### `verifyx` — run your curated checks +### `verifyx`: run your curated checks -With no subcommand, `verifyx` runs every `verify:*` script in parallel — nothing implicit, the scripts you define **are** the gate. This is the mode a top-level `"verify": "verifyx"` script points at, so `npm run verify` runs your whole gate. +With no subcommand, `verifyx` runs every `verify:*` script in parallel. Nothing is implicit: the scripts you define **are** the gate. This is the mode a top-level `"verify": "verifyx"` script points at, so `npm run verify` runs your whole gate. A clean run is **completely silent**: no preamble, no per-script output, just exit `0`. Each script's output is buffered and printed **only if that script fails**, so `verifyx` is cheap to run in a loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table.) @@ -76,15 +76,15 @@ You curate your checks by adding `verify:*` scripts. Prefer calling the built-in } ``` -### `verifyx all` — run every built-in +### `verifyx all`: run everything -`verifyx all` runs **every** built-in check with its default options — a quick "run everything" without wiring up scripts. Where you've defined a `verify:` script for one of them, it **overrides** that built-in, so you can swap a single check's implementation without redefining the rest — e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. (Custom `verify:*` scripts with no matching built-in aren't run by `verifyx all` — use bare `verifyx` for those.) +`verifyx all` runs **every** built-in check with its default options, plus any custom `verify:*` scripts you've defined. It's the quick "run everything" without hand-curating a list. Where you've defined a `verify:` script matching a built-in, it **overrides** that built-in, so you can swap a single check's implementation without redefining the rest, e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. ### Fix locally, check in CI -Fixable checks (`lint`, `format`) **auto-fix by default** so the person — or AI agent — running `verifyx` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Force a mode with `verifyx --fix` or `verifyx --check`. +Fixable checks (`lint`, `format`) **auto-fix by default** so the person (or AI agent) running `verifyx` locally doesn't burn effort hand-fixing lint and formatting. When `CI` is set (as CI systems do), the same command is **check-only** and **fails** instead of rewriting, so a PR can't pass with unformatted or unlinted code. Force a mode with `verifyx --fix` or `verifyx --check`. -To give a script-based override that same split, pair a `verify:` (check-mode) script with a `verify::fix` variant. `verifyx` runs the `:fix` variant locally and the base script in CI — never both — so even an override wrapping a tool that doesn't know about fix-vs-check still fixes locally and only checks in CI: +The same split works for any `verify:` script, whether it overrides a built-in or runs a custom command: pair it with a `verify::fix` variant, and `verifyx` runs the `:fix` variant locally and the base script in CI (never both). So even a script wrapping a tool that doesn't know about fix-vs-check still fixes locally and only checks in CI: ```jsonc { @@ -97,28 +97,28 @@ To give a script-based override that same split, pair a `verify:` (check-m Flags on the bare `verifyx` command: -- `--check` / `--fix` — force check-only or auto-fix (defaults: fix locally, check under CI). -- `--measure` — print a status/duration summary table. -- `--verbose` — stream all output instead of suppressing passing runs. +- `--check` / `--fix`: force check-only or auto-fix (defaults: fix locally, check under CI). +- `--measure`: print a status/duration summary table. +- `--verbose`: stream all output instead of suppressing passing runs. ## Built-in checks -| Check | Kind | What it catches | -| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | -| `comments` | native | Comment blocks longer than the limit (JSDoc / `context:` exempt); with `--block-new-comments`, also any comment on a line changed against `HEAD` (machine directives like `eslint-disable` / `@ts-expect-error`, and `context:`, exempt). | -| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | -| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | -| `lint` | external | Lint — auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | -| `format` | external | Formatting — writes locally, checks in CI ([oxfmt](https://oxc.rs)). | -| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | -| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | -| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | -| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | +| Check | Kind | What it catches | +| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | +| `comments` | native | Flags comment blocks taller than `--max-lines` (default 2), to push for self-documenting code. JSDoc and `context:`-prefixed blocks are always allowed. Add `--block-new-comments` to also fail any comment on a line you changed vs `HEAD`. | +| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | +| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `lint` | external | Linting; auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | +| `format` | external | Formatting; writes locally, checks in CI ([oxfmt](https://oxc.rs)). | +| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | +| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | +| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | -External checks shell out to their tool and **skip gracefully when it is not installed** — `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. +External checks shell out to their tool and **skip gracefully when it is not installed**; `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. -Because checks are named for their function, **on failure** an external check prints the tool it used, the exact command it ran, and a docs link — so you (or an agent) can add the tool's config (e.g. `knip.json`) without guessing. On success it prints nothing (output is buffered and flushed only on failure, to keep runs quiet and cheap). If you override a check with your own `verify:` script, a failure shows that `npm run verify:` was what ran. +Because checks are named for their function, **on failure** an external check prints the tool it used, the exact command it ran, and a docs link, so you (or an agent) can add the tool's config (e.g. `knip.json`) without guessing. On success it prints nothing (output is buffered and flushed only on failure, to keep runs quiet and cheap). If you override a check with your own `verify:` script, a failure shows that `npm run verify:` was what ran. ### `complexity` @@ -126,19 +126,19 @@ Because checks are named for their function, **on failure** an external check pr verifyx complexity --threshold 50 "src/**/*.ts" ``` -- `[pattern]` — glob, directory, or file. Defaults to `{src,server,shared}/**/*.ts`. -- `--threshold ` — fail when any file's minimum maintainability index is below `n`. -- `--ignore ` — exclude files (repeatable; appended to the default `**/*test.ts*`). +- `[pattern]`: glob, directory, or file. Defaults to `{src,server,shared}/**/*.ts`. +- `--threshold `: fail when any file's minimum maintainability index is below `n`. +- `--ignore `: exclude files (repeatable; appended to the default `**/*test.ts*`). -It parses your `.ts`/`.tsx` sources with the TypeScript compiler API and, for every function, computes three metrics — **cyclomatic complexity** (independent paths through the code), **Halstead volume** (a size measure derived from the operators and operands used), and **SLOC** (source lines of code, excluding blanks and comments) — then combines them into a single **maintainability index (MI)**, a 0–100 score where lower means harder to maintain: +It parses your `.ts`/`.tsx` sources with the TypeScript compiler API and, for every function, computes three metrics: **cyclomatic complexity** (independent paths through the code), **Halstead volume** (a size measure derived from the operators and operands used), and **SLOC** (source lines of code, excluding blanks and comments). It then combines them into a single **maintainability index (MI)**, a 0–100 score where lower means harder to maintain: ``` MI = 171 - 5.2 * ln(HalsteadVolume) - 0.23 * CyclomaticComplexity - 16.2 * ln(SLOC) ``` -The result is clamped to 0–100; a function with zero Halstead volume or zero SLOC scores 100. As a rough guide: **> 65** is good, **50–65** is moderate (watch for growth), and **< 50** is hard to maintain. Thresholds are a matter of taste — pick one that fits your codebase and enforce it in CI. +The result is clamped to 0–100; a function with zero Halstead volume or zero SLOC scores 100. As a rough guide: **> 65** is good, **50–65** is moderate (watch for growth), and **< 50** is hard to maintain. Thresholds are a matter of taste; pick one that fits your codebase and enforce it in CI. -A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown (SLOC, cyclomatic complexity, Halstead metrics, and MI) is printed instead of the gate — handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). +A file's score is the **minimum MI across its functions**. When exactly one file matches, a detailed per-function breakdown (SLOC, cyclomatic complexity, Halstead metrics, and MI) is printed instead of the gate, handy for diagnosing one file at a time. **Fix a failure by splitting the file**, not by gaming the metric (deleting comments, joining lines, shortening names). ### `comments` @@ -148,17 +148,17 @@ verifyx comments --max-lines 2 --pushback "src/**/*.ts" By default it flags **long comment blocks**. `--block-new-comments` adds a stricter, diff-based gate on top: any comment on a line changed against `HEAD` fails (machine directives like `eslint-disable` / `@ts-expect-error` and `context:`-prefixed comments are exempt). -- `[pattern]` — glob, directory, or file to scan. -- `--max-lines ` — fail on comment blocks longer than `n` lines (default 2). -- `--pushback` — add AI back-pressure framing to the failure (keeping the comment "pages a human"). -- `--warn` — report the long-block violations without failing. -- `--block-new-comments` — also fail on any comment on a line changed against `HEAD`. -- `--ignore ` — exclude files (repeatable). +- `[pattern]`: glob, directory, or file to scan. +- `--max-lines `: fail on comment blocks longer than `n` lines (default 2). +- `--pushback`: add AI back-pressure framing to the failure (keeping the comment "pages a human"). +- `--warn`: report the long-block violations without failing. +- `--block-new-comments`: also fail on any comment on a line changed against `HEAD`. +- `--ignore `: exclude files (repeatable). Prefix a comment's first line with `context:` to keep genuinely durable context: ```ts -// context: the upstream API returns seconds, not milliseconds — do not "fix" this +// context: the upstream API returns seconds, not milliseconds, so do not "fix" this const timeoutMs = timeout * 1000 ``` @@ -172,20 +172,20 @@ Interactively wire verifications and the agent integration into the current proj verifyx init ``` -It first asks how `verify` should run — **run all built-in checks** (`verifyx all`, no `verify:*` scripts) or **pick specific checks** to wire up. Then you multi-select **agent targets** (Claude and/or other agents) — and, if you chose to pick, the **checks** — after which it: +It first asks how `verify` should run: **run all built-in checks** (`verifyx all`, no `verify:*` scripts) or **pick specific checks** to wire up. Then you multi-select **agent targets** (Claude and/or other agents), and, if you chose to pick, the **checks**. After that it: - writes the selected `verify:*` scripts to `package.json` (never clobbering existing ones), - installs the external checks' tools as `--save-dev`, -- writes the **`verify` skill** — the same `SKILL.md` to `.claude/skills/verify/` (Claude) and `.agent-skills/verify/` (cross-vendor), so the integration is identical everywhere, +- writes the **`verify` skill**: the same `SKILL.md` to `.claude/skills/verify/` (Claude) and `.agent-skills/verify/` (cross-vendor), so the integration is identical everywhere, - appends a one-line pointer to `CLAUDE.md` / `AGENTS.md` (only if not already present; existing content is never rewritten), -- if `unused-code` is selected, adds the other external tools (`oxlint`/`oxfmt`/`skott`/`jscpd`) to knip's `ignoreDependencies` — verifyx runs them at runtime, so knip can't see them and would otherwise report them as unused. Merged into `knip.json` or `package.json#knip` (created if neither exists), adding only what's missing; a code-based `knip.ts`/`knip.js` is left for you to edit. +- if `unused-code` is selected, adds the other external tools (`oxlint`/`oxfmt`/`skott`/`jscpd`) to knip's `ignoreDependencies` (verifyx runs them at runtime, so knip can't see them and would otherwise report them as unused). Merged into `knip.json` or `package.json#knip` (created if neither exists), adding only what's missing; a code-based `knip.ts`/`knip.js` is left for you to edit. The skill auto-triggers on "verify"/"run checks", so agents run the checks proactively; the pointer reinforces it for tools that read `CLAUDE.md`/`AGENTS.md` as standing instructions. Options: -- `--defaults-only` — the non-interactive form of the "run all built-in checks" choice: do **not** write `verify:*` scripts; wire the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes the skill + pointer). -- `--yes` — non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. +- `--defaults-only`: the non-interactive form of the "run all built-in checks" choice. Does **not** write `verify:*` scripts; wires the top-level `verify` script to `verifyx all` so it runs every built-in (still installs opted-in tools and writes the skill + pointer). +- `--yes`: non-interactive; use `--select ` (repeatable), `--no-claude`, `--agents`. ### `verifyx upgrade-docs` @@ -230,19 +230,11 @@ const { failing, passed } = analyzeComplexity({ }) ``` -Exports include `analyzeComplexity`, the check registry (`CHECKS`, `getCheck`, `defaultChecks`), `orchestrate`, `runDefaults`, `applyInit`, `loadVerifyConfig`, the individual `run*` check functions, and the lower-level complexity helpers (`calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, `forEachFunction`). - -## The maintainability index formula - -``` -MI = 171 - 5.2 * ln(HalsteadVolume) - 0.23 * CyclomaticComplexity - 16.2 * ln(SLOC) -``` - -Clamped to 0–100. Rough interpretation: **> 65** good, **50–65** moderate, **< 50** hard to maintain. +Exports include `analyzeComplexity`, the check registry (`CHECKS`, `getCheck`, `recommendedChecks`), `orchestrate`, `runAll`, `applyInit`, `loadVerifyConfig`, the individual `run*` check functions, and the lower-level complexity helpers (`calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, `forEachFunction`). ## Attribution -The `verify` runner, `block-comments`, `hardcoded-colors`, and `forbidden-strings` checks are ported from [staff0rd/assist](https://github.com/staff0rd/assist); the maintainability metrics originate there too. The idempotent agent-file scaffolding follows the MakerX data-streams CLI's `upgrade-docs`. See [Steering the Vibe: Verify](https://staffordwilliams.com/blog/2025/12/14/steering-the-vibe-verify/) and [Complexity](https://staffordwilliams.com/blog/2026/02/22/steering-the-vibe-complexity/). +The `verify` runner, `comments`, `hardcoded-colors`, and `forbidden-strings` checks are ported from [staff0rd/assist](https://github.com/staff0rd/assist); the maintainability metrics originate there too. The idempotent agent-file scaffolding follows the MakerX data-streams CLI's `upgrade-docs`. See [Steering the Vibe: Verify](https://staffordwilliams.com/blog/2025/12/14/steering-the-vibe-verify/) and [Complexity](https://staffordwilliams.com/blog/2026/02/22/steering-the-vibe-complexity/). ## License From 8d66245f5c26994d90cdc11fa505b2e626469d8f Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:15:33 +0800 Subject: [PATCH 26/34] docs: rewrite the comments section with real explanation - Open with why long comments are a problem (capable models, Opus 4.8 included, narrate changes with low-value comment blocks that rot and add maintenance load) and what the default check does about it. - Give --pushback the explanation it deserves: it is prompt-engineering baked into the lint failure, telling the agent the only way to keep a comment pages a human, which stops agents silencing the check. - Promote --block-new-comments to its own paragraph, led by the override-and-opt-in instruction, and spell out the exemptions. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3279562..ee9ec93 100644 --- a/README.md +++ b/README.md @@ -146,16 +146,20 @@ A file's score is the **minimum MI across its functions**. When exactly one file verifyx comments --max-lines 2 --pushback "src/**/*.ts" ``` -By default it flags **long comment blocks**. `--block-new-comments` adds a stricter, diff-based gate on top: any comment on a line changed against `HEAD` fails (machine directives like `eslint-disable` / `@ts-expect-error` and `context:`-prefixed comments are exempt). +Capable coding models (Opus 4.8 very much included) love to narrate their work: multi-line comment blocks explaining _what_ the code does rather than _why_. They add little value, drift out of sync as the code changes (and a later reader, human or LLM, may trust the stale comment over the code), and quietly grow the surface area you have to maintain. By default this check pushes back on exactly that, flagging any comment block taller than `--max-lines` (default 2) so the pressure is on the code to document itself. - `[pattern]`: glob, directory, or file to scan. - `--max-lines `: fail on comment blocks longer than `n` lines (default 2). -- `--pushback`: add AI back-pressure framing to the failure (keeping the comment "pages a human"). -- `--warn`: report the long-block violations without failing. -- `--block-new-comments`: also fail on any comment on a line changed against `HEAD`. +- `--pushback`: reframe the failure message as back-pressure aimed at an AI agent (see below). +- `--warn`: report the long-block violations without failing the run. +- `--block-new-comments`: also fail on any comment on a line changed against `HEAD` (see below). - `--ignore `: exclude files (repeatable). -Prefix a comment's first line with `context:` to keep genuinely durable context: +**`--pushback` is the clever bit.** An AI agent that hits a failing check will often take the path of least resistance and just delete or weaken the check to make the run pass. So rather than a dry error, the pushback message tells the agent that the _only_ sanctioned way to keep the comment is to prefix it with `context:`, and that doing so **pages a human to approve it**. Confronted with a real person's time on the line, the agent tends to reconsider and remove the low-value comment instead of gaming the gate. It is a small piece of prompt-engineering baked into a lint failure, and in practice it stops agents silencing the check far more reliably than a plain error does. + +If you want to go further and block **new** comments outright, override the `verify:comments` script and add `--block-new-comments`. That turns on a stricter, diff-based gate on top of the long-block check: any comment sitting on a line you changed against `HEAD` fails, whether you added or merely edited it. Machine directives (`eslint-disable`, `@ts-expect-error`, and friends) stay exempt, as does anything marked `context:`. + +Two escape hatches keep genuinely useful comments alive. **JSDoc** (`/** … */`) is always allowed, and prefixing a comment's first line with `context:` marks it as durable context the code itself can't express: ```ts // context: the upstream API returns seconds, not milliseconds, so do not "fix" this From 413bdbb636004a27de716885b405291428cb3728 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:16:37 +0800 Subject: [PATCH 27/34] feat: verifyx all is silent on success and runs custom verify:* scripts Two behaviour changes to `verifyx all`: - Silent on green: in-process (native + external) check output is now buffered and flushed only on failure, matching bare verifyx. A clean `verifyx all` prints nothing; use --verbose/--measure for the roll-call. Overrides already ran quietly; now they respect the same buffering when not loud. - Runs everything: custom verify:* scripts (those with no matching built-in) now run too, so `all` is a true superset of the curated gate rather than built-ins only. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 32 +++++++++---------- src/orchestrator/runAll.ts | 65 +++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ee9ec93..0bad0e3 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ A check is either **built-in** or **custom**: There are three ways to invoke the CLI, from "all my checks" down to "one specific check": -| Command | What it runs | -| ----------------- | ------------------------------------------------------------------------------------------------------------------- | -| `verifyx` | Every `verify:*` script: the checks you've curated (built-in and custom). With none defined, nothing runs. | +| Command | What it runs | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `verifyx` | Every `verify:*` script: the checks you've curated (built-in and custom). With none defined, nothing runs. | | `verifyx all` | Every built-in check with default options, plus any custom `verify:*` scripts. A `verify:` overrides its built-in. | -| `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | +| `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | ### `verifyx`: run your curated checks @@ -103,18 +103,18 @@ Flags on the bare `verifyx` command: ## Built-in checks -| Check | Kind | What it catches | -| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | -| `comments` | native | Flags comment blocks taller than `--max-lines` (default 2), to push for self-documenting code. JSDoc and `context:`-prefixed blocks are always allowed. Add `--block-new-comments` to also fail any comment on a line you changed vs `HEAD`. | -| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | -| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | -| `lint` | external | Linting; auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | -| `format` | external | Formatting; writes locally, checks in CI ([oxfmt](https://oxc.rs)). | -| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | -| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | -| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | -| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | +| Check | Kind | What it catches | +| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | +| `comments` | native | Flags comment blocks taller than `--max-lines` (default 2), to push for self-documenting code. JSDoc and `context:`-prefixed blocks are always allowed. Add `--block-new-comments` to also fail any comment on a line you changed vs `HEAD`. | +| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | +| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `lint` | external | Linting; auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | +| `format` | external | Formatting; writes locally, checks in CI ([oxfmt](https://oxc.rs)). | +| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | +| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | +| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | External checks shell out to their tool and **skip gracefully when it is not installed**; `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. diff --git a/src/orchestrator/runAll.ts b/src/orchestrator/runAll.ts index 140deea..76f1b70 100644 --- a/src/orchestrator/runAll.ts +++ b/src/orchestrator/runAll.ts @@ -5,43 +5,78 @@ import { resolveMode } from '../shared/mode.ts' import { runCommand } from '../shared/spawn.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' import { chatty, reportOutcomes } from './report.ts' -import { resolveEntries, resolveOverride } from './resolveEntries.ts' +import { entryCheckName, resolveEntries, resolveOverride, selectEntries } from './resolveEntries.ts' export type RunAllOptions = { measure?: boolean } +// Run a native/external check in-process, buffering its console output so a passing check stays silent (flushed on failure). +async function runQuietly(run: () => Promise): Promise { + const chunks: string[] = [] + const original = { log: console.log, warn: console.warn, error: console.error } + const capture = (...args: unknown[]): void => void chunks.push(args.map(String).join(' ')) + console.log = capture + console.warn = capture + console.error = capture + let result: CheckResult + try { + result = await run() + } finally { + Object.assign(console, original) + } + if (!result.ok && chunks.length > 0) console.log(chunks.join('\n')) + return result +} + /** - * Run every built-in check (the explicit `verifyx all` opt-in). A check runs in-process unless the project - * defines a matching `verify:` script (or `verify::fix` in fix mode), in which case that script - * runs instead (per-check override). A clean run is quiet — use `--verbose` / `--measure` for the roll-call. + * Run every built-in check (the explicit `verifyx all` opt-in) plus any custom `verify:*` scripts, so `all` + * truly runs everything. A built-in runs in-process unless the project defines a matching `verify:` + * script (or `verify::fix` in fix mode), which overrides it. A clean run is quiet: passing checks print + * nothing (output is buffered and flushed only on failure), use `--verbose` / `--measure` for the roll-call. */ export async function runAll(opts: RunAllOptions = {}): Promise { const entries = resolveEntries() const mode = resolveMode() const loud = chatty(opts.measure) + const builtinNames = new Set(CHECKS.map((c) => c.name)) + const customEntries = selectEntries(entries, mode).filter((entry) => !builtinNames.has(entryCheckName(entry.name))) + if (loud) { - console.log(`Running all ${CHECKS.length} built-in verification(s):`) - for (const check of CHECKS) { + console.log(`Running all ${CHECKS.length} built-in verification(s)${customEntries.length ? ` + ${customEntries.length} custom` : ''}:`) + for (const check of CHECKS) console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) - } + for (const entry of customEntries) console.log(` - ${entry.name}${color.dim(' (custom)')}`) console.log() } const startTime = Date.now() const records: MeasureRecord[] = [] const results: CheckResult[] = [] - for (const check of CHECKS) { + + const record = async (name: string, run: () => Promise): Promise => { const checkStart = Date.now() - if (loud) console.log(color.heading(`▶ ${check.name}`)) - const override = resolveOverride(entries, check.name, mode) - const ok = override ? (await runCommand(override.command, { cwd: override.cwd })) === 0 : (await check.runDefault()).ok - // On an override failure, show the script that actually ran (only on failure — passing runs stay silent). - if (override && !ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) - results.push({ name: check.name, ok }) - records.push({ script: check.name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) + if (loud) console.log(color.heading(`▶ ${name}`)) + const ok = await run() + results.push({ name, ok }) + records.push({ script: name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) if (loud) console.log() } + for (const check of CHECKS) { + const override = resolveOverride(entries, check.name, mode) + await record(check.name, async () => { + if (!override) return loud ? (await check.runDefault()).ok : (await runQuietly(() => check.runDefault())).ok + const ok = (await runCommand(override.command, { cwd: override.cwd, quiet: !loud })) === 0 + // On an override failure, name the script that actually ran (only on failure: passing runs stay silent). + if (!ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) + return ok + }) + } + + for (const entry of customEntries) { + await record(entry.name, async () => (await runCommand(entry.command, { cwd: entry.cwd, quiet: !loud })) === 0) + } + if (opts.measure) printMeasureTable(records, Date.now() - startTime) return reportOutcomes(results, 'verification', loud) } From 488a126b541bc389077ba6e940c11b68f2602dc7 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:20:17 +0800 Subject: [PATCH 28/34] docs: per-check config inline, external config files, richer CI/CD + API - Document each native check config where the check is described (hardcoded-colors and forbidden-strings gain their own sections with CLI flags + verify-config keys); reframe the Configuration section as a reference and note external checks use their own tool config. - List each external check config file with a docs link. - Pull the silent-on-success note up as a general property of both orchestration modes (now that verifyx all shares it), and note single built-in runs still print their report. - Expand CI/CD: explain CI-mode switch, give a full GitHub Actions job, and describe the non-zero exit + flushed output. - Rewrite Programmatic API: clarify external checks run via the registry (getCheck().runDefault()), and list entry points by area. - Drop the data-streams upgrade-docs attribution line. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0bad0e3..f767d22 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,12 @@ There are three ways to invoke the CLI, from "all my checks" down to "one specif | `verifyx all` | Every built-in check with default options, plus any custom `verify:*` scripts. A `verify:` overrides its built-in. | | `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | +Both orchestration modes below (`verifyx` and `verifyx all`) are **completely silent on success**: no preamble, no per-check output, just exit `0`. Each check's output is buffered and printed **only if it fails**, so a run is cheap to loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table. Running a single built-in, `verifyx `, always prints its report, since you asked for that check specifically.) + ### `verifyx`: run your curated checks With no subcommand, `verifyx` runs every `verify:*` script in parallel. Nothing is implicit: the scripts you define **are** the gate. This is the mode a top-level `"verify": "verifyx"` script points at, so `npm run verify` runs your whole gate. -A clean run is **completely silent**: no preamble, no per-script output, just exit `0`. Each script's output is buffered and printed **only if that script fails**, so `verifyx` is cheap to run in a loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table.) - You curate your checks by adding `verify:*` scripts. Prefer calling the built-ins (`verifyx `) so their fix-vs-check behaviour stays centralised; drop to a raw command only for something bespoke: ```jsonc @@ -120,6 +120,15 @@ External checks shell out to their tool and **skip gracefully when it is not ins Because checks are named for their function, **on failure** an external check prints the tool it used, the exact command it ran, and a docs link, so you (or an agent) can add the tool's config (e.g. `knip.json`) without guessing. On success it prints nothing (output is buffered and flushed only on failure, to keep runs quiet and cheap). If you override a check with your own `verify:` script, a failure shows that `npm run verify:` was what ran. +Each external check is configured through its **tool's own config file**, exactly as you would use that tool standalone: + +- `lint`: [`.oxlintrc.json`](https://oxc.rs/docs/guide/usage/linter.html) +- `format`: [oxfmt configuration](https://oxc.rs) +- `check-types`: your [`tsconfig.json`](https://www.typescriptlang.org/tsconfig) +- `unused-code`: [`knip.json`](https://knip.dev/reference/configuration) +- `circular-deps`: [skott options](https://github.com/antoine-coulon/skott) +- `duplicate-code`: [`.jscpd.json` or `package.json#jscpd`](https://github.com/kucherenko/jscpd/tree/master/apps/jscpd#config) + ### `complexity` ```sh @@ -153,7 +162,7 @@ Capable coding models (Opus 4.8 very much included) love to narrate their work: - `--pushback`: reframe the failure message as back-pressure aimed at an AI agent (see below). - `--warn`: report the long-block violations without failing the run. - `--block-new-comments`: also fail on any comment on a line changed against `HEAD` (see below). -- `--ignore `: exclude files (repeatable). +- `--ignore `: exclude files (repeatable; the `--block-new-comments` gate also reads `comments.ignore` from your [verify config](#configuration)). **`--pushback` is the clever bit.** An AI agent that hits a failing check will often take the path of least resistance and just delete or weaken the check to make the run pass. So rather than a dry error, the pushback message tells the agent that the _only_ sanctioned way to keep the comment is to prefix it with `context:`, and that doing so **pages a human to approve it**. Confronted with a real person's time on the line, the agent tends to reconsider and remove the low-value comment instead of gaming the gate. It is a small piece of prompt-engineering baked into a lint failure, and in practice it stops agents silencing the check far more reliably than a plain error does. @@ -166,6 +175,35 @@ Two escape hatches keep genuinely useful comments alive. **JSDoc** (`/** … */` const timeoutMs = timeout * 1000 ``` +### `hardcoded-colors` + +```sh +verifyx hardcoded-colors --root src +``` + +Fails on literal hex or `0x` colour values in source (`.ts`, `.tsx`, `.css`, `.scss`, `.vue`, `.svelte`, and similar), nudging you toward named design tokens. It is pure JavaScript (no `grep`), so it behaves the same on every OS. + +- `--root `: directory to scan (default `src`). +- `--ignore `: exclude files (repeatable). + +Both default from your [verify config](#configuration) (`hardcodedColors.root` and `hardcodedColors.ignore`), so you can set them once instead of in the script. + +### `forbidden-strings` + +```sh +verifyx forbidden-strings +``` + +Fails when a configured JSON value matches a disallowed glob, handy for catching things like a `debug` log level or a staging URL left in a committed config file. It is entirely config-driven (no flags): define rules under `forbiddenStrings` in your [verify config](#configuration). Each rule reads its `file`, looks up every dotted `paths` entry, and fails if the value matches the `disallowed` glob: + +```jsonc +{ + "verify": { + "forbiddenStrings": [{ "file": "app.json", "paths": ["env.LOG_LEVEL"], "disallowed": "debug" }], + }, +} +``` + ## Scaffolding a project ### `verifyx init` @@ -202,7 +240,7 @@ verifyx upgrade-docs --no-agents # only .claude/ + CLAUDE.md ## Configuration -Some checks read per-repo config from `verify.config.json`, or a `verify` key in `package.json`: +The **native** checks that take persistent settings read them from a `verify.config.json` file, or a `verify` key in `package.json` (the standalone file wins if both are present). Each option is documented alongside its check above; collected here for reference: ```jsonc { @@ -214,31 +252,58 @@ Some checks read per-repo config from `verify.config.json`, or a `verify` key in } ``` +**External** checks don't use this file; each is configured through its own tool's config (`.oxlintrc.json`, `tsconfig.json`, `knip.json`, and so on), listed under [Built-in checks](#built-in-checks). + ## CI/CD -Because it is a pinned dev dependency, CI runs the identical tool: +`verifyx` is built to run the **same command** locally and in CI. The only thing that changes is the `CI` environment variable: when it's set (GitHub Actions, GitLab CI, and most providers export it automatically), `verifyx` switches from **fix** mode to **check** mode. Nothing is rewritten; any lint, formatting, type, complexity, or other issue **fails the job** instead, so a PR can't merge with a problem that should have been fixed locally first. + +Because the tool is a pinned dev dependency, CI runs the exact version in your lockfile. A minimal GitHub Actions job: ```yaml -- run: npm ci -- run: npm run verify +name: verify +on: [push, pull_request] +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run verify ``` +`npm run verify` exits non-zero on the first failing check (failing the job), and its buffered output is flushed so the log shows exactly what failed and how to fix it. To force a mode regardless of environment, use `verifyx --check` or `verifyx --fix`. + ## Programmatic API +Every export is available from the package root: + ```ts -import { analyzeComplexity, orchestrate, CHECKS } from '@makerx/verify' +import { analyzeComplexity, getCheck, runAll } from '@makerx/verify' + +// Run the maintainability analysis directly. +const { failing } = analyzeComplexity({ pattern: 'src/**/*.ts', threshold: 50 }) -const { failing, passed } = analyzeComplexity({ - pattern: 'src/**/*.ts', - threshold: 50, -}) +// Run any single check by name, including the ones that shell out to an external tool. +const lint = await getCheck('lint')?.runDefault() ``` -Exports include `analyzeComplexity`, the check registry (`CHECKS`, `getCheck`, `recommendedChecks`), `orchestrate`, `runAll`, `applyInit`, `loadVerifyConfig`, the individual `run*` check functions, and the lower-level complexity helpers (`calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`, `scoreFiles`, `findSourceFiles`, `forEachFunction`). +Native checks also expose a direct runner (`runComplexity`, `runComments`, `runHardcodedColors`, `runForbiddenStrings`); external checks (`lint`, `format`, `check-types`, `unused-code`, `circular-deps`, `duplicate-code`) have no standalone function and are run via the registry (`getCheck(name)?.runDefault()`) or the orchestrators. + +Entry points: + +- **Checks**: `CHECKS`, `getCheck`, `recommendedChecks`, and the native `run*` functions above. +- **Orchestration**: `orchestrate` (the bare `verifyx` runner) and `runAll` (`verifyx all`). +- **Complexity internals**: `analyzeComplexity`, `scoreFiles`, `findSourceFiles`, `resolvePattern`, `forEachFunction`, `findLongCommentBlocks`, and the metric helpers `calculateCyclomaticComplexity`, `calculateHalstead`, `calculateMaintainabilityIndex`, `countSloc`. +- **Scaffolding & config**: `applyInit` and `loadVerifyConfig`. ## Attribution -The `verify` runner, `comments`, `hardcoded-colors`, and `forbidden-strings` checks are ported from [staff0rd/assist](https://github.com/staff0rd/assist); the maintainability metrics originate there too. The idempotent agent-file scaffolding follows the MakerX data-streams CLI's `upgrade-docs`. See [Steering the Vibe: Verify](https://staffordwilliams.com/blog/2025/12/14/steering-the-vibe-verify/) and [Complexity](https://staffordwilliams.com/blog/2026/02/22/steering-the-vibe-complexity/). +The `verify` runner, `comments`, `hardcoded-colors`, and `forbidden-strings` checks are ported from [staff0rd/assist](https://github.com/staff0rd/assist); the maintainability metrics originate there too. See [Steering the Vibe: Verify](https://staffordwilliams.com/blog/2025/12/14/steering-the-vibe-verify/) and [Complexity](https://staffordwilliams.com/blog/2026/02/22/steering-the-vibe-complexity/). ## License From a2b5d3a9b090870ebc6b13e4e16cf3ac6393cda1 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:28:23 +0800 Subject: [PATCH 29/34] feat: verifyx runs tests by convention, with --no-tests to opt out verify now runs the project test suite as part of a run, without wiring a check: - locally: verify:test if present, else the standard test script; - on CI (CI env set): only test:ci, if present (a plain test never runs on CI, which usually needs a different invocation for junit output). No test:ci means no tests on CI. The test run is buffered like any other check (silent on success, streamed with --verbose) and is a normal gate entry (shows in --measure, fails the run). It applies to both bare verifyx and verifyx all. --no-tests skips the step, for when CI runs tests in a dedicated step. Wire the PR/publish workflows to `npm run verify -- --no-tests` (the `--` matters, or npm eats the flag) since they run test:ci separately. The tests step owns the `test` check name: an explicit verify:test is run by the step, not the normal batch, so --no-tests governs it and it never double-runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pr.yml | 5 +++-- .github/workflows/publish.yml | 5 +++-- README.md | 11 +++++++++++ src/cli.ts | 5 +++-- src/orchestrator/resolveEntries.ts | 24 ++++++++++++++---------- src/orchestrator/run.ts | 18 +++++++++--------- src/orchestrator/runAll.ts | 12 +++++++++--- src/orchestrator/tests.test.ts | 20 ++++++++++++++++++++ src/orchestrator/tests.ts | 29 +++++++++++++++++++++++++++++ 9 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 src/orchestrator/tests.test.ts create mode 100644 src/orchestrator/tests.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2ed97f4..b32bffa 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,7 +16,8 @@ jobs: node-version: 24.x run-build: true build-script: npm run build - # Runs the non-editing verifications (lint/format checks, complexity, comment-block, types). - lint-script: npm run verify + # Runs the non-editing verifications (lint/format checks, complexity, comments, types). Tests run + # separately via test-script, so --no-tests stops verify from running them a second time. + lint-script: npm run verify -- --no-tests test-script: npm run test:ci audit-script: npm run audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 68dfd39..87b9376 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,8 +17,9 @@ jobs: with: working-directory: . node-version: 24.x - # Runs the non-editing verifications (lint/format checks, complexity, comment-block, types). - lint-script: npm run verify + # Runs the non-editing verifications (lint/format checks, complexity, comments, types). Tests run + # separately via test-script, so --no-tests stops verify from running them a second time. + lint-script: npm run verify -- --no-tests audit-script: npm run audit test-script: npm run test:ci output-test-results: true diff --git a/README.md b/README.md index f767d22..a65bc9c 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,22 @@ The same split works for any `verify:` script, whether it overrides a buil } ``` +### Tests + +`verifyx` runs your test suite as part of a verify run, by convention, so you don't have to wire it in as a check: + +- **Locally** it runs your `verify:test` script if you have one, otherwise your standard `test` script. The run is buffered like any other check (silent on success, shown on failure), so it stays cheap in a loop; stream it with `--verbose`. +- **On CI** (`CI` set) it runs only a `test:ci` script, if present. A plain `test` (or `verify:test`) never runs on CI, because CI usually needs a different invocation (emitting `junit.xml`, coverage, and so on). With no `test:ci`, verify runs no tests on CI; run them in a separate step. +- **`--no-tests`** skips the step entirely, handy when CI already runs tests in a dedicated step, or under `verifyx all`. + +The test run is just another entry in the gate: it appears in `--measure` and fails the overall run if it fails. + Flags on the bare `verifyx` command: - `--check` / `--fix`: force check-only or auto-fix (defaults: fix locally, check under CI). - `--measure`: print a status/duration summary table. - `--verbose`: stream all output instead of suppressing passing runs. +- `--no-tests`: skip the automatic tests step. ## Built-in checks diff --git a/src/cli.ts b/src/cli.ts index 01b2924..fe97e34 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { setVerbose } from './shared/spawn.ts' const require = createRequire(import.meta.url) const pkg = require('../package.json') as { version: string } -type RunOptions = { measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean } +type RunOptions = { measure?: boolean; verbose?: boolean; check?: boolean; fix?: boolean; tests?: boolean } /** The options shared by the default run and `verifyx all`. */ function withRunOptions(command: Command): Command { @@ -24,6 +24,7 @@ function withRunOptions(command: Command): Command { .option('--verbose', 'stream all output instead of suppressing passing runs') .option('--check', 'check only — never auto-fix (the default under CI)') .option('--fix', 'auto-fix where possible (the default locally)') + .option('--no-tests', 'skip the automatic tests step (verify:test / test locally, test:ci on CI)') } const program = new Command() @@ -44,7 +45,7 @@ withRunOptions( ).action(async (opts: RunOptions) => { setVerbose(!!opts.verbose) configureMode(opts) - process.exitCode = await runAll({ measure: opts.measure }) + process.exitCode = await runAll({ measure: opts.measure, tests: opts.tests }) }) registerChecks(program) diff --git a/src/orchestrator/resolveEntries.ts b/src/orchestrator/resolveEntries.ts index 26c995a..8e60e79 100644 --- a/src/orchestrator/resolveEntries.ts +++ b/src/orchestrator/resolveEntries.ts @@ -32,21 +32,25 @@ function findPackageJson(startDir: string): string | null { type PackageJson = { scripts?: Record } -/** Collect the project's `verify:*` npm scripts as parallelisable entries, nearest package.json wins. */ -export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { +/** The nearest package.json's scripts + its directory, or null when none is found / it can't be parsed. */ +export function loadPackageScripts(cwd: string = process.cwd()): { scripts: Record; dir: string } | null { const pkgPath = findPackageJson(cwd) - if (!pkgPath) return [] - let pkg: PackageJson + if (!pkgPath) return null try { - pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as PackageJson + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as PackageJson + return { scripts: pkg.scripts ?? {}, dir: path.dirname(pkgPath) } } catch { - return [] + return null } - const scripts = pkg.scripts ?? {} - const dir = path.dirname(pkgPath) - return Object.keys(scripts) +} + +/** Collect the project's `verify:*` npm scripts as parallelisable entries, nearest package.json wins. */ +export function resolveEntries(cwd: string = process.cwd()): VerifyEntry[] { + const loaded = loadPackageScripts(cwd) + if (!loaded) return [] + return Object.keys(loaded.scripts) .filter((name) => name.startsWith(VERIFY_PREFIX)) - .map((name) => ({ name, command: `npm run ${name}`, cwd: dir })) + .map((name) => ({ name, command: `npm run ${name}`, cwd: loaded.dir })) } /** diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index c15606e..de48cc5 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -2,13 +2,16 @@ import { configureMode, resolveMode } from '../shared/mode.ts' import { runCommand, setVerbose } from '../shared/spawn.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' import { chatty, reportOutcomes } from './report.ts' -import { resolveEntries, selectEntries, type VerifyEntry } from './resolveEntries.ts' +import { entryCheckName, resolveEntries, selectEntries, type VerifyEntry } from './resolveEntries.ts' +import { resolveTestEntry, TEST_CHECK_NAME } from './tests.ts' export type OrchestrateOptions = { measure?: boolean verbose?: boolean check?: boolean fix?: boolean + /** When false, skip the automatic tests step (`--no-tests`). */ + tests?: boolean } async function runEntry(entry: VerifyEntry): Promise { @@ -27,16 +30,13 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise/:fix pairs per mode; the tests step owns the `test` check, so it's dropped and re-added below. + const gate = selectEntries(resolveEntries(), resolveMode()).filter((entry) => entryCheckName(entry.name) !== TEST_CHECK_NAME) + const testEntry = resolveTestEntry({ noTests: opts.tests === false }) + const entries = testEntry ? [...gate, testEntry] : gate - // Collapse verify: / verify::fix pairs to the variant for the current mode. - const entries = selectEntries(allEntries, resolveMode()) if (entries.length === 0) { - console.log('No verify:* scripts to run for this mode.') + console.log('No verify:* scripts defined — nothing to run. Add verify:* scripts, or run `verifyx all` to run every built-in check.') return 0 } diff --git a/src/orchestrator/runAll.ts b/src/orchestrator/runAll.ts index 76f1b70..27cb300 100644 --- a/src/orchestrator/runAll.ts +++ b/src/orchestrator/runAll.ts @@ -6,8 +6,9 @@ import { runCommand } from '../shared/spawn.ts' import { type MeasureRecord, printMeasureTable } from './measure.ts' import { chatty, reportOutcomes } from './report.ts' import { entryCheckName, resolveEntries, resolveOverride, selectEntries } from './resolveEntries.ts' +import { resolveTestEntry, TEST_CHECK_NAME } from './tests.ts' -export type RunAllOptions = { measure?: boolean } +export type RunAllOptions = { measure?: boolean; tests?: boolean } // Run a native/external check in-process, buffering its console output so a passing check stays silent (flushed on failure). async function runQuietly(run: () => Promise): Promise { @@ -38,14 +39,19 @@ export async function runAll(opts: RunAllOptions = {}): Promise { const mode = resolveMode() const loud = chatty(opts.measure) + // Custom verify:* scripts that don't map to a built-in run too, minus the `test` check the tests step owns. const builtinNames = new Set(CHECKS.map((c) => c.name)) - const customEntries = selectEntries(entries, mode).filter((entry) => !builtinNames.has(entryCheckName(entry.name))) + const customEntries = selectEntries(entries, mode).filter( + (entry) => !builtinNames.has(entryCheckName(entry.name)) && entryCheckName(entry.name) !== TEST_CHECK_NAME, + ) + const testEntry = resolveTestEntry({ noTests: opts.tests === false }) if (loud) { console.log(`Running all ${CHECKS.length} built-in verification(s)${customEntries.length ? ` + ${customEntries.length} custom` : ''}:`) for (const check of CHECKS) console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) for (const entry of customEntries) console.log(` - ${entry.name}${color.dim(' (custom)')}`) + if (testEntry) console.log(` - ${testEntry.name}${color.dim(' (tests)')}`) console.log() } @@ -73,7 +79,7 @@ export async function runAll(opts: RunAllOptions = {}): Promise { }) } - for (const entry of customEntries) { + for (const entry of [...customEntries, ...(testEntry ? [testEntry] : [])]) { await record(entry.name, async () => (await runCommand(entry.command, { cwd: entry.cwd, quiet: !loud })) === 0) } diff --git a/src/orchestrator/tests.test.ts b/src/orchestrator/tests.test.ts new file mode 100644 index 0000000..6726051 --- /dev/null +++ b/src/orchestrator/tests.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' + +import { resolveTestScript } from './tests.ts' + +describe('resolveTestScript', () => { + it('prefers an explicit verify:test locally, then the standard test script', () => { + expect(resolveTestScript({ 'verify:test': 'vitest run --silent', test: 'vitest' }, false)).toBe('verify:test') + expect(resolveTestScript({ test: 'vitest' }, false)).toBe('test') + }) + + it('runs only test:ci on CI, never a plain test/verify:test', () => { + expect(resolveTestScript({ 'verify:test': 'x', test: 'y', 'test:ci': 'vitest run --reporter=junit' }, true)).toBe('test:ci') + expect(resolveTestScript({ 'verify:test': 'x', test: 'y' }, true)).toBeNull() + }) + + it('returns null when no applicable script exists', () => { + expect(resolveTestScript({ build: 'tsc' }, false)).toBeNull() + expect(resolveTestScript({}, true)).toBeNull() + }) +}) diff --git a/src/orchestrator/tests.ts b/src/orchestrator/tests.ts new file mode 100644 index 0000000..77650b4 --- /dev/null +++ b/src/orchestrator/tests.ts @@ -0,0 +1,29 @@ +import { loadPackageScripts, type VerifyEntry } from './resolveEntries.ts' + +/** The `verify:*` check name the tests step owns, so it isn't also run in the normal batch. */ +export const TEST_CHECK_NAME = 'test' + +export type TestsOptions = { noTests?: boolean } + +/** + * Which npm script the automatic tests step runs. Locally it prefers an explicit `verify:test`, falling back + * to the standard `test` script. On CI it runs only `test:ci` (CI usually needs a different invocation, e.g. + * emitting junit.xml), so a plain `test` never runs there. Returns null when no applicable script exists. + */ +export function resolveTestScript(scripts: Record, ci: boolean): string | null { + const order = ci ? ['test:ci'] : ['verify:test', 'test'] + return order.find((name) => name in scripts) ?? null +} + +/** + * Resolve the automatic tests step to a runnable entry, so it flows through the same buffered/timed path as + * every other check. Returns null when `--no-tests` is set or no applicable test script exists. + */ +export function resolveTestEntry(opts: TestsOptions = {}): VerifyEntry | null { + if (opts.noTests) return null + const loaded = loadPackageScripts() + if (!loaded) return null + const script = resolveTestScript(loaded.scripts, !!process.env.CI) + if (!script) return null + return { name: script, command: `npm run ${script}`, cwd: loaded.dir } +} From 770a81fc7e29ddc5d7b93319cbfe69b5e3fba705 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 04:29:34 +0800 Subject: [PATCH 30/34] docs: slim CLAUDE.md and fold in the dogfood verify pointer Cut the CLAUDE.md detail that now lives in README (mode propagation, orchestrator internals) down to what an agent working in this repo actually needs: the verifyx-not-verify naming + dev shim, the --max-lines 1 comments gotcha, and the duplicate-code helper gotcha. Add the same "run npm run verify and fix what it reports" pointer that `verifyx init` scaffolds into a consumer, so the repo dogfoods it. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e72e3da..b4fb51a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,15 @@ # @makerx/verify -A growing collection of code **verifications** that give AI coding agents back-pressure against writing hard-to-maintain code. Ships a `verifyx` CLI that orchestrates native + external checks by convention, plus a scaffolder (`verifyx init` / `verifyx upgrade-docs`) that drops the checks, a `verify` skill (`.claude/skills` + `.agent-skills`), and a `CLAUDE.md`/`AGENTS.md` pointer into a project. +A `verifyx` CLI that orchestrates native + external code checks by convention, plus a scaffolder (`verifyx init` / `upgrade-docs`) that drops the checks, a `verify` skill (`.claude/skills` + `.agent-skills`), and a `CLAUDE.md`/`AGENTS.md` pointer into a project. See [README.md](./README.md) for the full design and check reference. -**The CLI binary is `verifyx`, not `verify`.** `verify` is a Windows `cmd` builtin, and `npm run` bodies + `npx` resolve through `cmd` there, so a bare `verify` runs the builtin. `verifyx` (verify + fix) avoids that on all platforms. The npm **script** stays named `verify` (`npm run verify` is a script lookup, not command resolution) and the package stays `@makerx/verify`. Locally, `prepare` (`scripts/dev-verifyx-bin.mjs`) writes a `node_modules/.bin/verifyx` shim → `node src/cli.ts`, so the repo's own `verify:*` scripts call `verifyx ` exactly like a consumer's; `prepare` never runs for registry consumers. +## Verification -## After making changes, run `npm run verify` +After making code changes, run `npm run verify` and fix everything it reports before finishing. Don't silence checks or game the metrics. -`npm run verify` runs `verifyx` — the orchestrator over the repo's own `verify:*` scripts, which call the built-in checks (`verifyx lint|format|check-types|complexity|comments|…`). It runs them in parallel and suppresses output unless one fails. Bare `verifyx` runs **only** the defined `verify:*` scripts (nothing if none); `verifyx all` runs **every** built-in check, with a `verify:` script overriding its matching built-in. +`npm run verify` runs this repo's own `verify:*` scripts (the built-in checks) in parallel plus the test suite, and stays silent unless something fails. It auto-fixes lint/format locally and is check-only under CI. -**Fix locally, check in CI.** Run locally, `verifyx` **auto-fixes** what it can (`oxlint --fix`, `oxfmt`) so you don't waste effort hand-fixing lint/format. Under CI (`CI` env set — both workflows call `npm run verify` via the shared workflow's `lint-script`) the same command is **check-only** and fails if anything isn't already right. Force a mode with `verifyx --check` / `verifyx --fix`. (Mode propagates to the `verify:*` child scripts via the `VERIFY_MODE` env, so they carry no `--check`/`--fix` flags — those live only on the root command.) +## Working in this repo -`verify:comments` runs with `--pushback`, so a flagged comment block prints a warning that keeping it pages a human. Take that seriously: delete the comment, or make the code self-explanatory, before reaching for the `context:` escape hatch. +- **The CLI binary is `verifyx`, not `verify`** — `verify` is a Windows `cmd` builtin that shadows it under `npm run`/`npx`. The npm _script_ stays named `verify` (so `npm run verify` works) and the package stays `@makerx/verify`. In dev, the `prepare` script (`scripts/dev-verifyx-bin.mjs`) writes a `node_modules/.bin/verifyx` shim pointing at `node src/cli.ts`, so the repo dogfoods `verifyx ` from source; run the CLI directly with `node src/cli.ts `. +- **`verify:comments` runs with `--max-lines 1`**, so never write a 2-line `//` comment block — use a single line, JSDoc (`/** … */`), or a `// context:` prefix. This is the check that trips changes most often. +- **`duplicate-code` (jscpd)** fails on near-identical blocks — extract a shared helper rather than repeating option lists or logic. From 4545177dc8748dcac318f622f250b813d6520c45 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 11:21:52 +0800 Subject: [PATCH 31/34] fix: verifyx all now honours run flags passed after the subcommand The shared run options (--measure, --verbose, --check, --fix, --no-tests) are declared on the root command, which commander treats as global. Passed after `all` they bound to the root, not the `all` subcommand, so runAll only saw the subcommand opts and ignored them: `verifyx all --measure` printed no table, and `verifyx all --no-tests` did not skip tests (now relevant since `verify` is `verifyx all` and CI passes --no-tests). Read command.optsWithGlobals() in the `all` action so globals passed after the subcommand are picked up. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 12 +----------- src/cli.ts | 4 +++- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 647fcc1..619c3e3 100644 --- a/package.json +++ b/package.json @@ -49,17 +49,7 @@ "clean": "rimraf dist", "build:rollup": "rollup --config rollup.config.mjs", "prepare": "node scripts/dev-verifyx-bin.mjs", - "verify": "verifyx", - "verify:lint": "verifyx lint", - "verify:format": "verifyx format", - "verify:check-types": "verifyx check-types", - "verify:complexity": "verifyx complexity --threshold 50 \"src/**/*.ts\"", - "verify:comments": "verifyx comments --max-lines 1 --pushback \"src/**/*.ts\"", - "verify:hardcoded-colors": "verifyx hardcoded-colors", - "verify:forbidden-strings": "verifyx forbidden-strings", - "verify:unused-code": "verifyx unused-code", - "verify:circular-deps": "verifyx circular-deps", - "verify:duplicate-code": "verifyx duplicate-code", + "verify": "verifyx all", "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=test-results.xml", diff --git a/src/cli.ts b/src/cli.ts index fe97e34..98ba131 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -42,7 +42,9 @@ withRunOptions( withRunOptions( program.command('all').description('Run every built-in check (verify: scripts override the matching built-in)'), -).action(async (opts: RunOptions) => { +).action(async (_opts: RunOptions, command: Command) => { + // Run options live on the root command (commander treats them as global), so merge globals to catch flags after `all`. + const opts = command.optsWithGlobals() as RunOptions setVerbose(!!opts.verbose) configureMode(opts) process.exitCode = await runAll({ measure: opts.measure, tests: opts.tests }) From 87ae112d08a81f7829db94a341a82b8661ac75f5 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 11:42:40 +0800 Subject: [PATCH 32/34] feat: run verifyx all in parallel; --measure prints only its table verifyx all now runs every check concurrently (like bare verifyx), cutting wall-clock from the sum of durations to roughly the slowest check. To keep parallel output clean, each check runs inside an AsyncLocalStorage-scoped capture (src/shared/output.ts): console and subprocess output route to a per-check buffer, printed in registry order afterwards (only failures normally, every check under --verbose), so concurrent checks never interleave. runCommand emits into that buffer when capturing, else flushes on failure as before, so bare verifyx is unchanged. Also decouple --measure from verbose: chatty() now tracks --verbose only, so `verifyx all --measure` (and bare `verifyx --measure`) print just the status/duration table, not the full roll-call. Failures still print so a measured run can not fail silently. Co-Authored-By: Claude Opus 4.8 (1M context) --- .oxlintrc.json | 2 +- README.md | 4 +- src/cli.ts | 4 +- src/orchestrator/report.test.ts | 11 +-- src/orchestrator/report.ts | 8 +- src/orchestrator/run.ts | 2 +- src/orchestrator/runAll.ts | 138 ++++++++++++++++++-------------- src/shared/output.ts | 38 +++++++++ src/shared/spawn.ts | 5 +- 9 files changed, 131 insertions(+), 81 deletions(-) create mode 100644 src/shared/output.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index e7ed544..a09f322 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -26,7 +26,7 @@ "overrides": [ { // The CLI and its reporting surface legitimately write to stdout/stderr. - "files": ["src/cli.ts", "src/report.ts", "src/commands/**", "src/orchestrator/**", "src/checks/**"], + "files": ["src/cli.ts", "src/report.ts", "src/commands/**", "src/orchestrator/**", "src/checks/**", "src/shared/output.ts"], "rules": { "no-console": "off" } diff --git a/README.md b/README.md index a65bc9c..bcdfdf7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ There are three ways to invoke the CLI, from "all my checks" down to "one specif | `verifyx all` | Every built-in check with default options, plus any custom `verify:*` scripts. A `verify:` overrides its built-in. | | `verifyx ` | A single built-in by name, e.g. `verifyx complexity`. `verifyx list` shows them all. | -Both orchestration modes below (`verifyx` and `verifyx all`) are **completely silent on success**: no preamble, no per-check output, just exit `0`. Each check's output is buffered and printed **only if it fails**, so a run is cheap to loop or hand to an agent. (`--verbose` streams everything as it runs; `--measure` prints a status/duration table. Running a single built-in, `verifyx `, always prints its report, since you asked for that check specifically.) +Both orchestration modes below (`verifyx` and `verifyx all`) run their checks **in parallel** and are **completely silent on success**: no preamble, no per-check output, just exit `0`. Each check's output is buffered and printed **only if it fails**, so a run is cheap to loop or hand to an agent. `--verbose` prints every check's output (not just failures); `--measure` prints only a status/duration table; neither implies the other. (Running a single built-in, `verifyx `, always prints its report, since you asked for that check specifically.) ### `verifyx`: run your curated checks @@ -78,7 +78,7 @@ You curate your checks by adding `verify:*` scripts. Prefer calling the built-in ### `verifyx all`: run everything -`verifyx all` runs **every** built-in check with its default options, plus any custom `verify:*` scripts you've defined. It's the quick "run everything" without hand-curating a list. Where you've defined a `verify:` script matching a built-in, it **overrides** that built-in, so you can swap a single check's implementation without redefining the rest, e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. +`verifyx all` runs **every** built-in check with its default options, plus any custom `verify:*` scripts you've defined, all in parallel. It's the quick "run everything" without hand-curating a list. Where you've defined a `verify:` script matching a built-in, it **overrides** that built-in, so you can swap a single check's implementation without redefining the rest, e.g. `"verify:lint": "eslint ."` makes `verifyx all` use ESLint for the lint step. ### Fix locally, check in CI diff --git a/src/cli.ts b/src/cli.ts index 98ba131..1294746 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,7 +10,6 @@ import { registerUpgradeDocs } from './commands/registerUpgradeDocs.ts' import { orchestrate } from './orchestrator/run.ts' import { runAll } from './orchestrator/runAll.ts' import { configureMode } from './shared/mode.ts' -import { setVerbose } from './shared/spawn.ts' const require = createRequire(import.meta.url) const pkg = require('../package.json') as { version: string } @@ -45,9 +44,8 @@ withRunOptions( ).action(async (_opts: RunOptions, command: Command) => { // Run options live on the root command (commander treats them as global), so merge globals to catch flags after `all`. const opts = command.optsWithGlobals() as RunOptions - setVerbose(!!opts.verbose) configureMode(opts) - process.exitCode = await runAll({ measure: opts.measure, tests: opts.tests }) + process.exitCode = await runAll({ measure: opts.measure, tests: opts.tests, verbose: opts.verbose }) }) registerChecks(program) diff --git a/src/orchestrator/report.test.ts b/src/orchestrator/report.test.ts index 398e5b3..14a895c 100644 --- a/src/orchestrator/report.test.ts +++ b/src/orchestrator/report.test.ts @@ -21,17 +21,12 @@ afterEach(() => { describe('chatty', () => { it('is quiet by default (a clean run prints nothing)', () => { setVerbose(false) - expect(chatty(false)).toBe(false) + expect(chatty()).toBe(false) }) - it('is loud when --measure is set', () => { - setVerbose(false) - expect(chatty(true)).toBe(true) - }) - - it('is loud when verbose is set', () => { + it('is loud only when verbose is set (not for --measure)', () => { setVerbose(true) - expect(chatty(false)).toBe(true) + expect(chatty()).toBe(true) }) }) diff --git a/src/orchestrator/report.ts b/src/orchestrator/report.ts index bce74a0..ced64a9 100644 --- a/src/orchestrator/report.ts +++ b/src/orchestrator/report.ts @@ -2,11 +2,11 @@ import { color } from '../shared/color.ts' import { isVerbose } from '../shared/spawn.ts' /** - * Whether to print the preamble + success footer. A clean run is otherwise silent (just exit 0) to save - * tokens; chatter is shown only when streaming (`--verbose`) or measuring (`--measure`). + * Whether to print the preamble + per-check output + success footer. A clean run is otherwise silent (just + * exit 0) to save tokens; that chatter is shown only under `--verbose`. `--measure` prints just its table. */ -export function chatty(measure?: boolean): boolean { - return isVerbose() || !!measure +export function chatty(): boolean { + return isVerbose() } export type RunOutcome = { name: string; ok: boolean } diff --git a/src/orchestrator/run.ts b/src/orchestrator/run.ts index de48cc5..117ddcf 100644 --- a/src/orchestrator/run.ts +++ b/src/orchestrator/run.ts @@ -40,7 +40,7 @@ export async function orchestrate(opts: OrchestrateOptions = {}): Promise Promise): Promise { - const chunks: string[] = [] - const original = { log: console.log, warn: console.warn, error: console.error } - const capture = (...args: unknown[]): void => void chunks.push(args.map(String).join(' ')) - console.log = capture - console.warn = capture - console.error = capture - let result: CheckResult - try { - result = await run() - } finally { - Object.assign(console, original) - } - if (!result.ok && chunks.length > 0) console.log(chunks.join('\n')) - return result -} +type Task = { name: string; note?: string; run: () => Promise } -/** - * Run every built-in check (the explicit `verifyx all` opt-in) plus any custom `verify:*` scripts, so `all` - * truly runs everything. A built-in runs in-process unless the project defines a matching `verify:` - * script (or `verify::fix` in fix mode), which overrides it. A clean run is quiet: passing checks print - * nothing (output is buffered and flushed only on failure), use `--verbose` / `--measure` for the roll-call. - */ -export async function runAll(opts: RunAllOptions = {}): Promise { +/** Build the ordered task list: every built-in (running its override script if defined), then customs, then tests. */ +function buildTasks(opts: RunAllOptions): Task[] { const entries = resolveEntries() const mode = resolveMode() - const loud = chatty(opts.measure) + const spawn = (name: string, command: string, cwd: string, note?: string): Task => ({ + name, + note, + run: async () => (await runCommand(command, { cwd, quiet: true })) === 0, + }) + + const tasks: Task[] = CHECKS.map((check) => { + const override = resolveOverride(entries, check.name, mode) + if (!override) return { name: check.name, run: async () => (await check.runDefault()).ok } + return { + name: check.name, + note: 'overridden', + run: async () => { + const ok = (await runCommand(override.command, { cwd: override.cwd, quiet: true })) === 0 + if (!ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) + return ok + }, + } + }) - // Custom verify:* scripts that don't map to a built-in run too, minus the `test` check the tests step owns. const builtinNames = new Set(CHECKS.map((c) => c.name)) - const customEntries = selectEntries(entries, mode).filter( - (entry) => !builtinNames.has(entryCheckName(entry.name)) && entryCheckName(entry.name) !== TEST_CHECK_NAME, - ) + for (const entry of selectEntries(entries, mode)) { + const check = entryCheckName(entry.name) + if (builtinNames.has(check) || check === TEST_CHECK_NAME) continue + tasks.push(spawn(entry.name, entry.command, entry.cwd, 'custom')) + } + const testEntry = resolveTestEntry({ noTests: opts.tests === false }) + if (testEntry) tasks.push(spawn(testEntry.name, testEntry.command, testEntry.cwd, 'tests')) + return tasks +} + +/** + * Run every built-in check (the explicit `verifyx all` opt-in) plus any custom `verify:*` scripts and the + * tests step, all in parallel. Each check's output is captured independently so a clean run is silent and + * failures print without interleaving. `--verbose` prints every check's output; `--measure` prints its table. + */ +export async function runAll(opts: RunAllOptions = {}): Promise { + const loud = !!opts.verbose + const tasks = buildTasks(opts) if (loud) { - console.log(`Running all ${CHECKS.length} built-in verification(s)${customEntries.length ? ` + ${customEntries.length} custom` : ''}:`) - for (const check of CHECKS) - console.log(` - ${check.name}${resolveOverride(entries, check.name, mode) ? color.dim(' (overridden)') : ''}`) - for (const entry of customEntries) console.log(` - ${entry.name}${color.dim(' (custom)')}`) - if (testEntry) console.log(` - ${testEntry.name}${color.dim(' (tests)')}`) + console.log(`Running ${tasks.length} verification(s) in parallel:`) + for (const task of tasks) console.log(` - ${task.name}${task.note ? color.dim(` (${task.note})`) : ''}`) console.log() } const startTime = Date.now() - const records: MeasureRecord[] = [] - const results: CheckResult[] = [] - - const record = async (name: string, run: () => Promise): Promise => { - const checkStart = Date.now() - if (loud) console.log(color.heading(`▶ ${name}`)) - const ok = await run() - results.push({ name, ok }) - records.push({ script: name, code: ok ? 0 : 1, durationMs: Date.now() - checkStart }) - if (loud) console.log() + const restore = installConsoleCapture() + let runs: Array<{ name: string; ok: boolean; output: string; durationMs: number }> + try { + runs = await Promise.all( + tasks.map(async (task) => { + const taskStart = Date.now() + const { result, output } = await runCaptured(async () => { + try { + return await task.run() + } catch (error) { + console.error(String(error)) + return false + } + }) + return { name: task.name, ok: result, output, durationMs: Date.now() - taskStart } + }), + ) + } finally { + restore() } - for (const check of CHECKS) { - const override = resolveOverride(entries, check.name, mode) - await record(check.name, async () => { - if (!override) return loud ? (await check.runDefault()).ok : (await runQuietly(() => check.runDefault())).ok - const ok = (await runCommand(override.command, { cwd: override.cwd, quiet: !loud })) === 0 - // On an override failure, name the script that actually ran (only on failure: passing runs stay silent). - if (!ok) console.error(color.dim(`↳ ${check.name}: ran \`${override.command}\` (override)`)) - return ok - }) + // Print each check's captured output: everything under --verbose, only failures otherwise. + for (const run of runs) { + if (!loud && run.ok) continue + if (loud) console.log(color.heading(`▶ ${run.name}`)) + if (run.output) process.stdout.write(run.output) } - for (const entry of [...customEntries, ...(testEntry ? [testEntry] : [])]) { - await record(entry.name, async () => (await runCommand(entry.command, { cwd: entry.cwd, quiet: !loud })) === 0) + if (opts.measure) { + const records: MeasureRecord[] = runs.map((r) => ({ script: r.name, code: r.ok ? 0 : 1, durationMs: r.durationMs })) + printMeasureTable(records, Date.now() - startTime) } - - if (opts.measure) printMeasureTable(records, Date.now() - startTime) - return reportOutcomes(results, 'verification', loud) + return reportOutcomes( + runs.map((r) => ({ name: r.name, ok: r.ok })), + 'verification', + loud, + ) } diff --git a/src/shared/output.ts b/src/shared/output.ts new file mode 100644 index 0000000..172eceb --- /dev/null +++ b/src/shared/output.ts @@ -0,0 +1,38 @@ +import { AsyncLocalStorage } from 'node:async_hooks' +import util from 'node:util' + +// A per-task buffer set by runCaptured; concurrent tasks each get their own, so parallel output never interleaves. +const captureStore = new AsyncLocalStorage() + +/** True while running inside runCaptured (output is being collected rather than written straight out). */ +export function isCapturing(): boolean { + return captureStore.getStore() !== undefined +} + +/** Write text to the active capture buffer if one is set, else straight to the real stdout/stderr stream. */ +export function emit(text: string, stream: 'out' | 'err' = 'out'): void { + const sink = captureStore.getStore() + if (sink) sink.push(text) + else if (stream === 'err') process.stderr.write(text) + else process.stdout.write(text) +} + +/** Patch console.* to route through emit(), so output produced anywhere in a task is captured. Returns a restore fn. */ +export function installConsoleCapture(): () => void { + const { log, warn, error } = console + console.log = (...args: unknown[]): void => emit(`${util.format(...args)}\n`, 'out') + console.warn = (...args: unknown[]): void => emit(`${util.format(...args)}\n`, 'err') + console.error = (...args: unknown[]): void => emit(`${util.format(...args)}\n`, 'err') + return () => { + console.log = log + console.warn = warn + console.error = error + } +} + +/** Run `fn` with everything it emits (via console.* or emit()) collected into a string instead of printed. */ +export async function runCaptured(fn: () => Promise): Promise<{ result: T; output: string }> { + const sink: string[] = [] + const result = await captureStore.run(sink, fn) + return { result, output: sink.join('') } +} diff --git a/src/shared/spawn.ts b/src/shared/spawn.ts index 0fc75b0..09783e8 100644 --- a/src/shared/spawn.ts +++ b/src/shared/spawn.ts @@ -1,5 +1,7 @@ import { spawn } from 'node:child_process' +import { emit, isCapturing } from './output.ts' + let verboseMode = false /** When verbose, per-command output is always streamed; otherwise it is buffered and only shown on failure. */ @@ -42,7 +44,8 @@ export function runCommand(command: string, opts: RunCommandOptions = {}): Promi } child.on('close', (code) => { const exitCode = code ?? 1 - if (suppress && exitCode !== 0 && chunks.length > 0) process.stdout.write(Buffer.concat(chunks)) + // When capturing (parallel `verifyx all`), hand all output to the buffer; otherwise flush only on failure. + if (suppress && chunks.length > 0 && (isCapturing() || exitCode !== 0)) emit(Buffer.concat(chunks).toString()) resolve(exitCode) }) child.on('error', () => resolve(127)) From 1341a570ab7a969257574f53c843ea80a75cae3c Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 11:51:19 +0800 Subject: [PATCH 33/34] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20CI=20diff=20base=20for=20new-comments=20+=20config=20ignores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - comments --block-new-comments diffed `git diff HEAD`, a no-op in CI where a clean checkout has nothing uncommitted (the gate silently passed). Resolve the diff base per environment: HEAD locally, and in CI the merge base with the PR base branch (GITHUB_BASE_REF, or VERIFY_DIFF_BASE to override), falling back to HEAD if unresolved. Renamed gitDiffHead -> gitDiffAgainstBase. - CLI --ignore defaulted to [] (via `collect, []`), so `opts.ignore ?? config.ignore ?? []` short-circuited on the empty array and dropped configured hardcodedColors.ignore / comments.ignore when run through the CLI, contradicting the README. Treat an empty array as "not provided" so config ignores apply. - Document the per-environment diff base (and the fetch-depth: 0 caveat). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 30 ++++++++++++++++-------------- src/checks/comments.ts | 8 ++++---- src/checks/hardcoded-colors.ts | 2 +- src/shared/git.ts | 19 ++++++++++++++++--- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index bcdfdf7..8a93d0b 100644 --- a/README.md +++ b/README.md @@ -114,18 +114,18 @@ Flags on the bare `verifyx` command: ## Built-in checks -| Check | Kind | What it catches | -| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | -| `comments` | native | Flags comment blocks taller than `--max-lines` (default 2), to push for self-documenting code. JSDoc and `context:`-prefixed blocks are always allowed. Add `--block-new-comments` to also fail any comment on a line you changed vs `HEAD`. | -| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | -| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | -| `lint` | external | Linting; auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | -| `format` | external | Formatting; writes locally, checks in CI ([oxfmt](https://oxc.rs)). | -| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | -| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | -| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | -| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | +| Check | Kind | What it catches | +| ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `complexity` | native | Maintainability-index gate (cyclomatic complexity + Halstead volume + SLOC). Fails files below a threshold. | +| `comments` | native | Flags comment blocks taller than `--max-lines` (default 2), to push for self-documenting code. JSDoc and `context:`-prefixed blocks are always allowed. Add `--block-new-comments` to also fail any comment on a changed line (vs `HEAD` locally, the PR base in CI). | +| `hardcoded-colors` | native | Literal hex / `0x` colour values in source (cross-platform; suggests using design tokens). | +| `forbidden-strings` | native | Disallowed JSON config values, from rules in your verify config. | +| `lint` | external | Linting; auto-fixes locally, checks in CI ([oxlint](https://oxc.rs)). | +| `format` | external | Formatting; writes locally, checks in CI ([oxfmt](https://oxc.rs)). | +| `check-types` | external | TypeScript type check (`tsc --noEmit`); skips when there is no `tsconfig.json`. | +| `unused-code` | external | Unused files, exports and dependencies ([knip](https://knip.dev)). | +| `circular-deps` | external | Circular dependencies ([skott](https://github.com/antoine-coulon/skott)). | +| `duplicate-code` | external | Copy-paste detection ([jscpd](https://github.com/kucherenko/jscpd)). | External checks shell out to their tool and **skip gracefully when it is not installed**; `verifyx init` installs the ones you opt into. They run the tool from your local `node_modules/.bin` regardless of how `verifyx` was invoked. `oxlint`/`oxfmt`/`tsc` are resolved if present; the rest are declared as optional `peerDependencies`. @@ -172,12 +172,14 @@ Capable coding models (Opus 4.8 very much included) love to narrate their work: - `--max-lines `: fail on comment blocks longer than `n` lines (default 2). - `--pushback`: reframe the failure message as back-pressure aimed at an AI agent (see below). - `--warn`: report the long-block violations without failing the run. -- `--block-new-comments`: also fail on any comment on a line changed against `HEAD` (see below). +- `--block-new-comments`: also fail on any comment on a changed line (vs `HEAD` locally, the PR base in CI; see below). - `--ignore `: exclude files (repeatable; the `--block-new-comments` gate also reads `comments.ignore` from your [verify config](#configuration)). **`--pushback` is the clever bit.** An AI agent that hits a failing check will often take the path of least resistance and just delete or weaken the check to make the run pass. So rather than a dry error, the pushback message tells the agent that the _only_ sanctioned way to keep the comment is to prefix it with `context:`, and that doing so **pages a human to approve it**. Confronted with a real person's time on the line, the agent tends to reconsider and remove the low-value comment instead of gaming the gate. It is a small piece of prompt-engineering baked into a lint failure, and in practice it stops agents silencing the check far more reliably than a plain error does. -If you want to go further and block **new** comments outright, override the `verify:comments` script and add `--block-new-comments`. That turns on a stricter, diff-based gate on top of the long-block check: any comment sitting on a line you changed against `HEAD` fails, whether you added or merely edited it. Machine directives (`eslint-disable`, `@ts-expect-error`, and friends) stay exempt, as does anything marked `context:`. +If you want to go further and block **new** comments outright, override the `verify:comments` script and add `--block-new-comments`. That turns on a stricter, diff-based gate on top of the long-block check: any comment sitting on a changed line fails, whether you added or merely edited it. Machine directives (`eslint-disable`, `@ts-expect-error`, and friends) stay exempt, as does anything marked `context:`. + +The "changed lines" are resolved per environment so the gate works the same locally and in CI. Locally it diffs the working tree against `HEAD` (your uncommitted changes). In CI (`CI` set) a clean checkout has nothing uncommitted, so it diffs against the **merge base with the PR base branch** instead, read from `GITHUB_BASE_REF` (GitHub Actions). Set `VERIFY_DIFF_BASE` to a ref to override the base explicitly; if no base can be resolved it falls back to `HEAD`. For CI, make sure the base branch is fetched (`actions/checkout` with `fetch-depth: 0`), or the merge base won't be found and the gate silently passes. Two escape hatches keep genuinely useful comments alive. **JSDoc** (`/** … */`) is always allowed, and prefixing a comment's first line with `context:` marks it as durable context the code itself can't express: diff --git a/src/checks/comments.ts b/src/checks/comments.ts index 918e5d1..501d6b8 100644 --- a/src/checks/comments.ts +++ b/src/checks/comments.ts @@ -9,7 +9,7 @@ import { color } from '../shared/color.ts' import { scanFileComments } from '../shared/comment-scan.ts' import { loadVerifyConfig } from '../shared/config.ts' import { parseDiffAddedLines } from '../shared/diff.ts' -import { gitDiffHead } from '../shared/git.ts' +import { gitDiffAgainstBase } from '../shared/git.ts' import type { CheckResult } from './types.ts' const DEFAULT_MAX_COMMENT_BLOCK_LINES = 2 @@ -52,9 +52,9 @@ function toSingleLine(text: string): string { type NewComment = { file: string; line: number; text: string } -// context: the --block-new-comments behaviour — flag any comment sitting on a line changed against HEAD. +// context: the --block-new-comments behaviour — flag any comment sitting on a changed line (vs HEAD, or the CI base). function findCommentsOnChangedLines(ignoreGlobs: readonly string[]): NewComment[] { - const added = parseDiffAddedLines(gitDiffHead()) + const added = parseDiffAddedLines(gitDiffAgainstBase()) const findings: NewComment[] = [] for (const [file, lines] of added) { if (!shouldScan(file, ignoreGlobs)) continue @@ -107,7 +107,7 @@ export function runComments(opts: CommentsOptions = {}): CheckResult { let changedLinesOk = true if (opts.blockNewComments) { - const ignoreGlobs = opts.ignore ?? loadVerifyConfig().comments?.ignore ?? [] + const ignoreGlobs = opts.ignore?.length ? opts.ignore : (loadVerifyConfig().comments?.ignore ?? []) const findings = findCommentsOnChangedLines(ignoreGlobs) if (findings.length === 0) { console.log(color.green('No comments on changed lines.')) diff --git a/src/checks/hardcoded-colors.ts b/src/checks/hardcoded-colors.ts index ea41889..b470793 100644 --- a/src/checks/hardcoded-colors.ts +++ b/src/checks/hardcoded-colors.ts @@ -28,7 +28,7 @@ export type HardcodedColorsOptions = { root?: string; ignore?: readonly string[] export function runHardcodedColors(opts: HardcodedColorsOptions = {}): CheckResult { const config = loadVerifyConfig() const root = opts.root ?? config.hardcodedColors?.root ?? 'src' - const ignoreGlobs = opts.ignore ?? config.hardcodedColors?.ignore ?? [] + const ignoreGlobs = opts.ignore?.length ? opts.ignore : (config.hardcodedColors?.ignore ?? []) const files: string[] = [] walk(root, files) diff --git a/src/shared/git.ts b/src/shared/git.ts index 3e7798b..bc8914b 100644 --- a/src/shared/git.ts +++ b/src/shared/git.ts @@ -1,10 +1,23 @@ import { execSync } from 'node:child_process' -/** Full working-tree diff against HEAD. Returns '' when git is unavailable or this is not a repo. */ -export function gitDiffHead(): string { +function run(command: string): string { try { - return execSync('git diff HEAD', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }) + return execSync(command, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim() } catch { return '' } } + +// context: locally we diff the working tree against HEAD (uncommitted changes). A CI checkout has nothing +// uncommitted, so there we diff against the merge base with the PR base branch, else the gate is a no-op. +function resolveDiffBase(): string { + if (!process.env.CI) return 'HEAD' + const base = process.env.VERIFY_DIFF_BASE || (process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : '') + if (!base) return 'HEAD' + return run(`git merge-base ${base} HEAD`) || 'HEAD' +} + +/** Diff used to detect new comments: working tree vs HEAD locally, vs the PR merge base in CI. '' on error. */ +export function gitDiffAgainstBase(): string { + return run(`git diff ${resolveDiffBase()}`) +} From f4c9d9a2c97e7703c20bdda5c75a05db839f2457 Mon Sep 17 00:00:00 2001 From: "Rob Moore (MakerX)" Date: Thu, 9 Jul 2026 12:08:54 +0800 Subject: [PATCH 34/34] chore: verbose CI verify output + accurate CLAUDE.md verify description - PR/publish workflows run `verify -- --no-tests --verbose` so CI logs show each check, not just failures. - CLAUDE.md: describe `npm run verify` as `verifyx all` (every built-in + overrides + tests), matching the package.json verify script. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pr.yml | 2 +- .github/workflows/publish.yml | 2 +- CLAUDE.md | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b32bffa..bc599f4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,6 @@ jobs: build-script: npm run build # Runs the non-editing verifications (lint/format checks, complexity, comments, types). Tests run # separately via test-script, so --no-tests stops verify from running them a second time. - lint-script: npm run verify -- --no-tests + lint-script: npm run verify -- --no-tests --verbose test-script: npm run test:ci audit-script: npm run audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 87b9376..8c38cb0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: node-version: 24.x # Runs the non-editing verifications (lint/format checks, complexity, comments, types). Tests run # separately via test-script, so --no-tests stops verify from running them a second time. - lint-script: npm run verify -- --no-tests + lint-script: npm run verify -- --no-tests --verbose audit-script: npm run audit test-script: npm run test:ci output-test-results: true diff --git a/CLAUDE.md b/CLAUDE.md index b4fb51a..d008f51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,8 @@ A `verifyx` CLI that orchestrates native + external code checks by convention, p After making code changes, run `npm run verify` and fix everything it reports before finishing. Don't silence checks or game the metrics. -`npm run verify` runs this repo's own `verify:*` scripts (the built-in checks) in parallel plus the test suite, and stays silent unless something fails. It auto-fixes lint/format locally and is check-only under CI. +`npm run verify` runs `verifyx all` — every built-in check (plus any `verify:*` overrides and the test suite) in parallel — and stays silent unless something fails. It auto-fixes lint/format locally and is check-only under CI. ## Working in this repo - **The CLI binary is `verifyx`, not `verify`** — `verify` is a Windows `cmd` builtin that shadows it under `npm run`/`npx`. The npm _script_ stays named `verify` (so `npm run verify` works) and the package stays `@makerx/verify`. In dev, the `prepare` script (`scripts/dev-verifyx-bin.mjs`) writes a `node_modules/.bin/verifyx` shim pointing at `node src/cli.ts`, so the repo dogfoods `verifyx ` from source; run the CLI directly with `node src/cli.ts `. -- **`verify:comments` runs with `--max-lines 1`**, so never write a 2-line `//` comment block — use a single line, JSDoc (`/** … */`), or a `// context:` prefix. This is the check that trips changes most often. -- **`duplicate-code` (jscpd)** fails on near-identical blocks — extract a shared helper rather than repeating option lists or logic.