diff --git a/.cspell.json b/.cspell.json index 73b0711d..5880e66b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -16,6 +16,7 @@ ".devcontainer/**", ".gitignore", ".gitattributes", + ".vale/styles/Vocab/**/reject.txt", "*.meta", "Samples~/**", "package-lock.json", @@ -37,6 +38,16 @@ "words": [ "DxMessaging", "dxmessaging", + "mtimes", + "nofilter", + "relitigate", + "DDOL", + "Reemit", + "reemit", + "unsub", + "unwaived", + "vstest", + "parameterizes", "wallstop", "DXMSG", "Untargeted", diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..39ba5182 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.5.7", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4", + "integrity": "sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 63f4ffe6..52a9a057 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,9 +14,10 @@ }, "containerEnv": { "DOTNET_CLI_TELEMETRY_OPTOUT": "1", - "DOTNET_NOLOGO": "1" + "DOTNET_NOLOGO": "1", + "NPM_CONFIG_PREFIX": "/home/vscode/.local" }, - "postCreateCommand": "(dotnet tool restore || true) && (npm install || true) && git config --global --add safe.directory ${containerWorkspaceFolder} && (tldr --update || true) && (pre-commit install --install-hooks || true)", + "postCreateCommand": "bash .devcontainer/post-create.sh", "customizations": { "vscode": { "extensions": [ @@ -27,6 +28,7 @@ "GitHub.copilot", "GitHub.copilot-chat", "anthropic.claude-code", + "openai.chatgpt", "streetsidesoftware.code-spell-checker", "DavidAnson.vscode-markdownlint", "yzhang.markdown-all-in-one" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..29f93239 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Post-create bootstrap for the DxMessaging devcontainer. + +set -euo pipefail + +BLUE='\033[0;34m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[OK]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +fail() { + echo -e "${RED}[ERROR]${NC} $1" >&2 + exit 1 +} + +run_optional() { + local label="$1" + shift + + log_info "$label" + if "$@"; then + log_success "$label completed" + else + log_warn "$label failed (continuing)" + fi +} + +ensure_path_line() { + local rc_file="$1" + local path_line='export PATH="$HOME/.local/bin:$PATH"' + + if [[ ! -f "$rc_file" ]]; then + return + fi + + if ! grep -Fqx "$path_line" "$rc_file"; then + { + echo "" + echo "# Ensure npm user-global binaries are available" + echo "$path_line" + } >> "$rc_file" + fi +} + +trap 'fail "post-create setup failed at line $LINENO"' ERR + +log_info "Starting post-create setup" + +mkdir -p "$HOME/.local/bin" + +log_info "Configuring npm global prefix for non-root installs" +npm config set prefix "$HOME/.local" + +current_prefix="$(npm config get prefix)" +if [[ "$current_prefix" != "$HOME/.local" ]]; then + fail "npm prefix is '$current_prefix', expected '$HOME/.local'" +fi +log_success "npm prefix configured: $current_prefix" + +# Make codex immediately available in this session, and persist for future shells. +export PATH="$HOME/.local/bin:$PATH" +ensure_path_line "$HOME/.bashrc" +ensure_path_line "$HOME/.zshrc" + +workspace_dir="${containerWorkspaceFolder:-$PWD}" + +run_optional "Restoring .NET local tools" dotnet tool restore +run_optional "Installing workspace npm dependencies" npm install +run_optional "Configuring git safe.directory" git config --global --add safe.directory "$workspace_dir" +run_optional "Updating tldr cache" tldr --update +run_optional "Installing pre-commit hooks" pre-commit install --install-hooks + +log_info "Installing Codex CLI" +npm install -g --prefix "$HOME/.local" @openai/codex@latest + +if ! command -v codex >/dev/null 2>&1; then + fail "Codex CLI was installed but is not on PATH" +fi + +codex_version="$(codex --version 2>/dev/null || true)" +if [[ -z "$codex_version" ]]; then + fail "Codex CLI did not return a version" +fi + +log_success "Codex ready: $codex_version" +log_success "Post-create setup finished" diff --git a/.devcontainer/verify-tools.sh b/.devcontainer/verify-tools.sh index 15ace91c..7c6ff7d8 100644 --- a/.devcontainer/verify-tools.sh +++ b/.devcontainer/verify-tools.sh @@ -151,6 +151,22 @@ echo "" echo -e "${BLUE}=== .NET Tools ===${NC}" check_tool "csharpier" "csharpier" "--version" +echo "" +echo -e "${BLUE}=== Node.js Global Tools ===${NC}" +check_tool "codex" "codex" "--version" + +echo "" +echo -e "${BLUE}=== npm Configuration ===${NC}" +printf "%-20s" "npm prefix" +npm_prefix=$(npm config get prefix 2>/dev/null || echo "error") +if [ "$npm_prefix" = "$HOME/.local" ]; then + echo -e "${GREEN}✓${NC} $npm_prefix" + ((PASS++)) +else + echo -e "${RED}✗${NC} $npm_prefix (expected $HOME/.local)" + ((FAIL++)) +fi + echo "" echo -e "${BLUE}=== Moreutils ===${NC}" check_tool_exists "sponge" "sponge" diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml index 7604e989..122f38ea 100644 --- a/.github/workflows/docs-lint.yml +++ b/.github/workflows/docs-lint.yml @@ -7,6 +7,7 @@ on: - "**/*.cs" - "scripts/validate-docs-ascii.js" - "scripts/validate-doc-code-patterns.js" + - "scripts/validate-docs-prose.js" push: branches: - main @@ -16,6 +17,7 @@ on: - "**/*.cs" - "scripts/validate-docs-ascii.js" - "scripts/validate-doc-code-patterns.js" + - "scripts/validate-docs-prose.js" workflow_dispatch: concurrency: @@ -65,3 +67,23 @@ jobs: - name: Run validate-doc-code-patterns run: node scripts/validate-doc-code-patterns.js + + validate-docs-prose: + name: Validate human-prose policy + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package.json + + - name: Run validate-docs-prose + run: node scripts/validate-docs-prose.js diff --git a/.github/workflows/hook-perf-measurement.yml b/.github/workflows/hook-perf-measurement.yml new file mode 100644 index 00000000..d6cbaf68 --- /dev/null +++ b/.github/workflows/hook-perf-measurement.yml @@ -0,0 +1,89 @@ +name: Hook Performance Measurement + +on: + pull_request: + paths: + - ".pre-commit-config.yaml" + - "scripts/**.js" + - "scripts/measure-hook-wallclock.js" + - ".github/workflows/hook-perf-measurement.yml" + schedule: + - cron: "13 6 * * 1" # Monday 06:13 UTC; nightly was overkill for a perf gate. + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + measure: + name: Measure git hook wall-clock + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: package.json + + - name: Setup Python (for pre-commit) + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "8.0.x" + + - name: Install npm dependencies + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm i --no-audit --no-fund + fi + + - name: Install pre-commit + run: pip install pre-commit==4.6.0 + + - name: Configure git user (pre-commit needs an author) + run: | + git config user.name "perf-measurement-bot" + git config user.email "perf-measurement-bot@users.noreply.github.com" + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Diagnose .NET tool availability + run: | + dotnet --info + dotnet tool list + + - name: Install pre-commit hooks + run: pre-commit install --install-hooks + + - name: Warm up the hook caches (first run is always cold) + run: | + pre-commit run --hook-stage pre-commit \ + --files Runtime/Core/MessageBus/MessageBus.cs >/dev/null 2>&1 || true + pre-commit run --hook-stage pre-commit \ + --files .llm/skills/performance/git-hook-performance.md >/dev/null 2>&1 || true + + - name: Measure wall-clock + run: node scripts/measure-hook-wallclock.js + + - name: Emit JSON for downstream tooling + if: always() + run: node scripts/measure-hook-wallclock.js --json diff --git a/.github/workflows/pre-commit-tooling-check.yml b/.github/workflows/pre-commit-tooling-check.yml index 219be04f..8fa947e8 100644 --- a/.github/workflows/pre-commit-tooling-check.yml +++ b/.github/workflows/pre-commit-tooling-check.yml @@ -129,7 +129,7 @@ jobs: run: pre-commit run validate-changelog-policy --all-files - name: Run parser script tests hook - run: pre-commit run script-parser-tests --all-files + run: pre-commit run --hook-stage pre-push script-parser-tests --all-files - name: Diagnose managed Jest fallback environment shell: bash diff --git a/.llm/context.md b/.llm/context.md index 3cb565d3..f75a5583 100644 --- a/.llm/context.md +++ b/.llm/context.md @@ -29,7 +29,9 @@ This file is intentionally concise. It contains only critical, high-signal guida - For user-visible code edits (`Runtime/`, `Samples~/`, user-facing `Editor/`, or shipped `SourceGenerators/` code), run `npm run validate:changelog:coverage` before finishing and resolve any `W002` warnings by rewriting entries around user impact. - When editing `.cs`, `.md`, `.json`, `.yml`, `.yaml`, `.ps1`, or `.js` files, run file-scoped cspell on touched files and update `.cspell.json` in the same change for legitimate domain terms. - For Node child-process calls in `scripts/*.js`, prefer argument-array invocations (`spawnSync` / `execFileSync`) and `stdio` options instead of shell redirection. +- For dynamic `import()` in `scripts/*.js`, convert filesystem paths with `pathToFileURL(...).href` before importing (raw Windows drive-letter paths fail Node's ESM loader). - When editing `.pre-commit-config.yaml`, `scripts/*` hook tooling, `.github/workflows/*.yml`, or hook-related scripts in `package.json`, run `npm run preflight:pre-commit` before finishing. +- When editing `.pre-commit-config.yaml` or hook scripts, the new performance budget test (`scripts/__tests__/hook-perf-budget.test.js`) must pass; see [Git Hook Performance Budget](./skills/performance/git-hook-performance.md). ## Build and Test Commands @@ -38,7 +40,9 @@ This file is intentionally concise. It contains only critical, high-signal guida - Script tests: `npm run test:scripts` - Validate pre-commit Node tooling policy: `npm run validate:pre-commit-tooling` - Pre-commit Node tooling preflight: `npm run preflight:pre-commit` -- Run parser hook suite exactly as pre-commit executes it: `pre-commit run script-parser-tests --all-files` +- Validate local Node tool dependency health: `npm run validate:node-tooling` +- Run markdown hook parity check: `npm run validate:hook-markdown` +- Run parser hook suite exactly as pre-push executes it: `pre-commit run --hook-stage pre-push script-parser-tests --all-files` - Check package.json format explicitly: `npm run check:package-json-format` - Check hook-managed Prettier targets: `npm run check:prettier:hooks` - Validate YAML formatting and lint policy: `npm run check:yaml` @@ -49,6 +53,7 @@ This file is intentionally concise. It contains only critical, high-signal guida - File-scoped spellcheck: `npx --yes cspell@9 --no-progress --no-summary ` - Script-wide spellcheck preflight: `npm run check:cspell:scripts` - Note: Prettier does not auto-wrap long YAML lines; yamllint enforces the 200-character limit. +- For long `.pre-commit-config.yaml` values (especially `description:` fields), use YAML folded scalars (`>-`) instead of single-line strings. - Auto-fix markdown fragments/lists: `node scripts/fix-md029-md051.js ` - Lint markdown: `npx markdownlint-cli2 ` - Validate skills + context: `node scripts/validate-skills.js` @@ -75,15 +80,16 @@ This file is intentionally concise. It contains only critical, high-signal guida - For pre-commit hooks that operate on staged files, remember pre-commit stashes unstaged changes and runs hooks against the staged snapshot on disk; reproduce failures through commit-equivalent hook runs when validating behavior. - For auto-fix hooks that restage files, guard restaging with `git diff --quiet -- "$@" || git add "$@"` so no-op runs do not touch the git index. - For Jest in hooks or npm scripts, use `node scripts/run-managed-jest.js` instead of bare `jest` invocations. -- For Prettier in hooks or npm scripts, use `node scripts/run-managed-prettier.js` instead of hardcoded `prettier@X.Y.Z` commands. The managed runner resolves versions in this order: package-lock.json, package.json, then static fallback. +- For Prettier in npm scripts (`format:*`, `check:prettier:hooks`) and ad-hoc invocations, use `node scripts/run-managed-prettier.js` instead of hardcoded `prettier@X.Y.Z` commands. The managed runner resolves versions in this order: package-lock.json, package.json, then static fallback. Pre-commit hook entries themselves use the inline `bash -c '[ -f node_modules/prettier/bin/prettier.cjs ] && exec node ...; else exec npx --yes --package=prettier@ prettier ...; fi'` pattern (cspell/markdownlint shape) plus the parity test at `scripts/__tests__/prettier-version-parity.test.js`. - For `npm`/`npx` child-process calls in `scripts/*.js` (`spawnSync`, `execFileSync`, `execSync`), use `spawnPlatformCommandSync()` from `scripts/lib/shell-command.js`. Do not call `spawnSync(toShellCommand(...))` directly; the helper applies Windows shell-shim execution rules consistently. - For validators that depend on `git` metadata (for example ignore-policy checks), treat `ENOENT`/missing-git failures as hard errors; never silently default to permissive behavior. - When editing `scripts/validate-npm-meta.js`, `scripts/__tests__/validate-npm-meta.test.js`, or npm package metadata, run `npm run validate:npm-meta` before finishing. - When editing `scripts/fix-csharp-underscore-methods.js` or its tests, run `node scripts/run-managed-jest.js --runTestsByPath scripts/__tests__/fix-csharp-underscore-methods.test.js` and then `npm run preflight:pre-commit` before finishing. -- For parser-script failures, verify both isolated and hook-parity execution before concluding root cause: run the focused Jest path first, then run `pre-commit run script-parser-tests --all-files` from the same shell used for commit operations. +- For parser-script failures, verify both isolated and hook-parity execution before concluding root cause: run the focused Jest path first, then run `pre-commit run --hook-stage pre-push script-parser-tests --all-files` from the same shell used for commit operations. - When editing `.pre-commit-config.yaml` or `scripts/validate-pre-commit-tooling.js`, run `node scripts/run-managed-jest.js --runTestsByPath scripts/__tests__/pre-commit-hook-stage-policy.test.js scripts/__tests__/validate-pre-commit-tooling.test.js` before `npm run preflight:pre-commit`. - On Windows, verify `npm --version` in the active shell before running hook-related checks (especially when using nvm/fnm). - On Windows hosts, run `npm run preflight:pre-commit` in the same shell you use for `git commit` so hook PATH/init, npm version drift, package.json formatting, and yamllint issues are caught before commit. +- If a Node-backed hook reports missing packages under `node_modules`, run `npm run validate:node-tooling` before retrying the hook; it imports the same local tool graph and reports incomplete installs directly. - For destructive test harness scripts (for example deleting files under `node_modules`), require explicit CLI opt-in flags and validate target paths defensively before mutation. - In workflows where `package-lock.json` is gitignored, dependency install blocks must be lockfile-aware (`npm ci` when lockfile exists, `npm i --no-audit --no-fund` fallback when absent); bare install-only blocks should be treated as policy violations. - For command alternation regexes, avoid optional-suffix shorthands that split words into partial tokens; prefer explicit alternation forms like `(?:install|i)` to keep patterns readable and spellcheck-safe. @@ -101,6 +107,10 @@ This file is intentionally concise. It contains only critical, high-signal guida - Treat failing tests as real defects until proven otherwise. - Prefer direct testing of production code rather than re-implementation in tests. - Cover normal, negative, and edge-case scenarios for new behavior. +- Tests that exercise dispatch across more than one of `Untargeted`/`Targeted`/`Broadcast` MUST be parameterized via `MessageScenarios.AllKinds`; see [Tests Must Be Parameterized by Message Kind](./skills/testing/tests-must-be-parameterized-by-message-kind.md). +- Bus dispatch-path changes must be covered by the canonical lifecycle edge-case set (scene unload mid-dispatch, DDOL transitions, prefab pooling churn, token disable / re-enable, post-Reset emit, OnApplicationQuit drain, cross-kind reentrancy); see [Lifecycle Edge-Case Test Coverage](./skills/testing/lifecycle-edge-coverage.md). +- Tests that create and tear down message registrations should bracket the work in a `LeakWatcher` to assert no registrations survive; see [LeakWatcher: Detecting Registration Leaks in Tests](./skills/testing/leak-watcher-usage.md). +- Benchmark and performance/allocation tests must stay isolated under `Tests/Runtime/Benchmarks` in asmdef `WallstopStudios.DxMessaging.Tests.00.Runtime.Benchmarks`; `.00` is a lexical prefix convention so the benchmark assembly sorts before peer test assemblies in Unity Test Runner. Keep `BenchmarkAssemblyContractTests` green when adding or moving perf tests. ## Documentation Expectations @@ -113,8 +123,10 @@ This file is intentionally concise. It contains only critical, high-signal guida - For edited Markdown files, run `node scripts/fix-md029-md051.js` and then `npx markdownlint-cli2` before finishing. - Ordered lists must follow MD029 `one` style (`1.` for each item). - Internal fragment links must match GitHub/markdownlint heading slugs exactly (MD051). -- Documentation and `///` XML doc comments must be pure ASCII; see [ASCII-Only Documentation Policy](./skills/documentation/ascii-only-docs.md). Run `node scripts/validate-docs-ascii.js` before finishing. -- Every C# code sample in docs - inline, fenced, and XML `` blocks - must compile; see [Code Samples Must Compile](./skills/documentation/code-samples-must-compile.md). Run `node scripts/validate-doc-code-patterns.js` and the `DocsSnippetCompilationTests` suite before finishing. +- Documentation and `///` XML doc comments must be pure ASCII; see [ASCII-Only Documentation Policy](./skills/documentation/ascii-only-docs.md). Run `node scripts/validate-docs-ascii.js` (or, for the hook-equivalent batch run, `node scripts/run-staged-md-pipeline.js ` for `.md` and `node scripts/run-staged-validators.js ` for `.cs`) before finishing. +- Every C# code sample in docs - inline, fenced, and XML `` blocks - must compile; see [Code Samples Must Compile](./skills/documentation/code-samples-must-compile.md). Run `node scripts/validate-doc-code-patterns.js` (or, for the hook-equivalent batch run, `node scripts/run-staged-md-pipeline.js ` for `.md` and `node scripts/run-staged-validators.js ` for `.cs`) and the `DocsSnippetCompilationTests` suite before finishing. +- Documentation prose must avoid LLM-style filler, marketing adjectives, hedge transitions, and vague quantifiers; see [Human-Prose Documentation Policy](./skills/documentation/human-prose-policy.md). Run `node scripts/validate-docs-prose.js` (or, for the hook-equivalent batch run, `node scripts/run-staged-md-pipeline.js ` for `.md` and `node scripts/run-staged-validators.js ` for `.cs`) before finishing. +- Subclasses of `MessageAwareComponent` MUST call `base.()` from every guarded lifecycle override (`Awake`, `OnEnable`, `OnDisable`, `OnDestroy`, `RegisterMessageHandlers`); see [MessageAwareComponent Base-Call Contract](./skills/unity/base-call-contract.md). Five enforcement layers (Roslyn analyzer DXMSG006-010, IL scanner, Inspector overlay, runtime self-check, meta-test) keep the contract honest. ## Skills to Prefer @@ -137,5 +149,10 @@ Use the index above and then select the most relevant skill pages. Frequently us - [Documentation Updates and Maintenance](./skills/documentation/documentation-updates.md) - [ASCII-Only Documentation Policy](./skills/documentation/ascii-only-docs.md) - [Code Samples Must Compile](./skills/documentation/code-samples-must-compile.md) +- [Human-Prose Documentation Policy](./skills/documentation/human-prose-policy.md) - [Cross-Platform Script Compatibility](./skills/scripting/cross-platform-compatibility.md) - [Test Failure Investigation and Zero-Flaky Policy](./skills/testing/test-failure-investigation.md) +- [Lifecycle Edge-Case Test Coverage](./skills/testing/lifecycle-edge-coverage.md) +- [LeakWatcher: Detecting Registration Leaks in Tests](./skills/testing/leak-watcher-usage.md) +- [MessageAwareComponent Base-Call Contract](./skills/unity/base-call-contract.md) +- [Git Hook Performance Budget](./skills/performance/git-hook-performance.md) diff --git a/.llm/skills/documentation/ascii-only-docs.md b/.llm/skills/documentation/ascii-only-docs.md index 9db7e08c..8831ec2f 100644 --- a/.llm/skills/documentation/ascii-only-docs.md +++ b/.llm/skills/documentation/ascii-only-docs.md @@ -149,7 +149,7 @@ Three layers, all wired up: 1. **`scripts/validate-docs-ascii.js`** - the runtime check, exits non-zero on any banned character. Reports `file:line:column` with codepoint and char. 1. **`scripts/normalize-docs-ascii.js`** - the auto-fixer, idempotent, applies the substitution table. Run with `--check` for a dry run. -1. **Pre-commit hook** (`validate-docs-ascii` in `.pre-commit-config.yaml`) and **CI workflow** (`.github/workflows/docs-lint.yml`) run the validator on every commit and PR. +1. **Pre-commit hooks** - the validator runs as part of `run-staged-md-pipeline` (for `.md` / `.markdown` files) and `run-staged-validators` (for `.cs` files) in `.pre-commit-config.yaml`. The standalone CLI `node scripts/validate-docs-ascii.js` is preserved for ad-hoc invocations. The same validator runs on every PR via the **CI workflow** at `.github/workflows/docs-lint.yml`. ## How to Fix Violations diff --git a/.llm/skills/documentation/code-samples-must-compile.md b/.llm/skills/documentation/code-samples-must-compile.md index 00bf1a49..955dd314 100644 --- a/.llm/skills/documentation/code-samples-must-compile.md +++ b/.llm/skills/documentation/code-samples-must-compile.md @@ -110,7 +110,7 @@ Three layers, all wired up. The two layers split responsibility cleanly: - `DocumentationSnippetsCompile` - fenced ` ```csharp ` blocks across `docs/`. - `InlineTableSnippetsCompile` - inline backtick code spans inside table rows. Filtered via `IsApiSignatureDocumentation` and a "must contain `(` and end with `)` or `;`" heuristic so single identifiers and bare type names don't get tested. - `XmlDocCodeBlocksCompile` - `...` and `...` blocks across `Runtime/`, `Editor/`, `SourceGenerators/`. -1. **Pre-commit hook** (`validate-doc-code-patterns` in `.pre-commit-config.yaml`) and **CI workflow** (`.github/workflows/docs-lint.yml`). +1. **Pre-commit hooks** - the validator runs as part of `run-staged-md-pipeline` (for `.md` / `.markdown` files) and `run-staged-validators` (for `.cs` files) in `.pre-commit-config.yaml`. The standalone CLI `node scripts/validate-doc-code-patterns.js` is preserved for ad-hoc invocations. The same validator runs on every PR via the **CI workflow** at `.github/workflows/docs-lint.yml`. The harness uses a minimal stub set (`GeneratorTestUtilities.SharedStubs`) rather than the full runtime, so doc snippets that reference real DxMessaging APIs without redeclaring them work. The corresponding diagnostic IDs (`CS0103`, `CS0246`, `CS1061`, etc., for missing identifiers and types) are tolerated via `IgnoredSnippetDiagnosticIds` so the test focuses on real semantic bugs that don't depend on external symbols. The trade-off: stub coverage gaps require ignoring `CS1510`, which means the textual lint is the only mechanism that catches the struct-rvalue-Emit bug class. diff --git a/.llm/skills/documentation/human-prose-policy.md b/.llm/skills/documentation/human-prose-policy.md new file mode 100644 index 00000000..75ddca96 --- /dev/null +++ b/.llm/skills/documentation/human-prose-policy.md @@ -0,0 +1,186 @@ +--- +title: "Human-Prose Documentation Policy" +id: "human-prose-policy" +category: "documentation" +version: "1.0.0" +created: "2026-05-02" +updated: "2026-05-02" + +source: + repository: "wallstop/DxMessaging" + files: + - path: "docs/" + - path: "README.md" + - path: "Runtime/" + url: "https://github.com/wallstop/DxMessaging" + +tags: + - "documentation" + - "prose" + - "linting" + - "policy" + - "tooling" + +complexity: + level: "basic" + reasoning: "Mechanical phrase enforcement with a small allow-marker system" + +impact: + performance: + rating: "none" + details: "Documentation only" + maintainability: + rating: "high" + details: "Removes LLM drift from docs and keeps voice consistent across contributors" + testability: + rating: "low" + details: "Validator and Vale rule packs cover the policy automatically" + +prerequisites: + - "Awareness of the project's documentation linting toolchain" + +dependencies: + packages: [] + skills: + - "ascii-only-docs" + - "documentation-style-guide" + +applies_to: + languages: + - "Markdown" + - "C#" + frameworks: + - "MkDocs" + - "GitHub" + +aliases: + - "Prose policy" + - "Anti-LLM-prose policy" + - "Human voice policy" + +related: + - "ascii-only-docs" + - "documentation-style-guide" + - "code-samples-must-compile" + +status: "stable" +--- + +# Human-Prose Documentation Policy + +> **One-line summary**: All documentation prose - in `.md` files and `///` XML doc comments - must avoid marketing adjectives, LLM filler idioms, hedge transitions, vague quantifiers, and soft conversational fluff. + +## Overview + +DxMessaging documentation is written for humans reading reference material. Prose that reads like a marketing landing page or a generic LLM completion costs the reader trust and the project tokens. This policy bans a specific set of LLM-signature phrasings and is enforced mechanically by `scripts/validate-docs-prose.js` (the source of truth) plus Vale rule packs under `.vale/styles/DxMessaging/` for structural prose checks. + +## Rationale + +Marketing adjectives without a measurement (`blazing fast`, `world-class`) signal that the writer did not have a number. Filler phrases like `it goes without saying` consume context and produce no signal. Banning a small set of phrases keeps voice convergent without per-PR debates. + +## Banned Categories + +Marketing adjectives (case-insensitive, whole-word): + +`cutting-edge`, `cutting edge`, `blazing fast`, `seamless`, `seamlessly`, `seamlessness`, `powerful`, `powerfully`, `robust`, `robustly`, `elegant`, `elegantly`, `world-class`, `next-generation`, `industry-leading`, `state-of-the-art`, `comprehensive`, `comprehensively`, `unparalleled`, `revolutionary`, `game-changing`, `best-in-class`, `production-ready`, `enterprise-grade`, `lightning-fast`, `frictionless`, `battle-tested`, `bulletproof`, `rock-solid`. + +LLM filler idioms (case-insensitive, phrase match): + +`delve into`, `delving into`, `delved into`, `delves into`, `harness the power`, `navigate the complexities`, `unlock the potential`, `tapestry`, `realm of`, `dive deep into`, `dive into`, `at the heart of`, `lies the`, `treasure trove`, `it goes without saying`, `needless to say`. + +Hedge transitions (only at the start of a sentence or list item; trailing comma optional): + +`Furthermore`, `Moreover`, `In conclusion`, `In essence`, `In summary`, `It's important to note`, `It's worth noting`, `That said`, `Overall`, `Ultimately`. + +Vague quantifiers (case-insensitive, whole-word): + +`a wide variety of`, `a wide array of`, `a plethora of`, `myriad`, `numerous`. + +Soft conversational fluff (regex): + +`gives you (the )?best`, `provides you with`, `helps you to`, `allows you to easily`, `enables you to`. + +The validator's `--list-rules` flag prints the canonical set with full term lists; the JS file is the source of truth. + +## Allowed Exceptions + +- **Skill files about the policy.** Files under `.llm/skills/documentation/` are wholly exempt. +- **`CHANGELOG.md` and `comprehensive`.** Release notes legitimately use the term. The exemption is matched case-insensitively on the basename. +- **Auto-generated files.** `.llm/skills/index.md` and `llms.txt` are exempt because they are regenerated mechanically. +- **YAML frontmatter.** A leading `---\n...\n---\n` block at the top of `.md` files is skipped entirely. Schema strings inside frontmatter (such as `complexity` reasoning fields) never trigger the validator. +- **Inline allow markers.** When a banned term is genuinely the right word for a specific sentence, mark it inline using one of: + + ```markdown + + + + ``` + + Markers must fit on a single line: the opening `` must be on the same line. A multi-line marker emits a `WARN` to stderr but does not fail the run. Marker comments are themselves stripped from the scan, so they never trigger on themselves. + - `prose-allow` matches on the same line. + - `prose-allow-next-line` applies to the next non-blank scanned line. + - `prose-allow-file` applies file-wide. + + Skill files outside `.llm/skills/documentation/` should use `` near the top when a banned term is necessary in the body. Use markers sparingly. The default answer to a flagged term is to rewrite the sentence. + +## Enforcement + +The policy is fully enforced going forward. There is no grandfather list: every violation reported by `scripts/validate-docs-prose.js` is a new defect to fix. + +| Layer | What it covers | When it runs | +| ----------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `scripts/validate-docs-prose.js` | All banned phrases, allow markers, exemptions | Standalone CLI for ad-hoc runs; same module also called in-process by the consolidated runners below | +| `run-staged-md-pipeline` pre-commit hook | Runs the prose validator in-process on staged `.md` files | Local pre-commit (consolidated `.md` pipeline that also handles fixers, prettier, markdownlint, ascii, and code-pattern lint) | +| `run-staged-validators` pre-commit hook | Runs the prose validator in-process on staged `.cs` files | Local pre-commit (consolidated `.cs` validator runner that also handles ascii and code-pattern lint) | +| `.vale.ini` + `.vale/styles/DxMessaging/` | Passive voice, weasel words, additional style rules | Local-only until committed and wired into CI | + +The custom JS validator is the source of truth. The Vale configuration is additive and currently lives only in working trees; once it is committed and wired into a workflow, this row will move to "CI". File an issue if Vale flags something the JS validator missed so the `RULES` array can absorb the rule first. + +An earlier transitional baseline list has been retired; the policy is now fully enforced from a clean slate. + +## How to Fix Violations + +There is no auto-fix. Each banned phrase is a sign that the sentence around it should be rewritten. The CLI tells you the rule and the suggested replacement strategy: + +```bash +node scripts/validate-docs-prose.js +``` + +```text +docs/install.md:42:5 [marketing/marketing] 'cutting-edge' -- Marketing adjective; replace with a concrete claim. modern, current, or describe the specific feature +``` + +To see the per-category counts across the repository: + +```bash +node scripts/validate-docs-prose.js --summary +``` + +To run a single rule (useful when you are sweeping one category): + +```bash +node scripts/validate-docs-prose.js --rule marketing +``` + +To list every configured rule and its term list: + +```bash +node scripts/validate-docs-prose.js --list-rules +``` + +### Before / After + +Marketing - bad: `DxMessaging is a powerful, comprehensive messaging library.` Good: `DxMessaging is a synchronous, allocation-free message bus for Unity.` + +LLM filler - bad: `At the heart of the system lies the MessageBus.` Good: `The MessageBus is the core of the system.` + +Hedge - bad: `It's important to note that registrations are reference-counted.` Good: `Registrations are reference-counted.` + +Soft fluff - bad: `The bus enables you to dispatch messages.` Good: `The bus dispatches messages.` + +## See Also + +- [ASCII-Only Documentation Policy](./ascii-only-docs.md) +- [Documentation Style Guide](./documentation-style-guide.md) +- [Code Samples Must Compile](./code-samples-must-compile.md) +- [Documentation Updates and Maintenance](./documentation-updates.md) diff --git a/.llm/skills/index.md b/.llm/skills/index.md index 3c6b3553..fc61f152 100644 --- a/.llm/skills/index.md +++ b/.llm/skills/index.md @@ -1,6 +1,6 @@ # Skills Index -> **Auto-generated** on 2026-05-01. Do not edit manually. +> **Auto-generated** on 2026-05-03. Do not edit manually. > Run `node scripts/generate-skills-index.js` to regenerate. --- @@ -9,20 +9,21 @@ | Metric | Value | | ------------ | ----- | -| Total Skills | 138 | -| Categories | 7 | +| Total Skills | 144 | +| Categories | 8 | --- ## Table of Contents -- [Documentation](#documentation) (26) +- [Documentation](#documentation) (27) - [GitHub Actions](#github-actions) (5) - [Packaging](#packaging) (2) -- [Performance](#performance) (40) +- [Performance](#performance) (42) - [Scripting](#scripting) (15) - [Solid](#solid) (15) -- [Testing](#testing) (35) +- [Testing](#testing) (37) +- [Unity](#unity) (1) --- @@ -43,6 +44,7 @@ | [Documentation Updates and Maintenance](./documentation/documentation-updates.md) | [ok] 149 | [basic] | [stable] | [risk: none] | documentation, code-comments | | [External URL Fragment Validation](./documentation/external-url-fragment-validation.md) | [ok] 182 | [basic] | [stable] | [risk: none] | documentation, links | | [GitHub Actions Version Consistency](./documentation/github-actions-version-consistency.md) | [ok] 204 | [basic] | [stable] | [risk: none] | github-actions, ci-cd | +| [Human-Prose Documentation Policy](./documentation/human-prose-policy.md) | [ok] 187 | [basic] | [stable] | [risk: none] | documentation, prose | | [Link Quality and External URL Management](./documentation/link-quality-guidelines.md) | [ok] 120 | [basic] | [stable] | [risk: none] | documentation, links | | [Link Quality and External URL Management Part 1](./documentation/link-quality-guidelines-part-1.md) | [ok] 196 | [intermediate] | [stable] | [risk: low] | migration, split | | [Link Quality and External URL Management Part 2](./documentation/link-quality-guidelines-part-2.md) | [draft] 64 | [intermediate] | [stable] | [risk: low] | migration, split | @@ -94,6 +96,8 @@ | [Collection Pooling with RAII Pattern](./performance/collection-pooling.md) | [draft] 119 | [intermediate] | [stable] | [risk: high] | memory, allocation | | [Collection Pooling with RAII Pattern Part 1](./performance/collection-pooling-part-1.md) | [ok] 206 | [intermediate] | [stable] | [risk: low] | migration, split | | [Collection Pooling with RAII Pattern Part 2](./performance/collection-pooling-part-2.md) | [draft] 57 | [intermediate] | [stable] | [risk: low] | migration, split | +| [Git Hook Performance Budget](./performance/git-hook-performance.md) | [warn] 299 | [intermediate] | [stable] | [risk: high] | git-hooks, pre-commit | +| [Git Hook Performance: Stages and Tooling](./performance/git-hook-performance-tooling.md) | [ok] 240 | [intermediate] | [stable] | [risk: high] | git-hooks, pre-commit | | [High-Performance Cache with Eviction Policies](./performance/cache-eviction-policies.md) | [ok] 177 | [advanced] | [stable] | [risk: high] | caching, memory | | [Object Pooling Anti-Patterns](./performance/object-pooling-anti-patterns.md) | [ok] 145 | [intermediate] | [stable] | [risk: high] | memory, allocation | | [Object Pooling for Zero-Allocation Messaging](./performance/object-pooling.md) | [ok] 124 | [intermediate] | [stable] | [risk: high] | memory, allocation | @@ -164,14 +168,15 @@ | Skill | Lines | Complexity | Status | Performance | Tags | | ------------------------------------------------------------------------------------------------------- | ----------- | -------------- | -------- | ---------------- | ---------------------------- | | [Allocation Coverage Required for Dispatch](./testing/allocation-coverage-required-for-dispatch.md) | [ok] 259 | [intermediate] | [stable] | [risk: critical] | testing, allocation | -| [Comprehensive Test Coverage Requirements](./testing/comprehensive-test-coverage.md) | [ok] 142 | [intermediate] | [stable] | [risk: none] | testing, coverage | | [Data-Driven Coverage Patterns](./testing/test-coverage-data-driven.md) | [ok] 173 | [intermediate] | [stable] | [risk: none] | testing, data-driven | | [Data-Driven Test Sources](./testing/data-driven-tests-sources.md) | [ok] 256 | [intermediate] | [stable] | [risk: none] | testing, parameterized | | [Data-Driven Test Usage Patterns](./testing/data-driven-tests-usage.md) | [draft] 108 | [intermediate] | [stable] | [risk: none] | testing, parameterized | | [Data-Driven Tests with TestCaseSource](./testing/data-driven-tests.md) | [ok] 198 | [intermediate] | [stable] | [risk: low] | testing, parameterized | -| [Git and Parser Robustness in CI/CD](./testing/git-workflow-robustness.md) | [ok] 214 | [intermediate] | [stable] | [risk: none] | testing, git | +| [Git and Parser Robustness in CI/CD](./testing/git-workflow-robustness.md) | [ok] 215 | [intermediate] | [stable] | [risk: none] | testing, git | | [Git and Parser Robustness in CI/CD Part 1](./testing/git-workflow-robustness-part-1.md) | [ok] 188 | [intermediate] | [stable] | [risk: low] | migration, split | -| [Inspector Overlay Invariants for MessageAwareComponent](./testing/inspector-overlay-invariants.md) | [ok] 152 | [intermediate] | [stable] | [risk: low] | testing, editor | +| [Inspector Overlay Invariants for MessageAwareComponent](./testing/inspector-overlay-invariants.md) | [ok] 153 | [intermediate] | [stable] | [risk: low] | testing, editor | +| [LeakWatcher: Detecting Registration Leaks in Tests](./testing/leak-watcher-usage.md) | [ok] 260 | [basic] | [stable] | [risk: low] | testing, leaks | +| [Lifecycle Edge-Case Test Coverage](./testing/lifecycle-edge-coverage.md) | [ok] 249 | [intermediate] | [stable] | [risk: none] | testing, lifecycle | | [Script Test Coverage Requirements](./testing/script-test-coverage.md) | [ok] 260 | [intermediate] | [stable] | [risk: none] | testing, scripts | | [Shared Fixtures: Generic Base](./testing/shared-test-fixtures-generic-base.md) | [ok] 186 | [advanced] | [stable] | [risk: high] | testing, fixtures | | [Shared Fixtures: Reference Counting](./testing/shared-test-fixtures-reference-counting.md) | [ok] 253 | [advanced] | [stable] | [risk: high] | testing, fixtures | @@ -184,6 +189,7 @@ | [Test Categories for Selective Execution Part 1](./testing/test-categories-part-1.md) | [draft] 67 | [intermediate] | [stable] | [risk: low] | migration, split | | [Test Category Execution](./testing/test-categories-execution.md) | [ok] 143 | [basic] | [stable] | [risk: none] | testing, organization | | [Test Code Quality and Accuracy](./testing/test-code-quality.md) | [ok] 244 | [intermediate] | [stable] | [risk: medium] | testing, documentation | +| [Test Coverage Requirements](./testing/comprehensive-test-coverage.md) | [ok] 142 | [intermediate] | [stable] | [risk: none] | testing, coverage | | [Test Coverage Scenario Categories](./testing/test-coverage-scenario-categories.md) | [ok] 224 | [intermediate] | [stable] | [risk: none] | testing, coverage | | [Test Diagnostics and Investigation Patterns](./testing/test-diagnostics.md) | [ok] 248 | [intermediate] | [stable] | [risk: low] | testing, diagnostics | | [Test Diagnostics Patterns](./testing/test-diagnostics-patterns.md) | [ok] 190 | [intermediate] | [stable] | [risk: low] | testing, diagnostics | @@ -196,9 +202,15 @@ | [Test Production Code Directly](./testing/test-production-code.md) | [ok] 146 | [intermediate] | [stable] | [risk: none] | testing, anti-patterns | | [Test Production Code Directly Part 1](./testing/test-production-code-part-1.md) | [ok] 205 | [intermediate] | [stable] | [risk: low] | migration, split | | [Test Production Code Directly Part 2](./testing/test-production-code-part-2.md) | [draft] 66 | [intermediate] | [stable] | [risk: low] | migration, split | -| [Tests Must Be Parameterized by Message Kind](./testing/tests-must-be-parameterized-by-message-kind.md) | [ok] 240 | [intermediate] | [stable] | [risk: none] | testing, data-driven | +| [Tests Must Be Parameterized by Message Kind](./testing/tests-must-be-parameterized-by-message-kind.md) | [ok] 242 | [intermediate] | [stable] | [risk: none] | testing, data-driven | | [Unity Test Considerations and Anti-Patterns](./testing/test-coverage-unity-anti-patterns.md) | [warn] 270 | [basic] | [stable] | [risk: none] | testing, unity | +## Unity + +| Skill | Lines | Complexity | Status | Performance | Tags | +| ------------------------------------------------------------------------- | ---------- | -------------- | -------- | ------------ | --------------- | +| [MessageAwareComponent Base-Call Contract](./unity/base-call-contract.md) | [warn] 267 | [intermediate] | [stable] | [risk: none] | unity, analyzer | + --- _Generated by `scripts/generate-skills-index.js`_ diff --git a/.llm/skills/performance/cache-eviction-policies.md b/.llm/skills/performance/cache-eviction-policies.md index 53dbfc14..d6b6f51e 100644 --- a/.llm/skills/performance/cache-eviction-policies.md +++ b/.llm/skills/performance/cache-eviction-policies.md @@ -72,7 +72,7 @@ status: "stable" # High-Performance Cache with Eviction Policies -> **One-line summary**: Build production-ready caches with LRU/LFU/SLRU eviction, TTL expiration, and statistics using a fluent builder. +> **One-line summary**: Build caches with LRU/LFU/SLRU eviction, TTL expiration, and statistics using a fluent builder. ## Overview diff --git a/.llm/skills/performance/git-hook-performance-tooling.md b/.llm/skills/performance/git-hook-performance-tooling.md new file mode 100644 index 00000000..720fac11 --- /dev/null +++ b/.llm/skills/performance/git-hook-performance-tooling.md @@ -0,0 +1,239 @@ +--- +title: "Git Hook Performance: Stages and Tooling" +id: "git-hook-performance-tooling" +category: "performance" +version: "1.1.0" +created: "2026-05-02" +updated: "2026-05-02" + +source: + repository: "wallstop/DxMessaging" + files: + - path: ".pre-commit-config.yaml" + - path: "scripts/run-staged-validators.js" + - path: "scripts/run-staged-md-pipeline.js" + - path: "scripts/measure-hook-wallclock.js" + - path: ".github/workflows/hook-perf-measurement.yml" + url: "https://github.com/wallstop/DxMessaging" + +tags: + - "git-hooks" + - "pre-commit" + - "ci-cd" + - "performance" + - "developer-experience" + - "tooling" + +complexity: + level: "intermediate" + reasoning: "Requires familiarity with pre-commit hook stages and Node startup cost" + +impact: + performance: + rating: "high" + details: "Documents the stage placement and consolidation rules that keep the pipeline under budget" + maintainability: + rating: "high" + details: "Centralizes the operational guidance that the budget skill links to" + testability: + rating: "medium" + details: "Tooling described here is enforced by tests in the budget skill" + +prerequisites: + - "Familiarity with the budget skill (git-hook-performance)" + +dependencies: + packages: [] + skills: + - "git-hook-performance" + +applies_to: + languages: + - "JavaScript" + - "YAML" + frameworks: + - "pre-commit" + versions: + pre-commit: ">=3.0" + +aliases: + - "Hook stages" + - "Hook tooling" + +related: + - "git-hook-performance" + +status: "stable" +--- + +# Git Hook Performance: Stages and Tooling + +> **One-line summary**: Where each hook lives (pre-commit / pre-push / CI), how to consolidate per-file validators, how to measure wall-clock, and the new-hook checklist. + +This page is the operational companion to the budget skill at +[Git Hook Performance Budget](git-hook-performance.md). Read the budget +skill first for the scoring rules and the waiver mechanics; this page +covers the workflow questions that drop out of those rules. + +## What lives where + +The pipeline divides along three axes: cost, scope, and recovery. + +- pre-commit (must be fast and per-file): + - Formatters that mutate the staged file (csharpier, prettier for + JSON/YAML/asmdef/asmref, fix-eol, fix-csharp-underscore-methods, + sync-banner-version). + - The consolidated markdown pipeline at + `scripts/run-staged-md-pipeline.js` (round-4): one Node process + that runs fix-md036-headings, fix-md029-md051, prettier --write, + markdownlint-cli2 --fix, and the three doc validators + (validate-docs-ascii, validate-doc-code-patterns, + validate-docs-prose) in sequence on every staged `.md` / + `.markdown` file. Replaces five separate hooks. + - The consolidated C# validator runner at + `scripts/run-staged-validators.js` (the same three validators, + narrowed to `.cs` for the round-4 pipeline split). + - Cheap structural validators that read only one or two files + (validate-skills, validate-vscode-settings, + validate-pre-commit-tooling, validate-lychee-config, + validate-changelog-policy, eol-bom-check, conflict-markers, + skills-index-regen, update-llms-txt). +- pre-push (cost gate; tests, networked checks, repo-wide scans): + - cspell (spell-check; about 5.5 s per fire is too slow for commit + cadence). + - skills-index-check, validate-npm-meta, script-parser-tests, + script-tests, actionlint, yamllint. + - check-llms-txt-fresh (cheap diff against a freshly generated + `llms.txt`). + - run-staged-validators on `.cs` (provides the validator gate + redundantly at push time so unrelated commits cannot land + documentation drift via the C# XML doc-comment surface). +- CI only (anything over 5 seconds or that reads from the network): + - Full Jest suite (`.github/workflows/script-tests.yml`). + - validate-llms-txt full generator-contract suite + (`.github/workflows/validate-llms-txt.yml`). + - markdownlint sweep across the whole repo + (`.github/workflows/markdownlint.yml`). + - The wall-clock measurement harness + (`.github/workflows/hook-perf-measurement.yml`). + - validate-docs-prose, validate-docs-ascii, and + validate-doc-code-patterns each run as standalone jobs on every + PR via `.github/workflows/docs-lint.yml` (round-4 added the prose + job). + +## Consolidating validators and fixers + +There are two consolidated runners. Adding a new check should target +one of them rather than introducing a fresh hook. + +### `scripts/run-staged-md-pipeline.js` (markdown path) + +For any new check that runs on `.md` / `.markdown` files, wire it into +the markdown pipeline. The pipeline currently chains, in one Node +process: + +1. `fix-md036-headings.processMarkdownContent` (in-process auto-fix). +1. `fix-md029-md051.processMarkdownContent` (in-process auto-fix). +1. `prettier --write` via the `prettier` programmatic API + (`format()` + `resolveConfig()`). +1. `markdownlint-cli2 --fix` via the `main(params)` API the package + exports from its `.mjs` entry (round-4 used dynamic import from + CommonJS). +1. `validate-docs-ascii.scanContent`, + `validate-doc-code-patterns.scanMarkdown`, and + `validate-docs-prose.scanContent`. + +Round-4 superseded the earlier guidance that "fixers must remain +separate hooks." Pre-commit reports "files were modified by this +hook" the same way whether five hooks or one hook performed the +rewrite, so consolidating the fixers does not change the user-visible +UX. The pipeline tracks rewrites via mtime/size so it can report a +stable "auto-fixed N file(s); re-stage to commit" message. + +A new markdown check qualifies for inclusion when: + +- It is per-file (input is a single file's content; no cross-file state). +- It exports a stable `processMarkdownContent(content)` (for fixers) + or `scanContent(filePath, content)` / `scanMarkdown(...)` (for + validators) function. + +### `scripts/run-staged-validators.js` (C# path) + +For any new per-file `.cs` validator, prefer adding it to +`scripts/run-staged-validators.js`. The runner imports each +validator's `scanContent` API and calls them in one Node process; +each extra hook entry costs 200 to 600 ms of Node startup on Windows. + +A new validator qualifies for consolidation when: + +- It is per-file (input is a single file's content; no cross-file state). +- It exports a stable `scanContent(filePath, content)` or + `scanFile(filePath)` function whose return shape includes + `violations[]`. +- Its `files:` regex is a subset of the consolidated runner's filter + (`\.cs$` excluding `Library/`, `Temp/`, `node_modules/`, `obj/`, + `bin/`, and `*/bin/` `*/obj/`). + +When consolidation is not appropriate, document the decision in the +new hook's description field so the next reviewer does not relitigate +it. + +## Wall-clock measurement + +The harness at `scripts/measure-hook-wallclock.js` measures real +wall-clock for a small set of representative scenarios and fails when +any scenario exceeds its per-scenario budget (8 seconds on Linux). + +```bash +node scripts/measure-hook-wallclock.js # human-readable +node scripts/measure-hook-wallclock.js --json # machine-readable +``` + +The harness is not a pre-commit hook (it touches files and is too slow +for that cadence). The `.github/workflows/hook-perf-measurement.yml` +workflow runs it on every PR that touches `.pre-commit-config.yaml` or +any `scripts/` file, and on a weekly cron, and fails the PR if any +scenario regresses past budget. + +## Adding a new hook (checklist) + +1. Default to `stages: [pre-push]` for tests, network calls, and tool + spawns. +1. Reserve `pre-commit` for staged-file formatters, the consolidated + validator runner, and cheap structural validators. +1. Always set both `files:` and (where the script can ignore generated + directories) `exclude:` filters. +1. For external-process hooks (`dotnet`, `java`, language toolchains), + set `require_serial: true` so pre-commit batches all staged files + into a single invocation. +1. Avoid `bash -lc`. Use `bash -c` unless you have a very specific + reason to load login profiles. +1. After editing `.pre-commit-config.yaml`, run: + + ```bash + node scripts/run-managed-jest.js --runTestsByPath \ + scripts/__tests__/hook-perf-budget.test.js \ + scripts/__tests__/precommit-perf-score.test.js \ + scripts/__tests__/pre-commit-hook-stage-policy.test.js + ``` + +1. For changes that look like they could affect wall-clock (a new hook, + a hook moved between stages, a wrapper script added or removed), run + `node scripts/measure-hook-wallclock.js` locally before pushing. + +## See Also + +- [Git Hook Performance Budget](git-hook-performance.md) +- [Cross-Platform Script Compatibility](../scripting/cross-platform-compatibility.md) + +## References + +- [pre-commit hook stages](https://pre-commit.com/#confining-hooks-to-run-at-certain-stages) +- [pre-commit require_serial](https://pre-commit.com/#hooks-require_serial) + +## Changelog + +| Version | Date | Changes | +| ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1.0.0 | 2026-05-02 | Initial split from git-hook-performance to honor the 300-line skill cap. | +| 1.1.0 | 2026-05-02 | Round-4: documented the new run-staged-md-pipeline.js (in-process .md fixer + prettier + markdownlint + validators), revised the "fixers cannot be consolidated" carve-out to reflect that pre-commit's modified-file UX is identical with one hook, and recorded the new wall-clock projections. | diff --git a/.llm/skills/performance/git-hook-performance-tooling.md.meta b/.llm/skills/performance/git-hook-performance-tooling.md.meta new file mode 100644 index 00000000..e0d4dcc6 --- /dev/null +++ b/.llm/skills/performance/git-hook-performance-tooling.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5a751ae07b41d3ae2d3ee36685d978f5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.llm/skills/performance/git-hook-performance.md b/.llm/skills/performance/git-hook-performance.md new file mode 100644 index 00000000..721426bf --- /dev/null +++ b/.llm/skills/performance/git-hook-performance.md @@ -0,0 +1,298 @@ +--- +title: "Git Hook Performance Budget" +id: "git-hook-performance" +category: "performance" +version: "1.3.0" +created: "2026-05-02" +updated: "2026-05-02" + +source: + repository: "wallstop/DxMessaging" + files: + - path: ".pre-commit-config.yaml" + - path: "scripts/lib/precommit-perf-score.js" + - path: "scripts/__tests__/hook-perf-budget.test.js" + - path: "scripts/measure-hook-wallclock.js" + - path: "scripts/run-staged-validators.js" + - path: "scripts/run-staged-md-pipeline.js" + url: "https://github.com/wallstop/DxMessaging" + +tags: + - "git-hooks" + - "pre-commit" + - "ci-cd" + - "performance" + - "developer-experience" + - "tooling" + +complexity: + level: "intermediate" + reasoning: "Requires understanding of pre-commit hook execution model and Windows process spawn cost" + +impact: + performance: + rating: "high" + details: "Keeps pre-commit under 8s on Linux (proxy for under 10s on Windows) on single-file commits; protects developer flow" + maintainability: + rating: "high" + details: "Static scorer plus wall-clock CI measurement catch regressions in PR review" + testability: + rating: "high" + details: "Integration test, unit-style scorer tests, version-parity tests, and a wall-clock harness all enforce the budget" + +prerequisites: + - "Familiarity with pre-commit hook configuration" + - "Understanding of process spawn cost on Windows" + +dependencies: + packages: [] + skills: [] + +applies_to: + languages: + - "JavaScript" + - "YAML" + frameworks: + - "pre-commit" + versions: + pre-commit: ">=3.0" + +aliases: + - "Pre-commit performance" + - "Hook budget" + +related: + - "cross-platform-compatibility" + - "git-hook-performance-tooling" + +status: "stable" +--- + +# Git Hook Performance Budget + +> **One-line summary**: Pre-commit on a single-file commit must finish under 8 seconds on the Linux dev container (proxy for under 10 seconds on Windows); a static scorer plus a wall-clock CI job enforce the budget. + +## Budget + +- pre-commit on a single-file commit: under 8 seconds wall-clock on Linux + (proxies to under 16 seconds on Windows; the goal is under 10 seconds on + Windows for the .md path through aggressive Node-spawn minimization). +- pre-push on a single-file push: under 8 seconds wall-clock on Linux. +- The static scorer enforces TWO ceilings: + - Total budget (10): cumulative anti-pattern score across all + pre-commit-stage hooks. Catches accumulated drift. + - Per-hook ceiling (3): final score on any single hook. Catches + single-rule regressions that would hide under the total budget's + slack (with the real config at score 2, a stray `bash -lc` (5) + lands at total 7 -- under 10 but well over the per-hook ceiling). +- Hooks that legitimately need a high-cost pattern must declare so via a + `# perf-allow[]: ` comment that names + every waived rule. See [How to opt out](#how-to-opt-out). A waived rule + does NOT count toward either ceiling. +- The wall-clock harness at `scripts/measure-hook-wallclock.js` enforces + the per-scenario Linux budget directly; the scorer is the + cross-platform proxy. + +### Why two ceilings + +Every defined rule is either =< 3 (small-cost) or >= 5 (high-cost), so +the per-hook ceiling of 3 mechanically partitions them: any single +high-cost rule trips the per-hook test on its own, and so does any +combination of small-cost rules summing above 3 on one entry. Examples: + +- `bash -lc` (3) + `npx --yes` (2) on one hook -> final 5 -> per-hook + violation (total budget would have allowed it). +- `npm install` (5) on one hook -> final 5 -> per-hook violation. +- `bash -lc` alone -> final 3 -> AT the ceiling, not in violation. +- `npm pack` (5) waived by `# perf-allow[npm-spawn]: ` -> + final 0 -> no violation (post-waiver score is what counts). + +## Anti-patterns + +The scorer at `scripts/lib/precommit-perf-score.js` walks every hook in +`.pre-commit-config.yaml`. For hooks that run at the `pre-commit` stage +(including hooks with no `stages:` declaration, since pre-commit defaults +to `[pre-commit]`), each rule below adds points to the pipeline budget. +Each rule has a stable ID used by the perf-allow waiver format described +in [How to opt out](#how-to-opt-out). + +- `+5` `[scans-the-world]` `pass_filenames: false` with no `files:` filter. + The hook scans the entire repo on every commit. Add a `files:` regex or + pass staged paths through to the script. +- `+3` `[scans-the-world-with-files]` `pass_filenames: false` with a + `files:` filter. The hook still pays the scan cost (the script does not + receive the staged file list as argv). Switch to `pass_filenames: true` + and accept `[files...]` argv when the script can consume them. +- `+5` `[always-run]` `always_run: true`. The hook fires on every commit + regardless of staged input. Replace with a `files:` regex. +- `+5` `[npm-spawn]` Entry contains `npm pack`, `npm install`, `npm exec`, + `npm test`, or `npm run validate:npm-meta`. These spawn heavy npm child + processes and belong at pre-push. +- `+5` `[dotnet-no-batch]` Entry uses `dotnet tool run` without + `require_serial: true`. Without serialization, pre-commit spawns one + tool process per file. With `require_serial`, all staged files batch + into one invocation. +- `+5` `[jest-at-pre-commit]` Entry runs Jest (via `run-managed-jest.js` + or bare `jest`) at the pre-commit stage. Jest startup alone costs five + to fifteen seconds. Move test runs to pre-push. +- `+2` `[npx-cold-start]` Entry uses `npx --yes`. On a cold cache the + package downloads before the hook runs. Most hooks should prefer the + `bash -c '[ -f node_modules// ] && node ...; else npx --yes +@ ...'` shape so cold-cache fallback works without a + managed Node wrapper. +- `+3` `[bash-login-shell]` Entry uses `bash -lc` or `bash --login -c`. + Login shells load `~/.bash_profile`, nvm/fnm init, and similar profile + scripts. That adds 100 to 500 ms per fire for nothing. Use `bash -c`. +- `+3` `[node-double-spawn]` Entry runs + `node scripts/run-managed-.js` where the wrapper exists only to + spawn another Node or npx process. The double-spawn cost is roughly + 600 to 1200 ms on Windows. Inline the version-pinned fallback into a + `bash -c` entry instead, and validate the pinned version against + `package.json` with a parity test (see + `scripts/__tests__/cspell-version-parity.test.js` and + `scripts/__tests__/prettier-version-parity.test.js`). Only the Jest + wrapper is exempt because managed Jest orchestrates a deterministic + local-vs-fallback Jest invocation that cannot be expressed inline, + and Jest only fires at pre-push so the cost is paid once per push. +- `+3` `[npm-run-at-hook]` Entry uses `npm run