From eaa097a62db9d4d6c19ef9bbe16353675c785022 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 5 Nov 2026 13:37:24 -0600 Subject: [PATCH 1/4] chore(sauce): regenerate agents readme and fix formatting Run pnpm run generate to refresh the stale agents/README.md, and run oxfmt on three files that were unformatted on main. --- AGENTS.md | 1 - agents/README.md | 32 +++++++++---------- release-kit/README.md | 4 +-- .../structural/paths-skill-discovery.test.mts | 4 ++- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3717e183..b6a17e03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,5 @@ # AGENTS.md - ## 📚 Fleet diff --git a/agents/README.md b/agents/README.md index b53561d9..e33af3d0 100644 --- a/agents/README.md +++ b/agents/README.md @@ -4,22 +4,22 @@ You have additional SKILLs documented in directories containing a "SKILL.md" fil ## Available Skills -| Skill | Description | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| brew-publish | Operate the socket-release Homebrew tap flow - the binary-download formula model, tap repo layout, formula bumps tied to published releases, and sha256 verification against the release's own checksums.txt. Use when bumping a Homebrew formula or standing up a tap for a Socket CLI. | -| cargo-publish | Operate the socket-release crates.io flow - the cargo staged model (dry-run default), trusted publishing via OIDC under the cargo-publish environment, index-propagation waits, and yank-as-rollback. Use when publishing a Rust crate in a repo carrying scripts/socket-release/. | -| github-release | Cut, verify, and reconcile immutable GitHub releases with the socket-release kit - the registry-resolvability ORDER RULE, the three-step draft-upload-undraft cut, checksums.txt production, and tag-gap healing. Use when tagging a release, healing a missing tag/release, or when the github-release workflow gate refuses a tag. | -| npm-publish | Operate the socket-release npm flow end to end - bootstrap a package (name reservation, permissive-then-staged-only publishing access, trusted publishing), dispatch a staged publish, promote with --approve, backfill an old version, and roll back with deprecate. Use when publishing an npm package in a repo carrying scripts/socket-release/. | -| socket-dep-cleanup | Evaluate and remove a single unused dependency from your project. Searches the entire codebase for all usages (imports, requires, config refs, scripts, type packages, indirect usage), reports findings, and performs full removal with verification. | -| socket-dep-patch | Apply Socket's binary-level security patches without changing dependency versions. Uses socket-patch apply to fix vulnerabilities in-place, then verifies automated patching is configured so patches persist across installs. | -| socket-dep-replace | Replace a dependency with an alternative package, eliminate it via code rewrite, or use socket-optimize for optimized replacements. | -| socket-dep-upgrade | Use socket fix to find and update vulnerable dependencies, then fix any breaking changes in the codebase. Security-audited upgrades with automated code migration. | -| socket-fix | Fix dependency security issues - either scan and fix everything (requires /socket-scan), or target a single named package. Orchestrates /socket-dep-cleanup, /socket-dep-replace, /socket-dep-patch, and /socket-dep-upgrade as subskills. | -| socket-inspect | Research a package before you depend on it - pull every signal from Socket (scores, alerts, malware verdicts, CVEs, supply-chain risk), check the socket.dev package page, evaluate alternatives, and surface available Socket patches. | -| socket-release | Stand up SocketDev publishing (npm, crates.io, GitHub releases, Homebrew tap) in a repo - copy in the socket-release kit from a sauce checkout and run its bootstrap through name reservation, GitHub environments, npm trusted publisher, publishing-access tightening, staged publish config, and verification. | -| socket-scan | Run a dependency scan using the Socket CLI. Prompts unauthenticated users to log in or create a free account. If the user skips login, falls back to cdxgen with greatly reduced alert accuracy and poor SBOM accuracy. Authenticated users get temporary read-only scans by default (--tmp). Creates a persistent dashboard scan only when explicitly requested. Includes reachability analysis for enterprise customers and license compliance auditing. | -| socket-scan-setup | Set up prerequisites for Socket scanning - install the CLI, configure auth with the public demo token, and verify scan access. Use this before the first scan or when encountering auth errors. | -| socket-setup | Set up Socket - prompt for API key, install the CLI, authenticate, configure policies and tokens, set up CI/CD for firewall or patch modes across GitHub, GitLab, Bitbucket, and other systems. | +| Skill | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| brew-publish | Operate the socket-release Homebrew tap flow - the binary-download formula model, tap repo layout, formula bumps tied to published releases, and sha256 verification against the release's own checksums.txt. Use when bumping a Homebrew formula or standing up a tap for a Socket CLI. | +| cargo-publish | Operate the socket-release crates.io flow - the cargo staged model (dry-run default), trusted publishing via OIDC under the cargo-publish environment, index-propagation waits, and yank-as-rollback. Use when publishing a Rust crate in a repo carrying scripts/socket-release/. | +| github-release | Cut, verify, and reconcile immutable GitHub releases with the socket-release kit - the registry-resolvability ORDER RULE, the three-step draft-upload-undraft cut, checksums.txt production, and tag-gap healing. Use when tagging a release, healing a missing tag/release, or when the github-release workflow gate refuses a tag. | +| npm-publish | Operate the socket-release npm flow end to end - bootstrap a package (name reservation, permissive-then-staged-only publishing access, trusted publishing), dispatch a staged publish, promote with --approve, backfill an old version, and roll back with deprecate. Use when publishing an npm package in a repo carrying scripts/socket-release/. | +| socket-dep-cleanup | Evaluate and remove a single unused dependency from your project. Searches the entire codebase for all usages (imports, requires, config refs, scripts, type packages, indirect usage), reports findings, and performs full removal with verification. | +| socket-dep-patch | Apply Socket's binary-level security patches without changing dependency versions. Uses socket-patch apply to fix vulnerabilities in-place, then verifies automated patching is configured so patches persist across installs. | +| socket-dep-replace | Replace a dependency with an alternative package, eliminate it via code rewrite, or use socket-optimize for optimized replacements. | +| socket-dep-upgrade | Use socket fix to find and update vulnerable dependencies, then fix any breaking changes in the codebase. Security-audited upgrades with automated code migration. | +| socket-fix | Fix dependency security issues - either scan and fix everything (requires /socket-scan), or target a single named package. Orchestrates /socket-dep-cleanup, /socket-dep-replace, /socket-dep-patch, and /socket-dep-upgrade as subskills. | +| socket-inspect | Research a package before you depend on it - pull every signal from Socket (scores, alerts, malware verdicts, CVEs, supply-chain risk), check the socket.dev package page, evaluate alternatives, and surface available Socket patches. | +| socket-release | Stand up SocketDev publishing (npm, crates.io, GitHub releases, Homebrew tap) in a repo - copy in the socket-release kit from a sauce checkout and run its bootstrap through name reservation, GitHub environments, npm trusted publisher, publishing-access tightening, staged publish config, and verification. | +| socket-scan | Run a dependency scan using the Socket CLI. Prompt unauthenticated users to log in or create an account. If they decline, offer a local SBOM that does not include Socket analysis. Authenticated users get temporary read-only scans by default (--tmp). Creates a persistent dashboard scan only when explicitly requested. Includes reachability analysis for enterprise customers and license compliance auditing. | +| socket-scan-setup | Set up prerequisites for Socket scanning - install the CLI, configure user-owned authentication, and verify scan access. Use this before the first scan or when encountering auth errors. | +| socket-setup | Set up Socket - prompt for API key, install the CLI, authenticate, configure policies and tokens, set up CI/CD for firewall or patch modes across GitHub, GitLab, Bitbucket, and other systems. | ## Usage diff --git a/release-kit/README.md b/release-kit/README.md index c71236b6..f93a7827 100644 --- a/release-kit/README.md +++ b/release-kit/README.md @@ -373,11 +373,11 @@ fleet's shared script tree is done **fleet-first or not at all**. **Nothing else may be added at root.** _(machine-enforced)_ 2. **Tiers.** A flow's implementation lives in `publish-infra//` using phase names from the closed set `{registry, staged, approve, placeholder, -trusted-publisher}`; flow-specific nouns (brew `formula`/`tap`) appear in + trusted-publisher}`; flow-specific nouns (brew `formula`/`tap`) appear in the layout table. A phase-named file contains only that phase. 3. **Pure vs effect.** PURE ⇔ imports no `node:{fs,child_process,net,http}`. Pure modules take the nouns `plan / parse / render / state / config / -gates`. Every injectable effect module is `seams.mts` exporting + gates`. Every injectable effect module is `seams.mts` exporting `Seams` + `resolveSeams`; browser lanes are the triple `-{plan,parse,page}` sharing `browser-session.mts`. 4. **Gate vocabulary.** A refusal point is a _gate_ - the only sanctioned diff --git a/test/repo/unit/structural/paths-skill-discovery.test.mts b/test/repo/unit/structural/paths-skill-discovery.test.mts index 0608c8bd..89366aa9 100644 --- a/test/repo/unit/structural/paths-skill-discovery.test.mts +++ b/test/repo/unit/structural/paths-skill-discovery.test.mts @@ -29,7 +29,9 @@ const EXPECTED_SUBSKILLS: Record = { 'socket-scan': ['socket-scan-setup'], } -export /** All skill directory paths (top-level as name, subskills as parent/name) */ +export /** + * All skill directory paths (top-level as name, subskills as parent/name) + */ function getAllSkillPaths(): string[] { const paths: string[] = [] const dirs = getSkillDirs() From a46a0dd18926b6e4ef99a08253ef659561281a19 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 5 Nov 2026 13:37:24 -0600 Subject: [PATCH 2/4] build: drop the fleet pack and own the repo tooling Match SocketDev/socket-mcp#232: sauce no longer hydrates the GHCR fleet pack and keeps no fleet code. The repo owns small scripts for each job, and CI uses SHA-pinned upstream actions. - test, cover, fuzz harnesses, structural: plain vitest through .config/repo/vitest.config.mts. - format and lint: oxfmt and oxlint with root .oxfmtrc.json and .oxlintrc.json. - type: tsc on tsconfig.json and .config/repo/tsconfig.release-kit.json. - check: scripts/repo/check.mts runs format, lint, type, check-paths, the scripts/repo/check/*.mts gates, generate:check, and a content scan. - prepare: scripts/repo/install-git-hooks.mts sets core.hooksPath. - git hooks: commit-msg checks Conventional Commits and AI attribution, pre-commit scans staged files, pre-push runs the type check. - CI: actions/checkout and actions/setup-node pinned by SHA, pnpm 12.14.0 from npm, and the no-telemetry env in every workflow. Delete .github/actions and the workflows that needed fleet scripts. From socket-mcp: scripts/repo/util/{main,commit-message,scan}.mts, scripts/repo/hooks/*.mts, .git-hooks/*, .oxfmtrc.json, CLAUDE.md, .gitattributes, test/repo/unit/repo-scan.test.mts, and the ci-gates.yml shape. The scan drops the private-repo-name rule, because sauce names that repository in its plugin installer and release-kit browser profile, and the shipped-content check governs that name. The scan also skips tracked directory symlinks, and pre-commit skips staged gitlinks, which have no blob to read. repo-scan.test.mts and commit-message.test.mts cover the scan, the hooks, and both differences. Delete the GHCR download, verification and hydration code (scripts/repo/bootstrap), its .gitignore block, and its setup steps in .claude/settings.json. Delete the fleet trees (scripts/fleet, .claude/hooks/fleet, .claude/skills/fleet, .config/fleet, test/fleet, the fleet git-hook chain, .husky), the fleet member configs under .config and .config/repo, and the scripts/repo entry points that only fleet tooling ran. Move the few helpers sauce code used next to their one consumer. Drop the devDependencies, catalog pins, soak exclusions, and patches that only that code needed, and rewrite AGENTS.md, .gitignore and the remaining fleet comments to describe this repository. --- .claude/output-styles/fleet.md | 10 - .claude/settings.json | 73 +- .claude/skills/repo/updating-xport/SKILL.md | 223 - .config/.markdownlint-cli2.jsonc | 51 - .config/.prettierignore | 47 - .config/esbuild/node-protocol.mts | 40 - .config/esbuild/shorten-paths.mts | 189 - .config/fleet/.prettierignore | 133 - .config/fleet/oxlintrc.json | 374 - .config/fleet/tsconfig.check.json | 7 - .../_shared/wheelhouse-self-skip.mjs | 43 - .../socket-no-private-wheelhouse-leak.mjs | 63 - .../socket-no-relative-sibling-script.mjs | 69 - .../socket-readme-required-sections.mjs | 95 - .config/oxfmtrc.json | 133 - .config/oxlintrc.json | 296 - .config/repo/.prettierignore | 4 - .config/repo/external-tools.json | 208 - .config/repo/rolldown/lib-stub.mts | 46 - .config/repo/root-files.json | 14 - .config/repo/socket-wheelhouse-schema.json | 1898 -- .config/repo/socket-wheelhouse.json | 126 - .config/repo/tsconfig.check.json | 4 - .config/repo/tsconfig.release-kit.json | 4 +- .config/repo/vitest.config.mts | 597 +- .config/repo/vitest.settings.mts | 202 - .config/sfw-bypass-list.txt | 143 - .config/socket-registry-pins.json | 26 - .config/taze.config.mts | 40 - .config/tsconfig.base.json | 30 - .config/vitest.config.mts | 61 - .config/vitest.coverage.fleet.config.mts | 56 - .editorconfig | 2 +- .git-hooks/_shared/canonical/bundle.mts | 123 - .git-hooks/_shared/canonical/fork-scan.mts | 260 - .git-hooks/_shared/canonical/git.mts | 164 - .git-hooks/_shared/canonical/patch.mts | 52 - .git-hooks/_shared/canonical/proof.mts | 357 - .git-hooks/_shared/canonical/receipt.mts | 114 - .git-hooks/_shared/canonical/replacements.mts | 158 - .git-hooks/_shared/canonical/source.mts | 145 - .git-hooks/_shared/check-output.mts | 48 - .git-hooks/_shared/commit-format.mts | 146 - .git-hooks/_shared/commit-subject.mts | 74 - .git-hooks/_shared/cross-repo-expressions.mts | 186 - .git-hooks/_shared/cross-repo.mts | 303 - .git-hooks/_shared/external-issue-ref.mts | 108 - .git-hooks/_shared/file-scan.mts | 90 - .git-hooks/_shared/git-context-vars.mts | 14 - .git-hooks/_shared/git-identity.mts | 207 - .git-hooks/_shared/git.mts | 66 - .git-hooks/_shared/isolate-git-env.mts | 59 - .git-hooks/_shared/logger-leaks.mts | 189 - .git-hooks/_shared/node-version.mts | 24 - .git-hooks/_shared/personal-path.mts | 57 - .git-hooks/_shared/pkg-script-target.mts | 80 - .git-hooks/_shared/push/commit-messages.mts | 66 - .git-hooks/_shared/push/durable-ref.mts | 85 - .git-hooks/_shared/push/file-scan.mts | 374 - .git-hooks/_shared/push/pr-commit-count.mts | 114 - .git-hooks/_shared/push/range.mts | 97 - .git-hooks/_shared/push/release-ledger.mts | 41 - .git-hooks/_shared/push/release-tags.mts | 253 - .git-hooks/_shared/push/repo-gates.mts | 548 - .git-hooks/_shared/push/signatures.mts | 184 - .git-hooks/_shared/push/squash-history.mts | 63 - .git-hooks/_shared/repo-containment.mts | 172 - .git-hooks/_shared/repository-source-root.mts | 96 - .git-hooks/_shared/resolve-node.sh | 55 - .git-hooks/_shared/run-step.sh | 223 - .git-hooks/_shared/sanitize-token-env.sh | 24 - .git-hooks/_shared/scan-code-refs.mts | 80 - .git-hooks/_shared/scan-comments.mts | 365 - .git-hooks/_shared/scan-commit-msg.mts | 162 - .git-hooks/_shared/scan-core.mts | 310 - .../_shared/scan-package-conventions.mts | 243 - .git-hooks/_shared/scan-secrets.mts | 190 - .git-hooks/_shared/scan-supply-chain.mts | 127 - .git-hooks/_shared/staged-gates.mts | 268 - .git-hooks/_shared/typecheck-cache.mts | 198 - .git-hooks/_shared/util.mts | 136 - .git-hooks/commit-msg | 26 +- .git-hooks/fleet/commit-msg | 22 - .git-hooks/fleet/commit-msg.mts | 325 - .git-hooks/fleet/post-commit | 21 - .git-hooks/fleet/pre-commit | 58 - .git-hooks/fleet/pre-commit.mts | 859 - .git-hooks/fleet/pre-merge-commit | 22 - .git-hooks/fleet/pre-merge-commit.mts | 174 - .git-hooks/fleet/pre-push | 31 - .git-hooks/fleet/pre-push.mts | 236 - .git-hooks/post-commit | 31 - .git-hooks/pre-commit | 30 +- .git-hooks/pre-merge-commit | 28 - .git-hooks/pre-push | 33 +- .gitattributes | 162 +- .../fleet/_shared/codeql-languages.d.mts | 13 - .../fleet/_shared/codeql-languages.mjs | 104 - .../actions/fleet/_shared/install-tool.d.mts | 29 - .../actions/fleet/_shared/install-tool.mjs | 434 - .../fleet/_shared/install/atomic.d.mts | 7 - .../actions/fleet/_shared/install/atomic.mjs | 68 - .github/actions/fleet/_shared/jq.d.mts | 14 - .github/actions/fleet/_shared/jq.mjs | 120 - .../actions/fleet/_shared/platform-key.mjs | 58 - .github/actions/fleet/_shared/platform.d.mts | 9 - .github/actions/fleet/_shared/platform.mjs | 105 - .../actions/fleet/_shared/release-asset.mts | 172 - ...esolve-external-tool-asset.generated.d.mts | 56 - .../resolve-external-tool-asset.generated.mjs | 493 - .../_shared/resolve-external-tool-asset.mts | 350 - .../resolve-external-tool-platform.mts | 185 - .../actions/fleet/_shared/runner-images.json | 411 - .../_shared/verify-integrity-provenance.d.mts | 55 - .../_shared/verify-integrity-provenance.mjs | 535 - .../actions/fleet/cache-pnpm-store/action.yml | 144 - .../fleet/cache-pnpm-store/ecosystems.mjs | 314 - .../resolve-store-cache.d.mts | 42 - .../cache-pnpm-store/resolve-store-cache.mjs | 214 - .github/actions/fleet/checkout/action.yml | 367 - .github/actions/fleet/debug/action.yml | 49 - .../fleet/expose-actions-runtime/action.yml | 16 - .../fleet/expose-actions-runtime/index.cjs | 40 - .../fleet/github-ci-fix-app-token/action.yml | 39 - .../mint-app-installation-token.mjs | 386 - .../fleet/github-payload-app-token/action.yml | 55 - .../mint-app-installation-token.mjs | 386 - .../github-pr-branch-app-token/action.yml | 42 - .../mint-app-installation-token.mjs | 386 - .../fleet/github-status-check/action.yml | 54 - .../probe-github-status.d.mts | 68 - .../probe-github-status.mjs | 313 - .github/actions/fleet/install/action.yml | 162 - .../fleet/install/verify-lib-floor.d.mts | 67 - .../fleet/install/verify-lib-floor.mjs | 320 - .../fleet/setup-and-install/action.yml | 256 - .../runner-resource-runtime.mts | 281 - .../setup-and-install/runner-swap-file.mts | 218 - .../setup-runner-resources.mts | 421 - .github/actions/fleet/setup/action.yml | 887 - .../actions/fleet/setup/bootstrap-pnpm.d.mts | 60 - .../actions/fleet/setup/bootstrap-pnpm.mjs | 371 - .../actions/fleet/setup/export-fleet-env.mjs | 79 - .../fleet/setup/external-tools.generated.json | 1024 -- .github/actions/fleet/setup/fleet-env.json | 40 - .../actions/fleet/setup/plan-setup-node.d.mts | 38 - .../actions/fleet/setup/plan-setup-node.mjs | 321 - .../fleet/setup/plan-setup-tools.d.mts | 93 - .../actions/fleet/setup/plan-setup-tools.mjs | 414 - .github/aw/actions-lock.json | 19 - .github/workflows/ci-e2e.yml | 49 +- .github/workflows/ci-fix.yml | 114 - .github/workflows/ci-gates.yml | 125 +- .../ci-publish-claude-marketplace.yml | 45 +- .github/workflows/cron-weekly-fuzz.yml | 636 - .github/workflows/cron-weekly-odai-cache.yml | 51 - .github/workflows/cron-weekly-update.yml | 336 - .github/workflows/nightly-npm.yml | 56 - .github/workflows/publish-npm.yml | 950 - .github/workflows/release-github.yml | 257 - .github/workflows/release-npm-record.yml | 99 - .github/workflows/sweep-jobs.yml | 127 - .gitignore | 5668 +----- .husky/commit-msg | 2 - .husky/pre-commit | 51 - .husky/pre-push | 2 - .npmrc | 37 +- .oxfmtrc.json | 52 + .oxlintrc.json | 134 + AGENTS.md | 194 +- CLAUDE.md | 5 + package.json | 271 +- patches/fleet/brace-expansion@5.0.12.patch | 20 - patches/fleet/brace-expansion@5.0.9.patch | 29 - patches/fleet/minimatch@10.2.6.patch | 29 - patches/fleet/run-local-ci@0.18.1.patch | 38 - patches/fleet/vitest@5.0.0.patch | 42 - pnpm-lock.yaml | 9069 +--------- pnpm-workspace.yaml | 189 +- release-kit/README.md | 12 +- scripts/fleet/npm/native-artifact-files.mts | 71 - scripts/fleet/npm/native-artifacts.mts | 358 - scripts/fleet/npm/native/build.mts | 214 - scripts/fleet/npm/scan/receipt.mts | 260 - scripts/fleet/npm/scan/staged.mts | 339 - scripts/fleet/registry/napi-matrix.mts | 192 - scripts/fleet/registry/npm/scan/ndjson.mts | 141 - scripts/fleet/registry/npm/scan/run.mts | 964 - scripts/fleet/registry/npm/staged.mts | 804 - scripts/fleet/release/record/run.mts | 292 - .../setup/bootstrap/zero-dep-packages.mjs | 293 - scripts/fleet/setup/lib/check/sfw.mjs | 94 - scripts/fleet/setup/lib/error-message.mjs | 33 - scripts/fleet/setup/lib/install-tool.mjs | 265 - .../setup/lib/read-package-integrity.mjs | 95 - .../fleet/setup/lib/read-pinned-version.mjs | 118 - scripts/repo/_shared/paths.mts | 11 - scripts/repo/ai-lint-fix.mts | 15 - scripts/repo/ai-lint-fix/cli.mts | 479 - scripts/repo/ai-lint-fix/rule-guidance.mts | 206 - scripts/repo/audit-transcript.mts | 461 - scripts/repo/bootstrap/fetch-session.mts | 464 - scripts/repo/bootstrap/fleet.d.mts | 854 - scripts/repo/bootstrap/fleet.mjs | 14541 ---------------- scripts/repo/bootstrap/prepare.mts | 962 - scripts/repo/check-lock-step-header.mts | 5 +- scripts/repo/check-lock-step-refs.mts | 5 +- scripts/repo/check-paths.mts | 3 +- scripts/repo/check-paths/allowlist.mts | 104 - scripts/repo/check-paths/cli.mts | 15 +- scripts/repo/check-paths/scan-code.mts | 2 +- scripts/repo/check-paths/segments.mts | 61 + scripts/repo/check-prompt-less-setup.mts | 443 - scripts/repo/check-soak-exclude-dates.mts | 166 - scripts/repo/check.mts | 128 +- .../repo/check/release-kit-is-coherent.mts | 7 +- .../release-kit-launches-are-sanctioned.mts | 7 +- .../repo/check/release-kit-types-resolve.mts | 67 - .../release-kit-workflows-are-env-mapped.mts | 7 +- .../shipped-content-is-consumer-clean.mts | 2 +- scripts/repo/clean.mts | 182 - scripts/repo/constants/shipped-surfaces.mts | 3 + scripts/repo/fix.mts | 132 - scripts/repo/gen/glyph-types.mts | 29 + scripts/repo/gen/glyph.mts | 2 +- scripts/repo/generate-agents.mts | 7 +- scripts/repo/generate-cursor-plugin.mts | 13 +- .../repo/git-partial-submodule-commands.mts | 345 - .../repo/git-partial-submodule-internal.mts | 225 - scripts/repo/git-partial-submodule.mts | 157 - scripts/repo/helpers/parse-dependencies.mts | 378 - scripts/repo/helpers/search-usage.mts | 242 - scripts/repo/hooks/commit-msg.mts | 27 + scripts/repo/hooks/pre-commit.mts | 40 + scripts/repo/inline-shared.mts | 5 +- scripts/repo/install-claude-plugins.mts | 13 +- scripts/repo/install-git-hooks.mts | 8 +- scripts/repo/install-sfw.mts | 250 - scripts/repo/install-token-minifier.mts | 345 - scripts/repo/janus.mts | 131 - scripts/repo/lint-github-settings-detect.mts | 319 - .../repo/lint-github-settings-evaluate.mts | 363 - scripts/repo/lint-github-settings-types.mts | 153 - scripts/repo/lint-github-settings.mts | 315 - scripts/repo/lint.mts | 213 - scripts/repo/lockstep-emit-schema.mts | 3 +- scripts/repo/lockstep-schema.mts | 6 - scripts/repo/lockstep.mts | 3 +- scripts/repo/lockstep/cli.mts | 7 +- scripts/repo/lockstep/emit-schema.mts | 25 +- scripts/repo/paths.mts | 33 + scripts/repo/power-state.mts | 182 - scripts/repo/publish.mts | 7 +- scripts/repo/security.mts | 96 - .../repo/socket-wheelhouse-emit-schema.mts | 84 - scripts/repo/socket-wheelhouse-schema.mts | 347 - scripts/repo/sync-versions.mts | 10 +- scripts/repo/test.mts | 185 - scripts/repo/update.mts | 99 - scripts/repo/util/commit-message.mts | 47 + scripts/repo/util/main.mts | 53 + scripts/repo/util/scan.mts | 93 + scripts/repo/util/staged.mts | 23 + scripts/repo/validate-bundle-deps.mts | 487 - scripts/repo/validate-config-paths.mts | 157 - scripts/repo/validate-esbuild-minify.mts | 109 - scripts/repo/validate-file-count.mts | 132 - scripts/repo/validate-file-size.mts | 195 - scripts/repo/validate-no-link-deps.mts | 155 - scripts/repo/validate-rolldown-minify.mts | 233 - test/fleet/_shared/lib/isolate-home-env.mts | 148 - .../check-lock-step-header.test.mts | 2 +- .../integration/check-lock-step-refs.test.mts | 5 +- .../integration/install-git-hooks.test.mts | 26 +- test/repo/unit/commit-message.test.mts | 86 + .../paths-cli-positional-gate.test.mts | 2 +- test/repo/unit/repo-scan.test.mts | 181 + .../paths-doc-command-lint.test.mts | 2 +- .../paths-manifest-consistency.test.mts | 2 +- ...-release-kit-tests-mirror-payload.test.mts | 2 +- .../paths-skill-content-quality.test.mts | 2 +- .../structural/paths-skill-discovery.test.mts | 2 +- .../paths-skill-path-portability.test.mts | 2 +- test/repo/util/git-env.mts | 31 + tsconfig.json | 38 +- 285 files changed, 2262 insertions(+), 73241 deletions(-) delete mode 100644 .claude/output-styles/fleet.md delete mode 100644 .claude/skills/repo/updating-xport/SKILL.md delete mode 100644 .config/.markdownlint-cli2.jsonc delete mode 100644 .config/.prettierignore delete mode 100644 .config/esbuild/node-protocol.mts delete mode 100644 .config/esbuild/shorten-paths.mts delete mode 100644 .config/fleet/.prettierignore delete mode 100644 .config/fleet/oxlintrc.json delete mode 100644 .config/fleet/tsconfig.check.json delete mode 100644 .config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs delete mode 100644 .config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs delete mode 100644 .config/markdownlint-rules/socket-no-relative-sibling-script.mjs delete mode 100644 .config/markdownlint-rules/socket-readme-required-sections.mjs delete mode 100644 .config/oxfmtrc.json delete mode 100644 .config/oxlintrc.json delete mode 100644 .config/repo/.prettierignore delete mode 100644 .config/repo/external-tools.json delete mode 100644 .config/repo/rolldown/lib-stub.mts delete mode 100644 .config/repo/root-files.json delete mode 100644 .config/repo/socket-wheelhouse-schema.json delete mode 100644 .config/repo/socket-wheelhouse.json delete mode 100644 .config/repo/tsconfig.check.json delete mode 100644 .config/repo/vitest.settings.mts delete mode 100644 .config/sfw-bypass-list.txt delete mode 100644 .config/socket-registry-pins.json delete mode 100644 .config/taze.config.mts delete mode 100644 .config/tsconfig.base.json delete mode 100644 .config/vitest.config.mts delete mode 100644 .config/vitest.coverage.fleet.config.mts delete mode 100644 .git-hooks/_shared/canonical/bundle.mts delete mode 100644 .git-hooks/_shared/canonical/fork-scan.mts delete mode 100644 .git-hooks/_shared/canonical/git.mts delete mode 100644 .git-hooks/_shared/canonical/patch.mts delete mode 100644 .git-hooks/_shared/canonical/proof.mts delete mode 100644 .git-hooks/_shared/canonical/receipt.mts delete mode 100644 .git-hooks/_shared/canonical/replacements.mts delete mode 100644 .git-hooks/_shared/canonical/source.mts delete mode 100644 .git-hooks/_shared/check-output.mts delete mode 100644 .git-hooks/_shared/commit-format.mts delete mode 100644 .git-hooks/_shared/commit-subject.mts delete mode 100644 .git-hooks/_shared/cross-repo-expressions.mts delete mode 100644 .git-hooks/_shared/cross-repo.mts delete mode 100644 .git-hooks/_shared/external-issue-ref.mts delete mode 100644 .git-hooks/_shared/file-scan.mts delete mode 100644 .git-hooks/_shared/git-context-vars.mts delete mode 100644 .git-hooks/_shared/git-identity.mts delete mode 100644 .git-hooks/_shared/git.mts delete mode 100644 .git-hooks/_shared/isolate-git-env.mts delete mode 100644 .git-hooks/_shared/logger-leaks.mts delete mode 100644 .git-hooks/_shared/node-version.mts delete mode 100644 .git-hooks/_shared/personal-path.mts delete mode 100644 .git-hooks/_shared/pkg-script-target.mts delete mode 100644 .git-hooks/_shared/push/commit-messages.mts delete mode 100644 .git-hooks/_shared/push/durable-ref.mts delete mode 100644 .git-hooks/_shared/push/file-scan.mts delete mode 100644 .git-hooks/_shared/push/pr-commit-count.mts delete mode 100644 .git-hooks/_shared/push/range.mts delete mode 100644 .git-hooks/_shared/push/release-ledger.mts delete mode 100644 .git-hooks/_shared/push/release-tags.mts delete mode 100644 .git-hooks/_shared/push/repo-gates.mts delete mode 100644 .git-hooks/_shared/push/signatures.mts delete mode 100644 .git-hooks/_shared/push/squash-history.mts delete mode 100644 .git-hooks/_shared/repo-containment.mts delete mode 100644 .git-hooks/_shared/repository-source-root.mts delete mode 100644 .git-hooks/_shared/resolve-node.sh delete mode 100644 .git-hooks/_shared/run-step.sh delete mode 100644 .git-hooks/_shared/sanitize-token-env.sh delete mode 100644 .git-hooks/_shared/scan-code-refs.mts delete mode 100644 .git-hooks/_shared/scan-comments.mts delete mode 100644 .git-hooks/_shared/scan-commit-msg.mts delete mode 100644 .git-hooks/_shared/scan-core.mts delete mode 100644 .git-hooks/_shared/scan-package-conventions.mts delete mode 100644 .git-hooks/_shared/scan-secrets.mts delete mode 100644 .git-hooks/_shared/scan-supply-chain.mts delete mode 100644 .git-hooks/_shared/staged-gates.mts delete mode 100644 .git-hooks/_shared/typecheck-cache.mts delete mode 100644 .git-hooks/_shared/util.mts delete mode 100755 .git-hooks/fleet/commit-msg delete mode 100644 .git-hooks/fleet/commit-msg.mts delete mode 100755 .git-hooks/fleet/post-commit delete mode 100755 .git-hooks/fleet/pre-commit delete mode 100644 .git-hooks/fleet/pre-commit.mts delete mode 100755 .git-hooks/fleet/pre-merge-commit delete mode 100644 .git-hooks/fleet/pre-merge-commit.mts delete mode 100755 .git-hooks/fleet/pre-push delete mode 100644 .git-hooks/fleet/pre-push.mts delete mode 100755 .git-hooks/post-commit delete mode 100755 .git-hooks/pre-merge-commit delete mode 100644 .github/actions/fleet/_shared/codeql-languages.d.mts delete mode 100644 .github/actions/fleet/_shared/codeql-languages.mjs delete mode 100644 .github/actions/fleet/_shared/install-tool.d.mts delete mode 100644 .github/actions/fleet/_shared/install-tool.mjs delete mode 100644 .github/actions/fleet/_shared/install/atomic.d.mts delete mode 100644 .github/actions/fleet/_shared/install/atomic.mjs delete mode 100644 .github/actions/fleet/_shared/jq.d.mts delete mode 100644 .github/actions/fleet/_shared/jq.mjs delete mode 100644 .github/actions/fleet/_shared/platform-key.mjs delete mode 100644 .github/actions/fleet/_shared/platform.d.mts delete mode 100644 .github/actions/fleet/_shared/platform.mjs delete mode 100644 .github/actions/fleet/_shared/release-asset.mts delete mode 100644 .github/actions/fleet/_shared/resolve-external-tool-asset.generated.d.mts delete mode 100644 .github/actions/fleet/_shared/resolve-external-tool-asset.generated.mjs delete mode 100644 .github/actions/fleet/_shared/resolve-external-tool-asset.mts delete mode 100644 .github/actions/fleet/_shared/resolve-external-tool-platform.mts delete mode 100644 .github/actions/fleet/_shared/runner-images.json delete mode 100644 .github/actions/fleet/_shared/verify-integrity-provenance.d.mts delete mode 100644 .github/actions/fleet/_shared/verify-integrity-provenance.mjs delete mode 100644 .github/actions/fleet/cache-pnpm-store/action.yml delete mode 100644 .github/actions/fleet/cache-pnpm-store/ecosystems.mjs delete mode 100644 .github/actions/fleet/cache-pnpm-store/resolve-store-cache.d.mts delete mode 100644 .github/actions/fleet/cache-pnpm-store/resolve-store-cache.mjs delete mode 100644 .github/actions/fleet/checkout/action.yml delete mode 100644 .github/actions/fleet/debug/action.yml delete mode 100644 .github/actions/fleet/expose-actions-runtime/action.yml delete mode 100644 .github/actions/fleet/expose-actions-runtime/index.cjs delete mode 100644 .github/actions/fleet/github-ci-fix-app-token/action.yml delete mode 100644 .github/actions/fleet/github-ci-fix-app-token/mint-app-installation-token.mjs delete mode 100644 .github/actions/fleet/github-payload-app-token/action.yml delete mode 100644 .github/actions/fleet/github-payload-app-token/mint-app-installation-token.mjs delete mode 100644 .github/actions/fleet/github-pr-branch-app-token/action.yml delete mode 100644 .github/actions/fleet/github-pr-branch-app-token/mint-app-installation-token.mjs delete mode 100644 .github/actions/fleet/github-status-check/action.yml delete mode 100644 .github/actions/fleet/github-status-check/probe-github-status.d.mts delete mode 100644 .github/actions/fleet/github-status-check/probe-github-status.mjs delete mode 100644 .github/actions/fleet/install/action.yml delete mode 100644 .github/actions/fleet/install/verify-lib-floor.d.mts delete mode 100644 .github/actions/fleet/install/verify-lib-floor.mjs delete mode 100644 .github/actions/fleet/setup-and-install/action.yml delete mode 100644 .github/actions/fleet/setup-and-install/runner-resource-runtime.mts delete mode 100644 .github/actions/fleet/setup-and-install/runner-swap-file.mts delete mode 100644 .github/actions/fleet/setup-and-install/setup-runner-resources.mts delete mode 100644 .github/actions/fleet/setup/action.yml delete mode 100644 .github/actions/fleet/setup/bootstrap-pnpm.d.mts delete mode 100644 .github/actions/fleet/setup/bootstrap-pnpm.mjs delete mode 100644 .github/actions/fleet/setup/export-fleet-env.mjs delete mode 100644 .github/actions/fleet/setup/external-tools.generated.json delete mode 100644 .github/actions/fleet/setup/fleet-env.json delete mode 100644 .github/actions/fleet/setup/plan-setup-node.d.mts delete mode 100644 .github/actions/fleet/setup/plan-setup-node.mjs delete mode 100644 .github/actions/fleet/setup/plan-setup-tools.d.mts delete mode 100644 .github/actions/fleet/setup/plan-setup-tools.mjs delete mode 100644 .github/aw/actions-lock.json delete mode 100644 .github/workflows/ci-fix.yml delete mode 100644 .github/workflows/cron-weekly-fuzz.yml delete mode 100644 .github/workflows/cron-weekly-odai-cache.yml delete mode 100644 .github/workflows/cron-weekly-update.yml delete mode 100644 .github/workflows/nightly-npm.yml delete mode 100644 .github/workflows/publish-npm.yml delete mode 100644 .github/workflows/release-github.yml delete mode 100644 .github/workflows/release-npm-record.yml delete mode 100644 .github/workflows/sweep-jobs.yml delete mode 100755 .husky/commit-msg delete mode 100755 .husky/pre-commit delete mode 100755 .husky/pre-push create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 CLAUDE.md delete mode 100644 patches/fleet/brace-expansion@5.0.12.patch delete mode 100644 patches/fleet/brace-expansion@5.0.9.patch delete mode 100644 patches/fleet/minimatch@10.2.6.patch delete mode 100644 patches/fleet/run-local-ci@0.18.1.patch delete mode 100644 patches/fleet/vitest@5.0.0.patch delete mode 100644 scripts/fleet/npm/native-artifact-files.mts delete mode 100644 scripts/fleet/npm/native-artifacts.mts delete mode 100644 scripts/fleet/npm/native/build.mts delete mode 100644 scripts/fleet/npm/scan/receipt.mts delete mode 100644 scripts/fleet/npm/scan/staged.mts delete mode 100644 scripts/fleet/registry/napi-matrix.mts delete mode 100644 scripts/fleet/registry/npm/scan/ndjson.mts delete mode 100644 scripts/fleet/registry/npm/scan/run.mts delete mode 100644 scripts/fleet/registry/npm/staged.mts delete mode 100644 scripts/fleet/release/record/run.mts delete mode 100644 scripts/fleet/setup/bootstrap/zero-dep-packages.mjs delete mode 100644 scripts/fleet/setup/lib/check/sfw.mjs delete mode 100644 scripts/fleet/setup/lib/error-message.mjs delete mode 100644 scripts/fleet/setup/lib/install-tool.mjs delete mode 100644 scripts/fleet/setup/lib/read-package-integrity.mjs delete mode 100644 scripts/fleet/setup/lib/read-pinned-version.mjs delete mode 100644 scripts/repo/_shared/paths.mts delete mode 100644 scripts/repo/ai-lint-fix.mts delete mode 100644 scripts/repo/ai-lint-fix/cli.mts delete mode 100644 scripts/repo/ai-lint-fix/rule-guidance.mts delete mode 100644 scripts/repo/audit-transcript.mts delete mode 100644 scripts/repo/bootstrap/fetch-session.mts delete mode 100644 scripts/repo/bootstrap/fleet.d.mts delete mode 100644 scripts/repo/bootstrap/fleet.mjs delete mode 100644 scripts/repo/bootstrap/prepare.mts create mode 100644 scripts/repo/check-paths/segments.mts delete mode 100644 scripts/repo/check-prompt-less-setup.mts delete mode 100644 scripts/repo/check-soak-exclude-dates.mts delete mode 100644 scripts/repo/check/release-kit-types-resolve.mts delete mode 100644 scripts/repo/clean.mts delete mode 100644 scripts/repo/fix.mts create mode 100644 scripts/repo/gen/glyph-types.mts delete mode 100644 scripts/repo/git-partial-submodule-commands.mts delete mode 100644 scripts/repo/git-partial-submodule-internal.mts delete mode 100644 scripts/repo/git-partial-submodule.mts delete mode 100644 scripts/repo/helpers/parse-dependencies.mts delete mode 100644 scripts/repo/helpers/search-usage.mts create mode 100644 scripts/repo/hooks/commit-msg.mts create mode 100644 scripts/repo/hooks/pre-commit.mts delete mode 100644 scripts/repo/install-sfw.mts delete mode 100644 scripts/repo/install-token-minifier.mts delete mode 100644 scripts/repo/janus.mts delete mode 100644 scripts/repo/lint-github-settings-detect.mts delete mode 100644 scripts/repo/lint-github-settings-evaluate.mts delete mode 100644 scripts/repo/lint-github-settings-types.mts delete mode 100644 scripts/repo/lint-github-settings.mts delete mode 100644 scripts/repo/lint.mts delete mode 100644 scripts/repo/lockstep-schema.mts create mode 100644 scripts/repo/paths.mts delete mode 100644 scripts/repo/power-state.mts delete mode 100644 scripts/repo/security.mts delete mode 100644 scripts/repo/socket-wheelhouse-emit-schema.mts delete mode 100644 scripts/repo/socket-wheelhouse-schema.mts delete mode 100644 scripts/repo/test.mts delete mode 100644 scripts/repo/update.mts create mode 100644 scripts/repo/util/commit-message.mts create mode 100644 scripts/repo/util/main.mts create mode 100644 scripts/repo/util/scan.mts create mode 100644 scripts/repo/util/staged.mts delete mode 100644 scripts/repo/validate-bundle-deps.mts delete mode 100644 scripts/repo/validate-config-paths.mts delete mode 100644 scripts/repo/validate-esbuild-minify.mts delete mode 100644 scripts/repo/validate-file-count.mts delete mode 100644 scripts/repo/validate-file-size.mts delete mode 100755 scripts/repo/validate-no-link-deps.mts delete mode 100644 scripts/repo/validate-rolldown-minify.mts delete mode 100644 test/fleet/_shared/lib/isolate-home-env.mts create mode 100644 test/repo/unit/commit-message.test.mts create mode 100644 test/repo/unit/repo-scan.test.mts create mode 100644 test/repo/util/git-env.mts diff --git a/.claude/output-styles/fleet.md b/.claude/output-styles/fleet.md deleted file mode 100644 index cce8d257..00000000 --- a/.claude/output-styles/fleet.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Fleet -description: Socket fleet prose doctrine and STE reporting rules -keep-coding-instructions: true ---- - -Read and apply `docs/fleet/agents.md/prose-style-and-doctrine.md` and `docs/fleet/agents.md/reporting-in-ste100.md` for every reply. -The doctrine owns writing policy. STE controls when writing guidance conflicts. -Use the prose skill to edit drafts. Do not duplicate its policy in this output style. -Read the default reply budgets from `scripts/fleet/prose/profiles.mts`. Preserve explicitly requested detail and material facts. diff --git a/.claude/settings.json b/.claude/settings.json index 94c7e30e..9be4e023 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,9 +1,5 @@ { - "// ": "Managed by socket-wheelhouse; edit the template, then cascade.", - "// auth": "No apiKeyHelper. Claude Code spawns that runner with neither env nor cwd, so it could never read AI_BALANCER_ENABLED, and a helper that returns nothing renders as 'apiKeyHelper failed: did not return a value' on every launch. The balancer route instead exports ANTHROPIC_API_KEY into CLAUDE_ENV_FILE from the ai-balancer-proxy-start SessionStart hook, which DOES receive the env and so can gate on the flag. Nothing persists in settings, so the claude.ai login is the default and needs no undo.", - "// env": "The fleet no-phone-home knobs, one copy per delivery surface. FLEET_ENV (.github/actions/fleet/setup/fleet-env.json) is the source; the shell-rc bridge and the CI workflow env already derive from it, and this block is the third surface — a Claude Code session and every tool it spawns. Kept in lockstep by agent/settings-env-matches-fleet-env.mts, so a knob added there fails this file until it is added here too.", "env": { - "AI_BALANCER_ENABLED": "1", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1", "COREPACK_ENABLE_PROJECT_SPEC": "0", "DISABLE_TELEMETRY": "1", @@ -12,67 +8,6 @@ "OTEL_SDK_DISABLED": "true", "SFW_TELEMETRY_DISABLED": "true" }, - "hooks": { - "PostToolUse": [ - { - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs PostToolUse" - } - ] - } - ], - "PreToolUse": [ - { - "matcher": ".*", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs PreToolUse" - } - ] - } - ], - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/scripts/repo/bootstrap/fetch-session.mts" - } - ] - }, - { - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs SessionStart" - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs Stop" - } - ] - } - ], - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fleet/index.cjs UserPromptSubmit" - } - ] - } - ] - }, "permissions": { "allow": [ "Bash(gh release create:*)", @@ -88,11 +23,5 @@ "Bash(git push -f:*)", "Bash(git push --force-with-lease:*)" ] - }, - "// statusLine": "Fail-soft on purpose. scripts/fleet is FETCHED from the release bundle, never tracked, so the script is absent until a fetch has run. Measured without the -f test: the statusline exited 1 on module-not-found, once per render. The test makes an absent script render nothing instead.", - "statusLine": { - "type": "command", - "command": "bin=\"$CLAUDE_PROJECT_DIR/.claude/hooks/fleet/_dist/statusline/statusline-render-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')\" && [ -x \"$bin\" ] && exec \"$bin\" || script=\"$CLAUDE_PROJECT_DIR/scripts/fleet/spend-statusline.mts\" && [ -f \"$script\" ] && node \"$script\" || true" - }, - "// ": "Repository-owned Claude settings belong below this marker." + } } diff --git a/.claude/skills/repo/updating-xport/SKILL.md b/.claude/skills/repo/updating-xport/SKILL.md deleted file mode 100644 index 45c18da9..00000000 --- a/.claude/skills/repo/updating-xport/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: updating-xport -description: "Act on `xport.json` lock-step drift: auto-bump `version-pin` rows; the other row kinds (file-fork, feature-parity, spec-conformance, lang-parity) are advisory only." -user-invocable: true -allowed-tools: Bash(pnpm:*), Bash(npm:*), Bash(git:*), Bash(node:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Bash(ls:*), Bash(cat:*), Bash(head:*), Bash(tail:*), Bash(wc:*), Bash(diff:*), Read, Edit, Grep, Glob -metadata: - internal: true ---- - -# updating-xport - - -Act on drift findings in `xport.json`. Auto-apply mechanical version-pin bumps; surface everything else as advisory notes for human review. Commit each actioned row as its own atomic commit so the PR reviewer can accept/reject per-row. - - - -**xport** is a cross-project lock-step manifest. Not every repo has one; this skill exits cleanly when `xport.json` is absent. See `xport.schema.json` (deployed via `socket-wheelhouse/sync-scaffolding.mjs`) for the five row kinds. - -The harness at `scripts/xport.mts` emits JSON reports with `severity ∈ {ok, drift, error}` per row. This skill consumes that JSON. - -**Per-kind action policy:** - -| Kind | Drift signal | Action | -|------|--------------|--------| -| `version-pin` | Upstream commits on default ref since pinned SHA | **Auto-bump** per `upgrade_policy`: `track-latest` → advance to latest stable tag; `major-gate` → advance patch/minor only; `locked` → advisory only | -| `file-fork` | Upstream file changed since `forked_at_sha` | **Advisory** - note in PR body; do NOT auto-merge (forks carry local deltas that need human review) | -| `feature-parity` | Parity score below `criticality/10` floor | **Advisory** - note in PR body; human decides implement vs downgrade criticality | -| `spec-conformance` | Spec submodule moved | **Advisory** - note in PR body; human decides whether to bump `spec_version` | -| `lang-parity` | Port divergence / `rejected` anti-pattern reintroduced | **Advisory** - note in PR body; humans fix the port or update the manifest | - -The common rule: **version-pin is mechanical** (safe to auto-apply with `track-latest`/`major-gate` policies); everything else is **advisory** (upstream semantics and local deltas matter, humans decide). - - - -**Requirements:** -- Start with clean working directory (check via `git status --porcelain`) -- Run from repo root -- Exit 0 cleanly if `xport.json` is absent - the repo doesn't use xport -- Conventional commit format: `chore(deps): bump to ` -- Update `.gitmodules` version comments when submodule tags change (pattern: `# -` on the line above the submodule block) -- Target stable releases only (filter `-rc`, `-alpha`, `-beta`, `-dev`, `-snapshot`, `-nightly`, `-preview`) - -**Forbidden:** -- Never auto-edit `file-fork`, `feature-parity`, `spec-conformance`, or `lang-parity` rows' tracked state -- Never bump a `locked` version-pin without human approval -- Never skip the tag-stability filter -- Never use `npx`, `pnpm dlx`, `yarn dlx` - use `pnpm exec` or `pnpm run` - -**CI mode** (`CI=true` or `GITHUB_ACTIONS`): skip per-row test validation (workflow validates at the end); emit advisory summary to `$GITHUB_OUTPUT` when present. - -**Interactive mode** (default): validate each auto-bump with `pnpm test` before committing the next. - - - - -## Phase 1 - Pre-flight - -```bash -test -f xport.json || { echo "no xport.json; skill n/a"; exit 0; } -test -f xport.schema.json || { echo "xport.schema.json missing — malformed scaffolding"; exit 1; } -test -f scripts/xport.mts || { echo "scripts/xport.mts missing — malformed scaffolding"; exit 1; } - -git status --porcelain | grep -v '^??' && { echo "dirty tree; aborting"; exit 1; } || true - -[ "$CI" = "true" ] || [ -n "$GITHUB_ACTIONS" ] && CI_MODE=true || CI_MODE=false -``` - -## Phase 2 - Collect drift - -```bash -pnpm run lockstep --json > /tmp/xport-report.json -``` - -Parse `reports[]` from the JSON. Split into: - -- **auto** - rows where `severity == "drift"` AND `kind == "version-pin"` AND `upgrade_policy` ∈ `{ "track-latest", "major-gate" }` -- **advisory** - everything else with `severity != "ok"` - -If both lists empty: exit 0 with "no xport drift". - -## Phase 3 - Auto-bump version-pin rows - -For each row in **auto** list, in manifest declaration order: - -
Steps 3a-3e: resolve, find the tag, check out, update the pins, validate + commit - -**3a. Resolve the upstream submodule + fetch tags** - -```bash -SUBMODULE=$(jq -r --arg a "$UPSTREAM_ALIAS" '.upstreams[$a].submodule' xport.json) -cd "$SUBMODULE" -git fetch origin --tags --quiet -OLD_SHA=$(git rev-parse HEAD) -``` - -**3b. Find the target tag** - -Examine existing `pinned_tag` to identify the tag scheme, then match: - -- `v1.2.3` (v-prefixed semver) -- `1.2.3` (bare semver) -- `-1.2.3` (project-prefixed) -- `_1_2_3` (underscore style; curl, liburing) - -For `major-gate` policy: parse major version from `LATEST` vs current `pinned_tag`. If majors differ, skip - add to advisory with note "major bump needs human review". - -**3c. Check out + capture new SHA** - -```bash -NEW_SHA_FOR_CHECK=$(git rev-parse "$LATEST") -[ "$OLD_SHA" = "$NEW_SHA_FOR_CHECK" ] && { cd -; continue; } -git checkout "$LATEST" --quiet -NEW_SHA=$(git rev-parse HEAD) -cd - -``` - -**3d. Update `xport.json` + `.gitmodules`** - -Use `jq` for structured edit: - -```bash -jq --arg id "$ROW_ID" --arg sha "$NEW_SHA" --arg tag "$LATEST" \ - '(.rows[] | select(.id == $id) | .pinned_sha) = $sha - | (.rows[] | select(.id == $id) | .pinned_tag) = $tag' \ - xport.json > xport.json.tmp && mv xport.json.tmp xport.json -``` - -Update `.gitmodules` version comment via Edit tool (NOT sed per CLAUDE.md) - replace `# -` with `# -` on the comment line above the submodule block. - -**3e. Validate + commit** - -```bash -# Confirm xport harness accepts the new state -pnpm run lockstep --json > /tmp/xport-post.json -jq --arg id "$ROW_ID" '.reports[] | select(.id == $id) | .severity' /tmp/xport-post.json -# expect "ok" - -if [ "$CI_MODE" = "false" ]; then - pnpm test || { - echo "tests failed; rolling back $ROW_ID" - git checkout xport.json .gitmodules "$SUBMODULE" - continue - } -fi - -git add xport.json .gitmodules "$SUBMODULE" -git commit -m "chore(deps): bump $UPSTREAM_ALIAS to $LATEST" -``` - -Record bumped row in summary accumulator. - -
- -## Phase 4 - Compose advisory notes - -For each row in **advisory**, accumulate a markdown line: - -``` -- **file-fork** ``: `` — upstream commit(s) since . Review diff, cherry-pick if applicable, bump forked_at_sha. -- **feature-parity** ``: parity score below floor . Implement or downgrade criticality with reason. -- **spec-conformance** ``: upstream spec repo moved. Review for breaking changes before bumping spec_version. -- **lang-parity** ``:
. -- **version-pin** ``: major bump to — policy=major-gate requires human review. -- **version-pin** ``: upgrade_policy=locked — skipped. -``` - -## Phase 5 - Report + emit - -Final human-readable report to stdout: - -
The report shape, the CI advisory output, and the HANDOFF block - -``` -## updating-xport report - -**Auto-bumped:** row(s) - - -**Advisory (human review):** row(s) - -``` - -In CI mode, emit the advisory block to `$GITHUB_OUTPUT` (base64-encoded) under key `xport-advisory` so the weekly-update workflow can include it in the PR body: - -```bash -if [ -n "$GITHUB_OUTPUT" ]; then - echo "xport-advisory=$(printf '%s' "$ADVISORY" | base64 | tr -d '\n')" >> "$GITHUB_OUTPUT" -fi -``` - -Emit a HANDOFF block per `_shared/report-format.md`: - -``` -=== HANDOFF: updating-xport === -Status: {pass|fail} -Findings: {auto_bumped: N, advisory: M} -Summary: {one-line description} -=== END HANDOFF === -``` - -
- - - -## Success Criteria - -- All actionable `version-pin` rows bumped atomically (one commit per row) -- Advisory rows collected for PR body / workflow output -- No edits to non-version-pin row state -- `pnpm run lockstep` exits 0 or 2 at end (never 1 - no schema errors introduced) -- `.gitmodules` version comments synchronized with `pinned_tag` - -## Commands - -- `pnpm run lockstep --json` - drift report (consumed by this skill) -- `jq` - parse + edit `xport.json` (structured JSON edits) -- `git submodule status` - verify submodule state after bumps - -## When to use - -- Invoked by the `updating` umbrella skill (weekly-update workflow) -- Standalone: `/updating-xport` when syncing just the xport manifest -- After manual submodule bumps, to refresh `xport.json` metadata diff --git a/.config/.markdownlint-cli2.jsonc b/.config/.markdownlint-cli2.jsonc deleted file mode 100644 index c681a7c9..00000000 --- a/.config/.markdownlint-cli2.jsonc +++ /dev/null @@ -1,51 +0,0 @@ -// markdownlint-cli2 configuration for fleet repos. -// -// Loaded by `pnpm run lint` (template/scripts/lint.mts invokes -// markdownlint-cli2 with `-c .config/.markdownlint-cli2.jsonc`). -// -// Two concerns: -// 1. Stock markdownlint rules — disable a handful that bite real prose -// without adding value, leave the rest at defaults. -// 2. Fleet-canonical custom rules under markdownlint-rules/ — these -// enforce the fleet's README hygiene contract (no private-repo -// mentions, no relative-path commands, README skeleton structure). -{ - "config": { - "default": true, - // MD013 (line-length) bites code-block-heavy READMEs without warning. - // Disabled fleet-wide; reviewers catch genuinely-too-long prose. - "MD013": false, - // MD033 (no inline HTML) bites our
collapsed Development - // sections and Socket-badge img tags. Allow specific tags only. - "MD033": { "allowed_elements": ["details", "summary", "img", "br"] }, - // MD041 (first-line-h1) is the contract; keep it on. - // MD024 (no-duplicate-heading) — siblings-only mode so that ### subsections - // can repeat under different ## parents (common in API docs). - "MD024": { "siblings_only": true }, - }, - // Globs: every *.md / *.mdx under the repo, except generated output, - // node_modules, vendored upstream trees, and CHANGELOG.md (auto-generated). - "globs": ["**/*.md", "**/*.mdx"], - "ignores": [ - "**/node_modules/**", - "**/dist/**", - "**/build/**", - "**/coverage/**", - "**/vendor/**", - "**/upstream/**", - "**/third_party/**", - "**/CHANGELOG.md", - // .claude/ markdown is internal scaffolding (hook READMEs, agent - // role files, skill SKILL.mds, command reminders) — scoped docs - // with their own shape conventions. Excluded from the public- - // facing markdown lint surface. - "**/.claude/**", - ], - // Custom rule plugins live under markdownlint-rules/, byte-identical - // across the fleet via sync-scaffolding manifest registration. - "customRules": [ - "./markdownlint-rules/socket-no-private-wheelhouse-leak.mjs", - "./markdownlint-rules/socket-no-relative-sibling-script.mjs", - "./markdownlint-rules/socket-readme-required-sections.mjs", - ], -} diff --git a/.config/.prettierignore b/.config/.prettierignore deleted file mode 100644 index 6cbe4c69..00000000 --- a/.config/.prettierignore +++ /dev/null @@ -1,47 +0,0 @@ -# Format-ignore (but track) — files we keep byte-identical with their -# upstream / vendored source. oxfmt reads this file when pointed at it -# via `--ignore-path .config/.prettierignore` (default is the CWD's -# .gitignore + .prettierignore). The lint runner threads the flag in -# so the convention works from any working directory. -# -# `.claude/` is treated like node_modules — never formatted, never -# linted, no matter whether the files are git-tracked. Hooks, skills, -# vendored AST tooling, settings — all opaque to the formatter. -**/.claude/** -.claude/** - -# Vendored acorn.wasm binary blob + the wasm-bindgen CJS glue. The -# glue (`acorn-bindgen.cjs`) is wasm-bindgen output we ship verbatim -# (after a single string rewrite of the wasm filename). It must NOT -# be touched by oxfmt or oxlint --fix: the -# `socket/export-top-level-functions` autofix rewrites internal -# helpers like `function getObject(idx) { ... }` into -# `export function getObject(idx) { ... }`, turning the CJS module -# into syntactically-ESM. The first `require()` then fails with -# `SyntaxError: Unexpected token 'export'`. Past incident: cascaded -# to two fleet repos before the break surfaced. -# -# The generators we DO own (`acorn-wasm-sync.{mts,cts}`, -# `acorn-wasm-embed.{mts,cts}`) are not listed here on purpose — -# the ultrathink build emits them already formatted+linted per fleet -# rules so they participate in the regular lint pass like any other -# JS source. Only the raw wasm blob + the bindgen glue skip the -# formatter. Marked `binary` in .gitattributes for the wasm blob too -# so PR diffs collapse. -template/.claude/hooks/_shared/acorn/acorn.wasm -template/.claude/hooks/_shared/acorn/acorn-bindgen.cjs -.claude/hooks/_shared/acorn/acorn-bindgen.cjs - -# Vendored / upstream trees — kept byte-identical with their source -# of truth. Per CLAUDE.md "Untracked-by-default for vendored / build- -# copied trees": these are someone else's source, not ours, and the -# formatter would happily rewrite (e.g.) an upstream HTML test -# fixture or shipped third-party JS into our local style. -**/upstream/** -upstream/** -**/vendor/** -vendor/** -**/third_party/** -third_party/** -**/external/** -external/** diff --git a/.config/esbuild/node-protocol.mts b/.config/esbuild/node-protocol.mts deleted file mode 100644 index ad41bf27..00000000 --- a/.config/esbuild/node-protocol.mts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file Esbuild plugin: rewrite bare Node.js builtin imports to - * use the `node:` protocol prefix and mark them external. Catches the - * shape where a dependency does `require('fs')` instead of - * `require('node:fs')` and stops it from leaking into the bundle as a - * bundled module reference. - * Source: lifted from socket-sdk-js. Fleet-canonical via socket-wheelhouse. - */ - -import Module from 'node:module' - -import type { OnResolveArgs, PluginBuild } from 'esbuild' - -export function createNodeProtocolPlugin() { - return { - __proto__: null, - name: 'node-protocol', - setup(build: PluginBuild) { - const builtins = Module.builtinModules - for (let i = 0, { length } = builtins; i < length; i += 1) { - const builtin = builtins[i] - // Skip builtins that already carry the node: prefix. - if (builtin.startsWith('node:')) { - continue - } - // Escape regex special chars in the module name before composing - // a bare-anchor filter regex (`^name$`). - const escapedBuiltin = builtin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - build.onResolve( - { filter: new RegExp(`^${escapedBuiltin}$`) }, - (_args: OnResolveArgs) => ({ - __proto__: null, - path: `node:${builtin}`, - external: true, - }), - ) - } - }, - } -} diff --git a/.config/esbuild/shorten-paths.mts b/.config/esbuild/shorten-paths.mts deleted file mode 100644 index 36dbe2c2..00000000 --- a/.config/esbuild/shorten-paths.mts +++ /dev/null @@ -1,189 +0,0 @@ -// Nested cached-length for-loops intentionally reuse `i`/`length` names for -// the fleet-wide cached-loop idiom; renaming would diverge from the codebase -// pattern. -/* eslint-disable no-shadow -- cached-loop idiom */ -/** - * @file Esbuild plugin: shorten pnpm `node_modules/.pnpm/...` - * paths in bundled output (comments + string literals) to plain - * `package/subpath` form. Detects collisions and falls back to the - * original path when two long paths would collapse to the same short - * form. - * Uses @babel/parser + magic-string for AST-precise rewrites — string - * replacement would corrupt JS in edge cases (paths inside template - * literals, JSDoc, etc.). - * Source: lifted from socket-sdk-js. Fleet-canonical via socket-wheelhouse. - */ - -import { promises as fs } from 'node:fs' - -import type { Comment } from '@babel/types' - -import { parse } from '@babel/parser' -import MagicString from 'magic-string' - -import type { BuildResult, PluginBuild } from 'esbuild' - -import { errorMessage } from '@socketsecurity/lib/errors/message' -import { isObject } from '@socketsecurity/lib/objects/predicates' -import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { NODE_MODULES } from '@socketsecurity/lib/paths/dirnames' - -const logger = getDefaultLogger() - -export function createPathShorteningPlugin() { - return { - __proto__: null, - name: 'shorten-module-paths', - setup(build: PluginBuild) { - build.onEnd(async (result: BuildResult) => { - if (!result.outputFiles && result.metafile) { - const outputs = Object.keys(result.metafile.outputs).filter( - f => f.endsWith('.js') || f.endsWith('.mjs'), - ) - - for (let i = 0, { length } = outputs; i < length; i += 1) { - const outputPath = outputs[i] - // eslint-disable-next-line no-await-in-loop - const content = await fs.readFile(outputPath, 'utf8') - const magicString = new MagicString(content) - - const pathMap = new Map() - const conflictDetector = new Map() - - // eslint-disable-next-line unicorn/consistent-function-scoping - const shortenPath = (longPath: string): string => { - if (pathMap.has(longPath)) { - return pathMap.get(longPath)! - } - - let shortPath = longPath - - // pnpm scoped packages: .pnpm/@scope+pkg@version/node_modules/@scope/pkg/subpath - const scopedPnpmMatch = longPath.match( - /node_modules\/\.pnpm\/@([^+/]+)\+([^@/]+)@[^/]+\/node_modules\/(@[^/]+\/[^/]+)\/(.+)/, - ) - if (scopedPnpmMatch) { - const [, _scope, _pkg, packageName, subpath] = scopedPnpmMatch - shortPath = `${packageName}/${subpath}` - } else { - // pnpm non-scoped packages: .pnpm/pkg@version/node_modules/pkg/subpath - const pnpmMatch = longPath.match( - /node_modules\/\.pnpm\/([^@/]+)@[^/]+\/node_modules\/([^/]+)\/(.+)/, - ) - if (pnpmMatch) { - const [, _pkgName, packageName, subpath] = pnpmMatch - shortPath = `${packageName}/${subpath}` - } - } - - // Two distinct long paths must not collapse to the same short - // path — that would corrupt the bundle. On collision, keep - // the original. - if (conflictDetector.has(shortPath)) { - const existingPath = conflictDetector.get(shortPath) - if (existingPath !== longPath) { - logger.warn(`Path conflict detected:`) - logger.warn(` "${shortPath}"`) - logger.warn(` Maps to: "${existingPath}"`) - logger.warn(` Also from: "${longPath}"`) - logger.warn(` Keeping original paths to avoid conflict.`) - shortPath = longPath - } - } else { - conflictDetector.set(shortPath, longPath) - } - - pathMap.set(longPath, shortPath) - return shortPath - } - - try { - const ast = parse(content, { - sourceType: 'module', - plugins: [], - }) - - // Babel's own parse result; Comment[] is the declared shape of - // `ast.comments` modulo the null the `||` already strips. - // eslint-disable-next-line typescript/no-unsafe-type-assertion -- see above - const comments = (ast.comments || []) as Comment[] - for (let ci = 0, { length } = comments; ci < length; ci += 1) { - const comment = comments[ci] - if ( - comment.type === 'CommentLine' && - comment.value.includes(NODE_MODULES) - ) { - const originalPath = comment.value.trim() - const shortPath = shortenPath(originalPath) - - if ( - shortPath !== originalPath && - comment.start != null && - comment.end != null - ) { - magicString.overwrite( - comment.start, - comment.end, - `// ${shortPath}`, - ) - } - } - } - - function walk(node: unknown): void { - if (!isObject(node)) { - return - } - const n = node - - if ( - n['type'] === 'StringLiteral' && - typeof n['value'] === 'string' && - n['value'].includes(NODE_MODULES) - ) { - const originalPath = n['value'] - const shortPath = shortenPath(originalPath) - const start = n['start'] - const end = n['end'] - - if ( - shortPath !== originalPath && - typeof start === 'number' && - typeof end === 'number' - ) { - magicString.overwrite(start + 1, end - 1, shortPath) - } - } - - const keys = Object.keys(n) - for (let ki = 0, { length } = keys; ki < length; ki += 1) { - const key = keys[ki] - if (key === 'end' || key === 'loc' || key === 'start') { - continue - } - const value = n[key] - if (Array.isArray(value)) { - for (let i = 0, { length } = value; i < length; i += 1) { - const item = value[i] - walk(item) - } - } else { - walk(value) - } - } - } - - walk(ast.program as unknown) - // eslint-disable-next-line no-await-in-loop - await fs.writeFile(outputPath, magicString.toString(), 'utf8') - } catch (e) { - logger.error( - `Failed to shorten paths in ${outputPath}: ${errorMessage(e)}`, - ) - } - } - } - }) - }, - } -} diff --git a/.config/fleet/.prettierignore b/.config/fleet/.prettierignore deleted file mode 100644 index e80a039a..00000000 --- a/.config/fleet/.prettierignore +++ /dev/null @@ -1,133 +0,0 @@ -**/build/** -!**/scripts/fleet/build/**/ -!**/scripts/fleet/build/**/*.mts -!**/scripts/repo/build/**/ -!**/scripts/repo/build/**/*.mts -**/build/**/build/** -**/build/**/*.d.mts - -# Format-ignore (but track) — files we keep byte-identical with their upstream / -# vendored / generated source. oxfmt reads this file via -# `--ignore-path .config/fleet/.prettierignore`; the lint runner threads the flag -# so the convention works from any working directory. -# -# EVERY pattern here is `**/`-anchored. oxfmt builds the matcher with -# `Gitignore::new(path)` (oxc apps/oxfmt/src/cli/resolve.rs), which roots it at -# THIS file's directory (.config/fleet/) — so a bare slashed pattern like -# `upstream/**` anchors to `.config/fleet/upstream/**` and silently matches nothing. -# A leading `**/` matches at any depth. Enforced by -# scripts/fleet/check/prettierignore-globs-are-anchored.mts. -# -# `.claude/` is treated like node_modules — never formatted, never linted, no -# matter whether the files are git-tracked. Hooks, skills, settings, and the -# acorn AST helper (`_shared/ast/`, a thin npm-backed wrapper over the -# `@ultrathink/acorn.rs.wasm` wasm parser) are all opaque to the formatter under -# this single exclusion. -**/.claude/** - -# `.agents/skills/` is the GENERATED cross-tool skill mirror -# (gen/agents-skills-mirror.mts flattens .claude/skills/{fleet,repo}// to -# .agents/skills/-/ for Codex + OpenCode). Generated output whose -# source (.claude/, above) is itself unformatted-by-design, so the byte-identical -# mirror is ignored too. agents-skills-mirror-is-current keeps it in sync. -**/.agents/** - -# Tooling mirrors are formatted at their canonical template source. -# Test ownership does not imply distribution. The formatter reads distributed -# test paths from the generated fleet ignore block in oxlintrc.json. -**/.config/fleet/** -**/.github/actions/fleet/** -**/.git-hooks/fleet/** -**/assets/fleet/** -**/docs/fleet/** -**/scripts/fleet/** - -# The rolldown-inlined dep-0 fetcher — a build artifact (from template/bootstrap/ -# src/* via scripts/repo/gen/bootstrap.mts), not hand-written source; oxfmt would -# fight the bundler output. The template/bootstrap/src/* modules ARE formatted. -# The .d.mts sibling is the generator's rolldown-plugin-dts declaration emit — -# same contract. -**/bootstrap/fleet.mjs -**/bootstrap/fleet.d.mts -# The SessionStart fetch-kernel's declaration emit, from the same generator. - -# The rolldown-bundled fleet oxlint plugin (scripts/fleet/build-oxlint-bundle.mts -# output) — generated + gitignored; oxfmt would fight the bundler output. The -# `.config/fleet/**` glob above does not reach it (the whole-tree walk's matcher skips -# the dotdir `.config`), so it needs an explicit line — twin of isNeverGated() in -# scripts/fleet/_shared/format-scope.mts and the oxfmtrc.json ignorePatterns. -# NB: this matcher is rooted at THIS file's dir (.config/fleet/), so the pattern -# is anchored to the BASENAME — a `**/.config/fleet/oxlint-plugin.generated.mjs` form is a -# silent no-op here (the file's path relative to the matcher root is just -# `oxlint-plugin.generated.mjs`). The `.gitignore` twin keeps the repo-root-anchored -# `**/.config/fleet/oxlint-plugin.generated.mjs` form because THAT matcher roots at the -# repo root. -**/oxlint-plugin.generated.mjs - -# The ownership registry, generated by emit-ownership.mts — twin of -# isNeverGated() in scripts/fleet/_shared/format-scope.mts. Its JSON output -# differs from oxfmt's (sorted keys, no trailing spaces), so a plain format -# run dirties it from a clean tree every time. The generator owns the bytes. -**/commit-cascade/manifest/fleet-files.json - -# The wheelhouse dogfood oxlint config, generated by -# scripts/repo/gen/dogfood-oxlint-config.mts, which serializes canonical -# 2-space JSON and holds its own staleness check. Same split as the registry -# above: isNeverGated() already skips it on a scoped run, so only oxfmt's own -# --all walk reached it, and the two disagreed the moment the generator emitted -# a long array. The generator owns the bytes. -**/.config/repo/oxlintrc.dogfood.json - -# The repo-tier external-tools pin file, written by -# scripts/fleet/update/external-tools.mts on every soak-plan apply. Same split -# again: the updater serializes it, so formatting it from a clean tree only -# dirties what the next apply rewrites. -**/.config/repo/external-tools.json - -# The per-repo config JSON Schema, emitted from the TypeBox source by -# scripts/fleet/socket-wheelhouse-emit-schema.mts. Same split as its two -# siblings above: the explicit-path formatter already skips it, so leaving it -# in the --all walk let the two disagree the moment a description grew long -# enough to rewrap. The emitter owns the bytes. -**/.config/repo/socket-wheelhouse-schema.json - -# Generated build/test output — machine-written trees whose generators own the -# bytes; formatting them is churn against the next build. These are the -# GENERATED_GLOBS twins (scripts/fleet/constants/generated-globs.mts is the -# single source; scripts/fleet/check/generated-globs-are-consistent.mts asserts -# this file covers every entry). -**/dist/** -**/out/** -**/target/** -**/test/fixtures/** - -# Vendored / upstream trees — kept byte-identical with their source of truth. Per -# CLAUDE.md "untracked-by-default for vendored / build-copied trees": someone -# else's source, not ours; the formatter would rewrite it into our local style. -# Fixture trees are test data, including conformance corpora and tiered fleet -# fixtures. Their harness or owning sync controls their byte representation. -**/fixtures/** -**/upstream/** - -# gh-aw generated workflows. `gh aw compile` turns a `.md` agentic workflow -# into a hardened `.lock.yml`; that artifact is tool-owned and must stay -# byte-identical to the compiler output (the .md is the source of truth). -**/.github/workflows/*.lock.yml - -# Everything from the top of this file through the end sentinel below is -# fleet-canonical: cascade and bundle placement rewrite that whole span from -# the wheelhouse source on every refresh — see -# scripts/fleet/_shared/fleet-canonical-splice.mts. Member-generated content, -# like the lockstep-mirrors block scripts/fleet/lockstep/emit-mirror-globs.mts -# appends, must live BELOW the end sentinel; placement preserves that tail -# byte-for-byte. - -**/*.generated.cjs -**/*.generated.cts -**/*.generated.js -**/*.generated.json -**/*.generated.mjs -**/*.generated.mts -**/*.generated.ts -**/template/generated/** -#fleet-canonical-end diff --git a/.config/fleet/oxlintrc.json b/.config/fleet/oxlintrc.json deleted file mode 100644 index a902c842..00000000 --- a/.config/fleet/oxlintrc.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/5306f24d9e82ae36ad9c3c964f33075bc589c799/npm/oxlint/configuration_schema.json", - "plugins": ["typescript", "unicorn", "import"], - "jsPlugins": ["./oxlint-plugin.generated.mjs"], - "categories": { - "correctness": "error", - "suspicious": "error" - }, - "rules": { - "eslint/complexity": ["error", 15], - "eslint/curly": "error", - "eslint/no-await-in-loop": "off", - "eslint/no-console": "off", - "eslint/no-control-regex": "off", - "eslint/no-empty": [ - "error", - { - "allowEmptyCatch": true - } - ], - "eslint/no-new": "error", - "eslint/no-proto": "error", - "eslint/no-shadow": "error", - "eslint/no-underscore-dangle": "off", - "eslint/no-unmodified-loop-condition": "off", - "eslint/no-unused-vars": [ - "error", - { - "args": "all", - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^$", - "caughtErrors": "all", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "ignoreRestSiblings": false - } - ], - "eslint/no-useless-catch": "off", - "eslint/no-var": "error", - "eslint/prefer-const": "error", - "eslint/prefer-regex-literals": [ - "error", - { - "disallowRedundantWrapping": true - } - ], - "eslint/preserve-caught-error": "off", - "eslint/sort-imports": "off", - "import/no-cycle": "off", - "import/no-named-as-default": "off", - "import/no-named-as-default-member": "off", - "import/no-self-import": "error", - "import/no-unassigned-import": "off", - "socket/backtick-commands-in-comments": "error", - "socket/bag-param-optionality-naming": "error", - "socket/const-enum-shape": "error", - "socket/export-top-level-functions": "error", - "socket/exported-name-has-domain-word": "error", - "socket/git-directories-are-hierarchical": "error", - "socket/guard-contract": "error", - "socket/import-block-is-hoisted": "error", - "socket/inclusive-language": "error", - "socket/lint-disable-precedes-code": "error", - "socket/match-assertions-normalize-paths": "error", - "socket/max-comment-block-lines": "error", - "socket/max-file-lines": "error", - "socket/mcp-tool-names-are-conventional": "error", - "socket/no-agent-brand-assumption": "error", - "socket/no-ai-slop-prose": "error", - "socket/no-bare-crypto-named-usage": "error", - "socket/no-bare-spawn-childproc-access": "error", - "socket/no-boolean-trap-param": "error", - "socket/no-cached-for-on-iterable": "error", - "socket/no-comment-glob-star-slash": "error", - "socket/no-console-prefer-logger": "error", - "socket/no-default-export": "error", - "socket/no-deprecation": "error", - "socket/no-dynamic-import-in-snapshot-hook": "error", - "socket/no-dynamic-import-outside-bundle": "error", - "socket/no-error-message-assertions": "error", - "socket/no-eslint-biome-config-ref": "error", - "socket/no-fetch-prefer-http-request": "error", - "socket/no-file-scope-oxlint-disable": "error", - "socket/no-fileoverview-prefer-file": "error", - "socket/no-force-delete": "error", - "socket/no-handbuilt-file-url": "error", - "socket/no-inline-defer-async": "error", - "socket/no-inline-logger": "error", - "socket/no-literal-control-char": "error", - "socket/no-logger-glyph-prefix": "error", - "socket/no-logger-newline-literal": "error", - "socket/no-malformed-bypass-marker": "error", - "socket/no-map-async-callback": "error", - "socket/no-minified-bundler-output": "error", - "socket/no-module-eval-side-effects": "error", - "socket/no-namespace-import": "error", - "socket/no-npx-dlx": "error", - "socket/no-optional-param-before-options-bag": ["warn"], - "socket/no-optional-positional-trap": "error", - "socket/no-options-param-mutation": "error", - "socket/no-package-manager-auto-update-reenable": "error", - "socket/no-parenthetical-aside": ["error"], - "socket/no-placeholders": "error", - "socket/no-platform-specific-import": "error", - "socket/no-private-path-in-source": "error", - "socket/no-process-chdir": "error", - "socket/no-process-cwd-in-scripts-hooks": "error", - "socket/no-promise-race": "error", - "socket/no-promise-race-in-loop": "error", - "socket/no-prose-jargon": "error", - "socket/no-redundant-spread-fallback": "error", - "socket/no-required-in-options-bag": ["error"], - "socket/no-runtime-features-below-engine-floor": "error", - "socket/no-snapshot-hostile-builtin": "error", - "socket/no-source-content-tests": "error", - "socket/no-source-sniffing": "error", - "socket/no-spawn-stream-double-consume": "error", - "socket/no-spawnsync-code-property": "error", - "socket/no-src-import-in-test-expect": "error", - "socket/no-status-emoji": "error", - "socket/no-structured-clone-prefer-json": "error", - "socket/no-sync-rm-in-test-lifecycle": "error", - "socket/no-top-level-await": "error", - "socket/no-truncated-lint-disable-reason": "error", - "socket/no-truthy-object-guard": "error", - "socket/no-underscore-identifier": "error", - "socket/no-use-strict-in-esm": "error", - "socket/no-vendored-alias-in-test": "error", - "socket/no-vitest-empty-test": "error", - "socket/no-vitest-focused-tests": "error", - "socket/no-vitest-identical-title": "error", - "socket/no-vitest-skipped-tests": "error", - "socket/no-vitest-standalone-expect": [ - "error", - { - "additionalTestBlockFunctions": ["cmdit"] - } - ], - "socket/no-which-for-local-bin": "error", - "socket/optional-explicit-undefined": "error", - "socket/options-null-proto": "error", - "socket/options-param-naming": "error", - "socket/paths-are-normalized-before-match-at-edit": "error", - "socket/personal-path-placeholders": "error", - "socket/prefer-all-settled": "error", - "socket/prefer-array-from": "error", - "socket/prefer-async-spawn": "error", - "socket/prefer-cached-for-loop": "error", - "socket/prefer-char-code-comparison": "error", - "socket/prefer-crlf-safe-split": "error", - "socket/prefer-ellipsis-char": "error", - "socket/prefer-env-as-boolean": "error", - "socket/prefer-error-message-helper": "error", - "socket/prefer-exec-path-over-node": "error", - "socket/prefer-exists-sync": "error", - "socket/prefer-find-repo-root": "error", - "socket/prefer-find-up-package-json": "error", - "socket/prefer-function-declaration": "error", - "socket/prefer-lib-versions-over-semver": "error", - "socket/prefer-mock-import": "error", - "socket/prefer-modern-syntax": "error", - "socket/prefer-node-builtin-imports": "error", - "socket/prefer-non-capturing-group": "error", - "socket/prefer-object-array-destructure": "error", - "socket/prefer-optional-chain": "error", - "socket/prefer-pure-call-form": "error", - "socket/prefer-refined-record": "error", - "socket/prefer-replace-function": "error", - "socket/prefer-repo-root-dot-cache": "error", - "socket/prefer-safe-delete": "error", - "socket/prefer-separate-type-import": "error", - "socket/prefer-setup-phrasing": "error", - "socket/prefer-shell-win32": "error", - "socket/prefer-socket-lib-compare-str": "error", - "socket/prefer-socket-lib-env": "error", - "socket/prefer-socket-lib-error-message": "error", - "socket/prefer-socket-lib-json-format": "error", - "socket/prefer-socket-lib-normalize-path": "error", - "socket/prefer-socket-lib-predicates": "error", - "socket/prefer-spawn-over-execsync": "error", - "socket/prefer-stable-self-import": "error", - "socket/prefer-static-type-import": "error", - "socket/prefer-typebox-schema": "error", - "socket/prefer-undefined-over-null": "error", - "socket/prefer-windows-test-helpers": "error", - "socket/repository-paths-are-contained": "error", - "socket/require-async-iife-entry": "error", - "socket/require-regex-comment": "error", - "socket/require-vitest-globals-import": "error", - "socket/require-void-run-hook": "error", - "socket/returned-object-null-proto": "error", - "socket/socket-api-token-env": "error", - "socket/sort-array-literals": "error", - "socket/sort-boolean-chains": "error", - "socket/sort-equality-disjunctions": "error", - "socket/sort-named-imports": "error", - "socket/sort-object-literal-properties": "error", - "socket/sort-regex-alternations": "error", - "socket/sort-set-args": "error", - "socket/sort-source-methods": "error", - "socket/terse-lint-disable-reason": "error", - "socket/use-fleet-canonical-api-token-getter": "error", - "typescript/array-type": [ - "error", - { - "default": "array-simple" - } - ], - "typescript/consistent-type-assertions": [ - "error", - { - "assertionStyle": "as" - } - ], - "typescript/consistent-type-imports": "error", - "typescript/no-duplicate-enum-values": "error", - "typescript/no-duplicate-type-constituents": [ - "error", - { - "ignoreUnions": true - } - ], - "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-extraneous-class": "off", - "typescript/no-misused-new": "error", - "typescript/no-non-null-asserted-optional-chain": "off", - "typescript/no-redundant-type-constituents": "off", - "typescript/no-this-alias": [ - "error", - { - "allowDestructuring": true - } - ], - "typescript/no-unnecessary-type-assertion": "off", - "typescript/no-useless-empty-export": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/triple-slash-reference": "error", - "unicorn/consistent-function-scoping": "off", - "unicorn/no-array-for-each": "off", - "unicorn/no-array-reverse": "error", - "unicorn/no-array-sort": "error", - "unicorn/no-empty-file": "off", - "unicorn/no-null": "off", - "unicorn/no-useless-fallback-in-spread": "off", - "unicorn/numeric-separators-style": "error", - "unicorn/prefer-node-protocol": "error", - "unicorn/prefer-spread": "off" - }, - "overrides": [ - { - "files": [ - "**/scripts/**", - "**/test/**", - "**/tests/**", - "**/.config/**", - "**/.git-hooks/**", - "**/.github/**", - "**/.claude/hooks/**", - "**/.claude/skills/**", - "**/.claude/workflows/**" - ], - "rules": { - "socket/export-top-level-functions": "off", - "socket/inclusive-language": "off", - "socket/no-default-export": "off", - "socket/no-dynamic-import-outside-bundle": "off", - "socket/no-npx-dlx": "off", - "socket/no-placeholders": "off", - "socket/no-status-emoji": "off", - "socket/prefer-function-declaration": "off", - "socket/sort-source-methods": "off" - } - }, - { - "files": [ - "**/template/base/universal/.claude/**", - "**/template/base/universal/.config/fleet/**", - "**/template/base/universal/.git-hooks/**", - "**/template/base/universal/scripts/fleet/**", - "**/template/base/universal/test/fleet/**" - ], - "rules": { - "socket/max-file-lines": "off" - } - }, - { - "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], - "rules": { - "eslint/no-unused-vars": "off" - } - }, - { - "files": [ - "**/scripts/fleet/build-hook-bundle.mts", - "**/scripts/fleet/build-hook-snapshot.mts", - "**/scripts/fleet/build-snapshot-launcher.mts", - "**/scripts/fleet/fetch-fleet-pack.mts", - "**/scripts/fleet/gen/hook-dispatch.mts", - "**/scripts/fleet/gen/hook-validators.mts", - "**/scripts/fleet/lib/stable-alias.mts", - "**/scripts/fleet/lockstep/emit-mirror-globs.mts", - "**/scripts/fleet/sync-oxlint-rules.mts", - "**/scripts/fleet/update.mts", - "**/scripts/fleet/update/fleet-pins.mts", - "**/scripts/repo/dogfood.mts", - "**/scripts/repo/commit-cascade/fixers/mirror-mode.mts" - ], - "rules": { - "socket/prefer-mirror-lock-write": "error" - } - } - ], - "ignorePatterns": [ - "**/.agents/**", - "**/.cache/**", - "**/.opencode-tmp/**", - "**/.claude/**", - "**/coverage/**", - "**/coverage-isolated/**", - "**/dist/**", - "**/node_modules/**", - "**/patches/**", - "**/test/fixtures/**", - "**/test/packages/**", - "**/test/repo/**", - "**/tests/fixtures/**", - "**/wasm_exec.js", - "**/.config/fleet/oxlint-plugin.generated.mjs", - "**/*.d.ts", - "**/*.d.ts.map", - "**/*.tsbuildinfo", - "#fleet-canonical-begin (managed by socket-wheelhouse sync)", - "**/.claude/agents/fleet/**", - "**/.claude/commands/fleet/**", - "**/.claude/hooks/fleet/**", - "**/.claude/rules/fleet/**", - "**/.claude/skills/fleet/**", - "**/.config/fleet/**", - "**/.config/fleet/oxlint-plugin/**", - "**/.config/repo/rolldown/**", - "**/.git-hooks/**", - "**/.github/actions/fleet/**", - "**/.pnpm-store/**", - "**/bootstrap/**", - "**/docs/fleet/agents.md/**", - "**/scripts/fleet/**", - "**/scripts/repo/bootstrap/**", - "**/test/fleet/_shared/**", - "**/test/fleet/scripts/**", - "**/wasm_exec.js", - "**/.config/repo/vitest.config.mts", - "**/.config/repo/vitest.settings.mts", - "**/.mcp.json", - "**/test/fleet/e2e/comment-voice.test.mts", - "**/test/fleet/integration/comment-voice.test.mts", - "**/test/fleet/nock-loopback-passthrough.test.mts", - "**/test/fleet/unit/ci/gates/run.test.mts", - "**/test/fleet/unit/comment-voice.test.mts", - "**/test/fleet/unit/credentials/otp/bindings.test.mts", - "**/test/fleet/unit/fix/plan.test.mts", - "**/test/fleet/unit/fix/run.test.mts", - "**/test/fleet/unit/lockstep/emit-mirror-globs.test.mts", - "**/test/fleet/unit/registry/apple/ci/keychain.test.mts", - "**/test/fleet/unit/registry/cargo/placeholder.test.mts", - "**/test/fleet/unit/registry/npm/placeholder.test.mts", - "**/test/fleet/unit/setup/lib/pnpm-path.test.mts", - "#fleet-canonical-end", - "", - "", - "release-kit/payload/**" - ] -} diff --git a/.config/fleet/tsconfig.check.json b/.config/fleet/tsconfig.check.json deleted file mode 100644 index 62c0749e..00000000 --- a/.config/fleet/tsconfig.check.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "//": "The repo half comes last so a member's own compilerOptions win. Path aliases live there: without it the type gate resolved none of a member's src/external/* mappings, because the root tsconfig is a sibling of this chain rather than an ancestor.", - "extends": ["./tsconfig.check.base.json", "../repo/tsconfig.check.json"], - "compilerOptions": { - "rootDir": "../.." - } -} diff --git a/.config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs b/.config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs deleted file mode 100644 index a4902eaf..00000000 --- a/.config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @file Shared helper for fleet markdown rules: detect whether the lint is - * running inside socket-wheelhouse itself, in which case the rule should - * bail. The custom rules in this directory exist to protect PUBLIC fleet - * consumers from leaking internal scaffolding; wheelhouse referencing itself - * in its own docs is the canonical case and must not trigger. Detection - * prefers explicit env override (CI sets SOCKET_FLEET_REPO_NAME) then falls - * back to checking the cwd's basename and git remote. - */ - -// markdownlint-cli2 calls isInsideWheelhouse() synchronously at rule init; -// an async spawn would require the rule loader to await, which -// markdownlint-cli2 doesn't support. -// oxlint-disable-next-line socket/prefer-async-spawn -- sync rule init -import { spawnSync } from 'node:child_process' -import path from 'node:path' -import process from 'node:process' - -export function isInsideWheelhouse() { - const envName = process.env['SOCKET_FLEET_REPO_NAME'] - if (envName) { - return envName === 'socket-wheelhouse' - } - const cwd = process.cwd() - if (path.basename(cwd) === 'socket-wheelhouse') { - return true - } - // Fallback: probe the git remote URL. Tolerates renamed local - // checkout dirs (`~/projects/wheelhouse/` would still match). - // spawnSync (not execSync) — array args, no shell interpolation. - // This file is loaded by markdownlint-cli2 as a regular ESM module, - // not bundled, so we cant pull in @socketsecurity/lib-stable/spawn — - // node:child_process spawnSync is the canonical fallback. - const r = spawnSync('git', ['config', '--get', 'remote.origin.url'], { - cwd, - stdio: ['ignore', 'pipe', 'ignore'], - }) - if (r.status !== 0 || !r.stdout) { - return false - } - const remote = r.stdout.toString().trim() - return /[/:]socket-wheelhouse(?:\.git)?$/.test(remote) -} diff --git a/.config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs b/.config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs deleted file mode 100644 index 9ecb7723..00000000 --- a/.config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @file Flag mentions of `socket-wheelhouse` in public-facing markdown. - * socket-wheelhouse is a private repo. Public READMEs / docs / release notes - * that link to it leak the internal tooling layout to users who can't access - * the link anyway. Whatever the markdown is trying to teach should be - * rewritten to not require the reference. Detects: - * - * - The literal token `socket-wheelhouse` (case-insensitive) anywhere in a - * line. - * - `https://github.com/SocketDev/socket-wheelhouse...` URL forms. Skips fenced - * code blocks because those are intentional examples (and fenced-block - * scanning would false-positive on the very markdownlint config that - * references this file). No autofix: the right rewrite is contextual. - */ - -import { isInsideWheelhouse } from './_shared/wheelhouse-self-skip.mjs' - -const RULE_NAME = 'socket-no-private-wheelhouse-leak' -const FORBIDDEN_TOKEN_RE = /socket-wheelhouse/i - -/** - * @type {import('markdownlint').Rule} - */ -const rule = { - description: - 'socket-wheelhouse is a private repo — never reference it in public markdown', - function(params, onError) { - if (isInsideWheelhouse()) { - return - } - let inFence = false - for (let i = 0; i < params.lines.length; i += 1) { - const line = params.lines[i] - // Track fenced-code state. Open/close on lines that START with ``` or ~~~. - if (/^\s*(?:```|~~~)/.test(line)) { - inFence = !inFence - continue - } - if (inFence) { - continue - } - const match = FORBIDDEN_TOKEN_RE.exec(line) - if (!match) { - continue - } - onError({ - lineNumber: i + 1, - detail: - 'Rewrite to not mention socket-wheelhouse — it is a private repo and the link will 404 for outside readers.', - context: line.trim().slice(0, 120), - range: [match.index + 1, match[0].length], - }) - } - }, - names: [RULE_NAME, 'socket/no-private-wheelhouse-leak'], - parser: 'none', - tags: ['socket', 'privacy'], -} - -// markdownlint-cli2 loads custom rules via dynamic import and expects the -// default export to be the rule object. -// oxlint-disable-next-line socket/no-default-export -- dynamic import -export default rule diff --git a/.config/markdownlint-rules/socket-no-relative-sibling-script.mjs b/.config/markdownlint-rules/socket-no-relative-sibling-script.mjs deleted file mode 100644 index 53d09d71..00000000 --- a/.config/markdownlint-rules/socket-no-relative-sibling-script.mjs +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @file Flag commands that reference sibling repos via relative paths. `node - * ../socket-foo/scripts/bar.mts` in a fleet README assumes the reader has the - * sibling repo checked out at exactly the right level relative to the current - * repo. That's almost never true for an outside user, and the command - * silently fails. Detects (inside fenced code blocks and inline `code`): - * - * - `node ..//...` invocations - * - `pnpm ..//...` invocations - * - Bare `../socket-/...` references in code/inline-code Skips: - * relative paths to the current repo's own tree (`./scripts/`, - * `../package.json` within a monorepo), which are useful and don't leak - * sibling state. No autofix: the rewrite is to either inline the script's - * content or publish the helper to npm and reference the published name. - */ - -import { isInsideWheelhouse } from './_shared/wheelhouse-self-skip.mjs' - -const RULE_NAME = 'socket-no-relative-sibling-script' -const SIBLING_PATH_RES = [ - // Detect ` ..//...` where runner is one of the common - // JS/TS toolchain binaries (any runtime invocation). - /\b(?:bun|deno|node|npm|pnpm|yarn)\s+\.\.\/[\w@-]+\//, - // Detect bare ..// where the first segment doesn't start with `.` - // (i.e. genuine sibling, not the current repo's `..` for monorepo packages). - // `(?:^|\s)` alternation order is the canonical regex idiom (anchor-first). - /(?:^|\s)\.\.\/socket-[\w-]+\//i, // socket-hook: allow regex-alternation-order - /(?:^|\s)\.\.\/sdxgen\//, // socket-hook: allow regex-alternation-order - /(?:^|\s)\.\.\/stuie\//, // socket-hook: allow regex-alternation-order -] - -/** - * @type {import('markdownlint').Rule} - */ -const rule = { - description: - 'Commands referencing sibling fleet repos via relative paths fail for outside readers', - function(params, onError) { - if (isInsideWheelhouse()) { - return - } - for (let i = 0; i < params.lines.length; i += 1) { - const line = params.lines[i] - for (let j = 0; j < SIBLING_PATH_RES.length; j += 1) { - const re = SIBLING_PATH_RES[j] - const match = re.exec(line) - if (!match) { - continue - } - onError({ - lineNumber: i + 1, - detail: - 'Rewrite the command to not depend on a sibling-repo checkout. Inline the script, link to its source on GitHub, or publish the helper to npm and reference the package name.', - context: line.trim().slice(0, 120), - range: [match.index + 1, match[0].length], - }) - break - } - } - }, - names: [RULE_NAME, 'socket/no-relative-sibling-script'], - parser: 'none', - tags: ['socket', 'fleet'], -} - -// markdownlint-cli2 loads custom rules via dynamic import and expects the -// default export to be the rule object. -// oxlint-disable-next-line socket/no-default-export -- dynamic import -export default rule diff --git a/.config/markdownlint-rules/socket-readme-required-sections.mjs b/.config/markdownlint-rules/socket-readme-required-sections.mjs deleted file mode 100644 index 0de7559c..00000000 --- a/.config/markdownlint-rules/socket-readme-required-sections.mjs +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file Enforce the canonical fleet README section list. Fires only on the - * repo-root `README.md` (skipped for nested READMEs under `packages/`, - * `docs/`, `.claude/`, etc. — those are scoped docs with their own shape). - * Every fleet root README must contain five level-2 sections in this order: - * - * 1. Why this repo exists - * 2. Install - * 3. Usage - * 4. Development - * 5. License The canonical skeleton lives at - * socket-wheelhouse/template/README.md. Additional sections between/after - * these are allowed; reordering / missing / typo'd sections are findings. - * No autofix: a missing section needs content, not just a heading. - */ - -import path from 'node:path' - -import { isInsideWheelhouse } from './_shared/wheelhouse-self-skip.mjs' - -const RULE_NAME = 'socket-readme-required-sections' -const REQUIRED_SECTIONS = [ - 'Why this repo exists', - 'Install', - 'Usage', - 'Development', - 'License', -] - -export function isRootReadme(filePath) { - // markdownlint passes `params.name` as a path relative to the working - // dir. The root README is the one whose basename is README.md AND - // whose directory is the cwd or `.`. - if (!filePath) { - return false - } - const base = path.basename(filePath) - if (base !== 'README.md') { - return false - } - const dir = path.dirname(filePath) - return dir === '.' || dir === '' || dir === process.cwd() -} - -/** - * @type {import('markdownlint').Rule} - */ -const rule = { - description: - 'Fleet root README must contain the canonical five sections in order', - function(params, onError) { - if (isInsideWheelhouse()) { - return - } - if (!isRootReadme(params.name)) { - return - } - const headings = [] - for (let i = 0; i < params.lines.length; i += 1) { - const line = params.lines[i] - const m = /^##\s+(.+?)\s*$/.exec(line) - if (m) { - headings.push({ text: m[1], lineNumber: i + 1 }) - } - } - let cursor = 0 - for (let r = 0; r < REQUIRED_SECTIONS.length; r += 1) { - const want = REQUIRED_SECTIONS[r] - let found = -1 - for (let h = cursor; h < headings.length; h += 1) { - if (headings[h].text === want) { - found = h - break - } - } - if (found === -1) { - onError({ - lineNumber: 1, - detail: `Missing required section "## ${want}" (or it appears out of order). Canonical order: ${REQUIRED_SECTIONS.map(s => `"## ${s}"`).join(' → ')}.`, - context: `README.md: required section "## ${want}" not found after position ${cursor}`, - }) - return - } - cursor = found + 1 - } - }, - names: [RULE_NAME, 'socket/readme-required-sections'], - parser: 'none', - tags: ['socket', 'fleet', 'readme'], -} - -// markdownlint-cli2 loads custom rules via dynamic import and expects the -// default export to be the rule object. -// oxlint-disable-next-line socket/no-default-export -- dynamic import -export default rule diff --git a/.config/oxfmtrc.json b/.config/oxfmtrc.json deleted file mode 100644 index 84874615..00000000 --- a/.config/oxfmtrc.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json", - "useTabs": false, - "tabWidth": 2, - "printWidth": 80, - "singleQuote": true, - "jsxSingleQuote": false, - "quoteProps": "as-needed", - "trailingComma": "all", - "semi": false, - "arrowParens": "avoid", - "bracketSameLine": false, - "bracketSpacing": true, - "singleAttributePerLine": false, - "jsdoc": { - "addDefaultToDescription": false, - "bracketSpacing": false, - "capitalizeDescriptions": true, - "commentLineStrategy": "multiline", - "descriptionTag": false, - "descriptionWithDot": true, - "keepUnparsableExampleIndent": false, - "lineWrappingStyle": "greedy", - "preferCodeFences": false, - "separateReturnsFromParam": false, - "separateTagGroups": true - }, - "ignorePatterns": [ - "**/.cache", - "**/.claude", - "**/.DS_Store", - "**/._.DS_Store", - "**/.env", - "**/.git", - "**/.github", - "**/.husky", - "**/.type-coverage", - "**/.vscode", - "**/coverage", - "**/coverage-isolated", - "**/dist", - "**/external", - "**/node_modules", - "**/package.json", - "**/pnpm-lock.yaml", - "**/test/fixtures", - "**/test/packages", - "**/xport.schema.json", - "#fleet-canonical-begin (managed by socket-wheelhouse sync)", - "**/.claude/**", - "**/.config/oxlint-plugin/**", - "**/.config/rolldown/**", - "**/.git-hooks/**", - "**/.pnpm-store/**", - "**/vendor/**", - "**/wasm_exec.js", - "**/scripts/plugin-patches/**/*.files/**", - "**/scripts/plugin-patches/**/*.patch", - "**/.config/lockstep.schema.json", - "**/.config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs", - "**/.config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs", - "**/.config/markdownlint-rules/socket-no-relative-sibling-script.mjs", - "**/.config/markdownlint-rules/socket-readme-required-sections.mjs", - "**/.config/socket-registry-pins.json", - "**/.config/socket-wheelhouse-schema.json", - "**/.config/taze.config.mts", - "**/.config/tsconfig.base.json", - "**/.config/vitest.coverage.fleet.config.mts", - "**/packages/build-infra/lib/release-checksums/consumer.mts", - "**/packages/build-infra/lib/release-checksums/core.mts", - "**/packages/build-infra/lib/release-checksums/producer.mts", - "**/packages/build-infra/release-assets.schema.json", - "**/scripts/ai-lint-fix.mts", - "**/scripts/ai-lint-fix/cli.mts", - "**/scripts/ai-lint-fix/rule-guidance.mts", - "**/scripts/audit-transcript.mts", - "**/scripts/check-lock-step-header.mts", - "**/scripts/check-lock-step-refs.mts", - "**/scripts/check-paths.mts", - "**/scripts/check-paths/allowlist.mts", - "**/scripts/check-paths/cli.mts", - "**/scripts/check-paths/exempt.mts", - "**/scripts/check-paths/rules.mts", - "**/scripts/check-paths/scan-code.mts", - "**/scripts/check-paths/scan-script.mts", - "**/scripts/check-paths/scan-workflow.mts", - "**/scripts/check-paths/state.mts", - "**/scripts/check-paths/types.mts", - "**/scripts/check-paths/walk.mts", - "**/scripts/check-prompt-less-setup.mts", - "**/scripts/check-provenance.mts", - "**/scripts/check-soak-exclude-dates.mts", - "**/scripts/fix.mts", - "**/scripts/git-partial-submodule.mts", - "**/scripts/install-claude-plugins.mts", - "**/scripts/install-git-hooks.mts", - "**/scripts/install-sfw.mts", - "**/scripts/install-token-minifier.mts", - "**/scripts/janus.mts", - "**/scripts/lint-github-settings.mts", - "**/scripts/lockstep-emit-schema.mts", - "**/scripts/lockstep-schema.mts", - "**/scripts/lockstep.mts", - "**/scripts/lockstep/checks.mts", - "**/scripts/lockstep/cli.mts", - "**/scripts/lockstep/emit-schema.mts", - "**/scripts/lockstep/git.mts", - "**/scripts/lockstep/manifest.mts", - "**/scripts/lockstep/report.mts", - "**/scripts/lockstep/scan.mts", - "**/scripts/lockstep/schema.mts", - "**/scripts/lockstep/types.mts", - "**/scripts/plugin-patches/codex-1.0.1-stdin-eagain.files/scripts/lib/read-stdin-sync.mjs", - "**/scripts/power-state.mts", - "**/scripts/publish-release.mts", - "**/scripts/publish-shared.mts", - "**/scripts/publish.mts", - "**/scripts/security.mts", - "**/scripts/socket-wheelhouse-emit-schema.mts", - "**/scripts/socket-wheelhouse-schema.mts", - "**/scripts/test/check-lock-step-header.test.mts", - "**/scripts/test/check-lock-step-refs.test.mts", - "**/scripts/test/install-claude-plugins.test.mts", - "**/scripts/test/install-git-hooks.test.mts", - "**/scripts/update.mts", - "**/scripts/util/run-command.mts", - "**/scripts/validate-bundle-deps.mts", - "**/scripts/validate-config-paths.mts", - "**/scripts/validate-file-size.mts", - "**/scripts/validate-rolldown-minify.mts", - "#fleet-canonical-end" - ] -} diff --git a/.config/oxlintrc.json b/.config/oxlintrc.json deleted file mode 100644 index 622210e3..00000000 --- a/.config/oxlintrc.json +++ /dev/null @@ -1,296 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/856781f99c7eb521b6221fab0047cfd09343df50/npm/oxlint/configuration_schema.json", - "plugins": ["typescript", "unicorn", "import"], - "jsPlugins": ["./fleet/oxlint-plugin/index.mts"], - "categories": { - "correctness": "error", - "suspicious": "error" - }, - "rules": { - "eslint/curly": "error", - "eslint/no-await-in-loop": "off", - "eslint/no-console": "off", - "eslint/no-control-regex": "off", - "eslint/no-empty": [ - "error", - { - "allowEmptyCatch": true - } - ], - "eslint/no-new": "error", - "eslint/no-proto": "error", - "eslint/no-shadow": "error", - "eslint/no-underscore-dangle": "off", - "eslint/no-unmodified-loop-condition": "off", - "eslint/no-unused-vars": "off", - "eslint/no-useless-catch": "off", - "eslint/no-var": "error", - "eslint/prefer-const": "error", - "eslint/preserve-caught-error": "off", - "eslint/sort-imports": "off", - "import/no-cycle": "off", - "import/no-named-as-default": "off", - "import/no-named-as-default-member": "off", - "import/no-self-import": "error", - "import/no-unassigned-import": "off", - "socket/bag-param-optionality-naming": "error", - "socket/export-top-level-functions": "error", - "socket/exported-name-has-domain-word": "error", - "socket/guard-contract": "error", - "socket/inclusive-language": "error", - "socket/lint-disable-precedes-code": "error", - "socket/max-comment-block-lines": "error", - "socket/max-file-lines": "error", - "socket/no-agent-brand-assumption": "error", - "socket/no-bare-crypto-named-usage": "error", - "socket/no-bare-spawn-childproc-access": "error", - "socket/no-boolean-trap-param": "error", - "socket/no-cached-for-on-iterable": "error", - "socket/no-comment-glob-star-slash": "error", - "socket/no-console-prefer-logger": "error", - "socket/no-default-export": "error", - "socket/no-deprecation": "error", - "socket/no-dynamic-import-in-snapshot-hook": "error", - "socket/no-dynamic-import-outside-bundle": "error", - "socket/no-eslint-biome-config-ref": "error", - "socket/no-fetch-prefer-http-request": "error", - "socket/no-file-scope-oxlint-disable": "error", - "socket/no-fileoverview-prefer-file": "error", - "socket/no-glob-in-ordered-run-s": "error", - "socket/no-handbuilt-file-url": "error", - "socket/no-inline-defer-async": "error", - "socket/no-inline-logger": "error", - "socket/no-literal-control-char": "error", - "socket/no-logger-glyph-prefix": "error", - "socket/no-logger-newline-literal": "error", - "socket/no-malformed-bypass-marker": "error", - "socket/no-minified-bundler-output": "error", - "socket/no-module-eval-side-effects": "error", - "socket/no-namespace-import": "error", - "socket/no-npx-dlx": "error", - "socket/no-optional-positional-trap": "error", - "socket/no-options-param-mutation": "error", - "socket/no-package-manager-auto-update-reenable": "error", - "socket/no-placeholders": "error", - "socket/no-platform-specific-import": "error", - "socket/no-private-path-in-source": "error", - "socket/no-process-chdir": "error", - "socket/no-process-cwd-in-scripts-hooks": "error", - "socket/no-promise-race": "error", - "socket/no-promise-race-in-loop": "error", - "socket/no-redundant-spread-fallback": "error", - "socket/no-runtime-features-below-engine-floor": "error", - "socket/no-shop-metaphor-jargon": "error", - "socket/no-source-content-tests": "error", - "socket/no-source-sniffing": "error", - "socket/no-spawn-stream-double-consume": "error", - "socket/no-spawnsync-code-field": "error", - "socket/no-src-import-in-test-expect": "error", - "socket/no-status-emoji": "error", - "socket/no-structured-clone-prefer-json": "error", - "socket/no-sync-rm-in-test-lifecycle": "error", - "socket/no-top-level-await": "error", - "socket/no-truncated-lint-disable-reason": "error", - "socket/no-underscore-identifier": "error", - "socket/no-use-strict-in-esm": "error", - "socket/no-vitest-empty-test": "error", - "socket/no-vitest-focused-tests": "error", - "socket/no-vitest-identical-title": "error", - "socket/no-vitest-skipped-tests": "error", - "socket/no-vitest-standalone-expect": "error", - "socket/no-which-for-local-bin": "error", - "socket/normalize-path-before-match": "error", - "socket/optional-explicit-undefined": "error", - "socket/options-null-proto": "error", - "socket/options-param-naming": "error", - "socket/personal-path-placeholders": "error", - "socket/prefer-all-settled": "error", - "socket/prefer-async-spawn": "error", - "socket/prefer-cached-for-loop": "error", - "socket/prefer-crlf-safe-split": "error", - "socket/prefer-ellipsis-char": "error", - "socket/prefer-env-as-boolean": "error", - "socket/prefer-error-message": "error", - "socket/prefer-error-message-helper": "error", - "socket/prefer-exists-sync": "error", - "socket/prefer-find-repo-root": "error", - "socket/prefer-find-up-package-json": "error", - "socket/prefer-function-declaration": "error", - "socket/prefer-lib-predicates": "error", - "socket/prefer-lib-versions-over-semver": "error", - "socket/prefer-mock-import": "error", - "socket/prefer-node-builtin-imports": "error", - "socket/prefer-node-modules-dot-cache": "error", - "socket/prefer-non-capturing-group": "error", - "socket/prefer-normalize-path": "error", - "socket/prefer-optional-chain": "error", - "socket/prefer-pure-call-form": "error", - "socket/prefer-refined-record": "error", - "socket/prefer-replace-function": "error", - "socket/prefer-repo-root-dot-cache": "error", - "socket/prefer-safe-delete": "error", - "socket/prefer-separate-type-import": "error", - "socket/prefer-shell-win32": "error", - "socket/prefer-spawn-over-execsync": "error", - "socket/prefer-stable-external-semver": "error", - "socket/prefer-stable-self-import": "error", - "socket/prefer-static-type-import": "error", - "socket/prefer-typebox-schema": "error", - "socket/prefer-undefined-over-null": "error", - "socket/prefer-windows-test-helpers": "error", - "socket/require-async-iife-entry": "error", - "socket/require-regex-comment": "error", - "socket/require-vitest-globals-import": "error", - "socket/socket-api-token-env": "error", - "socket/sort-array-literals": "error", - "socket/sort-boolean-chains": "error", - "socket/sort-equality-disjunctions": "error", - "socket/sort-named-imports": "error", - "socket/sort-object-literal-properties": "error", - "socket/sort-regex-alternations": "error", - "socket/sort-set-args": "error", - "socket/sort-source-methods": "error", - "socket/terse-lint-disable-reason": "error", - "socket/use-fleet-canonical-api-token-getter": "error", - "typescript/array-type": [ - "error", - { - "default": "array-simple" - } - ], - "typescript/consistent-type-assertions": [ - "error", - { - "assertionStyle": "as" - } - ], - "typescript/consistent-type-imports": "error", - "typescript/no-duplicate-enum-values": "error", - "typescript/no-duplicate-type-constituents": "error", - "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-extraneous-class": "off", - "typescript/no-misused-new": "error", - "typescript/no-non-null-asserted-optional-chain": "off", - "typescript/no-redundant-type-constituents": "off", - "typescript/no-this-alias": [ - "error", - { - "allowDestructuring": true - } - ], - "typescript/no-unnecessary-type-assertion": "off", - "typescript/no-useless-empty-export": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/triple-slash-reference": "error", - "unicorn/consistent-function-scoping": "off", - "unicorn/no-array-for-each": "off", - "unicorn/no-array-reverse": "error", - "unicorn/no-array-sort": "error", - "unicorn/no-empty-file": "off", - "unicorn/no-null": "off", - "unicorn/no-useless-fallback-in-spread": "off", - "unicorn/numeric-separators-style": "error", - "unicorn/prefer-node-protocol": "error", - "unicorn/prefer-spread": "off" - }, - "ignorePatterns": [ - "**/.cache", - "**/.claude", - "**/coverage", - "**/coverage-isolated", - "**/dist", - "**/external", - "**/node_modules", - "**/patches", - "**/test/fixtures", - "**/test/packages", - "**/*.d.ts", - "**/*.d.ts.map", - "**/*.tsbuildinfo", - "#fleet-canonical-begin (managed by socket-wheelhouse sync)", - "**/.claude/**", - "**/.config/oxlint-plugin/**", - "**/.config/rolldown/**", - "**/.git-hooks/**", - "**/.pnpm-store/**", - "**/vendor/**", - "**/wasm_exec.js", - "**/scripts/plugin-patches/**/*.files/**", - "**/scripts/plugin-patches/**/*.patch", - "**/.config/lockstep.schema.json", - "**/.config/markdownlint-rules/_shared/wheelhouse-self-skip.mjs", - "**/.config/markdownlint-rules/socket-no-private-wheelhouse-leak.mjs", - "**/.config/markdownlint-rules/socket-no-relative-sibling-script.mjs", - "**/.config/markdownlint-rules/socket-readme-required-sections.mjs", - "**/.config/socket-registry-pins.json", - "**/.config/socket-wheelhouse-schema.json", - "**/.config/taze.config.mts", - "**/.config/tsconfig.base.json", - "**/.config/vitest.coverage.fleet.config.mts", - "**/packages/build-infra/lib/release-checksums/consumer.mts", - "**/packages/build-infra/lib/release-checksums/core.mts", - "**/packages/build-infra/lib/release-checksums/producer.mts", - "**/packages/build-infra/release-assets.schema.json", - "**/scripts/ai-lint-fix.mts", - "**/scripts/ai-lint-fix/cli.mts", - "**/scripts/ai-lint-fix/rule-guidance.mts", - "**/scripts/audit-transcript.mts", - "**/scripts/check-lock-step-header.mts", - "**/scripts/check-lock-step-refs.mts", - "**/scripts/check-paths.mts", - "**/scripts/check-paths/allowlist.mts", - "**/scripts/check-paths/cli.mts", - "**/scripts/check-paths/exempt.mts", - "**/scripts/check-paths/rules.mts", - "**/scripts/check-paths/scan-code.mts", - "**/scripts/check-paths/scan-script.mts", - "**/scripts/check-paths/scan-workflow.mts", - "**/scripts/check-paths/state.mts", - "**/scripts/check-paths/types.mts", - "**/scripts/check-paths/walk.mts", - "**/scripts/check-prompt-less-setup.mts", - "**/scripts/check-provenance.mts", - "**/scripts/check-soak-exclude-dates.mts", - "**/scripts/fix.mts", - "**/scripts/git-partial-submodule.mts", - "**/scripts/install-claude-plugins.mts", - "**/scripts/install-git-hooks.mts", - "**/scripts/install-sfw.mts", - "**/scripts/install-token-minifier.mts", - "**/scripts/janus.mts", - "**/scripts/lint-github-settings.mts", - "**/scripts/lockstep-emit-schema.mts", - "**/scripts/lockstep-schema.mts", - "**/scripts/lockstep.mts", - "**/scripts/lockstep/checks.mts", - "**/scripts/lockstep/cli.mts", - "**/scripts/lockstep/emit-schema.mts", - "**/scripts/lockstep/git.mts", - "**/scripts/lockstep/manifest.mts", - "**/scripts/lockstep/report.mts", - "**/scripts/lockstep/scan.mts", - "**/scripts/lockstep/schema.mts", - "**/scripts/lockstep/types.mts", - "**/scripts/plugin-patches/codex-1.0.1-stdin-eagain.files/scripts/lib/read-stdin-sync.mjs", - "**/scripts/power-state.mts", - "**/scripts/publish-release.mts", - "**/scripts/publish-shared.mts", - "**/scripts/publish.mts", - "**/scripts/security.mts", - "**/scripts/socket-wheelhouse-emit-schema.mts", - "**/scripts/socket-wheelhouse-schema.mts", - "**/scripts/test/check-lock-step-header.test.mts", - "**/scripts/test/check-lock-step-refs.test.mts", - "**/scripts/test/install-claude-plugins.test.mts", - "**/scripts/test/install-git-hooks.test.mts", - "**/scripts/update.mts", - "**/scripts/util/run-command.mts", - "**/scripts/validate-bundle-deps.mts", - "**/scripts/validate-config-paths.mts", - "**/scripts/validate-file-size.mts", - "**/scripts/validate-rolldown-minify.mts", - "#fleet-canonical-end" - ] -} diff --git a/.config/repo/.prettierignore b/.config/repo/.prettierignore deleted file mode 100644 index 8d702014..00000000 --- a/.config/repo/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Repository fixtures and vendored patch payloads are byte-owned by their -# source or generator. Formatting them would invalidate their contract. -**/tests/fixtures/** -**/scripts/repo/plugin-patches/** diff --git a/.config/repo/external-tools.json b/.config/repo/external-tools.json deleted file mode 100644 index ac7db13c..00000000 --- a/.config/repo/external-tools.json +++ /dev/null @@ -1,208 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/SocketDev/socket-wheelhouse/main/packages/build-infra/lib/external-tools-schema.json", - "description": "External tools required to build + release this repo. The canonical schema lives at socket-wheelhouse/packages/build-infra/lib/external-tools-schema.json (generated from scripts/fleet/lib/external-tools-schema.mts); every fleet repo references it via the $schema field above so validators pick up the single source of truth. Extend the `tools` map with repo-specific entries (e.g. language toolchains for sdxgen, git credential helpers for packageurl-js).", - "tools": { - "gh": { - "description": "GitHub CLI — workflow dispatch, release downloads, PR creation in weekly-update; host for the gh-aw agentic-workflows extension.", - "version": "2.94.0", - "notes": [ - "Required: only in workflows that call `gh api` / `gh pr create` (weekly-update, provenance)", - "Preinstalled on GitHub-hosted runners", - "Local: `brew install gh` / `winget install gh` / `apt install gh`", - "The gh-aw extension needs gh >= 2.0.0; the pinned version above is the current latest" - ], - "origin": "system" - }, - "gh-aw": { - "description": "GitHub Agentic Workflows — `gh` extension that authors agentic workflows as `.github/workflows/.md` (markdown + frontmatter) and compiles them to a hardened `.lock.yml`. The fleet uses `engine: claude`.", - "version": "latest", - "repository": "github:github/gh-aw", - "notes": [ - "Required: any repo with a `.github/workflows/*.md` agentic workflow", - "Install: `gh extension install github/gh-aw`; init a repo with `gh aw init`; compile after editing frontmatter with `gh aw compile` (commit BOTH the .md and the generated .lock.yml)", - "Engine secrets (set per repo, by engine): `ANTHROPIC_API_KEY` (claude), `COPILOT_GITHUB_TOKEN` (copilot — distinct from the default GITHUB_TOKEN; or use `permissions: copilot-requests: write` to bill the org via the Actions token, no PAT), `OPENAI_API_KEY` (codex), `GEMINI_API_KEY` (gemini)", - "`copilot-requests: write` is Copilot-engine-only — claude/codex/gemini need no GitHub permission beyond their API key", - "Any action a compiled .lock.yml references must be added to the repo's GitHub Actions allowlist (Settings → Actions → Allowed actions)" - ], - "origin": "system" - }, - "git": { - "description": "Git CLI — checkout, submodule init, tag signing.", - "version": "2.30+", - "notes": [ - "Required: yes (all platforms)", - "Preinstalled on macOS (Xcode CLT) and most Linux distros", - "Windows: https://git-scm.com/download/win or via winget/scoop" - ], - "origin": "system" - }, - "node": { - "description": "Node.js — runs the build scripts and TypeScript stripping.", - "version": "24+", - "notes": [ - "Required: yes", - "Node 24+ runs .mts files with default-on native type stripping; Node 24.12+ marks it stable", - "Install via volta / nvm / fnm / official installer", - "The pinned version lives in .node-version at the repo root", - "Docker prebakes: node-base (docker/fleet/node-base.Dockerfile) builds node from source at the .node-version pin with V8 pointer compression (--experimental-enable-pointer-compression = lower memory), replicating the vendored platformatic/node-caged recipe rather than pulling its DockerHub image. scripts/repo/build-prebakes.mts injects NODE_VERSION (.node-version) + PNPM_VERSION (pnpm pin) as build-args; every compiler-base build COPYs node --from=node-base instead of installing it." - ], - "origin": "system" - }, - "sfw-enterprise": { - "notes": [ - "SFW (Socket Firewall) enterprise flavor (private, SocketDev/firewall-release). Same 7-platform set as sfw-free. Enterprise downloads require GITHUB_TOKEN auth (private repo); install-tool.mjs forwards GITHUB_TOKEN automatically when set.", - "Installed when SOCKET_API_KEY (or SOCKET_API_TOKEN) is set; otherwise the free flavor (sfw-free) is used. The two flavors share a version and install to the same `sfw` binary name." - ], - "description": "Socket Firewall (enterprise tier) — package manager command wrapper", - "version": "1.15.4", - "repository": "github:SocketDev/firewall-release", - "tag": "v1.15.4", - "binaryName": "sfw", - "platforms": { - "darwin-arm64": { - "asset": "sfw-macos-arm64", - "integrity": "sha512-rhW6U8/3w7eciriH9mLCE3krMAQuS1xl0CXayw26Xn1kWLh6PwIbpRDjF4QvKFNSpYQKdfEhh9dsBabUxza6sw==" - }, - "darwin-x64": { - "asset": "sfw-macos-x86_64", - "integrity": "sha512-J+xy3DdHah837x0bIlByCQ2pABtPPu5zkKdthIDpsdwNxLWmeFbPyaZQCwrWV4bzkFQz6p0aqGK0SdvqYXR8lw==" - }, - "linux-arm64": { - "asset": "sfw-linux-arm64", - "integrity": "sha512-+cOHb1eZOGSEnHMNUsrGOk7hUSEMqTNSBQBEX9zOk9bapFe6bFAz41286L5Od3VxOrlPk6+szHP4d+J7XM604A==" - }, - "linux-arm64-musl": { - "asset": "sfw-musl-linux-arm64", - "integrity": "sha512-wtPmCCSxNIhcTdyIb6eiUN1rK2fCxrH39QAuYgcyxwtPSAS1dEHYb7yHPNJlOQfZm+WnQ3j7VQL4sCrswSIzbQ==" - }, - "linux-x64": { - "asset": "sfw-linux-x86_64", - "integrity": "sha512-Kxuztnz5CkV/Jt5hMZnyjkwrUwlnzjnxt9oBQMAnUdfoPebDJ64s6PSa/WF3xxM9mIIt0k462+oC+ykzMTdzag==" - }, - "linux-x64-musl": { - "asset": "sfw-musl-linux-x86_64", - "integrity": "sha512-pz7rsn/WAAwEXzhFEcULpB8jfFANGN6g8AQNuTrl9ildDKrb/JTS9MbXylzCIc3GUWL1mE1olekPnBAkb8/A+w==" - }, - "win32-x64": { - "asset": "sfw-windows-x86_64.exe", - "integrity": "sha512-zi2isECucghnSVLVDjTaABKr/2nsmiUHVGEELcj1/7DRLbFIYLq+m3zpcfRJTLstElWbht2PjIJQuHWtnrzLrw==" - } - }, - "origin": "gh-asset" - }, - "sfw-free": { - "notes": [ - "SFW (Socket Firewall) free flavor (public, SocketDev/sfw-free). Ships a 7-platform set: linux-{x64,arm64}{,-musl}, darwin-{x64,arm64}, win-x64. Windows 11 ARM64 setup uses the verified win32-x64 asset through x64 emulation when a native asset is unavailable. SFW remains mandatory and must execute successfully before installation continues.", - "Installed when neither SOCKET_API_KEY nor SOCKET_API_TOKEN is set; the enterprise flavor (sfw-enterprise) is selected when one of those is present. The two flavors share a version and install to the same `sfw` binary name." - ], - "description": "Socket Firewall (free tier) — package manager command wrapper", - "version": "1.15.4", - "repository": "github:SocketDev/sfw-free", - "tag": "v1.15.4", - "binaryName": "sfw", - "platforms": { - "darwin-arm64": { - "asset": "sfw-free-macos-arm64", - "integrity": "sha512-r+J3NSFkpqzlXVCPohISrPoDU33y0eTWcSNmvPQRyQTV2D05eF6H/LzJRS4b7ONuKQIpu/t5KgXyoPkyGiAEgg==" - }, - "darwin-x64": { - "asset": "sfw-free-macos-x86_64", - "integrity": "sha512-Owc7IfGNh+ZQfDKhuUa7TtBWpnPQJWYbTzALDFsss+s3d8g9rISTcsdx4rhjM18e3TLG6QM3bHLYw2G3bYJTng==" - }, - "linux-arm64": { - "asset": "sfw-free-linux-arm64", - "integrity": "sha512-6jIeFfwMO3RRM0t8Fdpj2BBAREgSWwdaj1dAjTE+9rAYP1PyWDT+ZxJWoDHKCqePYrhvGANVs9GlQdLptLN3OQ==" - }, - "linux-arm64-musl": { - "asset": "sfw-free-musl-linux-arm64", - "integrity": "sha512-kMO5ZgMG9Xq+dbnwTueQ/kO/sx6+CGdGd+Lo0zTMoyvUtbeeO4xk5UKg9N3k7lG7KoBtcf5f4yQV4w1tNlSNxA==" - }, - "linux-x64": { - "asset": "sfw-free-linux-x86_64", - "integrity": "sha512-mOxJNA9c3jsOzxXE2grwWYGv/3BN8cj4t9Kfiu7z6Q0Z4p2UcEuR5ldHoZkMReD08F3hV13+c/YuHFtR9K5Bfw==" - }, - "linux-x64-musl": { - "asset": "sfw-free-musl-linux-x86_64", - "integrity": "sha512-Z1kyV0D0WP6P2N9FCmpITviITiGSE7vcdvBBJXUGNghtfZRVhhanwGf5FkS0UNDAXHziqE+rISs4Ve++sZj8tg==" - }, - "win32-x64": { - "asset": "sfw-free-windows-x86_64.exe", - "integrity": "sha512-kh7yWn5TzLGI+rpujGwzX+HIaVa1/BHTVpJ58cBAYnwPSuYRhgUciX0vqKKoK3P3nYYMGrN7IvhZ4L91krsYLw==" - } - }, - "origin": "gh-asset" - }, - "uv": { - "notes": [ - "uv (Astral) — the fleet's Python project tool. Installed in the bootstrap (release-asset, SRI-verified per platform, like janus/codedb) so a hash-locked uv install is available BEFORE the security-tools step that needs it (SkillSpector installs via a uv project + uv.lock, no pipx — the fleet 'uv for projects' rule). external-tools/update.mts re-hashes the GitHub release assets on a bump.", - "The GitHub release is a known-publisher binary distribution. The installer uses the version as the release tag." - ], - "description": "uv — Astral Python package/project manager (pinned, SRI-verified)", - "repository": "github:astral-sh/uv", - "version": "0.12.23", - "tag": "0.12.23", - "versionDate": "2026-10-03", - "binaryName": "uv", - "platforms": { - "darwin-arm64": { - "asset": "uv-aarch64-apple-darwin.tar.gz", - "integrity": "sha512-gfcAJYNFwKXKBwMYW/JKTLGO4O8YJ7bQAUhktvAG7nBRWwWhCBdLvFsFOL5qonUgZguve0oENxa2/o/ElT3E2A==" - }, - "darwin-x64": { - "asset": "uv-x86_64-apple-darwin.tar.gz", - "integrity": "sha512-T1F7q5xWEOkUPLGeekoWOwz8Cdkkc9akMfwLU2QO8i824WdfOipJ4/zddpkG/Xr9NWA+/LuyHFGwPdwtVv/grg==" - }, - "linux-arm64": { - "asset": "uv-aarch64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-ebIJu9Gjeq5JoMPzMgW3F8ZF87IiQRjGV48JaEriium+Qy1dYsEP720173ekwSwrKRvE0FdhPyRb8QUEsbxuBg==" - }, - "linux-x64": { - "asset": "uv-x86_64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-SYTJBAowVTXLInaa6v0NuBgzOOIC+CoupwDRwwcKHEHgYZOLUGOMUDgBEt2bX2n+Jc6Jz9PQfcvA7D+3Id+Gpg==" - }, - "win32-arm64": { - "asset": "uv-aarch64-pc-windows-msvc.zip", - "integrity": "sha512-RTNfWHH9F1k5oG1i4y8TACMbjh3PSyoR7bkoAKiaX0mNwZedokm4JXq3UpOZ/SwQZL6DpI1BRfuRsEwq2KtazQ==" - }, - "win32-x64": { - "asset": "uv-x86_64-pc-windows-msvc.zip", - "integrity": "sha512-8vtkGZbm5WXtEcuwkrgp50tNdxZpHah9tcwR/F9uMeNgw1yYTyVf6RYHhvliPnPUpMY3JEdzgUbID4LOhLflsw==" - } - }, - "origin": "gh-asset" - }, - "zizmor": { - "description": "GitHub Actions security linter — audits .github/ for workflow-injection / credential-leak patterns.", - "version": "1.30.1", - "repository": "github:zizmorcore/zizmor", - "tag": "v1.30.1", - "notes": [ - "Required: CI (blocks merges on medium+ findings)", - "Installed by the setup-and-install composite; SRI-verified (sha512) per platform" - ], - "platforms": { - "darwin-arm64": { - "asset": "zizmor-aarch64-apple-darwin.tar.gz", - "integrity": "sha512-0SJEhWNir4QzfGwrT2oX4w/4UJZp8CqDo2RK2bkcfHCilayAZkpvu/fq6rHo+X+gbJICazQBNRE2bo91mmNAKA==" - }, - "darwin-x64": { - "asset": "zizmor-x86_64-apple-darwin.tar.gz", - "integrity": "sha512-QqPld71wwABqcLyxkJ02nItUczTQrtpAAT6xiDvyyBySuywAFGW4mUU7iO2Kcwg+CDY5ZLvkd/vNm76GqqbwXw==" - }, - "linux-arm64": { - "asset": "zizmor-aarch64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-7AfMKqT+4SoyYRE4Ke7Ar5H+1SoU7QUuifuSKGjJIb7Qep29+oP0IrMsEMwjqAxpiGbEPyxSMgO7ghDlHhNsNQ==" - }, - "linux-x64": { - "asset": "zizmor-x86_64-unknown-linux-gnu.tar.gz", - "integrity": "sha512-sMHocgA1Rg12S12/szGaRumgmlAEjaeU5EIdGTczLNldhGFptlctCFqogoebWvE/PFioGlZ2vcSxoCZW3BegYg==" - }, - "win32-x64": { - "asset": "zizmor-x86_64-pc-windows-msvc.zip", - "integrity": "sha512-cYagZJ5fG+8UQ/Fr0LshEt7A6CXQgWeeBHlsztvzCepX661rrW9h3h//hedNUu7yM215f+fqDXE0++6MjuLgJg==" - } - }, - "origin": "gh-asset" - } - } -} diff --git a/.config/repo/rolldown/lib-stub.mts b/.config/repo/rolldown/lib-stub.mts deleted file mode 100644 index 76326e68..00000000 --- a/.config/repo/rolldown/lib-stub.mts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file Rolldown plugin: stub heavy `@socketsecurity/lib-stable` internals that - * runtime code never reaches. Why: `@socketsecurity/lib-stable` is the - * canonical fleet utility surface, but its module graph statically pulls in - * heavyweight files (e.g. globs.js → picomatch ~260KB, sorts.js → semver + - * npm-pack ~2.5MB) along import paths that real consumers never traverse. - * Tree-shaking can't drop unreachable subgraphs that look reachable to the - * static analyzer; we have to tell it explicitly. Each consumer passes a - * `stubPattern` regex matching the absolute resolved paths of the unreachable - * files for THEIR import surface. Verify reachability before adding a pattern - * — stubbing a file that IS reached at runtime gives runtime crashes, not - * bundle-time errors. Source: lifted from socket-packageurl-js's inline - * plugin (.config/repo/rolldown.config.mts), generalized so the stub-pattern - * is caller-provided. Fleet-canonical via socket-wheelhouse. - */ - -import type { Plugin } from 'rolldown' - -export type LibStubConfig = { - /** - * Regex matched against resolved module paths. Files matching get replaced - * with an empty CJS module. Required. - */ - readonly stubPattern: RegExp - /** - * Replacement code. Defaults to `module.exports = {}`. Override only if you - * need a non-empty stub (rare). - */ - readonly stubCode?: string | undefined -} - -export function createLibStubPlugin(config: LibStubConfig): Plugin { - const { stubCode = 'module.exports = {}', stubPattern } = { - __proto__: null, - ...config, - } as LibStubConfig - return { - name: 'stub-unused-lib-internals', - load(id) { - if (stubPattern.test(id)) { - return { code: stubCode, moduleSideEffects: false } - } - return undefined - }, - } -} diff --git a/.config/repo/root-files.json b/.config/repo/root-files.json deleted file mode 100644 index 3095bfb4..00000000 --- a/.config/repo/root-files.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "allow": { - ".claude-plugin": "Claude Code plugin manifest dir — the plugin loader discovers it at the repo root by convention.", - ".cursor-plugin": "Cursor plugin manifest dir — the Cursor plugin loader discovers it at the repo root by convention.", - ".husky": "Husky hook dir — husky resolves it from the repo root by convention.", - ".kimi-code": "Kimi Code agent config dir — the tool discovers it at the repo root by convention.", - ".opencode": "OpenCode agent config dir — the opencode CLI discovers it at the repo root by convention.", - "agents": "Repo content dir — published agent definitions, a product surface of this repo.", - "gemini-extension.json": "Gemini CLI extension manifest — the gemini CLI discovers it at the repo root by convention.", - "opencode.json": "OpenCode config — the opencode CLI reads it from the repo root by convention.", - "release-kit": "Repo content dir — the packaged release tooling this repo ships (payload, install seams, examples), a product surface of this repo.", - "skills": "Repo content dir — this repo's published skill definitions live here; the name is the product surface." - } -} diff --git a/.config/repo/socket-wheelhouse-schema.json b/.config/repo/socket-wheelhouse-schema.json deleted file mode 100644 index e4bceea2..00000000 --- a/.config/repo/socket-wheelhouse-schema.json +++ /dev/null @@ -1,1898 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/SocketDev/socket-wheelhouse-schema.json", - "title": "socket-wheelhouse per-repo config", - "type": "object", - "required": [ - "schemaVersion", - "repoName", - "repo", - "build" - ], - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema reference for editor autocompletion. Conventionally `./socket-wheelhouse-schema.json` — both the config and its schema live side-by-side in `.config/`." - }, - "schemaVersion": { - "type": "number", - "const": 1, - "description": "Schema version. Bump on breaking changes; readers gate on it." - }, - "repoName": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*$", - "description": "Canonical repo basename (e.g. `socket-lib`, `ultrathink`). Used for shape-independent exemptions like the oxlint `socket-lib` carve-out." - }, - "repo": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "solo" - }, - { - "type": "string", - "const": "mono" - } - ], - "description": "Package layout. `solo` = one `package.json` at root, no `packages/`. `mono` = pnpm workspaces under `packages/`." - } - }, - "description": "Repo shape.", - "additionalProperties": false - }, - "build": { - "type": "object", - "required": [ - "from", - "type" - ], - "properties": { - "from": { - "anyOf": [ - { - "type": "string", - "const": "npm-registry" - }, - { - "type": "string", - "const": "github-release" - }, - { - "type": "string", - "const": "crates-registry" - }, - { - "type": "string", - "const": "go-registry" - }, - { - "type": "string", - "const": "github-action" - } - ], - "description": "Release source/target. `npm-registry` = published as an npm package. `github-release` = raw artifacts attached to a GitHub Release. `crates-registry` = published as a Rust crate to crates.io. `go-registry` = the Go module ecosystem — published by pushing a semver tag; proxy.golang.org fetches it, pkg.go.dev indexes it (no registry upload/token). `github-action` = consumed as `owner/repo@` straight from the git tree; like `go-registry` it publishes by pushing a tag with no registry upload, and unlike every other value the COMMITTED build output is the artifact a consumer runs, so it must be rebuilt in the same change as its sources." - }, - "type": { - "anyOf": [ - { - "type": "string", - "const": "js" - }, - { - "type": "string", - "const": "addon" - }, - { - "type": "string", - "const": "binary" - }, - { - "type": "string", - "const": "rust" - }, - { - "type": "string", - "const": "go" - } - ], - "description": "Artifact kind. `js` = plain JS package. `addon` = `.node` native addon. `binary` = a native binary (executable or wasm module — wasm is a binary format, so it lives here, not its own value). `rust` = a native Rust crate (single crate or a Cargo workspace of crates) published to crates.io — no JS build. `go` = a native Go module with no JS build (symmetric to `rust`)." - }, - "bundlesVendoredDeps": { - "type": "boolean", - "description": "The build inlines its vendored dependencies into the published artifact, so the cascade treats those deps as shipped rather than resolved at install. Read by commit-cascade/utils.mts as a repo capability; declared here because a field the fleet reads but the schema rejects fails socket-wheelhouse-config-matches-schema in every member that sets it." - }, - "runtime": { - "anyOf": [ - { - "type": "string", - "const": "node" - }, - { - "type": "string", - "const": "bun" - }, - { - "type": "string", - "const": "deno" - } - ], - "description": "JS/TS execution runtime for a `type: js` repo — mirrors package.json `devEngines.runtime.name`. `node` (default, omit to get it) = the fleet standard: pnpm for deps, vitest for tests, node to run. `bun` = a Bun repo (bunfig.toml + bun.lock + `bun test`). `deno` = a Deno repo (deno.json + `deno test`). For any non-node runtime the cascade relaxes its pnpm/vitest/node expectations and keeps the repo’s own toolchain intact. Ignored for native builds (`rust`/`go`/`addon`/`binary`)." - }, - "coverageArtifacts": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!/)(?!.*\\\\)(?!.*(^|/)\\.\\.?(/|$)).+$" - }, - "description": "Relative build outputs that coverage can share across shards. The producer hashes every regular file and consumers require an exact source revision and inventory match. Omit to retain per-shard builds.", - "minItems": 1 - } - }, - "description": "How the repo is built + released. Drives the release-checksums file cascade + CI breadth. `from: github-release` repos build their own native artifacts and attach them to a GitHub Release; `from: npm-registry` + non-`js` type wrap prebuilt native bits; `type: js` is a plain package; `from: crates-registry` + `type: rust` is a native Rust crate (crates.io provides integrity, so no release-checksums cascade).", - "additionalProperties": false - }, - "secondaries": { - "type": "array", - "items": { - "type": "object", - "required": [ - "from", - "type" - ], - "properties": { - "from": { - "anyOf": [ - { - "type": "string", - "const": "npm-registry" - }, - { - "type": "string", - "const": "github-release" - }, - { - "type": "string", - "const": "crates-registry" - }, - { - "type": "string", - "const": "go-registry" - }, - { - "type": "string", - "const": "github-action" - } - ], - "description": "Release source/target. `npm-registry` = published as an npm package. `github-release` = raw artifacts attached to a GitHub Release. `crates-registry` = published as a Rust crate to crates.io. `go-registry` = the Go module ecosystem — published by pushing a semver tag; proxy.golang.org fetches it, pkg.go.dev indexes it (no registry upload/token). `github-action` = consumed as `owner/repo@` straight from the git tree; like `go-registry` it publishes by pushing a tag with no registry upload, and unlike every other value the COMMITTED build output is the artifact a consumer runs, so it must be rebuilt in the same change as its sources." - }, - "type": { - "anyOf": [ - { - "type": "string", - "const": "js" - }, - { - "type": "string", - "const": "addon" - }, - { - "type": "string", - "const": "binary" - }, - { - "type": "string", - "const": "rust" - }, - { - "type": "string", - "const": "go" - } - ], - "description": "Artifact kind. `js` = plain JS package. `addon` = `.node` native addon. `binary` = a native binary (executable or wasm module — wasm is a binary format, so it lives here, not its own value). `rust` = a native Rust crate (single crate or a Cargo workspace of crates) published to crates.io — no JS build. `go` = a native Go module with no JS build (symmetric to `rust`)." - } - }, - "description": "An additional publish channel beyond the primary `build`, e.g. `{from:npm-registry, type:addon}` for a `.node` addon shipped alongside a Rust crate.", - "additionalProperties": false - }, - "description": "Additional publish channels beyond the primary `build` — e.g. a Rust crate (crates-registry/rust) that also ships a `.node` addon to npm carries `{from:npm-registry, type:addon}`. Each channel gets its own publish workflow." - }, - "ai": { - "type": "object", - "properties": { - "localAssist": { - "type": "boolean", - "description": "Opt into keyless single-shot AI assists via the odai CLI from SocketDev/odai — on-device backends such as Gemini Nano through headless Chrome, a loopback llama-server, or the deterministic simulator; no ANTHROPIC_API_KEY involved. Summary-class tasks only, read by scripts/fleet/ai/odai.mts consumers such as the land-work commit-body summarizer. Default false; when no odai backend resolves the assist is a clean skip, never a failure." - } - }, - "description": "Keyless local AI opt-ins. Per-repo, default all-off." - }, - "credentials": { - "type": "object", - "properties": { - "secretResolvers": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "const": "one-password" - }, - { - "type": "string", - "const": "bitwarden" - }, - { - "type": "string", - "const": "vault" - }, - { - "type": "string", - "const": "aws-secrets-manager" - } - ] - }, - "description": "Ordered secret-manager resolvers for credential discovery. Default: [\"one-password\"]. Each resolver fails closed: an unresolvable reference errors rather than passing through." - } - } - }, - "apple": { - "type": "object", - "properties": { - "developerIdProfile": { - "type": "object", - "required": [ - "bundleIdentifier" - ], - "properties": { - "bundleIdentifier": { - "type": "string", - "description": "Explicit macOS App ID used by the Developer ID provisioning profile.", - "maxLength": 255, - "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$" - }, - "keychainAccessGroupSuffixes": { - "type": "array", - "items": { - "type": "string", - "maxLength": 255, - "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$" - }, - "description": "Team-relative keychain access groups the profile must authorize. Defaults to the bundle identifier.", - "minItems": 1, - "uniqueItems": true - } - }, - "additionalProperties": false, - "description": "Developer ID provisioning profile used by a macOS app bundle with restricted entitlements." - } - }, - "additionalProperties": false, - "description": "Apple Developer signing configuration." - }, - "buildStubs": { - "type": "object", - "properties": { - "keepExposed": { - "type": "array", - "items": { - "type": "object", - "required": [ - "leaf", - "reason" - ], - "properties": { - "leaf": { - "type": "string", - "description": "Export subpath, without the leading `./`. Example: `process/spawn/retry/node`." - }, - "reason": { - "type": "string", - "description": "The consumer this leaf waits for. Remove the entry once that consumer lands; the audit then reports the entry as redundant." - } - }, - "additionalProperties": false - }, - "description": "Leaves that ship with a real implementation even though no roster repo imports them yet. The stub list is consumer-driven, so a primitive published for the fleet to adopt is stubbed on its first release - and a stubbed leaf cannot acquire the consumer that would un-stub it." - }, - "unexposed": { - "type": "object", - "required": [ - "leaves", - "scannedRoster" - ], - "properties": { - "leaves": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Export subpaths the audit proved unused, swapped for throwing stubs at build time." - }, - "scannedRoster": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The repos the verdict was judged against. \"Unused\" is a claim about the whole fleet, so the list is only as good as the consumer set behind it - and with no record, one computed against a smaller fleet reads exactly like a correct one." - } - }, - "additionalProperties": false, - "description": "Regenerated by `audit-fleet-lib-usage.mts --write-stub-list` rather than hand-maintained. Committed so the published build reproduces without sibling checkouts." - } - }, - "additionalProperties": false, - "description": "Leaf lists for the published build-stub pass. Only a repo that publishes a per-leaf subpath surface sets this." - }, - "capabilities": { - "type": "object", - "properties": { - "cargo": { - "type": "array", - "items": { - "type": "string" - }, - "description": "This repo ships Rust; the value is the repo-relative paths of the package roots holding it (`[\".\"]` when the Cargo workspace sits at the repo root). Declaring `cargo` activates the `rust` coverage lane in `pnpm run cover`, folds its line coverage into the README badge, and arms the coverage-lanes-are-wired check: a declared capability whose lane measures nothing fails the gate instead of passing silently, while a machine with no cargo toolchain reports an explicit skip. `cargo` also gates capability-tagged fleet hooks at cascade time — an artifact whose header declares `@capability cargo` is installed only into a repo that declares this key (scripts/repo/commit-cascade/capabilities.mts)." - }, - "cpp": { - "type": "array", - "items": { - "type": "string" - }, - "description": "This repo ships C/C++; the value is the repo-relative paths of the package roots holding it. Declaring `cpp` activates the `cpp` coverage lane in `pnpm run cover`, folds its line coverage into the README badge, and arms the coverage-lanes-are-wired check: a declared capability whose lane measures nothing fails the gate instead of passing silently, while a machine with no C/C++ toolchain reports an explicit skip." - }, - "go": { - "type": "array", - "items": { - "type": "string" - }, - "description": "This repo ships Go; the value is the repo-relative paths of the package roots holding it. Declaring `go` activates the `go` coverage lane in `pnpm run cover`, folds its line coverage into the README badge, and arms the coverage-lanes-are-wired check: a declared capability whose lane measures nothing fails the gate instead of passing silently, while a machine with no Go toolchain reports an explicit skip." - }, - "swift": { - "type": "array", - "items": { - "type": "string" - }, - "description": "This repo ships Swift; the value is the repo-relative paths of the package roots holding it (an Xcode project directory, or a SwiftPM package root). Declaring `swift` activates the `swift` coverage lane in `pnpm run cover`, folds its line coverage into the README badge, and arms the coverage-lanes-are-wired check: a declared capability whose lane measures nothing fails the gate instead of passing silently, while a machine with no Swift toolchain reports an explicit skip. The common shape is a hybrid member whose logic is Rust and whose macOS shell is SwiftUI, so `swift` usually appears beside `cargo` rather than alone." - } - }, - "additionalProperties": false, - "description": "Language capabilities beyond JS/TS. Keys must stay in lockstep with VALID_CAPABILITIES in scripts/repo/commit-cascade/repo-shape.mts and LANE_BY_CAPABILITY in scripts/fleet/cover/lanes.mts." - }, - "claude": { - "type": "object", - "properties": { - "includeSecurityScanSkill": { - "type": "boolean", - "description": "Ship `.claude/skills/fleet/scanning-security/SKILL.md`." - }, - "includeSharedSkills": { - "type": "boolean", - "description": "Ship `.claude/skills/fleet/_shared/*` — env-check, path-guard-rule, report-format, security-tools, verify-build." - }, - "includeUpdatingSkill": { - "type": "boolean", - "description": "Ship the dependency-update skill. Reserved — no consumer wired today." - } - }, - "description": "Claude Code opt-ins." - }, - "cover": { - "type": "object", - "properties": { - "typeThreshold": { - "type": "number", - "minimum": 0, - "maximum": 100, - "description": "Minimum typed identifier percentage enforced by cover and cover:types." - }, - "budgetAllowances": { - "type": "object", - "properties": { - "fast": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "mid": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "slow": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "description": "Measured coverage lane limits. Expiry is never or a date at 00:00 UTC, at most one calendar month after created. The run-wide deadline still applies." - }, - "suites": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "config": { - "type": "string", - "description": "Explicit vitest config path override (repo-root-relative) for this suite; defaults to the repo-first resolution of the suite basename." - }, - "runExclude": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs passed as `vitest --exclude` for this suite — skips running matching test files (e.g. a cross-package test that would pollute this repo’s coverage denominator)." - } - }, - "additionalProperties": false - } - }, - "description": "Per-suite cover overrides, keyed by suite name (unit, shared, isolated, …)." - }, - "cumulativeCoverageThreshold": { - "type": "number", - "minimum": 0, - "maximum": 100, - "description": "Minimum weighted executable coverage across enabled languages: Go statements and JavaScript/Rust/C++ lines; types remain separate." - }, - "thresholds": { - "type": "object", - "properties": { - "statements": { - "type": "number" - }, - "branches": { - "type": "number" - }, - "functions": { - "type": "number" - }, - "lines": { - "type": "number" - } - }, - "additionalProperties": false, - "description": "Per-metric coverage thresholds (percent) the cover suite enforces; an absent metric inherits the fleet default." - }, - "groupThresholds": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "statements": { - "type": "number" - }, - "branches": { - "type": "number" - }, - "functions": { - "type": "number" - }, - "lines": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "description": "Aggregate coverage floors keyed by repo-relative source glob. Matching files combine their Istanbul counters after tier merging." - }, - "perFileThresholds": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "statements": { - "type": "number" - }, - "branches": { - "type": "number" - }, - "functions": { - "type": "number" - }, - "lines": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "description": "Per-file coverage thresholds (percent), keyed by repo-root-relative file path; a file listed here is held to these numbers instead of the repo-wide `thresholds`." - }, - "runner": { - "anyOf": [ - { - "type": "string", - "const": "bun" - }, - { - "type": "string", - "const": "vitest" - } - ], - "description": "Which test runner the cover suite drives. Set this to match the repo’s own `test` script — a repo whose tests run under bun but is left on the vitest default collects no coverage and reports a false green." - } - }, - "additionalProperties": false, - "description": "Coverage config the `cover` suite reads (folded in from the former .config/repo/cover.json): per-suite run overrides + per-metric thresholds. Absent = fleet defaults." - }, - "coverage": { - "type": "object", - "properties": { - "include": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Coverage include globs that REPLACE (not extend) the fleet-default `src/**` candidate set. The route for a repo whose instrumentable source lives elsewhere — a monorepo maps `packages/*/src/**/*.{ts,mts,cts}` (+ a `!packages/*/src/external/**` negation); the wheelhouse maps `scripts/**`. Absent = the fleet default stands." - }, - "exclude": { - "type": "object", - "properties": { - "add": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs appended to the fleet-default coverage excludes — repo-specific dirs to drop from the denominator." - }, - "remove": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Fleet-default exclude entries to filter OUT (exact string match). A monorepo removes `packages/**` so its package source becomes measurable; the wheelhouse removes `scripts/**` so its script source is measured." - } - }, - "additionalProperties": false, - "description": "Deltas against the fleet-default coverage excludes: `remove` filters base entries out, `add` appends new ones." - } - }, - "additionalProperties": false, - "description": "Coverage include/exclude overlay the canonical coverage config (.config/fleet/vitest.coverage.fleet.config.mts) reads (folded in from the former standalone .config/repo/coverage.json). `include` REPLACES the fleet default; `exclude.remove`/`exclude.add` filter/append base excludes. Absent = fleet defaults." - }, - "design": { - "type": "object", - "properties": { - "contrast": { - "type": "object", - "required": [ - "files" - ], - "properties": { - "files": { - "type": "array", - "items": { - "type": "object", - "required": [ - "path", - "checks" - ], - "properties": { - "path": { - "type": "string", - "description": "Repo-relative path to the file whose colors are checked." - }, - "checks": { - "type": "array", - "items": { - "type": "object", - "required": [ - "selector", - "bg" - ], - "properties": { - "selector": { - "type": "string", - "description": "CSS selector (regex-escaped) whose foreground color is checked." - }, - "bg": { - "type": "string", - "description": "Background color (hex) the foreground is measured against." - }, - "minRatio": { - "type": "number", - "description": "Minimum contrast ratio. Defaults to 4.5 (WCAG AA)." - }, - "label": { - "type": "string", - "description": "Human-readable label for the check." - } - }, - "additionalProperties": false, - "description": "One foreground/background contrast pair to verify." - }, - "description": "The contrast pairs to verify in this file." - } - }, - "additionalProperties": false, - "description": "A file and the set of contrast pairs to verify within it." - }, - "description": "Files with contrast pairs to verify." - } - }, - "additionalProperties": false, - "description": "WCAG color-contrast budget for the repo." - } - }, - "additionalProperties": false, - "description": "Per-repo design budgets (opt-in; only repos shipping UI assets set this)." - }, - "docker": { - "type": "object", - "properties": { - "prebakes": { - "type": "object", - "required": [ - "registry", - "prebakes" - ], - "properties": { - "description": { - "type": "string" - }, - "registry": { - "type": "string", - "description": "Registry images are pushed to / pulled from." - }, - "registryDescription": { - "type": "string", - "description": "What the registry namespace is for, including the long-form browse URL when the registry value is a short form." - }, - "pins": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "depotVersion": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "description": "Exact Depot CLI version used to build the image pack." - }, - "ubuntuDigest": { - "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$", - "description": "Digest the ubuntu roots FROM, pinning the OS layer." - }, - "ubuntuTag": { - "type": "string", - "description": "Human-readable ubuntu tag the digest corresponds to." - }, - "aptSnapshot": { - "type": "string", - "pattern": "^[0-9]{8}T[0-9]{6}Z$", - "description": "Snapshot timestamp (YYYYMMDDTHHMMSSZ) apt is pinned to, freezing transitive deps." - }, - "go": { - "type": "object", - "required": [ - "version", - "sha256" - ], - "properties": { - "version": { - "type": "string" - }, - "sha256": { - "type": "object", - "required": [ - "amd64", - "arm64" - ], - "properties": { - "amd64": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "arm64": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "description": "Go toolchain version + per-arch sha256." - }, - "odai": { - "type": "object", - "required": [ - "version", - "integrity" - ], - "properties": { - "version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" - }, - "integrity": { - "type": "string", - "pattern": "^sha512-[A-Za-z0-9+/]+={0,2}$" - } - }, - "additionalProperties": false - }, - "rustup": { - "type": "object", - "required": [ - "version", - "sha256" - ], - "properties": { - "version": { - "type": "string" - }, - "sha256": { - "type": "object", - "required": [ - "amd64", - "arm64" - ], - "properties": { - "amd64": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "arm64": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "description": "rustup-init version + per-arch sha256 (mirrors the .sha256 rustup publishes beside each binary)." - }, - "emsdkVersion": { - "type": "string" - } - }, - "additionalProperties": false, - "description": "Maximally-pinned build inputs injected as build-args." - }, - "prebakes": { - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "status", - "from", - "installs", - "purpose" - ], - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$", - "description": "Image name. Toolchain-named, not output-named." - }, - "status": { - "anyOf": [ - { - "type": "string", - "const": "active" - }, - { - "type": "string", - "const": "planned" - } - ], - "description": "`active` = built + pushed today; `planned` = designed only." - }, - "from": { - "type": "string", - "description": "Parent image: another prebake `name`, or an external `:`." - }, - "vendorSource": { - "type": "string", - "description": "Upstream recipe this layer is built from when vendored rather than pulled." - }, - "dockerfile": { - "type": "string", - "pattern": "^(?:packages/[a-z0-9-]+/docker|docker/(?:fleet|repo))/[a-z0-9-]+\\.Dockerfile$", - "description": "Repo-relative path to the Dockerfile that builds it." - }, - "installs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Toolchains/packages this layer adds on top of `from`." - }, - "libc": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "const": "glibc" - }, - { - "type": "string", - "const": "musl" - } - ] - }, - "description": "libc variants built." - }, - "platforms": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Target platforms (Docker `os/arch`)." - }, - "tagFrom": { - "type": "string", - "description": "Source of the content hash deciding when to rebuild." - }, - "warmTargets": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Intermediate Dockerfile stages baked cache-only (--target, no tag/push) BEFORE the full build, so a final-stage failure cannot cancel and lose their in-flight layers." - }, - "project": { - "type": "string", - "description": "Build-cache project id, if any." - }, - "consumers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Repos / builders that FROM this base." - }, - "purpose": { - "type": "string", - "minLength": 1, - "description": "Why this layer exists and what lands on it." - } - }, - "additionalProperties": false, - "description": "One prebaked base image." - }, - "description": "Each prebaked base image, ordered bottom-up." - } - }, - "additionalProperties": false, - "description": "Layered prebaked base-image manifest." - } - }, - "additionalProperties": false, - "description": "Per-repo Docker infrastructure (opt-in; only repos maintaining base images set this)." - }, - "docs": { - "type": "object", - "properties": { - "surfaces": { - "type": "array", - "items": { - "type": "object", - "required": [ - "kind", - "hub" - ], - "properties": { - "kind": { - "anyOf": [ - { - "type": "string", - "const": "api" - }, - { - "type": "string", - "const": "commands" - }, - { - "type": "string", - "const": "service" - }, - { - "type": "string", - "const": "guide" - }, - { - "type": "string", - "const": "action" - }, - { - "type": "string", - "const": "workflows" - } - ] - }, - "hub": { - "type": "string", - "pattern": "^(https://|docs/)" - }, - "sources": { - "type": "array", - "items": { - "type": "string" - } - }, - "generate": { - "type": "string" - }, - "check": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "apiMd": { - "type": "boolean", - "description": "Generate `docs/api.md` from the package.json `exports` map via `scripts/fleet/gen/api-md.mts`. Off unless set to true." - }, - "llmsTxt": { - "type": "boolean", - "description": "Generate the root `llms.txt` export index from the package.json `exports` map via `scripts/fleet/gen/llms-txt.mts`. Off unless set to true." - } - }, - "additionalProperties": false, - "description": "Per-repo public documentation surfaces and opt-ins for fleet doc generators. Surface hubs can be local under docs/ or hosted URLs. Existing apiMd and llmsTxt options keep their export-index behavior." - }, - "github": { - "type": "object", - "properties": { - "apps": { - "type": "array", - "items": { - "type": "string" - }, - "description": "GitHub App slugs that must be installed on the repo (e.g. `cursor`, `socket-security`, `socket-trufflehog`). Audited by `scripts/fleet/lint-github-settings.mts` — apps whose installation cannot be reliably detected via check-suites are trusted via this manifest." - }, - "codeql": { - "type": "boolean", - "description": "Opt into fleet-managed CodeQL scanning and conditional workflow delivery. Absent or false disables delivery. Initial sync preserves custom workflows and removes byte-identical managed copies. Cloud setup transitions require an authenticated local administrator; this flag grants no bot permissions." - } - }, - "description": "GitHub-related fleet config." - }, - "hooks": { - "type": "object", - "properties": { - "enablePrePush": { - "type": "boolean", - "description": "Wire `.git-hooks/pre-push` (shell shim) → `.git-hooks/pre-push.mts`. Mandatory security gate; default true." - }, - "enableCommitMsg": { - "type": "boolean", - "description": "Wire `.git-hooks/commit-msg` (shell shim) → `.git-hooks/commit-msg.mts`. Strips AI attribution; default true." - }, - "enablePreCommit": { - "type": "boolean", - "description": "Wire `.git-hooks/pre-commit` (shell shim) → `.git-hooks/pre-commit.mts`. Lint + secret scan on staged files; default true." - }, - "preCommitVariant": { - "anyOf": [ - { - "type": "string", - "const": "lint-only" - }, - { - "type": "string", - "const": "lint-test" - } - ], - "description": "`lint-only` runs format + secret scan; `lint-test` adds vitest on touched packages. Default `lint-test`." - } - }, - "description": "Git-hook opt-ins." - }, - "lint": { - "type": "object", - "properties": { - "profile": { - "anyOf": [ - { - "type": "string", - "const": "standard" - }, - { - "type": "string", - "const": "rich" - } - ], - "description": "`standard` requires the fleet plugin set (import + typescript + unicorn). `rich` opts into a wider set; check the runner for the exact basenames currently exempted." - } - }, - "description": "oxlint profile." - }, - "lockstep": { - "type": "object", - "properties": { - "roots": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "description": "Per-language impl roots the hook resolves `Lock-step with : ` refs against, most-preferred first. Keys are the `` tokens used in comments (`Rust`, `C++`, `TS`, …); values are repo-relative candidate dirs." - }, - "scan": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Directories the lock-step comment scanner walks for `Lock-step` refs." - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Source-file extensions (leading dot) the comment scanner considers." - } - }, - "additionalProperties": false, - "description": "Opt-in config for the `lock-step-ref-nudge` hook — validates `Lock-step with/from : ` code comments against real impl paths. Absent = malformed-shape checks only (stale-path checks off)." - }, - "napi": { - "type": "object", - "required": [ - "platforms" - ], - "properties": { - "platforms": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "const": "darwin-arm64" - }, - { - "type": "string", - "const": "darwin-x64" - }, - { - "type": "string", - "const": "linux-arm64-gnu" - }, - { - "type": "string", - "const": "linux-arm64-musl" - }, - { - "type": "string", - "const": "linux-x64-gnu" - }, - { - "type": "string", - "const": "linux-x64-musl" - }, - { - "type": "string", - "const": "wasm32-wasi" - }, - { - "type": "string", - "const": "win32-arm64-msvc" - }, - { - "type": "string", - "const": "win32-x64-msvc" - } - ] - }, - "description": "The napi targets this repo ships a .node addon for — the fleet-canonical NAPI_TARGETS (napi-rs vocabulary: -gnu/-musl/-msvc explicit, win32 not win). Drives the canonical CI build matrix; one build job per target.", - "minItems": 1 - }, - "runners": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "string" - } - }, - "description": "Optional per-target GitHub Actions runner overrides (napi target → runner label), for a repo needing a non-default image (e.g. darwin-x64 pinned to a specific intel-mac runner). Targets without an override use the fleet default runner." - } - }, - "additionalProperties": false, - "description": "Native napi .node addon distribution: which platform targets this repo builds + publishes, plus optional per-target runner overrides. Drives the canonical per-platform build matrix so no member hardcodes its own targets list." - }, - "pathsAllowlist": { - "type": "array", - "items": { - "type": "object", - "required": [ - "reason" - ], - "properties": { - "rule": { - "type": "string", - "description": "Rule letter (A, B, C, D, F, G). Omit to match any rule." - }, - "file": { - "type": "string", - "description": "Substring match against the relative file path." - }, - "pattern": { - "type": "string", - "description": "Substring match against the offending snippet." - }, - "line": { - "type": "number", - "description": "Exact line number. Strict — no fuzz tolerance." - }, - "snippet_hash": { - "type": "string", - "description": "12-char SHA-256 prefix of the normalized snippet (whitespace collapsed). Drift-resistant: keeps matching after reformatting that doesn't change the offending construction. Get via `node scripts/fleet/check/paths-are-canonical.mts --show-hashes`." - }, - "reason": { - "type": "string", - "description": "Why this site is genuinely exempt. Required." - } - }, - "description": "One exemption for the path-hygiene gate." - }, - "description": "Exemptions for the path-hygiene gate (scripts/fleet/check/paths-are-canonical.mts). Each entry needs a `reason`; prefer narrow entries (rule + file + snippet_hash + pattern) over blanket file-level exempts." - }, - "release": { - "type": "object", - "properties": { - "artifactWorkflow": { - "type": "string", - "pattern": "^\\.github/workflows/[A-Za-z0-9][A-Za-z0-9._-]*\\.yml(?![\\s\\S])", - "description": "Repo-relative root workflow path that publishes the npm artifact." - }, - "github": { - "type": "boolean", - "description": "Whether this repo publishes GitHub Releases. Defaults to true. Explicit false removes the fleet-owned release-github.yml workflow and skips GitHub tag, release, and asset creation. Use false for repositories that distribute entirely through npm or another registry. npm publication still requires approval, live registry verification, and applicable provenance checks. The cascade, bootstrap, release runner, and release checks share this setting." - }, - "githubAssetWorkflow": { - "type": "string", - "maxLength": 128, - "minLength": 5, - "pattern": "^[a-z0-9][a-z0-9_.-]*\\.ya?ml$", - "description": "Workflow filename under .github/workflows that produces the fixed github-release-assets artifact for each release tag. The GitHub release waits for a successful push run at the exact tag commit, verifies the artifact manifest and hashes, and uploads only those verified files. Leave unset when the release has no attached assets." - }, - "releaseLine": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "The ref the customer release line lives on, e.g. `origin/v1.x`. Set it only when releases are cut somewhere other than the branch being scanned — a repo may carry several independent release lines at once, and their divergence is the architecture rather than a defect. Consumers resolve the release boundary as the newest tag reachable from THIS ref instead of from the scanned ref. Second in precedence: `boundaryTag` overrides it, and `tagPattern` filters the ancestry search it selects the ref for." - }, - "boundaryTag": { - "type": "string", - "description": "The tag that IS the release boundary, e.g. `v1.1.152`. First in precedence — it overrides `branch` and `tagPattern` alike, because it names the answer outright and leaves nothing to search. Use it when the line's newest release is not the newest tag reachable from any ref — history at or below this tag is published and frozen, so scripts/fleet/check/commits-have-no-ai-attribution.mts reports findings there as frozen instead of actionable." - }, - "tagPattern": { - "type": "string", - "description": "A `git tag --list` glob naming which tags are RELEASE tags, e.g. `v*`. Set it when the repo pushes tags that are not releases — build-asset and bundle tags such as `fleet-pack-` or `base-assets--` sit on the same branch and are newer, so an unfiltered newest-ancestor pick lands on one of them instead of the real release. The glob filters the candidate tags BEFORE the newest-ancestor pick, so only matching tags can become the boundary. Third in precedence: `boundaryTag` wins outright, `branch` chooses which ref the ancestry search walks, and this narrows what that search may return. Unlike a hand-pinned `boundaryTag` it does not go stale — the next release tag matching the glob becomes the boundary on its own. A declared pattern that matches no ancestor tag fails loud rather than falling back to unfiltered ancestry, since a silent fallback would reinstate the asset tag the pattern exists to exclude." - } - }, - "additionalProperties": false, - "description": "Where this repo's customer release line lives, for gates that must tell published history from rewritable history. Resolved OFFLINE and by ANCESTRY: never by tag recency, since the newest tag by date can belong to a release line that never ships. Precedence is `boundaryTag`, then `branch`, then `tagPattern` filtering the ancestry search, then unfiltered ancestry." - }, - "publishedPackages": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "The npm package names this repo publishes, for the repos where the root package.json cannot answer. A monorepo whose root manifest is `private: true` (or whose published artifact is assembled by a builder package under a name no manifest carries) has no on-disk name for a check to derive, so it declares the names here. Read by scripts/fleet/check/trusted-publishers-match-source.mts, which resolves the packages it audits as: explicit argv names, then this key, then the root manifest's own name. An empty array disables npm workflow delivery. Leave it unset in the ordinary case where the root manifest already names the published package — a stale copy here would send the check at the wrong package's binding." - }, - "provenanceOrphanBaseline": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "reason" - ], - "properties": { - "id": { - "type": "string", - "description": "The published artifact as `@`, e.g. `@socketsecurity/lib@6.5.0`. Matched exactly against the audited package name and version." - }, - "reason": { - "type": "string", - "description": "Why this orphan is grandfathered rather than fixed. Required — one line." - } - }, - "additionalProperties": false, - "description": "One grandfathered provenance orphan." - }, - "description": "Published versions frozen in a state no commit can repair, grandfathered so check/release-tags-match-provenance.mts reports them informationally instead of failing. Covers both kinds: a version whose attested commit no release tag reaches, and a version published with NO attestation at all (npm mints attestations at publish time and they are immutable, so provenance can never be added retroactively). A RATCHET: history is frozen and its only remedy is a human decision, so it may not block main — but any version NOT listed here fails the gate, which is what forces every new release through the pipeline with publishConfig.provenance:true, and an entry whose version has since been reconciled fails as STALE so the list can only shrink." - }, - "versionPolicy": { - "anyOf": [ - { - "type": "string", - "const": "standard" - }, - { - "type": "string", - "const": "patch-only" - } - ], - "description": "Version-bump policy enforced by bump.mts. `standard` (default): derive major/minor/patch from Conventional Commits. `patch-only`: reject any major/minor bump — only the patch may increment (e.g. socket-wheelhouse stays 1.0.x)." - }, - "versionSource": { - "type": "string", - "minLength": 1, - "description": "Repo-relative path of the manifest that owns the release version, granting that ONE file the exemption from the private-package 0.0.0 pin. Declare it when the root manifest is private yet versioned for a non-npm channel (a browser-store or marketplace listing, a binary release), or when a workspace designates one member as the lockstep bump source. Read by scripts/fleet/release/version-source.mts — the single answer shared by private-packages-are-unpublishable, bump.mts, and any build stamping a version. Leave it unset when every package publishes to npm: each manifest's own version is then its released version." - }, - "latestDistTagBranch": { - "type": "string", - "description": "The branch that owns the `latest` npm dist-tag — the line customers get from a bare `npm install `. Defaults to the repo's default branch, which is right for almost every member; set it only when the consumable line lives elsewhere, such as a maintenance branch shipping to users while the default branch carries a prerelease major. publish-npm.yml refuses a `latest` publish dispatched from any other branch." - } - }, - "additionalProperties": false, - "description": "Release / version-bump policy." - }, - "scripts": { - "type": "object", - "properties": { - "commandGroups": { - "type": "object", - "patternProperties": { - "^[a-z][a-z0-9-]*$": { - "type": "object", - "required": [ - "commands", - "tests" - ], - "properties": { - "commands": { - "type": "object", - "required": [ - "check", - "fix", - "test", - "update" - ], - "properties": { - "check": { - "type": "string", - "pattern": "^[a-z][a-z0-9:-]*$" - }, - "fix": { - "type": "string", - "pattern": "^[a-z][a-z0-9:-]*$" - }, - "test": { - "type": "string", - "pattern": "^[a-z][a-z0-9:-]*$" - }, - "update": { - "type": "string", - "pattern": "^[a-z][a-z0-9:-]*$" - } - }, - "additionalProperties": false - }, - "tests": { - "type": "array", - "items": { - "type": "string", - "pattern": "^test/[^\\\\]+\\.(test|spec)\\.[cm]?[jt]sx?$" - }, - "minItems": 1 - }, - "requires": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "string", - "const": "cargo" - }, - { - "type": "string", - "const": "cpp" - }, - { - "type": "string", - "const": "go" - }, - { - "type": "string", - "const": "swift" - } - ] - } - } - }, - "additionalProperties": false - } - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Override REQUIRED_SCRIPTS from manifest.mts. Usually omitted — the fleet default applies." - }, - "optional": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "boolean" - } - }, - "description": "Per-script opt-in map keyed by script name. `true` = repo ships this RECOMMENDED script; `false` = explicit opt-out." - }, - "bodyExempt": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Script names whose body is allowed to drift from the canonical form (e.g. socket-lib runs a richer test runner than the standard `node scripts/fleet/test.mts`). Each entry is the script name only." - } - }, - "description": "package.json script tracking overrides." - }, - "vite": { - "type": "object", - "properties": { - "allowEsbuild": { - "type": "string", - "description": "Reasoned opt-out of the esbuild ban in vite-is-rolldown-native for a legitimate NON-BUNDLER esbuild use (e.g. an opt-in minify pass that dynamic-imports esbuild, a browser-bundle e2e arm). The vite<8 floor stays unconditional and the build bundler stays rolldown; this only tolerates esbuild as a declared test/dev dependency. The string is the why — name the consuming module(s)." - } - }, - "description": "vite/rolldown posture knobs read by scripts/fleet/check/vite-is-rolldown-native.mts." - }, - "vitest": { - "type": "object", - "properties": { - "bunTests": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "pattern": "^(?!.*\\.fuzz\\.test\\.)(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\\.test\\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$" - }, - "uniqueItems": true, - "description": "Exact repo-relative test paths owned by Bun. The fleet test runner executes these with Bun and excludes the same paths from Vitest." - }, - "budgetAllowances": { - "type": "object", - "properties": { - "fast": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "mid": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "slow": { - "type": "object", - "required": [ - "maxMs", - "reason", - "created", - "expires" - ], - "properties": { - "maxMs": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 600000 - }, - "reason": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "created": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - }, - "expires": { - "anyOf": [ - { - "type": "string", - "const": "never" - }, - { - "type": "string", - "minLength": 10, - "maxLength": 10, - "format": "date" - } - ] - }, - "measuredMs": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "node": { - "type": "string", - "minLength": 1 - }, - "platform": { - "type": "string", - "minLength": 1 - }, - "runId": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false, - "description": "Measured test lane limits with a receipt and an expiry of at most one calendar month." - }, - "alias": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "string" - } - }, - "description": "Module resolve aliases for the test transform, merged into the canonical vitest config. A KEY is a literal module specifier, never a glob — a monorepo maps one entry per package (`\"@stuie/core\": \"./packages/core/src/index.ts\"`). A dot-relative value resolves against the REPO ROOT, not the config file, so `./packages/...` reads the same from any tier config." - }, - "conditions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Extra `resolve.conditions` for the test transform. This is the source-condition route for a monorepo whose package `exports` map carries a `\"source\"` condition: without it vitest resolves a workspace package to its built `dist`, so the instrumented `src` never runs and the repo reports 0% coverage. Vite REPLACES the default condition list rather than appending, so list `source` FIRST and rely on vite's built-in client/server condition fallback for the rest." - }, - "conformanceExclude": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Heavy external-suite / cross-impl conformance wrapper globs excluded from the DEFAULT (unit) + cover suites, keeping the unit pass inside the fleet 10-second budget. A repo setting this MUST pair it with an explicit `test:conformance` runner so the tier never silently drops." - }, - "lanes": { - "type": "object", - "properties": { - "fast": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs identifying fast tests. The `fast` lane selects every test outside `mid` and `slow`, regardless of these patterns. Overlapping tests stay in `mid` or `slow`." - }, - "mid": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs for the `mid` lane: isolated in-process suites. Tests matching `slow` belong to `slow`. Run via `pnpm run test:mid`. Coverage runs this lane when its patterns are nonempty and requires matching tests unless an empty shard allowance applies." - }, - "slow": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs for the `slow` lane: heavy suites. These patterns take precedence over `fast` and `mid`. Run via `pnpm run test:slow`. Coverage runs this lane when its patterns are nonempty and requires matching tests unless an empty shard allowance applies." - } - }, - "description": "Test lanes group tests by speed. `slow` selects its declared patterns; `mid` selects its declared patterns except `slow`; `fast` selects every remaining test. Explicit `fast` patterns do not change this partition. The runner flag `--lane ` selects one lane. Bare `pnpm test` defaults to `fast`." - }, - "legacyScriptTests": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Repo-relative paths of legacy script-style test files (self-executing scripts, not vitest suites) excluded from every vitest tier. Each file keeps running through its own runner; listing it here keeps the tier configs from picking it up." - }, - "maxWorkers": { - "type": "number", - "minimum": 1, - "description": "Worker cap for the vitest pool. Unset lets vitest size the pool from the machine; set it when a repo's suites are memory-heavy enough that a full-width pool thrashes." - }, - "nodeTestExclude": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs of test files allowed to run on `node:test` instead of vitest. The prefer-vitest-guard hook reads THIS key as its allowlist, so the guard and the vitest exclude can never drift into disagreeing about which files are exempt." - }, - "nonIsolated": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Globs of suites that run WITHOUT per-file isolation. Faster, and safe only for suites that mutate no shared global state; a suite that mocks globals, chdirs, or writes process.env belongs in the `mid` lane instead." - }, - "pool": { - "anyOf": [ - { - "type": "string", - "const": "forks" - }, - { - "type": "string", - "const": "threads" - } - ], - "description": "Vitest pool implementation. `threads` (the fleet default) is faster; `forks` gives each file a real process, which a suite needing true process isolation — its own cwd, its own native addon state — requires." - }, - "testBudgetMs": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 10000, - "description": "Wall-clock budget for the unit test suites under cover.mts, in milliseconds. The unit tier answers in seconds, so 10000 is both the default and the ceiling: a repo may tune DOWN, never up. A suite that needs longer is telling you it is not a unit suite, and it belongs in the mid or slow lane (`vitest.lanes`). Reach for `pool: threads` and `vitest.nonIsolated` before reaching for a bigger number." - } - }, - "additionalProperties": false, - "description": "Tuning for the canonical vitest config (.config/repo/vitest.config.mts)." - }, - "workflows": { - "type": "object", - "properties": { - "ci": { - "type": "boolean", - "description": "Ship `.github/workflows/ci-gates.yml`." - }, - "provenance": { - "type": "boolean", - "description": "Repo publishes with npm provenance (OIDC). Hint for setup helpers; not enforced by the checker today." - }, - "requirePinnedFullSha": { - "type": "boolean", - "description": "Enforce 40-char SHA pins on every `uses:` ref. Defaults to true; an opt-out is reserved for special cases (e.g. workflow-dispatch test rigs) and currently has no consumer." - } - }, - "description": "CI workflow opt-ins." - }, - "workspace": { - "type": "object", - "properties": { - "allowBuilds": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "boolean" - } - }, - "description": "pnpm `onlyBuiltDependencies` allowlist. Map a package name to true/false to grant/deny build scripts." - }, - "blockExoticSubdeps": { - "type": "boolean", - "description": "Refuse transitive git/tarball subdeps (direct git deps still allowed). Required true; the field exists so a repo can document the intent locally." - }, - "catalogDriftIgnore": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Package patterns exempt from catalog drift checks." - }, - "minimumReleaseAge": { - "type": "integer", - "minimum": 0, - "description": "Soak time in minutes before installing freshly-published packages. Fleet default 10080 (= 7 days)." - }, - "minimumReleaseAgeExclude": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes / package patterns exempt from the soak time. Socket-owned scopes typically listed here." - }, - "resolutionMode": { - "anyOf": [ - { - "type": "string", - "const": "highest" - }, - { - "type": "string", - "const": "lowest-direct" - } - ], - "description": "pnpm `resolutionMode`. Fleet default `highest`." - }, - "trustPolicy": { - "anyOf": [ - { - "type": "string", - "const": "no-downgrade" - }, - { - "type": "string", - "const": "match-spec" - } - ], - "description": "pnpm `trustPolicy`. Fleet default `no-downgrade`." - } - }, - "description": "pnpm-workspace.yaml setting hints. The runner reads from the YAML; this block exists for repos that prefer to declare intent in JSON." - } - }, - "description": "Per-repo socket-wheelhouse config, at `.config/repo/socket-wheelhouse.json` (the segregated member surface)." -} diff --git a/.config/repo/socket-wheelhouse.json b/.config/repo/socket-wheelhouse.json deleted file mode 100644 index 95d76a59..00000000 --- a/.config/repo/socket-wheelhouse.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "ai": { - "localAssist": true - }, - "$schema": "./socket-wheelhouse-schema.json", - "schemaVersion": 1, - "repoName": "sauce", - "repo": { - "type": "solo" - }, - "build": { - "from": "npm-registry", - "type": "js" - }, - "fuzz": { - "exempt": true, - "reason": "The release-kit payload's pure parsers (brew formula rewrite, npm access/trusted-publisher/staged page parsers, the pnpm-workspace catalog editor, and the installer manifest/path-safety + byte-parity checker) ARE covered by Tier-1 fast-check property suites under test/repo/unit/release-kit/fuzz/*.fuzz.test.mts. The Tier-2 vitiate coverage-guided lane (*.fuzz.ts) is not adopted: vitiate is not a catalog dependency here, and the skills content tooling itself parses only repo-tracked SKILL.md/marketplace.json, never untrusted input." - }, - "hooks": { - "enablePrePush": true, - "enableCommitMsg": true, - "enablePreCommit": true, - "preCommitVariant": "lint-test" - }, - "scripts": { - "optional": { - "check": true, - "clean": false, - "cover": false, - "fix": true, - "security": true, - "setup": false, - "type": false, - "update": true - } - }, - "lint": { - "profile": "standard" - }, - "workflows": { - "ci": true, - "provenance": false, - "requirePinnedFullSha": true, - "weeklyUpdate": true - }, - "claude": { - "includeSecurityScanSkill": true, - "includeSharedSkills": true, - "includeUpdatingSkill": true - }, - "vitest": { - "nodeTestExclude": ["tests/fixtures/**"] - }, - "workspace": { - "blockExoticSubdeps": true, - "minimumReleaseAge": 10080, - "minimumReleaseAgeExclude": ["@socketregistry/*", "@socketsecurity/*"], - "resolutionMode": "highest", - "trustPolicy": "no-downgrade" - }, - "coverage": { - "include": ["release-kit/**/*.mts", "release-kit/**/*.mjs"], - "exclude": { - "add": [ - "release-kit/examples/**", - "release-kit/**/*.d.mts", - "release-kit/payload/scripts/socket-release/templates/**" - ] - } - }, - "bornTested": { - "grandfathered": [ - "scripts/repo/ai-lint-fix.mts", - "scripts/repo/ai-lint-fix/cli.mts", - "scripts/repo/audit-transcript.mts", - "scripts/repo/check-paths.mts", - "scripts/repo/check-paths/cli.mts", - "scripts/repo/check-prompt-less-setup.mts", - "scripts/repo/check-soak-exclude-dates.mts", - "scripts/repo/check/release-kit-is-coherent.mts", - "scripts/repo/check/release-kit-launches-are-sanctioned.mts", - "scripts/repo/check/release-kit-types-resolve.mts", - "scripts/repo/check/release-kit-workflows-are-env-mapped.mts", - "scripts/repo/clean.mts", - "scripts/repo/fix.mts", - "scripts/repo/generate-agents.mts", - "scripts/repo/git-partial-submodule.mts", - "scripts/repo/helpers/parse-dependencies.mts", - "scripts/repo/helpers/search-usage.mts", - "scripts/repo/inline-shared.mts", - "scripts/repo/install-sfw.mts", - "scripts/repo/install-token-minifier.mts", - "scripts/repo/janus.mts", - "scripts/repo/lint.mts", - "scripts/repo/lockstep-emit-schema.mts", - "scripts/repo/lockstep.mts", - "scripts/repo/lockstep/cli.mts", - "scripts/repo/lockstep/emit-schema.mts", - "scripts/repo/publish.mts", - "scripts/repo/security.mts", - "scripts/repo/socket-wheelhouse-emit-schema.mts", - "scripts/repo/test.mts", - "scripts/repo/update.mts", - "scripts/repo/validate-bundle-deps.mts", - "scripts/repo/validate-config-paths.mts", - "scripts/repo/validate-esbuild-minify.mts", - "scripts/repo/validate-file-count.mts", - "scripts/repo/validate-file-size.mts", - "scripts/repo/validate-no-link-deps.mts", - "scripts/repo/validate-rolldown-minify.mts" - ] - }, - "docs": { - "surfaces": [ - { - "kind": "workflows", - "hub": "docs/best-practices.md", - "sources": [ - "skills/socket-setup/SKILL.md", - "skills/socket-scan/SKILL.md", - "skills/socket-fix/SKILL.md" - ] - } - ] - } -} diff --git a/.config/repo/tsconfig.check.json b/.config/repo/tsconfig.check.json deleted file mode 100644 index 81309bad..00000000 --- a/.config/repo/tsconfig.check.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "//": "Member-owned half of the type-check config. .config/fleet/tsconfig.check.json extends this LAST, so anything declared here wins over the fleet defaults. Its main job is compilerOptions.paths: a member's package aliases resolve for `pnpm run type` only when they are declared here, because the root tsconfig.json is a sibling of the check chain rather than an ancestor. Paths are relative to THIS file, so a src/ wrapper reads ../../src/external/. Seeded empty and extended unconditionally, so leave the file in place even with nothing in it.", - "compilerOptions": {} -} diff --git a/.config/repo/tsconfig.release-kit.json b/.config/repo/tsconfig.release-kit.json index f35f4e57..d4f57822 100644 --- a/.config/repo/tsconfig.release-kit.json +++ b/.config/repo/tsconfig.release-kit.json @@ -1,7 +1,7 @@ { - "extends": "../fleet/tsconfig.check.base.json", + "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": "../.." + "lib": ["DOM", "ES2024"] }, "include": ["../../release-kit/**/*.mts"], "exclude": ["**/node_modules"] diff --git a/.config/repo/vitest.config.mts b/.config/repo/vitest.config.mts index b7137d50..e035e0ad 100644 --- a/.config/repo/vitest.config.mts +++ b/.config/repo/vitest.config.mts @@ -1,592 +1,41 @@ -/** - * @file Vitest configuration. Isolation: the fleet default is `isolate: true` — - * each test file gets a fresh module registry + globals, so cross-file - * leakage (process.env, path-rewire overrides, vi.mock state, nock - * interceptors) is impossible. Correctness by default. A repo that wants the - * faster shared-worker mode for a known-safe subset opts those files OUT by - * listing globs in the `vitest.nonIsolated` array of the settings file, - * `.config/repo/socket-wheelhouse.json`. When set, those globs run in a - * second, non-isolated project and the default isolated project excludes - * them. No globs → everything isolated. - */ -import { existsSync } from 'node:fs' -import os from 'node:os' -import path from 'node:path' import process from 'node:process' -import { envAsBoolean } from '@socketsecurity/lib-stable/env/boolean' -import { isCI } from '@socketsecurity/lib-stable/env/ci' import { defineConfig } from 'vitest/config' -import { GENERATED_GLOBS } from '../../scripts/fleet/constants/generated-globs.mts' -import { resolveGeneratedTestExcludes } from '../../scripts/fleet/test-runner/discovery.mts' -import { resolveCoverageConfig } from '../../.config/fleet/vitest.coverage.fleet.config.mts' -import { - discoverSharedTestFiles, - readBunTestFiles, - readConformanceExcludeGlobs, - readNonIsolatedGlobs, - readVitestLanes, - readVitestSettings, - repoNodeTestExcludeGlobs, - stringArray, -} from './vitest.settings.mts' - -// Pin TZ here, at config-eval time in the MAIN process, so every worker -// inherits TZ=UTC from its spawn env. This has to happen before a worker -// starts: under the default `threads` pool a worker cannot change its own -// timezone afterwards — V8 caches it at the first Date op — so a `test.env.TZ` -// entry or a setupFiles assignment LOOKS right and does nothing, because both -// run inside the worker. Measured: `main after set TZ=UTC` reads day 19 while -// `worker after self-set TZ=UTC` still reads day 18. -// -// Why it matters beyond flakiness: an unpinned TZ makes a date-formatting bug -// invisible to whoever wrote it and reproducible only for readers in another -// offset. socket-cli's analytics report labeled every row by LOCAL day while -// the API buckets by UTC day, so the same command emitted different dates -// west of UTC; its per-package config pinned TZ and hid the bug from the -// fleet runner. `??=` so an operator probing another zone can still override. process.env['TZ'] ??= 'UTC' -// Coverage is on when the COVERAGE env is set (cover.mts) or the `--coverage` -// flag is passed. Match the FLAG, not any argv containing the substring -// "coverage" — a nested test run whose file-path args happen to include -// "coverage" must not silently turn coverage on and clean the shared -// coverage/.tmp (see test.mts resolveVitestEnv). -const isCoverageEnabled = - envAsBoolean(process.env['COVERAGE']) || - process.argv.some(arg => arg.startsWith('--coverage')) - -// Ceiling on the contention multiplier. A starved spawn is queued, not hung, so -// it deserves more time — but a genuinely STUCK test must still fail in -// bounded time instead of hanging the run behind a growing budget. -const BUDGET_LOAD_CAP = 4 - -// Below this fraction of the core count the box counts as quiet and the base -// budget stands unchanged. Half the cores busy is normal for a test run. -const BUDGET_QUIET_LOAD_RATIO = 0.5 - -/** - * How much to stretch a test budget for the machine's current contention. - * `1` on a quiet box, rising toward {@link BUDGET_LOAD_CAP} as load climbs. - * - * A background build — a parallel `cargo build` saturating every core — starves - * a spawn-per-case suite: the child is queued behind the compiler, so a fixed - * ceiling turns machine load into a red suite with no code change. Observed: - * one hook spec went from 1 failure to 5, 6, then 7 across three consecutive - * runs as an unrelated `rustc` ramped to 779% CPU. - * - * Both readings are injectable so the arithmetic is testable without depending - * on the load of whatever machine runs the suite. - */ -export function resolveBudgetLoadFactor( - options?: - | { - cores?: number | undefined - loadAvg?: number | undefined - workers?: number | undefined - } - | undefined, -): number { - const opts = { __proto__: null, ...options } as { - cores?: number | undefined - loadAvg?: number | undefined - workers?: number | undefined - } - const cores = Math.max(1, opts.cores ?? os.availableParallelism()) - // loadavg() is [0, 0, 0] on win32, which yields the base budget — correct, - // since there is no signal to scale by. - const observed = Math.max(0, opts.loadAvg ?? os.loadavg()[0] ?? 0) - // The reading is taken as the config loads, BEFORE the run creates its own - // contention, so a quiet box reads quiet and the whole suite then runs at - // maxWorkers. Treat the run's own parallelism as a floor on load: a spawn in - // worker 7 competes with six siblings whatever the box looked like a second - // ago. Without this floor a full-suite run measured 74s against a 60s budget - // on a box that read 5.26 at startup. - const workers = Math.max(0, opts.workers ?? resolveMaxWorkers()) - const effective = Math.max(observed, workers) - const ratio = effective / (cores * BUDGET_QUIET_LOAD_RATIO) - return Math.min(BUDGET_LOAD_CAP, Math.max(1, ratio)) -} - -/** - * The per-test budget: the CI/coverage base ladder, stretched by current - * machine contention. Used for both `testTimeout` and `hookTimeout` so a - * starved `beforeAll` fixture gets the same headroom as the tests it feeds. - */ -export function resolveTestBudgetMs( - options?: - | { - cores?: number | undefined - loadAvg?: number | undefined - workers?: number | undefined - } - | undefined, -): number { - const ci = isCI() - const base = - ci && isCoverageEnabled - ? 120_000 - : ci - ? 60_000 - : isCoverageEnabled - ? 30_000 - : 10_000 - return Math.round(base * resolveBudgetLoadFactor(options)) -} - -// Local ceiling on the core-scaled worker count. Past this the suite stops -// getting faster and only costs memory: measured on a 14-core box, the -// integration tier ran 147s at 16 workers, 129s at 24 and 126s at 32 — a 2% -// gain for a third more Node heaps. -const LOCAL_MAX_WORKERS_CAP = 24 -const LOCAL_MAX_WORKERS_FLOOR = 8 -// Workers per core. Deliberately oversubscribed: a fleet test mostly WAITS on a -// spawned script rather than computing, so a blocked worker leaves a core idle -// and a sibling can use it. Under-subscribing measurably hurt — the same tier -// took 170s at 6 workers and 169s at 10, slower than the 16-worker default it -// was meant to improve on. -const LOCAL_WORKERS_PER_CORE = 2 - -/** - * Worker cap for the pool before any per-repo override, which - * {@link capMaxWorkers} floors this against. - */ -export function resolveFallbackMaxWorkers( - options?: - | { cores?: number | undefined; isCI?: boolean | undefined } - | undefined, -): number { - const opts = { __proto__: null, ...options } as { - cores?: number | undefined - isCI?: boolean | undefined - } - if (opts.isCI ?? isCI()) { - return 4 - } - // Coverage instrumentation is memory-hungry, so it keeps the flat, lower cap - // rather than scaling up into a heap-exhaustion failure. - if (isCoverageEnabled) { - return 8 - } - const cores = Math.max(1, opts.cores ?? os.availableParallelism()) - return Math.min( - LOCAL_MAX_WORKERS_CAP, - Math.max(LOCAL_MAX_WORKERS_FLOOR, cores * LOCAL_WORKERS_PER_CORE), - ) -} -export function resolveConfiguredMaxWorkers(): number | undefined { - const configured = readVitestSettings().maxWorkers - return typeof configured === 'number' && configured > 0 - ? configured - : undefined -} -export function capMaxWorkers( - configuredMaxWorkers: number | undefined, - fallbackMaxWorkers: number, -): number { - return configuredMaxWorkers === undefined - ? fallbackMaxWorkers - : Math.min(configuredMaxWorkers, fallbackMaxWorkers) -} -export function resolveMaxWorkers(): number { - return capMaxWorkers( - resolveConfiguredMaxWorkers(), - resolveFallbackMaxWorkers(), - ) -} -/** - * Fast-fail bail count. A coverage run MUST execute the FULL suite to measure - * it, so bail is INERT under coverage: bailing on the - * first failure aborts ~half the suite and its subprocess coverage, collapsing - * the aggregate to a phantom partial (#79: CI read 36% vs the true ~73% because - * one failing test bailed the run after 249 of 1224 files). Plain CI test jobs - * no coverage, keep fast-fail bail=1; local (no CI) runs the whole suite. - * Pure so the resolution is unit-testable without a real CI/coverage env. - */ -export function resolveBail(config: { - readonly isCI: boolean - readonly isCoverage: boolean -}): number { - const cfg = { __proto__: null, ...config } - return !cfg.isCoverage && cfg.isCI ? 1 : 0 -} -/** - * Resolve-alias merge. This config is CASCADED — a member repo that edited it - * directly lost the edit on the next cascade: socket-webext's - * `@socketsecurity/sdk` → browser-build alias was wiped exactly that way. The - * settings file's `vitest.alias` map is the repo-owned surface that survives. - * Two maps merge per key, the second winning, which is what lets a caller layer - * a fleet default under a repo's own entries. Dot-relative replacements — `./` - * or `../` — resolve against `root`, the repo root at config-load time, because - * vite substitutes alias replacements verbatim: left relative, the result would - * resolve against each importer instead of the repo root. Bare package names - * and absolute paths pass through untouched. - */ -export function mergeVitestAlias( - fleet: unknown, - repo: unknown, - root: string = process.cwd(), -): Record { - const entries = (tier: unknown): Array<[string, string]> => - tier !== null && typeof tier === 'object' && !Array.isArray(tier) - ? Object.entries(tier).filter( - (e): e is [string, string] => typeof e[1] === 'string', - ) - : [] - return Object.fromEntries( - [...entries(fleet), ...entries(repo)].map(([find, replacement]) => [ - find, - replacement.startsWith('./') || replacement.startsWith('../') - ? path.resolve(root, replacement) - : replacement, - ]), - ) -} -export function resolveVitestAlias(): Record { - return mergeVitestAlias(undefined, readVitestSettings().alias) -} -export function resolvePool(): 'forks' | 'threads' { - const chosen = readVitestSettings().pool - return chosen === 'forks' || chosen === 'threads' ? chosen : 'threads' -} -// Vite's own server-side resolve conditions, mirrored as a literal because -// `vite` is a transitive dep here, not a direct one — importing -// `defaultServerConditions` from it would not typecheck, and `vitest/config` -// does not re-export it. Verified against the installed vite 8.1.5, whose -// DEFAULT_SERVER_CONDITIONS is exactly this list. drift-watch: re-verify on a -// vite major bump. -const VITE_DEFAULT_SERVER_CONDITIONS = [ - 'module', - 'node', - 'development|production', -] as const +const coverage = process.argv.includes('--coverage') +const fuzz = process.env['TEST_FUZZ'] === '1' +const FUZZ_GLOB = 'test/repo/**/*.fuzz.test.mts' -/** - * Extra resolve conditions, from `vitest.conditions`. Vite REPLACES its default - * condition list rather than appending to it, so the repo's entries are listed - * FIRST and vite's own server defaults are appended — dropping them would break - * plain node resolution for every dependency. - */ -export function resolveVitestConditions(): string[] { - const configured = stringArray(readVitestSettings().conditions) - return configured.length > 0 - ? [...configured, ...VITE_DEFAULT_SERVER_CONDITIONS] - : [] -} -const nonIsolatedGlobs = readNonIsolatedGlobs() -const repoResolveAlias = resolveVitestAlias() -const repoResolveConditions = resolveVitestConditions() - -// Lane resolution. The runner sets FLEET_LANE (bare `pnpm test` → 'fast'); the -// filter also applies under coverage. An unset lane traverses every lane. -const vitestLanes = readVitestLanes() -const activeLane = process.env['FLEET_LANE'] -// A lane's dir globs → test-file include patterns (`--lane mid|slow` runs ONLY -// that lane; a trailing `/**` becomes `/**/*.test.{…}`). -export function laneToTestGlobs(globs: string[]): string[] { - return globs.map(g => - /\.[cm]?[jt]s(?:\}|$)/u.test(g) - ? g - : `${g.replace(/\/\*+$/, '')}/**/*.test.{js,ts,mjs,mts,cjs}`, - ) -} -const ALL_TEST_GLOBS = ['**/test/**/*.test.{js,ts,mjs,mts,cjs}'] - -/** - * Resolve one speed lane without dropping unclassified tests. - * - * Slow takes precedence over mid. Fast owns every test outside mid and slow, - * so a new test stays in the development loop until measurement moves it. - */ -export function resolveLaneSelection( - lanes: ReturnType, - lane: string | undefined, -): { exclude: string[]; include: string[] } { - const mid = lanes.mid ?? [] - const slow = lanes.slow ?? [] - if (lane === 'fast') { - return { exclude: [...mid, ...slow], include: [...ALL_TEST_GLOBS] } - } - if (lane === 'mid') { - return { - exclude: [...slow], - include: laneToTestGlobs(mid), - } - } - if (lane === 'slow') { - return { - exclude: [], - include: laneToTestGlobs(slow), - } - } - return { exclude: [], include: [...ALL_TEST_GLOBS] } -} - -const laneSelection = resolveLaneSelection(vitestLanes, activeLane) -// The conformance tier's dir globs, and whether THIS run is the explicit -// conformance run. Set by scripts/repo/test-conformance.mts, never by hand. -const conformanceGlobs = readConformanceExcludeGlobs() -const conformanceTier = process.env['FLEET_TEST_CONFORMANCE'] === '1' - -/** - * Vitest-runnable property/fuzz harnesses (`*.fuzz.test.mts`). The Tier-2 - * vitiate form (`*.fuzz.mts`) is already outside the include glob and runs on - * its own runner, so it needs no entry here. - */ -export const FUZZ_GLOBS: readonly string[] = [ - '**/test/**/*.fuzz.test.{js,ts,mjs,mts,cjs}', -] -export const ORDINARY_TEST_EXCLUDES: readonly string[] = ['**/test/e2e/**'] -// Whether THIS run is the explicit fuzz tier. Set by the weekly fuzz workflow, -// never by hand. -// -// Fuzzing is not a per-commit gate. A randomized harness is budgeted by -// wall-clock, so a red run depends on machine load and cannot be replayed; a -// deterministic one replays but costs far more than the signal is worth per -// commit (the seeded parity harness alone ran 5-20s). Both belong to a -// scheduled run that can afford the full budget and file what it finds. -const fuzzTier = process.env['FLEET_TEST_FUZZ'] === '1' - -const config = defineConfig({ - // Repo-owned resolution from the settings file's `vitest.alias` + - // `vitest.conditions` — see mergeVitestAlias and resolveVitestConditions. - // Spread conditionally so a repo declaring neither keeps vite's own - // resolution untouched. - ...(Object.keys(repoResolveAlias).length || repoResolveConditions.length - ? { - resolve: { - ...(Object.keys(repoResolveAlias).length - ? { alias: repoResolveAlias } - : {}), - ...(repoResolveConditions.length - ? { conditions: repoResolveConditions } - : {}), - }, - } - : {}), +export default defineConfig({ test: { - // Reuse content-keyed transforms while each isolated file keeps fresh state. - fsModuleCache: true, + coverage: { + enabled: coverage, + exclude: [ + 'release-kit/**/*.d.mts', + 'release-kit/examples/**', + 'release-kit/payload/scripts/socket-release/templates/**', + ], + include: ['release-kit/**/*.mts', 'release-kit/**/*.mjs'], + provider: 'v8', + reporter: ['text', 'json-summary'], + }, deps: { interopDefault: false, }, + environment: 'node', + exclude: ['**/node_modules/**', ...(fuzz ? [] : [FUZZ_GLOB])], + globals: false, + hookTimeout: 60_000, + include: fuzz ? [FUZZ_GLOB] : ['test/repo/**/*.test.mts'], + isolate: true, + pool: 'forks', server: { deps: { - // Treat @socketsecurity/lib-stable as external — bypass vite's - // transform pipeline so Node resolves it natively (CJS default - // condition). Without this, vite's `development` condition resolves - // lib-stable via its `source` exports field (TypeScript source), and - // the TS source files reference `./external/semver` sub-paths that are - // not listed in the lib-stable exports map, producing an unhandled - // EnvironmentPluginContainer.resolveId error that kills the test run. external: [/node_modules\/@socketsecurity\/lib-stable/], }, }, - globals: false, - environment: 'node', - // Test setup lives under test/scripts/{fleet,repo}/setup.mts — fleet-canonical - // setup, nock fail-closed, env scrubbing, in fleet/, repo-specific setup in - // repo/. Both are optional: vitest skips a setupFile that doesn't exist via - // the existsSync filter so scaffolding-only repos don't error. - setupFiles: [ - 'test/fleet/scripts/setup.mts', - 'test/repo/scripts/setup.mts', - ].filter(p => existsSync(p)), - // Slow takes precedence over mid; fast owns every remaining test. - // `**/`-anchored so a - // monorepo's nested `packages//test/**` trees are discovered from this - // one root config — a bare `test/**/*.test...` only anchors at the repo - // root, silently missing every sub-package's tests (each scoped `vitest run` - // returns "No test files found" and a full run "passes" having executed - // zero of them). - include: fuzzTier - ? [...FUZZ_GLOBS] - : conformanceTier - ? laneToTestGlobs(conformanceGlobs) - : laneSelection.include, - // Vitest treats `test/**` as `**/test/**`, so without an explicit - // exclude it picks up every nested `test/` directory in the repo - // — including the `.git-hooks/test/`, the oxlint plugin's per-rule - // `.config/fleet/oxlint-plugin/fleet//test/` suites, - // and `scripts/**/test/` suites that run under `node --test`, not - // vitest. Those tests use `import { test } from 'node:test'` and - // produce zero vitest suites, which vitest reports as failures. - // List the known node:test homes here so vitest skips them cleanly - // (their own `node --test` runners pick them up separately). - exclude: [ - '**/node_modules/**', - ...(conformanceTier ? [] : ORDINARY_TEST_EXCLUDES), - // The conformance tier is opt-in via `pnpm run test:conformance`. Every - // other lane drops it: these wrappers each spawn a FULL external corpus - // (Test262 is ~92k scenarios per implementation), which is minutes to - // hours, not a unit suite. Lifted only for the explicit conformance run, - // where the include above targets exactly these globs. - ...(conformanceTier ? [] : conformanceGlobs), - // Property/fuzz harnesses run ONLY in the fuzz tier, never in the - // ordinary suite or a pre-commit lane. Lifted only for the explicit fuzz - // run, where the include above targets exactly these globs. - ...(fuzzTier ? [] : FUZZ_GLOBS), - // Generated/vendored trees (dist, build, upstream, test/fixtures, …) — - // shared with lint + format from one source (constants/generated-globs.mts) - // so the ignore surfaces can't drift. vite's default loader can't - // transform many of these (a module-graph walk into a vendored tree or a - // wasm blob fails "ESM integration proposal for Wasm"), so discovery AND - // `vitest related` must skip them; scripts/fleet/test.mts filters the same - // set from the staged pre-commit run. - ...GENERATED_GLOBS, - '**/.{idea,git,cache,output,temp}/**', - '**/.claude/**', - '.git-hooks/**', - '.config/fleet/oxlint-plugin/**', - 'scripts/**/test/**', - // `template/**` holds CANONICAL non-test sources (the cascaded LIVE - // copies are what the suite runs); live test/repo is the sole test - // authoring home, so template is excluded unconditionally. - 'template/**', - // `test/isolated/**` is the isolated SUITE's turf — its own forks / longer - // -timeout config (`vitest.config.isolated.mts`), run as a separate suite - // by cover.mts. Exclude it from this shared suite ONLY when the repo ships - // that config, so a repo without the isolated suite still runs any - // `test/isolated` files here instead of silently dropping them. This is the - // isolated DIRECTORY tier — distinct from the `isolate:` state-isolation - // split (the `nonIsolated` projects) further down. - ...(existsSync('.config/repo/vitest.config.isolated.mts') - ? ['test/isolated/**'] - : []), - // Repo-tunable node:test homes (e.g. `tools/**/test/**`) from the - // settings file's `vitest.nodeTestExclude`. The same key feeds - // prefer-vitest-guard's allowlist so the two never drift. - ...repoNodeTestExcludeGlobs(), - ...readBunTestFiles(), - ...laneSelection.exclude, - ], - // Some repos in the fleet (scaffolding-only, hook-only, etc.) ship - // this config but don't yet have a `test/` directory — vitest's - // default behavior would fail "no tests found" there. Repos that - // do have tests still error on actual test failures; this flag - // only affects the empty-suite case. - // Zero discovered files is normal for a scoped run, but it is a FAILURE - // for the conformance tier: that run exists to execute those globs, so - // discovering none means the tier is misconfigured and a silent pass would - // report the heavy suites green without running one of them. - // - // The fuzz tier is deliberately NOT held to that: a repo can fuzz only at - // Tier 2 (`*.fuzz.mts`, its own runner), which discovers nothing here and - // is not a misconfiguration. Whether a repo fuzzes at all is - // fuzz-tiers-are-covered's question, and which legs fire is the workflow's. - passWithNoTests: !conformanceTier, - // Reporters left unset so vitest applies its own default: - // `[isAgent() ? 'minimal' : 'default', ...(GITHUB_ACTIONS ? ['github-actions'] : [])]` - // (vitest/src/defaults.ts). That yields the token-lean `minimal` reporter - // inside an AI coding agent (std-env `isAgent`: CLAUDECODE/CURSOR_/…), - // `default` for humans, and the `github-actions` annotations reporter in CI. - // Hard-coding `reporters: ['default']` would override that default and - // defeat all three. https://vitest.dev/guide/reporters - pool: resolvePool(), - // Vitest 4 removed `poolOptions`; the per-pool worker knobs are now - // top-level. `maxThreads`/`maxForks` → `maxWorkers`; `singleThread`/ - // `singleFork` → `fileParallelism: false` (forces maxWorkers to 1); - // `minThreads` and `useAtomics` were dropped with no replacement. - // Worker count tuned to physical CPUs: GH Actions ubuntu-latest has - // 4 cores, dev laptops typically 8-16. `isCI()` (rewire-aware - // presence check on `CI`) is truthy even for CI="" or CI=0, matching - // the fleet convention that any CI value means CI. - // - // Isolation: true by default (correctness — no cross-file state leak). A - // repo lists safe-to-share globs in the `vitest.nonIsolated` array of - // .config/repo/socket-wheelhouse.json; when set, this default project - // EXCLUDES them (the second project runs them non-isolated). When unset, - // every file is isolated. - isolate: true, - // Keep coverage file-parallel. Worker setup removes the already-consumed - // COVERAGE flag before test code runs, so a nested Vitest child cannot turn - // coverage back on and clean the outer run's shared .tmp reports. Ordinary - // Node children still inherit NODE_V8_COVERAGE for subprocess merging. - // Local coverage caps at 8 workers because this spawn-heavy suite saturates - // there; 16 workers add filesystem/process contention. Ordinary local tests - // retain 16 workers, while CI matches its 4 available cores. - maxWorkers: resolveMaxWorkers(), - // Coverage runs with V8 instrumentation that spawned children inherit, so - // spawn-heavy tests, hook integration specs launch a node child per case - // legitimately exceed 10s there. CI gets a 60s budget unconditionally: - // 2-core runners × parallel workers starve spawn-per-case suites - // (RuleTester spawns one oxlint child per case) — the 10s/30s ceilings - // killed lint-rule suites mid-queue on every OS while the same files pass - // locally. CI *with* coverage is strictly heavier than either alone - // (instrumentation + 4-core contention + thousands of instrumented child - // spawns in one run), so it gets the longest budget — the plain-CI 60s - // still timed out the spawn-per-case hook specs (npm-2fa-needs-pty-guard, - // single-lander-guard) under peak release-cover contention, losing their - // coverage and failing the gate while all four metrics were above - // threshold. Complete the ladder rather than shave the threshold. - testTimeout: resolveTestBudgetMs(), - hookTimeout: resolveTestBudgetMs(), - bail: resolveBail({ - isCI: isCI(), - isCoverage: isCoverageEnabled, - }), - // Coverage shape comes from the fleet base merged with the repo-owned - // `coverage` section of .config/repo/socket-wheelhouse.json (include - // replace, exclude add/remove) — one canonical exclude list instead of a - // drifted copy here. - coverage: { - enabled: isCoverageEnabled, - // Ordinary tests do not need the workspace scan for coverage aliases. - ...(isCoverageEnabled ? resolveCoverageConfig() : {}), - }, + testTimeout: 60_000, }, }) - -// The fast lane excludes every build-test home through its mid/slow lane -// filters. Avoid the repository-wide authored-build scan during the ten-second -// fast loop; mid, slow, and full runs retain the complete exception handling. -if (config.test && activeLane !== 'fast') { - config.test.exclude = resolveGeneratedTestExcludes({ - repoRoot: process.cwd(), - include: config.test.include ?? [], - exclude: config.test.exclude ?? [], - }) -} - -// Construct complete project options explicitly: Vite's extends merge concatenates -// include arrays, which otherwise makes the shared project rerun isolated files. -if (nonIsolatedGlobs.length && config.test) { - const baseTest = config.test - const include = baseTest.include ?? [] - const exclude = baseTest.exclude ?? [] - const sharedFiles = discoverSharedTestFiles(nonIsolatedGlobs, { - include, - exclude, - }) - - config.test = { - ...baseTest, - projects: [ - { - ...config, - extends: false, - test: { - ...baseTest, - name: 'isolated', - isolate: true, - exclude: [...exclude, ...nonIsolatedGlobs], - }, - }, - { - ...config, - extends: false, - test: { - ...baseTest, - name: 'non-isolated', - isolate: false, - include: sharedFiles.length - ? sharedFiles - : ['__fleet_no_shared_tests__'], - }, - }, - ], - } -} - -export default config diff --git a/.config/repo/vitest.settings.mts b/.config/repo/vitest.settings.mts deleted file mode 100644 index fab61ab4..00000000 --- a/.config/repo/vitest.settings.mts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @file The repo-tunable vitest settings surface — the `vitest` section of - * the canonical per-repo settings file - * (.config/repo/socket-wheelhouse.json), split from vitest.config.mts along - * its natural boundary: everything here READS settings, everything there - * RESOLVES runtime config from them. One file, one parse; each key's - * contract is on its field below, and docs/fleet/agents.md/test-layout.md - * carries the tier rationale. - * Exported readers are ordered alphabetically, which `socket/sort-source- - * methods` enforces in every consuming member. Neither copy of this file is - * linted in the wheelhouse itself, so the order is a downstream contract. - */ - -import { existsSync, globSync, lstatSync, readFileSync } from 'node:fs' -import path from 'node:path' - -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' - -import { - parseBunTestFiles, - readBunTestFiles as readConfiguredBunTestFiles, -} from '../../scripts/fleet/test-runner/bun-tests.mts' - -export interface VitestRepoConfig { - // Exact repo-relative test files run by Bun and excluded from Vitest. - bunTests?: string[] | undefined - // Module resolve aliases for the test transform, e.g. - // `{ "@socketsecurity/sdk": "./dist/index.browser.js" }`. A key is a LITERAL - // specifier, never a glob, so a monorepo lists one entry per package; see - // mergeVitestAlias for the dot-relative semantics. - alias?: Record | undefined - // Extra `resolve.conditions`. The route for a monorepo whose `exports` map - // carries a `source` condition: without it vitest resolves a workspace - // package to its built `dist`, the instrumented `src` never runs, and the - // repo reports 0% coverage. - conditions?: string[] | undefined - conformanceExclude?: string[] | undefined - lanes?: VitestLanes | undefined - // Worker cap for the pool, floored against the CI/coverage-aware fallback. - maxWorkers?: number | undefined - // Globs safe to run in the faster non-isolated pool. - nonIsolated?: string[] | undefined - // node:test homes excluded from vitest discovery, e.g. `tools/**/test/**` for - // a `node --test` tool corpus. prefer-vitest-guard reads the SAME key so its - // allowlist and this exclude never drift. - nodeTestExclude?: string[] | undefined - pool?: 'forks' | 'threads' | undefined -} - -/** - * Test LANES — a SPEED category, orthogonal to test TYPE (unit/integration/e2e) - * — from the `vitest.lanes` section of the canonical per-repo settings file - * (.config/repo/socket-wheelhouse.json; see paths.mts's resolver order for the - * fallbacks). `slow` = heavy suites (subprocess-per-case, e.g. hook integration - * specs); `mid` = isolated in-process suites (env-mutating / vi.mock / - * fs-heavy); `fast` = pure in-process. `slow` selects its declared patterns. - * `mid` selects its declared patterns except tests selected by `slow`. - * `fast` selects every remaining test, regardless of its declared patterns. - * The runner's - * `--lane ` flag (scripts/fleet/test.mts) selects one, and bare - * `pnpm test` defaults to `fast` for a quick local loop. The lane filter is - * active under coverage too. An unset FLEET_LANE traverses every lane; the - * coverage runner selects each lane in turn and merges their reports. - */ -export interface VitestLanes { - fast?: string[] | undefined - mid?: string[] | undefined - slow?: string[] | undefined -} - -// The settings file, canonical location first and the repo-root dotfile as the -// one fallback a member may ship. -export const SETTINGS_FILES = [ - '.config/repo/socket-wheelhouse.json', - '.socket-wheelhouse.json', -] as const - -/** - * Resolve the shared project while retaining Node's glob semantics. Most lane - * exclusions are exact test files: checking those in a Set avoids repeatedly - * matching hundreds of literal patterns during glob traversal. Only ordinary - * relative file paths qualify; escapes, glob syntax, unusual names, missing - * paths and directories stay with Node's original exclusion implementation. - */ -export function discoverSharedTestFiles( - patterns: string[], - options: { include: string[]; exclude: string[]; cwd?: string | undefined }, -): string[] { - const exactFiles = new Set() - const globExcludes: string[] = [] - for (const pattern of options.exclude) { - const normalizedPattern = normalizePath(pattern) - // Positive literal grammar: no escape, extglob, brace or bracket syntax; - // segments cannot be dot/dotdot. Unrecognized forms use native glob. - const literal = - /^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.test\.(?:js|ts|mjs|mts|cjs)$/u.test( - normalizedPattern, - ) - let regularFile = false - if (literal) { - try { - regularFile = lstatSync( - path.resolve(options.cwd ?? '.', normalizedPattern), - ).isFile() - } catch { - // Missing or unreadable candidates retain native exclusion behavior. - } - } - if (regularFile) { - exactFiles.add(normalizedPattern) - } else { - globExcludes.push(normalizedPattern) - } - } - return [ - ...globSync(patterns, { cwd: options.cwd, exclude: globExcludes }), - ].filter( - file => - !exactFiles.has(file.split(path.sep).join('/')) && - options.include.some(pattern => path.matchesGlob(file, pattern)), - ) -} - -/** - * The CONFORMANCE tier — heavy external-suite wrappers (a full Test262 corpus - * per implementation, upstream conformance harnesses) named by - * `vitest.conformanceExclude` in the settings file. - * - * `scripts/repo/test-conformance.mts` runs this tier explicitly with - * FLEET_TEST_CONFORMANCE=1; every other run must EXCLUDE it. Both halves live - * here so neither can sit unwired: an env var the runner sets and nothing - * reads, or a setting that names the tier while no lane excludes it, would — - * so `pnpm run cover` spawned a ~92k-scenario corpus per BUILT implementation, - * three at once. Against the 60s unit budget that reads as a hung run rather - * than the multi-hour sweep it actually is. - */ -export function readConformanceExcludeGlobs(): string[] { - return stringArray(readVitestSettings().conformanceExclude) -} - -export function readNonIsolatedGlobs(): string[] { - return stringArray(readVitestSettings().nonIsolated) -} - -export function readVitestLanes(): VitestLanes { - const lanes = readVitestSettings().lanes - return lanes !== null && typeof lanes === 'object' && !Array.isArray(lanes) - ? { - ...(Array.isArray(lanes.fast) ? { fast: stringArray(lanes.fast) } : {}), - mid: stringArray(lanes.mid), - slow: stringArray(lanes.slow), - } - : {} -} - -export function readBunTestFiles(repoRoot?: string | undefined): string[] { - return repoRoot === undefined - ? parseBunTestFiles(readVitestSettings().bunTests) - : readConfiguredBunTestFiles(repoRoot) -} - -/** - * The `vitest` section of the settings file. The ONE settings-file parse in - * this config: every resolver reads its key off this, so a torn or absent - * file degrades to fleet defaults in exactly one place instead of six. - */ -export function readVitestSettings(): VitestRepoConfig { - for (let i = 0, { length } = SETTINGS_FILES; i < length; i += 1) { - const file = SETTINGS_FILES[i]! - if (!existsSync(file)) { - continue - } - try { - const parsed: unknown = JSON.parse(readFileSync(file, 'utf8')) - const section = - parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) - ? (parsed as { vitest?: VitestRepoConfig | undefined }).vitest - : undefined - return section !== null && - typeof section === 'object' && - !Array.isArray(section) - ? section - : {} - } catch { - return {} - } - } - return {} -} - -export function repoNodeTestExcludeGlobs(): string[] { - return stringArray(readVitestSettings().nodeTestExclude) -} - -/** - * A settings value read as a string array; anything else reads as empty. - */ -export function stringArray(value: unknown): string[] { - return Array.isArray(value) - ? value.filter((g): g is string => typeof g === 'string') - : [] -} diff --git a/.config/sfw-bypass-list.txt b/.config/sfw-bypass-list.txt deleted file mode 100644 index 7aed7a57..00000000 --- a/.config/sfw-bypass-list.txt +++ /dev/null @@ -1,143 +0,0 @@ -# Socket Firewall bypass list — fleet-canonical source of truth. -# -# This file is the wheelhouse-tracked master. Every fleet repo gets a -# byte-identical copy at /.config/sfw-bypass-list.txt via the -# sync-scaffolding cascade, and consumers read their local copy to -# populate SFW_CUSTOM_REGISTRIES: -# -# - socket-registry → .github/actions/setup/action.yml grep-reads -# this list into SFW_CUSTOM_REGISTRIES in CI. -# - socket-btm et al → scripts/install-sfw.mts writes the env to -# ~/.socket/_wheelhouse/env.sh so local sfw gets the -# same set the CI shared worker uses. -# -# Edit ONLY in socket-wheelhouse/template/.config/sfw-bypass-list.txt -# — downstream copies are regenerated by the cascade and any local -# fork will be clobbered on the next sync. -# -# Format: one `:` entry per line. Comments + blank lines -# ignored. Kinds accepted: npm, pypi, golang, maven, gem, cargo, nuget, -# block, wrap, bypass. The bundled defaults baked into sfw live at -# SocketDev/firewall:src/lib/registries/default.ts — entries here ship -# *over* a binary release without waiting for a re-publish. -# -# When adding a host, group it with the ecosystem comment that owns it -# and keep within-group ordering stable so cascades produce minimal -# diffs. - -# GitHub release-asset CDNs (targets of github.com/*/releases/download/* -# redirects). -bypass:objects.githubusercontent.com -bypass:release-assets.githubusercontent.com -bypass:raw.githubusercontent.com -bypass:gist.githubusercontent.com -# Source-archive CDN: github.com///archive/.zip|.tar.gz -# 302-redirects to codeload.github.com, NOT *.githubusercontent.com. -# CMake FetchContent / go modules / build systems pulling SHA-pinned -# source archives hit this host — sfw was truncating the stream -# mid-transfer (curl status 18 "Transferred a partial file"), e.g. -# onnxruntime's eigen3 dep. Integrity stays enforced by the build's -# own SHA checks (deps.txt SHA1 / FetchContent URL_HASH). -bypass:codeload.github.com - -# VCS fallbacks (Go modules, npm git-deps, Ruby git sources). -bypass:gitlab.com -bypass:bitbucket.org - -# Build-tool toolchain downloads. -bypass:ziglang.org -bypass:cmake.org -bypass:sh.rustup.rs -bypass:nodejs.org -bypass:bootstrap.pypa.io -# OrbStack — recommended macOS Docker runtime for Dockerfile-based builds -# (socket-btm glibc/musl node-smol images). The /download endpoint on -# orbstack.dev 307-redirects to the cdn-updates.orbstack.dev .dmg; a -# `brew install --cask orbstack` onboarding step hits both. -bypass:orbstack.dev -bypass:cdn-updates.orbstack.dev - -# Go module proxy + toolchain auto-download. proxy.golang.org serves -# modules and 302s to storage.googleapis.com for the -# `go: downloading go1.x.y` flow; sum.golang.org is the checksum DB. -# Without these, `go build` fails with `tls: failed to verify -# certificate: x509: certificate signed by unknown authority` because -# SFW's MITM breaks the cert chain at the redirect target. -bypass:proxy.golang.org -bypass:sum.golang.org -bypass:storage.googleapis.com - -# Cargo web portal (search/info API). -bypass:crates.io - -# Conda ecosystem. -bypass:repo.anaconda.com -bypass:conda.anaconda.org -bypass:anaconda.org -bypass:conda-forge.org - -# Composer (PHP). -bypass:packagist.org -bypass:repo.packagist.org - -# Conan (C/C++). -bypass:center.conan.io -bypass:conan.io - -# CocoaPods (Swift/Obj-C). -bypass:cdn.cocoapods.org - -# Hackage (Haskell). -bypass:hackage.haskell.org - -# Hex (Elixir/Erlang). -bypass:hex.pm -bypass:repo.hex.pm -bypass:builds.hex.pm - -# HuggingFace model hub. -bypass:huggingface.co - -# Julia. -bypass:pkg.julialang.org -bypass:julialang-s3.julialang.org - -# LuaRocks. -bypass:luarocks.org - -# OPAM + OCaml toolchain. -bypass:opam.ocaml.org -bypass:ocaml.org - -# pub.dev (Dart / Flutter). -bypass:pub.dev - -# CPAN (Perl). -bypass:metacpan.org -bypass:cpan.metacpan.org - -# CRAN (R). -bypass:cran.r-project.org -bypass:cran.rstudio.com -bypass:bioconductor.org - -# Bitnami. -bypass:downloads.bitnami.com - -# Swift toolchain + package index. -bypass:swift.org -bypass:download.swift.org -bypass:swiftpackageindex.com - -# VSCode extension marketplace. -bypass:marketplace.visualstudio.com - -# Bazel external workspace mirror. -bypass:mirror.bazel.build - -# Homebrew anonymous analytics (InfluxDB Cloud bucket). brew sends -# usage telemetry on every command; the writes fail-fast and silent if -# blocked, but listing as bypass avoids users debugging phantom -# timeouts on runners that have brew installed. -# HOMEBREW_NO_ANALYTICS=1 opts out at the source. -bypass:eu-central-1-1.aws.cloud2.influxdata.com diff --git a/.config/socket-registry-pins.json b/.config/socket-registry-pins.json deleted file mode 100644 index 38fa7dd7..00000000 --- a/.config/socket-registry-pins.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "./socket-registry-pins.schema.json", - "_comment": [ - "Wheelhouse-tracked SocketDev/socket-registry SHAs that fleet consumers pin against.", - "", - "Why: socket-registry ships shared GitHub Actions + reusable workflows + a fleet-canonical", - ".config/fleet/sfw-bypass-list.txt that ALL fleet repos consume. Per the cascade discipline in", - "socket-registry/.claude/skills/fleet/updating-workflows/reference.md, every consumer pins to the", - "Layer 3 'propagation SHA' — the merge SHA of the most recent ci.yml / provenance.yml /", - "weekly-update.yml update. Tracking that SHA here means each fleet repo's own pin docs +", - "scripts can read a single source of truth instead of independently scraping GitHub.", - "", - "Cascade flow (after any socket-registry action / workflow / .config/ change merges):", - " 1. Run socket-registry's L1 -> L2a -> L2b -> L3 cascade per its updating-workflows skill.", - " 2. The Layer 3 merge SHA becomes the new propagation SHA.", - " 3. Update propagationSha below + commit + push wheelhouse.", - " 4. sync-scaffolding propagates the new SHA into every fleet repo's pin sites.", - "", - "Do NOT bump propagationSha to a SHA that hasn't completed the L1-L3 cascade in", - "socket-registry — Layer 4 (_local-not-for-reuse-*.yml) SHAs are not valid pins for", - "external consumers." - ], - "propagationSha": "f09a1cd39868ae45d304cfcead2d4f19a5325d8a", - "propagationShaUpdatedAt": "2026-06-01", - "propagationShaCommitSubject": "chore(wheelhouse): cascade template@cf15ef5a" -} diff --git a/.config/taze.config.mts b/.config/taze.config.mts deleted file mode 100644 index c5d71119..00000000 --- a/.config/taze.config.mts +++ /dev/null @@ -1,40 +0,0 @@ -import { defineConfig } from 'taze' - -/* Socket-owned scopes bypass the 7-day maturity cooldown. - * - * The cooldown (maturityPeriod: 7) exists to catch compromised - * upstream packages before we adopt them — but Socket-published - * packages go through our own provenance + publish pipeline, so - * we trust them to ship fresh. - * - * The scopes listed here are EXCLUDED from pass 1 (the - * cooldown-respecting pass) and INCLUDED in pass 2 (the - * immediate-bump pass). Keep this list in sync with - * scripts/update.mts if the repo ships one, or with whatever - * second-pass mechanism the consuming repo's update script - * uses. - */ -const SOCKET_SCOPES = [ - '@socketregistry/*', - '@socketsecurity/*', - '@socketdev/*', - 'socket-*', - 'ecc-agentshield', - 'sfw', -] - -export default defineConfig({ - // Interactive mode disabled for automation. - interactive: false, - // Minimal logging. - loglevel: 'warn', - // Socket scopes handled by a second pass with maturityPeriod 0. - exclude: SOCKET_SCOPES, - // 7-day cooldown on third-party deps — matches `.npmrc`'s - // min-release-age setting for install-time enforcement. - maturityPeriod: 7, - // Bump to latest across major boundaries. - mode: 'latest', - // Edit package.json in place. - write: true, -}) diff --git a/.config/tsconfig.base.json b/.config/tsconfig.base.json deleted file mode 100644 index e1ffc535..00000000 --- a/.config/tsconfig.base.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "allowJs": false, - "composite": false, - "declarationMap": false, - "erasableSyntaxOnly": true, - "esModuleInterop": true, - "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, - "incremental": false, - "isolatedModules": true, - "lib": ["ES2024"], - "noEmitOnError": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true, - "noUncheckedSideEffectImports": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": false, - "strict": true, - "strictNullChecks": true, - "target": "ES2024", - "useUnknownInCatchVariables": true, - "verbatimModuleSyntax": true - } -} diff --git a/.config/vitest.config.mts b/.config/vitest.config.mts deleted file mode 100644 index 568491a1..00000000 --- a/.config/vitest.config.mts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file Vitest configuration. - */ -import process from 'node:process' - -import { defineConfig } from 'vitest/config' - -const isCoverageEnabled = - process.env.COVERAGE === 'true' || - process.argv.some(arg => arg.includes('coverage')) - -const config = defineConfig({ - test: { - deps: { - interopDefault: false, - }, - globals: false, - environment: 'node', - include: ['test/**/*.test.{js,ts,mjs,mts,cjs}'], - // No test/ tree exists at the root — real suites live in tests/tier* - // (own vitest config) and scripts/test/ (node --test). A config-change - // escalation to this root config must pass on zero matches. - passWithNoTests: true, - reporters: ['default'], - pool: 'threads', - poolOptions: { - threads: { - singleThread: isCoverageEnabled, - maxThreads: isCoverageEnabled ? 1 : 16, - minThreads: isCoverageEnabled ? 1 : 2, - isolate: false, - useAtomics: true, - }, - }, - testTimeout: 10_000, - hookTimeout: 10_000, - bail: process.env.CI ? 1 : 0, - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov', 'clover'], - exclude: [ - '**/*.config.*', - '**/node_modules/**', - '**/[.]**', - '**/*.d.ts', - '**/virtual:*', - 'coverage/**', - 'dist/**', - 'scripts/**', - 'test/**', - ], - all: true, - clean: true, - skipFull: false, - }, - }, -}) - -// Vitest requires a default export from config files. -// oxlint-disable-next-line socket/no-default-export -- config contract -export default config diff --git a/.config/vitest.coverage.fleet.config.mts b/.config/vitest.coverage.fleet.config.mts deleted file mode 100644 index b18c19f3..00000000 --- a/.config/vitest.coverage.fleet.config.mts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @file Fleet-canonical coverage defaults — the shape every socket-* repo - * shares. Repos layer their own exclude entries + thresholds on top via - * .config/vitest.coverage.config.mts. Do NOT add repo-specific paths here; - * anything in this file cascades to every fleet repo. - */ - -import type { CoverageOptions } from 'vitest' - -/** - * Fleet-shared coverage base. Excludes cover the dirs every fleet repo has - * (node_modules, dist, test, scripts, perf, external bundles). Repo-specific - * source paths to skip (integration-only modules, generated artifacts) get - * appended in the repo's own coverage config. - */ -export const baseFleetCoverageConfig: CoverageOptions = { - all: true, - clean: true, - exclude: [ - '**/*.config.*', - '**/node_modules/**', - '**/[.]**', - '**/*.d.ts', - '**/virtual:*', - 'coverage/**', - 'test/**', - 'packages/**', - 'perf/**', - 'dist/**', - '**/dist/**', - '**/{dist,build,out}/**', - 'src/external/**', - 'dist/external/**', - '**/external/**', - 'src/types.ts', - 'scripts/**', - ], - excludeAfterRemap: true, - ignoreClassMethods: ['constructor'], - include: ['src/**/*.{ts,mts,cts}', '!src/external/**'], - provider: 'v8', - reporter: ['text', 'json', 'json-summary', 'html', 'lcov', 'clover'], - skipFull: false, -} - -/** - * Fleet-default cumulative threshold. A repo can override these in its own - * coverage config when its bar is materially different — the wheelhouse default - * is the conservative starting point. - */ -export const baseFleetAggregateThresholds = { - branches: 95, - functions: 99, - lines: 99, - statements: 99, -} diff --git a/.editorconfig b/.editorconfig index 12e6fe13..09755269 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ # EditorConfig — editor-agnostic source of truth for whitespace + width. -# Mirrors the fleet oxfmt config (.config/fleet/oxfmtrc.json: useTabs false, +# Mirrors the oxfmt config (.oxfmtrc.json: useTabs false, # tabWidth 2, printWidth 80). oxfmt formats JS/TS; this reaches every other # editor + file type (JSON, YAML, Markdown, shell) so the whole tree agrees. # https://editorconfig.org diff --git a/.git-hooks/_shared/canonical/bundle.mts b/.git-hooks/_shared/canonical/bundle.mts deleted file mode 100644 index 8ee826df..00000000 --- a/.git-hooks/_shared/canonical/bundle.mts +++ /dev/null @@ -1,123 +0,0 @@ -import { canonicalOriginAllowed } from './source.mts' -import crypto from 'node:crypto' -import { readFileSync, statSync } from 'node:fs' -import path from 'node:path' -import process from 'node:process' -import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' -import { canonicalPathIsSafe } from './git.mts' -import type { CanonicalIndexEntry } from './git.mts' - -function record(value: unknown): Record | undefined { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? Object.fromEntries(Object.entries(value)) - : undefined -} - -function unscopedTarget( - manifest: Record, - file: string, -): boolean { - for (const field of [ - 'capabilityScopedFiles', - 'conditionalScopedFiles', - 'shapeScopedFiles', - ]) { - const groups = manifest[field] - if (groups === undefined) { - continue - } - if (!Array.isArray(groups)) { - return false - } - for (const value of groups) { - const files = record(value)?.['files'] - if (!Array.isArray(files) || files.includes(file)) { - return false - } - } - } - return true -} - -export function canonicalBundleBytesMatch( - ref: string, - file: string, - entry: CanonicalIndexEntry, - manifest: unknown, -): boolean { - const parsed = record(manifest) - const files = record(parsed?.['files']) - const expected = files?.[file] - return ( - /^fleet-pack-[0-9a-f]{40}$/u.test(ref) && - parsed?.['templateSha'] === ref.slice('fleet-pack-'.length) && - unscopedTarget(parsed, file) && - canonicalPathIsSafe(file) && - entry.mode === '100644' && - typeof expected === 'string' && - /^[0-9a-f]{64}$/u.test(expected) && - crypto.hash('sha256', entry.content, 'hex') === expected - ) -} - -const manifests = new Map() -function fetchManifest(member: string, ref: string): unknown { - const key = `${member}:${ref}` - if (!manifests.has(key)) { - const result = spawnSync( - process.execPath, - [path.join(member, 'scripts/fleet/pack/canonical-proof.mts'), ref], - { - cwd: member, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 30_000, - maxBuffer: 4_194_304, - }, - ) - if (result.status !== 0) { - return undefined - } - const lines = result.stdout.trim().split(/\r?\n/u) - const manifest: unknown = JSON.parse(lines.at(-1) ?? '') - if (record(manifest)?.['templateSha'] !== ref.slice('fleet-pack-'.length)) { - return undefined - } - manifests.set(key, manifest) - } - return manifests.get(key) -} - -export function canonicalBundleCopyMatches( - member: string, - file: string, - entry: CanonicalIndexEntry, - options: { - fetchManifest?: ((member: string, ref: string) => unknown) | undefined - } = {}, -): boolean { - if (!canonicalOriginAllowed(member)) { - return false - } - try { - const appliedPath = path.join( - member, - '.cache/fleet/socket-wheelhouse/bundle-applied', - ) - if (statSync(appliedPath).size > 128) { - return false - } - const ref = readFileSync(appliedPath, 'utf8').trim() - if (!/^fleet-pack-[0-9a-f]{40}$/u.test(ref)) { - return false - } - return canonicalBundleBytesMatch( - ref, - file, - entry, - (options.fetchManifest ?? fetchManifest)(member, ref), - ) - } catch { - return false - } -} diff --git a/.git-hooks/_shared/canonical/fork-scan.mts b/.git-hooks/_shared/canonical/fork-scan.mts deleted file mode 100644 index 938497af..00000000 --- a/.git-hooks/_shared/canonical/fork-scan.mts +++ /dev/null @@ -1,260 +0,0 @@ -import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' -import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs' -import path from 'node:path' -import { readCanonicalIndexEntry, readCanonicalTreeEntry } from './git.mts' -import { canonicalMemberCopyMatches } from './proof.mts' -import type { CanonicalIndexEntry } from './git.mts' - -import { - fleetCanonicalEntries, - isOperatorLocalPath, - isPerRepoMarkerPath, -} from '../../../.claude/hooks/fleet/_shared/fleet-fork.mts' -import { - findFleetRegions, - textHasFleetBlockMarkers, -} from '../../../.claude/hooks/fleet/_shared/fleet-markers.mts' - -// Each child names one capability or member, never an arbitrary generated -// bucket. Generated universal files map directly to the destination tree. -const TEMPLATE_ROOTS: readonly string[] = [ - path.join('base', 'conditional'), - 'overrides', - path.join('generated', 'conditional'), -] - -/** - * Candidate template sources for a live repo-relative path. - */ -export function templateTwinPaths(repoRoot: string, file: string): string[] { - const candidates = [ - path.join(repoRoot, 'template', 'base', 'universal', file), - ] - const generatedUniversal = path.join( - repoRoot, - 'template', - 'generated', - 'universal', - ) - if (existsSync(generatedUniversal)) { - candidates.push(path.join(generatedUniversal, file)) - } - for (let i = 0, { length } = TEMPLATE_ROOTS; i < length; i += 1) { - const root = path.join(repoRoot, 'template', TEMPLATE_ROOTS[i]!) - let names: string[] - try { - names = readdirSync(root) - } catch { - continue - } - for (let j = 0, { length: namesLength } = names; j < namesLength; j += 1) { - candidates.push(path.join(root, names[j]!, file)) - } - } - return candidates -} - -/** - * Whether the live content is byte-identical to one of its template twins. - * - * Then it is cascade OUTPUT, not a fork: a fork is a live copy that DIVERGED - * from canonical. The cascade lands its own mirrors outside this hook chain, - * but when it loses the index lock to a parallel session it leaves them staged, - * and only the operator can land them. Refusing that commit leaves no reachable - * fix - the tool-call guard forbids writing the mirror by hand, and the cascade - * cannot retry while the lock is held - so the operator's only remaining route - * is skipping every hook, which is strictly worse than this exemption. - */ -export function matchesTemplateTwin( - repoRoot: string, - file: string, - content: string | Uint8Array, - mode = '100644', -): boolean { - const candidates = templateTwinPaths(repoRoot, file) - for (let i = 0, { length } = candidates; i < length; i += 1) { - try { - const stat = lstatSync(candidates[i]!) - const candidateMode = stat.mode & 0o111 ? '100755' : '100644' - if (!stat.isFile() || candidateMode !== mode) { - continue - } - if ( - readFileSync(candidates[i]!).equals( - typeof content === 'string' ? Buffer.from(content) : content, - ) - ) { - return true - } - } catch { - continue - } - } - return false -} - -function readForkGit(repoRoot: string, args: string[]): string { - const result = spawnSync('git', ['--literal-pathspecs', ...args], { - cwd: repoRoot, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 5000, - }) - return result.status === 0 ? (result.stdout ?? '') : '' -} - -function mergeParentIds(repoRoot: string): string[] { - const mergeHeadPath = readForkGit(repoRoot, [ - 'rev-parse', - '--git-path', - 'MERGE_HEAD', - ]).trim() - const parents = readFileSync(path.resolve(repoRoot, mergeHeadPath), 'utf8') - .trim() - .split(/\r?\n/) - // Each parent is a full SHA-1 (40 hex digits) or SHA-256 (64 hex digits). - if ( - !parents.every(parent => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(parent)) - ) { - return [] - } - return parents -} - -export function matchesMergeParentIndex( - repoRoot: string, - file: string, -): boolean { - try { - const parents = mergeParentIds(repoRoot) - if (parents.length === 0) { - return false - } - const staged = readForkGit(repoRoot, [ - 'ls-files', - '--stage', - '-z', - '--', - file, - ]) - // Match one stage-zero entry: mode, object ID, stage, then a tab. - const entry = /^(\d+) ([a-f0-9]+) 0\t/.exec(staged) - if (!entry || staged !== `${entry[0]}${file}\0`) { - return false - } - const expected = `${entry[1]} blob ${entry[2]}\t${file}\0` - for (const parent of parents) { - if ( - readForkGit(repoRoot, ['cat-file', '-t', parent]).trim() !== 'commit' - ) { - return false - } - if ( - readForkGit(repoRoot, ['ls-tree', '-z', parent, '--', file]) === - expected - ) { - return true - } - } - } catch { - return false - } - return false -} - -export interface CanonicalForkFinding { - file: string -} - -function isInsideTemplateRelative(file: string): boolean { - return file === 'template' || file.startsWith('template/') -} - -/** - * Every staged path (repo-relative, POSIX-normalized, add/change/modify - * only — a caller filters deletions out via `--diff-filter=ACM`) that is - * fleet-canonical and differs from its canonical source or merge parent. - */ -export function scanCanonicalForkPaths( - stagedFiles: readonly string[], - repoRoot: string, -): CanonicalForkFinding[] { - const entries = fleetCanonicalEntries(repoRoot) - if (entries.length === 0) { - return [] - } - const findings: CanonicalForkFinding[] = [] - for (let i = 0, { length } = stagedFiles; i < length; i += 1) { - const file = stagedFiles[i]! - if (isInsideTemplateRelative(file)) { - continue - } - if (isPerRepoMarkerPath(file) || isOperatorLocalPath(file)) { - continue - } - let isCanonical = false - for ( - let j = 0, { length: entriesLength } = entries; - j < entriesLength; - j += 1 - ) { - const entry = entries[j]! - // Glob entries are best-effort excluded here too — same conservative - // call `isCanonicalRelativePath` makes, so a bad pattern can never - // over-block a commit. - if (entry.includes('*')) { - continue - } - if (file === entry || file.startsWith(`${entry}/`)) { - isCanonical = true - break - } - } - if (!isCanonical) { - continue - } - const entry = readCanonicalIndexEntry(repoRoot, file) - if (entry && stagedCanonicalFileIsAllowed(repoRoot, file, entry)) { - continue - } - findings.push({ file }) - } - return findings -} - -function fleetRegionBodies(content: Uint8Array): string[] { - const text = Buffer.from(content).toString('utf8') - if (!textHasFleetBlockMarkers(text)) { - return [] - } - const lines = text.split(/(?<=\n)/u) - return findFleetRegions(lines).map(region => - lines.slice(region.start, region.end + 1).join(''), - ) -} - -function stagedCanonicalFileIsAllowed( - repoRoot: string, - file: string, - entry: CanonicalIndexEntry, -): boolean { - if (matchesMergeParentIndex(repoRoot, file)) { - return true - } - const baseline = readCanonicalTreeEntry(repoRoot, 'HEAD', file) - if (baseline && baseline.mode === entry.mode) { - const before = fleetRegionBodies(baseline.content) - const after = fleetRegionBodies(entry.content) - if ( - before.length > 0 && - before.length === after.length && - before.every((body, index) => body === after[index]) - ) { - return true - } - } - return ( - matchesTemplateTwin(repoRoot, file, entry.content, entry.mode) || - canonicalMemberCopyMatches(repoRoot, file, entry) - ) -} diff --git a/.git-hooks/_shared/canonical/git.mts b/.git-hooks/_shared/canonical/git.mts deleted file mode 100644 index e9396dda..00000000 --- a/.git-hooks/_shared/canonical/git.mts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * @file Read immutable Git blobs and the caller's active stage-zero index. - */ -import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' -import process from 'node:process' -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' -import { GIT_CONTEXT_VARS } from '../git-context-vars.mts' - -export interface CanonicalGitOptions { - input?: Uint8Array | undefined - preserveIndex?: boolean | undefined -} -export interface CanonicalGitResult { - status: number - stdout: Uint8Array -} -export type CanonicalGitRead = ( - root: string, - args: readonly string[], - options?: CanonicalGitOptions | undefined, -) => CanonicalGitResult -export interface CanonicalIndexEntry { - mode: string - oid: string - content: Uint8Array -} - -export function readCanonicalGit( - root: string, - args: readonly string[], - options: CanonicalGitOptions = {}, -): CanonicalGitResult { - const env = { ...process.env } - for (let i = 0, { length } = GIT_CONTEXT_VARS; i < length; i += 1) { - const name = GIT_CONTEXT_VARS[i]! - if (name !== 'GIT_INDEX_FILE' || !options.preserveIndex) { - delete env[name] - } - } - const result = spawnSync( - 'git', - ['--no-replace-objects', '--literal-pathspecs', ...args], - { - cwd: root, - encoding: 'buffer', - env, - input: options.input, - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 5000, - maxBuffer: 4_194_304, - }, - ) - return { - status: result.status ?? 1, - stdout: Buffer.isBuffer(result.stdout) - ? result.stdout - : Buffer.from(result.stdout ?? ''), - } -} - -export function canonicalGitText( - root: string, - args: readonly string[], - readGit: CanonicalGitRead = readCanonicalGit, -): string | undefined { - const result = readGit(root, args) - return result.status === 0 - ? Buffer.from(result.stdout).toString('utf8') - : undefined -} - -export function canonicalPathIsSafe(file: string): boolean { - const normalized = normalizePath(file) - return ( - file === normalized && - file.length > 0 && - file.length < 1024 && - !normalized.startsWith('/') && - !normalized.includes('\\') && - !/[\x00-\x1f\x7f:]/u.test(file) && - normalizePath(file) - .split('/') - .every( - part => - part !== '' && - part !== '.' && - part !== '..' && - part.toLowerCase() !== '.git', - ) - ) -} - -function readBlobEntry( - root: string, - oid: string, - mode: string, - readGit: CanonicalGitRead, -): CanonicalIndexEntry | undefined { - if (!['100644', '100755'].includes(mode)) { - return undefined - } - const result = readGit(root, ['cat-file', 'blob', oid]) - return result.status === 0 ? { mode, oid, content: result.stdout } : undefined -} - -export function readCanonicalIndexEntry( - root: string, - file: string, - readGit: CanonicalGitRead = readCanonicalGit, -): CanonicalIndexEntry | undefined { - if (!canonicalPathIsSafe(file)) { - return undefined - } - const result = readGit(root, ['ls-files', '--stage', '-z', '--', file], { - preserveIndex: true, - }) - if (result.status !== 0) { - return undefined - } - // A single stage-zero regular blob, with its exact NUL-terminated path. - const match = - /^(100644|100755) ((?:[a-f0-9]{40}|[a-f0-9]{64})) 0\t([^\0]+)\0$/u.exec( - Buffer.from(result.stdout).toString('utf8'), - ) - return match?.[3] === file - ? readBlobEntry(root, match[2]!, match[1]!, readGit) - : undefined -} - -export function readCanonicalTreeEntry( - root: string, - ref: string, - file: string, - readGit: CanonicalGitRead = readCanonicalGit, -): CanonicalIndexEntry | undefined { - if (!canonicalPathIsSafe(file)) { - return undefined - } - const text = canonicalGitText( - root, - ['ls-tree', '-z', ref, '--', file], - readGit, - ) - if (text === undefined) { - return undefined - } - // A single regular tree blob, with its exact NUL-terminated path. - const match = - /^(100644|100755) blob ((?:[a-f0-9]{40}|[a-f0-9]{64}))\t([^\0]+)\0$/u.exec( - text, - ) - return match?.[3] === file - ? readBlobEntry(root, match[2]!, match[1]!, readGit) - : undefined -} - -export function canonicalEntriesEqual( - left: CanonicalIndexEntry, - right: CanonicalIndexEntry, -): boolean { - return ( - left.mode === right.mode && Buffer.from(left.content).equals(right.content) - ) -} diff --git a/.git-hooks/_shared/canonical/patch.mts b/.git-hooks/_shared/canonical/patch.mts deleted file mode 100644 index ef3bf7ad..00000000 --- a/.git-hooks/_shared/canonical/patch.mts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @file Select complete context-preserving hunks from an immutable Git patch. - */ -export function canonicalHunksAreValid(value: unknown): value is number[] { - return ( - Array.isArray(value) && - value.length > 0 && - value.length <= 128 && - value.every( - (index, position) => - Number.isSafeInteger(index) && - index >= 0 && - (position === 0 || index > value[position - 1]), - ) - ) -} - -export function selectCanonicalPatch( - text: string, - source: string, - selected?: readonly number[] | undefined, -): Uint8Array | undefined { - if ( - !text.startsWith(`diff --git a/${source} b/${source}\n`) || - text.split('\ndiff --git ').length !== 1 - ) { - return undefined - } - if ( - /^(?:Binary files |GIT binary patch|copy |deleted file mode|new file mode|new mode|old mode|rename )/mu.test( - text, - ) - ) { - return undefined - } - const sections = text.split(/(?=^@@ )/mu) - const header = sections.shift() - if ( - !header?.includes(`--- a/${source}\n+++ b/${source}\n`) || - sections.length === 0 - ) { - return undefined - } - const indexes = selected ?? [...sections.keys()] - if ( - !canonicalHunksAreValid(indexes) || - indexes.some(index => index >= sections.length) - ) { - return undefined - } - return Buffer.from(header + indexes.map(index => sections[index]).join('')) -} diff --git a/.git-hooks/_shared/canonical/proof.mts b/.git-hooks/_shared/canonical/proof.mts deleted file mode 100644 index f01724b7..00000000 --- a/.git-hooks/_shared/canonical/proof.mts +++ /dev/null @@ -1,357 +0,0 @@ -import { canonicalBundleCopyMatches } from './bundle.mts' -/** - * @file Prove stage-zero bytes from signed producer trees or ordered template - * patches. - */ -import { - chmodSync, - mkdirSync, - mkdtempSync, - readFileSync, - writeFileSync, -} from 'node:fs' -import os from 'node:os' -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' -import { selectCanonicalPatch } from './patch.mts' -import { applyCanonicalReplacements } from './replacements.mts' -import path from 'node:path' -import { - canonicalEntriesEqual, - canonicalGitText, - readCanonicalGit, - readCanonicalTreeEntry, -} from './git.mts' -import { - canonicalEligibleSources, - canonicalProducerAllowed, - canonicalSourceAllowed, - findCanonicalProducer, -} from './source.mts' -import { readCanonicalReceipt, validateCanonicalRecipe } from './receipt.mts' -import type { CanonicalGitRead, CanonicalIndexEntry } from './git.mts' -import type { CanonicalPatchRecipe, CanonicalPatchStep } from './receipt.mts' -import { safeDeleteSync } from '@socketsecurity/lib-stable/fs/safe' -export { readCanonicalIndexEntry } from './git.mts' -export { validateCanonicalRecipe } from './receipt.mts' -export type { - CanonicalGitRead, - CanonicalIndexEntry, - CanonicalGitOptions, - CanonicalGitResult, -} from './git.mts' -export type { CanonicalPatchRecipe, CanonicalPatchStep } from './receipt.mts' - -export interface CanonicalProofOptions { - readGit?: CanonicalGitRead | undefined - producerAllowed?: ((root: string) => boolean) | undefined - sourceAllowed?: - | (( - member: string, - producer: string, - commit: string, - source: string, - target: string, - ) => boolean) - | undefined -} - -function signedParent( - root: string, - commit: string, - readGit: CanonicalGitRead, -): string | undefined { - if ( - canonicalGitText( - root, - ['log', '-1', '--format=%G?', commit], - readGit, - )?.trim() !== 'G' - ) { - return undefined - } - const parents = canonicalGitText( - root, - ['rev-list', '--parents', '-n', '1', commit], - readGit, - ) - ?.trim() - .split(' ') - return parents?.length === 2 && parents[0] === commit ? parents[1] : undefined -} - -function stepSourceAllowed( - member: string, - recipe: CanonicalPatchRecipe, - step: CanonicalPatchStep, - config: CanonicalProofOptions, -): boolean { - const readGit = config.readGit ?? readCanonicalGit - const allowed = - config.sourceAllowed ?? - ((memberRoot, producerRoot, commit, source, target) => - canonicalSourceAllowed( - memberRoot, - producerRoot, - commit, - source, - target, - readGit, - )) - return allowed( - member, - recipe.producerRoot, - step.commit, - step.sourcePath, - step.targetPath, - ) -} - -function sourcePatch( - member: string, - recipe: CanonicalPatchRecipe, - step: CanonicalPatchStep, - config: CanonicalProofOptions, -): Uint8Array | undefined { - const readGit = config.readGit ?? readCanonicalGit - const parent = signedParent(recipe.producerRoot, step.commit, readGit) - if (!parent || !stepSourceAllowed(member, recipe, step, config)) { - return undefined - } - const before = readCanonicalTreeEntry( - recipe.producerRoot, - parent, - step.sourcePath, - readGit, - ) - const after = readCanonicalTreeEntry( - recipe.producerRoot, - step.commit, - step.sourcePath, - readGit, - ) - if (!before || !after || before.mode !== after.mode) { - return undefined - } - const patch = readGit(recipe.producerRoot, [ - 'diff', - '--no-ext-diff', - '--no-textconv', - '--src-prefix=a/', - '--dst-prefix=b/', - parent, - step.commit, - '--', - step.sourcePath, - ]) - if (patch.status !== 0 || patch.stdout.length === 0) { - return undefined - } - return selectCanonicalPatch( - Buffer.from(patch.stdout).toString('utf8'), - step.sourcePath, - step.hunks, - ) -} - -function applyStrictPatch( - scratch: string, - patch: Uint8Array, - strip: number, -): boolean { - return ( - readCanonicalGit( - scratch, - ['apply', '--whitespace=nowarn', `-p${strip}`, '-'], - { input: patch }, - ).status === 0 - ) -} - -function applyIndividualHunks( - scratch: string, - destination: string, - patch: Uint8Array, - strip: number, -): boolean { - const sections = Buffer.from(patch) - .toString('utf8') - .split(/(?=^@@ )/mu) - const header = sections.shift() - if (!header || sections.length === 0) { - return false - } - for (let i = 0, { length } = sections; i < length; i += 1) { - const hunk = Buffer.from(header + sections[i]) - if (applyStrictPatch(scratch, hunk, strip)) { - continue - } - const replacement = applyCanonicalReplacements( - readFileSync(destination), - hunk, - ) - if (!replacement) { - return false - } - writeFileSync(destination, replacement) - } - return true -} - -function applyProofSteps( - member: string, - file: string, - recipe: CanonicalPatchRecipe, - baseline: CanonicalIndexEntry, - config: CanonicalProofOptions, -): Uint8Array | undefined { - const scratch = mkdtempSync(path.join(os.tmpdir(), 'fleet-canonical-proof-')) - try { - const destination = path.join(scratch, file) - mkdirSync(path.dirname(destination), { recursive: true }) - writeFileSync(destination, baseline.content) - chmodSync(destination, baseline.mode === '100755' ? 0o755 : 0o644) - const seen = new Set() - const steps = recipe.patches.filter(item => item.targetPath === file) - for ( - let stepIndex = 0, { length } = steps; - stepIndex < length; - stepIndex += 1 - ) { - const step = steps[stepIndex]! - if (seen.has(step.commit)) { - return undefined - } - seen.add(step.commit) - const patch = sourcePatch(member, recipe, step, config) - if (!patch) { - return undefined - } - const strip = - normalizePath(step.sourcePath).split('/').length - - normalizePath(file).split('/').length + - 1 - if ( - !applyStrictPatch(scratch, patch, strip) && - (step.application !== 'unique-replacement' || - !applyIndividualHunks(scratch, destination, patch, strip)) - ) { - return undefined - } - } - return readFileSync(destination) - } finally { - safeDeleteSync(scratch) - } -} - -export function canonicalRecipeMatches( - member: string, - file: string, - entry: CanonicalIndexEntry, - value: unknown, - options: CanonicalProofOptions = {}, -): boolean { - const recipe = validateCanonicalRecipe(value) - if (!recipe || !recipe.patches.some(step => step.targetPath === file)) { - return false - } - const readGit = options.readGit ?? readCanonicalGit - if ( - !(options.producerAllowed ?? canonicalProducerAllowed)(recipe.producerRoot) - ) { - return false - } - if ( - canonicalGitText( - member, - ['rev-parse', '--verify', 'HEAD'], - readGit, - )?.trim() !== recipe.memberHead - ) { - return false - } - const baseline = readCanonicalTreeEntry(member, 'HEAD', file, readGit) - if (!baseline || baseline.mode !== entry.mode) { - return false - } - try { - const result = applyProofSteps(member, file, recipe, baseline, options) - return result !== undefined && Buffer.from(result).equals(entry.content) - } catch { - return false - } -} - -export function canonicalRecipeCopyMatches( - member: string, - file: string, - entry: CanonicalIndexEntry, - value: unknown, - options: CanonicalProofOptions = {}, -): boolean { - const recipe = validateCanonicalRecipe(value) - const readGit = options.readGit ?? readCanonicalGit - if ( - !recipe || - !(options.producerAllowed ?? canonicalProducerAllowed)( - recipe.producerRoot, - ) || - canonicalGitText( - member, - ['rev-parse', '--verify', 'HEAD'], - readGit, - )?.trim() !== recipe.memberHead - ) { - return false - } - return recipe.patches.some(step => { - if ( - step.targetPath !== file || - !signedParent(recipe.producerRoot, step.commit, readGit) || - !stepSourceAllowed(member, recipe, step, options) - ) { - return false - } - const source = readCanonicalTreeEntry( - recipe.producerRoot, - step.commit, - step.sourcePath, - readGit, - ) - return source !== undefined && canonicalEntriesEqual(entry, source) - }) -} - -export function canonicalMemberCopyMatches( - member: string, - file: string, - entry: CanonicalIndexEntry, -): boolean { - const producer = findCanonicalProducer(member) - if (producer) { - const head = canonicalGitText(producer, [ - 'rev-parse', - '--verify', - 'HEAD', - ])?.trim() - if (head && signedParent(producer, head, readCanonicalGit)) { - for (const source of canonicalEligibleSources(member, file)) { - if (!canonicalSourceAllowed(member, producer, head, source, file)) { - continue - } - const committed = readCanonicalTreeEntry(producer, head, source) - if (committed && canonicalEntriesEqual(entry, committed)) { - return true - } - } - } - } - if (canonicalBundleCopyMatches(member, file, entry)) { - return true - } - const receipt = readCanonicalReceipt(member) - return ( - canonicalRecipeCopyMatches(member, file, entry, receipt) || - canonicalRecipeMatches(member, file, entry, receipt) - ) -} diff --git a/.git-hooks/_shared/canonical/receipt.mts b/.git-hooks/_shared/canonical/receipt.mts deleted file mode 100644 index 08438264..00000000 --- a/.git-hooks/_shared/canonical/receipt.mts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @file Validate runtime-only recipes for reproducible signed template deltas. - */ -import { readFileSync } from 'node:fs' -import path from 'node:path' -import { canonicalHunksAreValid } from './patch.mts' -import { - canonicalGitText, - canonicalPathIsSafe, - readCanonicalGit, -} from './git.mts' -import type { CanonicalGitRead } from './git.mts' - -export interface CanonicalPatchStep { - commit: string - sourcePath: string - targetPath: string - hunks?: number[] | undefined - application?: 'unique-replacement' | undefined -} -export interface CanonicalPatchRecipe { - version: 1 - memberHead: string - producerRoot: string - patches: CanonicalPatchStep[] -} -// Accept immutable SHA-1 or SHA-256 object IDs only. -const SHA_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u -function recordValue(value: unknown): Record | undefined { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : undefined -} -function validStep(value: unknown): value is CanonicalPatchStep { - const record = recordValue(value) - if ( - record?.['application'] !== undefined && - record['application'] !== 'unique-replacement' - ) { - return false - } - if ( - record?.['hunks'] !== undefined && - !canonicalHunksAreValid(record['hunks']) - ) { - return false - } - return Boolean( - record && - typeof record['commit'] === 'string' && - SHA_PATTERN.test(record['commit']) && - typeof record['sourcePath'] === 'string' && - canonicalPathIsSafe(record['sourcePath']) && - typeof record['targetPath'] === 'string' && - canonicalPathIsSafe(record['targetPath']) && - record['sourcePath'].endsWith(`/${record['targetPath']}`), - ) -} -export function validateCanonicalRecipe( - value: unknown, -): CanonicalPatchRecipe | undefined { - const record = recordValue(value) - if ( - !record || - record['version'] !== 1 || - typeof record['memberHead'] !== 'string' || - !SHA_PATTERN.test(record['memberHead']) - ) { - return undefined - } - if ( - typeof record['producerRoot'] !== 'string' || - !path.isAbsolute(record['producerRoot']) || - record['producerRoot'].includes('\0') - ) { - return undefined - } - if ( - !Array.isArray(record['patches']) || - record['patches'].length === 0 || - record['patches'].length > 64 || - !record['patches'].every(validStep) - ) { - return undefined - } - return record as unknown as CanonicalPatchRecipe -} -export function canonicalReceiptPath( - root: string, - readGit: CanonicalGitRead = readCanonicalGit, -): string | undefined { - const relative = canonicalGitText( - root, - ['rev-parse', '--git-path', 'fleet/canonical-patch-receipt.json'], - readGit, - )?.trim() - return relative ? path.resolve(root, relative) : undefined -} -export function readCanonicalReceipt( - root: string, -): CanonicalPatchRecipe | undefined { - const receipt = canonicalReceiptPath(root) - if (!receipt) { - return undefined - } - try { - const bytes = readFileSync(receipt) - return bytes.length <= 65_536 - ? validateCanonicalRecipe(JSON.parse(bytes.toString('utf8'))) - : undefined - } catch { - return undefined - } -} diff --git a/.git-hooks/_shared/canonical/replacements.mts b/.git-hooks/_shared/canonical/replacements.mts deleted file mode 100644 index 1608ebcd..00000000 --- a/.git-hooks/_shared/canonical/replacements.mts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @file Apply uniquely located replacement runs from verified signed hunks. - */ -import { selectCanonicalPatch } from './patch.mts' - -interface ReplacementRun { - before: string - after: string -} -interface HunkState { - before: number - after: number - previous: string - run: ReplacementRun - runs: ReplacementRun[] -} - -function finishRun(state: HunkState): boolean { - const run = state.run - if (!run.before && !run.after) { - return true - } - if (!run.before) { - return false - } - state.runs.push(run) - state.run = { before: '', after: '' } - return true -} - -function consumeHunkLine(state: HunkState, line: string): boolean { - const tag = line[0] - if (tag === ' ') { - state.before += 1 - state.after += 1 - state.previous = tag - return finishRun(state) - } - if (tag === '-') { - state.before += 1 - state.run.before += line.slice(1) - } else if (tag === '+') { - state.after += 1 - state.run.after += line.slice(1) - } else if ( - line === '\\ No newline at end of file' || - line === '\\ No newline at end of file\n' - ) { - if (state.previous === '-') { - state.run.before = state.run.before.slice(0, -1) - } else if (state.previous === '+') { - state.run.after = state.run.after.slice(0, -1) - } else if (state.previous !== ' ') { - return false - } - return true - } else { - return false - } - state.previous = tag - return true -} - -function hunkReplacements(section: string): ReplacementRun[] | undefined { - // Preserve each line terminator, including a final unterminated line. - const lines = section.match(/[^\n]*\n|[^\n]+$/gu) ?? [] - const header = lines.shift() - // Capture old and new line counts from one complete unified hunk header. - const match = - header && /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@[^\n]*\n$/u.exec(header) - if (!match) { - return undefined - } - const state: HunkState = { - before: 0, - after: 0, - previous: '', - run: { before: '', after: '' }, - runs: [], - } - if (!lines.every(line => consumeHunkLine(state, line)) || !finishRun(state)) { - return undefined - } - if ( - state.before !== Number(match[1] ?? 1) || - state.after !== Number(match[2] ?? 1) - ) { - return undefined - } - return state.runs -} - -function utf8Text(bytes: Uint8Array): string | undefined { - const text = Buffer.from(bytes).toString('utf8') - return !text.includes('\0') && Buffer.from(text).equals(bytes) - ? text - : undefined -} - -function replaceUniqueRun( - text: string, - run: ReplacementRun, -): string | undefined { - const index = text.indexOf(run.before) - if ( - index < 0 || - (index > 0 && text.charCodeAt(index - 1) !== 10) /* '\n' */ || - text.indexOf(run.before, index + 1) !== -1 || - (!run.before.endsWith('\n') && index + run.before.length !== text.length) - ) { - return undefined - } - return ( - text.slice(0, index) + run.after + text.slice(index + run.before.length) - ) -} - -export function applyCanonicalReplacements( - content: Uint8Array, - patch: Uint8Array, -): Uint8Array | undefined { - let text = utf8Text(content) - const delta = utf8Text(patch) - if (text === undefined || delta === undefined) { - return undefined - } - const first = delta.split(/\r?\n/, 1)[0] - const source = first?.startsWith('diff --git a/') - ? first.slice('diff --git a/'.length).split(' b/', 1)[0] - : undefined - if (!source || !selectCanonicalPatch(delta, source)) { - return undefined - } - const sections = delta.split(/(?=^@@ )/mu).slice(1) - let total = 0 - for ( - let sectionIndex = 0, { length } = sections; - sectionIndex < length; - sectionIndex += 1 - ) { - const runs = hunkReplacements(sections[sectionIndex]!) - if (!runs) { - return undefined - } - for ( - let runIndex = 0, { length: runLength } = runs; - runIndex < runLength; - runIndex += 1 - ) { - total += 1 - text = replaceUniqueRun(text, runs[runIndex]!) - if (text === undefined) { - return undefined - } - } - } - return total > 0 ? Buffer.from(text) : undefined -} diff --git a/.git-hooks/_shared/canonical/source.mts b/.git-hooks/_shared/canonical/source.mts deleted file mode 100644 index f8a67f7d..00000000 --- a/.git-hooks/_shared/canonical/source.mts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * @file Authorize producer origins and member-specific committed template - * sources. - */ -import { findWheelhouseRoot } from '../../../.claude/hooks/fleet/_shared/wheelhouse-root.mts' -import fleetRosterJson from '../../../.claude/skills/fleet/cascading-commits/lib/fleet-repos.json' with { type: 'json' } -import { SOCKET_GITHUB_ORGS } from '../../../scripts/fleet/constants/socket-scopes.mts' -import { isFleetPackProducerSlug } from '../../../scripts/fleet/member/fleet-membership.mts' -import { - canonicalGitText, - canonicalPathIsSafe, - readCanonicalGit, - readCanonicalTreeEntry, -} from './git.mts' -import type { CanonicalGitRead } from './git.mts' - -export function canonicalMemberSlug(root: string): string | undefined { - const remote = canonicalGitText(root, ['remote', 'get-url', 'origin'])?.trim() - // Accept the three GitHub transports, retaining the organization segment. - const match = - remote && - /^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/iu.exec( - remote, - ) - if (!match) { - return undefined - } - const member = fleetRosterJson.repos.find( - repo => repo.name.toLowerCase() === match[2]!.toLowerCase(), - ) - if (!member) { - return undefined - } - const owners = - 'owner' in member && typeof member.owner === 'string' - ? [member.owner] - : SOCKET_GITHUB_ORGS - return owners.some(owner => owner.toLowerCase() === match[1]!.toLowerCase()) - ? member.name - : undefined -} -export function canonicalOriginAllowed(root: string): boolean { - return canonicalMemberSlug(root) !== undefined -} -export function canonicalProducerAllowed(root: string): boolean { - return isFleetPackProducerSlug(canonicalMemberSlug(root)) -} -export function findCanonicalProducer(memberRoot: string): string | undefined { - const root = findWheelhouseRoot({ startDir: memberRoot }) - return root && canonicalProducerAllowed(root) ? root : undefined -} - -function memberConfig( - member: string, - readGit: CanonicalGitRead, -): Record | undefined { - const source = canonicalGitText( - member, - ['show', 'HEAD:.config/repo/socket-wheelhouse.json'], - readGit, - ) - try { - const parsed: unknown = - source === undefined ? undefined : JSON.parse(source) - return parsed !== null && - typeof parsed === 'object' && - !Array.isArray(parsed) - ? (parsed as Record) - : undefined - } catch { - return undefined - } -} - -function objectValue(value: unknown): Record { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : {} -} - -function memberLayers(member: string, readGit: CanonicalGitRead): string[] { - const slug = canonicalMemberSlug(member) - if (!slug) { - return [] - } - const config = memberConfig(member, readGit) - if (!config || config['repoName'] !== slug) { - return [] - } - const kind = objectValue(config['repo'])['type'] - const capabilities = Object.entries(objectValue(config['capabilities'])) - .filter(([, scopes]) => Array.isArray(scopes) && scopes.length > 0) - .map(([name]) => name) - const layers = [`template/overrides/${slug}`] - const generated = ['template/generated/conditional/npm'] - const release = objectValue(config['release']) - if (release['github'] !== false) { - generated.push('template/generated/conditional/github-release') - layers.push('template/base/conditional/github-release') - } - for (let i = 0, { length } = capabilities; i < length; i += 1) { - const capability = capabilities[i]! - if (/^[a-z][a-z0-9-]*$/u.test(capability)) { - if (capability !== 'npm') { - generated.push(`template/generated/conditional/${capability}`) - } - layers.push(`template/base/conditional/${capability}`) - } - } - if (kind === 'mono' || kind === 'solo') { - layers.push(`template/${kind}`) - } - layers.push('template/base/universal') - return [...generated, 'template/generated/universal', ...layers] -} - -export function canonicalSourceAllowed( - member: string, - producer: string, - commit: string, - source: string, - target: string, - readGit: CanonicalGitRead = readCanonicalGit, -): boolean { - if (!canonicalPathIsSafe(source) || !canonicalPathIsSafe(target)) { - return false - } - for (const layer of memberLayers(member, readGit)) { - const candidate = `${layer}/${target}` - if (readCanonicalTreeEntry(producer, commit, candidate, readGit)) { - return source === candidate - } - } - return false -} - -export function canonicalEligibleSources( - member: string, - file: string, - readGit: CanonicalGitRead = readCanonicalGit, -): string[] { - return canonicalPathIsSafe(file) - ? memberLayers(member, readGit).map(layer => `${layer}/${file}`) - : [] -} diff --git a/.git-hooks/_shared/check-output.mts b/.git-hooks/_shared/check-output.mts deleted file mode 100644 index 48855d5c..00000000 --- a/.git-hooks/_shared/check-output.mts +++ /dev/null @@ -1,48 +0,0 @@ -import { readFileSync } from 'node:fs' -import process from 'node:process' -import { stripVTControlCharacters } from 'node:util' - -import { isDebugNs } from '@socketsecurity/lib-stable/debug/namespace' -import { debugNs } from '@socketsecurity/lib-stable/debug/output' - -import { isMainModule } from '../../scripts/fleet/process/is-main-module.mts' - -export const CHECK_DEBUG_NS = '*' - -export function debugCheck(...args: unknown[]): void { - if (isDebugNs(CHECK_DEBUG_NS)) { - debugNs(CHECK_DEBUG_NS, ...args) - } -} - -export function showCheckOutput(status: number | null, output: string): void { - if (!output) { - return - } - if ( - status !== 0 || - // Keep incomplete gates, Node warning classes, and tool warning labels. - /(?:NOT a pass|Warning:|\bwarn(?:ing)?s?\b)/i.test( - stripVTControlCharacters(output).replace(/\b0 warnings?\b/gi, ''), - ) - ) { - process.stderr.write(output) - } else if (isDebugNs(CHECK_DEBUG_NS)) { - debugCheck(output.trimEnd()) - } -} - -export function showCheckResult(result: { - status: number | null - stdout: string | Buffer | null - stderr: string | Buffer | null -}): void { - showCheckOutput(result.status, `${result.stdout ?? ''}${result.stderr ?? ''}`) -} - -if (isMainModule(import.meta.url)) { - showCheckOutput( - Number(process.argv[2]), - readFileSync(process.argv[3]!, 'utf8'), - ) -} diff --git a/.git-hooks/_shared/commit-format.mts b/.git-hooks/_shared/commit-format.mts deleted file mode 100644 index d2633752..00000000 --- a/.git-hooks/_shared/commit-format.mts +++ /dev/null @@ -1,146 +0,0 @@ -// Conventional Commits 1.0 header validation, shared by both enforcement -// surfaces: -// - the commit-message-format-guard PreToolUse hook (.claude/hooks/), which -// inspects `git commit -m` tool calls at Claude Bash time, and -// - the commit-msg git-stage backstop (.git-hooks/), which inspects the -// subject regardless of how the commit was made (subprocess / worktree / -// CI / test harness) — the layer the tool-call guard never sees. -// Canonical home: .git-hooks/_shared/; the .claude/hooks/ guard imports this -// cross-tree (the shared thing is this code, per the fleet "DRY across the two -// hook trees" rule). This module is side-effect-free — it reads no stdin, spawns -// nothing, and calls process.exit nowhere — so the git-stage hook can import it -// without triggering the guard's stdin-reading `main()`. -// -// Spec: https://www.conventionalcommits.org/en/v1.0.0/ - -// Real Claude-specific integration: "the Claude tool-call guard" names the -// .claude/hooks/ PreToolUse guard, which only exists for Claude Code, not -// generic agent guidance. -// oxlint-disable-next-line socket/no-agent-brand-assumption -- real -export const ALLOWED_TYPES = [ - 'build', - 'chore', - 'ci', - 'docs', - 'feat', - 'fix', - 'perf', - 'refactor', - 'revert', - 'style', - 'test', -] as const - -export const ALLOWED_TYPE_SET: ReadonlySet = new Set(ALLOWED_TYPES) - -// Header form: [(scope)][!]: -// - type: lowercase letters -// - optional (scope) in parens -// - optional `!` breaking-change marker -// - `: ` separator (colon + space) -// - non-empty description -export const HEADER_RE = /^([a-z]+)(\([^)]+\))?(!)?: (.+)$/ - -// Subjects git itself writes for non-`-m` commits. These are never -// Conventional Commits and must not be format-blocked at the git-stage twin -// (the Claude tool-call guard never sees them — they carry no inline -m -// message). `git merge` → `Merge branch …`/`Merge pull request …`; -// `git revert` → `Revert "…"`; autosquash → `fixup! …`/`squash! …`/`amend! …`. -const AUTO_SUBJECT_RE = /^(?:(?:amend|fixup|squash)! |Merge\b|Revert\b)/ - -/** - * True when the subject is one git auto-generates for a merge / revert / - * autosquash commit. Those are exempt from Conventional Commits enforcement. - */ -export function isAutoGeneratedSubject(subject: string): boolean { - return AUTO_SUBJECT_RE.test(subject.trim()) -} - -/** - * Result of validating a single message header. - * - * - Kind: 'ok' — header passes - * - Kind: 'no-type' — first line has no `: ` prefix at all - * - Kind: 'bad-type' — first line has a `: ` prefix but word isn't - * lowercase / not in the type set - * - Kind: 'uppercase-type' — type letters are present but include uppercase - * - Kind: 'empty-description' — header has `: ` but description is - * empty/whitespace - */ -export type HeaderCheck = - | { kind: 'ok' } - | { kind: 'no-type'; line: string } - | { kind: 'bad-type'; line: string; type: string } - | { kind: 'uppercase-type'; line: string; type: string } - | { kind: 'empty-description'; line: string; type: string } - -export function validateHeader(line: string): HeaderCheck { - // Quick pre-check: does the line look like a Conventional header at all? - // We accept any leading word-token before `: ` for diagnosis even if the - // case is wrong; the strict HEADER_RE then refines. - const looseMatch = /^([A-Za-z]+)(\([^)]+\))?(!)?:\s*(.*)$/.exec(line) - if (!looseMatch) { - return { kind: 'no-type', line } - } - const type = looseMatch[1]! - const desc = looseMatch[4]! - // Type must be all-lowercase. - if (type !== type.toLowerCase()) { - return { kind: 'uppercase-type', line, type } - } - // Type must be in the allowed set. - if (!ALLOWED_TYPE_SET.has(type)) { - return { kind: 'bad-type', line, type } - } - // Strict format check (catches "feat:description" without space, etc.). - const strictMatch = HEADER_RE.exec(line) - if (!strictMatch) { - // The loose pattern matched but the strict one didn't — that means - // either the `: ` separator is missing the space, or the description - // is empty. - if (!desc.trim()) { - return { kind: 'empty-description', line, type } - } - return { kind: 'no-type', line } - } - const description = strictMatch[4]! - if (!description.trim()) { - return { kind: 'empty-description', line, type } - } - return { kind: 'ok' } -} - -/** - * Build a context-appropriate suggestion for an invalid header. We look at the - * user's input and propose ONE example of a valid replacement based on what - * they typed. - */ -export function suggestReplacement(check: HeaderCheck): string { - if (check.kind === 'ok') { - return '' - } - const text = check.line.trim() - // Lowercase variant: try to recover the intent. - if (check.kind === 'uppercase-type') { - return `${check.type.toLowerCase()}: ${text.slice(text.indexOf(':') + 1).trim()}` - } - if (check.kind === 'bad-type') { - // Suggest 'feat' as a generic recoverable type, keep the rest. - const rest = - text.slice(text.indexOf(':') + 1).trim() || 'describe the change' - return `feat: ${rest}` - } - if (check.kind === 'empty-description') { - return `${check.type}: describe the change` - } - // no-type: try to fold whatever the user typed into a feat header. - const words = text.split(/\s+/).filter(Boolean) - const first = (words[0] ?? '').toLowerCase() - // If the first word looks like a noun (e.g. "parser", "extension"), use it - // as a scope and keep the rest as the description. - if (words.length >= 2 && /^[a-z][a-z0-9-]*$/.test(first)) { - const rest = words.slice(1).join(' ') - return `feat(${first}): ${rest}` - } - return `feat: ${text || 'describe the change'}` -} diff --git a/.git-hooks/_shared/commit-subject.mts b/.git-hooks/_shared/commit-subject.mts deleted file mode 100644 index ac64c0a1..00000000 --- a/.git-hooks/_shared/commit-subject.mts +++ /dev/null @@ -1,74 +0,0 @@ -// Placeholder commit-subject detection, shared by both enforcement surfaces: -// - the no-placeholder-commit-subject-guard PreToolUse hook (.claude/hooks/), -// which inspects `git commit -m` tool calls, and -// - the commit-msg git-stage backstop (.git-hooks/), which inspects the -// subject regardless of how the commit was made (subprocess / worktree / -// CI / test harness). -// Canonical home: .git-hooks/_shared/; the .claude/hooks/ guard imports this -// cross-tree (the shared thing is this code, per the fleet "DRY across the two -// hook trees" rule). - -// Subjects that say nothing about the change — the fingerprint of a -// test-harness / replayed / sandbox commit (a batch of `initial` commits once -// reached a fleet repo's main). Matched case-insensitively against the whole -// trimmed subject, after stripping one trailing period. -const PLACEHOLDER_SUBJECTS = new Set([ - '.', - 'changes', - 'commit', - 'fix', - 'fixes', - 'fixup', - 'init', - 'initial', - 'initial commit', - 'temp', - 'test', - 'tmp', - 'update', - 'updates', - 'wip', -]) - -/** - * The subject line of a commit message: the first non-blank, non-comment line. - */ -export function commitSubject(message: string): string { - return ( - message - .split(/\r?\n/) - .find(l => l.trim() && !l.trimStart().startsWith('#')) - ?.trim() ?? '' - ) -} - -/** - * Why a commit subject is unusable, or `ok`. - * - * `empty` and `placeholder` are separated because they have DIFFERENT causes - * and different fixes. An empty subject is almost never someone typing nothing: - * it is a `-F` path that did not resolve, a truncated command line, or an - * editor closed without saving. Telling that author their subject looks like - * `wip` sends them looking for a denylist entry that was never involved. - */ -export type CommitSubjectVerdict = 'empty' | 'ok' | 'placeholder' - -/** - * Classify a commit subject. Strips a single trailing period and lowercases - * before matching the denylist. - */ -export function commitSubjectVerdict(subject: string): CommitSubjectVerdict { - const norm = subject.trim().replace(/\.$/, '').trim().toLowerCase() - if (!norm) { - return 'empty' - } - return PLACEHOLDER_SUBJECTS.has(norm) ? 'placeholder' : 'ok' -} - -/** - * True when a commit subject is unusable, either kind. Callers that need to - * explain WHICH problem it is read {@link commitSubjectVerdict} instead. - */ -export function isPlaceholderSubject(subject: string): boolean { - return commitSubjectVerdict(subject) !== 'ok' -} diff --git a/.git-hooks/_shared/cross-repo-expressions.mts b/.git-hooks/_shared/cross-repo-expressions.mts deleted file mode 100644 index ac7fb2b1..00000000 --- a/.git-hooks/_shared/cross-repo-expressions.mts +++ /dev/null @@ -1,186 +0,0 @@ -import { fileURLToPath } from 'node:url' - -import type { AcornNode } from '../../.claude/hooks/fleet/_shared/ast/core.mts' -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' -import { - repositoryPathApi, - resolveRepositoryPath, -} from './repo-containment.mts' - -export interface RepositoryExpressionContext { - root: string - file: string - bindings: Map -} - -export function repositoryAstNode(value: unknown): AcornNode | undefined { - return value !== null && - typeof value === 'object' && - typeof (value as AcornNode).type === 'string' - ? (value as AcornNode) - : undefined -} - -export function repositoryExpressionName(node: AcornNode | undefined): string { - if (!node) { - return '' - } - if (node.type === 'Identifier') { - return String(node['name']) - } - if (node.type === 'MetaProperty') { - return 'import.meta' - } - if (node.type === 'MemberExpression' && !node['computed']) { - return `${repositoryExpressionName(repositoryAstNode(node['object']))}.${repositoryExpressionName(repositoryAstNode(node['property']))}` - } - return '' -} - -export function isRepositoryPathExpression(node: AcornNode): boolean { - const name = repositoryExpressionName(repositoryAstNode(node['callee'])) - return ( - (node.type === 'CallExpression' && - // Match path namespace methods and named imports of the same operations. - /^(?:(?:path|posix|win32)(?:\.(?:posix|win32))?\.)?(?:dirname|fileURLToPath|join|resolve)$/u.test( - name, - )) || - (node.type === 'NewExpression' && name === 'URL') - ) -} - -function evaluateRepositoryName( - node: AcornNode, - context: RepositoryExpressionContext, - visiting: Set, -): string | undefined { - const name = repositoryExpressionName(node) - if (name === '__dirname' || name === 'import.meta.dirname') { - return repositoryPathApi(context.file).dirname(context.file) - } - if (name === '__filename' || name === 'import.meta.url') { - return context.file - } - if (node.type !== 'Identifier') { - return undefined - } - if (!context.bindings.has(name)) { - return name === 'REPO_ROOT' ? context.root : undefined - } - if (visiting.has(name)) { - return undefined - } - const next = new Set(visiting) - next.add(name) - return evaluateRepositoryValue(context.bindings.get(name), context, next) -} - -function evaluateRepositoryTemplate( - node: AcornNode, - context: RepositoryExpressionContext, - visiting: Set, -): string | undefined { - const expressions = node['expressions'] as AcornNode[] - const quasis = node['quasis'] as Array<{ value: { cooked: string } }> - let value = quasis[0]?.value.cooked ?? '' - for (let index = 0; index < expressions.length; index += 1) { - const part = evaluateRepositoryValue(expressions[index], context, visiting) - if (part === undefined) { - return undefined - } - value += part + (quasis[index + 1]?.value.cooked ?? '') - } - return value -} - -function evaluateRepositoryUrl(args: string[]): string | undefined { - if (args[0]?.startsWith('file:')) { - try { - return fileURLToPath(args[0]) - } catch { - return undefined - } - } - if (!args[0] || !args[1] || /^[a-z]+:/iu.test(args[1])) { - return undefined - } - return resolveRepositoryPath( - repositoryPathApi(args[1]).dirname(args[1]), - args[0], - ) -} - -function evaluateRepositoryCall( - node: AcornNode, - context: RepositoryExpressionContext, - visiting: Set, -): string | undefined { - const callee = repositoryExpressionName(repositoryAstNode(node['callee'])) - if (node.type === 'CallExpression' && callee === 'process.cwd') { - return context.root - } - if (!isRepositoryPathExpression(node)) { - return undefined - } - const values = (node['arguments'] as AcornNode[]).map(argument => - evaluateRepositoryValue(argument, context, visiting), - ) - if (values.some(value => value === undefined)) { - return undefined - } - const args = values as string[] - if (node.type === 'NewExpression') { - return evaluateRepositoryUrl(args) - } - if (callee.endsWith('fileURLToPath')) { - return args[0]?.startsWith('file:') ? evaluateRepositoryUrl(args) : args[0] - } - const api = repositoryPathApi(context.root) - if (callee.endsWith('dirname')) { - return args[0] === undefined ? undefined : api.dirname(args[0]) - } - const normalized = args.map(argument => normalizePath(argument)) - return callee.endsWith('resolve') - ? api.resolve(context.root, ...normalized) - : api.join(...normalized) -} - -function evaluateRepositoryValue( - node: AcornNode | undefined, - context: RepositoryExpressionContext, - visiting: Set, -): string | undefined { - if (!node) { - return undefined - } - if (node.type === 'Literal' && typeof node['value'] === 'string') { - return node['value'] - } - if (node.type === 'TemplateLiteral') { - return evaluateRepositoryTemplate(node, context, visiting) - } - if (node.type === 'BinaryExpression' && node['operator'] === '+') { - const left = evaluateRepositoryValue( - repositoryAstNode(node['left']), - context, - visiting, - ) - const right = evaluateRepositoryValue( - repositoryAstNode(node['right']), - context, - visiting, - ) - return left === undefined || right === undefined ? undefined : left + right - } - return ( - evaluateRepositoryName(node, context, visiting) ?? - evaluateRepositoryCall(node, context, visiting) - ) -} - -export function evaluateRepositoryExpression( - node: AcornNode | undefined, - context: RepositoryExpressionContext, -): string | undefined { - return evaluateRepositoryValue(node, context, new Set()) -} diff --git a/.git-hooks/_shared/cross-repo.mts b/.git-hooks/_shared/cross-repo.mts deleted file mode 100644 index 6c298b25..00000000 --- a/.git-hooks/_shared/cross-repo.mts +++ /dev/null @@ -1,303 +0,0 @@ -import { repositorySourceRoot } from './repository-source-root.mts' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' -import { - offsetToLineCol, - tryParse, -} from '../../.claude/hooks/fleet/_shared/ast/core.mts' -import type { AcornNode } from '../../.claude/hooks/fleet/_shared/ast/core.mts' -import { - evaluateRepositoryExpression, - repositoryAstNode, - repositoryExpressionName, -} from './cross-repo-expressions.mts' -import type { RepositoryExpressionContext } from './cross-repo-expressions.mts' -import { - containsRepositoryPath, - findRepositoryRoot, - repositoryContainsTarget, - repositoryPathApi, - repositoryRealPath, - resolveRepositoryPath, -} from './repo-containment.mts' - -export interface CrossRepositoryHit { - lineNumber: number - line: string - matched: string -} - -export interface CrossRepositoryAnalysis { - hits: CrossRepositoryHit[] - unproven: number[] -} - -function repositoryPathLiteral(value: string): boolean { - const normalized = normalizePath(value) - return ( - value.startsWith('./') || - value.startsWith('.\\') || - // Recognize drive, relative, absolute, and local dependency prefixes. - /^(?:[a-z]:\/|\.\.?\/|\/|file:|link:)/iu.test(normalized) - ) -} - -function isRepositorySourceRead(name: string): boolean { - const separator = name.indexOf('.') - if ( - separator !== -1 && - !['fs', 'fsp', 'fsPromises', 'promises'].includes(name.slice(0, separator)) - ) { - return false - } - const qualifiedMethod = name.slice(separator + 1) - const method = qualifiedMethod.endsWith('Sync') - ? qualifiedMethod.slice(0, -4) - : qualifiedMethod - return ['createReadStream', 'readFile', 'readlink'].includes(method) -} - -function isKernelRuntimePath(value: string): boolean { - // Match the two virtual kernel roots and every path below them. - return /^\/(?:proc|sys)(?:\/|$)/u.test(normalizePath(value)) -} - -function repositoryAstChildren(node: AcornNode): AcornNode[] { - const children: AcornNode[] = [] - const values = Object.values(node) - for (let index = 0, { length } = values; index < length; index += 1) { - const value = values[index] - for (const candidate of Array.isArray(value) ? value : [value]) { - const child = repositoryAstNode(candidate) - if (child) { - children.push(child) - } - } - } - return children -} - -function collectRepositoryBindings( - node: AcornNode, - context: RepositoryExpressionContext, -): void { - if (node.type === 'VariableDeclarator') { - const id = repositoryAstNode(node['id']) - if (id?.type === 'Identifier') { - const name = String(id['name']) - context.bindings.set( - name, - context.bindings.has(name) - ? undefined - : repositoryAstNode(node['init']), - ) - } - } - if ( - node.type === 'AssignmentExpression' || - node.type === 'UpdateExpression' - ) { - const target = repositoryAstNode(node['left'] ?? node['argument']) - if (target?.type === 'Identifier') { - context.bindings.set(String(target['name']), undefined) - } - } - if (/^(?:ArrowFunction|Function)/u.test(node.type)) { - for (const parameter of (node['params'] ?? []) as AcornNode[]) { - if (parameter.type === 'Identifier') { - context.bindings.set(String(parameter['name']), undefined) - } - } - } - for (const child of repositoryAstChildren(node)) { - collectRepositoryBindings(child, context) - } -} - -function repositoryReferenceEscapes( - value: string, - base: string, - root: string, - physicalRoot: string, -): boolean { - const target = resolveRepositoryPath( - base, - value.replace(/^(?:file:|link:)/u, ''), - ) - const modules = resolveRepositoryPath(root, 'node_modules') - if (containsRepositoryPath(modules, target)) { - return false - } - if (base !== root) { - const resolvedPhysicalRoot = repositoryRealPath(physicalRoot) - const resolvedTarget = repositoryRealPath(target) - if ( - resolvedPhysicalRoot !== undefined && - resolvedTarget !== undefined && - containsRepositoryPath(resolvedPhysicalRoot, resolvedTarget) - ) { - return false - } - } - if (root === physicalRoot) { - return !repositoryContainsTarget(root, target) - } - const resolvedRoot = repositoryRealPath(root) - const resolvedTarget = repositoryRealPath(target) - return ( - resolvedRoot === undefined || - resolvedTarget === undefined || - !containsRepositoryPath(resolvedRoot, resolvedTarget) - ) -} - -export function analyzeCrossRepositoryPaths( - source: string, - fileAbsPath: string, - options?: { root?: string | undefined } | undefined, -): CrossRepositoryAnalysis { - const settings = { __proto__: null, ...options } - const file = resolveRepositoryPath( - settings.root ?? - findRepositoryRoot(fileURLToPath(import.meta.url)) ?? - path.dirname(fileURLToPath(import.meta.url)), - fileAbsPath, - ) - const physicalRoot = - settings.root ?? - findRepositoryRoot(file) ?? - repositoryPathApi(file).dirname(file) - const root = repositorySourceRoot(physicalRoot, file) - const context: RepositoryExpressionContext = { - root, - file, - bindings: new Map(), - } - const analysis: CrossRepositoryAnalysis = { hits: [], unproven: [] } - const lines = source.split(/\r?\n/u) - const seen = new Set() - const unproven = new Set() - function recordUnproven(start: number): void { - const line = offsetToLineCol(source, start).line - if (!unproven.has(line)) { - unproven.add(line) - analysis.unproven.push(line) - } - } - function record(value: string, start: number, base: string): void { - if ( - !repositoryPathLiteral(value) || - isKernelRuntimePath(value) || - !repositoryReferenceEscapes(value, base, root, physicalRoot) || - seen.has(start) - ) { - return - } - seen.add(start) - const lineNumber = offsetToLineCol(source, start).line - analysis.hits.push({ - lineNumber, - line: lines[lineNumber - 1] ?? '', - matched: value, - }) - } - function visitRepositorySink(node: AcornNode, base: string): boolean { - if (node.type.startsWith('Import') || node.type.startsWith('Export')) { - const target = repositoryAstNode(node['source']) - if (target) { - visitPathSink(target, base) - return true - } - } - if (node.type === 'CallExpression') { - const name = repositoryExpressionName(repositoryAstNode(node['callee'])) - if (name === 'require' || isRepositorySourceRead(name)) { - const argument = repositoryAstNode( - (node['arguments'] as unknown[] | undefined)?.[0], - ) - if (argument) { - visitPathSink(argument, name === 'require' ? base : root) - } - return true - } - } - return false - } - function visitPathSink(node: AcornNode, base: string): void { - const value = evaluateRepositoryExpression(node, context) - if (value === undefined) { - recordUnproven(node.start) - } else { - record(value, node.start, base) - } - } - function visit(node: AcornNode, base: string): void { - if (visitRepositorySink(node, base)) { - return - } - for (const child of repositoryAstChildren(node)) { - visit(child, base) - } - } - const ast = parseRepositorySource(source) - if (ast) { - collectRepositoryBindings(ast, context) - visit(ast, repositoryPathApi(file).dirname(file)) - } else if ( - // Dependency manifests carry quoted paths and file/link values. - /\.(?:json|toml|ya?ml)$/iu.test(file) || - path.basename(file) === '.gitmodules' - ) { - let offset = 0 - for ( - let lineIndex = 0, { length } = lines; - lineIndex < length; - lineIndex += 1 - ) { - const line = lines[lineIndex]! - if (!line.trimStart().startsWith('#')) { - for (const match of line.matchAll( - // Capture each local dependency value or quoted filesystem path. - /(?:file:|link:)(?:[/\\]|[a-z]:[/\\]|\.\.?[/\\])[^\s'",}]+|(?:["'])(?:[a-z]:[/\\](?![*?"<>|:])|\.\.?[/\\])[^"'\r\n]+["']/giu, - )) { - // Remove only the opening and closing quote delimiters. - const value = match[0].replace(/^["']|["']$/gu, '') - record( - value, - Buffer.byteLength(source.slice(0, offset + match.index)), - repositoryPathApi(file).dirname(file), - ) - } - } - offset += line.length + 1 - } - } else if (source.trim()) { - recordUnproven(0) - } - analysis.hits.sort((left, right) => left.lineNumber - right.lineNumber) - return analysis -} - -export function scanRepositoryReferences( - source: string, - file: string, -): CrossRepositoryHit[] { - return analyzeCrossRepositoryPaths(source, file).hits -} - -const repositoryAstCache = new Map>() -function parseRepositorySource(source: string): ReturnType { - if (repositoryAstCache.has(source)) { - return repositoryAstCache.get(source) - } - const ast = tryParse(source) - if (source.length <= 262_144) { - if (repositoryAstCache.size >= 32) { - repositoryAstCache.clear() - } - repositoryAstCache.set(source, ast) - } - return ast -} diff --git a/.git-hooks/_shared/external-issue-ref.mts b/.git-hooks/_shared/external-issue-ref.mts deleted file mode 100644 index c02dd20f..00000000 --- a/.git-hooks/_shared/external-issue-ref.mts +++ /dev/null @@ -1,108 +0,0 @@ -// External-issue-ref matcher — shared by the commit-msg git-stage backstop -// AND the Claude-side no-ext-issue-ref-guard. Gate-free (no Node-25 hard-exit, -// unlike helpers.mts) so the Claude hook can import it on the operator's Node. -// Flags foreign /# refs + github.com issue/PR URLs that -// would auto-link a backref into an upstream maintainer's issue. - -// CRLF-tolerant line split. Inlined (not imported from helpers.mts) so this -// module stays free of the Node-25 hard-exit gate that helpers.mts carries — -// a Claude hook importing it must run on the operator's, possibly older, Node. -function splitLines(text: string): string[] { - return text.replace(/\r\n/g, '\n').split(/\r?\n/) -} - -// -// Foreign `/#` tokens (and full github.com issue/PR -// URLs) auto-link on GitHub and post an `added N commits that reference -// this issue` event back to the target. A fleet cascade of N commits = -// N pings to a maintainer who never asked to be tagged. The canonical -// CLAUDE.md "public-surface hygiene" block documents the policy; this -// scanner makes it mechanical on the commit-msg git-stage so a foreign -// ref can't slip past `--no-verify` or onto a subprocess / worktree / -// CI commit the Bash-time no-ext-issue-ref-guard never sees. -// -// Canonical home: this module. The Claude-side -// .claude/hooks/fleet/no-ext-issue-ref-guard/index.mts imports the same -// matcher cross-tree so the two surfaces never diverge. -// -// Allowed (NOT reported): -// - bare `#123` (resolves against the current repo — no cross-repo leak) -// - `SocketDev/#` (same org — case-insensitive) -// - `https://github.com/SocketDev/...`, same org -// -// Blocked (reported): -// - `/#` -// - `https://github.com///{issues,pull}/` - -export interface ExternalIssueRef { - kind: 'token' | 'url' - owner: string - repo: string - num: string - raw: string -} - -// Org allowlist — case-insensitive, stored lowercase for comparison. -// GitHub resolves orgs case-insensitively in URLs and refs, so -// `socketdev` / `SocketDev` / `SOCKETDEV` all name the same org. -export const ALLOWED_ISSUE_REF_ORGS = new Set(['socketdev']) - -// Detect `/#` token. Owner and repo names follow -// GitHub's rules: alphanumerics, dashes, underscores, dots (no leading -// dot/dash). Permissive on boundaries since we pattern-match prose, not -// validate canonical refs. -// -// (^|\s|\() — anchor at start, whitespace, or open paren so we don't -// re-match the owner/repo fragment already inside a URL. -// ([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?) — owner / repo -// #(\d+) — issue/PR number -// (?=\b|[\s.,;:)\]]|$) — terminate cleanly -const OWNER_REPO_REF_RE = - /(?:^|\s|\()([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)#(\d+)(?=\b|[\s.,;:)\]]|$)/g - -// Detect full GitHub issue/PR URLs to non-SocketDev orgs. -const GITHUB_ISSUE_URL_RE = - /https?:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\/(?:issues|pull)\/(\d+)/g - -// Walk the text and collect every external-org reference. Returns an -// empty array when the text only references same-repo (`#123`) or -// SocketDev-owned (`SocketDev/socket-lib#42`) issues. Comment lines -// (after leading whitespace, start with `#`) are skipped — git inlines -// the diff snippet + "Please enter the commit message" hint there, and a -// foreign ref quoted in that snippet isn't part of the authored message. -export function scanExternalIssueRefs(text: string): ExternalIssueRef[] { - const out: ExternalIssueRef[] = [] - for (const rawLine of splitLines(text)) { - if (rawLine.trimStart().startsWith('#')) { - continue - } - let m: RegExpExecArray | null - OWNER_REPO_REF_RE.lastIndex = 0 - while ((m = OWNER_REPO_REF_RE.exec(rawLine)) !== null) { - const owner = m[1]! - const repo = m[2]! - const num = m[3]! - if (!ALLOWED_ISSUE_REF_ORGS.has(owner.toLowerCase())) { - out.push({ - kind: 'token', - owner, - repo, - num, - raw: `${owner}/${repo}#${num}`, - }) - } - } - GITHUB_ISSUE_URL_RE.lastIndex = 0 - while ((m = GITHUB_ISSUE_URL_RE.exec(rawLine)) !== null) { - const owner = m[1]! - const repo = m[2]! - const num = m[3]! - if (!ALLOWED_ISSUE_REF_ORGS.has(owner.toLowerCase())) { - out.push({ kind: 'url', owner, repo, num, raw: m[0]! }) - } - } - } - return out -} - -// ── File classification ──────────────────────────────────────────── diff --git a/.git-hooks/_shared/file-scan.mts b/.git-hooks/_shared/file-scan.mts deleted file mode 100644 index 613e2628..00000000 --- a/.git-hooks/_shared/file-scan.mts +++ /dev/null @@ -1,90 +0,0 @@ -// File classification + content reading for the git-hook scanners: which paths -// to skip, and how to read a file's text (binaries run through `strings`). -// Gate-free. - -import { existsSync, readFileSync, statSync } from 'node:fs' - -import { normalizePath } from '@socketsecurity/lib-stable/paths/normalize' -import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' - -// ── File classification ──────────────────────────────────────────── - -// Files we never scan: hooks themselves (both the .mts files and the -// shell shims under .git-hooks/), test fixtures, vendored lockfiles. -const SKIP_FILE_RE = - /\.(?:spec|test)\.(?:cts|m?[jt]s|mts|tsx?)$|\.example$|\/test\/|\/tests\/|fixtures\/|\.git-hooks\/|node_modules\/|pnpm-lock\.yaml/ - -export const shouldSkipFile = (filePath: string): boolean => - SKIP_FILE_RE.test(filePath) - -/** - * Source-code extensions. THE canonical definition for the fleet's source-only - * convention scanners — the commit-time hook and the - * `private-paths-are-absent-at-commit` check gate import this one constant so - * they cannot disagree about what "source code" means. - * - * Markdown, docs, JSON, and YAML are deliberately out of scope: they reference - * these patterns legitimately. That divergence was real — the check script - * carried a private copy of this regex under a comment claiming it was - * "lock-step with the hook", while the hook had no such constant and scanned - * every non-skipped file. Generated JSON therefore passed the gate and failed - * the hook, and the hook's suggested fix would have corrupted the data. - */ -export const SOURCE_FILE_RE = - /\.(?:[ch]|[cm]?[jt]sx?|bash|cc|cpp|cxx|go|hh|hpp|java|kt|py|rb|rs|sh|swift|zsh)$/ - -export const isSourceCodeFile = (filePath: string): boolean => - SOURCE_FILE_RE.test(filePath) - -/** - * The composed predicate for a SOURCE-ONLY convention scan: skip the universal - * exclusions, then skip anything that is not source code. - */ -export const shouldSkipSourceScan = (filePath: string): boolean => - shouldSkipFile(filePath) || !isSourceCodeFile(filePath) - -// Structured DATA payloads. A generated rule table, detector corpus, or -// fixture blob legitimately CONTAINS the very strings a convention scanner -// hunts for — a detection regex spelling `/Users/`, a rule description warning -// about `npx` — and applying the scanner's suggested fix would corrupt the -// data rather than repair a violation. -const DATA_FILE_RE = /\.(?:jsonc?|ya?ml)$/ -// The one exception: a package manifest is data, but an `npx` in its `scripts` -// really is the violation the rule exists to catch. -const PACKAGE_MANIFEST_RE = /(?:^|\/)package\.json$/ - -export const isStructuredDataFile = (filePath: string): boolean => { - const p = normalizePath(filePath) - return DATA_FILE_RE.test(p) && !PACKAGE_MANIFEST_RE.test(p) -} - -// Returns file content as a string. Text files stay in-process; binaries run -// through `strings` to catch paths embedded in WASM or compiled artifacts. -// A NUL byte is the stable cross-platform binary signal for the artifacts this -// hook scans, and avoids spawning Git-for-Windows `grep` for every text file. -export const readFileForScan = (filePath: string): string => { - if (!existsSync(filePath)) { - return '' - } - try { - if (statSync(filePath).isDirectory()) { - return '' - } - } catch { - return '' - } - let bytes: Buffer - try { - bytes = readFileSync(filePath) - } catch { - return '' - } - if (!bytes.includes(0)) { - return bytes.toString('utf8') - } - // NUL-bearing binary — extract printable strings. - const stringsResult = spawnSync('strings', [filePath], { - encoding: 'utf8', - }) - return stringsResult.stdout || '' -} diff --git a/.git-hooks/_shared/git-context-vars.mts b/.git-hooks/_shared/git-context-vars.mts deleted file mode 100644 index e00df058..00000000 --- a/.git-hooks/_shared/git-context-vars.mts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @file Git environment variables that override repository discovery. - */ -export const GIT_CONTEXT_VARS: readonly string[] = [ - 'GIT_ALTERNATE_OBJECT_DIRECTORIES', - 'GIT_CEILING_DIRECTORIES', - 'GIT_COMMON_DIR', - 'GIT_DIR', - 'GIT_INDEX_FILE', - 'GIT_NAMESPACE', - 'GIT_OBJECT_DIRECTORY', - 'GIT_PREFIX', - 'GIT_WORK_TREE', -] diff --git a/.git-hooks/_shared/git-identity.mts b/.git-hooks/_shared/git-identity.mts deleted file mode 100644 index 88bc9e7d..00000000 --- a/.git-hooks/_shared/git-identity.mts +++ /dev/null @@ -1,207 +0,0 @@ -// Git author/committer identity policy reader. The single source of truth is -// the wheelhouse-cascaded config FILE, resolved repo-scoped only (no machine- -// local fallback, by design — minimize outside-wheelhouse settings): -// -// .config/repo/socket-wheelhouse.json → "gitAuthors", per-repo, optional -// .config/fleet/git-authors.json, cascaded fleet default -// -// The per-repo half is a SECTION of the member settings file rather than its -// own JSON: per-repo config lives in ONE surface, and a second `.config/repo/ -// *.json` is the fragmentation `no-new-config-guard` refuses. -// -// Shape: { denylist: { emails[], names[] }, canonical: {name,email}, aliases[] }. -// -// Two checks, deliberately distinct: -// - isDeniedIdentity: a placeholder/sandbox identity (test@example.com, Test, -// empty) that is NEVER valid anywhere — the universal fleet denylist. -// - isAllowedAuthor: when an allowlist (canonical/aliases) is configured, the -// email must be in it. With no allowlist configured, only the denylist -// applies (so a repo without a .config/repo allowlist still blocks junk). -// -// This is the .git-hooks/ copy; .claude/hooks/fleet/_shared/git-identity.mts is -// a byte-equivalent copy for the other (separately-cascaded) hook tree — the -// shared thing is the config file, not cross-tree code. - -import { existsSync, readFileSync } from 'node:fs' -import path from 'node:path' - -export interface GitAuthor { - readonly name?: string | undefined - readonly email?: string | undefined -} - -export interface IdentityPolicy { - readonly denyEmails: readonly string[] - readonly denyNames: readonly string[] - readonly canonical: GitAuthor - readonly aliases: readonly GitAuthor[] -} - -interface RawConfig { - denylist?: - | { emails?: string[] | undefined; names?: string[] | undefined } - | undefined - canonical?: GitAuthor | undefined - aliases?: GitAuthor[] | undefined -} - -// The per-repo half lives as a SECTION of the one member settings file, not -// as its own `.config/repo/*.json`. Per-repo config lives in ONE surface -// (`config-segregation`); a second file there is exactly the fragmentation -// `no-new-config-guard` exists to refuse. -const REPO_SETTINGS = '.config/repo/socket-wheelhouse.json' -const REPO_SETTINGS_KEY = 'gitAuthors' -const FLEET_CONFIG = '.config/fleet/git-authors.json' - -function loadJson(file: string): RawConfig | undefined { - if (!existsSync(file)) { - return undefined - } - try { - return JSON.parse(readFileSync(file, 'utf8')) as RawConfig - } catch { - return undefined - } -} - -/** - * The `gitAuthors` section of the member settings file, or undefined when the - * file or the section is absent. - */ -function loadRepoSection(repoRoot: string): RawConfig | undefined { - const settings = loadJson(path.join(repoRoot, REPO_SETTINGS)) as - | { [REPO_SETTINGS_KEY]?: RawConfig | undefined } - | undefined - return settings?.[REPO_SETTINGS_KEY] -} - -/** - * Split a `git var GIT_AUTHOR_IDENT` / `GIT_COMMITTER_IDENT` string - * (`Name `) into its name and email. A field git left - * empty comes back as undefined rather than an empty string, so the denylist - * and allowlist checks see the same "unset" shape either way. - */ -export function parseGitIdentLine(ident: string): GitAuthor { - const match = /^(.*?)\s*<([^>]*)>/.exec(ident) - return { - name: match?.[1]?.trim() || undefined, - email: match?.[2]?.trim() || undefined, - } -} - -/** - * One config's denylist halves, each defaulted to empty. - */ -function readDenyEntries(config: RawConfig | undefined): { - emails: string[] - names: string[] -} { - return { - emails: config?.denylist?.emails ?? [], - names: config?.denylist?.names ?? [], - } -} - -/** - * The union of both denylists, lowercased. A repo can ADD denied identities; - * the fleet denylist always applies, so neither side can shorten the other. - */ -function mergeIdentityDenyList( - fleet: RawConfig | undefined, - repo: RawConfig | undefined, -): { denyEmails: string[]; denyNames: string[] } { - const fleetDeny = readDenyEntries(fleet) - const repoDeny = readDenyEntries(repo) - return { - denyEmails: [...fleetDeny.emails, ...repoDeny.emails].map(e => - e.toLowerCase(), - ), - denyNames: [...fleetDeny.names, ...repoDeny.names].map(n => - n.toLowerCase(), - ), - } -} - -/** - * The allowlist half, taken whole from the first config that declares one: - * the repo override when it names a canonical email or any alias, else the - * cascaded fleet default. - */ -function selectIdentityAllowlist( - fleet: RawConfig | undefined, - repo: RawConfig | undefined, -): { canonical: GitAuthor; aliases: GitAuthor[] } { - const repoHasAllow = !!repo?.canonical?.email || !!repo?.aliases?.length - const src = repoHasAllow ? repo! : (fleet ?? {}) - return { - aliases: Array.isArray(src.aliases) ? src.aliases : [], - canonical: src.canonical ?? {}, - } -} - -/** - * Resolve the identity policy: a repo override (.config/repo) takes precedence - * over the cascaded fleet default (.config/fleet). The denylist merges both (a - * repo can ADD denied identities but the fleet denylist always applies); the - * allowlist is taken from the first config that declares a non-empty one. - * `repoRoot` is the directory both config paths resolve against. - */ -export function readIdentityPolicy(repoRoot: string): IdentityPolicy { - const fleet = loadJson(path.join(repoRoot, FLEET_CONFIG)) - const repo = loadRepoSection(repoRoot) - const { denyEmails, denyNames } = mergeIdentityDenyList(fleet, repo) - const { aliases, canonical } = selectIdentityAllowlist(fleet, repo) - return { denyEmails, denyNames, canonical, aliases } -} - -/** - * True when an identity is on the universal denylist — a placeholder email - * (exact, or a `*@domain` whole-domain wildcard) or a placeholder name. - */ -export function isDeniedIdentity( - candidate: GitAuthor, - policy: IdentityPolicy, -): boolean { - const email = candidate.email?.toLowerCase() ?? '' - const name = candidate.name?.toLowerCase() ?? '' - for (let i = 0, { length } = policy.denyEmails; i < length; i += 1) { - const pat = policy.denyEmails[i]! - if (pat.startsWith('*@')) { - if (email.endsWith(pat.slice(1))) { - return true - } - } else if (email === pat) { - return true - } - } - return !!name && policy.denyNames.includes(name) -} - -/** - * True when `candidate`'s email is the canonical identity or an alias. When no - * allowlist is configured (empty canonical + aliases), returns true — only the - * denylist gates that repo. A candidate with no email is treated as allowed - * git fails on its own when no identity is set. - */ -export function isAllowedAuthor( - candidate: GitAuthor, - policy: IdentityPolicy, -): boolean { - const email = candidate.email?.toLowerCase() - if (!email) { - return true - } - const hasAllowlist = !!policy.canonical.email || policy.aliases.length > 0 - if (!hasAllowlist) { - return true - } - if (policy.canonical.email?.toLowerCase() === email) { - return true - } - for (let i = 0, { length } = policy.aliases; i < length; i += 1) { - if (policy.aliases[i]!.email?.toLowerCase() === email) { - return true - } - } - return false -} diff --git a/.git-hooks/_shared/git.mts b/.git-hooks/_shared/git.mts deleted file mode 100644 index 6bc847c1..00000000 --- a/.git-hooks/_shared/git.mts +++ /dev/null @@ -1,66 +0,0 @@ -// Git subprocess wrappers for the hooks. Gate-free. -// -// Two flavors: -// -// git(...) — loose. Returns '' on failure. Used by callers that -// legitimately tolerate a missing ref (e.g. probing -// remote default-branch HEAD which may not be set up -// locally) and provide their own fallback. Silent -// by design — _shared/util.mts can't import the canonical -// logger because it runs before the Node-version -// gate has cleared, and a fire-and-forget dynamic -// import races process exit. Callers that need to -// know about failure should use gitOrThrow(). -// -// gitOrThrow(...) — strict. Throws on either spawn error (git not on -// PATH, EAGAIN, …) or non-zero exit. Used by gitLines -// and every security-gate caller in pre-commit / -// pre-push: if `git diff --cached --name-only` fails -// we MUST refuse to greenlight the commit, not pass -// it with "no files to check." -// -// gitLines goes through gitOrThrow because every call site we have -// staged-file iteration, push-range walking, repo-toplevel lookup -// makes a security or correctness decision based on the result; an -// empty array from a failed git invocation is a fail-open. - -import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' - -import { splitLines } from './scan-core.mts' - -export const git = (...args: string[]): string => { - const result = spawnSync('git', args, { encoding: 'utf8' }) - return (result.stdout ?? '').trim() -} - -export const gitOrThrow = (...args: string[]): string => { - const result = spawnSync('git', args, { encoding: 'utf8' }) - if (result.error) { - throw new Error(`git ${args.join(' ')}: ${result.error.message}`) - } - if (typeof result.status !== 'number' || result.status !== 0) { - const err = result.stderr?.trim() || `exit ${result.status}` - throw new Error(`git ${args.join(' ')}: ${err}`) - } - return (result.stdout ?? '').trim() -} - -export const gitLines = (...args: string[]): string[] => { - const out = gitOrThrow(...args) - return out ? splitLines(out) : [] -} - -/** - * Run git with `stdin` piped in, returning the raw status and stdout. - * - * Unlike `git`/`gitOrThrow`, this surfaces the exit status instead of - * swallowing it or throwing — a query like `git check-ignore` uses its status - * as the ANSWER (0 = matched, 1 = no match), not as a failure. - */ -export function gitWithInput( - args: readonly string[], - stdin: string, -): { status: number | undefined; stdout: string } { - const result = spawnSync('git', [...args], { encoding: 'utf8', input: stdin }) - return { status: result.status ?? undefined, stdout: result.stdout ?? '' } -} diff --git a/.git-hooks/_shared/isolate-git-env.mts b/.git-hooks/_shared/isolate-git-env.mts deleted file mode 100644 index bb71cba5..00000000 --- a/.git-hooks/_shared/isolate-git-env.mts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * @file Neutralize the inherited git environment so a test's `git` spawns can - * never touch the live repo. Importing this module runs the SAFE default - * strip discovery vars, as a side effect; call `isolateGitEnv({ … })` for - * the stronger variant. Why this is load-bearing: when a suite runs from the - * pre-commit / pre-push hook, or just inherits the ambient env, git exports - * `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE` pointing at THE LIVE repo, - * and git honors those above cwd-based discovery. A fixture that does `git - * init` + `git config user.email …` in a `cwd: tmpDir` then escapes onto the - * real `.git/config` and HEAD — observed damage: `core.bare=true` (breaks - * every worktree op with "must be run in a work tree"), a junk - * `test@example.com` identity, and stray commits on the working branch. Two - * consumers: - * - * - `node --test` git-fixture suites (`.git-hooks/fleet/test/*`, etc.) do NOT - * load the vitest setup, so each imports this module — the side-effect - * default (strip-only) stops the escape while leaving each fixture free to - * scope its own `GIT_CONFIG_GLOBAL` per-spawn (the signing-gate tests need - * that). `no-unisolated-git-fixture-guard` recognizes the import. - * - vitest, via `setupFiles` (`test/fleet/scripts/setup.mts`), calls - * `isolateGitEnv({ pinConfigToNull: true })` for the stronger form (no - * fixture there manipulates a controlled global config). Lives in - * `.git-hooks/_shared/` (alongside `git-identity.mts`) so the git-hook test - * tree imports it within-tree; the vitest setup reaches it cross-tree (both - * cascade together). - */ - -import process from 'node:process' -import { GIT_CONTEXT_VARS } from './git-context-vars.mts' - -export interface IsolateGitEnvOptions { - /** - * Also pin `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` to `/dev/null` so `git - * config` (without `--local`) can't reach a real config file at all. - * Stronger, but it OVERRIDES any per-spawn global a fixture sets — only use - * it where no fixture manipulates a controlled global config (vitest). The - * strip alone already prevents escape; this is belt-and-suspenders. - */ - pinConfigToNull?: boolean | undefined -} - -/** - * Strip the inherited git context vars (always). Optionally pin the config - * files to `/dev/null`. Idempotent — safe to call or import more than once. - */ -export function isolateGitEnv(options: IsolateGitEnvOptions = {}): void { - for (let i = 0, { length } = GIT_CONTEXT_VARS; i < length; i += 1) { - delete process.env[GIT_CONTEXT_VARS[i]!] - } - if (options.pinConfigToNull) { - process.env['GIT_CONFIG_GLOBAL'] = '/dev/null' - process.env['GIT_CONFIG_SYSTEM'] = '/dev/null' - } -} - -// Side-effect default for the bare `import '…/isolate-git-env.mts'` form: the -// safe strip-only isolation. Consumers wanting the stronger pin call -// isolateGitEnv({ pinConfigToNull: true }) explicitly. -isolateGitEnv() diff --git a/.git-hooks/_shared/logger-leaks.mts b/.git-hooks/_shared/logger-leaks.mts deleted file mode 100644 index 9c8b6e80..00000000 --- a/.git-hooks/_shared/logger-leaks.mts +++ /dev/null @@ -1,189 +0,0 @@ -// Logger-leak matcher — shared by the commit-time scanLoggerLeaks -// (.git-hooks/_shared/util.mts) and the edit-time logger-guard -// (.claude/hooks/fleet/). Both flag direct `console.*` / `process.std*.write` -// calls (the fleet rule: use getDefaultLogger()). Previously the commit side -// used a REGEX and the edit side an AST walk — divergent engines that could -// disagree (a regex flags `console.log` inside a string literal or comment; -// the AST walk does not). This is the single AST-based source so they agree. -// -// AST (acorn-wasm) not regex: the parser is loaded for other commit-time -// checks anyway, and it eliminates the string/comment false positives the -// regex had. Gate-free: imports only the vendored acorn (no Node-25 exit), so -// either hook tree can use it. - -import { findMemberCalls } from '../../.claude/hooks/fleet/_shared/ast/calls.mts' - -export interface LoggerLeak { - // 1-based line of the call. - line: number - // Source text of the line, trimmed by the caller as needed. - text: string - // The dotted call, e.g. `console.log` / `process.stderr.write`. - fullCall: string - // Canonical logger replacement, e.g. `logger.info`. - replacement: string -} - -// The forbidden direct-output calls and their canonical logger replacement. -// Two-segment (`console.log`) and three-segment (`process.stderr.write`) -// chains — findMemberCalls handles both via a dotted `object`. -export const FORBIDDEN_LOGGER_CALLS: ReadonlyArray<{ - object: string - property: string - replacement: string -}> = [ - { object: 'console', property: 'debug', replacement: 'logger.debug' }, - { object: 'console', property: 'error', replacement: 'logger.error' }, - { object: 'console', property: 'info', replacement: 'logger.info' }, - { object: 'console', property: 'log', replacement: 'logger.info' }, - { object: 'console', property: 'warn', replacement: 'logger.warn' }, - { object: 'process.stderr', property: 'write', replacement: 'logger.error' }, - { object: 'process.stdout', property: 'write', replacement: 'logger.info' }, -] - -// Find every direct logger-leak call in `source` via the AST. Returns one entry -// per call site with its line, the dotted call, and the canonical replacement. -// Per-line `// oxlint-disable-next-line socket/no-console-prefer-logger` suppression is the CALLER's job -// each tree applies its own marker semantics. -export function findLoggerLeaks(source: string): LoggerLeak[] { - const leaks: LoggerLeak[] = [] - for (let i = 0, { length } = FORBIDDEN_LOGGER_CALLS; i < length; i += 1) { - const spec = FORBIDDEN_LOGGER_CALLS[i]! - const matches = findMemberCalls(source, spec.object, spec.property) - for (let j = 0, mlen = matches.length; j < mlen; j += 1) { - const m = matches[j]! - leaks.push({ - line: m.line, - text: m.text, - fullCall: `${spec.object}.${spec.property}`, - replacement: spec.replacement, - }) - } - } - return leaks -} - -export interface LoggerDecoration { - // 1-based line of the call. - line: number - // Source text of the line. - text: string - // The logger method called (e.g. `error`, `fail`, `log`). - method: string - // What kind of hand-rolled decoration leads the first argument. - kind: 'glyph' | 'indent' | 'bullet' - // For 'glyph': the leading glyph + the logger method that OWNS it, the fix. - glyph: string | undefined - ownerMethod: string | undefined -} - -// Logger methods whose first string/template arg we inspect for a hand-rolled -// prefix. Excludes structural methods (group/groupEnd/dir/table). -const DECORATION_SCAN_METHODS = [ - 'error', - 'fail', - 'info', - 'log', - 'progress', - 'skip', - 'step', - 'substep', - 'success', - 'warn', -] - -// Status glyph → the logger method that OWNS it. The method renders the glyph; -// hand-writing it double-marks, and skips theme-aware color. Keep in lockstep -// with @socketsecurity/lib-stable/logger symbols-builder + the no-status-emoji rule. -const GLYPH_OWNER: Readonly> = { - '‼': 'warn', - '×': 'fail', - '√': 'success', - '☑': 'success', - '⚠': 'warn', - '⛔': 'warn', - '✅': 'success', - '✓': 'success', - '✔': 'success', - '✖': 'fail', - '✗': 'fail', - '✘': 'fail', - '❌': 'fail', - '❎': 'fail', - '❕': 'warn', - '❗': 'warn', - '🚨': 'warn', - ℹ: 'info', -} - -// Leading status glyph, after optional whitespace. One named capture, consumed -// below to look up the owning method. Built dynamically from GLYPH_OWNER so the -// table is the single source. -const GLYPH_LEAD_RE = new RegExp( - `^\\s*(?${Object.keys(GLYPH_OWNER) - .map(g => g.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - .join('|')})`, -) -// Leading indentation: a tab or 2+ spaces. The logger owns indentation via -// group()/substep(); a hand-rolled indent fights it. -const INDENT_LEAD_RE = /^(?: {2,}|\t)/ -// Leading bullet glyph — use logger.substep() for indented sub-items. -const BULLET_LEAD_RE = /^\s*[•‣◦·]\s/ - -// Find every `logger.(...)` call whose first argument's leading static -// text is hand-rolled decoration the logger/group already owns — a status -// glyph, leading indentation, or a bullet. Works for string literals AND -// template literals (`\` ✗ ${x}\``). Per-line suppression is the CALLER's job. -export function findLoggerDecoration(source: string): LoggerDecoration[] { - const out: LoggerDecoration[] = [] - for (let i = 0, { length } = DECORATION_SCAN_METHODS; i < length; i += 1) { - const method = DECORATION_SCAN_METHODS[i]! - const calls = findMemberCalls(source, 'logger', method) - for (let j = 0, clen = calls.length; j < clen; j += 1) { - const call = calls[j]! - const lead = call.firstArgLeadingText - if (typeof lead !== 'string' || lead.length === 0) { - continue - } - const glyphMatch = GLYPH_LEAD_RE.exec(lead) - const glyph = glyphMatch?.groups?.['glyph'] - if (glyph) { - out.push({ - __proto__: null, - line: call.line, - text: call.text, - method, - kind: 'glyph', - glyph, - ownerMethod: GLYPH_OWNER[glyph], - } as LoggerDecoration) - continue - } - if (BULLET_LEAD_RE.test(lead)) { - out.push({ - __proto__: null, - line: call.line, - text: call.text, - method, - kind: 'bullet', - glyph: undefined, - ownerMethod: undefined, - } as LoggerDecoration) - continue - } - if (INDENT_LEAD_RE.test(lead)) { - out.push({ - __proto__: null, - line: call.line, - text: call.text, - method, - kind: 'indent', - glyph: undefined, - ownerMethod: undefined, - } as LoggerDecoration) - } - } - } - out.sort((a, b) => a.line - b.line) - return out -} diff --git a/.git-hooks/_shared/node-version.mts b/.git-hooks/_shared/node-version.mts deleted file mode 100644 index 5ec40c3f..00000000 --- a/.git-hooks/_shared/node-version.mts +++ /dev/null @@ -1,24 +0,0 @@ -import process from 'node:process' - -// Hard-fail if Node is below 24. This runs at module load — every hook -// invocation imports this module before doing anything. -const NODE_MIN_MAJOR = 24 -const nodeMajor = Number.parseInt( - process.versions.node.split('.')[0] || '0', - 10, -) -if (nodeMajor < NODE_MIN_MAJOR) { - // This import-light shared helper does not own a logger. Use raw - // process.stderr with ASCII (no status-emoji glyph) so the no-status-emoji - // lint rule stays clean — the recommendation to use logger.fail() does not - // apply when the entire branch is the logger-unavailable bail. - // oxlint-disable-next-line socket/no-module-eval-side-effects -- floor bail - process.stderr.write( - `\x1b[0;31mHook requires Node >= ${NODE_MIN_MAJOR}.0.0 (have v${process.versions.node})\x1b[0m\n`, - ) - // oxlint-disable-next-line socket/no-module-eval-side-effects -- floor bail - process.stderr.write( - 'Install Node 24+ — these hooks rely on default-on .mts type stripping.\n', - ) - process.exit(1) -} diff --git a/.git-hooks/_shared/personal-path.mts b/.git-hooks/_shared/personal-path.mts deleted file mode 100644 index 37d39b19..00000000 --- a/.git-hooks/_shared/personal-path.mts +++ /dev/null @@ -1,57 +0,0 @@ -// Personal-path leak matcher — shared by the commit-time scanPersonalPaths -// (.git-hooks/_shared/util.mts) and the edit-time personal-path-guard -// (.claude/hooks/fleet/). Both surfaces import THESE regexes + helpers so the -// two can't drift, which lock-step inline copies cannot promise. Gate-free -// (no Node-25 hard-exit like helpers.mts) so the Claude hook can import it on -// the operator's possibly-older Node. -// -// Flags a hardcoded USERNAME leak: /Users//, /home//, -// C:\Users\\. Username-free forms (`~/`, `$HOME/`) are the OPPOSITE — the -// recommended shapes — and are NOT flagged. Pure-placeholder lines -// (/Users//, $USER) are documentation, not leaks. - -// Real personal paths to flag. NFKC-normalize the line before matching (the -// caller does this) so full-width / ligature variants of `/Users` don't slip -// past the ASCII-only class. -export const PERSONAL_PATH_RE = - /(?:\/Users\/[^/\s]+\/|\/home\/[^/\s]+\/|C:\\Users\\[^\\]+\\)/ - -// Placeholder forms we ALLOW, documentation, not leaks: `<...>` components and -// `$VAR` / `${VAR}` under the platform user dir. Canonical fleet style: -// /Users//... /home//... C:\Users\\... -export const PERSONAL_PATH_PLACEHOLDER_RE = - /(?:\/Users\/<[^>]*>\/|\/home\/<[^>]*>\/|C:\\Users\\<[^>]*>\\|\/Users\/\$\{?[A-Z_]+\}?\/|\/home\/\$\{?[A-Z_]+\}?\/)/ - -// Well-known CI / system home dirs whose "username" is a service account, not a -// person, so the GitHub Actions and Ubuntu CI homes below are not personal -// leaks. gh-aw's compiled `.lock.yml` emits tool-cache mounts under the runner -// home; those are correct, not a leak. Matched as the path's username segment -// only. -// oxlint-disable-next-line socket/personal-path-placeholders -- `runner` is CI -export const KNOWN_NON_PERSONAL_PATH_RE = - /(?:\/Users\/(?:runner)\/|\/home\/(?:circleci|runner|ubuntu|vscode|vsts)\/)/ - -// True when a line is a PURE placeholder: it matches the placeholder shape AND -// nothing real remains after stripping every placeholder. Such lines are -// documentation, so the scanners skip them. A line whose only "personal" paths -// are well-known CI/system homes (e.g. `/home//` for a CI service -// account) is also pure, because those usernames are not people. -export function isPurePlaceholder(line: string): boolean { - const hasPlaceholder = PERSONAL_PATH_PLACEHOLDER_RE.test(line) - const hasCiHome = KNOWN_NON_PERSONAL_PATH_RE.test(line) - if (!hasPlaceholder && !hasCiHome) { - return false - } - let stripped = line.replace(new RegExp(PERSONAL_PATH_PLACEHOLDER_RE, 'g'), '') - stripped = stripped.replace(new RegExp(KNOWN_NON_PERSONAL_PATH_RE, 'g'), '') - return !PERSONAL_PATH_RE.test(stripped) -} - -// Rewrite the real personal paths on a line into the canonical placeholders, so -// both surfaces print the same fix recipe. -export function suggestPlaceholder(line: string): string { - return line - .replace(/\/Users\/[^/\s]+\//g, '/Users//') - .replace(/\/home\/[^/\s]+\//g, '/home//') - .replace(/C:\\Users\\[^\\]+\\/g, 'C:\\Users\\\\') -} diff --git a/.git-hooks/_shared/pkg-script-target.mts b/.git-hooks/_shared/pkg-script-target.mts deleted file mode 100644 index a56514eb..00000000 --- a/.git-hooks/_shared/pkg-script-target.mts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file Resolve a package.json script to the node script it delegates to, so - * the pre-commit hook can run that script directly instead of through `pnpm - * run`. `pnpm` on PATH is the Socket Firewall shim: it boots the sfw proxy, - * which boots pnpm, which re-resolves the workspace — seconds of startup - * before the script's first line, many times the work a staged-scope gate - * does, and the same wrapper whose deadlock the step budget exists to - * survive. Prints the script path when the body is exactly `node ` — - * that command runs identically under the repo-pinned node - * `_shared/resolve-node.sh` already put on PATH. Prints nothing for any other - * body (a `bun test`, a body carrying extra flags, a shell pipeline), so - * those keep the wrapper: the hook must run what `pnpm run