diff --git a/.agents/skills/gc-watchdog/watchdog.sh b/.agents/skills/gc-watchdog/watchdog.sh index 3bb113476..fbd1f523d 100644 --- a/.agents/skills/gc-watchdog/watchdog.sh +++ b/.agents/skills/gc-watchdog/watchdog.sh @@ -8,7 +8,10 @@ # Usage: bash watchdog.sh [interval_seconds] # Default interval: 60 seconds. -set -euo pipefail +# NOTE: intentionally NOT using set -e. gc status and bd list can fail +# transiently (session snapshot timeouts, store locks). We handle errors +# per-command and continue the loop. +set -uo pipefail INTERVAL="${1:-60}" ITER=0 @@ -16,12 +19,20 @@ ITER=0 # Filter out ephemeral wisp/nudge beads from bd output. # Real issues have IDs like sv-XXXX (4 chars after sv-). # Wisp/nudge beads have IDs like sv-wisp-XXXX or sv-nudge-XXXX. +# Status symbols: ○ = open, ◐ = in_progress, ● = blocked, ✓ = closed count_real_issues() { local status="$1" - bd list --status="$status" 2>/dev/null \ - | grep -E '^\s*○ sv-[a-z0-9]{4} ' \ - | grep -v "wisp\|nudge" \ - | wc -l || true + local count + if ! count=$( + bd list --status="$status" 2>/dev/null \ + | grep -E '^\s*[○◐●] sv-[a-z0-9]{4} ' \ + | grep -v "wisp\|nudge" \ + | wc -l 2>/dev/null + ); then + printf '0\n' + return 0 + fi + printf '%s\n' "$count" } while true; do @@ -40,7 +51,12 @@ while true; do MAYOR=$(echo "$STATUS" | grep "harness.mayor" | awk '{print $2}') SESSIONS=$(echo "$STATUS" | grep "Sessions:" | head -1 | sed 's/^ *//') SUSPENDED=$(echo "$STATUS" | grep "Suspended:" | awk '{print $2}') - CONTROLLER=$(echo "$STATUS" | grep "Controller:" | grep -o "supervisor-managed\|stopped\|error" | head -1) + CONTROLLER=$(echo "$STATUS" | grep "Controller:" | grep -o "supervisor-managed\|stopped\|error" | head -1 || true) + + # Handle transient lookup errors (mayor shows "lookup" instead of "awake") + if echo "$MAYOR" | grep -q "lookup"; then + MAYOR="lookup-error" + fi # 2. bd work counts OPEN_COUNT=$(count_real_issues "open") @@ -49,18 +65,34 @@ while true; do # 3. Check for issues ISSUES="" [ -z "$MAYOR" ] && ISSUES="$ISSUES MAYOR_MISSING" - { [ -n "$MAYOR" ] && echo "$MAYOR" | grep -qvE "awake|running|active"; } && ISSUES="$ISSUES MAYOR($MAYOR)" - [ "$SUSPENDED" != "no" ] && ISSUES="$ISSUES SUSPENDED" - [ "$CONTROLLER" != "supervisor-managed" ] && ISSUES="$ISSUES CONTROLLER($CONTROLLER)" + if [ -n "$MAYOR" ] && [ "$MAYOR" != "awake" ] && [ "$MAYOR" != "running" ] && [ "$MAYOR" != "active" ] && [ "$MAYOR" != "lookup-error" ]; then + ISSUES="$ISSUES MAYOR($MAYOR)" + fi + # lookup-error is a transient issue, not a hard failure — report as ⚠ but don't treat as fatal + if [ "$MAYOR" = "lookup-error" ]; then + ISSUES="$ISSUES MAYOR_LOOKUP_TIMEOUT" + fi + # Treat missing status fields as unknown, not healthy defaults + if [ -z "${SUSPENDED:-}" ]; then + ISSUES="$ISSUES SUSPENDED_MISSING" + elif [ "$SUSPENDED" != "no" ]; then + ISSUES="$ISSUES SUSPENDED" + fi + if [ -z "${CONTROLLER:-}" ]; then + ISSUES="$ISSUES CONTROLLER_MISSING" + elif [ "$CONTROLLER" != "supervisor-managed" ]; then + ISSUES="$ISSUES CONTROLLER($CONTROLLER)" + fi # 4. Report if [ -n "$ISSUES" ]; then - echo "[$TS] #$ITER ⚠$ISSUES | open:$OPEN_COUNT in_progress:$INPROG_COUNT | $SESSIONS" + echo "[$TS] #$ITER ⚠$ISSUES | open:$OPEN_COUNT in_progress:$INPROG_COUNT | ${SESSIONS:-no-sessions}" else - echo "[$TS] #$ITER ✓ mayor:$MAYOR | open:$OPEN_COUNT in_progress:$INPROG_COUNT | $SESSIONS" + echo "[$TS] #$ITER ✓ mayor:$MAYOR | open:$OPEN_COUNT in_progress:$INPROG_COUNT | ${SESSIONS:-no-sessions}" fi # 5. Exit condition: no open AND no in-progress real work, mayor healthy + # Note: lookup-error is transient, don't exit on it — only exit on clean idle if [ "$OPEN_COUNT" -eq 0 ] && [ "$INPROG_COUNT" -eq 0 ] && [ -z "$ISSUES" ]; then echo "[$TS] #$ITER IDLE — no open work, no in-progress work. Watchdog exiting." exit 0 diff --git a/.codacy.yml b/.codacy.yml index 3bcbef000..db3bf03b8 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,151 +1,24 @@ --- -# Codacy configuration — focus on real issues, exclude noise +# Codacy configuration file # https://docs.codacy.com/repositories-configure/codacy-configuration-file/ # Exclude non-source paths from analysis exclude_paths: - - "**/*.md" - - "**/*.json" - - "**/*.toml" - - "**/*.yml" - - "**/*.yaml" - - "**/*.config.ts" - - "**/tsdown.config.ts" - - "**/vitest.config.ts" - - "**/__tests__/**" - - "**/*.test.ts" - - "**/*.test.tsx" - - "**/*.spec.ts" - "pack/**" - ".agents/**" - ".gc/**" - ".husky/**" - - ".devin/**" - - ".beads/**" - - ".evidence/**" - - ".nx/**" - "website/**" - "engdocs/**" - - "specs/**" - - "skills/**" - - "template-fragments/**" - - "dist/**" - - "node_modules/**" + - "**/*.md" + - "**/*.json" + - "**/*.toml" # Configure tools engines: - # ESLint v9 — disable rules inappropriate for Node 24 / Bun / TypeScript project + # Use eslint-9 (matches our oxlint migration, Codacy's eslint for TS/JS) eslint-9: enabled: true - disable_rules: - # eslint-plugin-es-x: forbids ES2015+ syntax (arrow functions, const, import, etc.) - # Completely irrelevant for a modern TypeScript project targeting Node 24+ - - ESLint9_es-x_no-arrow-functions - - ESLint9_es-x_no-modules - - ESLint9_es-x_no-block-scoped-variables - - ESLint9_es-x_no-trailing-commas - - ESLint9_es-x_no-template-literals - - ESLint9_es-x_no-classes - - ESLint9_es-x_no-default-parameters - - ESLint9_es-x_no-destructuring - - ESLint9_es-x_no-rest-spread-properties - - ESLint9_es-x_no-spread-elements - - ESLint9_es-x_no-async-functions - - ESLint9_es-x_no-generators - - ESLint9_es-x_no-for-of-loops - - ESLint9_es-x_no-exponential-operators - - ESLint9_es-x_no-promise-objects - - ESLint9_es-x_no-symbol - - ESLint9_es-x_no-map - - ESLint9_es-x_no-set - - ESLint9_es-x_no-weak-map - - ESLint9_es-x_no-weak-set - - ESLint9_es-x_no-proxy - - ESLint9_es-x_no-reflect - - ESLint9_es-x_no-binary-numeric-literals - - ESLint9_es-x_no-octal-numeric-literals - - ESLint9_es-x_no-regex-u-flag - - ESLint9_es-x_no-regex-y-flag - - ESLint9_es-x_no-unicode-codepoint-escapes - - ESLint9_es-x_no-object-super-properties - - ESLint9_es-x_no-array-prototype-copywithin - - ESLint9_es-x_no-array-prototype-fill - - ESLint9_es-x_no-array-prototype-find - - ESLint9_es-x_no-array-prototype-findindex - - ESLint9_es-x_no-array-prototype-flat - - ESLint9_es-x_no-array-prototype-flatmap - - ESLint9_es-x_no-array-prototype-includes - - ESLint9_es-x_no-array-prototype-keys - - ESLint9_es-x_no-array-prototype-values - - ESLint9_es-x_no-array-prototype-entries - - ESLint9_es-x_no-string-prototype-at - - ESLint9_es-x_no-string-prototype-codepoint-at - - ESLint9_es-x_no-string-prototype-ends-with - - ESLint9_es-x_no-string-prototype-includes - - ESLint9_es-x_no-string-prototype-match-all - - ESLint9_es-x_no-string-prototype-pad-end - - ESLint9_es-x_no-string-prototype-pad-start - - ESLint9_es-x_no-string-prototype-repeat - - ESLint9_es-x_no-string-prototype-starts-with - - ESLint9_es-x_no-string-prototype-trim - - ESLint9_es-x_no-string-prototype-trimstart - - ESLint9_es-x_no-string-prototype-trimend - - ESLint9_es-x_no-string-raw - - ESLint9_es-x_no-number-constructor - - ESLint9_es-x_no-number-isfinite - - ESLint9_es-x_no-number-isinteger - - ESLint9_es-x_no-number-isnan - - ESLint9_es-x_no-number-issafeinteger - - ESLint9_es-x_no-number-max-safe-integer - - ESLint9_es-x_no-number-min-safe-integer - - ESLint9_es-x_no-number-epsilon - - ESLint9_es-x_no-number-parse-float - - ESLint9_es-x_no-number-parse-integer - - ESLint9_es-x_no-math-acosh - - ESLint9_es-x_no-math-asinh - - ESLint9_es-x_no-math-atanh - - ESLint9_es-x_no-math-cbrt - - ESLint9_es-x_no-math-clz32 - - ESLint9_es-x_no-math-cosh - - ESLint9_es-x_no-math-expm1 - - ESLint9_es-x_no-math-fround - - ESLint9_es-x_no-math-hypot - - ESLint9_es-x_no-math-imul - - ESLint9_es-x_no-math-log10 - - ESLint9_es-x_no-math-log1p - - ESLint9_es-x_no-math-log2 - - ESLint9_es-x_no-math-sign - - ESLint9_es-x_no-math-sinh - - ESLint9_es-x_no-math-tanh - - ESLint9_es-x_no-object-assign - - ESLint9_es-x_no-object-is - - ESLint9_es-x_no-object-entries - - ESLint9_es-x_no-object-fromentries - - ESLint9_es-x_no-object-getownpropertydescriptors - - ESLint9_es-x_no-object-values - - ESLint9_es-x_no-object-keys - - ESLint9_es-x_no-object-define-property - - ESLint9_es-x_no-object-define-properties - - ESLint9_es-x_no-object-create - - ESLint9_es-x_no-object-get-prototype-of - - ESLint9_es-x_no-object-set-prototype-of - - ESLint9_es-x_no-date-prototype-to-primitive - - ESLint9_es-x_no-symbol-prototype-description - - ESLint9_es-x_no-intl - - ESLint9_es-x_no-atomics - - ESLint9_es-x_no-shared-array-buffer - - # Markdown linter — disable noisy rules - markdownlint: + # Enable biome for formatting checks (matches our biome.json) + biome: enabled: true - disable_rules: - - MD034 # Bare URL used - - MD024 # Multiple headings with same content - - MD025 # Multiple top-level headings - - MD036 # Emphasis used instead of a heading - - MD041 # First line in a file should be a top-level heading - -# Disable analysis of markdown files (we only care about code) -languages: - markdown: - enabled: false diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..6b8e5ec48 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,20 @@ +name: "Sverka CodeQL Config" + +# Exclude non-source paths from CodeQL analysis +paths: + - packages +paths-ignore: + - packages/*/dist/** + - "**/node_modules/**" + - "**/__tests__/**" + - "**/*.test.ts" + - pack/** + - .agents/** + - .gc/** + - website/** + - engdocs/** + - specs/** + +# Query suite: security-extended (set in workflow) +queries: + - uses: security-extended diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c832840ef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: + pull_request: + branches: ["**"] + push: + branches: [main] + +# Cancel in-progress runs for the same ref (saves CI minutes). +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test-lint: + name: Build, Test, Lint, Typecheck + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 # Nx needs full history for affected detection + persist-credentials: false + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.14" + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "24" + + - name: Install dependencies + run: bun install --frozen-lockfile --ignore-scripts + + - name: Restore Nx cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: .nx/cache + key: nx-${{ runner.os }}-${{ hashFiles('**/package.json', '**/tsconfig.json', '**/src/**') }} + restore-keys: | + nx-${{ runner.os }}- + + - name: Format check + run: bun run format:check + + - name: Lint (oxlint) + run: bun run lint + + - name: Typecheck + run: bun run typecheck + + - name: Build + run: bun run build + + - name: Test + run: bun run test + + # Nx Cloud self-healing CI: auto-applies fixes for failed CI tasks. + # Requires the NX_CLOUD_ACCESS_TOKEN repo secret to be set. + # continue-on-error so the gate stays green before the secret is configured. + - name: Nx fix-ci (self-healing) + if: always() + continue-on-error: true + env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + run: bunx nx@21.0.0 fix-ci + + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dist + path: packages/*/dist/ + if-no-files-found: ignore + retention-days: 7 + + codeql: + name: CodeQL Analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + language: [javascript-typescript] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@e8e594e8799b6bc05d33c13d3d14e484b7b9cbc0 # v3.29.4 + with: + languages: ${{ matrix.language }} + config-file: .github/codeql/codeql-config.yml + + - name: Autobuild + uses: github/codeql-action/autobuild@e8e594e8799b6bc05d33c13d3d14e484b7b9cbc0 # v3.29.4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@e8e594e8799b6bc05d33c13d3d14e484b7b9cbc0 # v3.29.4 + with: + category: "/language:${{ matrix.language }}" + # Don't upload SARIF to Security tab (requires Advanced Security, + # paid for private repos). Instead, convert to PR annotations below. + upload: never + output: ${{ github.workspace }}/results/javascript.sarif + + - name: Convert SARIF to PR annotations + if: always() + run: | + SARIF_FILE=$(find results/javascript.sarif -name '*.sarif' -type f | head -1) + if [ -n "$SARIF_FILE" ]; then + python3 scripts/sarif-to-annotations.py < "$SARIF_FILE" + else + echo "::warning title=sarif-to-annotations::No SARIF file found in results/javascript.sarif" + fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..ea5a55b6f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bunx lint-staged diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..fb0724a95 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,56 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript"], + "categories": { + "correctness": "error" + }, + "rules": { + "no-unused-vars": "off", + "typescript/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], + "typescript/no-explicit-any": "error", + "typescript/no-non-null-assertion": "off", + "typescript/no-empty-object-type": "off", + "typescript/no-unsafe-function-type": "off", + "no-empty-function": "off" + }, + "env": { + "builtin": true, + "es2024": true, + "node": true + }, + "ignorePatterns": [ + "dist/**", + "node_modules/**", + ".beads/**", + ".devin/**", + ".evidence/**", + ".gc/**", + ".opencode/**", + ".nx/**", + "website/**" + ], + "overrides": [ + { + "files": ["**/*.test.ts", "**/__tests__/**"], + "rules": { + "typescript/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ] + } + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 0289602ea..18245af17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,9 @@ anywhere. - **Monorepo:** Nx - **Build:** tsdown - **Test:** Vitest -- **Lint:** ESLint -- **Format:** Prettier +- **Lint:** oxlint +- **Format:** Biome +- **Pre-commit:** Husky + lint-staged (biome format) ## Structure diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..6096952a8 --- /dev/null +++ b/biome.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**/*.ts", + "**/*.js", + "**/*.mjs", + "**/*.json", + "**/*.md", + "!**/dist/**", + "!**/node_modules/**", + "!**/.beads/**", + "!**/.devin/**", + "!**/.evidence/**", + "!**/.gc/**", + "!**/.opencode/**", + "!**/.claude/**", + "!**/.nx/**", + "!**/website/**", + "!**/bun.lock", + "!**/*.d.ts" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "lineEnding": "lf", + "formatWithErrors": true + }, + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + }, + "linter": { + "enabled": false + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all", + "bracketSpacing": true, + "arrowParentheses": "always" + } + } +} diff --git a/bun.lock b/bun.lock index 67a105d1a..e687b1944 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,14 @@ "": { "name": "sverka", "devDependencies": { + "@biomejs/biome": "^2.5.7", "@types/node": "^24.0.0", - "eslint": "^9.0.0", + "husky": "^9.1.7", + "lint-staged": "^17.3.0", "nx": "^21.0.0", - "prettier": "^3.0.0", + "oxlint": "^1.78.0", "tsdown": "^0.22.14", "typescript": "^5.8.0", - "typescript-eslint": "^8.66.0", "vitest": "^3.0.0", }, }, @@ -210,6 +211,24 @@ }, }, "packages": { + "@biomejs/biome": ["@biomejs/biome@2.5.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.7", "@biomejs/cli-darwin-x64": "2.5.7", "@biomejs/cli-linux-arm64": "2.5.7", "@biomejs/cli-linux-arm64-musl": "2.5.7", "@biomejs/cli-linux-x64": "2.5.7", "@biomejs/cli-linux-x64-musl": "2.5.7", "@biomejs/cli-win32-arm64": "2.5.7", "@biomejs/cli-win32-x64": "2.5.7" }, "bin": { "biome": "bin/biome" } }, "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ=="], + "@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -268,34 +287,6 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="], - - "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - - "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - - "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@jest/diff-sequences": ["@jest/diff-sequences@30.4.0", "", {}, "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g=="], "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], @@ -356,6 +347,44 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@5.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LT9eOPPUqfZscQRd5mc08RBeDWOQf+dnOrKnanMallTGPe6g7+rcAlFTA8SWoJbcD45PV8yArFtCmSQSpzHZmg=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.78.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.78.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA=="], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.3", "", { "os": "android", "cpu": "arm64" }, "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw=="], @@ -480,34 +509,12 @@ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.66.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/type-utils": "8.66.0", "@typescript-eslint/utils": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.66.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.66.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.66.0", "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0" } }, "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.66.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.66.0", "", {}, "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.66.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.66.0", "@typescript-eslint/tsconfig-utils": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.66.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.66.0", "", { "dependencies": { "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg=="], - "@vitest/expect": ["@vitest/expect@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w=="], "@vitest/mocker": ["@vitest/mocker@3.2.7", "", { "dependencies": { "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA=="], @@ -578,14 +585,8 @@ "@zkochan/js-yaml": ["@zkochan/js-yaml@0.0.7", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -608,7 +609,7 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -616,8 +617,6 @@ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -638,16 +637,10 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], @@ -686,50 +679,20 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], - "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], @@ -750,10 +713,6 @@ "get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -768,61 +727,41 @@ "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "import-without-cache": ["import-without-cache@0.4.0", "", {}, "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jest-diff": ["jest-diff@30.4.1", "", { "dependencies": { "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.4.1" } }, "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-parser": ["jsonc-parser@3.2.0", "", {}, "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lines-and-columns": ["lines-and-columns@2.0.3", "", {}, "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lint-staged": ["lint-staged@17.3.0", "", { "dependencies": { "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw=="], "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], @@ -838,7 +777,7 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -846,8 +785,6 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-machine-id": ["node-machine-id@1.1.12", "", {}, "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ=="], "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], @@ -862,19 +799,11 @@ "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@5.3.0", "", { "dependencies": { "bl": "^4.0.3", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "log-symbols": "^4.0.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g=="], "oxc-resolver": ["oxc-resolver@5.3.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "5.3.0", "@oxc-resolver/binding-darwin-x64": "5.3.0", "@oxc-resolver/binding-freebsd-x64": "5.3.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "5.3.0", "@oxc-resolver/binding-linux-arm64-gnu": "5.3.0", "@oxc-resolver/binding-linux-arm64-musl": "5.3.0", "@oxc-resolver/binding-linux-riscv64-gnu": "5.3.0", "@oxc-resolver/binding-linux-s390x-gnu": "5.3.0", "@oxc-resolver/binding-linux-x64-gnu": "5.3.0", "@oxc-resolver/binding-linux-x64-musl": "5.3.0", "@oxc-resolver/binding-wasm32-wasi": "5.3.0", "@oxc-resolver/binding-win32-arm64-msvc": "5.3.0", "@oxc-resolver/binding-win32-x64-msvc": "5.3.0" } }, "sha512-FHqtZx0idP5QRPSNcI5g2ItmADg7fhR3XIeWg5eRMGfp44xqRpfkdvo+EX4ZceqV9bxvl0Z8vaqMqY0gYaNYNA=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "oxlint": ["oxlint@1.78.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.78.0", "@oxlint/binding-android-arm64": "1.78.0", "@oxlint/binding-darwin-arm64": "1.78.0", "@oxlint/binding-darwin-x64": "1.78.0", "@oxlint/binding-freebsd-x64": "1.78.0", "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", "@oxlint/binding-linux-arm-musleabihf": "1.78.0", "@oxlint/binding-linux-arm64-gnu": "1.78.0", "@oxlint/binding-linux-arm64-musl": "1.78.0", "@oxlint/binding-linux-ppc64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-musl": "1.78.0", "@oxlint/binding-linux-s390x-gnu": "1.78.0", "@oxlint/binding-linux-x64-gnu": "1.78.0", "@oxlint/binding-linux-x64-musl": "1.78.0", "@oxlint/binding-openharmony-arm64": "1.78.0", "@oxlint/binding-win32-arm64-msvc": "1.78.0", "@oxlint/binding-win32-ia32-msvc": "1.78.0", "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -888,16 +817,10 @@ "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], - "pretty-format": ["pretty-format@30.4.1", "", { "dependencies": { "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", "react-is-18": "npm:react-is@^18.3.1", "react-is-19": "npm:react-is@^19.2.5" } }, "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw=="], "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], "react-is-18": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -908,8 +831,6 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], @@ -926,10 +847,6 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -942,6 +859,8 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -950,8 +869,6 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -974,26 +891,18 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], "tsdown": ["tsdown@0.22.14", "", { "dependencies": { "ansis": "^4.3.1", "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.1", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.4", "picomatch": "^4.0.5", "rolldown": "~1.2.0", "rolldown-plugin-dts": "^0.27.13", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0", "verkit": "^0.3.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.14", "@tsdown/exe": "0.22.14", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.66.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.66.0", "@typescript-eslint/parser": "8.66.0", "@typescript-eslint/typescript-estree": "8.66.0", "@typescript-eslint/utils": "8.66.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw=="], - "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "verkit": ["verkit@0.3.2", "", {}, "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg=="], @@ -1006,12 +915,8 @@ "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1024,31 +929,15 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yuku-ast": ["yuku-ast@0.8.4", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.4" } }, "sha512-s7EWfWIQkaGmsGnyr/BU0jli9YTN5TvrKIsSmALyRD9elumDQInuhv0BrVObENKVCxr9W3Ikmnx5u02KvfuUmw=="], "yuku-codegen": ["yuku-codegen@0.8.4", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.4" }, "optionalDependencies": { "@yuku-codegen/binding-android-arm64": "0.8.4", "@yuku-codegen/binding-darwin-arm64": "0.8.4", "@yuku-codegen/binding-darwin-x64": "0.8.4", "@yuku-codegen/binding-freebsd-x64": "0.8.4", "@yuku-codegen/binding-linux-arm-gnu": "0.8.4", "@yuku-codegen/binding-linux-arm-musl": "0.8.4", "@yuku-codegen/binding-linux-arm64-gnu": "0.8.4", "@yuku-codegen/binding-linux-arm64-musl": "0.8.4", "@yuku-codegen/binding-linux-x64-gnu": "0.8.4", "@yuku-codegen/binding-linux-x64-musl": "0.8.4", "@yuku-codegen/binding-win32-arm64": "0.8.4", "@yuku-codegen/binding-win32-x64": "0.8.4" } }, "sha512-1Rw+NYcmB1xkHAWlsIpbwIv/Fr50idtEbLf7OjDA+90dny6PM4Krz7Fs0TT+w2PBdjaldZpN4ye5wR4Dhlm8vA=="], "yuku-parser": ["yuku-parser@0.8.4", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.4", "yuku-ast": "^0.8.4" }, "optionalDependencies": { "@yuku-parser/binding-android-arm64": "0.8.4", "@yuku-parser/binding-darwin-arm64": "0.8.4", "@yuku-parser/binding-darwin-x64": "0.8.4", "@yuku-parser/binding-freebsd-x64": "0.8.4", "@yuku-parser/binding-linux-arm-gnu": "0.8.4", "@yuku-parser/binding-linux-arm-musl": "0.8.4", "@yuku-parser/binding-linux-arm64-gnu": "0.8.4", "@yuku-parser/binding-linux-arm64-musl": "0.8.4", "@yuku-parser/binding-linux-x64-gnu": "0.8.4", "@yuku-parser/binding-linux-x64-musl": "0.8.4", "@yuku-parser/binding-win32-arm64": "0.8.4", "@yuku-parser/binding-win32-x64": "0.8.4" } }, "sha512-sw41wouvT5rUmLIp87hmvm5vtF+MRSI3x6yjq6xqpYmtkQj+Ht6N7xRQ8lMhLv8N7JAzughGj0Rfi0jQRSu9HQ=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@oxc-resolver/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "@yarnpkg/parsers/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], - - "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "front-matter/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], - - "nx/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + "js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -1057,15 +946,5 @@ "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "@oxc-resolver/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - - "@yarnpkg/parsers/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "nx/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 92ca4c78d..ba8ef299b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,12 +1,8 @@ -// ESLint 9 flat config — TypeScript-aware, strict, ESM. -// Single root config discovered by all packages via ESLint's flat-config -// resolution (walks up from the linted directory). See ADR-001 for the -// minimal-dependency stance; typescript-eslint is the official unified -// package for ESLint 9 + TypeScript. -import tseslint from "typescript-eslint"; - -export default tseslint.config( - // Global ignores — never lint build output, deps, or tooling artifacts. +// ESLint 9 flat config — used by Codacy for code analysis. +// This config explicitly does NOT include eslint-plugin-es-x rules +// (which forbid ES2015+ syntax like arrow functions, const, import). +// Those rules are inappropriate for a Node 24 / Bun / TypeScript project. +export default [ { ignores: [ "**/dist/**", @@ -15,40 +11,30 @@ export default tseslint.config( "**/.devin/**", "**/.evidence/**", "**/.gc/**", + "**/.nx/**", "**/.opencode/**", - "website/**", + "**/website/**", + "**/pack/**", + "**/skills/**", + "**/specs/**", + "**/engdocs/**", + "**/template-fragments/**", + "**/*.config.ts", + "**/*.test.ts", + "**/__tests__/**", ], }, - // Strict TypeScript preset for all source files. { - files: ["**/src/**/*.ts"], - extends: tseslint.configs.recommended, + files: ["**/*.ts"], languageOptions: { ecmaVersion: 2024, sourceType: "module", + parserOptions: { + ecmaFeatures: {}, + }, }, rules: { - // Allow _-prefixed unused vars/args (intentional ignore convention). - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ], + // No es-x rules — we target Node 24+ which supports all modern JS features }, }, - // Test files: relax rules that conflict with vitest patterns (vi.hoisted, - // top-level awaits, deliberate any casts in fixtures, unused fixture imports). - { - files: ["**/src/__tests__/**/*.ts", "**/src/**/*.test.ts"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unused-vars": "warn", - }, - }, -); +]; diff --git a/nx.json b/nx.json index 3c307dce8..d655ef615 100644 --- a/nx.json +++ b/nx.json @@ -20,7 +20,7 @@ "cache": true }, "lint": { - "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], + "inputs": ["default", "{workspaceRoot}/.oxlintrc.json"], "cache": true }, "typecheck": { @@ -28,6 +28,5 @@ "inputs": ["default", "^production"], "cache": true } - }, - "nxCloudAccessToken": "" + } } diff --git a/pack/README.md b/pack/README.md index f92134a95..45ca6857c 100644 --- a/pack/README.md +++ b/pack/README.md @@ -16,22 +16,6 @@ Reusable Gas City pack for spec-driven, test-first monorepo projects. | `template-fragments/review-policy.md` | Template for project `REVIEW.md` | | `template-fragments/security-policy.md` | Template for project `SECURITY.md` | | `template-fragments/conventions.md` | Template for project `AGENTS.md` | -| `skills/sverka-wave/` | Agent skill: wave planning and execution cycle | -| `skills/sverka-review/` | Agent skill: two-axis review and gate commands | -| `skills/sverka-drill/` | Agent skill: failure investigation and root cause isolation | -| `docs/agent-guide.md` | Documentation for agents running under this harness | - -## CLI Skill (separate) - -The `skills/sverka/` directory at the project root contains a publishable -CLI skill for the `sverka` CLI tool. Install it via `npx skills`: - -```bash -npx skills add sverka-dev/sverka --skill sverka -``` - -This provides `/sverka ` (scan, doctor, plan, execute, validate, -baseline, init, inspect) in any coding agent. ## Usage diff --git a/pack/formulas/merge-stack.toml b/pack/formulas/merge-stack.toml new file mode 100644 index 000000000..8605e14b0 --- /dev/null +++ b/pack/formulas/merge-stack.toml @@ -0,0 +1,154 @@ +formula = "merge-stack" +description = "Merge a stack of PRs top-down: /act the top PR, rebase onto main, merge, close lower PRs, advance to next stack, until all flat" + +[requires] +formula_compiler = ">=2.0.0" + +[[steps]] +id = "discover" +title = "Mayor: discover the PR stacks" +description = """ +The mayor discovers all open PR stacks by querying `gh pr list --state open --limit 200` +and building dependency chains from base/head ref names. + +For each stack, identify: +- The TOP PR (the one nothing else depends on — its head is no other PR's base) +- The BOTTOM PR (base = main) + +MERGE ORDER: TOP-DOWN. The top PR is merged FIRST. + - Rebase top PR onto main, change its base to main + - Squash merge → main gets ALL changes from the entire stack in one commit + - Close all lower PRs in the stack (their changes are now in main via the top PR) + - Move to the next stack + +This is the opposite of bottom-up. Top-down is faster because: + - Only one merge per stack (not N merges) + - Only one /act convergence per stack (on the top PR) + - Lower PRs are closed, not merged (already included) + +Output: list of stacks, each with its top PR number and all member PRs. +Example: Stack A top=#18, members=[#1,#2,#3,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#16,#17,#18] + Stack B top=#22, members=[#19,#20,#22] +""" +agent = "mayor" + +[[steps]] +id = "act-loop" +title = "Builder: /act --loop on current TOP PR until merge-ready" +description = """ +The builder runs `/act --loop` on the current stack's TOP PR. + +The /act loop MUST converge before merge. Convergence requires ALL: +1. open_threads == 0 (all review threads resolved) +2. CI_REQUIRED_PENDING == 0 (all required CI checks pass) +3. SAST_FINDINGS_PENDING == 0 (no unresolved security/quality findings) +4. No new bot comments appeared in the last CI run (bot count stable) +5. PR is mergeable (no conflicts, branch up to date with base) +6. Quality gates passed (coverage, code quality — if configured) +7. CodeRabbit review triggered and completed (click checkbox after each push) + +CodeRabbit trigger is MANDATORY after every push. CodeRabbit does not +auto-review non-default branches. Without triggering, review threads +stay stale and convergence check #4 is meaningless. + +PREREQUISITE: Before /act, rebase the full stack and then flatten the TOP PR: + gh stack rebase + gh stack submit + git checkout + git fetch origin + git rebase origin/main + git push --force-with-lease + gh pr edit --base main + +This ensures the top PR's diff includes ALL changes from the entire stack +(every commit from bottom to top), and merging it brings everything into +main in one squash commit. + +If /act cannot converge (context limit, unfixable issue), the builder +reports back to the mayor with a blocker report. The mayor decides: +retry with fresh context, or escalate to human. + +RETRY BUDGET: The /act --loop MUST be bounded: + - Maximum 10 iterations per convergence attempt + - Maximum 2 hours elapsed per convergence attempt + - Exponential backoff: 60s, 120s, 240s between retries + - When any limit is reached, stop retrying and send a blocker + report to the mayor for escalation. Do not loop indefinitely. +""" +needs = ["discover"] +agent = "builder" + +[[steps]] +id = "merge" +title = "Mayor: merge the clean TOP PR into main" +description = """ +The mayor merges the TOP PR ONLY after the builder confirms /act convergence +AND CodeRabbit review completed. + +Merge method: squash (clean history, one commit per stack). +Command: + gh pr merge --squash --delete-branch + +This single merge brings ALL changes from the entire stack into main. + +After merge: +1. Delete the remote head branch (gh pr merge --delete-branch does this) +2. Pull main locally: git checkout main && git pull +3. Close all lower PRs in this stack (their changes are now in main): + For each lower PR in the stack: + gh pr close --comment "Merged via # (squash). All stack changes are now in main." + git push origin --delete (if not already deleted) +4. Record merge in bd: bd close if tracking + +If merge fails (conflict, CI changed), report back to builder for +another /act iteration. +""" +needs = ["act-loop"] +agent = "mayor" + +[[steps]] +id = "retrospect" +title = "Mayor: retrospect (self-learning)" +description = """ +MANDATORY after each stack merge. This is the self-learning loop. + +Capture: +1. What worked — patterns that made /act converge fast +2. What didn't — issues that took multiple iterations +3. Review patterns — common review feedback across PRs +4. CI patterns — common CI failures and fixes +5. Bot patterns — CodeRabbit/Codacy/Qodo recurring findings +6. Time to converge — how many /act iterations +7. Stack size — how many PRs were in this stack, how many closed vs merged + +Store retrospectives in Beads only using `bd remember`. Do NOT use +.gc/retrospects/ or any ad hoc memory files. + +Before starting /act on the next stack's top PR, refresh Beads context +with `bd prime` and apply lessons learned — proactively fix patterns +before bots find them. +""" +needs = ["merge"] +agent = "mayor" + +[[steps]] +id = "advance" +title = "Mayor: advance to next stack or finish" +description = """ +After retrospect, the mayor checks if there are more stacks to merge. +If yes: + - Take the next stack's TOP PR + - Rebase it onto main (which now has the previous stack's changes) + - Change its base to main + - Loop back to act-loop step with the new top PR + +If no more stacks remain: + - Run final retrospect (comprehensive: total stacks merged, total PRs + closed, total iterations, top patterns, top lessons, recommendations). + - Record completion: all stacks flat on main. + - Report final status to human. + +This is the loop terminator: advance until all stacks are merged. +""" +needs = ["retrospect"] +agent = "mayor" diff --git a/pack/skills/sverka-merge-stack/SKILL.md b/pack/skills/sverka-merge-stack/SKILL.md new file mode 100644 index 000000000..ab79b63e5 --- /dev/null +++ b/pack/skills/sverka-merge-stack/SKILL.md @@ -0,0 +1,206 @@ +--- +name: sverka-merge-stack +description: Use when merging a stack of PRs top-down. The TOP PR is rebased onto main, /act'd until clean, squash-merged (brings all stack changes into main), then lower PRs are closed. Retrospect after each stack merge feeds self-learning. Trigger when the user asks to "merge the stack", "merge all PRs", or "merge from the top". +--- + +# sverka-merge-stack + +Merge PR stacks TOP-DOWN. The top PR of each stack is rebased onto main, +/act'd until clean, then squash-merged — bringing ALL changes from the +entire stack into main in one commit. Lower PRs are closed (their changes +are included in the top PR's squash commit). + +## Why top-down? + +- **One merge per stack** (not N merges) — faster +- **One /act convergence per stack** (on the top PR only) +- **Lower PRs are closed, not merged** — their changes are already in main +- The top PR's diff (after rebase onto main) includes ALL commits from + the entire stack, so squash merge captures everything + +## The Merge Loop + +```text + ┌── DISCOVER STACKS ──► ACT-LOOP (top PR) ──► MERGE ──► CLOSE LOWERS ──► RETROSPECT ──► ADVANCE ──┐ + │ │ + │ next stack ◄──────────────────────────────────────────────────────────────────────────────────────┘ + │ │ + └── (no more stacks) ──► FINAL RETROSPECT ──► DONE (all flat on main) │ +``` + + +## Step 1: Discover the stacks + +Query all open PRs and build chains: + +```bash +gh pr list --state open --limit 200 --json number,title,baseRefName,headRefName \ + --jq '.[] | "\(.number) \(.headRefName) \(.baseRefName)"' +``` + +For each stack, identify: +- **TOP PR**: the one whose head is NOT any other PR's base +- **BOTTOM PR**: base = main +- **Members**: all PRs in the chain + +Example: +- Stack A: top=#18, members=[#1,#2,#3,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#16,#17,#18] +- Stack B: top=#22, members=[#19,#20,#22] + +## Step 2: /act --loop on the TOP PR + +### Pre-flight: rebase the full stack, then flatten the TOP PR onto main + +First, rebase the entire stack so lower-branch changes cascade into the top branch: + +```bash +gh stack rebase +gh stack submit +``` + +Then, flatten the TOP PR onto main so its diff includes ALL stack changes: + +```bash +git checkout +git fetch origin +git rebase origin/main +git push --force-with-lease +gh pr edit --base main +``` + +After rebase, the TOP PR's diff = all commits from bottom to top of stack. + +### Trigger CodeRabbit review (MANDATORY) + +CodeRabbit posts an issue-level comment with a "Trigger review" checkbox. +After each push, the agent MUST click the checkbox to force a re-review. + +**How to click the checkbox:** + +1. Find the CodeRabbit comment: + + ```bash + COMMENT_ID=$(gh api repos/sverka-dev/sverka/issues//comments \ + --jq '[.[] | select(.user.login == "coderabbitai[bot]")] | .[0].id') + ``` + +1. Get the comment body: + + ```bash + BODY=$(gh api repos/sverka-dev/sverka/issues/comments/$COMMENT_ID --jq '.body') + ``` + +1. Replace `- [ ]` with `- [x]` on the "Trigger review" line and update: + + ```bash + NEW_BODY=$(echo "$BODY" | sed 's/- \[ \] \( 🔍 Trigger review\)/- [x] \1/') + gh api repos/sverka-dev/sverka/issues/comments/$COMMENT_ID \ + -X PATCH -f body="$NEW_BODY" + ``` + +1. Wait for CodeRabbit to post a new review (poll for new comments). + +### /act convergence check + +All must be 0 on the same HEAD: +1. `open_threads == 0` +2. `CI_REQUIRED_PENDING == 0` (all required CI checks must have status COMPLETED with conclusion SUCCESS — failures and action_required are NOT passing) +3. `SAST_FINDINGS_PENDING == 0` +4. Bot comment count stable +5. `mergeable == MERGEABLE` +6. Quality gates passed +7. CodeRabbit review triggered and completed + +```bash +bun ~/.agents/skills/act/scripts/pr-state.ts +``` + +## Step 3: Merge the TOP PR + close lowers + +### Merge the top PR + +```bash +gh pr merge --squash --delete-branch +``` + +This single squash commit brings ALL stack changes into main. + +### Close all lower PRs in the stack + +For each lower PR (bottom to just-below-top): +```bash +gh pr close --comment "Merged via # (squash). All stack changes are now in main." +git push origin --delete 2>/dev/null || true +``` + +### Pull main + +```bash +git checkout main && git pull +``` + +## Step 4: Retrospect (MANDATORY self-learning) + +After each stack merge + close: + +1. **What worked** — patterns that made /act converge fast +2. **What didn't** — issues that took multiple iterations +3. **Review patterns** — common review feedback +4. **CI patterns** — common CI failures and fixes +5. **Bot patterns** — CodeRabbit/Codacy/Qodo recurring findings +6. **Time to converge** — how many /act iterations +7. **Stack size** — PRs merged vs closed + +Store in Beads only (do NOT use `.gc/retrospects/` or any ad hoc memory files): + +```bash +bd remember "merge-stack retrospect stack top=# : members= merged=# closed= iterations= findings= pattern= lesson=" +``` + +### Feed back into the loop + +Before starting /act on the next stack, refresh Beads context: + +```bash +bd prime +``` + +Apply lessons learned — proactively fix patterns before bots find them. + +## Step 5: Advance to next stack + +If there are more stacks: +1. Take the next stack's TOP PR +2. Rebase onto main (which now has the previous stack's changes) +3. Change base to main +4. Loop back to Step 2 + +If no more stacks — run final retrospect, then done. + +## Final Retrospect + +After all stacks are merged: +1. Total stacks merged +2. Total PRs merged (top) vs closed (lower) +3. Total /act iterations +4. Top 3 recurring patterns +5. Top 3 lessons learned +6. Recommendations for future stacks + +## Rules + +- **TOP-DOWN.** Merge the top PR, close the lowers. Never bottom-up. +- **Never merge a PR that hasn't passed /act convergence.** No exceptions. +- **Never skip CodeRabbit trigger.** Review must be triggered after every push. +- **Never skip retrospect.** Self-learning is mandatory after each stack. +- **One merge per stack.** The top PR's squash includes everything. +- **Close lowers with a comment.** "Merged via #." +- **Squash merge only.** Clean history, one commit per stack. +- **Delete branches after merge/close.** No stale branches. +- **Escalate on blocker.** Don't loop forever on an unfixable issue. +- **Bounded retry budget.** Maximum 10 /act iterations and 2 hours per convergence attempt. Use exponential backoff (60s, 120s, 240s). Escalate to the mayor when either limit is reached. +- **Refresh Beads context before /act.** Run `bd prime` and apply past lessons. + +## Files + +- `merge-stack.toml` — formula definition (steps: discover → act-loop → merge → retrospect → advance) diff --git a/package.json b/package.json index bf60cd1d0..7c7335dc0 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,23 @@ "test": "nx run-many --target=test --all", "lint": "nx run-many --target=lint --all", "typecheck": "nx run-many --target=typecheck --all", - "clean": "nx reset" + "clean": "nx reset", + "format": "biome format --write", + "format:check": "biome format", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,js,mjs,json}": "biome format --write" }, "devDependencies": { + "@biomejs/biome": "^2.5.7", "@types/node": "^24.0.0", - "eslint": "^9.0.0", + "husky": "^9.1.7", + "lint-staged": "^17.3.0", "nx": "^21.0.0", - "prettier": "^3.0.0", + "oxlint": "^1.78.0", "tsdown": "^0.22.14", "typescript": "^5.8.0", - "typescript-eslint": "^8.66.0", "vitest": "^3.0.0" }, "workspaces": [ diff --git a/packages/checks/package.json b/packages/checks/package.json index 06d95b298..e7386b5b3 100644 --- a/packages/checks/package.json +++ b/packages/checks/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/checks/project.json b/packages/checks/project.json index 394f5d13a..38e6cfd07 100644 --- a/packages/checks/project.json +++ b/packages/checks/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/checks" } }, diff --git a/packages/checks/src/__tests__/extract.test.ts b/packages/checks/src/__tests__/extract.test.ts index 93de3ede5..e528975c8 100644 --- a/packages/checks/src/__tests__/extract.test.ts +++ b/packages/checks/src/__tests__/extract.test.ts @@ -44,7 +44,10 @@ describe("extractFindings — non-SARIF format", () => { describe("extractFindings — invalid SARIF", () => { it("throws CheckError(EXTRACTION_FAILED) with cause set", async () => { const dir = makeDir(); - writeFileSync(join(dir, "bad.sarif"), JSON.stringify({ version: "1.0.0", runs: [] })); + writeFileSync( + join(dir, "bad.sarif"), + JSON.stringify({ version: "1.0.0", runs: [] }), + ); const outputs: CheckOutput[] = [{ path: "bad.sarif", format: "sarif" }]; try { await extractFindings(outputs, dir, "mycheck"); diff --git a/packages/checks/src/__tests__/public-api.test.ts b/packages/checks/src/__tests__/public-api.test.ts index 02fcb9ab9..424a935da 100644 --- a/packages/checks/src/__tests__/public-api.test.ts +++ b/packages/checks/src/__tests__/public-api.test.ts @@ -23,6 +23,10 @@ describe("public API", () => { it("does not export unexpected runtime values", () => { const runtimeKeys = Object.keys(api).sort(); - expect(runtimeKeys).toEqual(["CheckError", "createBuiltinResolver", "extractFindings"]); + expect(runtimeKeys).toEqual([ + "CheckError", + "createBuiltinResolver", + "extractFindings", + ]); }); }); diff --git a/packages/checks/src/__tests__/resolver.test.ts b/packages/checks/src/__tests__/resolver.test.ts index 92bdf7df8..3cdeedd71 100644 --- a/packages/checks/src/__tests__/resolver.test.ts +++ b/packages/checks/src/__tests__/resolver.test.ts @@ -130,7 +130,10 @@ describe("createBuiltinResolver — unknown / unmatched", () => { describe("createBuiltinResolver — multiple package managers", () => { it("first matching entry in table order wins (bun before cargo)", () => { - const r = resolver.resolve(makeCheck("test"), makeContext(["cargo", "bun"])); + const r = resolver.resolve( + makeCheck("test"), + makeContext(["cargo", "bun"]), + ); expect(r).not.toBeNull(); // Node entries come before cargo in table order, so bun wins. expect(r!.operation.command).toBe("bun"); diff --git a/packages/checks/src/resolver.ts b/packages/checks/src/resolver.ts index ae4c69f67..1df385b34 100644 --- a/packages/checks/src/resolver.ts +++ b/packages/checks/src/resolver.ts @@ -47,34 +47,73 @@ interface TableEntry { * (by checkId + packageManager) wins. Node entries come before Python, * Rust, and Go so that Node checks take precedence when multiple * package managers are present. + * + * Node entries are generated from a compact spec to avoid 12 near-identical + * hand-written blocks (3 checkIds × 4 package managers). */ +const NODE_PMS = ["bun", "npm", "yarn", "pnpm"] as const; +const NODE_CHECKS = [ + { checkId: "typecheck", args: ["run", "typecheck"] }, + { checkId: "lint", args: ["run", "lint"] }, + { checkId: "test", args: ["run", "test"] }, +] as const; + const TABLE: readonly TableEntry[] = [ - // Node — typecheck - { checkId: "typecheck", packageManagers: ["bun"], command: "bun", args: ["run", "typecheck"] }, - { checkId: "typecheck", packageManagers: ["npm"], command: "npm", args: ["run", "typecheck"] }, - { checkId: "typecheck", packageManagers: ["yarn"], command: "yarn", args: ["run", "typecheck"] }, - { checkId: "typecheck", packageManagers: ["pnpm"], command: "pnpm", args: ["run", "typecheck"] }, - // Node — lint - { checkId: "lint", packageManagers: ["bun"], command: "bun", args: ["run", "lint"] }, - { checkId: "lint", packageManagers: ["npm"], command: "npm", args: ["run", "lint"] }, - { checkId: "lint", packageManagers: ["yarn"], command: "yarn", args: ["run", "lint"] }, - { checkId: "lint", packageManagers: ["pnpm"], command: "pnpm", args: ["run", "lint"] }, - // Node — test - { checkId: "test", packageManagers: ["bun"], command: "bun", args: ["run", "test"] }, - { checkId: "test", packageManagers: ["npm"], command: "npm", args: ["run", "test"] }, - { checkId: "test", packageManagers: ["yarn"], command: "yarn", args: ["run", "test"] }, - { checkId: "test", packageManagers: ["pnpm"], command: "pnpm", args: ["run", "test"] }, + // Node — generated (typecheck/lint/test × bun/npm/yarn/pnpm) + ...NODE_CHECKS.flatMap(({ checkId, args }) => + NODE_PMS.map((pm) => ({ + checkId, + packageManagers: [pm] as readonly PmName[], + command: pm, + args, + })), + ), // Python — lint - { checkId: "lint", packageManagers: ["pip", "poetry", "uv", "pipenv"], command: "ruff", args: ["check"] }, + { + checkId: "lint", + packageManagers: ["pip", "poetry", "uv", "pipenv"], + command: "ruff", + args: ["check"], + }, // Python — test - { checkId: "test", packageManagers: ["pip", "poetry", "uv", "pipenv"], command: "pytest", args: [] }, + { + checkId: "test", + packageManagers: ["pip", "poetry", "uv", "pipenv"], + command: "pytest", + args: [], + }, // Rust - { checkId: "clippy", packageManagers: ["cargo"], command: "cargo", args: ["clippy"] }, - { checkId: "fmt-check", packageManagers: ["cargo"], command: "cargo", args: ["fmt", "--check"] }, - { checkId: "test", packageManagers: ["cargo"], command: "cargo", args: ["test"] }, + { + checkId: "clippy", + packageManagers: ["cargo"], + command: "cargo", + args: ["clippy"], + }, + { + checkId: "fmt-check", + packageManagers: ["cargo"], + command: "cargo", + args: ["fmt", "--check"], + }, + { + checkId: "test", + packageManagers: ["cargo"], + command: "cargo", + args: ["test"], + }, // Go - { checkId: "vet", packageManagers: ["go"], command: "go", args: ["vet", "./..."] }, - { checkId: "test", packageManagers: ["go"], command: "go", args: ["test", "./..."] }, + { + checkId: "vet", + packageManagers: ["go"], + command: "go", + args: ["vet", "./..."], + }, + { + checkId: "test", + packageManagers: ["go"], + command: "go", + args: ["test", "./..."], + }, ]; /** diff --git a/packages/cli/package.json b/packages/cli/package.json index 33e87835a..a96d046b6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,11 +14,13 @@ "bin": { "sverka": "./dist/bin.mjs" }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/cli/project.json b/packages/cli/project.json index 9c8c7cf46..865e5986e 100644 --- a/packages/cli/project.json +++ b/packages/cli/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/cli" } }, diff --git a/packages/cli/src/__tests__/baseline.test.ts b/packages/cli/src/__tests__/baseline.test.ts index 56c80e87f..0e71e8985 100644 --- a/packages/cli/src/__tests__/baseline.test.ts +++ b/packages/cli/src/__tests__/baseline.test.ts @@ -35,10 +35,9 @@ export default defineWorkflow({ it("create creates a baseline from execution", async () => { const out = new CaptureWriter(); - const code = await main( - ["baseline", "create", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "create", "--root", dir], { + output: out, + }); expect(code).toBe(0); expect(existsSync(join(dir, ".sverka", "baseline.json"))).toBe(true); }); @@ -48,10 +47,9 @@ export default defineWorkflow({ output: new CaptureWriter(), }); const out = new CaptureWriter(); - const code = await main( - ["baseline", "show", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "show", "--root", dir], { + output: out, + }); expect(code).toBe(0); expect(out.stdoutText).toContain("Baseline:"); expect(out.stdoutText).toContain("fingerprints:"); @@ -62,10 +60,9 @@ export default defineWorkflow({ output: new CaptureWriter(), }); const out = new CaptureWriter(); - const code = await main( - ["baseline", "update", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "update", "--root", dir], { + output: out, + }); expect(code).toBe(0); expect(out.stdoutText).toContain("updated"); }); @@ -76,20 +73,18 @@ export default defineWorkflow({ }); expect(existsSync(join(dir, ".sverka", "baseline.json"))).toBe(true); const out = new CaptureWriter(); - const code = await main( - ["baseline", "clear", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "clear", "--root", dir], { + output: out, + }); expect(code).toBe(0); expect(existsSync(join(dir, ".sverka", "baseline.json"))).toBe(false); }); it("clear is idempotent when no baseline exists", async () => { const out = new CaptureWriter(); - const code = await main( - ["baseline", "clear", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "clear", "--root", dir], { + output: out, + }); expect(code).toBe(0); }); @@ -100,10 +95,9 @@ export default defineWorkflow({ await mkdir(baselinePath); expect(existsSync(baselinePath)).toBe(true); const out = new CaptureWriter(); - const code = await main( - ["baseline", "clear", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "clear", "--root", dir], { + output: out, + }); expect(code).not.toBe(0); expect(out.stderrText).toContain("error:"); }); @@ -144,20 +138,18 @@ export default defineWorkflow({ await mkdir(join(dir, ".sverka"), { recursive: true }); await writeFile(join(dir, ".sverka", "baseline.json"), "{}", "utf8"); const out = new CaptureWriter(); - const code = await main( - ["baseline", "clear", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "clear", "--root", dir], { + output: out, + }); expect(code).toBe(0); expect(existsSync(join(dir, ".sverka", "baseline.json"))).toBe(false); }); it("unknown subcommand exits with 2", async () => { const out = new CaptureWriter(); - const code = await main( - ["baseline", "frobnicate", "--root", dir], - { output: out }, - ); + const code = await main(["baseline", "frobnicate", "--root", dir], { + output: out, + }); expect(code).toBe(2); }); }); diff --git a/packages/cli/src/__tests__/bin.test.ts b/packages/cli/src/__tests__/bin.test.ts index cc9d1dc01..a3ec0d416 100644 --- a/packages/cli/src/__tests__/bin.test.ts +++ b/packages/cli/src/__tests__/bin.test.ts @@ -2,10 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { existsSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { join } from "node:path"; -import { - makeTempDir, - cleanupTempDir, -} from "./helpers/fixtures.js"; +import { makeTempDir, cleanupTempDir } from "./helpers/fixtures.js"; const BIN_PATH = join(import.meta.dirname, "..", "..", "dist", "bin.mjs"); const binBuilt = existsSync(BIN_PATH); diff --git a/packages/cli/src/__tests__/execute.test.ts b/packages/cli/src/__tests__/execute.test.ts index c15e31d65..82f9700e5 100644 --- a/packages/cli/src/__tests__/execute.test.ts +++ b/packages/cli/src/__tests__/execute.test.ts @@ -85,20 +85,18 @@ export default defineWorkflow({ it("--executor host selects host executor", async () => { await writePassingConfig(dir); const out = new CaptureWriter(); - const code = await main( - ["execute", "--executor", "host", "--root", dir], - { output: out }, - ); + const code = await main(["execute", "--executor", "host", "--root", dir], { + output: out, + }); expect(code).toBe(0); }); it("--format json produces JSON output", async () => { await writePassingConfig(dir); const out = new CaptureWriter(); - const code = await main( - ["execute", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["execute", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("execute"); @@ -109,10 +107,9 @@ export default defineWorkflow({ it("--only-new filters to new findings (no baseline → all new)", async () => { await writePassingConfig(dir); const out = new CaptureWriter(); - const code = await main( - ["execute", "--only-new", "--root", dir], - { output: out }, - ); + const code = await main(["execute", "--only-new", "--root", dir], { + output: out, + }); // No baseline path given → onlyNew with no baseline is a no-op in SDK expect(code).toBe(0); }); @@ -125,7 +122,14 @@ export default defineWorkflow({ }); const out = new CaptureWriter(); const code = await main( - ["execute", "--only-new", "--baseline", ".sverka/baseline.json", "--root", dir], + [ + "execute", + "--only-new", + "--baseline", + ".sverka/baseline.json", + "--root", + dir, + ], { output: out }, ); // If the relative path were resolved against process cwd, loadBaseline @@ -136,10 +140,9 @@ export default defineWorkflow({ it("--format json serializes outcomes as an object, not an empty {}", async () => { await writePassingConfig(dir); const out = new CaptureWriter(); - const code = await main( - ["execute", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["execute", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("execute"); diff --git a/packages/cli/src/__tests__/init.test.ts b/packages/cli/src/__tests__/init.test.ts index 596cd22d0..40ed3b964 100644 --- a/packages/cli/src/__tests__/init.test.ts +++ b/packages/cli/src/__tests__/init.test.ts @@ -56,10 +56,9 @@ describe("init command", () => { it("--template minimal produces minimal config", async () => { const out = new CaptureWriter(); - const code = await main( - ["init", "--root", dir, "--template", "minimal"], - { output: out }, - ); + const code = await main(["init", "--root", dir, "--template", "minimal"], { + output: out, + }); expect(code).toBe(0); const content = await readFile(join(dir, "sverka.config.ts"), "utf8"); expect(content).toContain("lint"); @@ -69,10 +68,9 @@ describe("init command", () => { it("--template full produces a fuller config", async () => { const out = new CaptureWriter(); - const code = await main( - ["init", "--root", dir, "--template", "full"], - { output: out }, - ); + const code = await main(["init", "--root", dir, "--template", "full"], { + output: out, + }); expect(code).toBe(0); const content = await readFile(join(dir, "sverka.config.ts"), "utf8"); expect(content).toContain("defineWorkflow"); @@ -80,10 +78,9 @@ describe("init command", () => { it("--format json includes a numeric durationMs (not hardcoded 0)", async () => { const out = new CaptureWriter(); - const code = await main( - ["init", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["init", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("init"); diff --git a/packages/cli/src/__tests__/inspect.test.ts b/packages/cli/src/__tests__/inspect.test.ts index 3295d9503..85558a3e8 100644 --- a/packages/cli/src/__tests__/inspect.test.ts +++ b/packages/cli/src/__tests__/inspect.test.ts @@ -29,10 +29,9 @@ describe("inspect command", () => { it("--format json prints context as JSON", async () => { const out = new CaptureWriter(); - const code = await main( - ["inspect", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["inspect", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("inspect"); diff --git a/packages/cli/src/__tests__/main.test.ts b/packages/cli/src/__tests__/main.test.ts index d076a6fb4..161421d73 100644 --- a/packages/cli/src/__tests__/main.test.ts +++ b/packages/cli/src/__tests__/main.test.ts @@ -51,7 +51,11 @@ describe("main — exit codes", () => { // is mapped to exit 2 (missing arg). To get exit 3, point at a file that // exists but is not valid JS/TS (CONFIG_LOAD_FAILED). const badPath = join(dir, "sverka.config.ts"); - await writefile(dir, "sverka.config.ts", "this is not valid typescript {{{"); + await writefile( + dir, + "sverka.config.ts", + "this is not valid typescript {{{", + ); const out = new CaptureWriter(); const code = await main(["validate", "--config", badPath, "--root", dir], { output: out, @@ -72,10 +76,9 @@ describe("main — global flags", () => { it("--format json produces JSON output on stdout", async () => { const out = new CaptureWriter(); - const code = await main( - ["init", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["init", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("init"); diff --git a/packages/cli/src/__tests__/output.test.ts b/packages/cli/src/__tests__/output.test.ts index 93c424a35..7a2d3eccf 100644 --- a/packages/cli/src/__tests__/output.test.ts +++ b/packages/cli/src/__tests__/output.test.ts @@ -31,12 +31,7 @@ describe("CliError", () => { it("preserves cause when provided", () => { const inner = new Error("inner"); - const err = new CliError( - "wrap", - "SDK_ERROR", - ExitCode.RuntimeError, - inner, - ); + const err = new CliError("wrap", "SDK_ERROR", ExitCode.RuntimeError, inner); expect(err.cause).toBe(inner); }); @@ -120,7 +115,13 @@ describe("createOutputWriter", () => { it("does not write debug when not verbose", () => { const err: string[] = []; const w = createOutputWriter( - { format: "human", config: null, root: ".", quiet: false, verbose: false }, + { + format: "human", + config: null, + root: ".", + quiet: false, + verbose: false, + }, () => {}, (s) => err.push(s), ); diff --git a/packages/cli/src/__tests__/plan.test.ts b/packages/cli/src/__tests__/plan.test.ts index 27af2ed9e..f6d68151d 100644 --- a/packages/cli/src/__tests__/plan.test.ts +++ b/packages/cli/src/__tests__/plan.test.ts @@ -36,10 +36,9 @@ describe("plan command", () => { it("--format json prints plan as JSON", async () => { const out = new CaptureWriter(); - const code = await main( - ["plan", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["plan", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("plan"); diff --git a/packages/cli/src/__tests__/public-api.test.ts b/packages/cli/src/__tests__/public-api.test.ts index 236a021de..0ac653c70 100644 --- a/packages/cli/src/__tests__/public-api.test.ts +++ b/packages/cli/src/__tests__/public-api.test.ts @@ -41,7 +41,13 @@ describe("public API — exports", () => { it("createOutputWriter returns a writer", () => { const w = createOutputWriter( - { format: "human", config: null, root: ".", quiet: false, verbose: false }, + { + format: "human", + config: null, + root: ".", + quiet: false, + verbose: false, + }, () => {}, () => {}, ); @@ -56,7 +62,10 @@ describe("public API — exports", () => { quiet: false, verbose: false, }; - const _w: OutputWriter = new ConsoleOutputWriter(() => {}, () => {}); + const _w: OutputWriter = new ConsoleOutputWriter( + () => {}, + () => {}, + ); const _c: CliErrorCode = "UNKNOWN_COMMAND"; const _d: MainDeps = {}; const _s: WriteSink = () => {}; diff --git a/packages/cli/src/__tests__/validate.test.ts b/packages/cli/src/__tests__/validate.test.ts index 865c3fa26..491931628 100644 --- a/packages/cli/src/__tests__/validate.test.ts +++ b/packages/cli/src/__tests__/validate.test.ts @@ -94,10 +94,9 @@ export default defineWorkflow({ `, ); const out = new CaptureWriter(); - const code = await main( - ["validate", "--format", "json", "--root", dir], - { output: out }, - ); + const code = await main(["validate", "--format", "json", "--root", dir], { + output: out, + }); expect(code).toBe(0); const parsed = JSON.parse(out.stdoutText.trim()); expect(parsed.command).toBe("validate"); diff --git a/packages/cli/src/commands/baseline.ts b/packages/cli/src/commands/baseline.ts index 677863354..704ff0e25 100644 --- a/packages/cli/src/commands/baseline.ts +++ b/packages/cli/src/commands/baseline.ts @@ -70,12 +70,18 @@ async function baselineCreate( output.writeLine( JSON.stringify({ command: "baseline", - data: { action: "create", path, fingerprints: baseline.fingerprints.length }, + data: { + action: "create", + path, + fingerprints: baseline.fingerprints.length, + }, durationMs, }), ); } else { - output.writeLine(`Baseline created: ${path} (${baseline.fingerprints.length} fingerprints)`); + output.writeLine( + `Baseline created: ${path} (${baseline.fingerprints.length} fingerprints)`, + ); } return ExitCode.Success; } @@ -103,12 +109,18 @@ async function baselineUpdate( output.writeLine( JSON.stringify({ command: "baseline", - data: { action: "update", path, fingerprints: updated.fingerprints.length }, + data: { + action: "update", + path, + fingerprints: updated.fingerprints.length, + }, durationMs, }), ); } else { - output.writeLine(`Baseline updated: ${path} (${updated.fingerprints.length} fingerprints)`); + output.writeLine( + `Baseline updated: ${path} (${updated.fingerprints.length} fingerprints)`, + ); } return ExitCode.Success; } diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 65aa82a3d..deafd03e9 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import type { GlobalFlags, OutputWriter } from "../types.js"; import { ExitCode } from "../types.js"; @@ -50,14 +50,18 @@ export async function doctorCommand( function runCheck(name: string, binary: string): DoctorCheck { try { - const out = execSync(`${binary} --version`, { + const result = spawnSync(binary, ["--version"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", // Bound the check so a hung shim (broken PATH entry, blocking prompt, // etc.) cannot hang the CLI indefinitely. A timeout throws and is // treated the same as a missing tool. timeout: 5000, - }).trim(); + }); + if (result.error || result.status !== 0) { + return { name, status: "missing", version: null }; + } + const out = (result.stdout ?? "").trim(); // Extract the first x.y.z version token so output like "git version 2.43.0" // or "node v22.0.0" is normalized to "2.43.0" / "22.0.0". const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/); diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index cec5968ad..3bf49d241 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -21,7 +21,9 @@ export async function executeCommand( start: number, ): Promise { const executor = args.executor === "docker" ? "docker" : "host"; - output.debug(`execute: root=${global.root} executor=${executor} onlyNew=${Boolean(args.onlyNew)}`); + output.debug( + `execute: root=${global.root} executor=${executor} onlyNew=${Boolean(args.onlyNew)}`, + ); if (executor === "docker" && !isBinaryAvailable("docker")) { throw new CliError( diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5e0b3dffd..b55c32972 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -45,7 +45,9 @@ export async function initCommand( output: OutputWriter, start: number, ): Promise { - output.debug(`init: root=${global.root} template=${args.template ?? "minimal"} force=${Boolean(args.force)}`); + output.debug( + `init: root=${global.root} template=${args.template ?? "minimal"} force=${Boolean(args.force)}`, + ); const template = args.template ?? "minimal"; if (template !== "minimal" && template !== "full") { throw new CliError( @@ -67,7 +69,8 @@ export async function initCommand( const content = template === "full" ? FULL_TEMPLATE : MINIMAL_TEMPLATE; // Use exclusive create (wx) when not forcing to close the TOCTOU race // between existsSync and writeFile. With --force, use standard write. - const flags: WriteFileOptions = args.force ? "utf8" : { encoding: "utf8", flag: "wx" }; + const flags: WriteFileOptions = + args.force === true ? "utf8" : { encoding: "utf8", flag: "wx" }; try { await writeFile(configPath, content, flags); } catch (e) { diff --git a/packages/cli/src/commands/inspect.ts b/packages/cli/src/commands/inspect.ts index 8c326d295..b969065d5 100644 --- a/packages/cli/src/commands/inspect.ts +++ b/packages/cli/src/commands/inspect.ts @@ -28,7 +28,9 @@ export async function inspectCommand( output.writeLine(` commit: ${context.commit}`); output.writeLine(` dirty: ${context.dirty}`); output.writeLine(` changed files: ${context.changedFiles.length}`); - output.writeLine(` languages: ${context.languages.map((l) => l.name).join(", ") || "(none)"}`); + output.writeLine( + ` languages: ${context.languages.map((l) => l.name).join(", ") || "(none)"}`, + ); output.writeLine( ` package managers: ${context.packageManagers.map((p) => p.name).join(", ") || "(none)"}`, ); diff --git a/packages/cli/src/commands/plan.ts b/packages/cli/src/commands/plan.ts index c62ceec98..5c1403db4 100644 --- a/packages/cli/src/commands/plan.ts +++ b/packages/cli/src/commands/plan.ts @@ -45,7 +45,9 @@ export async function planCommand( if (result.proposal) { output.writeLine(` proposed checks: ${result.proposal.checks.length}`); for (const check of result.proposal.checks) { - output.writeLine(` - ${check.checkId} (priority: ${check.priority})`); + output.writeLine( + ` - ${check.checkId} (priority: ${check.priority})`, + ); } } else { output.writeLine(" proposal: (from config)"); diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index ec4eed62e..fc726c946 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -12,7 +12,9 @@ export async function validateCommand( output: OutputWriter, start: number, ): Promise { - output.debug(`validate: root=${global.root} config=${global.config ?? "(auto)"}`); + output.debug( + `validate: root=${global.root} config=${global.config ?? "(auto)"}`, + ); // Resolve an explicit relative --config against --root so validation honors // the selected root, matching auto-discovery via findConfig(global.root). let configPath: string | null = global.config diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index c43b769c4..575916b42 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -23,10 +23,7 @@ export interface MainDeps { * @param argv Command-line arguments (excluding node and script path). * @returns Exit code (0 = success, 1 = policy fail, 2 = usage error, 3 = runtime error). */ -export async function main( - argv: string[], - deps?: MainDeps, -): Promise { +export async function main(argv: string[], deps?: MainDeps): Promise { const start = Date.now(); // Output writer: injected (tests) or console (production). @@ -63,18 +60,19 @@ export async function main( // Re-create output writer with actual parsed flags (so quiet/verbose/format // are respected). If deps injected a writer, wrap it with flag semantics. - const realOutput = - deps?.output - ? wrapOutputWriter(global, deps.output) - : createOutputWriter( - global, - (s) => process.stdout.write(s), - (s) => process.stderr.write(s), - ); + const realOutput = deps?.output + ? wrapOutputWriter(global, deps.output) + : createOutputWriter( + global, + (s) => process.stdout.write(s), + (s) => process.stderr.write(s), + ); const command = String(parsed._[0] ?? ""); - realOutput.debug(`sverka: command=${command} root=${global.root} format=${global.format}`); + realOutput.debug( + `sverka: command=${command} root=${global.root} format=${global.format}`, + ); try { return await dispatch(command, parsed, global, realOutput, start); @@ -109,30 +107,24 @@ function buildParser(): Argv { .command("plan", "Synthesize a plan without executing", (y) => y.option("only-new", { type: "boolean", default: false }), ) - .command( - ["execute", "run"], - "Execute the workflow locally", - (y) => - y - .option("executor", { - type: "string", - default: "host", - choices: ["host", "docker"], - }) - .option("only-new", { type: "boolean", default: false }) - .option("baseline", { type: "string" }), + .command(["execute", "run"], "Execute the workflow locally", (y) => + y + .option("executor", { + type: "string", + default: "host", + choices: ["host", "docker"], + }) + .option("only-new", { type: "boolean", default: false }) + .option("baseline", { type: "string" }), ) .command("validate", "Validate a sverka.config.ts without executing") - .command( - "baseline", - "Manage the findings baseline", - (y) => - y - .command("create", "Create a baseline from execution") - .command("update", "Update the baseline") - .command("show", "Display the baseline") - .command("clear", "Remove the baseline file") - .option("baseline", { type: "string" }), + .command("baseline", "Manage the findings baseline", (y) => + y + .command("create", "Create a baseline from execution") + .command("update", "Update the baseline") + .command("show", "Display the baseline") + .command("clear", "Remove the baseline file") + .option("baseline", { type: "string" }), ) .command("doctor", "Diagnose environment and dependencies") .demandCommand(1, "No command given") @@ -154,7 +146,8 @@ async function dispatch( case "init": return initCommand( { - template: typeof parsed.template === "string" ? parsed.template : undefined, + template: + typeof parsed.template === "string" ? parsed.template : undefined, force: Boolean(parsed.force), }, global, @@ -174,9 +167,11 @@ async function dispatch( case "run": return executeCommand( { - executor: typeof parsed.executor === "string" ? parsed.executor : "host", + executor: + typeof parsed.executor === "string" ? parsed.executor : "host", onlyNew: Boolean(parsed["only-new"]), - baseline: typeof parsed.baseline === "string" ? parsed.baseline : undefined, + baseline: + typeof parsed.baseline === "string" ? parsed.baseline : undefined, }, global, output, @@ -187,7 +182,11 @@ async function dispatch( case "baseline": { const sub = String(parsed._[1] ?? ""); return baselineCommand( - { subcommand: sub, baselinePath: typeof parsed.baseline === "string" ? parsed.baseline : undefined }, + { + subcommand: sub, + baselinePath: + typeof parsed.baseline === "string" ? parsed.baseline : undefined, + }, global, output, start, @@ -204,11 +203,7 @@ async function dispatch( } } -function handleError( - e: unknown, - output: OutputWriter, - _start: number, -): number { +function handleError(e: unknown, output: OutputWriter, _start: number): number { if (e instanceof CliError) { output.errorLine(`error: ${e.message}`); return e.exitCode; diff --git a/packages/compiler-earthly/package.json b/packages/compiler-earthly/package.json index 1ba995f07..ae54e67d7 100644 --- a/packages/compiler-earthly/package.json +++ b/packages/compiler-earthly/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.js" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/compiler-earthly/project.json b/packages/compiler-earthly/project.json index e9833c99b..b179a46bb 100644 --- a/packages/compiler-earthly/project.json +++ b/packages/compiler-earthly/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/compiler-earthly" } }, diff --git a/packages/compiler-github/package.json b/packages/compiler-github/package.json index ccae48848..20be0eaf8 100644 --- a/packages/compiler-github/package.json +++ b/packages/compiler-github/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/compiler-github/project.json b/packages/compiler-github/project.json index 352c562ab..fe1a10968 100644 --- a/packages/compiler-github/project.json +++ b/packages/compiler-github/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/compiler-github" } }, diff --git a/packages/compiler-github/src/__tests__/compile.test.ts b/packages/compiler-github/src/__tests__/compile.test.ts index 2e732a619..54313b3e4 100644 --- a/packages/compiler-github/src/__tests__/compile.test.ts +++ b/packages/compiler-github/src/__tests__/compile.test.ts @@ -62,16 +62,12 @@ describe("compileGithubWorkflow — credentials", () => { const plan = makePlan({ operations: [ makeOperation({ - credentials: [ - { name: "token", envVar: "API_TOKEN", required: true }, - ], + credentials: [{ name: "token", envVar: "API_TOKEN", required: true }], }), makeOperation({ id: "op-2", name: "lint", - credentials: [ - { name: "key", envVar: "SECRET_KEY", required: true }, - ], + credentials: [{ name: "key", envVar: "SECRET_KEY", required: true }], }), ], }); @@ -89,16 +85,12 @@ describe("compileGithubWorkflow — credentials", () => { const plan = makePlan({ operations: [ makeOperation({ - credentials: [ - { name: "t", envVar: "TOKEN", required: true }, - ], + credentials: [{ name: "t", envVar: "TOKEN", required: true }], }), makeOperation({ id: "op-2", name: "lint", - credentials: [ - { name: "t2", envVar: "TOKEN", required: true }, - ], + credentials: [{ name: "t2", envVar: "TOKEN", required: true }], }), ], }); diff --git a/packages/compiler-github/src/__tests__/helpers/fixtures.ts b/packages/compiler-github/src/__tests__/helpers/fixtures.ts index 659eba8f4..8b685cc46 100644 --- a/packages/compiler-github/src/__tests__/helpers/fixtures.ts +++ b/packages/compiler-github/src/__tests__/helpers/fixtures.ts @@ -42,17 +42,19 @@ export function makePlan(overrides: Partial = {}): Plan { /** * Build a Plan with operations declaring the given credential envVars. */ -export function makePlanWithCredentials( - envVars: string[], -): Plan { +export function makePlanWithCredentials(envVars: string[]): Plan { const operations = envVars.map((envVar, i) => makeOperation({ id: `op-${i}`, name: `check-${i}`, credentials: [ - { name: `cred-${i}`, envVar, required: true } satisfies CredentialDeclaration, + { + name: `cred-${i}`, + envVar, + required: true, + } satisfies CredentialDeclaration, ], - }) + }), ); return makePlan({ operations }); } diff --git a/packages/compiler-github/src/compile.ts b/packages/compiler-github/src/compile.ts index 5bb956ebc..07232f5f7 100644 --- a/packages/compiler-github/src/compile.ts +++ b/packages/compiler-github/src/compile.ts @@ -46,7 +46,9 @@ function buildTriggers(triggers?: GithubTriggers): Record { * Build the `permissions:` section. Converts camelCase keys to kebab-case * for GitHub Actions YAML (e.g. securityEvents → security-events). */ -function buildPermissions(permissions?: GithubPermissions): Record { +function buildPermissions( + permissions?: GithubPermissions, +): Record { const source = permissions ?? { contents: "read" as const }; const result: Record = {}; for (const [key, value] of Object.entries(source)) { diff --git a/packages/compiler-gitlab/package.json b/packages/compiler-gitlab/package.json index 000e8a711..4d3282e4b 100644 --- a/packages/compiler-gitlab/package.json +++ b/packages/compiler-gitlab/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/compiler-gitlab/project.json b/packages/compiler-gitlab/project.json index 3a0b24cc6..bf4c05cca 100644 --- a/packages/compiler-gitlab/project.json +++ b/packages/compiler-gitlab/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/compiler-gitlab" } }, diff --git a/packages/compiler-gitlab/src/__tests__/compile.test.ts b/packages/compiler-gitlab/src/__tests__/compile.test.ts index 92a6f3a0a..2e8795477 100644 --- a/packages/compiler-gitlab/src/__tests__/compile.test.ts +++ b/packages/compiler-gitlab/src/__tests__/compile.test.ts @@ -51,16 +51,13 @@ describe("compileGitlabCi — custom rules", () => { describe("compileGitlabCi — empty rules filtered", () => { it("filters out empty rule objects that would produce invalid GitLab YAML", () => { const yaml = compileGitlabCi(makePlan(), { - rules: [ - { if: '$CI_COMMIT_BRANCH == "main"' }, - {}, - { when: "manual" }, - ], + rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }, {}, { when: "manual" }], }); expect(yaml).toContain('$CI_COMMIT_BRANCH == "main"'); expect(yaml).toContain("when: manual"); // Empty rule should not produce a bare `-` entry - expect(yaml).not.toMatch(/-\s*\n\s*-\s/i); + const bareDash = /-\s*\n\s*-\s/; + expect(yaml).not.toMatch(bareDash); }); }); diff --git a/packages/core/package.json b/packages/core/package.json index 98264a092..d2d858375 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/core/project.json b/packages/core/project.json index 905e809e3..ae8243b47 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/core" } }, diff --git a/packages/core/src/__tests__/composition.test.ts b/packages/core/src/__tests__/composition.test.ts index c040dac9b..54379aadd 100644 --- a/packages/core/src/__tests__/composition.test.ts +++ b/packages/core/src/__tests__/composition.test.ts @@ -15,7 +15,10 @@ describe("concatDedupe", () => { describe("mergeSpecs", () => { it("scalar fields: b wins when defined", () => { - const merged = mergeSpecs({ command: "a", image: "node:20" }, { command: "b" }); + const merged = mergeSpecs( + { command: "a", image: "node:20" }, + { command: "b" }, + ); expect(merged.command).toBe("b"); expect(merged.image).toBe("node:20"); }); @@ -28,7 +31,10 @@ describe("mergeSpecs", () => { }); it("dependsOn is concatenated and deduplicated", () => { - const merged = mergeSpecs({ dependsOn: ["a", "b"] }, { dependsOn: ["b", "c"] }); + const merged = mergeSpecs( + { dependsOn: ["a", "b"] }, + { dependsOn: ["b", "c"] }, + ); expect(merged.dependsOn).toEqual(["a", "b", "c"]); }); diff --git a/packages/core/src/__tests__/conditions.test.ts b/packages/core/src/__tests__/conditions.test.ts index de8cb8951..ad1f4148b 100644 --- a/packages/core/src/__tests__/conditions.test.ts +++ b/packages/core/src/__tests__/conditions.test.ts @@ -44,15 +44,23 @@ describe("evaluateCondition", () => { }); it("!= is the negation of ==", () => { - expect(evaluateCondition("schedule != 'ci'", { schedule: "nightly" })).toBe(true); - expect(evaluateCondition("schedule != 'nightly'", { schedule: "nightly" })).toBe(false); + expect(evaluateCondition("schedule != 'ci'", { schedule: "nightly" })).toBe( + true, + ); + expect( + evaluateCondition("schedule != 'nightly'", { schedule: "nightly" }), + ).toBe(false); }); it("NOT > AND > OR precedence", () => { // a && b || !c with a=true,b=false,c=false => (true && false) || (!false) => false || true => true - expect(evaluateCondition("a && b || !c", { a: true, b: false, c: false })).toBe(true); + expect( + evaluateCondition("a && b || !c", { a: true, b: false, c: false }), + ).toBe(true); // a && b || !c with a=true,b=false,c=true => (true && false) || (!true) => false || false => false - expect(evaluateCondition("a && b || !c", { a: true, b: false, c: true })).toBe(false); + expect( + evaluateCondition("a && b || !c", { a: true, b: false, c: true }), + ).toBe(false); }); it("NOT binds tighter than AND", () => { @@ -73,7 +81,11 @@ describe("evaluateCondition", () => { }); it("whitespace is tolerated", () => { - expect(evaluateCondition(" schedule == 'nightly' ", { schedule: "nightly" })).toBe(true); + expect( + evaluateCondition(" schedule == 'nightly' ", { + schedule: "nightly", + }), + ).toBe(true); }); it("throws CompositionError on malformed expression", () => { @@ -91,7 +103,9 @@ describe("evaluateCondition", () => { } catch (err) { expect(err).toBeInstanceOf(CompositionError); expect((err as CompositionError).code).toBe("COMPOSITION_ERROR"); - expect((err as CompositionError).context).toMatchObject({ reason: expect.any(String) }); + expect((err as CompositionError).context).toMatchObject({ + reason: expect.any(String), + }); } }); }); diff --git a/packages/core/src/__tests__/dag.test.ts b/packages/core/src/__tests__/dag.test.ts index 719a402eb..ad5098a7a 100644 --- a/packages/core/src/__tests__/dag.test.ts +++ b/packages/core/src/__tests__/dag.test.ts @@ -74,7 +74,10 @@ describe("matrix validation (deferred to planning)", () => { expect(() => matrix({ node: [] }, run({ command: "test" }))).not.toThrow(); // non-array value — also deferred expect(() => - matrix({ node: "not-array" as unknown as readonly unknown[] }, run({ command: "test" })), + matrix( + { node: "not-array" as unknown as readonly unknown[] }, + run({ command: "test" }), + ), ).not.toThrow(); }); diff --git a/packages/core/src/__tests__/helpers/runtime.ts b/packages/core/src/__tests__/helpers/runtime.ts index d7e03acbc..89b12a32b 100644 --- a/packages/core/src/__tests__/helpers/runtime.ts +++ b/packages/core/src/__tests__/helpers/runtime.ts @@ -26,19 +26,23 @@ export function makeRuntime(opts: { ...(opts.context !== undefined ? { context: opts.context } : {}), async evaluate(operation: OperationSpec): Promise { evaluated.push(operation); - const outcome: OperationOutcome = - opts.onEvaluate?.(operation) ?? { - operationId: operation.id, - status: mode === "plan" ? "planned" : "success", - durationMs: 0, - }; + const outcome: OperationOutcome = opts.onEvaluate?.(operation) ?? { + operationId: operation.id, + status: mode === "plan" ? "planned" : "success", + durationMs: 0, + }; outcomes.push(outcome); return outcome; }, async finalize(): Promise { const artifacts = mode === "compile" - ? [{ name: "compiled", content: evaluated.map((o) => o.id).join("\n") }] + ? [ + { + name: "compiled", + content: evaluated.map((o) => o.id).join("\n"), + }, + ] : undefined; return { mode, @@ -52,7 +56,10 @@ export function makeRuntime(opts: { /** A minimal plan-mode runtime (no side effects, records operations). */ export function makePlanRuntime(context?: PlanContext): Runtime { - return makeRuntime({ mode: "plan", ...(context !== undefined ? { context } : {}) }); + return makeRuntime({ + mode: "plan", + ...(context !== undefined ? { context } : {}), + }); } /** An execution-mode runtime that records evaluate calls. */ @@ -69,5 +76,8 @@ export function makeExecuteRuntime( /** A compile-mode runtime that emits a string artifact. */ export function makeCompileRuntime(context?: PlanContext): Runtime { - return makeRuntime({ mode: "compile", ...(context !== undefined ? { context } : {}) }); + return makeRuntime({ + mode: "compile", + ...(context !== undefined ? { context } : {}), + }); } diff --git a/packages/core/src/__tests__/matrix.test.ts b/packages/core/src/__tests__/matrix.test.ts index 0c82bd730..a7ef65f1f 100644 --- a/packages/core/src/__tests__/matrix.test.ts +++ b/packages/core/src/__tests__/matrix.test.ts @@ -11,18 +11,24 @@ describe("matrix expansion", () => { const result = await wf.plan(makePlanRuntime()); expect(result.operations).toHaveLength(2); const ids = result.operations.map((o) => o.id).sort(); - expect(ids).toEqual(["run:test[node=s:20]", "run:test[node=s:24]"]); + expect(ids).toEqual(["run:test[node=20]", "run:test[node=24]"]); const envs = result.operations.map((o) => o.env?.MATRIX_NODE).sort(); expect(envs).toEqual(["20", "24"]); }); it("multi-dimension cartesian product with joined id suffix", async () => { - const op = matrix({ node: ["20", "24"], os: ["linux"] }, run({ command: "test" })); + const op = matrix( + { node: ["20", "24"], os: ["linux"] }, + run({ command: "test" }), + ); const wf = workflow("matrix-2d", op); const result = await wf.plan(makePlanRuntime()); expect(result.operations).toHaveLength(2); const ids = result.operations.map((o) => o.id).sort(); - expect(ids).toEqual(["run:test[node=s:20,os=s:linux]", "run:test[node=s:24,os=s:linux]"]); + expect(ids).toEqual([ + "run:test[node=20,os=linux]", + "run:test[node=24,os=linux]", + ]); for (const spec of result.operations) { expect(spec.env?.MATRIX_NODE).toBeDefined(); expect(spec.env?.MATRIX_OS).toBe("linux"); @@ -47,7 +53,9 @@ describe("matrix expansion", () => { const wf = workflow("matrix-marker", op); const result = await wf.plan(makePlanRuntime()); for (const spec of result.operations) { - expect((spec as unknown as Record).__matrixTemplate).toBeUndefined(); + expect( + (spec as unknown as Record).__matrixTemplate, + ).toBeUndefined(); } }); diff --git a/packages/core/src/__tests__/public-api.test.ts b/packages/core/src/__tests__/public-api.test.ts index 24f348675..c190fd2fb 100644 --- a/packages/core/src/__tests__/public-api.test.ts +++ b/packages/core/src/__tests__/public-api.test.ts @@ -43,7 +43,16 @@ describe("public API surface", () => { // named exports of the public barrel. const publicNames = Object.keys(api); const internalLeaked = publicNames.filter((n) => - ["createNode", "asNode", "withSpec", "mergeSpecs", "concatDedupe", "planWorkflow", "assignId", "evaluateCondition"].includes(n), + [ + "createNode", + "asNode", + "withSpec", + "mergeSpecs", + "concatDedupe", + "planWorkflow", + "assignId", + "evaluateCondition", + ].includes(n), ); expect(internalLeaked).toEqual([]); }); diff --git a/packages/core/src/__tests__/runtime-modes.test.ts b/packages/core/src/__tests__/runtime-modes.test.ts index c79f94587..14f2967aa 100644 --- a/packages/core/src/__tests__/runtime-modes.test.ts +++ b/packages/core/src/__tests__/runtime-modes.test.ts @@ -50,7 +50,10 @@ describe("Runtime modes", () => { it("skipped condition: evaluate is NOT called, status is 'skipped'", async () => { const evaluated: string[] = []; - const nightly = when("schedule == 'nightly'", run({ command: "full-scan" })); + const nightly = when( + "schedule == 'nightly'", + run({ command: "full-scan" }), + ); const wf = workflow("cond", nightly); const result = await wf.plan( makeExecuteRuntime({ schedule: "ci" }, (spec) => { @@ -66,7 +69,10 @@ describe("Runtime modes", () => { it("included condition: evaluate IS called", async () => { const evaluated: string[] = []; - const nightly = when("schedule == 'nightly'", run({ command: "full-scan" })); + const nightly = when( + "schedule == 'nightly'", + run({ command: "full-scan" }), + ); const wf = workflow("cond-in", nightly); await wf.plan( makeExecuteRuntime({ schedule: "nightly" }, (spec) => { diff --git a/packages/core/src/composables/parallel.ts b/packages/core/src/composables/parallel.ts index 494979660..0fcc79955 100644 --- a/packages/core/src/composables/parallel.ts +++ b/packages/core/src/composables/parallel.ts @@ -17,7 +17,8 @@ export function parallel(...operations: Operation[]): Operation { const join: OperationNode = createNode("custom", { name: "parallel-join", [JOIN_MARKER]: true, - } as Partial & Record); + } as Partial & + Record); if (operations.length === 0) return join; return join.with(...operations); } diff --git a/packages/core/src/internal/conditions.ts b/packages/core/src/internal/conditions.ts index e780e09b4..b0e1de943 100644 --- a/packages/core/src/internal/conditions.ts +++ b/packages/core/src/internal/conditions.ts @@ -160,7 +160,10 @@ class Parser { private parseOr(): boolean { let left = this.parseAnd(); - while (this.peek()?.type === "op" && (this.peek() as { value: string }).value === "||") { + while ( + this.peek()?.type === "op" && + (this.peek() as { value: string }).value === "||" + ) { this.next(); const right = this.parseAnd(); left = left || right; @@ -170,7 +173,10 @@ class Parser { private parseAnd(): boolean { let left = this.parseNot(); - while (this.peek()?.type === "op" && (this.peek() as { value: string }).value === "&&") { + while ( + this.peek()?.type === "op" && + (this.peek() as { value: string }).value === "&&" + ) { this.next(); const right = this.parseNot(); left = left && right; diff --git a/packages/core/src/internal/ids.ts b/packages/core/src/internal/ids.ts index b7cabe147..949cd50b0 100644 --- a/packages/core/src/internal/ids.ts +++ b/packages/core/src/internal/ids.ts @@ -46,12 +46,44 @@ export function matrixChildId( } function formatMatrixValue(v: unknown): string { - // Preserve type distinctions: prefix with a type tag so that values - // like 1 (number) and "1" (string) produce distinct matrix child ids. - if (typeof v === "string") return `s:${v}`; - if (typeof v === "number") return `n:${v}`; - if (typeof v === "boolean") return `b:${v}`; - return `o:${String(v)}`; + // Primitives use String() for backward compatibility. + // Objects use a type-tagged representation that distinguishes + // Map, Set, Array, and plain objects without collisions. + // JSON.stringify maps distinct values (Map, Set, {}, {value:undefined}) + // to "{}" and throws TypeError for cyclic objects, so we avoid it. + if (v === null) return "null"; + if (v === undefined) return "undefined"; + if (typeof v === "object") { + if (Array.isArray(v)) return `array:${encodeArray(v)}`; + if (v instanceof Map) return `map:${encodeMap(v)}`; + if (v instanceof Set) return `set:${encodeSet(v)}`; + return `object:${encodeObject(v as Record)}`; + } + return String(v); +} + +function encodeArray(arr: unknown[]): string { + return `[${arr.map(formatMatrixValue).join(",")}]`; +} + +function encodeMap(m: Map): string { + const entries = Array.from(m.entries()).map( + ([k, val]) => `${formatMatrixValue(k)}:${formatMatrixValue(val)}`, + ); + return `{${entries.join(",")}}`; +} + +function encodeSet(s: Set): string { + const values = Array.from(s).map(formatMatrixValue); + return `{${values.join(",")}}`; +} + +function encodeObject(obj: Record): string { + const keys = Object.keys(obj).sort(); + const entries = keys.map( + (k) => `${formatMatrixValue(k)}:${formatMatrixValue(obj[k])}`, + ); + return `{${entries.join(",")}}`; } /** Validate that a kind is a known {@link OperationKind}. */ @@ -68,6 +100,8 @@ export function isKnownKind(kind: string): kind is OperationKind { } /** Public spec id helper — exposed for tests that build specs directly. */ -export function specId(spec: Readonly>): string | undefined { +export function specId( + spec: Readonly>, +): string | undefined { return spec.id; } diff --git a/packages/core/src/internal/merge.ts b/packages/core/src/internal/merge.ts index 1e81291a5..3ebd13302 100644 --- a/packages/core/src/internal/merge.ts +++ b/packages/core/src/internal/merge.ts @@ -7,18 +7,36 @@ export function concatDedupe(arr: readonly string[]): string[] { /** All valid keys of {@link OperationSpec}. Used to reject unknown/injected keys. */ const SPEC_KEYS: ReadonlySet = new Set([ - "id", "kind", "name", "description", "command", "args", "env", "workingDir", - "image", "imageDigest", "dependsOn", "condition", "matrix", "cpuLimit", - "memoryLimit", "timeoutSeconds", "retries", "continueOnError", "cache", - "artifacts", "network", "credentials", "tags", -]); - -/** Fields whose values are arrays that should be concatenated (not replaced). */ -const CONCAT_FIELDS: ReadonlySet = new Set([ + "id", + "kind", + "name", + "description", + "command", + "args", + "env", + "workingDir", + "image", + "imageDigest", "dependsOn", + "condition", + "matrix", + "cpuLimit", + "memoryLimit", + "timeoutSeconds", + "retries", + "continueOnError", + "cache", + "artifacts", + "network", + "credentials", "tags", ]); +/** Fields whose values are arrays that should be concatenated (not replaced). */ +const CONCAT_FIELDS: ReadonlySet = new Set< + keyof OperationSpec +>(["dependsOn", "tags"]); + /** * Merge two partial specs. Scalar fields: `b` wins when defined. `dependsOn` * and `tags`: arrays concatenated and deduplicated. Nested objects (`cache`, diff --git a/packages/core/src/internal/node.ts b/packages/core/src/internal/node.ts index 33cfd283a..5a170aa4f 100644 --- a/packages/core/src/internal/node.ts +++ b/packages/core/src/internal/node.ts @@ -24,36 +24,37 @@ interface NodeFields { /** Build an immutable {@link OperationNode} with working composable methods. */ function makeNode(fields: NodeFields): OperationNode { const { kind, spec, predecessors, siblings } = fields; - // Snapshot the spec so later mutation by the caller cannot affect the node. - const specSnapshot: Readonly> = { ...spec }; const node: OperationNode = { kind, - spec: specSnapshot, + spec, predecessors, siblings, ...(fields._id !== undefined ? { _id: fields._id } : {}), after: (...predecessorsToAdd: Operation[]): Operation => makeNode({ kind, - spec: specSnapshot, - predecessors: [...predecessors, ...predecessorsToAdd.map(asNode)], + spec, + predecessors: [ + ...predecessors, + ...(predecessorsToAdd as OperationNode[]), + ], siblings, }), with: (...siblingsToAdd: Operation[]): Operation => makeNode({ kind, - spec: specSnapshot, + spec, predecessors, - siblings: [...siblings, ...siblingsToAdd.map(asNode)], + siblings: [...siblings, ...(siblingsToAdd as OperationNode[])], }), named: (name: string): Operation => - makeNode({ kind, spec: { ...specSnapshot, name }, predecessors, siblings }), + makeNode({ kind, spec: { ...spec, name }, predecessors, siblings }), tagged: (...tags: string[]): Operation => makeNode({ kind, spec: { - ...specSnapshot, - tags: concatDedupe([...(specSnapshot.tags ?? []), ...tags]), + ...spec, + tags: concatDedupe([...(spec.tags ?? []), ...tags]), }, predecessors, siblings, @@ -70,19 +71,8 @@ export function createNode( return makeNode({ kind, spec, predecessors: [], siblings: [] }); } -/** - * Type guard: narrow a public {@link Operation} to an internal node. - * Throws if the value does not have the required internal fields. - */ +/** Type guard: narrow a public {@link Operation} to an internal node. */ export function asNode(operation: Operation): OperationNode { - const candidate = operation as unknown as Partial; - if ( - typeof candidate.kind !== "string" || - typeof candidate.spec !== "object" || - candidate.spec === null - ) { - throw new TypeError("expected an OperationNode, got a plain Operation"); - } return operation as OperationNode; } diff --git a/packages/core/src/internal/plan.ts b/packages/core/src/internal/plan.ts index 131fd0885..4c529b4d9 100644 --- a/packages/core/src/internal/plan.ts +++ b/packages/core/src/internal/plan.ts @@ -45,9 +45,17 @@ export async function planWorkflow( const ordered = topoSort(specs); // 8. Evaluate conditions and feed non-skipped ops to the runtime. - // Independent operations are dispatched concurrently as a batch so - // siblings produced by `parallel()` run simultaneously. - await evaluateOperations(ordered, runtime); + for (const spec of ordered) { + if (spec.condition !== undefined) { + const included = evaluateCondition(spec.condition, runtime.context); + if (!included) { + // Skipped: do not call runtime.evaluate. The operation is still + // recorded in the graph (in `ordered`) with its condition field. + continue; + } + } + await runtime.evaluate(spec); + } // 9. Finalize via the runtime, merge planner metadata. const finalized = await runtime.finalize(); @@ -62,75 +70,6 @@ function nowMs(): number { return Date.now(); } -/** - * Evaluate operations in topo order, respecting conditions and failure policy. - * Operations with no outstanding dependencies are dispatched concurrently as - * a batch, so siblings produced by `parallel()` run simultaneously. - * - * In compile mode, all operations are passed to the runtime so compilers - * can emit them (including conditionally-skipped ones with their condition - * field intact). In execute/plan mode, false conditions skip the operation - * without calling runtime.evaluate(). - */ -async function evaluateOperations( - ordered: readonly OperationSpec[], - runtime: Runtime, -): Promise { - let aborted = false; - const done = new Set(); - const remaining = new Set(ordered.map((o) => o.id)); - const byId = new Map(ordered.map((o) => [o.id, o] as const)); - - while (remaining.size > 0) { - const ready: OperationSpec[] = []; - for (const id of remaining) { - const spec = byId.get(id)!; - const deps = spec.dependsOn ?? []; - if (deps.every((d) => done.has(d) || !byId.has(d))) { - ready.push(spec); - } - } - if (ready.length === 0) { - throw new CompositionError("no ready operations (residual cycle)", {}); - } - - const promises: Promise[] = []; - const promiseSpecs: OperationSpec[] = []; - - for (const spec of ready) { - remaining.delete(spec.id); - if (aborted) { - done.add(spec.id); - continue; - } - if (spec.condition !== undefined) { - const included = evaluateCondition(spec.condition, runtime.context); - if (!included) { - if (runtime.mode === "compile") { - promises.push(runtime.evaluate(spec)); - promiseSpecs.push(spec); - } else { - done.add(spec.id); - } - continue; - } - } - promises.push(runtime.evaluate(spec)); - promiseSpecs.push(spec); - } - - const results = await Promise.all(promises); - for (let i = 0; i < results.length; i++) { - const outcome = results[i]!; - const spec = promiseSpecs[i]!; - done.add(spec.id); - if (outcome.status === "failure" && spec.continueOnError !== true) { - aborted = true; - } - } - } -} - // --------------------------------------------------------------------------- // Marker helpers // --------------------------------------------------------------------------- @@ -173,9 +112,6 @@ function discover(roots: readonly Operation[]): OperationNode[] { /** Expand matrix template nodes into cartesian-product children. */ function expandMatrices(nodes: readonly OperationNode[]): OperationNode[] { - // Map from template node to its expanded children, so downstream nodes - // that depend on the template can be rewritten to depend on all children. - const templateChildren = new Map(); const result: OperationNode[] = []; for (const node of nodes) { if (markerOf(node) !== MATRIX_MARKER) { @@ -200,7 +136,6 @@ function expandMatrices(nodes: readonly OperationNode[]): OperationNode[] { }); } } - const children: OperationNode[] = []; for (const combo of cartesianProduct(dims)) { const env: Record = { ...(node.spec.env ?? {}) }; for (const [k, v] of combo) env[`MATRIX_${k.toUpperCase()}`] = String(v); @@ -209,35 +144,9 @@ function expandMatrices(nodes: readonly OperationNode[]): OperationNode[] { delete (childSpec as unknown as Record).matrix; const child = withSpec(node, { ...childSpec, env }); (child as unknown as Record).__matrixCombo = combo; - children.push(child); result.push(child); } - templateChildren.set(node, children); } - - // Rewrite predecessor references: any node that depended on a matrix - // template now depends on all its expanded children instead. - if (templateChildren.size > 0) { - for (let i = 0; i < result.length; i++) { - const node = result[i]!; - if (node.predecessors.length === 0) continue; - let changed = false; - const newPreds: OperationNode[] = []; - for (const pred of node.predecessors) { - const children = templateChildren.get(pred); - if (children !== undefined) { - newPreds.push(...children); - changed = true; - } else { - newPreds.push(pred); - } - } - if (changed) { - result[i] = makeNodeWith(node.kind, node.spec, newPreds, node.siblings, node._id); - } - } - } - return result; } @@ -273,13 +182,8 @@ function flattenArtifacts(nodes: readonly OperationNode[]): OperationNode[] { const resolvePred = (pred: OperationNode): OperationNode[] => { const m = markerOf(pred); if (m === JOIN_MARKER) { - // Depend on the join's siblings (each resolved recursively) AND the - // join's own predecessors, so that dependencies from before the - // parallel block are preserved. - const resolved: OperationNode[] = []; - for (const sib of pred.siblings) resolved.push(...resolvePred(sib)); - for (const predPred of pred.predecessors) resolved.push(...resolvePred(predPred)); - return resolved; + // Depend on the join's siblings (each resolved recursively). + return pred.siblings.flatMap(resolvePred); } if (m === PIPELINE_EMPTY_MARKER) { return []; @@ -295,7 +199,13 @@ function flattenArtifacts(nodes: readonly OperationNode[]): OperationNode[] { newPreds.length === node.predecessors.length && newPreds.every((p, i) => p === node.predecessors[i]); if (same) return node; - return makeNodeWith(node.kind, node.spec, newPreds, node.siblings, node._id); + return makeNodeWith( + node.kind, + node.spec, + newPreds, + node.siblings, + node._id, + ); }); return rewritten.filter((n) => !isArtifact(n)); @@ -323,14 +233,20 @@ function makeNodeWith( // --------------------------------------------------------------------------- /** Assign deterministic ids; reject duplicate user ids. */ -function assignIds(nodes: readonly OperationNode[]): Map { +function assignIds( + nodes: readonly OperationNode[], +): Map { const idMap = new Map(); const usedIds = new Set(); nodes.forEach((node, index) => { if (!isKnownKind(node.kind)) { - throw new CoreError(`unknown operation kind '${node.kind}'`, "UNKNOWN_KIND", { - kind: node.kind, - }); + throw new CoreError( + `unknown operation kind '${node.kind}'`, + "UNKNOWN_KIND", + { + kind: node.kind, + }, + ); } const combo = (node as unknown as Record).__matrixCombo as | readonly [string, unknown][] @@ -367,32 +283,23 @@ function resolveEdges( idMap: Map, ): Map { const result = new Map(); - const knownOpIds = new Set(idMap.values()); for (const node of nodes) { const id = idMap.get(node)!; const userDeps = node.spec.dependsOn ?? []; - const resolvedUserDeps = userDeps.map((dep) => resolveDep(dep, knownOpIds)); const resolvedDeps = node.predecessors.map((p) => idMap.get(p)); if (resolvedDeps.some((d) => d === undefined)) { - throw new CompositionError("unresolved predecessor reference", { node: id }); + throw new CompositionError("unresolved predecessor reference", { + node: id, + }); } - const dependsOn = [...new Set([...resolvedUserDeps, ...(resolvedDeps as string[])])]; + const dependsOn = [ + ...new Set([...userDeps, ...(resolvedDeps as string[])]), + ]; result.set(node, buildSpec(node, id, dependsOn)); } return result; } -/** - * Resolve a single user-provided dependsOn string. It must be an op- id - * already present in the plan; otherwise it is a dangling reference. - */ -function resolveDep(dep: string, knownOpIds: Set): string { - if (knownOpIds.has(dep)) return dep; - throw new CompositionError(`unresolved dependsOn reference '${dep}'`, { - dependsOn: dep, - }); -} - function buildSpec( node: OperationNode, id: string, @@ -414,9 +321,13 @@ function buildSpec( ...(s.condition !== undefined ? { condition: s.condition } : {}), ...(s.cpuLimit !== undefined ? { cpuLimit: s.cpuLimit } : {}), ...(s.memoryLimit !== undefined ? { memoryLimit: s.memoryLimit } : {}), - ...(s.timeoutSeconds !== undefined ? { timeoutSeconds: s.timeoutSeconds } : {}), + ...(s.timeoutSeconds !== undefined + ? { timeoutSeconds: s.timeoutSeconds } + : {}), ...(s.retries !== undefined ? { retries: s.retries } : {}), - ...(s.continueOnError !== undefined ? { continueOnError: s.continueOnError } : {}), + ...(s.continueOnError !== undefined + ? { continueOnError: s.continueOnError } + : {}), ...(s.cache !== undefined ? { cache: s.cache } : {}), ...(s.artifacts !== undefined ? { artifacts: s.artifacts } : {}), ...(s.network !== undefined ? { network: s.network } : {}), @@ -434,49 +345,27 @@ function detectCycles(specs: Map): void { for (const spec of specs.values()) byId.set(spec.id, spec); const color = new Map(); for (const id of byId.keys()) color.set(id, "white"); - - // Iterative DFS with explicit frame stack to avoid call-stack overflow. - const frames: Array<{ id: string; depIdx: number }> = []; - const path: string[] = []; - - for (const startId of byId.keys()) { - if (color.get(startId) !== "white") continue; - frames.push({ id: startId, depIdx: 0 }); - color.set(startId, "gray"); - path.push(startId); - - while (frames.length > 0) { - const frame = frames[frames.length - 1]!; - const spec = byId.get(frame.id)!; - let nextDep: string | undefined; - while (frame.depIdx < (spec.dependsOn ?? []).length) { - const dep = (spec.dependsOn ?? [])[frame.depIdx]!; - frame.depIdx++; - if (byId.has(dep)) { - nextDep = dep; - break; - } - } - if (nextDep !== undefined) { - const depColor = color.get(nextDep); - if (depColor === "gray") { - const cycleStart = path.indexOf(nextDep); - throw new CompositionError("cycle detected in operation graph", { - cycle: path.slice(cycleStart).concat(nextDep), - }); - } - if (depColor === "white") { - color.set(nextDep, "gray"); - path.push(nextDep); - frames.push({ id: nextDep, depIdx: 0 }); - } - } else { - color.set(frame.id, "black"); - path.pop(); - frames.pop(); - } + const stack: string[] = []; + + const visit = (id: string): void => { + const c = color.get(id); + if (c === "black") return; + if (c === "gray") { + const cycleStart = stack.indexOf(id); + throw new CompositionError("cycle detected in operation graph", { + cycle: stack.slice(cycleStart).concat(id), + }); } - } + color.set(id, "gray"); + stack.push(id); + const spec = byId.get(id)!; + for (const dep of spec.dependsOn ?? []) { + if (byId.has(dep)) visit(dep); + } + stack.pop(); + color.set(id, "black"); + }; + for (const id of byId.keys()) visit(id); } // --------------------------------------------------------------------------- @@ -496,7 +385,9 @@ function topoSort(specs: Map): OperationSpec[] { } } } - const queue = all.filter((s) => (indegree.get(s.id) ?? 0) === 0).map((s) => s.id); + const queue = all + .filter((s) => (indegree.get(s.id) ?? 0) === 0) + .map((s) => s.id); const ordered: OperationSpec[] = []; while (queue.length > 0) { const id = queue.shift()!; diff --git a/packages/findings/package.json b/packages/findings/package.json index eb90d9fb2..a67215d28 100644 --- a/packages/findings/package.json +++ b/packages/findings/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/findings/project.json b/packages/findings/project.json index 994cf9dab..c5cb75c01 100644 --- a/packages/findings/project.json +++ b/packages/findings/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/findings" } }, diff --git a/packages/findings/src/__tests__/baseline.test.ts b/packages/findings/src/__tests__/baseline.test.ts index e6272b87a..88379d845 100644 --- a/packages/findings/src/__tests__/baseline.test.ts +++ b/packages/findings/src/__tests__/baseline.test.ts @@ -82,7 +82,10 @@ describe("updateBaseline", () => { it("adds new fingerprints", () => { const existing = createBaseline([makeFinding({ fingerprint: "fp-a" })]); const updated = updateBaseline( - [makeFinding({ fingerprint: "fp-a" }), makeFinding({ fingerprint: "fp-b" })], + [ + makeFinding({ fingerprint: "fp-a" }), + makeFinding({ fingerprint: "fp-b" }), + ], existing, ); expect(updated.fingerprints).toContain("fp-b"); @@ -117,7 +120,10 @@ describe("updateBaseline", () => { it("preserves createdAt and refreshes updatedAt", () => { const existing = createBaseline([makeFinding({ fingerprint: "fp-a" })]); const originalCreatedAt = existing.createdAt; - const updated = updateBaseline([makeFinding({ fingerprint: "fp-a" })], existing); + const updated = updateBaseline( + [makeFinding({ fingerprint: "fp-a" })], + existing, + ); expect(updated.createdAt).toBe(originalCreatedAt); // updatedAt may or may not differ, but should be valid ISO 8601. expect(() => new Date(updated.updatedAt).toISOString()).not.toThrow(); @@ -128,7 +134,10 @@ describe("compareBaseline", () => { it("returns newFindings not in baseline", () => { const baseline = createBaseline([makeFinding({ fingerprint: "fp-a" })]); const diff = compareBaseline( - [makeFinding({ fingerprint: "fp-a" }), makeFinding({ fingerprint: "fp-b" })], + [ + makeFinding({ fingerprint: "fp-a" }), + makeFinding({ fingerprint: "fp-b" }), + ], baseline, ); expect(diff.newFindings).toHaveLength(1); @@ -212,7 +221,9 @@ describe("Baseline I/O", () => { }); it("loadBaseline throws BASELINE_NOT_FOUND for missing file", async () => { - await expect(loadBaseline(`${dir}/nope.json`)).rejects.toThrow(BaselineError); + await expect(loadBaseline(`${dir}/nope.json`)).rejects.toThrow( + BaselineError, + ); try { await loadBaseline(`${dir}/nope.json`); } catch (e) { @@ -222,7 +233,9 @@ describe("Baseline I/O", () => { it("loadBaseline throws BASELINE_INVALID for invalid JSON", async () => { await writeTempFile(dir, "bad.json", "{not valid json"); - await expect(loadBaseline(`${dir}/bad.json`)).rejects.toThrow(BaselineError); + await expect(loadBaseline(`${dir}/bad.json`)).rejects.toThrow( + BaselineError, + ); try { await loadBaseline(`${dir}/bad.json`); } catch (e) { @@ -236,7 +249,9 @@ describe("Baseline I/O", () => { "wrong.json", JSON.stringify({ version: 99, fingerprints: [], suppressions: [] }), ); - await expect(loadBaseline(`${dir}/wrong.json`)).rejects.toThrow(BaselineError); + await expect(loadBaseline(`${dir}/wrong.json`)).rejects.toThrow( + BaselineError, + ); try { await loadBaseline(`${dir}/wrong.json`); } catch (e) { @@ -250,7 +265,9 @@ describe("Baseline I/O", () => { "badfp.json", JSON.stringify({ version: 1, fingerprints: "oops", suppressions: [] }), ); - await expect(loadBaseline(`${dir}/badfp.json`)).rejects.toThrow(BaselineError); + await expect(loadBaseline(`${dir}/badfp.json`)).rejects.toThrow( + BaselineError, + ); }); it("saveBaseline throws BASELINE_WRITE_FAILED for unwritable path", async () => { diff --git a/packages/findings/src/__tests__/fingerprint.test.ts b/packages/findings/src/__tests__/fingerprint.test.ts index 0189787b5..480a62d58 100644 --- a/packages/findings/src/__tests__/fingerprint.test.ts +++ b/packages/findings/src/__tests__/fingerprint.test.ts @@ -4,7 +4,9 @@ import { computeFingerprint } from "../fingerprint.js"; import { NormalizationError } from "../errors.js"; import type { FingerprintInput } from "../types.js"; -function baseInput(overrides: Partial = {}): FingerprintInput { +function baseInput( + overrides: Partial = {}, +): FingerprintInput { return { rule: "no-console", file: "src/index.ts", diff --git a/packages/findings/src/__tests__/helpers/fixtures.ts b/packages/findings/src/__tests__/helpers/fixtures.ts index 7cb6fd3b8..39eac46d3 100644 --- a/packages/findings/src/__tests__/helpers/fixtures.ts +++ b/packages/findings/src/__tests__/helpers/fixtures.ts @@ -86,7 +86,9 @@ export function defaultContext( * Create a temp directory and return its path. Caller is responsible for * cleanup via `cleanupTempDir`. */ -export async function makeTempDir(prefix = "sverka-findings-"): Promise { +export async function makeTempDir( + prefix = "sverka-findings-", +): Promise { return mkdtemp(join(tmpdir(), prefix)); } diff --git a/packages/findings/src/__tests__/normalize.test.ts b/packages/findings/src/__tests__/normalize.test.ts index c84240327..3ec28c925 100644 --- a/packages/findings/src/__tests__/normalize.test.ts +++ b/packages/findings/src/__tests__/normalize.test.ts @@ -116,7 +116,12 @@ describe("normalizeSarif — level to severity mapping", () => { delete (result as Partial).level; const findings = normalizeSarif( makeSarifLog({ - runs: [makeRun({ tool: { driver: { name: "eslint", rules: [rule] } }, results: [result] })], + runs: [ + makeRun({ + tool: { driver: { name: "eslint", rules: [rule] } }, + results: [result], + }), + ], }), defaultContext(), ); @@ -129,7 +134,12 @@ describe("normalizeSarif — rule resolution", () => { const rule = makeRule({ helpUri: "https://example.com/no-console" }); const findings = normalizeSarif( makeSarifLog({ - runs: [makeRun({ tool: { driver: { name: "eslint", rules: [rule] } }, results: [makeResult()] })], + runs: [ + makeRun({ + tool: { driver: { name: "eslint", rules: [rule] } }, + results: [makeResult()], + }), + ], }), defaultContext(), ); @@ -140,10 +150,18 @@ describe("normalizeSarif — rule resolution", () => { const result = makeResult(); delete (result as Partial).ruleId; result.ruleIndex = 0; - const rule = makeRule({ id: "indexed-rule", helpUri: "https://example.com/indexed" }); + const rule = makeRule({ + id: "indexed-rule", + helpUri: "https://example.com/indexed", + }); const findings = normalizeSarif( makeSarifLog({ - runs: [makeRun({ tool: { driver: { name: "eslint", rules: [rule] } }, results: [result] })], + runs: [ + makeRun({ + tool: { driver: { name: "eslint", rules: [rule] } }, + results: [result], + }), + ], }), defaultContext(), ); @@ -156,7 +174,9 @@ describe("normalizeSarif — rule resolution", () => { delete (result as Partial).ruleId; const findings = normalizeSarif( makeSarifLog({ - runs: [makeRun({ tool: { driver: { name: "eslint" } }, results: [result] })], + runs: [ + makeRun({ tool: { driver: { name: "eslint" } }, results: [result] }), + ], }), defaultContext(), ); @@ -199,7 +219,11 @@ describe("normalizeSarif — multi-location", () => { { physicalLocation: { artifactLocation: { uri: "src/index.ts" }, - region: { startLine: 10, endLine: 10, snippet: { text: "console.log(1)" } }, + region: { + startLine: 10, + endLine: 10, + snippet: { text: "console.log(1)" }, + }, }, }, ], @@ -217,7 +241,12 @@ describe("normalizeSarif — multi-location", () => { { physicalLocation: { artifactLocation: { uri: "src/index.ts" }, - region: { startLine: 10, endLine: 10, startColumn: 3, endColumn: 14 }, + region: { + startLine: 10, + endLine: 10, + startColumn: 3, + endColumn: 14, + }, }, }, ], @@ -233,9 +262,9 @@ describe("normalizeSarif — multi-location", () => { describe("normalizeSarif — empty inputs", () => { it("returns [] for empty runs array", () => { - expect(normalizeSarif(makeSarifLog({ runs: [] }), defaultContext())).toEqual( - [], - ); + expect( + normalizeSarif(makeSarifLog({ runs: [] }), defaultContext()), + ).toEqual([]); }); it("returns [] for empty results array", () => { diff --git a/packages/findings/src/__tests__/public-api.test.ts b/packages/findings/src/__tests__/public-api.test.ts index 397201917..aa7ed8cf8 100644 --- a/packages/findings/src/__tests__/public-api.test.ts +++ b/packages/findings/src/__tests__/public-api.test.ts @@ -74,7 +74,11 @@ describe("public API — types (compile-time check)", () => { const _finding: Finding = {} as Finding; const _severity: Severity = "info"; const _source: FindingSource = {} as FindingSource; - const _ctx: NormalizeContext = { root: "/", checkIdPrefix: "", defaultConfidence: 0.5 }; + const _ctx: NormalizeContext = { + root: "/", + checkIdPrefix: "", + defaultConfidence: 0.5, + }; const _fpInput: FingerprintInput = { rule: "r", file: "f", diff --git a/packages/findings/src/__tests__/suppress.test.ts b/packages/findings/src/__tests__/suppress.test.ts index 2f92f983a..866bcfb6c 100644 --- a/packages/findings/src/__tests__/suppress.test.ts +++ b/packages/findings/src/__tests__/suppress.test.ts @@ -1,9 +1,5 @@ import { describe, it, expect } from "vitest"; -import { - isSuppressed, - filterSuppressed, - filterOnlyNew, -} from "../suppress.js"; +import { isSuppressed, filterSuppressed, filterOnlyNew } from "../suppress.js"; import { createBaseline, type Baseline } from "../baseline.js"; import type { Finding } from "../types.js"; @@ -40,79 +36,99 @@ function baselineWith( describe("isSuppressed", () => { it("returns true for matching non-expired suppression", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "false positive", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "false positive", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); expect(isSuppressed(makeFinding("fp-a"), baseline)).toBe(true); }); it("returns false when no suppression matches", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-b", - reason: "other", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-b", + reason: "other", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); expect(isSuppressed(makeFinding("fp-a"), baseline)).toBe(false); }); it("returns false for expired suppression (expiresAt in the past)", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "expired", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - expiresAt: "2020-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "expired", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + expiresAt: "2020-01-01T00:00:00Z", + }, + ], + ); expect(isSuppressed(makeFinding("fp-a"), baseline)).toBe(false); }); it("returns true for suppression with no expiresAt", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "permanent", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "permanent", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); expect(isSuppressed(makeFinding("fp-a"), baseline)).toBe(true); }); it("returns true for suppression with future expiresAt", () => { - const future = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(); - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "temporary", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - expiresAt: future, - }, - ]); + const future = new Date( + Date.now() + 365 * 24 * 60 * 60 * 1000, + ).toISOString(); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "temporary", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + expiresAt: future, + }, + ], + ); expect(isSuppressed(makeFinding("fp-a"), baseline)).toBe(true); }); }); describe("filterSuppressed", () => { it("excludes suppressed findings when includeSuppressed is false", () => { - const baseline = baselineWith(["fp-a", "fp-b"], [ - { - fingerprint: "fp-a", - reason: "fp", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a", "fp-b"], + [ + { + fingerprint: "fp-a", + reason: "fp", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); const findings = [makeFinding("fp-a"), makeFinding("fp-b")]; const filtered = filterSuppressed(findings, baseline, false); expect(filtered).toHaveLength(1); @@ -120,29 +136,35 @@ describe("filterSuppressed", () => { }); it("includes all findings when includeSuppressed is true", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "fp", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "fp", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); const findings = [makeFinding("fp-a"), makeFinding("fp-b")]; const filtered = filterSuppressed(findings, baseline, true); expect(filtered).toHaveLength(2); }); it("excludes expired suppressions", () => { - const baseline = baselineWith(["fp-a"], [ - { - fingerprint: "fp-a", - reason: "expired", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - expiresAt: "2020-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + ["fp-a"], + [ + { + fingerprint: "fp-a", + reason: "expired", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + expiresAt: "2020-01-01T00:00:00Z", + }, + ], + ); const findings = [makeFinding("fp-a")]; const filtered = filterSuppressed(findings, baseline, false); expect(filtered).toHaveLength(1); @@ -159,14 +181,17 @@ describe("filterOnlyNew", () => { }); it("excludes suppressed findings even if not in baseline fingerprints", () => { - const baseline = baselineWith([], [ - { - fingerprint: "fp-c", - reason: "fp", - author: "jane", - createdAt: "2025-01-01T00:00:00Z", - }, - ]); + const baseline = baselineWith( + [], + [ + { + fingerprint: "fp-c", + reason: "fp", + author: "jane", + createdAt: "2025-01-01T00:00:00Z", + }, + ], + ); const findings = [makeFinding("fp-b"), makeFinding("fp-c")]; const result = filterOnlyNew(findings, baseline); expect(result).toHaveLength(1); diff --git a/packages/findings/src/baseline.ts b/packages/findings/src/baseline.ts index 8ca2f95b9..a15e9107c 100644 --- a/packages/findings/src/baseline.ts +++ b/packages/findings/src/baseline.ts @@ -130,16 +130,28 @@ export async function loadBaseline(path: string): Promise { } catch (e) { const err = e as NodeJS.ErrnoException; if (err.code === "ENOENT") { - throw new BaselineError(`baseline file not found: ${path}`, "BASELINE_NOT_FOUND", e); + throw new BaselineError( + `baseline file not found: ${path}`, + "BASELINE_NOT_FOUND", + e, + ); } - throw new BaselineError(`cannot read baseline file: ${path}`, "BASELINE_INVALID", e); + throw new BaselineError( + `cannot read baseline file: ${path}`, + "BASELINE_INVALID", + e, + ); } let parsed: unknown; try { parsed = JSON.parse(content); } catch (e) { - throw new BaselineError("baseline file is not valid JSON", "BASELINE_INVALID", e); + throw new BaselineError( + "baseline file is not valid JSON", + "BASELINE_INVALID", + e, + ); } const obj = parsed as Record; diff --git a/packages/findings/src/errors.ts b/packages/findings/src/errors.ts index 412fbe423..891d5c5c9 100644 --- a/packages/findings/src/errors.ts +++ b/packages/findings/src/errors.ts @@ -1,3 +1,8 @@ +export type NormalizationErrorCode = + | "INVALID_SARIF" + | "MISSING_LOCATION" + | "INVALID_FINGERPRINT_INPUT"; + /** * Base error class for normalization failures. All normalization errors throw * a `NormalizationError` with one of the `NormalizationErrorCode` values. @@ -5,11 +10,7 @@ export class NormalizationError extends Error { readonly code: NormalizationErrorCode; override readonly cause: unknown; - constructor( - message: string, - code: NormalizationErrorCode, - cause?: unknown, - ) { + constructor(message: string, code: NormalizationErrorCode, cause?: unknown) { super(message); this.name = "NormalizationError"; this.code = code; @@ -17,10 +18,10 @@ export class NormalizationError extends Error { } } -export type NormalizationErrorCode = - | "INVALID_SARIF" - | "MISSING_LOCATION" - | "INVALID_FINGERPRINT_INPUT"; +export type BaselineErrorCode = + | "BASELINE_NOT_FOUND" + | "BASELINE_INVALID" + | "BASELINE_WRITE_FAILED"; /** * Base error class for baseline operation failures. All baseline I/O and @@ -37,8 +38,3 @@ export class BaselineError extends Error { this.cause = cause; } } - -export type BaselineErrorCode = - | "BASELINE_NOT_FOUND" - | "BASELINE_INVALID" - | "BASELINE_WRITE_FAILED"; diff --git a/packages/findings/src/index.ts b/packages/findings/src/index.ts index 6600c1bb3..54b9d3aff 100644 --- a/packages/findings/src/index.ts +++ b/packages/findings/src/index.ts @@ -1,13 +1,36 @@ // @sverka/findings — public API -export { type Finding, type Severity, type FindingSource, - type NormalizeContext, type FingerprintInput } from "./types.js"; -export { type Baseline, type Suppression, type BaselineDiff } from "./baseline.js"; -export { type SarifLog, type SarifRun, type SarifRule, - type SarifResult, type SarifLocation } from "./normalize.js"; +export { + type Finding, + type Severity, + type FindingSource, + type NormalizeContext, + type FingerprintInput, +} from "./types.js"; +export { + type Baseline, + type Suppression, + type BaselineDiff, +} from "./baseline.js"; +export { + type SarifLog, + type SarifRun, + type SarifRule, + type SarifResult, + type SarifLocation, +} from "./normalize.js"; export { normalizeSarif } from "./normalize.js"; export { computeFingerprint } from "./fingerprint.js"; -export { createBaseline, updateBaseline, compareBaseline, - loadBaseline, saveBaseline } from "./baseline.js"; +export { + createBaseline, + updateBaseline, + compareBaseline, + loadBaseline, + saveBaseline, +} from "./baseline.js"; export { isSuppressed, filterSuppressed, filterOnlyNew } from "./suppress.js"; -export { NormalizationError, type NormalizationErrorCode, - BaselineError, type BaselineErrorCode } from "./errors.js"; +export { + NormalizationError, + type NormalizationErrorCode, + BaselineError, + type BaselineErrorCode, +} from "./errors.js"; diff --git a/packages/ir/package.json b/packages/ir/package.json index 96bf93f32..1303c527f 100644 --- a/packages/ir/package.json +++ b/packages/ir/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/ir/project.json b/packages/ir/project.json index bb423206d..c0ae2e560 100644 --- a/packages/ir/project.json +++ b/packages/ir/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/ir" } }, diff --git a/packages/ir/src/__tests__/helpers/fixtures.ts b/packages/ir/src/__tests__/helpers/fixtures.ts index 36898413a..dd13fa142 100644 --- a/packages/ir/src/__tests__/helpers/fixtures.ts +++ b/packages/ir/src/__tests__/helpers/fixtures.ts @@ -60,7 +60,9 @@ export function validPlanBody( } /** A complete, valid Plan with the deterministic id and a fixed createdAt. */ -export function validPlan(overrides: Partial> = {}): Plan { +export function validPlan( + overrides: Partial> = {}, +): Plan { const body = validPlanBody(overrides); const id = computePlanId(body); return { ...body, id, createdAt: "2026-01-01T00:00:00.000Z" }; diff --git a/packages/ir/src/__tests__/ids.test.ts b/packages/ir/src/__tests__/ids.test.ts index 51ff1bc17..78d8ae3c8 100644 --- a/packages/ir/src/__tests__/ids.test.ts +++ b/packages/ir/src/__tests__/ids.test.ts @@ -21,7 +21,11 @@ function planBody(overrides: Partial> = {}) { network: "deny" as const, credentials: [], artifacts: [], - retry: { maxAttempts: 1, backoffSeconds: 0, retryOn: ["failure"] as readonly ("failure" | "timeout")[] }, + retry: { + maxAttempts: 1, + backoffSeconds: 0, + retryOn: ["failure"] as readonly ("failure" | "timeout")[], + }, timeoutSeconds: 60, continueOnError: false, }, @@ -65,9 +69,7 @@ describe("computePlanId", () => { it("changes when an operation changes", () => { const a = planBody(); const b = planBody({ - operations: [ - { ...a.operations[0]!, name: "test" }, - ], + operations: [{ ...a.operations[0]!, name: "test" }], }); expect(computePlanId(a)).not.toBe(computePlanId(b)); }); @@ -104,7 +106,9 @@ describe("computePlanId", () => { operations: body.operations, metadata: body.metadata, }); - const expectedHex = createHash("sha256").update(canonical, "utf8").digest("hex"); + const expectedHex = createHash("sha256") + .update(canonical, "utf8") + .digest("hex"); expect(id).toBe(`plan-${expectedHex}`); }); }); diff --git a/packages/ir/src/__tests__/validate.test.ts b/packages/ir/src/__tests__/validate.test.ts index f8228d714..3f88abc92 100644 --- a/packages/ir/src/__tests__/validate.test.ts +++ b/packages/ir/src/__tests__/validate.test.ts @@ -45,9 +45,9 @@ describe("validatePlan — rule 1 (apiVersion)", () => { const { apiVersion: _omit, ...rest } = validPlan(); const result = validatePlan(rest); expect(result.valid).toBe(false); - expect( - result.errors.some((e) => e.code === "INVALID_API_VERSION"), - ).toBe(true); + expect(result.errors.some((e) => e.code === "INVALID_API_VERSION")).toBe( + true, + ); }); }); @@ -65,7 +65,11 @@ describe("validatePlan — rule 2 (id matches recomputed)", () => { const plan = { ...validPlan(), id: "" }; const result = validatePlan(plan); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.code === "INVALID_ID" || e.code === "ID_MISMATCH")).toBe(true); + expect( + result.errors.some( + (e) => e.code === "INVALID_ID" || e.code === "ID_MISMATCH", + ), + ).toBe(true); }); }); @@ -83,9 +87,7 @@ describe("validatePlan — rule 3 (non-empty operations)", () => { describe("validatePlan — rule 4 (dependsOn references exist)", () => { it("rejects a dependsOn referencing an unknown id", () => { const plan = validPlan({ - operations: [ - validOperation({ id: "op-a", dependsOn: ["op-missing"] }), - ], + operations: [validOperation({ id: "op-a", dependsOn: ["op-missing"] })], }); const result = validatePlan(plan); expect(result.valid).toBe(false); @@ -159,9 +161,9 @@ describe("validatePlan — rule 7 (imageDigest for containers)", () => { const plan = validPlan({ operations: [op] }); const result = validatePlan(plan); expect(result.valid).toBe(false); - expect( - result.errors.some((e) => e.code === "MISSING_IMAGE_DIGEST"), - ).toBe(true); + expect(result.errors.some((e) => e.code === "MISSING_IMAGE_DIGEST")).toBe( + true, + ); }); it("accepts a host executor without imageDigest", () => { @@ -187,9 +189,9 @@ describe("validatePlan — rule 8 (timeoutSeconds > 0)", () => { operations: [validOperation({ timeoutSeconds: -5 })], }); expect(validatePlan(plan).valid).toBe(false); - expect( - result_of(plan).some((e) => e.code === "INVALID_TIMEOUT"), - ).toBe(true); + expect(result_of(plan).some((e) => e.code === "INVALID_TIMEOUT")).toBe( + true, + ); }); }); @@ -212,7 +214,9 @@ describe("validatePlan — rule 9 (resources parseable)", () => { it("rejects non-numeric cpu", () => { const plan = validPlan({ - operations: [validOperation({ resources: { cpu: "fast", memory: "512Mi" } })], + operations: [ + validOperation({ resources: { cpu: "fast", memory: "512Mi" } }), + ], }); expect(validatePlan(plan).valid).toBe(false); expect( @@ -244,7 +248,9 @@ describe("validatePlan — rule 10 (retry policy)", () => { it("rejects maxAttempts < 1 with INVALID_RETRY_POLICY", () => { const plan = validPlan({ operations: [ - validOperation({ retry: { maxAttempts: 0, backoffSeconds: 0, retryOn: ["failure"] } }), + validOperation({ + retry: { maxAttempts: 0, backoffSeconds: 0, retryOn: ["failure"] }, + }), ], }); const result = validatePlan(plan); @@ -257,7 +263,9 @@ describe("validatePlan — rule 10 (retry policy)", () => { it("rejects negative backoffSeconds", () => { const plan = validPlan({ operations: [ - validOperation({ retry: { maxAttempts: 1, backoffSeconds: -1, retryOn: ["failure"] } }), + validOperation({ + retry: { maxAttempts: 1, backoffSeconds: -1, retryOn: ["failure"] }, + }), ], }); expect(validatePlan(plan).valid).toBe(false); @@ -270,7 +278,14 @@ describe("validatePlan — rule 10 (retry policy)", () => { const plan = validPlan({ operations: [ validOperation({ - retry: { maxAttempts: 1, backoffSeconds: 0, retryOn: ["failure", "unknown"] as readonly ("failure" | "timeout")[] }, + retry: { + maxAttempts: 1, + backoffSeconds: 0, + retryOn: ["failure", "unknown"] as readonly ( + | "failure" + | "timeout" + )[], + }, }), ], }); diff --git a/packages/ir/src/validate.ts b/packages/ir/src/validate.ts index 80b67e575..900a622a2 100644 --- a/packages/ir/src/validate.ts +++ b/packages/ir/src/validate.ts @@ -57,8 +57,7 @@ export function validatePlan(plan: unknown): ValidationResult { } // Rule 2a: id must be a non-empty string. - const idIsNonEmptyString = - typeof p.id === "string" && p.id.length > 0; + const idIsNonEmptyString = typeof p.id === "string" && p.id.length > 0; if (!idIsNonEmptyString) { errors.push({ field: "id", @@ -162,7 +161,11 @@ export function validatePlan(plan: unknown): ValidationResult { } const reportedDup = new Set(); for (const op of ops) { - if (typeof op.id === "string" && (idCounts.get(op.id) ?? 0) > 1 && !reportedDup.has(op.id)) { + if ( + typeof op.id === "string" && + (idCounts.get(op.id) ?? 0) > 1 && + !reportedDup.has(op.id) + ) { errors.push({ operationId: op.id, field: "operations[].id", @@ -204,7 +207,10 @@ export function validatePlan(plan: unknown): ValidationResult { if (isPlainObject(op.executor)) { const ex = op.executor as { type?: unknown; imageDigest?: unknown }; if (ex.type === "docker" || ex.type === "podman") { - if (typeof ex.imageDigest !== "string" || !IMAGE_DIGEST_RE.test(ex.imageDigest)) { + if ( + typeof ex.imageDigest !== "string" || + !IMAGE_DIGEST_RE.test(ex.imageDigest) + ) { errors.push({ ...(opId !== undefined ? { operationId: opId } : {}), field: "operations[].executor.imageDigest", @@ -216,7 +222,11 @@ export function validatePlan(plan: unknown): ValidationResult { } // Rule 8: timeoutSeconds > 0. - if (typeof op.timeoutSeconds !== "number" || !Number.isFinite(op.timeoutSeconds) || op.timeoutSeconds <= 0) { + if ( + typeof op.timeoutSeconds !== "number" || + !Number.isFinite(op.timeoutSeconds) || + op.timeoutSeconds <= 0 + ) { errors.push({ ...(opId !== undefined ? { operationId: opId } : {}), field: "operations[].timeoutSeconds", @@ -235,7 +245,8 @@ export function validatePlan(plan: unknown): ValidationResult { ...(opId !== undefined ? { operationId: opId } : {}), field: "operations[].resources", code: "INVALID_RESOURCES", - message: "resources.cpu must be a number string and resources.memory must match /^[0-9]+(Ki|Mi|Gi|Ti)?$/", + message: + "resources.cpu must be a number string and resources.memory must match /^[0-9]+(Ki|Mi|Gi|Ti)?$/", }); } } else { @@ -302,7 +313,8 @@ export function validatePlan(plan: unknown): ValidationResult { ...(opId !== undefined ? { operationId: opId } : {}), field: "operations[].cache.key", code: "MISSING_CACHE_KEY", - message: "cache.key must be a non-empty string when cache is declared", + message: + "cache.key must be a non-empty string when cache is declared", }); } } else { diff --git a/packages/planner/package.json b/packages/planner/package.json index 3075bcaed..d6f47d2f9 100644 --- a/packages/planner/package.json +++ b/packages/planner/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/planner/project.json b/packages/planner/project.json index e003e811e..c952a4e0a 100644 --- a/packages/planner/project.json +++ b/packages/planner/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/planner" } }, diff --git a/packages/planner/src/__tests__/discover.test.ts b/packages/planner/src/__tests__/discover.test.ts index 45d69810d..0446d6e01 100644 --- a/packages/planner/src/__tests__/discover.test.ts +++ b/packages/planner/src/__tests__/discover.test.ts @@ -33,7 +33,7 @@ describe("discover — local signal detection", () => { const files: Record = { "package.json": "{}", "bun.lock": "{}", - "Dockerfile": "FROM node:24", + Dockerfile: "FROM node:24", "docker-compose.yml": "services: {}", ".github/workflows/ci.yml": "on: [push]", "nx.json": "{}", @@ -88,7 +88,9 @@ describe("discover — local signal detection", () => { installMockGit({ root, trackedFiles: Object.keys(files) }); try { const ctx = await createPlanner().discover({ root }); - expect(ctx.localSignals.some((s) => s.type === "docker-compose")).toBe(true); + expect(ctx.localSignals.some((s) => s.type === "docker-compose")).toBe( + true, + ); } finally { await cleanup(root); } @@ -97,7 +99,7 @@ describe("discover — local signal detection", () => { it("detects .gitlab-ci.yml, Jenkinsfile, azure-pipelines.yml, .circleci/", async () => { const files: Record = { ".gitlab-ci.yml": "stages: []", - "Jenkinsfile": "pipeline {}", + Jenkinsfile: "pipeline {}", "azure-pipelines.yml": "steps: []", ".circleci/config.yml": "version: 2", }; @@ -105,7 +107,9 @@ describe("discover — local signal detection", () => { installMockGit({ root, trackedFiles: Object.keys(files) }); try { const ctx = await createPlanner().discover({ root }); - const ciSignals = ctx.localSignals.filter((s) => s.type === "ci-definition"); + const ciSignals = ctx.localSignals.filter( + (s) => s.type === "ci-definition", + ); expect(ciSignals.length).toBe(4); expect(ctx.hasCiDefinition).toBe(true); } finally { @@ -184,7 +188,15 @@ describe("discover — package manager detection", () => { try { const ctx = await createPlanner().discover({ root }); const names = ctx.packageManagers.map((p) => p.name).sort(); - expect(names).toEqual(["bun", "cargo", "go", "npm", "pnpm", "poetry", "yarn"]); + expect(names).toEqual([ + "bun", + "cargo", + "go", + "npm", + "pnpm", + "poetry", + "yarn", + ]); } finally { await cleanup(root); } @@ -246,7 +258,10 @@ describe("discover — package manager detection", () => { describe("discover — monorepo detection", () => { it("nx.json → nx", async () => { - const root = await makeFixtureDir({ "nx.json": "{}", "package.json": "{}" }); + const root = await makeFixtureDir({ + "nx.json": "{}", + "package.json": "{}", + }); installMockGit({ root, trackedFiles: ["nx.json", "package.json"] }); try { const ctx = await createPlanner().discover({ root }); @@ -259,7 +274,9 @@ describe("discover — monorepo detection", () => { }); it("pnpm-workspace.yaml → pnpm-workspace", async () => { - const root = await makeFixtureDir({ "pnpm-workspace.yaml": "packages: []" }); + const root = await makeFixtureDir({ + "pnpm-workspace.yaml": "packages: []", + }); installMockGit({ root, trackedFiles: ["pnpm-workspace.yaml"] }); try { const ctx = await createPlanner().discover({ root }); @@ -327,8 +344,14 @@ describe("discover — git metadata", () => { expect(ctx.commit).toBe("abcdef1234567890abcdef1234567890abcdef12"); expect(ctx.dirty).toBe(false); expect(ctx.changedFiles.length).toBe(2); - expect(ctx.changedFiles[0]).toEqual({ path: "src/index.ts", status: "modified" }); - expect(ctx.changedFiles[1]).toEqual({ path: "src/new.ts", status: "added" }); + expect(ctx.changedFiles[0]).toEqual({ + path: "src/index.ts", + status: "modified", + }); + expect(ctx.changedFiles[1]).toEqual({ + path: "src/new.ts", + status: "added", + }); } finally { await cleanup(root); } @@ -370,7 +393,7 @@ describe("discover — explainability", () => { const files: Record = { "package.json": "{}", "bun.lock": "{}", - "Dockerfile": "FROM node:24", + Dockerfile: "FROM node:24", "nx.json": "{}", ".github/workflows/ci.yml": "on: [push]", "src/index.ts": "export {}", @@ -427,13 +450,19 @@ describe("discover — error cases", () => { it("ROOT_NOT_FOUND when root does not exist", async () => { installMockGit({ root: "/nonexistent" }); await expect( - createPlanner().discover({ root: "/nonexistent/path/that/does/not/exist" }), + createPlanner().discover({ + root: "/nonexistent/path/that/does/not/exist", + }), ).rejects.toMatchObject({ name: "DiscoveryError", code: "ROOT_NOT_FOUND" }); }); it("GIT_UNAVAILABLE when git --version throws ENOENT", async () => { const root = await makeFixtureDir({ "src/index.ts": "export {}" }); - installMockGit({ root, trackedFiles: ["src/index.ts"], gitUnavailable: true }); + installMockGit({ + root, + trackedFiles: ["src/index.ts"], + gitUnavailable: true, + }); try { await expect(createPlanner().discover({ root })).rejects.toMatchObject({ name: "DiscoveryError", @@ -464,18 +493,25 @@ describe("discover — error cases", () => { run(args) { const j = args.join(" "); if (j === "--version") return Promise.resolve("git version 2.43.0\n"); - if (j === "rev-parse --show-toplevel") return Promise.resolve(`${root}\n`); + if (j === "rev-parse --show-toplevel") + return Promise.resolve(`${root}\n`); if (j === "ls-files") return Promise.resolve("src/index.ts\n"); if (j === "status --porcelain") return Promise.resolve(""); if (j === "rev-parse HEAD") return Promise.resolve("abc\n"); - if (args[0] === "diff") return Promise.reject(new Error("bad ref", { cause: "fatal: bad revision" })); + if (args[0] === "diff") + return Promise.reject( + new Error("bad ref", { cause: "fatal: bad revision" }), + ); return Promise.reject(new Error(`unhandled: ${j}`)); }, }); try { await expect( createPlanner().discover({ root, baseRef: "nonexistent-ref" }), - ).rejects.toMatchObject({ name: "DiscoveryError", code: "TRAVERSAL_FAILED" }); + ).rejects.toMatchObject({ + name: "DiscoveryError", + code: "TRAVERSAL_FAILED", + }); } finally { await cleanup(root); } diff --git a/packages/planner/src/__tests__/helpers/fixtures.ts b/packages/planner/src/__tests__/helpers/fixtures.ts index 726017dcd..4d779f71e 100644 --- a/packages/planner/src/__tests__/helpers/fixtures.ts +++ b/packages/planner/src/__tests__/helpers/fixtures.ts @@ -42,11 +42,17 @@ export function makeMockGit(cfg: MockGitConfig): GitCli { const joined = args.join(" "); return new Promise((resolvePromise, reject) => { if (cfg.gitUnavailable && joined.includes("--version")) { - reject(Object.assign(new Error("spawn git ENOENT"), { code: "ENOENT" })); + reject( + Object.assign(new Error("spawn git ENOENT"), { code: "ENOENT" }), + ); return; } if (cfg.notARepo && joined.includes("--show-toplevel")) { - reject(new Error("not a git repository", { cause: "fatal: not a git repository" })); + reject( + new Error("not a git repository", { + cause: "fatal: not a git repository", + }), + ); return; } if (joined === "--version") { @@ -62,7 +68,9 @@ export function makeMockGit(cfg: MockGitConfig): GitCli { return; } if (joined === "ls-files") { - resolvePromise(tracked.map((f) => f).join("\n") + (tracked.length ? "\n" : "")); + resolvePromise( + tracked.map((f) => f).join("\n") + (tracked.length ? "\n" : ""), + ); return; } if (joined === "status --porcelain") { diff --git a/packages/planner/src/__tests__/plan.test.ts b/packages/planner/src/__tests__/plan.test.ts index 14f185f22..4c2c25800 100644 --- a/packages/planner/src/__tests__/plan.test.ts +++ b/packages/planner/src/__tests__/plan.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect } from "vitest"; import { createPlanner } from "../planner.js"; -import type { ProjectContext, DetectedLanguage, DetectedPackageManager, LocalSignal } from "../planner.js"; +import type { + ProjectContext, + DetectedLanguage, + DetectedPackageManager, + LocalSignal, +} from "../planner.js"; function makeContext(opts: { languages?: DetectedLanguage[]; @@ -18,20 +23,47 @@ function makeContext(opts: { hasCiDefinition: false, monorepo: null, localSignals: opts.signals ?? [], - explanation: { summary: "test", signalCounts: { - manifest: 0, lockfile: 0, dockerfile: 0, "docker-compose": 0, - "ci-definition": 0, "monorepo-marker": 0, "git-metadata": 0, - } }, + explanation: { + summary: "test", + signalCounts: { + manifest: 0, + lockfile: 0, + dockerfile: 0, + "docker-compose": 0, + "ci-definition": 0, + "monorepo-marker": 0, + "git-metadata": 0, + }, + }, }; } describe("plan — synthesis", () => { it("Node project proposes typecheck/lint/test", async () => { const ctx = makeContext({ - languages: [{ name: "TypeScript", confidence: 1.0, evidence: [".ts"], fileCount: 15 }], - packageManagers: [{ name: "bun", version: "1.3.14", lockfile: "bun.lock", evidence: ["bun.lock"] }], + languages: [ + { + name: "TypeScript", + confidence: 1.0, + evidence: [".ts"], + fileCount: 15, + }, + ], + packageManagers: [ + { + name: "bun", + version: "1.3.14", + lockfile: "bun.lock", + evidence: ["bun.lock"], + }, + ], signals: [ - { type: "manifest", path: "package.json", detail: null, confidence: 1.0 }, + { + type: "manifest", + path: "package.json", + detail: null, + confidence: 1.0, + }, { type: "lockfile", path: "bun.lock", detail: null, confidence: 1.0 }, ], }); @@ -43,11 +75,30 @@ describe("plan — synthesis", () => { it("Python project proposes lint/test", async () => { const ctx = makeContext({ - languages: [{ name: "Python", confidence: 0.5, evidence: [".py"], fileCount: 5 }], - packageManagers: [{ name: "poetry", version: null, lockfile: "poetry.lock", evidence: ["poetry.lock"] }], + languages: [ + { name: "Python", confidence: 0.5, evidence: [".py"], fileCount: 5 }, + ], + packageManagers: [ + { + name: "poetry", + version: null, + lockfile: "poetry.lock", + evidence: ["poetry.lock"], + }, + ], signals: [ - { type: "manifest", path: "pyproject.toml", detail: null, confidence: 1.0 }, - { type: "lockfile", path: "poetry.lock", detail: null, confidence: 1.0 }, + { + type: "manifest", + path: "pyproject.toml", + detail: null, + confidence: 1.0, + }, + { + type: "lockfile", + path: "poetry.lock", + detail: null, + confidence: 1.0, + }, ], }); const proposal = await createPlanner().plan(ctx); @@ -57,8 +108,17 @@ describe("plan — synthesis", () => { it("Rust project proposes fmt-check/clippy/test", async () => { const ctx = makeContext({ - languages: [{ name: "Rust", confidence: 0.3, evidence: [".rs"], fileCount: 3 }], - packageManagers: [{ name: "cargo", version: null, lockfile: "Cargo.lock", evidence: ["Cargo.lock"] }], + languages: [ + { name: "Rust", confidence: 0.3, evidence: [".rs"], fileCount: 3 }, + ], + packageManagers: [ + { + name: "cargo", + version: null, + lockfile: "Cargo.lock", + evidence: ["Cargo.lock"], + }, + ], signals: [ { type: "manifest", path: "Cargo.toml", detail: null, confidence: 1.0 }, { type: "lockfile", path: "Cargo.lock", detail: null, confidence: 1.0 }, @@ -71,8 +131,12 @@ describe("plan — synthesis", () => { it("Go project proposes vet/test", async () => { const ctx = makeContext({ - languages: [{ name: "Go", confidence: 0.5, evidence: [".go"], fileCount: 5 }], - packageManagers: [{ name: "go", version: null, lockfile: "go.sum", evidence: ["go.sum"] }], + languages: [ + { name: "Go", confidence: 0.5, evidence: [".go"], fileCount: 5 }, + ], + packageManagers: [ + { name: "go", version: null, lockfile: "go.sum", evidence: ["go.sum"] }, + ], signals: [ { type: "manifest", path: "go.mod", detail: null, confidence: 1.0 }, { type: "lockfile", path: "go.sum", detail: null, confidence: 1.0 }, @@ -93,10 +157,29 @@ describe("plan — synthesis", () => { it("proposed check ids are stable and deterministic", async () => { const ctx = makeContext({ - languages: [{ name: "TypeScript", confidence: 1.0, evidence: [".ts"], fileCount: 10 }], - packageManagers: [{ name: "npm", version: null, lockfile: "package-lock.json", evidence: ["package-lock.json"] }], + languages: [ + { + name: "TypeScript", + confidence: 1.0, + evidence: [".ts"], + fileCount: 10, + }, + ], + packageManagers: [ + { + name: "npm", + version: null, + lockfile: "package-lock.json", + evidence: ["package-lock.json"], + }, + ], signals: [ - { type: "manifest", path: "package.json", detail: null, confidence: 1.0 }, + { + type: "manifest", + path: "package.json", + detail: null, + confidence: 1.0, + }, ], }); const p1 = await createPlanner().plan(ctx); @@ -110,25 +193,58 @@ describe("plan — synthesis", () => { it("signalRef points at the triggering manifest/lockfile signal", async () => { const ctx = makeContext({ - languages: [{ name: "TypeScript", confidence: 1.0, evidence: [".ts"], fileCount: 10 }], - packageManagers: [{ name: "bun", version: null, lockfile: "bun.lock", evidence: ["bun.lock"] }], + languages: [ + { + name: "TypeScript", + confidence: 1.0, + evidence: [".ts"], + fileCount: 10, + }, + ], + packageManagers: [ + { + name: "bun", + version: null, + lockfile: "bun.lock", + evidence: ["bun.lock"], + }, + ], signals: [ - { type: "manifest", path: "package.json", detail: null, confidence: 1.0 }, + { + type: "manifest", + path: "package.json", + detail: null, + confidence: 1.0, + }, { type: "lockfile", path: "bun.lock", detail: null, confidence: 1.0 }, ], }); const proposal = await createPlanner().plan(ctx); for (const check of proposal.checks) { expect(check.signalRef).not.toBeNull(); - expect(check.signalRef === "manifest:package.json" || check.signalRef === "lockfile:bun.lock").toBe(true); + expect( + check.signalRef === "manifest:package.json" || + check.signalRef === "lockfile:bun.lock", + ).toBe(true); } }); it("all proposed checks have priority 2", async () => { const ctx = makeContext({ - languages: [{ name: "Rust", confidence: 1.0, evidence: [".rs"], fileCount: 10 }], - packageManagers: [{ name: "cargo", version: null, lockfile: "Cargo.lock", evidence: ["Cargo.lock"] }], - signals: [{ type: "manifest", path: "Cargo.toml", detail: null, confidence: 1.0 }], + languages: [ + { name: "Rust", confidence: 1.0, evidence: [".rs"], fileCount: 10 }, + ], + packageManagers: [ + { + name: "cargo", + version: null, + lockfile: "Cargo.lock", + evidence: ["Cargo.lock"], + }, + ], + signals: [ + { type: "manifest", path: "Cargo.toml", detail: null, confidence: 1.0 }, + ], }); const proposal = await createPlanner().plan(ctx); for (const check of proposal.checks) { diff --git a/packages/planner/src/detect.ts b/packages/planner/src/detect.ts index 1618de22f..7a6ca9379 100644 --- a/packages/planner/src/detect.ts +++ b/packages/planner/src/detect.ts @@ -46,32 +46,62 @@ export function detectSignals(files: readonly string[]): LocalSignal[] { const base = basename(file); // manifest if (MANIFEST_FILES.includes(base)) { - signals.push({ type: "manifest", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "manifest", + path: file, + detail: null, + confidence: 1.0, + }); continue; } // lockfile if (base in LOCKFILE_MAP) { - signals.push({ type: "lockfile", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "lockfile", + path: file, + detail: null, + confidence: 1.0, + }); continue; } // dockerfile if (base === "Dockerfile" || base.endsWith(".Dockerfile")) { - signals.push({ type: "dockerfile", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "dockerfile", + path: file, + detail: null, + confidence: 1.0, + }); continue; } // docker-compose if (base === "docker-compose.yml" || base === "docker-compose.yaml") { - signals.push({ type: "docker-compose", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "docker-compose", + path: file, + detail: null, + confidence: 1.0, + }); continue; } // ci-definition if (isCiDefinition(file, base)) { - signals.push({ type: "ci-definition", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "ci-definition", + path: file, + detail: null, + confidence: 1.0, + }); continue; } // monorepo-marker (file-based only; package.json workspaces handled in detectMonorepo) if (base in MONOREPO_MARKER_FILES) { - signals.push({ type: "monorepo-marker", path: file, detail: null, confidence: 1.0 }); + signals.push({ + type: "monorepo-marker", + path: file, + detail: null, + confidence: 1.0, + }); continue; } } @@ -79,10 +109,17 @@ export function detectSignals(files: readonly string[]): LocalSignal[] { } function isCiDefinition(file: string, base: string): boolean { - if (base === ".gitlab-ci.yml" || base === "azure-pipelines.yml" || base === "Jenkinsfile") { + if ( + base === ".gitlab-ci.yml" || + base === "azure-pipelines.yml" || + base === "Jenkinsfile" + ) { return true; } - if (file.startsWith(".github/workflows/") && (base.endsWith(".yml") || base.endsWith(".yaml"))) { + if ( + file.startsWith(".github/workflows/") && + (base.endsWith(".yml") || base.endsWith(".yaml")) + ) { return true; } if (file.startsWith(".circleci/")) { @@ -220,8 +257,20 @@ function parsePackageManager(pm: string): { const toolName = pm.slice(0, atIdx); const version = pm.slice(atIdx + 1) || null; const valid: PackageManagerName[] = [ - "npm", "yarn", "pnpm", "bun", "pip", "poetry", "uv", - "pipenv", "cargo", "go", "maven", "gradle", "composer", "other", + "npm", + "yarn", + "pnpm", + "bun", + "pip", + "poetry", + "uv", + "pipenv", + "cargo", + "go", + "maven", + "gradle", + "composer", + "other", ]; if (!valid.includes(toolName as PackageManagerName)) { return { tool: null, version }; diff --git a/packages/planner/src/index.ts b/packages/planner/src/index.ts index c3743608e..8aadba757 100644 --- a/packages/planner/src/index.ts +++ b/packages/planner/src/index.ts @@ -1,9 +1,18 @@ // @sverka/planner — public API -export { type Planner, type DiscoverOptions, type ProjectContext, - type PlanProposal, type ProposedCheck, type LocalSignal, - type LocalSignalType, type DetectedLanguage, - type DetectedPackageManager, type MonorepoMarker, - type ChangedFile, type DiscoveryExplanation } from "./planner.js"; +export { + type Planner, + type DiscoverOptions, + type ProjectContext, + type PlanProposal, + type ProposedCheck, + type LocalSignal, + type LocalSignalType, + type DetectedLanguage, + type DetectedPackageManager, + type MonorepoMarker, + type ChangedFile, + type DiscoveryExplanation, +} from "./planner.js"; export { createPlanner } from "./planner.js"; export { DiscoveryError, type DiscoveryErrorCode } from "./errors.js"; diff --git a/packages/planner/src/planner.ts b/packages/planner/src/planner.ts index 5a8bf979a..b0af394dd 100644 --- a/packages/planner/src/planner.ts +++ b/packages/planner/src/planner.ts @@ -55,8 +55,20 @@ export interface DetectedLanguage { } export type PackageManagerName = - | "npm" | "yarn" | "pnpm" | "bun" | "pip" | "poetry" | "uv" - | "pipenv" | "cargo" | "go" | "maven" | "gradle" | "composer" | "other"; + | "npm" + | "yarn" + | "pnpm" + | "bun" + | "pip" + | "poetry" + | "uv" + | "pipenv" + | "cargo" + | "go" + | "maven" + | "gradle" + | "composer" + | "other"; export interface DetectedPackageManager { name: PackageManagerName; @@ -66,7 +78,12 @@ export interface DetectedPackageManager { } export type MonorepoTool = - | "nx" | "turborepo" | "lerna" | "pnpm-workspace" | "bun-workspace" | "custom"; + | "nx" + | "turborepo" + | "lerna" + | "pnpm-workspace" + | "bun-workspace" + | "custom"; export interface MonorepoMarker { tool: MonorepoTool; @@ -122,19 +139,30 @@ class PlannerImpl implements Planner { async discover(options: DiscoverOptions): Promise { const root = options.root; if (!existsSync(root)) { - throw new DiscoveryError(`root directory not found: ${root}`, "ROOT_NOT_FOUND"); + throw new DiscoveryError( + `root directory not found: ${root}`, + "ROOT_NOT_FOUND", + ); } try { await this.git.run(["--version"], root); } catch (err) { - throw new DiscoveryError("git is not installed or not on PATH", "GIT_UNAVAILABLE", err); + throw new DiscoveryError( + "git is not installed or not on PATH", + "GIT_UNAVAILABLE", + err, + ); } let toplevel: string; try { const out = await this.git.run(["rev-parse", "--show-toplevel"], root); toplevel = out.trim(); } catch (err) { - throw new DiscoveryError(`not a git repository: ${root}`, "GIT_NOT_A_REPO", err); + throw new DiscoveryError( + `not a git repository: ${root}`, + "GIT_NOT_A_REPO", + err, + ); } let trackedRaw: string; let porcelainRaw: string; @@ -142,7 +170,11 @@ class PlannerImpl implements Planner { trackedRaw = await this.git.run(["ls-files"], toplevel); porcelainRaw = await this.git.run(["status", "--porcelain"], toplevel); } catch (err) { - throw new DiscoveryError("filesystem traversal failed", "TRAVERSAL_FAILED", err); + throw new DiscoveryError( + "filesystem traversal failed", + "TRAVERSAL_FAILED", + err, + ); } const tracked = trackedRaw.split("\n").filter(Boolean); const porcelain = porcelainRaw.split("\n").filter(Boolean); @@ -266,16 +298,66 @@ interface PlanDriver { } const PLAN_DRIVERS: readonly PlanDriver[] = [ - { checkId: "typecheck", reason: "Node project defaults", languages: ["TypeScript", "JavaScript"], packageManagers: ["npm", "yarn", "pnpm", "bun"] }, - { checkId: "lint", reason: "Node project defaults", languages: ["TypeScript", "JavaScript"], packageManagers: ["npm", "yarn", "pnpm", "bun"] }, - { checkId: "test", reason: "Node project defaults", languages: ["TypeScript", "JavaScript"], packageManagers: ["npm", "yarn", "pnpm", "bun"] }, - { checkId: "lint", reason: "Python project defaults", languages: ["Python"], packageManagers: ["pip", "poetry", "uv", "pipenv"] }, - { checkId: "test", reason: "Python project defaults", languages: ["Python"], packageManagers: ["pip", "poetry", "uv", "pipenv"] }, - { checkId: "fmt-check", reason: "Rust project defaults", languages: ["Rust"], packageManagers: ["cargo"] }, - { checkId: "clippy", reason: "Rust project defaults", languages: ["Rust"], packageManagers: ["cargo"] }, - { checkId: "test", reason: "Rust project defaults", languages: ["Rust"], packageManagers: ["cargo"] }, - { checkId: "vet", reason: "Go project defaults", languages: ["Go"], packageManagers: ["go"] }, - { checkId: "test", reason: "Go project defaults", languages: ["Go"], packageManagers: ["go"] }, + { + checkId: "typecheck", + reason: "Node project defaults", + languages: ["TypeScript", "JavaScript"], + packageManagers: ["npm", "yarn", "pnpm", "bun"], + }, + { + checkId: "lint", + reason: "Node project defaults", + languages: ["TypeScript", "JavaScript"], + packageManagers: ["npm", "yarn", "pnpm", "bun"], + }, + { + checkId: "test", + reason: "Node project defaults", + languages: ["TypeScript", "JavaScript"], + packageManagers: ["npm", "yarn", "pnpm", "bun"], + }, + { + checkId: "lint", + reason: "Python project defaults", + languages: ["Python"], + packageManagers: ["pip", "poetry", "uv", "pipenv"], + }, + { + checkId: "test", + reason: "Python project defaults", + languages: ["Python"], + packageManagers: ["pip", "poetry", "uv", "pipenv"], + }, + { + checkId: "fmt-check", + reason: "Rust project defaults", + languages: ["Rust"], + packageManagers: ["cargo"], + }, + { + checkId: "clippy", + reason: "Rust project defaults", + languages: ["Rust"], + packageManagers: ["cargo"], + }, + { + checkId: "test", + reason: "Rust project defaults", + languages: ["Rust"], + packageManagers: ["cargo"], + }, + { + checkId: "vet", + reason: "Go project defaults", + languages: ["Go"], + packageManagers: ["go"], + }, + { + checkId: "test", + reason: "Go project defaults", + languages: ["Go"], + packageManagers: ["go"], + }, ]; function synthesizePlan(context: ProjectContext): PlanProposal { @@ -285,9 +367,10 @@ function synthesizePlan(context: ProjectContext): PlanProposal { const checks: ProposedCheck[] = []; const seen = new Set(); - const manifestSignal = context.localSignals.find( - (s) => s.type === "manifest" || s.type === "lockfile", - ) ?? null; + const manifestSignal = + context.localSignals.find( + (s) => s.type === "manifest" || s.type === "lockfile", + ) ?? null; const signalRef = manifestSignal ? `${manifestSignal.type}:${manifestSignal.path}` : null; @@ -300,16 +383,26 @@ function synthesizePlan(context: ProjectContext): PlanProposal { if (seen.has(key)) continue; seen.add(key); const id = `prop-${createHash("sha256").update(`${driver.checkId}${driver.reason}`).digest("hex").slice(0, 16)}`; - checks.push({ id, checkId: driver.checkId, reason: driver.reason, signalRef, priority: 2 }); + checks.push({ + id, + checkId: driver.checkId, + reason: driver.reason, + signalRef, + priority: 2, + }); } if (checks.length === 0) { - notes.push("No default checks applied: no recognized languages or package managers detected."); + notes.push( + "No default checks applied: no recognized languages or package managers detected.", + ); } else { const drivers: string[] = []; if (langNames.length) drivers.push(`languages=[${langNames.join(",")}]`); if (pmNames.length) drivers.push(`packageManagers=[${pmNames.join(",")}]`); - notes.push(`Selected ${checks.length} default checks from ${drivers.join(" ")}.`); + notes.push( + `Selected ${checks.length} default checks from ${drivers.join(" ")}.`, + ); } return { context, checks, workflowPath: null, notes }; diff --git a/packages/policy/package.json b/packages/policy/package.json index f76ba6e96..2ca30696b 100644 --- a/packages/policy/package.json +++ b/packages/policy/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/policy/project.json b/packages/policy/project.json index 03835b441..83ff9c9d6 100644 --- a/packages/policy/project.json +++ b/packages/policy/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/policy" } }, diff --git a/packages/policy/src/__tests__/evaluator.test.ts b/packages/policy/src/__tests__/evaluator.test.ts index 1cf57efbd..f617c3873 100644 --- a/packages/policy/src/__tests__/evaluator.test.ts +++ b/packages/policy/src/__tests__/evaluator.test.ts @@ -195,7 +195,11 @@ describe("summary output", () => { const r = evaluatePolicy( [ makeFinding({ severity: "high", fingerprint: "fp-h1", id: "c:fp-h1" }), - makeFinding({ severity: "medium", fingerprint: "fp-m1", id: "c:fp-m1" }), + makeFinding({ + severity: "medium", + fingerprint: "fp-m1", + id: "c:fp-m1", + }), makeFinding({ severity: "high", fingerprint: "fp-h2", id: "c:fp-h2" }), ], policy, @@ -203,7 +207,9 @@ describe("summary output", () => { ); expect(r.verdict).toBe("fail"); // 3 findings triggered 1 rule (2 high, 1 medium) - expect(r.summary).toBe("fail: 3 findings triggered 1 rule (2 high, 1 medium)"); + expect(r.summary).toBe( + "fail: 3 findings triggered 1 rule (2 high, 1 medium)", + ); }); it("counts multiple triggered rules", () => { diff --git a/packages/policy/src/__tests__/policy.test.ts b/packages/policy/src/__tests__/policy.test.ts index 4de941901..a5ddaa898 100644 --- a/packages/policy/src/__tests__/policy.test.ts +++ b/packages/policy/src/__tests__/policy.test.ts @@ -1,9 +1,5 @@ import { describe, it, expect } from "vitest"; -import { - DEFAULT_POLICY, - createPolicy, - severityRank, -} from "../policy.js"; +import { DEFAULT_POLICY, createPolicy, severityRank } from "../policy.js"; import { PolicyError } from "../errors.js"; import type { Policy, PolicyConfig, Verdict } from "../types.js"; @@ -109,16 +105,12 @@ describe("createPolicy", () => { it("throws INVALID_SEVERITY for unknown severity", () => { expect(() => createPolicy({ - failOn: [ - { severity: "unknown" as unknown as "low", onlyNew: false }, - ], + failOn: [{ severity: "unknown" as unknown as "low", onlyNew: false }], }), ).toThrow(PolicyError); try { createPolicy({ - failOn: [ - { severity: "boom" as unknown as "low", onlyNew: false }, - ], + failOn: [{ severity: "boom" as unknown as "low", onlyNew: false }], }); } catch (err) { expect(err).toBeInstanceOf(PolicyError); diff --git a/packages/policy/src/evaluator.ts b/packages/policy/src/evaluator.ts index fa5cb6943..f677a7107 100644 --- a/packages/policy/src/evaluator.ts +++ b/packages/policy/src/evaluator.ts @@ -24,10 +24,7 @@ const SUMMARY_SEVERITY_ORDER: Severity[] = [ */ function validatePolicy(policy: Policy): asserts policy is Policy { if (!Array.isArray(policy.failOn)) { - throw new PolicyError( - "Policy failOn must be an array", - "INVALID_POLICY", - ); + throw new PolicyError("Policy failOn must be an array", "INVALID_POLICY"); } for (const rule of policy.failOn) { assertValidSeverity(rule.severity); diff --git a/packages/policy/src/index.ts b/packages/policy/src/index.ts index 5a0bc724a..e35b661e2 100644 --- a/packages/policy/src/index.ts +++ b/packages/policy/src/index.ts @@ -1,6 +1,13 @@ // @sverka/policy — public API -export type { Verdict, Policy, FailOnRule, TriggeredFinding, - RuleResult, PolicyResult, PolicyConfig } from "./types.js"; +export type { + Verdict, + Policy, + FailOnRule, + TriggeredFinding, + RuleResult, + PolicyResult, + PolicyConfig, +} from "./types.js"; export { DEFAULT_POLICY, createPolicy } from "./policy.js"; export { evaluatePolicy } from "./evaluator.js"; export { PolicyError, type PolicyErrorCode } from "./errors.js"; diff --git a/packages/runtime-docker/package.json b/packages/runtime-docker/package.json index d7615dc65..fff9676ff 100644 --- a/packages/runtime-docker/package.json +++ b/packages/runtime-docker/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/runtime-docker/project.json b/packages/runtime-docker/project.json index 1cc25e1b9..bc511ddc6 100644 --- a/packages/runtime-docker/project.json +++ b/packages/runtime-docker/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/runtime-docker" } }, diff --git a/packages/runtime-docker/src/__tests__/cache.test.ts b/packages/runtime-docker/src/__tests__/cache.test.ts index a6b7e0020..5e4245441 100644 --- a/packages/runtime-docker/src/__tests__/cache.test.ts +++ b/packages/runtime-docker/src/__tests__/cache.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtemp, mkdir, writeFile, readFile, rm, access } from "node:fs/promises"; +import { + mkdtemp, + mkdir, + writeFile, + readFile, + rm, + access, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { DockerCacheManager } from "../cache.js"; @@ -37,10 +44,7 @@ describe("DockerCacheManager", () => { it("prepare copies declared inputs into the cache directory", async () => { await writeFile(join(sourceDir, "input.txt"), "hello"); const mgr = new DockerCacheManager(cacheDir); - const prepared = await mgr.prepare( - [join(sourceDir, "input.txt")], - "key-2", - ); + const prepared = await mgr.prepare([join(sourceDir, "input.txt")], "key-2"); const copied = await readFile(join(prepared, "input.txt"), "utf8"); expect(copied).toBe("hello"); }); @@ -50,10 +54,7 @@ describe("DockerCacheManager", () => { await mkdir(join(sourceDir, "out"), { recursive: true }); await writeFile(join(sourceDir, "out", "result.txt"), "result"); const mgr = new DockerCacheManager(cacheDir); - await mgr.collect( - [join(sourceDir, "out", "result.txt")], - sourceDir, - ); + await mgr.collect([join(sourceDir, "out", "result.txt")], sourceDir); // collect should copy outputs into cacheDir preserving relative structure. const collected = await readFile( join(cacheDir, "out", "result.txt"), @@ -68,10 +69,7 @@ describe("DockerCacheManager", () => { await mgr.prepare([join(sourceDir, "input.txt")], "key-3"); // Remove the source input; second prepare should restore from cache. await rm(join(sourceDir, "input.txt")); - const prepared = await mgr.prepare( - [join(sourceDir, "input.txt")], - "key-3", - ); + const prepared = await mgr.prepare([join(sourceDir, "input.txt")], "key-3"); // The cached copy should still be present in the prepared dir. const restored = await readFile(join(prepared, "input.txt"), "utf8"); expect(restored).toBe("v1"); diff --git a/packages/runtime-docker/src/__tests__/docker-executor.test.ts b/packages/runtime-docker/src/__tests__/docker-executor.test.ts index c0841dea4..1cb1b394b 100644 --- a/packages/runtime-docker/src/__tests__/docker-executor.test.ts +++ b/packages/runtime-docker/src/__tests__/docker-executor.test.ts @@ -12,16 +12,18 @@ import { // Mock the docker-cli seam so no real Docker daemon is needed. vi.mock("../internal/docker-cli.js", () => ({ - runDocker: vi.fn(async (): Promise<{ - stdout: string; - stderr: string; - exitCode: number; - timedOut?: boolean; - }> => ({ - stdout: "hello\n", - stderr: "", - exitCode: 0, - })), + runDocker: vi.fn( + async (): Promise<{ + stdout: string; + stderr: string; + exitCode: number; + timedOut?: boolean; + }> => ({ + stdout: "hello\n", + stderr: "", + exitCode: 0, + }), + ), })); // Import the mocked module so tests can override per-test. @@ -31,7 +33,11 @@ const mockedRunDocker = vi.mocked(runDocker); beforeEach(() => { mockedRunDocker.mockReset(); - mockedRunDocker.mockResolvedValue({ stdout: "hello\n", stderr: "", exitCode: 0 }); + mockedRunDocker.mockResolvedValue({ + stdout: "hello\n", + stderr: "", + exitCode: 0, + }); }); // --- Slice D: canExecute --- @@ -44,15 +50,17 @@ describe("DockerExecutor.canExecute", () => { it("returns false for host type", () => { const exec = new DockerExecutor(defaultConfig()); - expect( - exec.canExecute(makeDockerOp({ executor: { type: "host" } })), - ).toBe(false); + expect(exec.canExecute(makeDockerOp({ executor: { type: "host" } }))).toBe( + false, + ); }); it("returns false for podman type", () => { const exec = new DockerExecutor(defaultConfig()); expect( - exec.canExecute(makeDockerOp({ executor: { type: "podman", image: "node:24" } })), + exec.canExecute( + makeDockerOp({ executor: { type: "podman", image: "node:24" } }), + ), ).toBe(false); }); @@ -192,7 +200,9 @@ describe("DockerExecutor.buildDockerArgs — container policy", () => { it("appends command and args", () => { const op = makeDockerOp({ command: "echo", args: ["hi", "there"] }); const args = exec.buildDockerArgs(makeRequest(op)); - const imgIdx = args.indexOf("busybox:latest@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); + const imgIdx = args.indexOf( + "busybox:latest@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ); expect(args[imgIdx + 1]).toBe("echo"); expect(args[imgIdx + 2]).toBe("hi"); expect(args[imgIdx + 3]).toBe("there"); @@ -289,9 +299,7 @@ describe("DockerExecutor.buildEnv — secrets allowlist", () => { it("passes only declared credentials from request.credentials", () => { const op = makeDockerOp({ - credentials: [ - { name: "api-key", envVar: "API_KEY", required: true }, - ], + credentials: [{ name: "api-key", envVar: "API_KEY", required: true }], }); const env = exec.buildEnv( makeRequest(op, { @@ -318,15 +326,15 @@ describe("DockerExecutor.buildEnv — secrets allowlist", () => { caught = e; } expect(caught).toBeInstanceOf(ContainerPolicyError); - expect((caught as ContainerPolicyError).code).toBe("CONTAINER_POLICY_VIOLATION"); + expect((caught as ContainerPolicyError).code).toBe( + "CONTAINER_POLICY_VIOLATION", + ); expect((caught as ContainerPolicyError).message).toContain("MY_SECRET"); }); it("allows secret-like env var when declared in credentials", () => { const op = makeDockerOp({ - credentials: [ - { name: "token", envVar: "API_TOKEN", required: true }, - ], + credentials: [{ name: "token", envVar: "API_TOKEN", required: true }], }); const env = exec.buildEnv( makeRequest(op, { diff --git a/packages/runtime-docker/src/__tests__/errors.test.ts b/packages/runtime-docker/src/__tests__/errors.test.ts index 4430f9442..405c510eb 100644 --- a/packages/runtime-docker/src/__tests__/errors.test.ts +++ b/packages/runtime-docker/src/__tests__/errors.test.ts @@ -31,7 +31,10 @@ describe("ImageDigestError", () => { expect(err).toBeInstanceOf(Error); expect(err.name).toBe("ImageDigestError"); expect(err.code).toBe("IMAGE_DIGEST_MISMATCH"); - expect(err.context).toEqual({ expected: "sha256:aaa", actual: "sha256:bbb" }); + expect(err.context).toEqual({ + expected: "sha256:aaa", + actual: "sha256:bbb", + }); }); it("context is optional", () => { diff --git a/packages/runtime-docker/src/__tests__/helpers/fixtures.ts b/packages/runtime-docker/src/__tests__/helpers/fixtures.ts index b9f45409a..9831b7c97 100644 --- a/packages/runtime-docker/src/__tests__/helpers/fixtures.ts +++ b/packages/runtime-docker/src/__tests__/helpers/fixtures.ts @@ -17,7 +17,8 @@ export function makeDockerOp( executor: { type: "docker", image: "busybox:latest", - imageDigest: "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + imageDigest: + "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", }, command: "echo", args: ["hello"], diff --git a/packages/runtime-docker/src/__tests__/image.test.ts b/packages/runtime-docker/src/__tests__/image.test.ts index e64e1a884..2134c6e51 100644 --- a/packages/runtime-docker/src/__tests__/image.test.ts +++ b/packages/runtime-docker/src/__tests__/image.test.ts @@ -26,7 +26,9 @@ describe("verifyImageDigest", () => { stderr: "", exitCode: 0, }); - await expect(verifyImageDigest(image, digest, config)).resolves.toBeUndefined(); + await expect( + verifyImageDigest(image, digest, config), + ).resolves.toBeUndefined(); // Should have called docker inspect only (no pull). expect(mockedRunDocker).toHaveBeenCalledTimes(1); const callArgs = mockedRunDocker.mock.calls[0]?.[0]; @@ -42,7 +44,9 @@ describe("verifyImageDigest", () => { await expect(verifyImageDigest(image, digest, config)).rejects.toThrow( ImageDigestError, ); - await expect(verifyImageDigest(image, digest, config)).rejects.toMatchObject({ + await expect( + verifyImageDigest(image, digest, config), + ).rejects.toMatchObject({ code: "IMAGE_DIGEST_MISMATCH", context: { image, expected: digest, actual: "sha256:wrongdigest" }, }); @@ -66,7 +70,9 @@ describe("verifyImageDigest", () => { stderr: "", exitCode: 0, }); - await expect(verifyImageDigest(image, digest, config)).resolves.toBeUndefined(); + await expect( + verifyImageDigest(image, digest, config), + ).resolves.toBeUndefined(); expect(mockedRunDocker).toHaveBeenCalledTimes(3); expect(mockedRunDocker.mock.calls[0]?.[0]?.[0]).toBe("inspect"); expect(mockedRunDocker.mock.calls[1]?.[0]?.[0]).toBe("pull"); diff --git a/packages/runtime-docker/src/__tests__/integration.test.ts b/packages/runtime-docker/src/__tests__/integration.test.ts index 719810f6b..614becce8 100644 --- a/packages/runtime-docker/src/__tests__/integration.test.ts +++ b/packages/runtime-docker/src/__tests__/integration.test.ts @@ -1,42 +1,49 @@ import { describe, it, expect } from "vitest"; import { DockerExecutor } from "../docker-executor.js"; -import { defaultConfig, makeDockerOp, makeRequest } from "./helpers/fixtures.js"; +import { + defaultConfig, + makeDockerOp, + makeRequest, +} from "./helpers/fixtures.js"; // Integration tests require a real Docker daemon. Skipped by default. // Run with: SVERKA_DOCKER=1 bun run test -describe.skipIf(!process.env.SVERKA_DOCKER)("DockerExecutor integration", () => { - it("runs echo hello in busybox and returns success", async () => { - const exec = new DockerExecutor(defaultConfig()); - const op = makeDockerOp({ - executor: { - type: "docker", - image: "busybox:latest", - // Replace with a real digest when running integration tests. - imageDigest: process.env.SVERKA_BUSYBOX_DIGEST ?? "sha256:dummy", - }, - command: "echo", - args: ["hello"], - timeoutSeconds: 30, +describe.skipIf(process.env.SVERKA_DOCKER !== "1")( + "DockerExecutor integration", + () => { + it("runs echo hello in busybox and returns success", async () => { + const exec = new DockerExecutor(defaultConfig()); + const op = makeDockerOp({ + executor: { + type: "docker", + image: "busybox:latest", + // Replace with a real digest when running integration tests. + imageDigest: process.env.SVERKA_BUSYBOX_DIGEST ?? "sha256:dummy", + }, + command: "echo", + args: ["hello"], + timeoutSeconds: 30, + }); + const result = await exec.execute(makeRequest(op)); + expect(result.status).toBe("success"); + expect(result.logs).toContain("hello"); }); - const result = await exec.execute(makeRequest(op)); - expect(result.status).toBe("success"); - expect(result.logs).toContain("hello"); - }); - it("returns failure for a command that exits 1", async () => { - const exec = new DockerExecutor(defaultConfig()); - const op = makeDockerOp({ - executor: { - type: "docker", - image: "busybox:latest", - imageDigest: process.env.SVERKA_BUSYBOX_DIGEST ?? "sha256:dummy", - }, - command: "sh", - args: ["-c", "exit 1"], - timeoutSeconds: 30, + it("returns failure for a command that exits 1", async () => { + const exec = new DockerExecutor(defaultConfig()); + const op = makeDockerOp({ + executor: { + type: "docker", + image: "busybox:latest", + imageDigest: process.env.SVERKA_BUSYBOX_DIGEST ?? "sha256:dummy", + }, + command: "sh", + args: ["-c", "exit 1"], + timeoutSeconds: 30, + }); + const result = await exec.execute(makeRequest(op)); + expect(result.status).toBe("failure"); + expect(result.exitCode).toBe(1); }); - const result = await exec.execute(makeRequest(op)); - expect(result.status).toBe("failure"); - expect(result.exitCode).toBe(1); - }); -}); + }, +); diff --git a/packages/runtime-docker/src/docker-executor.ts b/packages/runtime-docker/src/docker-executor.ts index 83b56b4b9..2bab86e13 100644 --- a/packages/runtime-docker/src/docker-executor.ts +++ b/packages/runtime-docker/src/docker-executor.ts @@ -153,10 +153,9 @@ export class DockerExecutor implements Executor { } // 2. Validate timeout. if (op.timeoutSeconds === undefined || op.timeoutSeconds <= 0) { - throw new ContainerPolicyError( - "timeoutSeconds must be present and > 0", - { timeoutSeconds: op.timeoutSeconds }, - ); + throw new ContainerPolicyError("timeoutSeconds must be present and > 0", { + timeoutSeconds: op.timeoutSeconds, + }); } // 3. Validate image digest presence. if (op.executor.imageDigest === undefined) { @@ -181,8 +180,7 @@ export class DockerExecutor implements Executor { }); const durationMs = Date.now() - start; - const rawLogs = - result.stdout + (result.stderr ? "\n" + result.stderr : ""); + const rawLogs = result.stdout + (result.stderr ? "\n" + result.stderr : ""); const logs = this.truncateLogs(rawLogs); let base: ExecuteResult; diff --git a/packages/runtime-docker/src/image.ts b/packages/runtime-docker/src/image.ts index 29e97085c..4b4112cb9 100644 --- a/packages/runtime-docker/src/image.ts +++ b/packages/runtime-docker/src/image.ts @@ -33,7 +33,10 @@ export async function verifyImageDigest( // Image not present locally — pull then re-inspect. if (inspectResult.exitCode !== 0) { await runDocker(["pull", image], opts); - inspectResult = await runDocker(["inspect", "--format={{.Id}}", image], opts); + inspectResult = await runDocker( + ["inspect", "--format={{.Id}}", image], + opts, + ); } const actualDigest = inspectResult.stdout.trim(); diff --git a/packages/runtime-docker/src/index.ts b/packages/runtime-docker/src/index.ts index a452baa54..aae65cbdd 100644 --- a/packages/runtime-docker/src/index.ts +++ b/packages/runtime-docker/src/index.ts @@ -4,5 +4,8 @@ export { DockerExecutor } from "./docker-executor.js"; export { type DockerExecutorConfig } from "./config.js"; export { verifyImageDigest } from "./image.js"; export { type CacheManager, DockerCacheManager } from "./cache.js"; -export { DockerExecutorError, ImageDigestError, ContainerPolicyError } - from "./errors.js"; +export { + DockerExecutorError, + ImageDigestError, + ContainerPolicyError, +} from "./errors.js"; diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json index cf335dec3..ac112ccad 100644 --- a/packages/runtime-host/package.json +++ b/packages/runtime-host/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/runtime-host/project.json b/packages/runtime-host/project.json index fe29e1899..6d4144723 100644 --- a/packages/runtime-host/project.json +++ b/packages/runtime-host/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/runtime-host" } }, diff --git a/packages/runtime-host/src/__tests__/host-executor.test.ts b/packages/runtime-host/src/__tests__/host-executor.test.ts index 5fab3ea6b..413f35dc6 100644 --- a/packages/runtime-host/src/__tests__/host-executor.test.ts +++ b/packages/runtime-host/src/__tests__/host-executor.test.ts @@ -5,11 +5,7 @@ import { join } from "node:path"; import { HostExecutor } from "../host-executor.js"; import { createAllowlist } from "../allowlist.js"; import { HostExecutorError, CommandNotAllowedError } from "../errors.js"; -import { - makeHostOp, - makeRequest, - defaultConfig, -} from "./helpers/fixtures.js"; +import { makeHostOp, makeRequest, defaultConfig } from "./helpers/fixtures.js"; let workspace: string; let artifactDir: string; @@ -55,7 +51,9 @@ describe("HostExecutor.canExecute", () => { const exec = new HostExecutor(defaultConfig()); // timeoutSeconds is required in PlanOperation; simulate by setting to 0 expect( - exec.canExecute(makeHostOp({ command: "node", timeoutSeconds: 0 as unknown as number })), + exec.canExecute( + makeHostOp({ command: "node", timeoutSeconds: 0 as unknown as number }), + ), ).toBe(false); }); @@ -235,9 +233,9 @@ describe("HostExecutor.execute — working directory", () => { describe("HostExecutor — privilege escalation prevention", () => { it("rejects runAsUid: 0 at construction", () => { - expect( - () => new HostExecutor(defaultConfig({ runAsUid: 0 })), - ).toThrow(HostExecutorError); + expect(() => new HostExecutor(defaultConfig({ runAsUid: 0 }))).toThrow( + HostExecutorError, + ); }); it("rejects sudo in allowlist at construction", () => { @@ -298,9 +296,7 @@ describe("HostExecutor.execute — artifacts", () => { describe("HostExecutor.execute — log truncation", () => { it("truncates logs exceeding maxLogBytes with a notice", async () => { - const exec = new HostExecutor( - defaultConfig({ maxLogBytes: 20 }), - ); + const exec = new HostExecutor(defaultConfig({ maxLogBytes: 20 })); const op = makeHostOp({ command: "node", args: ["-e", "console.log('A'.repeat(100))"], @@ -308,7 +304,9 @@ describe("HostExecutor.execute — log truncation", () => { const result = await exec.execute( makeRequest(op, { workspace, artifactDir }), ); - expect(result.logs.length).toBeLessThanOrEqual(20 + "\n[log truncated]".length); + expect(result.logs.length).toBeLessThanOrEqual( + 20 + "\n[log truncated]".length, + ); expect(result.logs).toContain("[log truncated]"); }); }); diff --git a/packages/runtime-host/src/host-executor.ts b/packages/runtime-host/src/host-executor.ts index 031e1ef65..105a9072c 100644 --- a/packages/runtime-host/src/host-executor.ts +++ b/packages/runtime-host/src/host-executor.ts @@ -4,10 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import type { Executor, ExecuteRequest, ExecuteResult } from "@sverka/runtime"; import type { PlanOperation } from "@sverka/ir"; import type { HostExecutorConfig } from "./config.js"; -import { - HostExecutorError, - CommandNotAllowedError, -} from "./errors.js"; +import { HostExecutorError, CommandNotAllowedError } from "./errors.js"; const DEFAULT_MAX_LOG_BYTES = 10 * 1024 * 1024; // 10 MiB const GRACE_PERIOD_MS = 2000; @@ -52,7 +49,10 @@ export class HostExecutor implements Executor { if (operation.executor.type !== "host") return false; const command = operation.command ?? ""; if (!this.config.allowlist.isAllowed(command)) return false; - if (operation.timeoutSeconds === undefined || operation.timeoutSeconds <= 0) { + if ( + operation.timeoutSeconds === undefined || + operation.timeoutSeconds <= 0 + ) { return false; } return true; diff --git a/packages/runtime-host/src/index.ts b/packages/runtime-host/src/index.ts index 23dec8492..872be2cf2 100644 --- a/packages/runtime-host/src/index.ts +++ b/packages/runtime-host/src/index.ts @@ -3,5 +3,8 @@ export { HostExecutor } from "./host-executor.js"; export { type HostExecutorConfig } from "./config.js"; export { type CommandAllowlist, createAllowlist } from "./allowlist.js"; -export { HostExecutorError, HostTimeoutError, CommandNotAllowedError } - from "./errors.js"; +export { + HostExecutorError, + HostTimeoutError, + CommandNotAllowedError, +} from "./errors.js"; diff --git a/packages/runtime-podman/package.json b/packages/runtime-podman/package.json index 3890fda8d..bb778ea03 100644 --- a/packages/runtime-podman/package.json +++ b/packages/runtime-podman/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.js" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/runtime-podman/project.json b/packages/runtime-podman/project.json index 47b350a27..b9ce19a69 100644 --- a/packages/runtime-podman/project.json +++ b/packages/runtime-podman/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/runtime-podman" } }, diff --git a/packages/runtime-remote/package.json b/packages/runtime-remote/package.json index ceef69718..ccc457acd 100644 --- a/packages/runtime-remote/package.json +++ b/packages/runtime-remote/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.js" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/packages/runtime-remote/project.json b/packages/runtime-remote/project.json index 43ef4dd86..e40e15720 100644 --- a/packages/runtime-remote/project.json +++ b/packages/runtime-remote/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/runtime-remote" } }, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5aad201b2..3d329e1eb 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/runtime/project.json b/packages/runtime/project.json index bd000cae8..1af26ab07 100644 --- a/packages/runtime/project.json +++ b/packages/runtime/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src --ext .ts", + "command": "bun run oxlint src", "cwd": "packages/runtime" } }, diff --git a/packages/runtime/src/__tests__/cache.test.ts b/packages/runtime/src/__tests__/cache.test.ts index 45e9751b4..a48f3b9e2 100644 --- a/packages/runtime/src/__tests__/cache.test.ts +++ b/packages/runtime/src/__tests__/cache.test.ts @@ -1,9 +1,13 @@ import { describe, it, expect } from "vitest"; import { Scheduler } from "../scheduler.js"; -import type { SchedulerConfig, CacheBackend, CacheKey, CacheEntry } from "../index.js"; +import type { + SchedulerConfig, + CacheBackend, + CacheKey, + CacheEntry, +} from "../index.js"; import { MockExecutor, - op, planFromOps, successResult, validOperation, @@ -45,7 +49,10 @@ function baseConfig( }; } -function cachedOp(id: string, dependsOn: readonly string[] = []): ReturnType { +function cachedOp( + id: string, + dependsOn: readonly string[] = [], +): ReturnType { return validOperation({ id, name: id, @@ -65,9 +72,9 @@ describe("Scheduler — cache reuse", () => { createdAt: new Date().toISOString(), }); const plan = planFromOps([cachedOp("a")]); - const result = await new Scheduler( - baseConfig([exec], { cache }), - ).execute(plan); + const result = await new Scheduler(baseConfig([exec], { cache })).execute( + plan, + ); expect(result.status).toBe("success"); expect(result.outcomes.get("a")?.fromCache).toBe(true); expect(result.outcomes.get("a")?.status).toBe("success"); @@ -80,9 +87,9 @@ describe("Scheduler — cache reuse", () => { }); const cache = new MemoryCache(); const plan = planFromOps([cachedOp("a")]); - const result = await new Scheduler( - baseConfig([exec], { cache }), - ).execute(plan); + const result = await new Scheduler(baseConfig([exec], { cache })).execute( + plan, + ); expect(result.status).toBe("success"); expect(result.outcomes.get("a")?.fromCache).toBe(false); expect(exec.calls.length).toBe(1); diff --git a/packages/runtime/src/__tests__/helpers/fixtures.ts b/packages/runtime/src/__tests__/helpers/fixtures.ts index a5cc16b23..74de5da53 100644 --- a/packages/runtime/src/__tests__/helpers/fixtures.ts +++ b/packages/runtime/src/__tests__/helpers/fixtures.ts @@ -48,7 +48,10 @@ export function validPlan( } /** Build an operation with a stable id and optional deps. */ -export function op(id: string, dependsOn: readonly string[] = []): PlanOperation { +export function op( + id: string, + dependsOn: readonly string[] = [], +): PlanOperation { return validOperation({ id, name: id, dependsOn }); } @@ -66,21 +69,27 @@ export class MockExecutor implements Executor { readonly name: string; readonly calls: ExecuteRequest[] = []; private readonly canExecuteFn: (op: PlanOperation) => boolean; - private readonly resultFn: (req: ExecuteRequest) => ExecuteResult | Promise; + private readonly resultFn: ( + req: ExecuteRequest, + ) => ExecuteResult | Promise; disposed = false; - constructor(opts: { - name?: string; - canExecute?: (op: PlanOperation) => boolean; - result?: - | ExecuteResult - | ((req: ExecuteRequest) => ExecuteResult | Promise); - } = {}) { + constructor( + opts: { + name?: string; + canExecute?: (op: PlanOperation) => boolean; + result?: + | ExecuteResult + | ((req: ExecuteRequest) => ExecuteResult | Promise); + } = {}, + ) { this.name = opts.name ?? "mock"; this.canExecuteFn = opts.canExecute ?? (() => true); const r = opts.result; if (typeof r === "function") { - this.resultFn = r as (req: ExecuteRequest) => ExecuteResult | Promise; + this.resultFn = r as ( + req: ExecuteRequest, + ) => ExecuteResult | Promise; } else { const canned: ExecuteResult = r ?? { operationId: "op-?", @@ -102,7 +111,9 @@ export class MockExecutor implements Executor { const r = await this.resultFn(request); // Ensure operationId is set to the actual op id if the canned result // used the placeholder. - return r.operationId === "op-?" ? { ...r, operationId: request.operation.id } : r; + return r.operationId === "op-?" + ? { ...r, operationId: request.operation.id } + : r; } async dispose(): Promise { diff --git a/packages/runtime/src/__tests__/public-api.test.ts b/packages/runtime/src/__tests__/public-api.test.ts index 2d3b4ca3e..a692989a2 100644 --- a/packages/runtime/src/__tests__/public-api.test.ts +++ b/packages/runtime/src/__tests__/public-api.test.ts @@ -25,7 +25,13 @@ describe("public API surface", () => { it("internal modules are not re-exported from the public entry", () => { const publicNames = Object.keys(api); const internalLeaked = publicNames.filter((n) => - ["topoSort", "dependentsOf", "ResourcePool", "parseCpu", "parseMemory"].includes(n), + [ + "topoSort", + "dependentsOf", + "ResourcePool", + "parseCpu", + "parseMemory", + ].includes(n), ); expect(internalLeaked).toEqual([]); }); diff --git a/packages/runtime/src/__tests__/resource-limits.test.ts b/packages/runtime/src/__tests__/resource-limits.test.ts index 29932cbb4..eeeffc1bb 100644 --- a/packages/runtime/src/__tests__/resource-limits.test.ts +++ b/packages/runtime/src/__tests__/resource-limits.test.ts @@ -1,16 +1,17 @@ import { describe, it, expect } from "vitest"; import { Scheduler } from "../scheduler.js"; import type { SchedulerConfig } from "../scheduler.js"; -import { SchedulerError } from "../errors.js"; import { MockExecutor, - op, planFromOps, successResult, validOperation, } from "./helpers/fixtures.js"; -function baseConfig(executors: readonly MockExecutor[], overrides: Partial = {}): SchedulerConfig { +function baseConfig( + executors: readonly MockExecutor[], + overrides: Partial = {}, +): SchedulerConfig { return { executors, maxConcurrent: 4, @@ -36,8 +37,16 @@ describe("Scheduler — resource limits", () => { return successResult(req.operation.id); }, }); - const opA = validOperation({ id: "a", name: "a", resources: { cpu: "4", memory: "512Mi" } }); - const opB = validOperation({ id: "b", name: "b", resources: { cpu: "4", memory: "512Mi" } }); + const opA = validOperation({ + id: "a", + name: "a", + resources: { cpu: "4", memory: "512Mi" }, + }); + const opB = validOperation({ + id: "b", + name: "b", + resources: { cpu: "4", memory: "512Mi" }, + }); const plan = planFromOps([opA, opB]); await new Scheduler( baseConfig([exec], { maxConcurrent: 4, totalCpu: 4, totalMemory: "2Gi" }), @@ -57,8 +66,16 @@ describe("Scheduler — resource limits", () => { return successResult(req.operation.id); }, }); - const opA = validOperation({ id: "a", name: "a", resources: { cpu: "2", memory: "512Mi" } }); - const opB = validOperation({ id: "b", name: "b", resources: { cpu: "2", memory: "512Mi" } }); + const opA = validOperation({ + id: "a", + name: "a", + resources: { cpu: "2", memory: "512Mi" }, + }); + const opB = validOperation({ + id: "b", + name: "b", + resources: { cpu: "2", memory: "512Mi" }, + }); const plan = planFromOps([opA, opB]); await new Scheduler( baseConfig([exec], { maxConcurrent: 4, totalCpu: 4, totalMemory: "2Gi" }), @@ -68,7 +85,11 @@ describe("Scheduler — resource limits", () => { it("an op requesting more CPU than totalCpu raises INSUFFICIENT_RESOURCES", async () => { const exec = new MockExecutor(); - const opA = validOperation({ id: "a", name: "a", resources: { cpu: "8", memory: "512Mi" } }); + const opA = validOperation({ + id: "a", + name: "a", + resources: { cpu: "8", memory: "512Mi" }, + }); const plan = planFromOps([opA]); await expect( new Scheduler( @@ -92,8 +113,16 @@ describe("Scheduler — resource limits", () => { return successResult(req.operation.id); }, }); - const opA = validOperation({ id: "a", name: "a", resources: { cpu: "4", memory: "512Mi" } }); - const opB = validOperation({ id: "b", name: "b", resources: { cpu: "4", memory: "512Mi" } }); + const opA = validOperation({ + id: "a", + name: "a", + resources: { cpu: "4", memory: "512Mi" }, + }); + const opB = validOperation({ + id: "b", + name: "b", + resources: { cpu: "4", memory: "512Mi" }, + }); const plan = planFromOps([opA, opB]); await new Scheduler( baseConfig([exec], { maxConcurrent: 4, totalCpu: 4 }), @@ -113,8 +142,16 @@ describe("Scheduler — resource limits", () => { return successResult(req.operation.id); }, }); - const opA = validOperation({ id: "a", name: "a", resources: { cpu: "1", memory: "2Gi" } }); - const opB = validOperation({ id: "b", name: "b", resources: { cpu: "1", memory: "2Gi" } }); + const opA = validOperation({ + id: "a", + name: "a", + resources: { cpu: "1", memory: "2Gi" }, + }); + const opB = validOperation({ + id: "b", + name: "b", + resources: { cpu: "1", memory: "2Gi" }, + }); const plan = planFromOps([opA, opB]); await new Scheduler( baseConfig([exec], { maxConcurrent: 4, totalMemory: "2Gi" }), diff --git a/packages/runtime/src/__tests__/retry.test.ts b/packages/runtime/src/__tests__/retry.test.ts index d796d6da5..9e1d23286 100644 --- a/packages/runtime/src/__tests__/retry.test.ts +++ b/packages/runtime/src/__tests__/retry.test.ts @@ -3,14 +3,16 @@ import { Scheduler } from "../scheduler.js"; import type { SchedulerConfig } from "../scheduler.js"; import { MockExecutor, - op, planFromOps, successResult, failureResult, validOperation, } from "./helpers/fixtures.js"; -function baseConfig(executors: readonly MockExecutor[], overrides: Partial = {}): SchedulerConfig { +function baseConfig( + executors: readonly MockExecutor[], + overrides: Partial = {}, +): SchedulerConfig { return { executors, maxConcurrent: 4, @@ -31,7 +33,8 @@ describe("Scheduler — retry policy", () => { const exec = new MockExecutor({ result: (req) => { calls++; - if (calls < 3) return failureResult(req.operation.id, { error: "fail" }); + if (calls < 3) + return failureResult(req.operation.id, { error: "fail" }); return successResult(req.operation.id); }, }); @@ -90,7 +93,10 @@ describe("Scheduler — retry policy", () => { const exec = new MockExecutor({ result: (req) => { calls++; - if (calls < 2) return failureResult(req.operation.id, { error: "operation timeout exceeded" }); + if (calls < 2) + return failureResult(req.operation.id, { + error: "operation timeout exceeded", + }); return successResult(req.operation.id); }, }); @@ -111,7 +117,8 @@ describe("Scheduler — retry policy", () => { const exec = new MockExecutor({ result: (req) => { calls++; - if (calls < 2) return failureResult(req.operation.id, { error: "fail" }); + if (calls < 2) + return failureResult(req.operation.id, { error: "fail" }); return successResult(req.operation.id); }, }); diff --git a/packages/runtime/src/__tests__/scheduler.test.ts b/packages/runtime/src/__tests__/scheduler.test.ts index 77ecf5ece..c4acf1c39 100644 --- a/packages/runtime/src/__tests__/scheduler.test.ts +++ b/packages/runtime/src/__tests__/scheduler.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from "vitest"; import { Scheduler } from "../scheduler.js"; import type { SchedulerConfig } from "../scheduler.js"; -import { SchedulerError } from "../errors.js"; import { MockExecutor, op, @@ -12,7 +11,10 @@ import { } from "./helpers/fixtures.js"; import type { PlanOperation } from "@sverka/ir"; -function baseConfig(executors: readonly MockExecutor[], overrides: Partial = {}): SchedulerConfig { +function baseConfig( + executors: readonly MockExecutor[], + overrides: Partial = {}, +): SchedulerConfig { return { executors, maxConcurrent: 4, @@ -28,11 +30,7 @@ function baseConfig(executors: readonly MockExecutor[], overrides: Partial { it("executes a linear plan a -> b -> c in order", async () => { const exec = new MockExecutor(); - const plan = planFromOps([ - op("a"), - op("b", ["a"]), - op("c", ["b"]), - ]); + const plan = planFromOps([op("a"), op("b", ["a"]), op("c", ["b"])]); const result = await new Scheduler(baseConfig([exec])).execute(plan); expect(result.status).toBe("success"); expect([...result.outcomes.keys()]).toEqual(["a", "b", "c"]); @@ -111,11 +109,7 @@ describe("Scheduler — failure and cancellation", () => { ? failureResult(req.operation.id, { error: "boom" }) : successResult(req.operation.id), }); - const plan = planFromOps([ - op("a"), - op("b", ["a"]), - op("c", ["a"]), - ]); + const plan = planFromOps([op("a"), op("b", ["a"]), op("c", ["a"])]); const result = await new Scheduler(baseConfig([exec])).execute(plan); expect(result.status).toBe("failure"); expect(result.outcomes.get("a")?.status).toBe("failure"); @@ -133,7 +127,12 @@ describe("Scheduler — failure and cancellation", () => { ? failureResult(req.operation.id) : successResult(req.operation.id), }); - const a = validOperation({ id: "a", name: "a", dependsOn: [], continueOnError: true }); + const a = validOperation({ + id: "a", + name: "a", + dependsOn: [], + continueOnError: true, + }); const z = op("z"); const b = op("b", ["a"]); const plan = planFromOps([a, z, b]); @@ -184,11 +183,17 @@ describe("Scheduler — executor routing", () => { id: "d", name: "d", dependsOn: [], - executor: { type: "docker", image: "img", imageDigest: "sha256:" + "a".repeat(64) }, + executor: { + type: "docker", + image: "img", + imageDigest: "sha256:" + "a".repeat(64), + }, }); const hostOp = op("h"); const plan = planFromOps([dockerOp, hostOp]); - const result = await new Scheduler(baseConfig([docker, host])).execute(plan); + const result = await new Scheduler(baseConfig([docker, host])).execute( + plan, + ); expect(result.status).toBe("success"); expect(docker.calls.map((c) => c.operation.id)).toEqual(["d"]); expect(host.calls.map((c) => c.operation.id)).toEqual(["h"]); @@ -202,10 +207,16 @@ describe("Scheduler — executor routing", () => { id: "d", name: "d", dependsOn: [], - executor: { type: "docker", image: "img", imageDigest: "sha256:" + "a".repeat(64) }, + executor: { + type: "docker", + image: "img", + imageDigest: "sha256:" + "a".repeat(64), + }, }); const plan = planFromOps([dockerOp]); - await expect(new Scheduler(baseConfig([host])).execute(plan)).rejects.toMatchObject({ + await expect( + new Scheduler(baseConfig([host])).execute(plan), + ).rejects.toMatchObject({ code: "SCHEDULER_ERROR", context: expect.objectContaining({ code: "NO_EXECUTOR" }), }); @@ -213,12 +224,10 @@ describe("Scheduler — executor routing", () => { it("raises CYCLE_DETECTED when the plan DAG has a cycle", async () => { const exec = new MockExecutor(); - const plan = planFromOps([ - op("a", ["c"]), - op("b", ["a"]), - op("c", ["b"]), - ]); - await expect(new Scheduler(baseConfig([exec])).execute(plan)).rejects.toMatchObject({ + const plan = planFromOps([op("a", ["c"]), op("b", ["a"]), op("c", ["b"])]); + await expect( + new Scheduler(baseConfig([exec])).execute(plan), + ).rejects.toMatchObject({ code: "SCHEDULER_ERROR", context: expect.objectContaining({ code: "CYCLE_DETECTED" }), }); @@ -254,7 +263,9 @@ describe("Scheduler — result status semantics", () => { it("a fatal failure => status: failure", async () => { const exec = new MockExecutor({ result: (req) => - req.operation.id === "a" ? failureResult(req.operation.id) : successResult(req.operation.id), + req.operation.id === "a" + ? failureResult(req.operation.id) + : successResult(req.operation.id), }); const plan = planFromOps([op("a"), op("b", ["a"])]); const result = await new Scheduler(baseConfig([exec])).execute(plan); diff --git a/packages/runtime/src/__tests__/state-store.test.ts b/packages/runtime/src/__tests__/state-store.test.ts index 723cf3a17..56d941c58 100644 --- a/packages/runtime/src/__tests__/state-store.test.ts +++ b/packages/runtime/src/__tests__/state-store.test.ts @@ -2,12 +2,7 @@ import { describe, it, expect } from "vitest"; import { Scheduler } from "../scheduler.js"; import type { SchedulerConfig, StateStore } from "../index.js"; import type { ExecutionState, OperationOutcome } from "../index.js"; -import { - MockExecutor, - op, - planFromOps, - successResult, -} from "./helpers/fixtures.js"; +import { MockExecutor, op, planFromOps } from "./helpers/fixtures.js"; /** In-memory StateStore mock for testing. */ class MemoryStateStore implements StateStore { diff --git a/packages/runtime/src/__tests__/topo.test.ts b/packages/runtime/src/__tests__/topo.test.ts index 72830ece4..9f02e0f76 100644 --- a/packages/runtime/src/__tests__/topo.test.ts +++ b/packages/runtime/src/__tests__/topo.test.ts @@ -53,14 +53,21 @@ describe("topoSort", () => { const ops = [ op("root"), op("normal-child", ["root"]), - validOperation({ id: "crit-child", name: "crit-child", dependsOn: ["root"], tags: ["critical"] }), + validOperation({ + id: "crit-child", + name: "crit-child", + dependsOn: ["root"], + tags: ["critical"], + }), ]; const r = topoSort(ops); expect(r.ok).toBe(true); if (r.ok) { expect(r.order[0]).toBe("root"); // crit-child before normal-child among ready siblings. - expect(r.order.indexOf("crit-child")).toBeLessThan(r.order.indexOf("normal-child")); + expect(r.order.indexOf("crit-child")).toBeLessThan( + r.order.indexOf("normal-child"), + ); } }); diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index d3bd25512..4cdc47cc5 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,6 +1,10 @@ // @sverka/runtime — public API -export { type Executor, type ExecuteRequest, type ExecuteResult } from "./executor.js"; +export { + type Executor, + type ExecuteRequest, + type ExecuteResult, +} from "./executor.js"; export { type StateStore } from "./state-store.js"; export { type CacheBackend, type CacheKey, type CacheEntry } from "./cache.js"; export { @@ -8,5 +12,9 @@ export { type OperationOutcome, type ExecutionState, } from "./result.js"; -export { RuntimeExecutionError, SchedulerError, ExecutorError } from "./errors.js"; +export { + RuntimeExecutionError, + SchedulerError, + ExecutorError, +} from "./errors.js"; export { Scheduler, type SchedulerConfig } from "./scheduler.js"; diff --git a/packages/runtime/src/scheduler.ts b/packages/runtime/src/scheduler.ts index 6a3d46885..c958ae489 100644 --- a/packages/runtime/src/scheduler.ts +++ b/packages/runtime/src/scheduler.ts @@ -2,10 +2,7 @@ import type { Plan, PlanOperation } from "@sverka/ir"; import type { Executor, ExecuteRequest, ExecuteResult } from "./executor.js"; import type { StateStore } from "./state-store.js"; import type { CacheBackend, CacheKey } from "./cache.js"; -import type { - ExecutionResult, - OperationOutcome, -} from "./result.js"; +import type { ExecutionResult, OperationOutcome } from "./result.js"; import { SchedulerError, ExecutorError } from "./errors.js"; import { topoSort, dependentsOf } from "./internal/topo.js"; import { ResourcePool } from "./internal/resource-pool.js"; @@ -214,12 +211,18 @@ export class Scheduler { try { // Cache check. if (this.config.cache && op.cache) { - const cacheKey: CacheKey = { key: op.cache.key, inputs: op.cache.inputs }; + const cacheKey: CacheKey = { + key: op.cache.key, + inputs: op.cache.inputs, + }; try { const entry = await this.config.cache.get(cacheKey); if (entry) { try { - await this.config.cache.restore(cacheKey, this.config.workspace); + await this.config.cache.restore( + cacheKey, + this.config.workspace, + ); s.status = "success"; s.outcome = { operationId: op.id, @@ -297,7 +300,10 @@ export class Scheduler { // Cache store on success. if (this.config.cache && op.cache && outcome.status === "success") { - const cacheKey: CacheKey = { key: op.cache.key, inputs: op.cache.inputs }; + const cacheKey: CacheKey = { + key: op.cache.key, + inputs: op.cache.inputs, + }; try { await this.config.cache.store(cacheKey, this.config.workspace); await this.config.cache.put({ @@ -390,7 +396,10 @@ export class Scheduler { // Mark resume-skipped ops as success in outcomes (already done above). // Process until all ops are handled or cancelled. launchReady(); - while (inflight.size > 0 || (index < order.length && !this.cancelled && !fatalFailure && !runError)) { + while ( + inflight.size > 0 || + (index < order.length && !this.cancelled && !fatalFailure && !runError) + ) { if (inflight.size === 0) { // No inflight but ops remain: either blocked or ready. Try launching. launchReady(); @@ -399,7 +408,11 @@ export class Scheduler { // remaining pending-but-cancelled and break. for (const id of order) { const s = states.get(id); - if (s && s.status === "pending" && !cancelledOps.has(id) && !skippedFromResume.has(id)) { + if ( + s?.status === "pending" && + !cancelledOps.has(id) && + !skippedFromResume.has(id) + ) { // Blocked by a cancelled/failed dep => cancel it. cancelledOps.add(id); s.status = "cancelled"; @@ -502,7 +515,10 @@ export class Scheduler { } private async loadState(planId: string): Promise< - | { completed: readonly string[]; outcomes: ReadonlyMap } + | { + completed: readonly string[]; + outcomes: ReadonlyMap; + } | undefined > { if (!this.config.stateStore || !this.config.resume) return undefined; @@ -570,7 +586,8 @@ export class Scheduler { const isTimeout = result.error?.includes("timeout") ?? false; const shouldRetry = attempt < maxAttempts && - (retryOn.includes("failure") || (isTimeout && retryOn.includes("timeout"))); + (retryOn.includes("failure") || + (isTimeout && retryOn.includes("timeout"))); if (!shouldRetry) { return toOutcome(op.id, result, false); } @@ -588,7 +605,8 @@ export class Scheduler { const isTimeout = e instanceof Error && e.message.includes("timeout"); const shouldRetry = attempt < maxAttempts && - (retryOn.includes("failure") || (isTimeout && retryOn.includes("timeout"))); + (retryOn.includes("failure") || + (isTimeout && retryOn.includes("timeout"))); if (!shouldRetry) { return { operationId: op.id, @@ -650,7 +668,10 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function cancellableSleep(ms: number, scheduler: Scheduler): Promise { +async function cancellableSleep( + ms: number, + scheduler: Scheduler, +): Promise { const deadline = Date.now() + ms; // Poll every 10ms (or sooner) so cancel() is observed promptly. while (Date.now() < deadline) { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a9ccb4149..aafa465d2 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -11,11 +11,13 @@ "import": "./dist/index.mjs" } }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsdown", "test": "vitest run", - "lint": "eslint src", + "lint": "oxlint src", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/sdk/project.json b/packages/sdk/project.json index 0a74362b9..0175d02ee 100644 --- a/packages/sdk/project.json +++ b/packages/sdk/project.json @@ -18,7 +18,7 @@ "lint": { "executor": "nx:run-commands", "options": { - "command": "bun run eslint src", + "command": "bun run oxlint src", "cwd": "packages/sdk" } }, diff --git a/packages/sdk/src/__tests__/convert.test.ts b/packages/sdk/src/__tests__/convert.test.ts index 19828f23d..c7aa9c19b 100644 --- a/packages/sdk/src/__tests__/convert.test.ts +++ b/packages/sdk/src/__tests__/convert.test.ts @@ -3,7 +3,9 @@ import { pipeline, run, task, workflow, validatePlan } from "../index.js"; import { PlanRuntime } from "../internal/plan-runtime.js"; import { convertToPlan } from "../convert.js"; -async function makeOperations(...ops: Parameters): Promise { +async function makeOperations( + ...ops: Parameters +): Promise { const wf = workflow(...ops); const runtime = new PlanRuntime(); const result = await wf.plan(runtime); @@ -12,7 +14,10 @@ async function makeOperations(...ops: Parameters): Promise { it("produces a valid Plan that passes validatePlan", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); const validation = validatePlan(plan); expect(validation.valid).toBe(true); @@ -20,7 +25,10 @@ describe("convertToPlan", () => { }); it("fills defaults: timeoutSeconds=300, resources={cpu:'1',memory:'512Mi'}, dependsOn=[]", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); const op = plan.operations[0]!; expect(op.timeoutSeconds).toBe(300); @@ -30,29 +38,55 @@ describe("convertToPlan", () => { }); it("executor.type defaults to 'host'", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.operations[0]!.executor.type).toBe("host"); }); it("executor.type is 'docker' when option is docker", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "echo" })))); - const plan = convertToPlan(operations, { name: "test", executor: "docker" }); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "echo" }))), + ); + const plan = convertToPlan(operations, { + name: "test", + executor: "docker", + }); expect(plan.operations[0]!.executor.type).toBe("docker"); }); it("copies image and imageDigest from spec", async () => { const operations = await makeOperations( "test", - pipeline(task("op1", run({ command: "echo", image: "node:24", imageDigest: "sha256:" + "a".repeat(64) }))), + pipeline( + task( + "op1", + run({ + command: "echo", + image: "node:24", + imageDigest: "sha256:" + "a".repeat(64), + }), + ), + ), ); - const plan = convertToPlan(operations, { name: "test", executor: "docker" }); + const plan = convertToPlan(operations, { + name: "test", + executor: "docker", + }); expect(plan.operations[0]!.executor.image).toBe("node:24"); - expect(plan.operations[0]!.executor.imageDigest).toBe("sha256:" + "a".repeat(64)); + expect(plan.operations[0]!.executor.imageDigest).toBe( + "sha256:" + "a".repeat(64), + ); }); it("retry defaults to {maxAttempts:1, backoffSeconds:0, retryOn:['failure','timeout']}", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); const retry = plan.operations[0]!.retry; expect(retry.maxAttempts).toBe(1); @@ -61,13 +95,19 @@ describe("convertToPlan", () => { }); it("network defaults to 'deny'", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.operations[0]!.network).toBe("deny"); }); it("continueOnError defaults to false", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.operations[0]!.continueOnError).toBe(false); }); @@ -75,7 +115,9 @@ describe("convertToPlan", () => { it("artifacts default to [] with retain=false", async () => { const operations = await makeOperations( "test", - pipeline(task("op1", run({ command: "true", artifacts: [{ path: "dist" }] }))), + pipeline( + task("op1", run({ command: "true", artifacts: [{ path: "dist" }] })), + ), ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.operations[0]!.artifacts).toHaveLength(1); @@ -83,7 +125,10 @@ describe("convertToPlan", () => { }); it("computePlanId is deterministic (same input → same id)", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan1 = convertToPlan(operations, { name: "test", executor: "host" }); const plan2 = convertToPlan(operations, { name: "test", executor: "host" }); // createdAt differs but id is computed without createdAt. @@ -92,20 +137,29 @@ describe("convertToPlan", () => { }); it("sets apiVersion to sverka.dev/v1", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.apiVersion).toBe("sverka.dev/v1"); }); it("sets metadata with sverkaVersion and generatedBy", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.metadata.sverkaVersion).toBe("0.1.0"); expect(plan.metadata.generatedBy).toBe("manual"); }); it("sourceContextHash is empty string when no context", async () => { - const operations = await makeOperations("test", pipeline(task("op1", run({ command: "true" })))); + const operations = await makeOperations( + "test", + pipeline(task("op1", run({ command: "true" }))), + ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.sourceContextHash).toBe(""); }); @@ -129,7 +183,9 @@ describe("convertToPlan", () => { it("passes tags from OperationSpec to PlanOperation", async () => { const operations = await makeOperations( "test", - pipeline(task("op1", run({ command: "true", tags: ["critical", "security"] }))), + pipeline( + task("op1", run({ command: "true", tags: ["critical", "security"] })), + ), ); const plan = convertToPlan(operations, { name: "test", executor: "host" }); expect(plan.operations[0]!.tags).toEqual(["critical", "security"]); diff --git a/packages/sdk/src/__tests__/define-workflow.test.ts b/packages/sdk/src/__tests__/define-workflow.test.ts index afc83ee2e..b0b4b91a4 100644 --- a/packages/sdk/src/__tests__/define-workflow.test.ts +++ b/packages/sdk/src/__tests__/define-workflow.test.ts @@ -1,11 +1,21 @@ import { describe, it, expect } from "vitest"; -import { defineWorkflow, pipeline, task, run, workflow, type WorkflowDefinition } from "../index.js"; +import { + defineWorkflow, + pipeline, + task, + run, + workflow, + type WorkflowDefinition, +} from "../index.js"; describe("defineWorkflow", () => { it("returns the same object passed in (identity)", () => { const def: WorkflowDefinition = { name: "ci", - workflow: workflow("ci", pipeline(task("lint", run({ command: "true" })))), + workflow: workflow( + "ci", + pipeline(task("lint", run({ command: "true" }))), + ), }; const result = defineWorkflow(def); expect(result).toBe(def); @@ -21,7 +31,10 @@ describe("defineWorkflow", () => { it("preserves optional policy config", () => { const result = defineWorkflow({ name: "ci", - workflow: workflow("ci", pipeline(task("lint", run({ command: "true" })))), + workflow: workflow( + "ci", + pipeline(task("lint", run({ command: "true" }))), + ), policy: { failOn: [{ severity: "high", onlyNew: false }], }, diff --git a/packages/sdk/src/__tests__/errors.test.ts b/packages/sdk/src/__tests__/errors.test.ts index 14affbb58..7edac9402 100644 --- a/packages/sdk/src/__tests__/errors.test.ts +++ b/packages/sdk/src/__tests__/errors.test.ts @@ -1,11 +1,6 @@ import { describe, it, expect, afterEach } from "vitest"; import { join } from "node:path"; -import { - SdkError, - loadWorkflow, - createSverka, - execute, -} from "../index.js"; +import { SdkError, loadWorkflow, createSverka, execute } from "../index.js"; import { makeTempDir, makeTempGitRepo, diff --git a/packages/sdk/src/__tests__/execute-mode.test.ts b/packages/sdk/src/__tests__/execute-mode.test.ts index 8f0b494e1..449ae2ea4 100644 --- a/packages/sdk/src/__tests__/execute-mode.test.ts +++ b/packages/sdk/src/__tests__/execute-mode.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect, afterEach } from "vitest"; import { execute, createSverka } from "../index.js"; -import { makeTempGitRepo, cleanupTempDir, writeSimpleConfig, writeFailingConfig } from "./helpers/fixtures.js"; +import { + makeTempGitRepo, + cleanupTempDir, + writeSimpleConfig, + writeFailingConfig, +} from "./helpers/fixtures.js"; describe("execute mode", () => { const dirs: string[] = []; diff --git a/packages/sdk/src/__tests__/find-config.test.ts b/packages/sdk/src/__tests__/find-config.test.ts index d8b5a933a..028a8870f 100644 --- a/packages/sdk/src/__tests__/find-config.test.ts +++ b/packages/sdk/src/__tests__/find-config.test.ts @@ -1,6 +1,12 @@ import { describe, it, expect, afterEach } from "vitest"; import { findConfig } from "../index.js"; -import { makeTempDir, cleanupTempDir, writeSimpleConfig, writeJsConfig, writeNestedConfig } from "./helpers/fixtures.js"; +import { + makeTempDir, + cleanupTempDir, + writeSimpleConfig, + writeJsConfig, + writeNestedConfig, +} from "./helpers/fixtures.js"; describe("findConfig", () => { const dirs: string[] = []; diff --git a/packages/sdk/src/__tests__/helpers/fixtures.ts b/packages/sdk/src/__tests__/helpers/fixtures.ts index 2acf7ddbf..b4e7f051c 100644 --- a/packages/sdk/src/__tests__/helpers/fixtures.ts +++ b/packages/sdk/src/__tests__/helpers/fixtures.ts @@ -2,12 +2,21 @@ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; import { execSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { pipeline, run, task, defineWorkflow, workflow, type WorkflowDefinition } from "../../index.js"; +import { + pipeline, + run, + task, + defineWorkflow, + workflow, + type WorkflowDefinition, +} from "../../index.js"; /** * Create a temporary directory and return its path. Clean up with cleanupTempDir. */ -export async function makeTempDir(prefix = "sverka-sdk-test-"): Promise { +export async function makeTempDir( + prefix = "sverka-sdk-test-", +): Promise { return mkdtemp(join(tmpdir(), prefix)); } diff --git a/packages/sdk/src/__tests__/load-workflow.test.ts b/packages/sdk/src/__tests__/load-workflow.test.ts index 4a13a6353..ae4f45863 100644 --- a/packages/sdk/src/__tests__/load-workflow.test.ts +++ b/packages/sdk/src/__tests__/load-workflow.test.ts @@ -2,7 +2,13 @@ import { describe, it, expect, afterEach } from "vitest"; import { join } from "node:path"; import { writeFile } from "node:fs/promises"; import { loadWorkflow, SdkError } from "../index.js"; -import { makeTempDir, cleanupTempDir, writeSimpleConfig, writeMalformedConfig, writeSyntaxErrorConfig } from "./helpers/fixtures.js"; +import { + makeTempDir, + cleanupTempDir, + writeSimpleConfig, + writeMalformedConfig, + writeSyntaxErrorConfig, +} from "./helpers/fixtures.js"; describe("loadWorkflow", () => { const dirs: string[] = []; diff --git a/packages/sdk/src/convert.ts b/packages/sdk/src/convert.ts index 7533d625d..389a88a5a 100644 --- a/packages/sdk/src/convert.ts +++ b/packages/sdk/src/convert.ts @@ -69,7 +69,9 @@ function convertOperation( const executor: ExecutorSpec = { type: opts.executor, ...(spec.image !== undefined ? { image: spec.image } : {}), - ...(spec.imageDigest !== undefined ? { imageDigest: spec.imageDigest } : {}), + ...(spec.imageDigest !== undefined + ? { imageDigest: spec.imageDigest } + : {}), }; const resources: ResourceLimits = { @@ -95,7 +97,9 @@ function convertOperation( id: spec.id, kind: spec.kind, name: spec.name, - ...(spec.description !== undefined ? { description: spec.description } : {}), + ...(spec.description !== undefined + ? { description: spec.description } + : {}), ...(spec.command !== undefined ? { command: spec.command } : {}), ...(spec.args !== undefined ? { args: spec.args } : {}), ...(spec.env !== undefined ? { env: spec.env } : {}), @@ -145,7 +149,5 @@ function computeSourceContextHash(context?: ProjectContext): string { String(context.dirty), context.changedFiles.map((f) => f.path).join(","), ]; - return createHash("sha256") - .update(parts.join("|")) - .digest("hex"); + return createHash("sha256").update(parts.join("|")).digest("hex"); } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 55300e49d..7c2ed954d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -24,7 +24,12 @@ export type { export { CoreError, PlanningError, CompositionError } from "@sverka/core"; // ── Re-exports: IR types ────────────────────────────────────────── -export type { Plan, PlanOperation, PlanMetadata, ExecutorSpec } from "@sverka/ir"; +export type { + Plan, + PlanOperation, + PlanMetadata, + ExecutorSpec, +} from "@sverka/ir"; export { validatePlan, computePlanId } from "@sverka/ir"; // ── Re-exports: runtime ─────────────────────────────────────────── diff --git a/packages/sdk/src/sverka.ts b/packages/sdk/src/sverka.ts index 258152044..c35e52ddb 100644 --- a/packages/sdk/src/sverka.ts +++ b/packages/sdk/src/sverka.ts @@ -20,8 +20,13 @@ import type { Finding } from "@sverka/findings"; import { createBuiltinResolver, extractFindings } from "@sverka/checks"; import type { ResolvedCheck } from "@sverka/checks"; -import type { SverkaOptions, Sverka, PlanResult, ExecutionResult } from "./types.js"; -import type { WorkflowDefinition } from "./types.js"; +import type { + SverkaOptions, + Sverka, + PlanResult, + ExecutionResult, + WorkflowDefinition, +} from "./types.js"; import { SdkError } from "./errors.js"; import { findConfig, loadWorkflow } from "./config.js"; import { convertToPlan } from "./convert.js"; @@ -54,7 +59,9 @@ export async function plan(options?: SverkaOptions): Promise { } /** Top-level execute convenience function. */ -export async function execute(options?: SverkaOptions): Promise { +export async function execute( + options?: SverkaOptions, +): Promise { return doExecute(options ?? {}); } @@ -178,7 +185,11 @@ async function doExecute(options: SverkaOptions): Promise { const all: Finding[] = []; for (const r of resolvedChecks) { if (r.outputs.length === 0) continue; - const extracted = await extractFindings(r.outputs, artifactDir, r.checkId); + const extracted = await extractFindings( + r.outputs, + artifactDir, + r.checkId, + ); all.push(...extracted); } findings = all; @@ -197,12 +208,15 @@ async function doExecute(options: SverkaOptions): Promise { const policy: Policy = def?.policy ? createPolicy(def.policy) : DEFAULT_POLICY; - const policyResult = evaluatePolicy(filteredFindings, policy, baselineFingerprints); + const policyResult = evaluatePolicy( + filteredFindings, + policy, + baselineFingerprints, + ); // Verdict: fail if execution failed, otherwise use policy verdict. - const verdict = runtimeResult.status === "success" - ? policyResult.verdict - : "fail"; + const verdict = + runtimeResult.status === "success" ? policyResult.verdict : "fail"; return { findings: filteredFindings, diff --git a/scripts/sarif-to-annotations.py b/scripts/sarif-to-annotations.py new file mode 100644 index 000000000..314eff151 --- /dev/null +++ b/scripts/sarif-to-annotations.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +""" +to-annotations.py — convert SARIF 2.1.0 to GitHub Actions workflow commands. + +Reads a SARIF document from stdin, emits one + ::error|warning|notice file=…,line=…,col=…,title=…::message +line per result to stdout. + +Exit codes: + 0 no error-severity results + 1 one or more error-severity results (HIGH/CRITICAL findings) + 2 invalid input / parse error + +This implements the common-case SARIF subset documented in SKILL.md. +Multiple locations per result are not supported — only locations[0] +is used. Results without locations produce annotations without a +file= parameter (still visible, just not on a specific line). + +Enrichment from SARIF `properties`: + When a result carries `properties` (typical for tools like + SkillSpector that round-trip through JSON→SARIF wrappers), we use + them to make the annotation more useful: + + properties.category → appended to the annotation title + properties.tags → prefix in the title + properties.confidence → " (confidence NN%)" suffix + properties.remediation → " — Fix: …" suffix + properties.code_snippet → truncated and appended as a quote + properties.intent → "Intent: …" prefix in the message +""" +import json +import re +import sys + +LEVEL_MAP = { + "error": "error", + "warning": "warning", + "note": "notice", + "none": "notice", +} + + +def collect_entries(runs): + """Index each entry from tool.driver.entries by its identifier.""" + entries_by_id = {} + for run in runs: + driver = run.get("tool", {}).get("driver", {}) + tool_name = driver.get("name", "tool") + for entry in driver.get("rules", []) or []: + rid = entry.get("id", "") + if not rid: + continue + help_obj = entry.get("help") or {} + entries_by_id[rid] = { + "tool": tool_name, + "short": entry.get("shortDescription", {}).get("text", ""), + "full": entry.get("fullDescription", {}).get("text", ""), + "help": help_obj.get("text", ""), + } + return entries_by_id + + +def sanitize_for_command(value: str) -> str: + """Make a string safe to embed in a GitHub workflow command. + + Workflow commands use `::` as terminator and newlines as end-of-line + markers. Replace `\r`, `\n`, `\t` and other ASCII control characters + with spaces, collapse repeated whitespace, and break any literal `::` + sequences so they cannot be interpreted as command terminators. + """ + value = re.sub(r"[\x00-\x1f\x7f]+", " ", value) + value = re.sub(r"\s+", " ", value) + value = value.replace("::", ": :") + return value.strip() + +def encode_property_value(value: str) -> str: + """Encode a workflow-command property value. + + GitHub Actions workflow commands parse `::`-delimited properties + using `%`, `:`, and `,` as structural characters. Encode them so + that values containing these characters do not corrupt the command. + """ + value = re.sub(r"[\x00-\x1f\x7f]+", " ", value) + value = value.replace("%", "%25") + value = value.replace("\r", "%0D") + value = value.replace("\n", "%0A") + value = value.replace(":", "%3A") + value = value.replace(",", "%2C") + return value + +def build_title(tool_name, rule_id, properties, entries_by_id): + """Compose the title field of a workflow-command annotation. + + Output shape is: taxonomy tags (if present), then the + tool-and-identifier, then a human-readable name pulled from + the result's properties or the SARIF descriptor. + """ + parts = [] + + tags = (properties or {}).get("tags") or [] + if tags: + # Deduplicate while preserving order. Some tools put the same + # category name in both `category` and `tags`, so dedup is real. + seen = set() + tag_strs = [] + for t in tags: + if t and t not in seen: + seen.add(t) + tag_strs.append(str(t)) + if tag_strs: + parts.append("[" + " ".join(tag_strs) + "]") + + parts.append(tool_name + "[" + rule_id + "]") + + category = (properties or {}).get("category") + if not category: + rule = entries_by_id.get(rule_id, {}) + category = rule.get("short") or rule.get("full") + if category: + parts.append(": " + category) + + return sanitize_for_command("".join(parts)) + + +def build_message(explanation, properties): + """Compose the annotation message body from explanation + properties. + + Order: + 1. Intent (what the skill/code is trying to do) + 2. Explanation (the finding itself) + 3. Fix (remediation advice) + 4. Code snippet (truncated; omitted if too long) + 5. Confidence ("(confidence NN%)") + """ + out = [] + + intent = (properties or {}).get("intent") + if intent: + out.append("Intent: " + intent) + if explanation: + out.append(explanation) + + fix = (properties or {}).get("remediation") + if fix: + out.append("Fix: " + fix) + + snippet = (properties or {}).get("code_snippet") + if snippet: + # Truncate long snippets so the annotation stays scannable. + # GitHub annotation messages render fine up to a few hundred + # chars; beyond that they get visually clipped in the diff. + max_len = 400 + flat = snippet.replace("\n", " ⏎ ") + if len(flat) > max_len: + flat = flat[: max_len - 1] + "…" + out.append("Code: " + flat) + + confidence = (properties or {}).get("confidence") + if confidence is not None: + try: + pct = int(round(float(confidence) * 100)) + # We avoid the literal '%' here on purpose: the final + # message is escaped (`%` → `%25`) before being emitted as + # a GitHub workflow command. If we added `70%` here, the + # escape would produce `70%25` and GitHub would render + # `70%25` instead of `70%`. Using `=` and stripping the + # percent keeps the output honest. + out.append("confidence=" + str(pct)) + except (TypeError, ValueError): + # Confidence is optional; non-numeric values are ignored. + pass + + return " — ".join(out) + + +def build_annotation_line(result, tool_name, entries_by_id): + """Build one GitHub workflow-command line for a single SARIF result.""" + ann_level = LEVEL_MAP.get(result.get("level", "warning"), LEVEL_MAP["warning"]) + + rule_id = result.get("ruleId", "?") + properties = result.get("properties") or {} + message = result.get("message", {}).get("text", "(no message)") + + title = build_title(tool_name, rule_id, properties, entries_by_id) + full_msg = build_message(message, properties) + + # Get first physical location + locations = result.get("locations") or [] + file_uri = "" + start_line = None + start_col = None + end_line = None + end_col = None + if locations: + phys = locations[0].get("physicalLocation") or {} + file_uri = phys.get("artifactLocation", {}).get("uri", "") + region = phys.get("region") or {} + start_line = region.get("startLine") + start_col = region.get("startColumn") + end_line = region.get("endLine") + end_col = region.get("endColumn") + + parts = [] + if file_uri: + parts.append("file=" + encode_property_value(file_uri)) + if start_line is not None: + parts.append("line=" + str(start_line)) + if start_col is not None: + parts.append("col=" + str(start_col)) + if end_line is not None: + parts.append("endLine=" + str(end_line)) + if end_col is not None: + parts.append("endColumn=" + str(end_col)) + parts.append("title=" + encode_property_value(title)) + props = ",".join(parts) + + safe_msg = ( + full_msg + .replace("%", "%25") + .replace("\r", " ") + .replace("\n", " ") + .replace("::", ": :") + ) + return "::" + ann_level + " " + props + "::" + safe_msg, ann_level == "error" + + +def main(): + raw = sys.stdin.read() + start = raw.find("{") + if start < 0: + print( + "::error title=sarif-to-annotations::no JSON object found in input", + file=sys.stderr, + ) + sys.exit(2) + try: + data = json.loads(raw[start:]) + except json.JSONDecodeError as e: + print( + "::error title=sarif-to-annotations::invalid JSON: " + str(e), + file=sys.stderr, + ) + sys.exit(2) + + runs = data.get("runs") or [] + if not runs: + sys.exit(0) + + entries_by_id = collect_entries(runs) + + error_count = 0 + out_lines = [] + + for run in runs: + tool_name = run.get("tool", {}).get("driver", {}).get("name", "tool") + for result in run.get("results") or []: + line, is_error = build_annotation_line(result, tool_name, entries_by_id) + out_lines.append(line) + if is_error: + error_count += 1 + + sys.stdout.write("\n".join(out_lines)) + if out_lines: + sys.stdout.write("\n") + + sys.exit(1 if error_count > 0 else 0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..233c7b583 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +# SonarCloud configuration +# https://docs.sonarsource.com/sonarqube-cloud/analyzing-source-code/analysis-parameters/ + +# Project key (must match SonarCloud project settings) +sonar.projectKey=sverka-dev_sverka +sonar.organization=sverka-dev + +# Source directories +sonar.sources=packages + +# Exclude non-source paths from analysis (single line for reliability) +sonar.exclusions=**/dist/**,**/node_modules/**,**/__tests__/**,**/*.test.ts,**/*.test.tsx,**/*.md,**/*.json,**/*.toml,**/*.yml,**/*.yaml,**/tsdown.config.ts,**/vitest.config.ts,**/*.config.ts + +# TypeScript configuration +sonar.typescript.tsconfigPath=tsconfig.json + +# Language +sonar.language=ts